1use std::{
4 future::Future,
5 net::{IpAddr, SocketAddr},
6 sync::Arc,
7};
8
9use axum::extract::FromRequestParts;
10use http::{HeaderMap, Method, request::Parts};
11use serde::Serialize;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ClientKind {
17 ApiKey,
19 Authenticated,
21 Anonymous,
23}
24
25impl ClientKind {
26 pub const fn as_str(self) -> &'static str {
28 match self {
29 Self::ApiKey => "api_key",
30 Self::Authenticated => "authenticated",
31 Self::Anonymous => "anonymous",
32 }
33 }
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct RequestContext {
69 inner: Arc<RequestContextInner>,
70 method: Method,
71 client_kind: ClientKind,
72}
73
74#[derive(Clone, Debug, Eq, PartialEq)]
75struct RequestContextInner {
76 request_id: String,
77 correlation_id: Option<String>,
78 route: Option<String>,
79 path: String,
80 trace_id: Option<String>,
81 span_id: Option<String>,
82 user_id: Option<String>,
83 tenant_id: Option<String>,
84 session_id: Option<String>,
85}
86
87impl RequestContext {
88 pub fn new(request_id: impl Into<String>, method: Method, path: impl Into<String>) -> Self {
94 Self {
95 inner: Arc::new(RequestContextInner {
96 request_id: request_id.into(),
97 correlation_id: None,
98 route: None,
99 path: path.into(),
100 trace_id: None,
101 span_id: None,
102 user_id: None,
103 tenant_id: None,
104 session_id: None,
105 }),
106 method,
107 client_kind: ClientKind::Anonymous,
108 }
109 }
110
111 pub fn from_parts(parts: &Parts, request_id: impl Into<String>) -> Self {
118 let request_id = request_id.into();
119 let correlation_id = header_to_string(&parts.headers, "x-correlation-id")
120 .or_else(|| (!request_id.is_empty()).then(|| request_id.clone()));
121 let mut context = Self::new(request_id, parts.method.clone(), parts.uri.path());
122 let inner = Arc::make_mut(&mut context.inner);
123 inner.correlation_id = correlation_id;
124 inner.route = parts
125 .extensions
126 .get::<axum::extract::MatchedPath>()
127 .map(|path| path.as_str().to_owned());
128 context.client_kind = infer_client_kind(&parts.headers);
129 if let Some(trace_context) = parts
130 .headers
131 .get("traceparent")
132 .and_then(|value| value.to_str().ok())
133 .and_then(parse_traceparent)
134 {
135 inner.trace_id = Some(trace_context.trace_id.to_owned());
136 inner.span_id = Some(trace_context.parent_id.to_owned());
137 }
138 context
139 }
140
141 pub fn request_id(&self) -> &str {
146 &self.inner.request_id
147 }
148
149 pub(crate) fn into_request_id(self) -> String {
150 match Arc::try_unwrap(self.inner) {
151 Ok(inner) => inner.request_id,
152 Err(inner) => inner.request_id.clone(),
153 }
154 }
155
156 pub fn correlation_id(&self) -> Option<&str> {
161 self.inner.correlation_id.as_deref()
162 }
163
164 pub const fn method(&self) -> &Method {
166 &self.method
167 }
168
169 pub fn route(&self) -> Option<&str> {
175 self.inner.route.as_deref()
176 }
177
178 pub fn path(&self) -> &str {
180 &self.inner.path
181 }
182
183 pub fn trace_id(&self) -> Option<&str> {
187 self.inner.trace_id.as_deref()
188 }
189
190 pub fn span_id(&self) -> Option<&str> {
192 self.inner.span_id.as_deref()
193 }
194
195 pub const fn client_kind(&self) -> ClientKind {
200 self.client_kind
201 }
202
203 pub fn user_id(&self) -> Option<&str> {
205 self.inner.user_id.as_deref()
206 }
207
208 pub fn tenant_id(&self) -> Option<&str> {
210 self.inner.tenant_id.as_deref()
211 }
212
213 pub fn session_id(&self) -> Option<&str> {
215 self.inner.session_id.as_deref()
216 }
217
218 pub fn with_route(mut self, route: impl Into<String>) -> Self {
220 Arc::make_mut(&mut self.inner).route = Some(route.into());
221 self
222 }
223
224 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
226 Arc::make_mut(&mut self.inner).user_id = Some(user_id.into());
227 self
228 }
229
230 pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
232 Arc::make_mut(&mut self.inner).tenant_id = Some(tenant_id.into());
233 self
234 }
235
236 pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
238 Arc::make_mut(&mut self.inner).session_id = Some(session_id.into());
239 self
240 }
241}
242
243impl<S> FromRequestParts<S> for RequestContext
244where
245 S: Send + Sync,
246{
247 type Rejection = axum::http::StatusCode;
248
249 fn from_request_parts(
250 parts: &mut Parts,
251 _state: &S,
252 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
253 let context = parts.extensions.get::<Self>().cloned();
254 async move { context.ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR) }
255 }
256}
257
258#[derive(Clone, Debug, Eq, Hash, PartialEq)]
260pub struct RequestIdentity(String);
261
262impl RequestIdentity {
263 pub fn new(value: impl Into<String>) -> Self {
265 Self(value.into())
266 }
267
268 pub fn as_str(&self) -> &str {
270 &self.0
271 }
272}
273
274pub trait IdentityExtractor: Clone + Send + Sync + 'static {
276 fn extract(&self, parts: &Parts) -> Option<RequestIdentity>;
278}
279
280impl<F> IdentityExtractor for F
281where
282 F: Fn(&Parts) -> Option<RequestIdentity> + Clone + Send + Sync + 'static,
283{
284 fn extract(&self, parts: &Parts) -> Option<RequestIdentity> {
285 self(parts)
286 }
287}
288
289pub fn context_identity() -> impl IdentityExtractor {
291 |parts: &Parts| {
292 if let Some(context) = parts.extensions.get::<RequestContext>()
293 && let Some(value) = context.user_id().or_else(|| context.tenant_id())
294 {
295 return Some(RequestIdentity::new(value.to_owned()));
296 }
297 header_to_string(&parts.headers, "x-api-key").map(RequestIdentity::new)
298 }
299}
300
301pub fn api_key_identity() -> impl IdentityExtractor {
303 |parts: &Parts| header_to_string(&parts.headers, "x-api-key").map(RequestIdentity::new)
304}
305
306pub fn client_ip_identity() -> impl IdentityExtractor {
313 |parts: &Parts| {
314 peer_ip(parts)
315 .map(|ip| RequestIdentity::new(ip.to_string()))
316 .or_else(|| Some(RequestIdentity::new("anonymous")))
317 }
318}
319
320pub fn trusted_proxy_client_ip_identity(
328 trusted_proxies: impl IntoIterator<Item = IpAddr>,
329) -> impl IdentityExtractor {
330 let trusted_proxies = trusted_proxies.into_iter().collect::<Vec<_>>();
331 move |parts: &Parts| {
332 peer_ip(parts)
333 .map(|peer| {
334 if trusted_proxies.contains(&peer)
335 && let Some(forwarded_ip) = forwarded_for_ip(&parts.headers)
336 {
337 RequestIdentity::new(forwarded_ip.to_string())
338 } else {
339 RequestIdentity::new(peer.to_string())
340 }
341 })
342 .or_else(|| Some(RequestIdentity::new("anonymous")))
343 }
344}
345
346fn peer_ip(parts: &Parts) -> Option<IpAddr> {
347 parts
348 .extensions
349 .get::<axum::extract::ConnectInfo<SocketAddr>>()
350 .map(|connect| connect.0.ip())
351}
352
353fn forwarded_for_ip(headers: &HeaderMap) -> Option<IpAddr> {
354 header_to_string(headers, "x-forwarded-for").and_then(|value| {
355 value
356 .split(',')
357 .next()
358 .map(str::trim)
359 .filter(|value| !value.is_empty())
360 .and_then(|value| value.parse().ok())
361 })
362}
363
364pub(crate) fn header_to_string(headers: &HeaderMap, name: &'static str) -> Option<String> {
365 header_to_str(headers, name).map(str::to_owned)
366}
367
368pub(crate) fn header_to_str<'a>(headers: &'a HeaderMap, name: &'static str) -> Option<&'a str> {
369 headers
370 .get(name)
371 .and_then(|value| value.to_str().ok())
372 .filter(|value| !value.is_empty())
373}
374
375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
376pub(crate) struct ParsedTraceParent<'a> {
377 pub(crate) trace_id: &'a str,
378 pub(crate) parent_id: &'a str,
379 pub(crate) flags: u8,
380}
381
382pub(crate) fn parse_traceparent(value: &str) -> Option<ParsedTraceParent<'_>> {
383 let bytes = value.as_bytes();
384 if bytes.len() < 55 || bytes[2] != b'-' || bytes[35] != b'-' || bytes[52] != b'-' {
385 return None;
386 }
387
388 let version = &bytes[..2];
389 let trace_id = &bytes[3..35];
390 let parent_id = &bytes[36..52];
391 let flags = &bytes[53..55];
392 if !is_lower_hex(version)
393 || version == b"ff"
394 || !is_valid_id(trace_id)
395 || !is_valid_id(parent_id)
396 || !is_lower_hex(flags)
397 || (version == b"00" && bytes.len() != 55)
398 || (bytes.len() > 55 && bytes[55] != b'-')
399 {
400 return None;
401 }
402
403 Some(ParsedTraceParent {
404 trace_id: &value[3..35],
405 parent_id: &value[36..52],
406 flags: u8::from_str_radix(&value[53..55], 16).ok()?,
407 })
408}
409
410fn is_valid_id(value: &[u8]) -> bool {
411 is_lower_hex(value) && value.iter().any(|byte| *byte != b'0')
412}
413
414fn is_lower_hex(value: &[u8]) -> bool {
415 value
416 .iter()
417 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte))
418}
419
420fn infer_client_kind(headers: &HeaderMap) -> ClientKind {
421 if headers.contains_key("x-api-key") {
422 ClientKind::ApiKey
423 } else if headers.contains_key(http::header::AUTHORIZATION) {
424 ClientKind::Authenticated
425 } else {
426 ClientKind::Anonymous
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use http::Request;
434
435 #[test]
436 fn request_context_can_consume_request_id() {
437 let context = RequestContext::new("req-123", Method::GET, "/users");
438
439 assert_eq!(context.into_request_id(), "req-123");
440 }
441
442 #[test]
443 fn cloned_request_context_uses_copy_on_write_for_enrichment() {
444 let original = RequestContext::new("req-123", Method::GET, "/users");
445 let shared = original.clone();
446 assert!(Arc::ptr_eq(&original.inner, &shared.inner));
447
448 let enriched = shared
449 .with_route("/users/{id}")
450 .with_user_id("user-42")
451 .with_tenant_id("tenant-7")
452 .with_session_id("session-9");
453
454 assert!(!Arc::ptr_eq(&original.inner, &enriched.inner));
455 assert_eq!(original.route(), None);
456 assert_eq!(original.user_id(), None);
457 assert_eq!(enriched.route(), Some("/users/{id}"));
458 assert_eq!(enriched.user_id(), Some("user-42"));
459 assert_eq!(enriched.tenant_id(), Some("tenant-7"));
460 assert_eq!(enriched.session_id(), Some("session-9"));
461 }
462
463 #[test]
464 fn request_context_extracts_valid_trace_and_parent_span_ids() {
465 let (parts, ()) = Request::builder()
466 .uri("/users/42")
467 .header(
468 "traceparent",
469 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
470 )
471 .body(())
472 .unwrap()
473 .into_parts();
474
475 let context = RequestContext::from_parts(&parts, "request-1");
476
477 assert_eq!(context.trace_id(), Some("4bf92f3577b34da6a3ce929d0e0e4736"));
478 assert_eq!(context.span_id(), Some("00f067aa0ba902b7"));
479 }
480
481 #[test]
482 fn request_context_ignores_invalid_traceparent_values() {
483 for traceparent in [
484 "not-a-traceparent",
485 "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
486 "00-00000000000000000000000000000000-00f067aa0ba902b7-01",
487 "00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01",
488 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra",
489 ] {
490 let (parts, ()) = Request::builder()
491 .uri("/")
492 .header("traceparent", traceparent)
493 .body(())
494 .unwrap()
495 .into_parts();
496 let context = RequestContext::from_parts(&parts, "request-1");
497
498 assert_eq!(context.trace_id(), None, "{traceparent}");
499 assert_eq!(context.span_id(), None, "{traceparent}");
500 }
501 }
502
503 #[test]
504 fn client_ip_identity_ignores_forwarded_headers_without_peer_info() {
505 let parts = request_parts(None, Some("203.0.113.10"));
506 let identity = client_ip_identity().extract(&parts).unwrap();
507
508 assert_eq!(identity.as_str(), "anonymous");
509 }
510
511 #[test]
512 fn trusted_proxy_client_ip_identity_uses_forwarded_header_from_trusted_peer() {
513 let parts = request_parts(Some("127.0.0.1:5000"), Some("203.0.113.10, 10.0.0.5"));
514 let trusted_proxy = "127.0.0.1".parse::<IpAddr>().unwrap();
515 let identity = trusted_proxy_client_ip_identity([trusted_proxy])
516 .extract(&parts)
517 .unwrap();
518
519 assert_eq!(identity.as_str(), "203.0.113.10");
520 }
521
522 #[test]
523 fn trusted_proxy_client_ip_identity_ignores_forwarded_header_from_untrusted_peer() {
524 let parts = request_parts(Some("127.0.0.1:5000"), Some("203.0.113.10"));
525 let trusted_proxy = "10.0.0.1".parse::<IpAddr>().unwrap();
526 let identity = trusted_proxy_client_ip_identity([trusted_proxy])
527 .extract(&parts)
528 .unwrap();
529
530 assert_eq!(identity.as_str(), "127.0.0.1");
531 }
532
533 fn request_parts(peer: Option<&str>, forwarded_for: Option<&str>) -> Parts {
534 let mut builder = Request::builder().uri("/");
535 if let Some(forwarded_for) = forwarded_for {
536 builder = builder.header("x-forwarded-for", forwarded_for);
537 }
538 let (mut parts, ()) = builder.body(()).unwrap().into_parts();
539 if let Some(peer) = peer {
540 parts.extensions.insert(axum::extract::ConnectInfo(
541 peer.parse::<SocketAddr>().unwrap(),
542 ));
543 }
544 parts
545 }
546}