use url::Url;
use super::client::NetError;
#[derive(Clone, PartialEq, Eq)]
pub struct OriginHeaders {
pub host: String,
pub headers: Vec<(String, String)>,
}
impl std::fmt::Debug for OriginHeaders {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names: Vec<&str> = self.headers.iter().map(|(name, _)| name.as_str()).collect();
f.debug_struct("OriginHeaders")
.field("host", &self.host)
.field("names", &names)
.finish_non_exhaustive()
}
}
impl OriginHeaders {
pub fn headers_for(&self, url: &Url) -> &[(String, String)] {
match url.host_str() {
Some(host) if host.eq_ignore_ascii_case(&self.host) => &self.headers,
_ => &[],
}
}
}
pub fn has_userinfo(url: &Url) -> bool {
!url.username().is_empty() || url.password().is_some()
}
pub(crate) fn refuse_userinfo(url: &Url) -> Result<(), NetError> {
if !has_userinfo(url) {
return Ok(());
}
Err(NetError::Blocked(format!(
"a URL with credentials in it is refused: {}",
without_userinfo(url)
)))
}
pub(crate) fn without_userinfo(url: &Url) -> Url {
let mut clean = url.clone();
let _ = clean.set_password(None);
let _ = clean.set_username("");
clean
}
#[cfg(test)]
mod tests {
use super::*;
fn url(s: &str) -> Url {
Url::parse(s).expect("test URL must parse")
}
fn credential() -> OriginHeaders {
OriginHeaders {
host: "example.com".to_owned(),
headers: vec![
("X-Custom".to_owned(), "value".to_owned()),
("Authorization".to_owned(), "Basic dXNlcjpwdw==".to_owned()),
],
}
}
#[test]
fn the_scoped_host_gets_the_headers_on_any_scheme_and_port() {
let credential = credential();
assert_eq!(
credential.headers_for(&url("https://EXAMPLE.com:8443/a")),
credential.headers.as_slice()
);
}
#[test]
fn another_host_gets_nothing() {
let credential = credential();
for other in [
"http://cdn.test/a.js",
"http://sub.example.com/",
"http://example.com.evil.test/",
] {
assert!(
credential.headers_for(&url(other)).is_empty(),
"{other} must get nothing"
);
}
}
#[test]
fn a_url_with_userinfo_is_refused_without_it() {
let error = refuse_userinfo(&url("http://user:s3cret@example.com/a")).expect_err("must be refused");
let text = error.to_string();
assert!(!text.contains("s3cret"), "{text}");
assert!(text.contains("http://example.com/a"), "{text}");
assert!(refuse_userinfo(&url("http://example.com/a")).is_ok());
}
#[test]
fn debug_output_hides_the_value() {
let rendered = format!("{:?}", credential());
assert!(
!rendered.contains("dXNlcjpwdw") && !rendered.contains("value"),
"{rendered}"
);
assert!(rendered.contains("Authorization"), "{rendered}");
}
}