structured_proxy/transcode/
metadata.rs1use std::convert::Infallible;
9use std::time::Duration;
10
11use axum::http::header::{Entry, OccupiedEntry};
12use axum::http::{HeaderMap, HeaderName, HeaderValue};
13use tonic::metadata::MetadataMap;
14
15#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
17#[error("header `{header}` has a value gRPC metadata cannot carry")]
18pub struct InvalidForwardedHeader {
19 pub header: HeaderName,
21}
22
23pub fn try_http_headers_to_grpc_metadata(
43 headers: &HeaderMap,
44 forwarded_headers: &[String],
45) -> Result<MetadataMap, InvalidForwardedHeader> {
46 forward(headers, forwarded_headers, |name, value| {
47 if carries(name, value.as_bytes()) {
48 Ok(())
49 } else {
50 Err(InvalidForwardedHeader {
51 header: name.clone(),
52 })
53 }
54 })
55}
56
57#[deprecated(
60 note = "forwards values gRPC metadata cannot carry, which a receiver may drop; \
61 use try_http_headers_to_grpc_metadata"
62)]
63pub fn http_headers_to_grpc_metadata(
64 headers: &HeaderMap,
65 forwarded_headers: &[String],
66) -> MetadataMap {
67 match forward::<Infallible>(headers, forwarded_headers, |_, _| Ok(())) {
68 Ok(metadata) => metadata,
69 Err(never) => match never {},
70 }
71}
72
73fn forward<E>(
75 headers: &HeaderMap,
76 forwarded_headers: &[String],
77 mut check: impl FnMut(&HeaderName, &HeaderValue) -> Result<(), E>,
78) -> Result<MetadataMap, E> {
79 let mut forwarded = HeaderMap::new();
80 for name in forwarded_headers {
81 if is_trace_context(name) {
83 continue;
84 }
85 let values = headers.get_all(name.as_str());
86 if values.iter().next().is_none() {
87 continue;
88 }
89 let name = HeaderName::from_bytes(name.as_bytes())
90 .expect("a name the request carries a header under is a valid header name");
91 if !is_grpc_key(name.as_str()) {
94 continue;
95 }
96 let Entry::Vacant(vacant) = forwarded.entry(name) else {
98 continue;
99 };
100 let key = vacant.key().clone();
101 let binary = key.as_str().ends_with("-bin");
102 let mut vacant = Some(vacant);
103 let mut entry = None;
104 let mut push = |value: HeaderValue| match entry.as_mut() {
105 Some(entry) => {
106 OccupiedEntry::append(entry, value);
107 }
108 None => {
109 let vacant = vacant
110 .take()
111 .expect("only the first value finds the entry vacant");
112 entry = Some(vacant.insert_entry(value));
113 }
114 };
115 for value in values {
116 check(&key, value)?;
117 if binary && value.as_bytes().contains(&b',') {
121 for part in value.as_bytes().split(|&b| b == b',') {
122 push(
123 HeaderValue::from_bytes(part.trim_ascii())
124 .expect("a trimmed part of a header value is a header value"),
125 );
126 }
127 } else {
128 push(value.clone());
129 }
130 }
131 }
132 let mut metadata = MetadataMap::from_headers(forwarded);
133
134 inject_trace_context(&mut metadata, headers);
135
136 Ok(metadata)
137}
138
139fn carries(name: &HeaderName, value: &[u8]) -> bool {
143 if name.as_str().ends_with("-bin") {
144 is_base64_value(value)
145 } else {
146 is_ascii_value(value)
147 }
148}
149
150pub(crate) fn is_grpc_key(name: &str) -> bool {
154 !name.is_empty()
155 && name.bytes().all(|b| {
156 let b = b.to_ascii_lowercase();
157 b.is_ascii_digit() || b.is_ascii_lowercase() || matches!(b, b'_' | b'-' | b'.')
158 })
159}
160
161fn is_trace_context(name: &str) -> bool {
163 name.eq_ignore_ascii_case("traceparent") || name.eq_ignore_ascii_case("tracestate")
164}
165
166fn is_ascii_value(value: &[u8]) -> bool {
169 !value.is_empty() && value.iter().all(|b| (0x20..=0x7e).contains(b))
170}
171
172fn is_base64_value(value: &[u8]) -> bool {
175 value.split(|&b| b == b',').all(|part| {
176 let part = part.trim_ascii();
177 let data = part.strip_suffix(b"==").or_else(|| part.strip_suffix(b"="));
178 let alphabet = |data: &[u8]| {
179 data.iter()
180 .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
181 };
182 let (data, well_formed) = match data {
183 Some(data) => (data, part.len() % 4 == 0),
185 None => (part, part.len() % 4 != 1),
187 };
188 well_formed && alphabet(data) && canonical_tail(data)
189 })
190}
191
192fn canonical_tail(data: &[u8]) -> bool {
195 let unused_bits_mask = match data.len() % 4 {
196 2 => 0b1111,
199 3 => 0b0011,
200 _ => return true,
201 };
202 data.last()
203 .is_some_and(|&digit| base64_digit(digit) & unused_bits_mask == 0)
204}
205
206fn base64_digit(digit: u8) -> u8 {
209 match digit {
210 b'A'..=b'Z' => digit - b'A',
211 b'a'..=b'z' => digit - b'a' + 26,
212 b'0'..=b'9' => digit - b'0' + 52,
213 b'+' => 62,
214 _ => 63,
215 }
216}
217
218fn insert_ascii(metadata: &mut MetadataMap, key: &str, value: &[u8]) {
221 if !is_ascii_value(value) {
222 return;
223 }
224 if let (Ok(k), Ok(v)) = (
225 key.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>(),
226 tonic::metadata::AsciiMetadataValue::try_from(value),
227 ) {
228 metadata.insert(k, v);
229 }
230}
231
232fn append_ascii(metadata: &mut MetadataMap, key: &'static str, value: &[u8]) {
235 if !is_ascii_value(value) {
236 return;
237 }
238 if let Ok(v) = tonic::metadata::AsciiMetadataValue::try_from(value) {
239 metadata.append(key, v);
240 }
241}
242
243fn inject_trace_context(metadata: &mut MetadataMap, headers: &HeaderMap) {
249 if let Some(tp) = headers.get("traceparent").and_then(|v| v.to_str().ok()) {
252 if is_valid_traceparent(tp) {
253 insert_ascii(metadata, "traceparent", tp.as_bytes());
254 for ts in headers.get_all("tracestate") {
257 append_ascii(metadata, "tracestate", ts.as_bytes());
258 }
259 return;
260 }
261 }
262 if let Some(tp) = new_traceparent() {
263 insert_ascii(metadata, "traceparent", tp.as_bytes());
264 }
265}
266
267fn is_valid_traceparent(tp: &str) -> bool {
274 let parts: Vec<&str> = tp.split('-').collect();
275 if parts.len() < 4 {
276 return false;
277 }
278 let (version, trace_id, parent_id, flags) = (parts[0], parts[1], parts[2], parts[3]);
279 if version == "00" && parts.len() != 4 {
280 return false;
281 }
282 let is_hex = |s: &str, len: usize| s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit());
283 is_hex(version, 2)
284 && !version.eq_ignore_ascii_case("ff")
285 && is_hex(trace_id, 32)
286 && is_hex(parent_id, 16)
287 && is_hex(flags, 2)
288 && trace_id.bytes().any(|b| b != b'0')
289 && parent_id.bytes().any(|b| b != b'0')
290}
291
292fn new_traceparent() -> Option<String> {
295 let mut buf = [0u8; 24];
296 getrandom::fill(&mut buf).ok()?;
297 let trace_id = hex(&buf[..16]);
298 let span_id = hex(&buf[16..]);
299 Some(format!("00-{trace_id}-{span_id}-01"))
300}
301
302fn hex(bytes: &[u8]) -> String {
304 use std::fmt::Write;
305 let mut s = String::with_capacity(bytes.len() * 2);
306 for b in bytes {
307 let _ = write!(s, "{b:02x}");
308 }
309 s
310}
311
312pub fn apply_request_deadline<T>(
321 request: &mut tonic::Request<T>,
322 headers: &HeaderMap,
323) -> Option<Duration> {
324 let timeout = headers
325 .get("grpc-timeout")
326 .and_then(|v| v.to_str().ok())
327 .and_then(parse_grpc_timeout)?;
328 request.set_timeout(timeout);
329 Some(timeout)
330}
331
332fn parse_grpc_timeout(value: &str) -> Option<Duration> {
340 let value = value.trim();
341 let (digits, unit) = value.split_at(value.len().checked_sub(1)?);
342 if digits.is_empty() || digits.len() > 8 {
344 return None;
345 }
346 let n: u64 = digits.parse().ok()?;
347 let dur = match unit {
350 "H" => Duration::from_secs(n * 3600),
351 "M" => Duration::from_secs(n * 60),
352 "S" => Duration::from_secs(n),
353 "m" => Duration::from_millis(n),
354 "u" => Duration::from_micros(n),
355 "n" => Duration::from_nanos(n),
356 _ => return None,
357 };
358 if dur.is_zero() {
359 return None;
360 }
361 Some(dur)
362}
363
364#[cfg(test)]
365mod tests;