use reqwest::header::{HeaderName, HeaderValue};
const SENSITIVE_HEADERS: [&str; 4] = [
"authorization",
"cookie",
"proxy-authorization",
"x-api-key",
];
pub fn is_sensitive_header(name: &HeaderName) -> bool {
SENSITIVE_HEADERS.contains(&name.as_str())
}
pub fn parse_header(spec: &str) -> Result<(HeaderName, HeaderValue), String> {
let (name, value) = spec
.split_once(':')
.ok_or_else(|| format!("Invalid --header `{spec}`: expected `Name: Value`"))?;
let name = HeaderName::from_bytes(name.trim().as_bytes())
.map_err(|e| format!("Invalid --header `{spec}`: {e}"))?;
let mut value = HeaderValue::from_str(value.trim())
.map_err(|e| format!("Invalid --header `{spec}`: {e}"))?;
value.set_sensitive(true);
Ok((name, value))
}
pub fn terminal_safe(text: &str) -> String {
text.chars()
.map(|c| match c {
c if c.is_control() => char::REPLACEMENT_CHARACTER,
'\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}' => char::REPLACEMENT_CHARACTER,
c => c,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{HeaderName, is_sensitive_header, parse_header, terminal_safe};
#[test]
fn table_text_cannot_carry_terminal_control_sequences() {
for hostile in [
"\u{1b}[31mroot\u{1b}[0m",
"\u{1b}[2J\u{1b}[H",
"\u{1b}]52;c;bWFsaWNl\u{7}",
"admin\r\nfake row",
"bell\u{7}",
"nul\u{0}byte",
"c1\u{9b}31m",
"\u{202e}moc.live@nimda",
] {
let safe = terminal_safe(hostile);
assert!(
!safe.chars().any(|c| c.is_control()
|| ('\u{202A}'..='\u{202E}').contains(&c)
|| ('\u{2066}'..='\u{2069}').contains(&c)),
"{safe:?} still carries a control character"
);
assert_eq!(safe.chars().count(), hostile.chars().count());
}
}
#[test]
fn table_text_leaves_ordinary_names_alone() {
for benign in [
"Ada Lovelace",
"ada@example.com",
"Gerd Zellweger",
"Ólafur Þórðarson",
"\u{5f20}\u{4f1f}",
"\u{645}\u{62d}\u{645}\u{62f}",
"tenant-1_a.b",
"",
] {
assert_eq!(terminal_safe(benign), benign);
}
}
#[test]
fn header_arguments_are_parsed_like_curl_spells_them() {
for (spec, name, value) in [
("Cookie: session=abc", "cookie", "session=abc"),
(" X-Trace-Id:42 ", "x-trace-id", "42"),
(
"Authorization: Bearer tok.en",
"authorization",
"Bearer tok.en",
),
("X-Range: bytes:0:10", "x-range", "bytes:0:10"),
("X-Empty:", "x-empty", ""),
] {
let (parsed_name, parsed_value) = parse_header(spec).expect(spec);
assert_eq!(parsed_name.as_str(), name);
assert_eq!(parsed_value.to_str().expect("printable value"), value);
assert!(
parsed_value.is_sensitive(),
"a header passed on the command line may carry a credential"
);
}
}
#[test]
fn unparsable_header_arguments_are_rejected() {
for spec in [
"no-colon-here",
":empty-name",
"Bad Name: value",
"X-Newline: one\r\ntwo",
] {
let err = parse_header(spec).expect_err(spec);
assert!(
err.contains(spec),
"the report must quote the argument: {err}"
);
}
}
#[test]
fn credential_headers_are_told_apart_from_the_rest() {
for name in [
"authorization",
"cookie",
"proxy-authorization",
"x-api-key",
] {
assert!(is_sensitive_header(&HeaderName::from_static(name)));
}
for name in ["feldera-tenant", "user-agent", "x-trace-id"] {
assert!(!is_sensitive_header(&HeaderName::from_static(name)));
}
}
}