Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions iroh-relay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ lru = "0.18.3"
n0-error = "1.0.0"
n0-future = "0.3"
num_enum = "0.7"
percent-encoding = "2.3"
pin-project = "1"
postcard = { version = "1", default-features = false, features = [
"alloc",
Expand Down
88 changes: 81 additions & 7 deletions iroh-relay/src/client/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use std::{collections::VecDeque, net::IpAddr};

use bytes::Bytes;
use data_encoding::BASE64URL;
use data_encoding::BASE64;
use http_body_util::Empty;
use hyper::{Request, upgrade::Parts};
use hyper_util::rt::TokioIo;
Expand All @@ -18,6 +18,7 @@ use n0_future::{
FuturesUnordered, MaybeFuture, StreamExt, task,
time::{self},
};
use percent_encoding::percent_decode_str;
use rustls::client::Resumption;
use tokio::net::TcpStream;
use tracing::{Instrument, error, info_span};
Expand Down Expand Up @@ -180,12 +181,11 @@ impl MaybeTlsStreamBuilder {
"setting proxy-authorization: username={}",
proxy_url.username()
);
let to_encode = format!(
"{}:{}",
proxy_url.username(),
proxy_url.password().unwrap_or_default()
);
let encoded = BASE64URL.encode(to_encode.as_bytes());
// `Url` keeps the username and password percent-encoded.
let mut to_encode: Vec<u8> = percent_decode_str(proxy_url.username()).collect();
to_encode.push(b':');
to_encode.extend(percent_decode_str(proxy_url.password().unwrap_or_default()));
let encoded = BASE64.encode(&to_encode);
req_builder = req_builder.header("Proxy-Authorization", format!("Basic {encoded}"));
}
let req = req_builder
Expand Down Expand Up @@ -447,4 +447,78 @@ mod tests {
.expect_err("no addresses to dial");
assert!(matches!(err, DialError::Dns { .. }));
}

/// Dials through a fake proxy with the given userinfo and returns the
/// `Proxy-Authorization` header the proxy received.
///
/// The proxy reads the `CONNECT` request and hangs up, so the dial fails.
#[cfg(with_crypto_provider)]
async fn proxy_authorization_for(userinfo: &str) -> String {
use std::time::Duration;

use tokio::io::AsyncReadExt;

let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let port = listener.local_addr().unwrap().port();
let proxy = task::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let mut head = Vec::new();
while !head.ends_with(b"\r\n\r\n") {
head.push(stream.read_u8().await.unwrap());
}
String::from_utf8(head).unwrap()
});

let proxy_url = format!("http://{userinfo}@proxy.test:{port}")
.parse()
.expect("valid url");
let resolver = static_resolver(vec![Ipv4Addr::LOCALHOST], vec![]);
let res = MaybeTlsStreamBuilder::new(
relay_url(443),
resolver,
crate::tls::make_dangerous_client_config(),
)
.proxy_url(Some(proxy_url))
.connect()
.await;
assert!(res.is_err(), "the proxy never opens the tunnel");

let head = time::timeout(Duration::from_secs(5), proxy)
.await
.expect("the proxy received no request")
.unwrap();
head.lines()
.find_map(|line| {
let (name, value) = line.split_once(':')?;
name.eq_ignore_ascii_case("proxy-authorization")
.then(|| value.trim().to_string())
})
.expect("no Proxy-Authorization header")
}

/// Tests that the proxy credentials use the standard base64 alphabet (RFC 7617).
#[cfg(with_crypto_provider)]
#[tokio::test]
async fn proxy_authorization_uses_standard_base64() {
// The URL-safe alphabet gives "dXNlcjpzM2NyZXR-fg==".
assert_eq!(
proxy_authorization_for("user:s3cret~~").await,
"Basic dXNlcjpzM2NyZXR+fg=="
);
}

/// Tests that the proxy credentials are percent-decoded before they are encoded.
#[cfg(with_crypto_provider)]
#[tokio::test]
async fn proxy_authorization_decodes_credentials() {
// `Url` keeps these passwords as "s3cret%3E%3E" and "p%40ss".
assert_eq!(
proxy_authorization_for("user:s3cret>>").await,
"Basic dXNlcjpzM2NyZXQ+Pg=="
);
assert_eq!(
proxy_authorization_for("user:p%40ss").await,
"Basic dXNlcjpwQHNz"
);
}
}
Loading