#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::sync::Arc;
use axum::http::{HeaderValue, Request, Response};
use http::header::HeaderName;
use pin_project_lite::pin_project;
use tower::{Layer, Service};
use crate::entropy::{Entropy, OsEntropy};
use crate::security::ProxyResolver;
use uuid::Uuid;
static X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
#[derive(Clone, Debug)]
pub struct RequestId {
uuid: Uuid,
inbound: Option<InboundForm>,
}
#[derive(Clone, Copy, Debug)]
struct InboundForm {
hyphenated: bool,
upper: u32,
}
impl RequestId {
#[must_use]
pub const fn as_uuid(&self) -> Uuid {
self.uuid
}
#[must_use]
pub fn parse_inbound(value: &str) -> Option<Self> {
let hyphenated = match value.len() {
36 => true,
32 => false,
_ => return None,
};
let uuid = Uuid::try_parse(value).ok()?;
let upper = value
.bytes()
.filter(u8::is_ascii_hexdigit)
.enumerate()
.filter(|(_, byte)| byte.is_ascii_uppercase())
.fold(0_u32, |mask, (digit, _)| mask | (1 << digit));
Some(Self {
uuid,
inbound: Some(InboundForm { hyphenated, upper }),
})
}
#[must_use]
pub const fn is_inbound(&self) -> bool {
self.inbound.is_some()
}
const fn minted(uuid: Uuid) -> Self {
Self {
uuid,
inbound: None,
}
}
pub(crate) fn encode<'a>(&self, buf: &'a mut [u8; uuid::fmt::Hyphenated::LENGTH]) -> &'a str {
let Some(form) = self.inbound else {
return self.uuid.as_hyphenated().encode_lower(buf);
};
let len = if form.hyphenated {
self.uuid.as_hyphenated().encode_lower(buf).len()
} else {
self.uuid.as_simple().encode_lower(buf).len()
};
let Some(text) = buf.get_mut(..len) else {
return "";
};
for (digit, byte) in text.iter_mut().filter(|b| **b != b'-').enumerate() {
if form.upper & (1 << digit) != 0 {
byte.make_ascii_uppercase();
}
}
std::str::from_utf8(text).unwrap_or_default()
}
fn header_value(&self) -> Option<HeaderValue> {
let mut buf = [0u8; uuid::fmt::Hyphenated::LENGTH];
HeaderValue::from_str(self.encode(&mut buf)).ok()
}
}
impl fmt::Display for RequestId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buf = [0u8; uuid::fmt::Hyphenated::LENGTH];
f.write_str(self.encode(&mut buf))
}
}
#[derive(Clone, Debug)]
pub struct RequestIdLayer {
entropy: Arc<dyn Entropy>,
trust: Option<Arc<ProxyResolver>>,
}
impl Default for RequestIdLayer {
fn default() -> Self {
Self {
entropy: Arc::new(OsEntropy),
trust: None,
}
}
}
impl RequestIdLayer {
#[must_use]
pub fn with_entropy(entropy: Arc<dyn Entropy>) -> Self {
Self {
entropy,
trust: None,
}
}
#[must_use]
pub fn with_inbound_trust(mut self, resolver: Arc<ProxyResolver>) -> Self {
self.trust = Some(resolver);
self
}
}
impl<S> Layer<S> for RequestIdLayer {
type Service = RequestIdService<S>;
fn layer(&self, inner: S) -> Self::Service {
RequestIdService {
inner,
shared: Arc::new(RequestIdShared {
entropy: self.entropy.clone(),
trust: self.trust.clone(),
}),
}
}
}
#[derive(Clone, Debug)]
pub struct RequestIdService<S> {
inner: S,
shared: Arc<RequestIdShared>,
}
#[derive(Debug)]
struct RequestIdShared {
entropy: Arc<dyn Entropy>,
trust: Option<Arc<ProxyResolver>>,
}
impl<S> RequestIdService<S> {
fn inbound_id<B>(&self, req: &Request<B>) -> Option<RequestId> {
let trust = self.shared.trust.as_ref()?;
let mut values = req.headers().get_all(&X_REQUEST_ID).iter();
let value = values.next()?.to_str().ok()?;
if values.next().is_some() || !trust.is_trusted_peer(req) {
return None;
}
RequestId::parse_inbound(value)
}
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for RequestIdService<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = RequestIdFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
let id = self
.inbound_id(&req)
.unwrap_or_else(|| RequestId::minted(self.shared.entropy.uuid_v4()));
req.extensions_mut().insert(id.clone());
RequestIdFuture {
inner: self.inner.call(req),
request_id: Some(id),
}
}
}
pin_project! {
pub struct RequestIdFuture<F> {
#[pin]
inner: F,
request_id: Option<RequestId>,
}
}
impl<F, ResBody, E> Future for RequestIdFuture<F>
where
F: Future<Output = Result<Response<ResBody>, E>>,
{
type Output = Result<Response<ResBody>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.inner.poll(cx) {
Poll::Ready(Ok(mut response)) => {
if let Some(value) = this.request_id.take().and_then(|id| id.header_value()) {
response.headers_mut().insert(X_REQUEST_ID.clone(), value);
}
Poll::Ready(Ok(response))
}
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Pending => Poll::Pending,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::Router;
use axum::body::Body;
use axum::extract::Extension;
use axum::routing::get;
use tower::ServiceExt;
#[tokio::test]
async fn response_has_request_id_header() {
let app = Router::new()
.route("/", get(|| async { "ok" }))
.layer(RequestIdLayer::default());
let response = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert!(response.headers().contains_key("x-request-id"));
let id_str = response.headers()["x-request-id"].to_str().unwrap();
assert!(Uuid::parse_str(id_str).is_ok());
}
#[tokio::test]
async fn each_request_gets_unique_id() {
let app = Router::new()
.route("/", get(|| async { "ok" }))
.layer(RequestIdLayer::default());
let r1 = app
.clone()
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
let r2 = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
let id1 = r1.headers()["x-request-id"].to_str().unwrap();
let id2 = r2.headers()["x-request-id"].to_str().unwrap();
assert_ne!(id1, id2);
}
#[tokio::test]
async fn request_id_available_in_extensions() {
async fn handler(Extension(id): Extension<RequestId>) -> String {
id.to_string()
}
let app = Router::new()
.route("/", get(handler))
.layer(RequestIdLayer::default());
let response = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(Uuid::parse_str(&body_str).is_ok());
}
#[test]
fn request_id_display() {
let id = RequestId::minted(Uuid::nil());
assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000");
}
#[test]
fn request_id_stays_small() {
assert!(
std::mem::size_of::<RequestId>() <= 24,
"{} bytes",
std::mem::size_of::<RequestId>()
);
}
#[test]
fn inbound_text_round_trips_in_every_case() {
for text in [
"0f8FAD5b-D9cb-469F-a165-70867728950E",
"0F8FAD5BD9CB469FA16570867728950E",
"0f8fad5bd9cb469fa16570867728950e",
"AbCdEf01-2345-6789-aBcD-eF0123456789",
] {
let id = RequestId::parse_inbound(text).unwrap();
assert!(id.is_inbound());
assert_eq!(id.to_string(), text);
assert_eq!(id.header_value().unwrap(), text);
}
let minted = RequestId::minted(Uuid::nil());
assert!(!minted.is_inbound());
assert_eq!(
minted.header_value().unwrap(),
"00000000-0000-0000-0000-000000000000"
);
}
#[test]
fn parse_inbound_accepts_only_hyphenated_and_simple_uuids() {
let hyphenated = "0F8FAD5B-D9CB-469F-A165-70867728950E";
let id = RequestId::parse_inbound(hyphenated).unwrap();
assert_eq!(id.to_string(), hyphenated, "text is kept as sent");
assert_eq!(id.as_uuid(), Uuid::parse_str(hyphenated).unwrap());
let simple = "0f8fad5bd9cb469fa16570867728950e";
assert_eq!(
RequestId::parse_inbound(simple).unwrap().to_string(),
simple
);
for bad in [
"",
"not-a-uuid",
"0f8fad5b-d9cb-469f-a165-70867728950",
"{0f8fad5b-d9cb-469f-a165-70867728950e}",
"urn:uuid:0f8fad5b-d9cb-469f-a165-70867728950e",
"0f8fad5b-d9cb-469f-a165-70867728950g",
] {
assert!(RequestId::parse_inbound(bad).is_none(), "{bad}");
}
}
fn trusting_layer(config: &crate::security::config::TrustedProxiesConfig) -> RequestIdLayer {
RequestIdLayer::default().with_inbound_trust(Arc::new(ProxyResolver::from_config(config)))
}
async fn response_id(layer: RequestIdLayer, peer: Option<&str>, inbound: &str) -> String {
response_id_with(layer, peer, &[("x-request-id", inbound)]).await
}
async fn response_id_with(
layer: RequestIdLayer,
peer: Option<&str>,
headers: &[(&str, &str)],
) -> String {
let app = Router::new()
.route("/", get(|| async { "ok" }))
.layer(layer);
let mut builder = Request::builder().uri("/");
for (name, value) in headers {
builder = builder.header(*name, *value);
}
let mut request = builder.body(Body::empty()).unwrap();
if let Some(peer) = peer {
let addr: std::net::SocketAddr = peer.parse().unwrap();
request
.extensions_mut()
.insert(axum::extract::ConnectInfo(addr));
}
let response = app.oneshot(request).await.unwrap();
response.headers()["x-request-id"]
.to_str()
.unwrap()
.to_owned()
}
#[tokio::test]
async fn inbound_id_needs_a_trusted_peer() {
let inbound = "0f8fad5b-d9cb-469f-a165-70867728950e";
let ranges = crate::security::config::TrustedProxiesConfig {
ranges: vec!["10.0.0.0/8".to_owned()],
trusted_hops: None,
trust_forwarded_headers: true,
};
let id = response_id(RequestIdLayer::default(), None, inbound).await;
assert_ne!(id, inbound);
let off = crate::security::config::TrustedProxiesConfig {
ranges: Vec::new(),
trusted_hops: None,
trust_forwarded_headers: false,
};
let id = response_id(trusting_layer(&off), Some("10.1.2.3:4000"), inbound).await;
assert_ne!(id, inbound);
let id = response_id(trusting_layer(&ranges), Some("203.0.113.9:4000"), inbound).await;
assert_ne!(id, inbound);
let id = response_id(trusting_layer(&ranges), Some("10.1.2.3:4000"), inbound).await;
assert_eq!(id, inbound);
let open = crate::security::config::TrustedProxiesConfig {
ranges: Vec::new(),
trusted_hops: None,
trust_forwarded_headers: true,
};
let id = response_id(trusting_layer(&open), Some("203.0.113.9:4000"), inbound).await;
assert_eq!(id, inbound);
let broken = crate::security::config::TrustedProxiesConfig {
ranges: vec!["not-a-range".to_owned()],
trusted_hops: None,
trust_forwarded_headers: true,
};
let id = response_id(trusting_layer(&broken), Some("10.1.2.3:4000"), inbound).await;
assert_ne!(id, inbound);
}
#[tokio::test]
async fn hop_mode_needs_a_long_enough_forwarded_chain() {
let inbound = "0f8fad5b-d9cb-469f-a165-70867728950e";
let hops = crate::security::config::TrustedProxiesConfig {
ranges: Vec::new(),
trusted_hops: Some(1),
trust_forwarded_headers: true,
};
let peer = Some("203.0.113.9:4000");
let id = response_id(trusting_layer(&hops), peer, inbound).await;
assert_ne!(id, inbound);
let headers = [
("x-request-id", inbound),
("x-forwarded-for", "198.51.100.7, 10.0.0.2"),
];
let id = response_id_with(trusting_layer(&hops), peer, &headers).await;
assert_eq!(id, inbound);
}
#[tokio::test]
async fn repeated_inbound_header_is_ignored() {
let open = crate::security::config::TrustedProxiesConfig {
ranges: Vec::new(),
trusted_hops: None,
trust_forwarded_headers: true,
};
let first = "0f8fad5b-d9cb-469f-a165-70867728950e";
let headers = [
("x-request-id", first),
("x-request-id", "1f8fad5b-d9cb-469f-a165-70867728950e"),
];
let id = response_id_with(trusting_layer(&open), None, &headers).await;
assert_ne!(id, first);
assert!(Uuid::parse_str(&id).is_ok());
}
#[test]
fn inbound_flag_marks_only_proxy_ids() {
assert!(!RequestId::minted(Uuid::nil()).is_inbound());
let inbound = RequestId::parse_inbound("0f8fad5bd9cb469fa16570867728950e").unwrap();
assert!(inbound.is_inbound());
}
}