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")]
18#[non_exhaustive]
19pub struct InvalidForwardedHeader {
20 pub header: HeaderName,
22}
23
24pub fn try_http_headers_to_grpc_metadata(
49 headers: &HeaderMap,
50 forwarded_headers: &[String],
51) -> Result<MetadataMap, InvalidForwardedHeader> {
52 checked(
53 headers,
54 Forwarded::Listed(forwarded_headers),
55 Source::Client,
56 )
57}
58
59#[derive(Clone, Copy)]
61pub(crate) enum Forwarded<'a> {
62 Listed(&'a [String]),
63 Named(&'a [HeaderName]),
64}
65
66pub(crate) fn forwarded_metadata(
71 headers: &HeaderMap,
72 forwarded: Forwarded<'_>,
73 rewritten: bool,
74) -> Result<MetadataMap, InvalidForwardedHeader> {
75 let source = if rewritten {
76 Source::Rewritten
77 } else {
78 Source::Client
79 };
80 checked(headers, forwarded, source)
81}
82
83#[derive(Clone, Copy, PartialEq, Eq)]
85enum Source {
86 Client,
88 Rewritten,
90}
91
92fn checked(
93 headers: &HeaderMap,
94 forwarded: Forwarded<'_>,
95 source: Source,
96) -> Result<MetadataMap, InvalidForwardedHeader> {
97 forward(headers, forwarded, source, |name, value| {
98 if carries(name, value.as_bytes()) {
99 Ok(())
100 } else {
101 Err(InvalidForwardedHeader {
102 header: name.clone(),
103 })
104 }
105 })
106}
107
108#[deprecated(
111 note = "forwards values gRPC metadata cannot carry, which a receiver may drop; \
112 use try_http_headers_to_grpc_metadata"
113)]
114pub fn http_headers_to_grpc_metadata(
115 headers: &HeaderMap,
116 forwarded_headers: &[String],
117) -> MetadataMap {
118 let forwarded = Forwarded::Listed(forwarded_headers);
119 match forward::<Infallible>(headers, forwarded, Source::Client, |_, _| Ok(())) {
120 Ok(metadata) => metadata,
121 Err(never) => match never {},
122 }
123}
124
125fn forward<E>(
127 headers: &HeaderMap,
128 forwarded: Forwarded<'_>,
129 source: Source,
130 mut check: impl FnMut(&HeaderName, &HeaderValue) -> Result<(), E>,
131) -> Result<MetadataMap, E> {
132 let mut metadata = HeaderMap::new();
133 match forwarded {
134 Forwarded::Listed(names) => {
135 for name in names {
136 if let Ok(name) = HeaderName::from_bytes(name.as_bytes()) {
139 forward_one(&mut metadata, headers, &name, source, &mut check)?;
140 }
141 }
142 }
143 Forwarded::Named(names) => {
144 for name in names {
145 forward_one(&mut metadata, headers, name, source, &mut check)?;
146 }
147 }
148 }
149 let mut metadata = MetadataMap::from_headers(metadata);
150
151 inject_trace_context(&mut metadata, headers);
152
153 Ok(metadata)
154}
155
156fn forward_one<E>(
159 forwarded: &mut HeaderMap,
160 headers: &HeaderMap,
161 name: &HeaderName,
162 source: Source,
163 check: &mut impl FnMut(&HeaderName, &HeaderValue) -> Result<(), E>,
164) -> Result<(), E> {
165 let text = name.as_str();
166 if is_trace_context(text) || (source == Source::Client && crate::client_address::owns(text)) {
169 return Ok(());
170 }
171 let values = headers.get_all(name);
172 if values.iter().next().is_none() {
173 return Ok(());
174 }
175 if !is_grpc_key(text) {
178 return Ok(());
179 }
180 let Entry::Vacant(vacant) = forwarded.entry(name.clone()) else {
182 return Ok(());
183 };
184 let binary = text.ends_with("-bin");
185 let mut vacant = Some(vacant);
186 let mut entry = None;
187 let mut push = |value: HeaderValue| match entry.as_mut() {
188 Some(entry) => {
189 OccupiedEntry::append(entry, value);
190 }
191 None => {
192 let vacant = vacant
193 .take()
194 .expect("only the first value finds the entry vacant");
195 entry = Some(vacant.insert_entry(value));
196 }
197 };
198 for value in values {
199 check(name, value)?;
200 if binary && value.as_bytes().contains(&b',') {
204 for part in value.as_bytes().split(|&b| b == b',') {
205 push(
206 HeaderValue::from_bytes(part.trim_ascii())
207 .expect("a trimmed part of a header value is a header value"),
208 );
209 }
210 } else {
211 push(value.clone());
212 }
213 }
214 Ok(())
215}
216
217fn carries(name: &HeaderName, value: &[u8]) -> bool {
221 if name.as_str().ends_with("-bin") {
222 is_base64_value(value)
223 } else {
224 is_ascii_value(value)
225 }
226}
227
228pub(crate) fn is_grpc_key(name: &str) -> bool {
232 !name.is_empty()
233 && name.bytes().all(|b| {
234 let b = b.to_ascii_lowercase();
235 b.is_ascii_digit() || b.is_ascii_lowercase() || matches!(b, b'_' | b'-' | b'.')
236 })
237}
238
239fn is_trace_context(name: &str) -> bool {
241 name.eq_ignore_ascii_case("traceparent") || name.eq_ignore_ascii_case("tracestate")
242}
243
244fn is_ascii_value(value: &[u8]) -> bool {
247 !value.is_empty() && value.iter().all(|b| (0x20..=0x7e).contains(b))
248}
249
250fn is_base64_value(value: &[u8]) -> bool {
253 value.split(|&b| b == b',').all(|part| {
254 let part = part.trim_ascii();
255 let data = part.strip_suffix(b"==").or_else(|| part.strip_suffix(b"="));
256 let alphabet = |data: &[u8]| {
257 data.iter()
258 .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
259 };
260 let (data, well_formed) = match data {
261 Some(data) => (data, part.len() % 4 == 0),
263 None => (part, part.len() % 4 != 1),
265 };
266 well_formed && alphabet(data) && canonical_tail(data)
267 })
268}
269
270fn canonical_tail(data: &[u8]) -> bool {
273 let unused_bits_mask = match data.len() % 4 {
274 2 => 0b1111,
277 3 => 0b0011,
278 _ => return true,
279 };
280 data.last()
281 .is_some_and(|&digit| base64_digit(digit) & unused_bits_mask == 0)
282}
283
284fn base64_digit(digit: u8) -> u8 {
287 match digit {
288 b'A'..=b'Z' => digit - b'A',
289 b'a'..=b'z' => digit - b'a' + 26,
290 b'0'..=b'9' => digit - b'0' + 52,
291 b'+' => 62,
292 _ => 63,
293 }
294}
295
296fn insert_ascii(metadata: &mut MetadataMap, key: &str, value: &[u8]) {
299 if !is_ascii_value(value) {
300 return;
301 }
302 if let (Ok(k), Ok(v)) = (
303 key.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>(),
304 tonic::metadata::AsciiMetadataValue::try_from(value),
305 ) {
306 metadata.insert(k, v);
307 }
308}
309
310fn append_ascii(metadata: &mut MetadataMap, key: &'static str, value: &[u8]) {
313 if !is_ascii_value(value) {
314 return;
315 }
316 if let Ok(v) = tonic::metadata::AsciiMetadataValue::try_from(value) {
317 metadata.append(key, v);
318 }
319}
320
321fn inject_trace_context(metadata: &mut MetadataMap, headers: &HeaderMap) {
327 static TRACEPARENT: HeaderName = HeaderName::from_static("traceparent");
328 static TRACESTATE: HeaderName = HeaderName::from_static("tracestate");
329 if let Some(tp) = headers.get(&TRACEPARENT).and_then(|v| v.to_str().ok()) {
332 if is_valid_traceparent(tp) {
333 insert_ascii(metadata, "traceparent", tp.as_bytes());
334 for ts in headers.get_all(&TRACESTATE) {
337 append_ascii(metadata, "tracestate", ts.as_bytes());
338 }
339 return;
340 }
341 }
342 if let Some(tp) = new_traceparent() {
343 let value = tonic::metadata::AsciiMetadataValue::try_from(&tp[..])
344 .expect("a traceparent is visible ASCII");
345 metadata.insert(
346 tonic::metadata::MetadataKey::from_static("traceparent"),
347 value,
348 );
349 }
350}
351
352fn is_valid_traceparent(tp: &str) -> bool {
359 let parts: Vec<&str> = tp.split('-').collect();
360 if parts.len() < 4 {
361 return false;
362 }
363 let (version, trace_id, parent_id, flags) = (parts[0], parts[1], parts[2], parts[3]);
364 if version == "00" && parts.len() != 4 {
365 return false;
366 }
367 let is_hex = |s: &str, len: usize| s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit());
368 is_hex(version, 2)
369 && !version.eq_ignore_ascii_case("ff")
370 && is_hex(trace_id, 32)
371 && is_hex(parent_id, 16)
372 && is_hex(flags, 2)
373 && trace_id.bytes().any(|b| b != b'0')
374 && parent_id.bytes().any(|b| b != b'0')
375}
376
377fn new_traceparent() -> Option<[u8; 55]> {
380 let (high, low, span) = (random_id()?, random_id()?, random_id()?);
381 let low = if high == 0 && low == 0 { 1 } else { low };
383 let span = if span == 0 { 1 } else { span };
384 let mut tp = *b"00-00000000000000000000000000000000-0000000000000000-01";
385 write_hex(&mut tp[3..19], high);
386 write_hex(&mut tp[19..35], low);
387 write_hex(&mut tp[36..52], span);
388 Some(tp)
389}
390
391fn write_hex(digits: &mut [u8], value: u64) {
393 const DIGITS: &[u8; 16] = b"0123456789abcdef";
394 for (at, digit) in digits.iter_mut().enumerate() {
395 let shift = 60 - 4 * at;
396 *digit = DIGITS[usize::try_from((value >> shift) & 0xf).expect("a nibble")];
397 }
398}
399
400fn random_id() -> Option<u64> {
406 thread_local! {
407 static STATE: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
408 }
409 STATE.with(|state| {
410 let seed = match state.get() {
411 Some(seed) => seed,
412 None => {
413 let mut bytes = [0u8; 8];
414 getrandom::fill(&mut bytes).ok()?;
415 u64::from_ne_bytes(bytes)
416 }
417 };
418 let next = seed.wrapping_add(0xa076_1d64_78bd_642f);
419 state.set(Some(next));
420 let wide = u128::from(next) * u128::from(next ^ 0xe703_7ed1_a0b4_28db);
421 Some((wide >> 64) as u64 ^ wide as u64)
423 })
424}
425
426pub fn apply_request_deadline<T>(
435 request: &mut tonic::Request<T>,
436 headers: &HeaderMap,
437) -> Option<Duration> {
438 let timeout = headers
439 .get("grpc-timeout")
440 .and_then(|v| v.to_str().ok())
441 .and_then(parse_grpc_timeout)?;
442 request.set_timeout(timeout);
443 Some(timeout)
444}
445
446fn parse_grpc_timeout(value: &str) -> Option<Duration> {
454 let value = value.trim();
455 let (digits, unit) = value.split_at(value.len().checked_sub(1)?);
456 if digits.is_empty() || digits.len() > 8 {
458 return None;
459 }
460 let n: u64 = digits.parse().ok()?;
461 let dur = match unit {
464 "H" => Duration::from_secs(n * 3600),
465 "M" => Duration::from_secs(n * 60),
466 "S" => Duration::from_secs(n),
467 "m" => Duration::from_millis(n),
468 "u" => Duration::from_micros(n),
469 "n" => Duration::from_nanos(n),
470 _ => return None,
471 };
472 if dur.is_zero() {
473 return None;
474 }
475 Some(dur)
476}
477
478#[cfg(test)]
479mod tests;