Skip to content

Commit 4aa2428

Browse files
fix URI basic auth extraction
1 parent 37aa93e commit 4aa2428

1 file changed

Lines changed: 50 additions & 29 deletions

File tree

‎src/sinks/util/uri.rs‎

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -131,39 +131,26 @@ fn get_basic_auth(authority: &Authority) -> crate::Result<(Authority, Option<Aut
131131
// `http::Uri` accepts authorities that `url::Url` rejects (e.g. a
132132
// non-numeric port), so this parse can fail; propagate the error instead
133133
// of panicking.
134-
let mut url = url::Url::parse(&format!("http://{authority}"))?;
135-
136-
let user = url.username();
137-
if !user.is_empty() {
138-
let user = percent_decode_str(user).decode_utf8_lossy().into_owned();
134+
let url = url::Url::parse(&format!("http://{authority}"))?;
135+
let Some((_, host_port)) = authority.as_str().rsplit_once('@') else {
136+
return Ok((authority.clone(), None));
137+
};
139138

140-
let password = url.password().unwrap_or("");
141-
let password = percent_decode_str(password)
139+
let has_auth = !url.username().is_empty() || url.password().is_some();
140+
let auth = has_auth.then(|| {
141+
let user = percent_decode_str(url.username())
142142
.decode_utf8_lossy()
143143
.into_owned();
144+
let password = percent_decode_str(url.password().unwrap_or(""))
145+
.decode_utf8_lossy()
146+
.into_owned();
147+
Auth::Basic {
148+
user,
149+
password: password.into(),
150+
}
151+
});
144152

145-
// These methods have the same failure condition as `username`,
146-
// because we have a non-empty username, they cannot fail here.
147-
url.set_username("")
148-
.map_err(|_| "unexpected empty authority")?;
149-
url.set_password(None)
150-
.map_err(|_| "unexpected empty authority")?;
151-
152-
let authority = Uri::from_maybe_shared(String::from(url))?
153-
.authority()
154-
.ok_or_else(|| "unexpected empty authority".to_string())?
155-
.clone();
156-
157-
Ok((
158-
authority,
159-
Some(Auth::Basic {
160-
user,
161-
password: password.into(),
162-
}),
163-
))
164-
} else {
165-
Ok((authority.clone(), None))
166-
}
153+
Ok((host_port.parse()?, auth))
167154
}
168155

169156
/// Simplify the URI into a protocol and endpoint by removing the
@@ -497,6 +484,18 @@ mod tests {
497484
);
498485

499486
test_parse("user@example.com", "example.com", Some(("user", "")));
487+
488+
test_parse(
489+
"https://user:pass@example.com:80/api",
490+
"https://example.com:80/api",
491+
Some(("user", "pass")),
492+
);
493+
494+
test_parse(
495+
"https://:secret@example.com/api",
496+
"https://example.com/api",
497+
Some(("", "secret")),
498+
);
500499
}
501500

502501
#[test]
@@ -563,6 +562,28 @@ mod tests {
563562
assert!(matches!(auth, Some(Auth::Basic { user, .. }) if user == "user"));
564563
}
565564

565+
#[test]
566+
fn http_endpoint_auth_extraction_preserves_explicit_port() {
567+
let endpoint = HttpEndpoint::parse("user:pass@example.com:80/path").unwrap();
568+
let (endpoint, auth) = endpoint.extract_basic_auth().unwrap();
569+
570+
assert_eq!(endpoint.to_string(), "https://example.com:80/path");
571+
assert!(matches!(auth, Some(Auth::Basic { user, .. }) if user == "user"));
572+
}
573+
574+
#[test]
575+
fn http_endpoint_auth_extraction_accepts_empty_username() {
576+
let endpoint = HttpEndpoint::parse(":secret@example.com/path").unwrap();
577+
let (endpoint, auth) = endpoint.extract_basic_auth().unwrap();
578+
579+
assert_eq!(endpoint.to_string(), "https://example.com/path");
580+
assert!(matches!(
581+
auth,
582+
Some(Auth::Basic { user, password })
583+
if user.is_empty() && password.inner() == "secret"
584+
));
585+
}
586+
566587
#[test]
567588
fn http_endpoint_defaults_missing_scheme_to_https() {
568589
for endpoint in [

0 commit comments

Comments
 (0)