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(headers, forwarded_headers, Source::Client)
53}
54
55pub(crate) fn rewritten_headers_to_grpc_metadata(
59 headers: &HeaderMap,
60 forwarded_headers: &[String],
61) -> Result<MetadataMap, InvalidForwardedHeader> {
62 checked(headers, forwarded_headers, Source::Rewritten)
63}
64
65#[derive(Clone, Copy, PartialEq, Eq)]
67enum Source {
68 Client,
70 Rewritten,
72}
73
74fn checked(
75 headers: &HeaderMap,
76 forwarded_headers: &[String],
77 source: Source,
78) -> Result<MetadataMap, InvalidForwardedHeader> {
79 forward(headers, forwarded_headers, source, |name, value| {
80 if carries(name, value.as_bytes()) {
81 Ok(())
82 } else {
83 Err(InvalidForwardedHeader {
84 header: name.clone(),
85 })
86 }
87 })
88}
89
90#[deprecated(
93 note = "forwards values gRPC metadata cannot carry, which a receiver may drop; \
94 use try_http_headers_to_grpc_metadata"
95)]
96pub fn http_headers_to_grpc_metadata(
97 headers: &HeaderMap,
98 forwarded_headers: &[String],
99) -> MetadataMap {
100 match forward::<Infallible>(headers, forwarded_headers, Source::Client, |_, _| Ok(())) {
101 Ok(metadata) => metadata,
102 Err(never) => match never {},
103 }
104}
105
106fn forward<E>(
108 headers: &HeaderMap,
109 forwarded_headers: &[String],
110 source: Source,
111 mut check: impl FnMut(&HeaderName, &HeaderValue) -> Result<(), E>,
112) -> Result<MetadataMap, E> {
113 let mut forwarded = HeaderMap::new();
114 for name in forwarded_headers {
115 if is_trace_context(name) || (source == Source::Client && crate::client_address::owns(name))
118 {
119 continue;
120 }
121 let values = headers.get_all(name.as_str());
122 if values.iter().next().is_none() {
123 continue;
124 }
125 let name = HeaderName::from_bytes(name.as_bytes())
126 .expect("a name the request carries a header under is a valid header name");
127 if !is_grpc_key(name.as_str()) {
130 continue;
131 }
132 let Entry::Vacant(vacant) = forwarded.entry(name) else {
134 continue;
135 };
136 let key = vacant.key().clone();
137 let binary = key.as_str().ends_with("-bin");
138 let mut vacant = Some(vacant);
139 let mut entry = None;
140 let mut push = |value: HeaderValue| match entry.as_mut() {
141 Some(entry) => {
142 OccupiedEntry::append(entry, value);
143 }
144 None => {
145 let vacant = vacant
146 .take()
147 .expect("only the first value finds the entry vacant");
148 entry = Some(vacant.insert_entry(value));
149 }
150 };
151 for value in values {
152 check(&key, value)?;
153 if binary && value.as_bytes().contains(&b',') {
157 for part in value.as_bytes().split(|&b| b == b',') {
158 push(
159 HeaderValue::from_bytes(part.trim_ascii())
160 .expect("a trimmed part of a header value is a header value"),
161 );
162 }
163 } else {
164 push(value.clone());
165 }
166 }
167 }
168 let mut metadata = MetadataMap::from_headers(forwarded);
169
170 inject_trace_context(&mut metadata, headers);
171
172 Ok(metadata)
173}
174
175fn carries(name: &HeaderName, value: &[u8]) -> bool {
179 if name.as_str().ends_with("-bin") {
180 is_base64_value(value)
181 } else {
182 is_ascii_value(value)
183 }
184}
185
186pub(crate) fn is_grpc_key(name: &str) -> bool {
190 !name.is_empty()
191 && name.bytes().all(|b| {
192 let b = b.to_ascii_lowercase();
193 b.is_ascii_digit() || b.is_ascii_lowercase() || matches!(b, b'_' | b'-' | b'.')
194 })
195}
196
197fn is_trace_context(name: &str) -> bool {
199 name.eq_ignore_ascii_case("traceparent") || name.eq_ignore_ascii_case("tracestate")
200}
201
202fn is_ascii_value(value: &[u8]) -> bool {
205 !value.is_empty() && value.iter().all(|b| (0x20..=0x7e).contains(b))
206}
207
208fn is_base64_value(value: &[u8]) -> bool {
211 value.split(|&b| b == b',').all(|part| {
212 let part = part.trim_ascii();
213 let data = part.strip_suffix(b"==").or_else(|| part.strip_suffix(b"="));
214 let alphabet = |data: &[u8]| {
215 data.iter()
216 .all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
217 };
218 let (data, well_formed) = match data {
219 Some(data) => (data, part.len() % 4 == 0),
221 None => (part, part.len() % 4 != 1),
223 };
224 well_formed && alphabet(data) && canonical_tail(data)
225 })
226}
227
228fn canonical_tail(data: &[u8]) -> bool {
231 let unused_bits_mask = match data.len() % 4 {
232 2 => 0b1111,
235 3 => 0b0011,
236 _ => return true,
237 };
238 data.last()
239 .is_some_and(|&digit| base64_digit(digit) & unused_bits_mask == 0)
240}
241
242fn base64_digit(digit: u8) -> u8 {
245 match digit {
246 b'A'..=b'Z' => digit - b'A',
247 b'a'..=b'z' => digit - b'a' + 26,
248 b'0'..=b'9' => digit - b'0' + 52,
249 b'+' => 62,
250 _ => 63,
251 }
252}
253
254fn insert_ascii(metadata: &mut MetadataMap, key: &str, value: &[u8]) {
257 if !is_ascii_value(value) {
258 return;
259 }
260 if let (Ok(k), Ok(v)) = (
261 key.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>(),
262 tonic::metadata::AsciiMetadataValue::try_from(value),
263 ) {
264 metadata.insert(k, v);
265 }
266}
267
268fn append_ascii(metadata: &mut MetadataMap, key: &'static str, value: &[u8]) {
271 if !is_ascii_value(value) {
272 return;
273 }
274 if let Ok(v) = tonic::metadata::AsciiMetadataValue::try_from(value) {
275 metadata.append(key, v);
276 }
277}
278
279fn inject_trace_context(metadata: &mut MetadataMap, headers: &HeaderMap) {
285 if let Some(tp) = headers.get("traceparent").and_then(|v| v.to_str().ok()) {
288 if is_valid_traceparent(tp) {
289 insert_ascii(metadata, "traceparent", tp.as_bytes());
290 for ts in headers.get_all("tracestate") {
293 append_ascii(metadata, "tracestate", ts.as_bytes());
294 }
295 return;
296 }
297 }
298 if let Some(tp) = new_traceparent() {
299 insert_ascii(metadata, "traceparent", tp.as_bytes());
300 }
301}
302
303fn is_valid_traceparent(tp: &str) -> bool {
310 let parts: Vec<&str> = tp.split('-').collect();
311 if parts.len() < 4 {
312 return false;
313 }
314 let (version, trace_id, parent_id, flags) = (parts[0], parts[1], parts[2], parts[3]);
315 if version == "00" && parts.len() != 4 {
316 return false;
317 }
318 let is_hex = |s: &str, len: usize| s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit());
319 is_hex(version, 2)
320 && !version.eq_ignore_ascii_case("ff")
321 && is_hex(trace_id, 32)
322 && is_hex(parent_id, 16)
323 && is_hex(flags, 2)
324 && trace_id.bytes().any(|b| b != b'0')
325 && parent_id.bytes().any(|b| b != b'0')
326}
327
328fn new_traceparent() -> Option<String> {
331 let mut buf = [0u8; 24];
332 getrandom::fill(&mut buf).ok()?;
333 let trace_id = hex(&buf[..16]);
334 let span_id = hex(&buf[16..]);
335 Some(format!("00-{trace_id}-{span_id}-01"))
336}
337
338fn hex(bytes: &[u8]) -> String {
340 use std::fmt::Write;
341 let mut s = String::with_capacity(bytes.len() * 2);
342 for b in bytes {
343 let _ = write!(s, "{b:02x}");
344 }
345 s
346}
347
348pub fn apply_request_deadline<T>(
357 request: &mut tonic::Request<T>,
358 headers: &HeaderMap,
359) -> Option<Duration> {
360 let timeout = headers
361 .get("grpc-timeout")
362 .and_then(|v| v.to_str().ok())
363 .and_then(parse_grpc_timeout)?;
364 request.set_timeout(timeout);
365 Some(timeout)
366}
367
368fn parse_grpc_timeout(value: &str) -> Option<Duration> {
376 let value = value.trim();
377 let (digits, unit) = value.split_at(value.len().checked_sub(1)?);
378 if digits.is_empty() || digits.len() > 8 {
380 return None;
381 }
382 let n: u64 = digits.parse().ok()?;
383 let dur = match unit {
386 "H" => Duration::from_secs(n * 3600),
387 "M" => Duration::from_secs(n * 60),
388 "S" => Duration::from_secs(n),
389 "m" => Duration::from_millis(n),
390 "u" => Duration::from_micros(n),
391 "n" => Duration::from_nanos(n),
392 _ => return None,
393 };
394 if dur.is_zero() {
395 return None;
396 }
397 Some(dur)
398}
399
400#[cfg(test)]
401mod tests;