From dee5f7910ad8c24cf28bab528f8d007550af2b23 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:17:55 +0200 Subject: [PATCH] Match URL schemes case-insensitively in Url::parse and Url::join Found while testing crates/fetch/src/http.rs:71 (Url::parse), also Url::join at :115 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/fetch/src/http.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/fetch/src/http.rs b/crates/fetch/src/http.rs index 0b1b0ae98..bad39163c 100644 --- a/crates/fetch/src/http.rs +++ b/crates/fetch/src/http.rs @@ -58,6 +58,12 @@ pub struct Url { pub path: String, } +/// `s` without the leading `prefix`, matched ASCII case-insensitively (RFC 3986 ยง3.1). +fn strip_scheme<'a>(s: &'a str, prefix: &str) -> Option<&'a str> { + let head = s.get(..prefix.len())?; + head.eq_ignore_ascii_case(prefix).then(|| s.get(prefix.len()..).unwrap_or_default()) +} + impl Url { pub fn parse(s: &str) -> Result { let bad = |why: &str| HttpError::BadUrl(why.to_string()); @@ -68,9 +74,9 @@ impl Url { if s.chars().any(|c| c.is_control() || c == ' ') { return Err(bad("contains spaces or control characters")); } - let (tls, rest) = if let Some(r) = s.strip_prefix("https://") { + let (tls, rest) = if let Some(r) = strip_scheme(s, "https://") { (true, r) - } else if let Some(r) = s.strip_prefix("http://") { + } else if let Some(r) = strip_scheme(s, "http://") { (false, r) } else { return Err(bad("only http:// and https:// are supported")); @@ -112,7 +118,7 @@ impl Url { /// `Location` of a redirect, resolved against this URL. pub fn join(&self, location: &str) -> Result { let l = location.trim(); - if l.starts_with("http://") || l.starts_with("https://") { + if strip_scheme(l, "http://").is_some() || strip_scheme(l, "https://").is_some() { let url = Url::parse(l)?; if self.tls && !url.tls { return Err(HttpError::BadUrl("redirect from https to http refused".into())); @@ -618,6 +624,15 @@ mod tests { assert!(Url::parse("https://example.com/x?secret=abc\r\nInjected: 1").is_err()); } + #[test] + fn url_scheme_is_matched_case_insensitively() { + let u = Url::parse("HTTPS://A.example/z").unwrap(); + assert_eq!((u.tls, u.host.as_str(), u.port, u.path.as_str()), (true, "a.example", 443, "/z")); + assert!(Url::parse("Http://a.example/").is_ok()); + let j = Url::parse("https://a.example/start").unwrap().join("HTTPS://cdn.example/file").unwrap(); + assert_eq!((j.tls, j.host.as_str(), j.path.as_str()), (true, "cdn.example", "/file")); + } + #[test] fn parses_urls() { let u = Url::parse("https://cdn.example.com/models/sam3/model.safetensors?sig=1#x").unwrap();