turul_a2a_aws_lambda/
adapter.rs1use axum::body::Body;
4use http_body_util::BodyExt;
5
6pub const AUTHORIZER_HEADER_PREFIX: &str = "x-authorizer-";
9
10pub fn lambda_to_axum_request(
16 event: lambda_http::Request,
17) -> Result<http::Request<Body>, lambda_http::Error> {
18 let (mut parts, body) = event.into_parts();
19
20 let keys_to_remove: Vec<http::header::HeaderName> = parts
22 .headers
23 .keys()
24 .filter(|k| k.as_str().starts_with(AUTHORIZER_HEADER_PREFIX))
25 .cloned()
26 .collect();
27 for key in keys_to_remove {
28 parts.headers.remove(&key);
29 }
30
31 if let Some(ctx) = parts
36 .extensions
37 .get::<lambda_http::request::RequestContext>()
38 {
39 match ctx {
40 lambda_http::request::RequestContext::ApiGatewayV2(apigw) => {
41 if let Some(ref authorizer) = apigw.authorizer {
42 if let Some(ref jwt) = authorizer.jwt {
44 for (key, value) in &jwt.claims {
45 inject_authorizer_header(&mut parts.headers, key, value);
46 }
47 }
48 }
52 }
53 lambda_http::request::RequestContext::ApiGatewayV1(apigw) => {
54 for (key, value) in &apigw.authorizer.fields {
56 if let serde_json::Value::String(s) = value {
57 inject_authorizer_header(&mut parts.headers, key, s);
58 }
59 }
60 }
61 _ => {} }
63 }
64
65 let body_bytes: bytes::Bytes = match body {
67 lambda_http::Body::Empty => bytes::Bytes::new(),
68 lambda_http::Body::Text(s) => bytes::Bytes::from(s),
69 lambda_http::Body::Binary(b) => bytes::Bytes::from(b),
70 _ => bytes::Bytes::new(),
71 };
72
73 let req = http::Request::from_parts(parts, Body::from(body_bytes));
74 Ok(req)
75}
76
77fn inject_authorizer_header(headers: &mut http::HeaderMap, key: &str, value: &str) {
78 let header_name = format!("{AUTHORIZER_HEADER_PREFIX}{}", key.to_lowercase());
79 if let Ok(name) = http::header::HeaderName::from_bytes(header_name.as_bytes()) {
80 if let Ok(val) = http::header::HeaderValue::from_str(value) {
81 headers.insert(name, val);
82 }
83 }
84}
85
86pub async fn axum_to_lambda_response(
88 resp: http::Response<Body>,
89) -> Result<lambda_http::Response<lambda_http::Body>, lambda_http::Error> {
90 let (parts, body) = resp.into_parts();
91 let body_bytes = body
92 .collect()
93 .await
94 .map_err(|e| lambda_http::Error::from(format!("Body collect error: {e}")))?
95 .to_bytes();
96
97 let lambda_body = if body_bytes.is_empty() {
98 lambda_http::Body::Empty
99 } else {
100 lambda_http::Body::Text(String::from_utf8_lossy(&body_bytes).into_owned())
101 };
102
103 let resp = http::Response::from_parts(parts, lambda_body);
104 Ok(resp)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn strips_client_supplied_authorizer_headers() {
113 let req: lambda_http::Request = http::Request::builder()
114 .method("POST")
115 .uri("/message:send")
116 .header("content-type", "application/json")
117 .header("a2a-version", "1.0")
118 .header("x-authorizer-userid", "forged-admin")
119 .header("x-authorizer-role", "superuser")
120 .header("x-real-header", "keep-this")
121 .body(lambda_http::Body::Text("{}".into()))
122 .unwrap();
123
124 let axum_req = lambda_to_axum_request(req).unwrap();
125
126 assert!(
128 axum_req.headers().get("x-authorizer-userid").is_none(),
129 "Forged x-authorizer-userid must be stripped"
130 );
131 assert!(
132 axum_req.headers().get("x-authorizer-role").is_none(),
133 "Forged x-authorizer-role must be stripped"
134 );
135
136 assert_eq!(
138 axum_req.headers().get("x-real-header").unwrap(),
139 "keep-this"
140 );
141 assert_eq!(
142 axum_req.headers().get("content-type").unwrap(),
143 "application/json"
144 );
145 }
146
147 #[test]
148 fn inject_authorizer_header_works() {
149 let mut headers = http::HeaderMap::new();
150 inject_authorizer_header(&mut headers, "userId", "user-123");
151 assert_eq!(headers.get("x-authorizer-userid").unwrap(), "user-123");
152 }
153}