1use url::Url;
2
3use crate::error::Error;
4use crate::http::{HeaderMap, HeaderValue};
5
6const SENSITIVE_HEADERS: &[&str] = &["authorization", "cookie", "set-cookie", "x-csrf-token"];
7
8pub fn require_secure_endpoint(url: &Url) -> Result<(), Error> {
11 if url.scheme() == "https" || (url.scheme() == "http" && is_localhost(url)) {
12 Ok(())
13 } else {
14 Err(Error::usage(format!("{url} must use HTTPS")))
15 }
16}
17
18pub fn is_localhost(url: &Url) -> bool {
21 match url.host_str() {
22 Some(host) => {
23 let host = host
24 .trim_start_matches('[')
25 .trim_end_matches(']')
26 .to_ascii_lowercase();
27 host == "localhost"
28 || host == "127.0.0.1"
29 || host == "::1"
30 || host.ends_with(".localhost")
31 }
32 None => false,
33 }
34}
35
36pub fn is_same_origin(a: &Url, b: &Url) -> bool {
39 a.scheme().eq_ignore_ascii_case(b.scheme())
40 && a.host_str()
41 .unwrap_or_default()
42 .eq_ignore_ascii_case(b.host_str().unwrap_or_default())
43 && a.port_or_known_default() == b.port_or_known_default()
44}
45
46pub fn redact_headers(headers: &HeaderMap) -> HeaderMap {
48 let mut redacted = headers.clone();
49 for name in SENSITIVE_HEADERS {
50 if redacted.contains_key(*name) {
51 redacted.insert(*name, HeaderValue::from_static("[REDACTED]"));
52 }
53 }
54 redacted
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn localhost_may_use_plain_http() {
63 assert!(require_secure_endpoint(&Url::parse("http://localhost:3000").unwrap()).is_ok());
64 assert!(require_secure_endpoint(&Url::parse("http://127.0.0.1:8080/x").unwrap()).is_ok());
65 assert!(require_secure_endpoint(&Url::parse("http://app.localhost").unwrap()).is_ok());
66 assert!(require_secure_endpoint(&Url::parse("https://app.hey.com").unwrap()).is_ok());
67 assert!(require_secure_endpoint(&Url::parse("http://evil.example.com").unwrap()).is_err());
68 }
69
70 #[test]
71 fn same_origin_ignores_default_ports_and_case() {
72 let a = Url::parse("https://App.HEY.com/boxes").unwrap();
73 assert!(is_same_origin(
74 &a,
75 &Url::parse("HTTPS://app.hey.com:443/other").unwrap()
76 ));
77 assert!(!is_same_origin(
78 &a,
79 &Url::parse("http://app.hey.com/other").unwrap()
80 ));
81 assert!(!is_same_origin(
82 &a,
83 &Url::parse("https://evil.example.com/boxes").unwrap()
84 ));
85 }
86
87 #[test]
88 fn credentials_are_redacted() {
89 let mut headers = HeaderMap::new();
90 headers.insert("Authorization", HeaderValue::from_static("Bearer secret"));
91 headers.insert("Accept", HeaderValue::from_static("application/json"));
92 let redacted = redact_headers(&headers);
93 assert_eq!(redacted["authorization"], "[REDACTED]");
94 assert_eq!(redacted["accept"], "application/json");
95 }
96}