use std::collections::HashMap;
use std::sync::Arc;
use axum::extract::Request;
use axum::middleware::Next;
use axum::response::Response;
use http::HeaderMap;
#[derive(Debug, Clone, Default)]
pub struct ForwardedHeaders {
values: HashMap<String, String>,
}
impl ForwardedHeaders {
pub fn get(&self, name: &str) -> Option<&str> {
self.values
.get(&name.to_ascii_lowercase())
.map(String::as_str)
}
pub fn authorization(&self) -> Option<&str> {
self.get("authorization")
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn from_header_map(headers: &HeaderMap, allow: &[&str]) -> Self {
let mut values = HashMap::new();
for name in allow {
if let Some(v) = headers.get(*name).and_then(|v| v.to_str().ok()) {
values.insert(name.to_ascii_lowercase(), v.to_string());
}
}
Self { values }
}
pub fn from_request_context<R>(ctx: &rmcp::service::RequestContext<R>) -> Self
where
R: rmcp::service::ServiceRole,
{
if let Some(fwd) = ctx.extensions.get::<ForwardedHeaders>() {
return fwd.clone();
}
if let Some(parts) = ctx.extensions.get::<http::request::Parts>()
&& let Some(fwd) = parts.extensions.get::<ForwardedHeaders>()
{
return fwd.clone();
}
Self::default()
}
}
#[derive(Debug, Clone)]
pub struct ForwardConfig {
pub allow: Arc<Vec<String>>,
}
impl ForwardConfig {
pub fn new<I, S>(names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
allow: Arc::new(names.into_iter().map(Into::into).collect()),
}
}
pub fn authorization_only() -> Self {
Self::new(["Authorization"])
}
}
pub async fn forward_headers_middleware(
axum::extract::State(config): axum::extract::State<ForwardConfig>,
mut request: Request,
next: Next,
) -> Response {
let allow: Vec<&str> = config.allow.iter().map(String::as_str).collect();
let forwarded = ForwardedHeaders::from_header_map(request.headers(), &allow);
request.extensions_mut().insert(forwarded);
next.run(request).await
}
#[cfg(test)]
mod tests {
use super::*;
fn header_map(pairs: &[(&str, &str)]) -> HeaderMap {
let mut m = HeaderMap::new();
for (k, v) in pairs {
m.insert(
http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
http::HeaderValue::from_str(v).unwrap(),
);
}
m
}
#[test]
fn captures_only_allowlisted_headers() {
let headers = header_map(&[
("Authorization", "Bearer abc"),
("X-Other", "nope"),
("X-Upstream-User", "alice"),
]);
let fwd =
ForwardedHeaders::from_header_map(&headers, &["Authorization", "X-Upstream-User"]);
assert_eq!(fwd.authorization(), Some("Bearer abc"));
assert_eq!(fwd.get("x-upstream-user"), Some("alice"));
assert_eq!(
fwd.get("X-Other"),
None,
"non-allowlisted header must be dropped"
);
}
#[test]
fn lookup_is_case_insensitive() {
let headers = header_map(&[("Authorization", "Bearer abc")]);
let fwd = ForwardedHeaders::from_header_map(&headers, &["authorization"]);
assert_eq!(fwd.get("AUTHORIZATION"), Some("Bearer abc"));
assert_eq!(fwd.get("Authorization"), Some("Bearer abc"));
}
#[test]
fn empty_when_no_allowlisted_headers_present() {
let headers = header_map(&[("X-Random", "v")]);
let fwd = ForwardedHeaders::from_header_map(&headers, &["Authorization"]);
assert!(fwd.is_empty());
assert_eq!(fwd.authorization(), None);
}
}