Skip to content

Commit 1fad6eb

Browse files
authored
fix(ext/fetch): respect authority from URL (#24705)
This commit fixes handling of "authority" in the URL by properly sending "Authorization Basic..." header in `fetch` API. This is a regression from #24593 Fixes #24697 CC @seanmonstar
1 parent c7f468d commit 1fad6eb

File tree

6 files changed

+78
-7
lines changed

6 files changed

+78
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/fetch/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ hyper.workspace = true
2727
hyper-rustls.workspace = true
2828
hyper-util.workspace = true
2929
ipnet.workspace = true
30+
percent-encoding.workspace = true
3031
rustls-webpki.workspace = true
3132
serde.workspace = true
3233
serde_json.workspace = true

ext/fetch/lib.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ use http::header::HeaderName;
5555
use http::header::HeaderValue;
5656
use http::header::ACCEPT;
5757
use http::header::ACCEPT_ENCODING;
58+
use http::header::AUTHORIZATION;
5859
use http::header::CONTENT_LENGTH;
5960
use http::header::HOST;
6061
use http::header::PROXY_AUTHORIZATION;
@@ -77,6 +78,7 @@ use tower_http::decompression::Decompression;
7778

7879
// Re-export data_url
7980
pub use data_url;
81+
pub use proxy::basic_auth;
8082

8183
pub use fs_fetch_handler::FsFetchHandler;
8284

@@ -349,7 +351,7 @@ where
349351
};
350352

351353
let method = Method::from_bytes(&method)?;
352-
let url = Url::parse(&url)?;
354+
let mut url = Url::parse(&url)?;
353355

354356
// Check scheme before asking for net permission
355357
let scheme = url.scheme();
@@ -385,6 +387,7 @@ where
385387
let permissions = state.borrow_mut::<FP>();
386388
permissions.check_net_url(&url, "fetch()")?;
387389

390+
let maybe_authority = extract_authority(&mut url);
388391
let uri = url
389392
.as_str()
390393
.parse::<Uri>()
@@ -428,6 +431,12 @@ where
428431
*request.method_mut() = method.clone();
429432
*request.uri_mut() = uri;
430433

434+
if let Some((username, password)) = maybe_authority {
435+
request.headers_mut().insert(
436+
AUTHORIZATION,
437+
proxy::basic_auth(&username, password.as_deref()),
438+
);
439+
}
431440
if let Some(len) = con_len {
432441
request.headers_mut().insert(CONTENT_LENGTH, len.into());
433442
}
@@ -1096,3 +1105,34 @@ impl Client {
10961105

10971106
pub type ReqBody = http_body_util::combinators::BoxBody<Bytes, Error>;
10981107
pub type ResBody = http_body_util::combinators::BoxBody<Bytes, Error>;
1108+
1109+
/// Copied from https://github.com/seanmonstar/reqwest/blob/b9d62a0323d96f11672a61a17bf8849baec00275/src/async_impl/request.rs#L572
1110+
/// Check the request URL for a "username:password" type authority, and if
1111+
/// found, remove it from the URL and return it.
1112+
pub fn extract_authority(url: &mut Url) -> Option<(String, Option<String>)> {
1113+
use percent_encoding::percent_decode;
1114+
1115+
if url.has_authority() {
1116+
let username: String = percent_decode(url.username().as_bytes())
1117+
.decode_utf8()
1118+
.ok()?
1119+
.into();
1120+
let password = url.password().and_then(|pass| {
1121+
percent_decode(pass.as_bytes())
1122+
.decode_utf8()
1123+
.ok()
1124+
.map(String::from)
1125+
});
1126+
if !username.is_empty() || password.is_some() {
1127+
url
1128+
.set_username("")
1129+
.expect("has_authority means set_username shouldn't fail");
1130+
url
1131+
.set_password(None)
1132+
.expect("has_authority means set_password shouldn't fail");
1133+
return Some((username, password));
1134+
}
1135+
}
1136+
1137+
None
1138+
}

ext/fetch/proxy.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,18 @@ pub(crate) fn from_env() -> Proxies {
106106
Proxies { intercepts, no }
107107
}
108108

109-
pub(crate) fn basic_auth(user: &str, pass: &str) -> HeaderValue {
109+
pub fn basic_auth(user: &str, pass: Option<&str>) -> HeaderValue {
110110
use base64::prelude::BASE64_STANDARD;
111111
use base64::write::EncoderWriter;
112112
use std::io::Write;
113113

114114
let mut buf = b"Basic ".to_vec();
115115
{
116116
let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
117-
let _ = write!(encoder, "{user}:{pass}");
117+
let _ = write!(encoder, "{user}:");
118+
if let Some(password) = pass {
119+
let _ = write!(encoder, "{password}");
120+
}
118121
}
119122
let mut header =
120123
HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
@@ -140,10 +143,10 @@ impl Intercept {
140143
pub(crate) fn set_auth(&mut self, user: &str, pass: &str) {
141144
match self.target {
142145
Target::Http { ref mut auth, .. } => {
143-
*auth = Some(basic_auth(user, pass));
146+
*auth = Some(basic_auth(user, Some(pass)));
144147
}
145148
Target::Https { ref mut auth, .. } => {
146-
*auth = Some(basic_auth(user, pass));
149+
*auth = Some(basic_auth(user, Some(pass)));
147150
}
148151
Target::Socks { ref mut auth, .. } => {
149152
*auth = Some((user.into(), pass.into()));
@@ -192,7 +195,7 @@ impl Target {
192195
if is_socks {
193196
socks_auth = Some((user.into(), pass.into()));
194197
} else {
195-
http_auth = Some(basic_auth(user, pass));
198+
http_auth = Some(basic_auth(user, Some(pass)));
196199
}
197200
builder = builder.authority(host_port);
198201
} else {

ext/node/ops/http.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use deno_fetch::ResourceToBodyAdapter;
1818
use http::header::HeaderMap;
1919
use http::header::HeaderName;
2020
use http::header::HeaderValue;
21+
use http::header::AUTHORIZATION;
2122
use http::header::CONTENT_LENGTH;
2223
use http::Method;
2324
use http_body_util::BodyExt;
@@ -43,7 +44,8 @@ where
4344
};
4445

4546
let method = Method::from_bytes(&method)?;
46-
let url = Url::parse(&url)?;
47+
let mut url = Url::parse(&url)?;
48+
let maybe_authority = deno_fetch::extract_authority(&mut url);
4749

4850
{
4951
let permissions = state.borrow_mut::<P>();
@@ -89,6 +91,12 @@ where
8991
.map_err(|_| type_error("Invalid URL"))?;
9092
*request.headers_mut() = header_map;
9193

94+
if let Some((username, password)) = maybe_authority {
95+
request.headers_mut().insert(
96+
AUTHORIZATION,
97+
deno_fetch::basic_auth(&username, password.as_deref()),
98+
);
99+
}
92100
if let Some(len) = con_len {
93101
request.headers_mut().insert(CONTENT_LENGTH, len.into());
94102
}

tests/unit/fetch_test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,3 +2042,21 @@ Deno.test("Response with subarray TypedArray body", async () => {
20422042
const expected = new Uint8Array([2, 3, 4, 5]);
20432043
assertEquals(actual, expected);
20442044
});
2045+
2046+
// Regression test for https://github.com/denoland/deno/issues/24697
2047+
Deno.test("URL authority is used as 'Authorization' header", async () => {
2048+
const deferred = Promise.withResolvers<string | null | undefined>();
2049+
const ac = new AbortController();
2050+
2051+
const server = Deno.serve({ port: 4502, signal: ac.signal }, (req) => {
2052+
deferred.resolve(req.headers.get("authorization"));
2053+
return new Response("Hello world");
2054+
});
2055+
2056+
const res = await fetch("http://deno:land@localhost:4502");
2057+
await res.text();
2058+
const authHeader = await deferred.promise;
2059+
ac.abort();
2060+
await server.finished;
2061+
assertEquals(authHeader, "Basic ZGVubzpsYW5k");
2062+
});

0 commit comments

Comments
 (0)