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();
121 if !key_str.eq_ignore_ascii_case("authorization")
122 && !key_str.eq_ignore_ascii_case("grpc-timeout")
123 && !key_str.starts_with("x-")
124 && !key_str.starts_with("grpc-")
125 {
126 final_key_str = format!("x-{}", key_str);
127 }
128
129 if final_key_str.ends_with("-bin") {
130 if let Ok(key_parsed) =
131 MetadataKey::<tonic::metadata::Binary>::from_bytes(final_key_str.as_bytes())
132 {
133 let val = MetadataValue::from_bytes(value.as_bytes());
134 metadata.insert_bin(key_parsed, val);
135 }
136 } else if let Ok(key_parsed) =
137 MetadataKey::<tonic::metadata::Ascii>::from_str(&final_key_str)
138 {
139 if let Ok(val) = MetadataValue::try_from(value.as_bytes()) {
140 metadata.insert(key_parsed, val);
141 }
142 }
143 }
144
145 if let Some(ext_map) = req.extensions().get::<MetadataMap>() {
149 for item in ext_map.iter() {
150 match item {
151 tonic::metadata::KeyAndValueRef::Ascii(key, val) => {
152 metadata.insert(key.clone(), val.clone());
153 }
154 tonic::metadata::KeyAndValueRef::Binary(key, val) => {
155 metadata.insert_bin(key.clone(), val.clone());
156 }
157 }
158 }
159 }
160}
161
162pub fn grpc_timeout(val: &str) -> Option<Duration> {
178 if val.is_empty() {
179 return None;
180 }
181 let (num, unit) = val.split_at(val.len() - 1);
182 let n: u64 = num.parse().ok()?;
183 match unit {
184 "H" => Some(Duration::from_secs(n * 3600)),
185 "M" => Some(Duration::from_secs(n * 60)),
186 "S" => Some(Duration::from_secs(n)),
187 "m" => Some(Duration::from_millis(n)),
188 "u" => Some(Duration::from_micros(n)),
189 "n" => Some(Duration::from_nanos(n)),
190 _ => None,
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_forward_metadata_allowed() {
200 let req = http::Request::builder()
201 .header("authorization", "token")
202 .header("grpc-metadata-custom", "val")
203 .body(())
204 .unwrap();
205 let mut md = MetadataMap::new();
206 forward_metadata(&req, &mut md);
207
208 assert_eq!(md.get("authorization").unwrap(), "token");
209 assert_eq!(md.get("grpc-metadata-custom").unwrap(), "val");
210 }
211
212 #[test]
213 fn test_forward_metadata_denied() {
214 let req = http::Request::builder()
215 .header("custom-header", "val") .body(())
217 .unwrap();
218 let mut md = MetadataMap::new();
219 forward_metadata(&req, &mut md);
220
221 assert!(md.is_empty());
222 }
223
224 #[test]
225 fn test_forward_metadata_custom_config() {
226 let config = MetadataForwardingConfig {
227 allowed_prefixes: crate::alloc::vec![],
228 allowed_headers: crate::alloc::vec!["x-custom-allowed".to_string()],
229 };
230 let mut req = http::Request::builder()
231 .header("x-custom-allowed", "val")
232 .header("other", "nope")
233 .body(())
234 .unwrap();
235 req.extensions_mut().insert(config);
236
237 let mut md = MetadataMap::new();
238 forward_metadata(&req, &mut md);
239
240 assert_eq!(md.get("x-custom-allowed").unwrap(), "val");
241 assert!(md.get("other").is_none());
242 }
243
244 #[test]
245 fn test_grpc_timeout_parsing() {
246 assert_eq!(grpc_timeout("1H"), Some(Duration::from_secs(3600)));
247 }
248
249 #[test]
250 fn test_forward_metadata_extension_map() {
251 let mut req = http::Request::builder().body(()).unwrap();
252 let mut ext_map = MetadataMap::new();
253 ext_map.insert("x-ctx-id", "123".parse().unwrap());
254 req.extensions_mut().insert(ext_map);
255
256 let mut md = MetadataMap::new();
257 forward_metadata(&req, &mut md);
258
259 assert_eq!(md.get("x-ctx-id").unwrap(), "123");
260 }
261}