Skip to content

Commit be06daf

Browse files
committed
Add endpoint for verifying codility webhooks
This probably shouldn't live on this particular server, but it's a convenient place to have some code, so...
1 parent 1bc8904 commit be06daf

5 files changed

Lines changed: 52 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ hyper-util = "0.1.14"
3333
indexmap = { version = "2.9.0", features = ["serde"] }
3434
itertools = "0.14.0"
3535
maplit = "1.0.2"
36+
md5 = "0.8.0"
3637
moka = { version = "0.12.10", features = ["future"] }
3738
octocrab = "0.44.1"
3839
octocrab-rate-limiter = "0.1.0"

src/bin/trainee-tracker.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use axum::routing::get;
1+
use axum::routing::{get, post};
22
use dotenv::dotenv;
33
use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer};
44
use tracing::info;
@@ -115,6 +115,10 @@ async fn main() {
115115
"/api/expected-attendance",
116116
get(trainee_tracker::endpoints::expected_attendance),
117117
)
118+
.route(
119+
"/codility/verify-webhook",
120+
post(trainee_tracker::codility::verify_webhook),
121+
)
118122
.layer(session_layer)
119123
.with_state(server_state);
120124

src/codility.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use axum::{Json, body::Body, extract::Request};
2+
use futures::StreamExt;
3+
use http::HeaderMap;
4+
5+
use crate::Error;
6+
7+
// See Callback Authentication section of https://codility.com/api-documentation/#/operations/tests_invite_create
8+
pub async fn verify_webhook(
9+
header_map: HeaderMap,
10+
body: Request<Body>,
11+
) -> Result<Json<bool>, Error> {
12+
let Some(auth_header) = header_map.get("authorization") else {
13+
return Err(Error::UserFacing("Missing authorization header".to_owned()));
14+
};
15+
let Some(token) = auth_header.as_bytes().strip_prefix(b"Bearer ") else {
16+
return Err(Error::UserFacing("Invalid authorization header".to_owned()));
17+
};
18+
let Some(posted_checksum) = header_map.get("checksum") else {
19+
return Err(Error::UserFacing("Missing checksum header".to_owned()));
20+
};
21+
22+
let mut hasher = md5::Context::new();
23+
24+
let mut data_stream = body.into_body().into_data_stream();
25+
while let Some(chunk) = data_stream.next().await {
26+
if let Ok(chunk) = chunk {
27+
hasher.consume(chunk);
28+
} else {
29+
return Err(Error::UserFacing("Failed to read request body".to_owned()));
30+
}
31+
}
32+
hasher.consume(token);
33+
let digest = hasher.finalize();
34+
let formatted_digest = format!("{:x}", digest);
35+
Ok(Json(
36+
formatted_digest.as_bytes() == posted_checksum.as_bytes(),
37+
))
38+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod config;
1414
pub use config::Config;
1515

1616
use crate::google_auth::GoogleScope;
17+
pub mod codility;
1718
pub mod course;
1819
pub mod endpoints;
1920
pub mod frontend;

0 commit comments

Comments
 (0)