payload.secret != expected_secret short-circuits on the first differing byte, which leaks timing information about how many leading bytes of a guess are correct. Use subtle::ConstantTimeEq instead.
74 lines
2.1 KiB
Rust
74 lines
2.1 KiB
Rust
use axum::{extract::State, http::StatusCode, response::IntoResponse};
|
|
use serde::Deserialize;
|
|
use subtle::ConstantTimeEq;
|
|
|
|
use crate::AppState;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct WebhookPayload {
|
|
pub secret: String,
|
|
|
|
#[serde(default)]
|
|
pub athlete_id: Option<String>,
|
|
|
|
#[serde(default)]
|
|
pub activity_id: Option<String>,
|
|
}
|
|
|
|
pub async fn receive(
|
|
State(state): State<AppState>,
|
|
axum::Json(payload): axum::Json<WebhookPayload>,
|
|
) -> impl IntoResponse {
|
|
let Some(expected_secret) = state.config.intervals_webhook_secret.as_deref() else {
|
|
tracing::error!("Intervals.icu webhook secret is not configured");
|
|
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"webhook secret not configured",
|
|
)
|
|
.into_response();
|
|
};
|
|
|
|
let secret_matches = payload.secret.as_bytes().ct_eq(expected_secret.as_bytes());
|
|
|
|
if !bool::from(secret_matches) {
|
|
tracing::warn!("received webhook with invalid secret");
|
|
|
|
return (StatusCode::UNAUTHORIZED, "invalid webhook secret").into_response();
|
|
}
|
|
|
|
let Some(activity_id) = payload.activity_id else {
|
|
tracing::warn!("received webhook without activity_id");
|
|
|
|
return (StatusCode::BAD_REQUEST, "missing activity_id").into_response();
|
|
};
|
|
|
|
let athlete_id = payload
|
|
.athlete_id
|
|
.or_else(|| std::env::var("INTERVALS_ATHLETE_ID").ok());
|
|
|
|
let Some(athlete_id) = athlete_id else {
|
|
tracing::error!(
|
|
"webhook has no athlete_id and \
|
|
INTERVALS_ATHLETE_ID is not configured"
|
|
);
|
|
|
|
return (StatusCode::BAD_REQUEST, "missing athlete_id").into_response();
|
|
};
|
|
|
|
let state_clone = state.clone();
|
|
|
|
tokio::spawn(async move {
|
|
if let Err(error) =
|
|
crate::process_activity(state_clone, athlete_id, activity_id.clone()).await
|
|
{
|
|
tracing::error!(
|
|
%error,
|
|
activity_id = %activity_id,
|
|
"failed to process webhook activity"
|
|
);
|
|
}
|
|
});
|
|
|
|
(StatusCode::ACCEPTED, "activity queued").into_response()
|
|
}
|