#![cfg(feature = "tokio")]
mod common;
use common::*;
#[tokio::test]
async fn test_http_proxy() {
let proxy_addr = start_server_with(|req| async move {
let uri = req.uri().to_string();
let host = req
.headers()
.get("host")
.map(|v| v.to_str().unwrap_or("").to_owned())
.unwrap_or_default();
let body = format!("proxied: uri={uri} host={host}");
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(body))))
})
.await;
let client = Client::<TokioRuntime>::builder()
.proxy(aioduct::ProxyConfig::http(&format!("http://{proxy_addr}")).unwrap())
.build();
let resp = client
.get("http://example.com/path")
.unwrap()
.send()
.await
.unwrap();
let body = resp.text().await.unwrap();
assert!(
body.contains("proxied:"),
"expected proxied response, got: {body}"
);
assert!(body.contains("/path"), "expected path in URI, got: {body}");
}
#[tokio::test]
async fn test_proxy_settings_no_proxy_bypass() {
let proxy_addr = start_server_with(|req| async move {
let uri = req.uri().to_string();
let body = format!("proxied: {uri}");
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(body))))
})
.await;
let target_addr = start_server_with(|_req| async move {
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("direct"))))
})
.await;
let settings = aioduct::ProxySettings::all(
aioduct::ProxyConfig::http(&format!("http://{proxy_addr}")).unwrap(),
)
.no_proxy(aioduct::NoProxy::new(&format!("{}", target_addr.ip())));
let client = Client::<TokioRuntime>::builder()
.proxy_settings(settings)
.build();
let resp = client
.get(&format!("http://{target_addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), "direct");
let resp = client
.get("http://example.com/test")
.unwrap()
.send()
.await
.unwrap();
let body = resp.text().await.unwrap();
assert!(body.starts_with("proxied:"), "expected proxy, got: {body}");
}
#[tokio::test]
async fn test_no_proxy_wildcard_bypasses_all() {
let target_addr = start_server_with(|_req| async move {
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from("direct"))))
})
.await;
let settings =
aioduct::ProxySettings::all(aioduct::ProxyConfig::http("http://127.0.0.1:9999").unwrap())
.no_proxy(aioduct::NoProxy::new("*"));
let client = Client::<TokioRuntime>::builder()
.proxy_settings(settings)
.build();
let resp = client
.get(&format!("http://{target_addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.text().await.unwrap(), "direct");
}
#[tokio::test]
async fn test_no_proxy_domain_suffix_matching() {
let no_proxy = aioduct::NoProxy::new(".example.com, localhost");
assert!(!no_proxy.matches("example.com")); assert!(no_proxy.matches("foo.example.com"));
assert!(no_proxy.matches("bar.baz.example.com"));
assert!(no_proxy.matches("localhost"));
assert!(!no_proxy.matches("notexample.com"));
assert!(!no_proxy.matches("other.com"));
}
#[tokio::test]
async fn test_no_proxy_bare_domain_matches_subdomains() {
let no_proxy = aioduct::NoProxy::new("example.com");
assert!(no_proxy.matches("example.com"));
assert!(no_proxy.matches("foo.example.com"));
assert!(!no_proxy.matches("notexample.com"));
}
#[tokio::test]
async fn test_socks5_proxy() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let target_addr = start_server().await;
let socks_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let socks_addr = socks_listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (mut client, _) = socks_listener.accept().await.unwrap();
tokio::spawn(async move {
let mut buf = [0u8; 256];
let n = client.read(&mut buf).await.unwrap();
assert!(n >= 3);
assert_eq!(buf[0], 0x05);
client.write_all(&[0x05, 0x00]).await.unwrap();
let n = client.read(&mut buf).await.unwrap();
assert!(n >= 7);
assert_eq!(buf[0], 0x05); assert_eq!(buf[1], 0x01); assert_eq!(buf[3], 0x03);
let domain_len = buf[4] as usize;
let port_offset = 5 + domain_len;
let port = ((buf[port_offset] as u16) << 8) | (buf[port_offset + 1] as u16);
let target = format!("127.0.0.1:{port}");
let mut upstream = tokio::net::TcpStream::connect(target).await.unwrap();
client
.write_all(&[0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
.await
.unwrap();
let _ = tokio::io::copy_bidirectional(&mut client, &mut upstream).await;
});
}
});
let client = Client::<TokioRuntime>::builder()
.proxy(aioduct::ProxyConfig::socks5(&format!("socks5://{socks_addr}")).unwrap())
.build();
let resp = client
.get(&format!("http://localhost:{}/", target_addr.port()))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
assert_eq!(resp.text().await.unwrap(), "hello aioduct");
}
#[tokio::test]
async fn test_socks5_proxy_with_auth() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let target_addr = start_server().await;
let socks_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let socks_addr = socks_listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (mut client, _) = socks_listener.accept().await.unwrap();
tokio::spawn(async move {
let mut buf = [0u8; 256];
let n = client.read(&mut buf).await.unwrap();
assert!(n >= 3);
assert_eq!(buf[0], 0x05);
client.write_all(&[0x05, 0x02]).await.unwrap();
let n = client.read(&mut buf).await.unwrap();
assert!(n >= 3);
assert_eq!(buf[0], 0x01); let ulen = buf[1] as usize;
let username = String::from_utf8_lossy(&buf[2..2 + ulen]).to_string();
let plen = buf[2 + ulen] as usize;
let password = String::from_utf8_lossy(&buf[3 + ulen..3 + ulen + plen]).to_string();
if username == "testuser" && password == "testpass" {
client.write_all(&[0x01, 0x00]).await.unwrap();
} else {
client.write_all(&[0x01, 0x01]).await.unwrap();
return;
}
let n = client.read(&mut buf).await.unwrap();
assert!(n >= 7);
let domain_len = buf[4] as usize;
let port_offset = 5 + domain_len;
let port = ((buf[port_offset] as u16) << 8) | (buf[port_offset + 1] as u16);
let target = format!("127.0.0.1:{port}");
let mut upstream = tokio::net::TcpStream::connect(target).await.unwrap();
client
.write_all(&[0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
.await
.unwrap();
let _ = tokio::io::copy_bidirectional(&mut client, &mut upstream).await;
});
}
});
let client = Client::<TokioRuntime>::builder()
.proxy(
aioduct::ProxyConfig::socks5(&format!("socks5://{socks_addr}"))
.unwrap()
.basic_auth("testuser", "testpass"),
)
.build();
let resp = client
.get(&format!("http://localhost:{}/", target_addr.port()))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
assert_eq!(resp.text().await.unwrap(), "hello aioduct");
}