use std::convert::Infallible;
use std::time::Duration;
use axum::http::header::{Entry, OccupiedEntry};
use axum::http::{HeaderMap, HeaderName, HeaderValue};
use tonic::metadata::MetadataMap;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("header `{header}` has a value gRPC metadata cannot carry")]
#[non_exhaustive]
pub struct InvalidForwardedHeader {
pub header: HeaderName,
}
pub fn try_http_headers_to_grpc_metadata(
headers: &HeaderMap,
forwarded_headers: &[String],
) -> Result<MetadataMap, InvalidForwardedHeader> {
checked(
headers,
Forwarded::Listed(forwarded_headers),
Source::Client,
)
}
#[derive(Clone, Copy)]
pub(crate) enum Forwarded<'a> {
Listed(&'a [String]),
Named(&'a [HeaderName]),
}
pub(crate) fn forwarded_metadata(
headers: &HeaderMap,
forwarded: Forwarded<'_>,
rewritten: bool,
) -> Result<MetadataMap, InvalidForwardedHeader> {
let source = if rewritten {
Source::Rewritten
} else {
Source::Client
};
checked(headers, forwarded, source)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Source {
Client,
Rewritten,
}
fn checked(
headers: &HeaderMap,
forwarded: Forwarded<'_>,
source: Source,
) -> Result<MetadataMap, InvalidForwardedHeader> {
forward(headers, forwarded, source, |name, value| {
if carries(name, value.as_bytes()) {
Ok(())
} else {
Err(InvalidForwardedHeader {
header: name.clone(),
})
}
})
}
#[deprecated(
note = "forwards values gRPC metadata cannot carry, which a receiver may drop; \
use try_http_headers_to_grpc_metadata"
)]
pub fn http_headers_to_grpc_metadata(
headers: &HeaderMap,
forwarded_headers: &[String],
) -> MetadataMap {
let forwarded = Forwarded::Listed(forwarded_headers);
match forward::<Infallible>(headers, forwarded, Source::Client, |_, _| Ok(())) {
Ok(metadata) => metadata,
Err(never) => match never {},
}
}
fn forward<E>(
headers: &HeaderMap,
forwarded: Forwarded<'_>,
source: Source,
mut check: impl FnMut(&HeaderName, &HeaderValue) -> Result<(), E>,
) -> Result<MetadataMap, E> {
let mut metadata = HeaderMap::new();
match forwarded {
Forwarded::Listed(names) => {
for name in names {
if let Ok(name) = HeaderName::from_bytes(name.as_bytes()) {
forward_one(&mut metadata, headers, &name, source, &mut check)?;
}
}
}
Forwarded::Named(names) => {
for name in names {
forward_one(&mut metadata, headers, name, source, &mut check)?;
}
}
}
let mut metadata = MetadataMap::from_headers(metadata);
inject_trace_context(&mut metadata, headers);
Ok(metadata)
}
fn forward_one<E>(
forwarded: &mut HeaderMap,
headers: &HeaderMap,
name: &HeaderName,
source: Source,
check: &mut impl FnMut(&HeaderName, &HeaderValue) -> Result<(), E>,
) -> Result<(), E> {
let text = name.as_str();
if is_trace_context(text) || (source == Source::Client && crate::client_address::owns(text)) {
return Ok(());
}
let values = headers.get_all(name);
if values.iter().next().is_none() {
return Ok(());
}
if !is_grpc_key(text) {
return Ok(());
}
let Entry::Vacant(vacant) = forwarded.entry(name.clone()) else {
return Ok(());
};
let binary = text.ends_with("-bin");
let mut vacant = Some(vacant);
let mut entry = None;
let mut push = |value: HeaderValue| match entry.as_mut() {
Some(entry) => {
OccupiedEntry::append(entry, value);
}
None => {
let vacant = vacant
.take()
.expect("only the first value finds the entry vacant");
entry = Some(vacant.insert_entry(value));
}
};
for value in values {
check(name, value)?;
if binary && value.as_bytes().contains(&b',') {
for part in value.as_bytes().split(|&b| b == b',') {
push(
HeaderValue::from_bytes(part.trim_ascii())
.expect("a trimmed part of a header value is a header value"),
);
}
} else {
push(value.clone());
}
}
Ok(())
}
fn carries(name: &HeaderName, value: &[u8]) -> bool {
if name.as_str().ends_with("-bin") {
is_base64_value(value)
} else {
is_ascii_value(value)
}
}
pub(crate) fn is_grpc_key(name: &str) -> bool {
!name.is_empty()
&& name.bytes().all(|b| {
let b = b.to_ascii_lowercase();
b.is_ascii_digit() || b.is_ascii_lowercase() || matches!(b, b'_' | b'-' | b'.')
})
}
fn is_trace_context(name: &str) -> bool {
name.eq_ignore_ascii_case("traceparent") || name.eq_ignore_ascii_case("tracestate")
}
fn is_ascii_value(value: &[u8]) -> bool {
!value.is_empty() && value.iter().all(|b| (0x20..=0x7e).contains(b))
}
fn is_base64_value(value: &[u8]) -> bool {
value.split(|&b| b == b',').all(|part| {
let part = part.trim_ascii();
let data = part.strip_suffix(b"==").or_else(|| part.strip_suffix(b"="));
let alphabet = |data: &[u8]| {
data.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'/')
};
let (data, well_formed) = match data {
Some(data) => (data, part.len() % 4 == 0),
None => (part, part.len() % 4 != 1),
};
well_formed && alphabet(data) && canonical_tail(data)
})
}
fn canonical_tail(data: &[u8]) -> bool {
let unused_bits_mask = match data.len() % 4 {
2 => 0b1111,
3 => 0b0011,
_ => return true,
};
data.last()
.is_some_and(|&digit| base64_digit(digit) & unused_bits_mask == 0)
}
fn base64_digit(digit: u8) -> u8 {
match digit {
b'A'..=b'Z' => digit - b'A',
b'a'..=b'z' => digit - b'a' + 26,
b'0'..=b'9' => digit - b'0' + 52,
b'+' => 62,
_ => 63,
}
}
fn insert_ascii(metadata: &mut MetadataMap, key: &str, value: &[u8]) {
if !is_ascii_value(value) {
return;
}
if let (Ok(k), Ok(v)) = (
key.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>(),
tonic::metadata::AsciiMetadataValue::try_from(value),
) {
metadata.insert(k, v);
}
}
fn append_ascii(metadata: &mut MetadataMap, key: &'static str, value: &[u8]) {
if !is_ascii_value(value) {
return;
}
if let Ok(v) = tonic::metadata::AsciiMetadataValue::try_from(value) {
metadata.append(key, v);
}
}
fn inject_trace_context(metadata: &mut MetadataMap, headers: &HeaderMap) {
static TRACEPARENT: HeaderName = HeaderName::from_static("traceparent");
static TRACESTATE: HeaderName = HeaderName::from_static("tracestate");
if let Some(tp) = headers.get(&TRACEPARENT).and_then(|v| v.to_str().ok()) {
if is_valid_traceparent(tp) {
insert_ascii(metadata, "traceparent", tp.as_bytes());
for ts in headers.get_all(&TRACESTATE) {
append_ascii(metadata, "tracestate", ts.as_bytes());
}
return;
}
}
if let Some(tp) = new_traceparent() {
let value = tonic::metadata::AsciiMetadataValue::try_from(&tp[..])
.expect("a traceparent is visible ASCII");
metadata.insert(
tonic::metadata::MetadataKey::from_static("traceparent"),
value,
);
}
}
fn is_valid_traceparent(tp: &str) -> bool {
let parts: Vec<&str> = tp.split('-').collect();
if parts.len() < 4 {
return false;
}
let (version, trace_id, parent_id, flags) = (parts[0], parts[1], parts[2], parts[3]);
if version == "00" && parts.len() != 4 {
return false;
}
let is_hex = |s: &str, len: usize| s.len() == len && s.bytes().all(|b| b.is_ascii_hexdigit());
is_hex(version, 2)
&& !version.eq_ignore_ascii_case("ff")
&& is_hex(trace_id, 32)
&& is_hex(parent_id, 16)
&& is_hex(flags, 2)
&& trace_id.bytes().any(|b| b != b'0')
&& parent_id.bytes().any(|b| b != b'0')
}
fn new_traceparent() -> Option<[u8; 55]> {
let (high, low, span) = (random_id()?, random_id()?, random_id()?);
let low = if high == 0 && low == 0 { 1 } else { low };
let span = if span == 0 { 1 } else { span };
let mut tp = *b"00-00000000000000000000000000000000-0000000000000000-01";
write_hex(&mut tp[3..19], high);
write_hex(&mut tp[19..35], low);
write_hex(&mut tp[36..52], span);
Some(tp)
}
fn write_hex(digits: &mut [u8], value: u64) {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
for (at, digit) in digits.iter_mut().enumerate() {
let shift = 60 - 4 * at;
*digit = DIGITS[usize::try_from((value >> shift) & 0xf).expect("a nibble")];
}
}
fn random_id() -> Option<u64> {
thread_local! {
static STATE: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
}
STATE.with(|state| {
let seed = match state.get() {
Some(seed) => seed,
None => {
let mut bytes = [0u8; 8];
getrandom::fill(&mut bytes).ok()?;
u64::from_ne_bytes(bytes)
}
};
let next = seed.wrapping_add(0xa076_1d64_78bd_642f);
state.set(Some(next));
let wide = u128::from(next) * u128::from(next ^ 0xe703_7ed1_a0b4_28db);
Some((wide >> 64) as u64 ^ wide as u64)
})
}
pub fn apply_request_deadline<T>(
request: &mut tonic::Request<T>,
headers: &HeaderMap,
) -> Option<Duration> {
let timeout = headers
.get("grpc-timeout")
.and_then(|v| v.to_str().ok())
.and_then(parse_grpc_timeout)?;
request.set_timeout(timeout);
Some(timeout)
}
fn parse_grpc_timeout(value: &str) -> Option<Duration> {
let value = value.trim();
let (digits, unit) = value.split_at(value.len().checked_sub(1)?);
if digits.is_empty() || digits.len() > 8 {
return None;
}
let n: u64 = digits.parse().ok()?;
let dur = match unit {
"H" => Duration::from_secs(n * 3600),
"M" => Duration::from_secs(n * 60),
"S" => Duration::from_secs(n),
"m" => Duration::from_millis(n),
"u" => Duration::from_micros(n),
"n" => Duration::from_nanos(n),
_ => return None,
};
if dur.is_zero() {
return None;
}
Some(dur)
}
#[cfg(test)]
mod tests;