use std::error::Error;
use base64::Engine;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio_socks::tcp::Socks5Stream;
use url::Url;
#[derive(Debug, Default)]
pub struct ProxyBuilder {
kind: Option<ProxyKind>,
auth: Option<ProxyAuth>,
}
#[derive(Debug)]
enum ProxyKind {
Http(Url),
Socks5(String),
}
impl ProxyBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn http(mut self, uri: &str) -> Result<Self, Box<dyn Error>> {
let parsed = Url::parse(uri)?;
self.kind = Some(ProxyKind::Http(parsed));
Ok(self)
}
pub fn socks5(mut self, addr: &str) -> Self {
self.kind = Some(ProxyKind::Socks5(addr.to_string()));
self
}
pub fn auth(mut self, username: &str, password: &str) -> Self {
self.auth = Some(ProxyAuth {
username: username.to_string(),
password: password.to_string(),
});
self
}
pub fn build(self) -> Result<Proxy, Box<dyn Error>> {
match self.kind {
Some(ProxyKind::Http(uri)) => Ok(Proxy::Http {
uri,
auth: self.auth,
}),
Some(ProxyKind::Socks5(addr)) => Ok(Proxy::Socks5 {
addr,
auth: self.auth,
}),
None => Err("Proxy type not specified".into()),
}
}
}
#[derive(Clone, Debug)]
pub struct ProxyAuth {
username: String,
password: String,
}
#[derive(Clone, Debug)]
pub enum Proxy {
Http {
uri: Url,
auth: Option<ProxyAuth>,
},
Socks5 {
addr: String,
auth: Option<ProxyAuth>,
},
}
impl Proxy {
pub(crate) async fn tunnel(
&self,
target_host: &str,
target_port: u16,
) -> Result<TcpStream, Box<dyn Error + Send + Sync>> {
match self {
Proxy::Http { uri, auth } => {
let proxy_addr = format!(
"{}:{}",
uri.host_str().ok_or("invalid proxy host")?,
uri.port_or_known_default().ok_or("proxy port missing")?
);
let mut stream = TcpStream::connect(&proxy_addr).await?;
let target = format!("{target_host}:{target_port}");
let mut req = format!(
"CONNECT {target} HTTP/1.1\r\nHost: {target}\r\nProxy-Connection: Keep-Alive\r\n"
);
if let Some(auth) = auth {
let token = base64::engine::general_purpose::STANDARD
.encode(format!("{}:{}", auth.username, auth.password));
req.push_str(&format!("Proxy-Authorization: Basic {token}\r\n"));
}
req.push_str("\r\n");
stream.write_all(req.as_bytes()).await?;
let mut buf = [0u8; 1024 * 16];
let n = stream.read(&mut buf).await?;
let resp = std::str::from_utf8(&buf[..n])?;
if !resp.starts_with("HTTP/1.1 200") && !resp.starts_with("HTTP/1.0 200") {
return Err(format!("proxy CONNECT failed: {resp}").into());
}
Ok(stream)
}
Proxy::Socks5 { addr, auth } => {
let address = addr.as_str();
let target = (target_host, target_port);
let stream = if let Some(auth) = auth {
Socks5Stream::connect_with_password(
address,
target,
&auth.username,
&auth.password,
)
.await?
} else {
Socks5Stream::connect(address, target).await?
};
Ok(stream.into_inner())
}
}
}
}