gateway_runtime/
metadata.rs1use core::str::FromStr;
23use core::time::Duration;
24use http::Request;
25use tonic::metadata::{MetadataKey, MetadataMap, MetadataValue};
26
27#[derive(Debug, Clone)]
29pub struct MetadataForwardingConfig {
30 pub allowed_prefixes: alloc::vec::Vec<alloc::string::String>,
33 pub allowed_headers: alloc::vec::Vec<alloc::string::String>,
35}
36
37impl Default for MetadataForwardingConfig {
38 fn default() -> Self {
39 Self {
40 allowed_prefixes: crate::alloc::vec![
41 "grpc-metadata-".into(),
42 ],
46 allowed_headers: crate::alloc::vec![
47 "authorization".into(),
48 "x-request-id".into(),
49 "x-b3-traceid".into(),
50 "x-b3-spanid".into(),
51 "x-b3-parentspanid".into(),
52 "x-b3-sampled".into(),
53 "x-b3-flags".into(),
54 "x-ot-span-context".into(),
55 "traceparent".into(),
56 "tracestate".into(),
57 ],
58 }
59 }
60}
61
62pub fn forward_metadata<B>(req: &Request<B>, metadata: &mut MetadataMap) {
76 let default_config = MetadataForwardingConfig::default();
77 let config = req
79 .extensions()
80 .get::<MetadataForwardingConfig>()
81 .unwrap_or(&default_config);
82
83 for (key, value) in req.headers() {
84 let key_str = key.as_str();
85
86 if key_str.eq_ignore_ascii_case("content-type")
88 || key_str.eq_ignore_ascii_case("content-length")
89 || key_str.eq_ignore_ascii_case("host")
90 || key_str.eq_ignore_ascii_case("connection")
91 || key_str.eq_ignore_ascii_case("keep-alive")
92 || key_str.eq_ignore_ascii_case("proxy-authenticate")
93 || key_str.eq_ignore_ascii_case("proxy-authorization")
94 || key_str.eq_ignore_ascii_case("te")
95 || key_str.eq_ignore_ascii_case("trailer")
96 || key_str.eq_ignore_ascii_case("transfer-encoding")
97 || key_str.eq_ignore_ascii_case("upgrade")
98 {
99 continue;
100 }
101
102 let is_allowed = config
104 .allowed_headers
105 .iter()
106 .any(|h| key_str.eq_ignore_ascii_case(h))
107 || config
108 .allowed_prefixes
109 .iter()
110 .any(|p| key_str.to_lowercase().starts_with(&p.to_lowercase()));
111
112 if !is_allowed {
113 continue;
114 }
115
116 let mut final_key_str = key_str.to_string();
129 if !key_str.eq_ignore_ascii_case("authorization")
130 && !key_str.eq_ignore_ascii_case("grpc-timeout")
131 && !key_str.starts_with("x-")
132 && !key_str.starts_with("grpc-")
133 {
134 final_key_str = format!("x-{}", key_str);
135 }
136
137 if final_key_str.ends_with("-bin") {
138 if let Ok(key_parsed) =
139 MetadataKey::<tonic::metadata::Binary>::from_bytes(final_key_str.as_bytes())
140 {
141 let val = MetadataValue::from_bytes(value.as_bytes());
142 metadata.insert_bin(key_parsed, val);
143 }
144 } else {
145 if let Ok(key_parsed) = MetadataKey::<tonic::metadata::Ascii>::from_str(&final_key_str)
146 {
147 if let Ok(val) = MetadataValue::try_from(value.as_bytes()) {
148 metadata.insert(key_parsed, val);
149 }
150 }
151 }
152 }
153
154 if let Some(ext_map) = req.extensions().get::<MetadataMap>() {
158 for item in ext_map.iter() {
159 match item {
160 tonic::metadata::KeyAndValueRef::Ascii(key, val) => {
161 metadata.insert(key.clone(), val.clone());
162 }
163 tonic::metadata::KeyAndValueRef::Binary(key, val) => {
164 metadata.insert_bin(key.clone(), val.clone());
165 }
166 }
167 }
168 }
169}
170
171pub fn grpc_timeout(val: &str) -> Option<Duration> {
187 if val.is_empty() {
188 return None;
189 }
190 let (num, unit) = val.split_at(val.len() - 1);
191 let n: u64 = num.parse().ok()?;
192 match unit {
193 "H" => Some(Duration::from_secs(n * 3600)),
194 "M" => Some(Duration::from_secs(n * 60)),
195 "S" => Some(Duration::from_secs(n)),
196 "m" => Some(Duration::from_millis(n)),
197 "u" => Some(Duration::from_micros(n)),
198 "n" => Some(Duration::from_nanos(n)),
199 _ => None,
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_forward_metadata_allowed() {
209 let req = http::Request::builder()
210 .header("authorization", "token")
211 .header("grpc-metadata-custom", "val")
212 .body(())
213 .unwrap();
214 let mut md = MetadataMap::new();
215 forward_metadata(&req, &mut md);
216
217 assert_eq!(md.get("authorization").unwrap(), "token");
218 assert_eq!(md.get("grpc-metadata-custom").unwrap(), "val");
219 }
220
221 #[test]
222 fn test_forward_metadata_denied() {
223 let req = http::Request::builder()
224 .header("custom-header", "val") .body(())
226 .unwrap();
227 let mut md = MetadataMap::new();
228 forward_metadata(&req, &mut md);
229
230 assert!(md.is_empty());
231 }
232
233 #[test]
234 fn test_forward_metadata_custom_config() {
235 let config = MetadataForwardingConfig {
236 allowed_prefixes: crate::alloc::vec![],
237 allowed_headers: crate::alloc::vec!["x-custom-allowed".to_string()],
238 };
239 let mut req = http::Request::builder()
240 .header("x-custom-allowed", "val")
241 .header("other", "nope")
242 .body(())
243 .unwrap();
244 req.extensions_mut().insert(config);
245
246 let mut md = MetadataMap::new();
247 forward_metadata(&req, &mut md);
248
249 assert_eq!(md.get("x-custom-allowed").unwrap(), "val");
250 assert!(md.get("other").is_none());
251 }
252
253 #[test]
254 fn test_grpc_timeout_parsing() {
255 assert_eq!(grpc_timeout("1H"), Some(Duration::from_secs(3600)));
256 }
257
258 #[test]
259 fn test_forward_metadata_extension_map() {
260 let mut req = http::Request::builder().body(()).unwrap();
261 let mut ext_map = MetadataMap::new();
262 ext_map.insert("x-ctx-id", "123".parse().unwrap());
263 req.extensions_mut().insert(ext_map);
264
265 let mut md = MetadataMap::new();
266 forward_metadata(&req, &mut md);
267
268 assert_eq!(md.get("x-ctx-id").unwrap(), "123");
269 }
270}