1use std::{
4 future::Future,
5 net::{IpAddr, SocketAddr},
6};
7
8use axum::extract::FromRequestParts;
9use http::{HeaderMap, Method, request::Parts};
10use serde::Serialize;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ClientKind {
16 ApiKey,
18 Authenticated,
20 Anonymous,
22}
23
24impl ClientKind {
25 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::ApiKey => "api_key",
29 Self::Authenticated => "authenticated",
30 Self::Anonymous => "anonymous",
31 }
32 }
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct RequestContext {
64 request_id: String,
65 correlation_id: Option<String>,
66 method: Method,
67 route: Option<String>,
68 path: String,
69 trace_id: Option<String>,
70 span_id: Option<String>,
71 client_kind: ClientKind,
72 user_id: Option<String>,
73 tenant_id: Option<String>,
74 session_id: Option<String>,
75}
76
77impl RequestContext {
78 pub fn new(request_id: impl Into<String>, method: Method, path: impl Into<String>) -> Self {
84 Self {
85 request_id: request_id.into(),
86 correlation_id: None,
87 method,
88 route: None,
89 path: path.into(),
90 trace_id: None,
91 span_id: None,
92 client_kind: ClientKind::Anonymous,
93 user_id: None,
94 tenant_id: None,
95 session_id: None,
96 }
97 }
98
99 pub fn from_parts(parts: &Parts, request_id: impl Into<String>) -> Self {
106 let request_id = request_id.into();
107 let correlation_id = header_to_string(&parts.headers, "x-correlation-id")
108 .or_else(|| (!request_id.is_empty()).then(|| request_id.clone()));
109 let mut context = Self::new(request_id, parts.method.clone(), parts.uri.path());
110 context.correlation_id = correlation_id;
111 context.route = parts
112 .extensions
113 .get::<axum::extract::MatchedPath>()
114 .map(|path| path.as_str().to_owned());
115 context.client_kind = infer_client_kind(&parts.headers);
116 context.trace_id = parts
117 .headers
118 .get("traceparent")
119 .and_then(|value| value.to_str().ok())
120 .filter(|value| !value.is_empty())
121 .and_then(|value| value.split('-').nth(1))
122 .map(str::to_owned);
123 context
124 }
125
126 pub fn request_id(&self) -> &str {
131 &self.request_id
132 }
133
134 pub(crate) fn into_request_id(self) -> String {
135 self.request_id
136 }
137
138 pub fn correlation_id(&self) -> Option<&str> {
143 self.correlation_id.as_deref()
144 }
145
146 pub const fn method(&self) -> &Method {
148 &self.method
149 }
150
151 pub fn route(&self) -> Option<&str> {
157 self.route.as_deref()
158 }
159
160 pub fn path(&self) -> &str {
162 &self.path
163 }
164
165 pub fn trace_id(&self) -> Option<&str> {
171 self.trace_id.as_deref()
172 }
173
174 pub fn span_id(&self) -> Option<&str> {
176 self.span_id.as_deref()
177 }
178
179 pub const fn client_kind(&self) -> ClientKind {
184 self.client_kind
185 }
186
187 pub fn user_id(&self) -> Option<&str> {
189 self.user_id.as_deref()
190 }
191
192 pub fn tenant_id(&self) -> Option<&str> {
194 self.tenant_id.as_deref()
195 }
196
197 pub fn session_id(&self) -> Option<&str> {
199 self.session_id.as_deref()
200 }
201
202 pub fn with_route(mut self, route: impl Into<String>) -> Self {
204 self.route = Some(route.into());
205 self
206 }
207
208 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
210 self.user_id = Some(user_id.into());
211 self
212 }
213
214 pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
216 self.tenant_id = Some(tenant_id.into());
217 self
218 }
219
220 pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
222 self.session_id = Some(session_id.into());
223 self
224 }
225}
226
227impl<S> FromRequestParts<S> for RequestContext
228where
229 S: Send + Sync,
230{
231 type Rejection = axum::http::StatusCode;
232
233 fn from_request_parts(
234 parts: &mut Parts,
235 _state: &S,
236 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
237 let context = parts.extensions.get::<Self>().cloned();
238 async move { context.ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR) }
239 }
240}
241
242#[derive(Clone, Debug, Eq, Hash, PartialEq)]
244pub struct RequestIdentity(String);
245
246impl RequestIdentity {
247 pub fn new(value: impl Into<String>) -> Self {
249 Self(value.into())
250 }
251
252 pub fn as_str(&self) -> &str {
254 &self.0
255 }
256}
257
258pub trait IdentityExtractor: Clone + Send + Sync + 'static {
260 fn extract(&self, parts: &Parts) -> Option<RequestIdentity>;
262}
263
264impl<F> IdentityExtractor for F
265where
266 F: Fn(&Parts) -> Option<RequestIdentity> + Clone + Send + Sync + 'static,
267{
268 fn extract(&self, parts: &Parts) -> Option<RequestIdentity> {
269 self(parts)
270 }
271}
272
273pub fn context_identity() -> impl IdentityExtractor {
275 |parts: &Parts| {
276 if let Some(context) = parts.extensions.get::<RequestContext>()
277 && let Some(value) = context.user_id().or_else(|| context.tenant_id())
278 {
279 return Some(RequestIdentity::new(value.to_owned()));
280 }
281 header_to_string(&parts.headers, "x-api-key").map(RequestIdentity::new)
282 }
283}
284
285pub fn api_key_identity() -> impl IdentityExtractor {
287 |parts: &Parts| header_to_string(&parts.headers, "x-api-key").map(RequestIdentity::new)
288}
289
290pub fn client_ip_identity() -> impl IdentityExtractor {
297 |parts: &Parts| {
298 peer_ip(parts)
299 .map(|ip| RequestIdentity::new(ip.to_string()))
300 .or_else(|| Some(RequestIdentity::new("anonymous")))
301 }
302}
303
304pub fn trusted_proxy_client_ip_identity(
312 trusted_proxies: impl IntoIterator<Item = IpAddr>,
313) -> impl IdentityExtractor {
314 let trusted_proxies = trusted_proxies.into_iter().collect::<Vec<_>>();
315 move |parts: &Parts| {
316 peer_ip(parts)
317 .map(|peer| {
318 if trusted_proxies.contains(&peer)
319 && let Some(forwarded_ip) = forwarded_for_ip(&parts.headers)
320 {
321 RequestIdentity::new(forwarded_ip.to_string())
322 } else {
323 RequestIdentity::new(peer.to_string())
324 }
325 })
326 .or_else(|| Some(RequestIdentity::new("anonymous")))
327 }
328}
329
330fn peer_ip(parts: &Parts) -> Option<IpAddr> {
331 parts
332 .extensions
333 .get::<axum::extract::ConnectInfo<SocketAddr>>()
334 .map(|connect| connect.0.ip())
335}
336
337fn forwarded_for_ip(headers: &HeaderMap) -> Option<IpAddr> {
338 header_to_string(headers, "x-forwarded-for").and_then(|value| {
339 value
340 .split(',')
341 .next()
342 .map(str::trim)
343 .filter(|value| !value.is_empty())
344 .and_then(|value| value.parse().ok())
345 })
346}
347
348pub(crate) fn header_to_string(headers: &HeaderMap, name: &'static str) -> Option<String> {
349 headers
350 .get(name)
351 .and_then(|value| value.to_str().ok())
352 .filter(|value| !value.is_empty())
353 .map(str::to_owned)
354}
355
356fn infer_client_kind(headers: &HeaderMap) -> ClientKind {
357 if headers.contains_key("x-api-key") {
358 ClientKind::ApiKey
359 } else if headers.contains_key(http::header::AUTHORIZATION) {
360 ClientKind::Authenticated
361 } else {
362 ClientKind::Anonymous
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use http::Request;
370
371 #[test]
372 fn request_context_can_consume_request_id() {
373 let context = RequestContext::new("req-123", Method::GET, "/users");
374
375 assert_eq!(context.into_request_id(), "req-123");
376 }
377
378 #[test]
379 fn client_ip_identity_ignores_forwarded_headers_without_peer_info() {
380 let parts = request_parts(None, Some("203.0.113.10"));
381 let identity = client_ip_identity().extract(&parts).unwrap();
382
383 assert_eq!(identity.as_str(), "anonymous");
384 }
385
386 #[test]
387 fn trusted_proxy_client_ip_identity_uses_forwarded_header_from_trusted_peer() {
388 let parts = request_parts(Some("127.0.0.1:5000"), Some("203.0.113.10, 10.0.0.5"));
389 let trusted_proxy = "127.0.0.1".parse::<IpAddr>().unwrap();
390 let identity = trusted_proxy_client_ip_identity([trusted_proxy])
391 .extract(&parts)
392 .unwrap();
393
394 assert_eq!(identity.as_str(), "203.0.113.10");
395 }
396
397 #[test]
398 fn trusted_proxy_client_ip_identity_ignores_forwarded_header_from_untrusted_peer() {
399 let parts = request_parts(Some("127.0.0.1:5000"), Some("203.0.113.10"));
400 let trusted_proxy = "10.0.0.1".parse::<IpAddr>().unwrap();
401 let identity = trusted_proxy_client_ip_identity([trusted_proxy])
402 .extract(&parts)
403 .unwrap();
404
405 assert_eq!(identity.as_str(), "127.0.0.1");
406 }
407
408 fn request_parts(peer: Option<&str>, forwarded_for: Option<&str>) -> Parts {
409 let mut builder = Request::builder().uri("/");
410 if let Some(forwarded_for) = forwarded_for {
411 builder = builder.header("x-forwarded-for", forwarded_for);
412 }
413 let (mut parts, ()) = builder.body(()).unwrap().into_parts();
414 if let Some(peer) = peer {
415 parts.extensions.insert(axum::extract::ConnectInfo(
416 peer.parse::<SocketAddr>().unwrap(),
417 ));
418 }
419 parts
420 }
421}