use async_net::TcpStream;
use http::{HeaderValue, Uri};
use super::{Transport, happy_eyeballs, socks5, stream::Stream, tunnel};
use crate::{Error, error::ProxyErrorKind};
#[derive(Clone, Copy, Debug)]
pub enum Protocols {
Http1,
#[cfg_attr(not(feature = "hyper-backend"), allow(dead_code))]
Http2OrHttp1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Protocol {
Http1,
#[cfg(feature = "http2")]
Http2,
}
#[derive(Clone, Copy, Debug)]
pub struct Target<'a> {
pub host: &'a str,
pub port: u16,
pub tls: bool,
pub tunnel_plaintext: bool,
pub protocols: Protocols,
}
#[derive(Clone, Debug)]
pub enum Via {
Direct,
HttpProxy {
#[cfg_attr(not(feature = "hyper-backend"), allow(dead_code))]
authorization: Option<HeaderValue>,
},
}
#[derive(Debug)]
pub struct Connection {
pub stream: Stream,
pub via: Via,
#[cfg_attr(not(feature = "hyper-backend"), allow(dead_code))]
pub protocol: Protocol,
}
#[cfg(http3)]
pub fn proxied(transport: &Transport, target: Target<'_>) -> Result<bool, Error> {
Ok(transport
.proxy()
.intercept(&destination_uri(target)?)
.is_some())
}
pub async fn connect(transport: &Transport, target: Target<'_>) -> Result<Connection, Error> {
let Some(intercept) = transport.proxy().intercept(&destination_uri(target)?) else {
let tcp = tcp(target.host, target.port).await?;
let stream = finish(transport, target, tcp).await?;
return connection(stream, Via::Direct, target);
};
let proxy_uri = intercept.uri();
let scheme = proxy_uri.scheme_str().unwrap_or("http");
let proxy_host = proxy_uri.host().ok_or(ProxyErrorKind::MissingHost)?;
match scheme {
"http" | "https" => {
let proxy_tls = scheme == "https";
let proxy_port = proxy_uri
.port_u16()
.unwrap_or(if proxy_tls { 443 } else { 80 });
let tcp = tcp(proxy_host, proxy_port).await?;
if !target.tls && !target.tunnel_plaintext {
let stream = if proxy_tls {
Stream::Tls(Box::new(
transport
.tls()
.connect(proxy_host, tcp, Protocols::Http1)
.await?,
))
} else {
Stream::Tcp(tcp)
};
return connection(
stream,
Via::HttpProxy {
authorization: intercept.basic_auth().cloned(),
},
target,
);
}
let authority = authority(target.host, target.port);
let stream = if proxy_tls {
let to_proxy = transport
.tls()
.connect(proxy_host, tcp, Protocols::Http1)
.await?;
let tunneled =
tunnel::connect(to_proxy, &authority, intercept.basic_auth()).await?;
if target.tls {
Stream::TlsOverTls(Box::new(
transport
.tls()
.connect(target.host, tunneled, target.protocols)
.await?,
))
} else {
Stream::Tls(Box::new(tunneled))
}
} else {
let tunneled = tunnel::connect(tcp, &authority, intercept.basic_auth()).await?;
finish(transport, target, tunneled).await?
};
connection(stream, Via::Direct, target)
}
"socks5" | "socks5h" => {
let proxy_port = proxy_uri.port_u16().unwrap_or(1080);
let tcp = tcp(proxy_host, proxy_port).await?;
let tcp = socks5::connect(
tcp,
target.host,
target.port,
intercept.raw_auth(),
scheme == "socks5h",
)
.await?;
let stream = finish(transport, target, tcp).await?;
connection(stream, Via::Direct, target)
}
other => Err(ProxyErrorKind::UnsupportedScheme(other.to_owned()).into()),
}
}
fn connection(stream: Stream, via: Via, target: Target<'_>) -> Result<Connection, Error> {
let protocol = match (target.tls, stream.negotiated_alpn()?.as_deref()) {
#[cfg(feature = "http2")]
(true, Some(b"h2")) => Protocol::Http2,
_ => Protocol::Http1,
};
Ok(Connection {
stream,
via,
protocol,
})
}
async fn finish(
transport: &Transport,
target: Target<'_>,
tcp: TcpStream,
) -> Result<Stream, Error> {
if target.tls {
Ok(Stream::Tls(Box::new(
transport
.tls()
.connect(target.host, tcp, target.protocols)
.await?,
)))
} else {
Ok(Stream::Tcp(tcp))
}
}
async fn tcp(host: &str, port: u16) -> Result<TcpStream, Error> {
let tcp = happy_eyeballs::connect(host, port)
.await
.map_err(|error| Error::Transport(Box::new(error)))?;
tcp.set_nodelay(true)
.map_err(|error| Error::Transport(Box::new(error)))?;
Ok(tcp)
}
fn authority(host: &str, port: u16) -> String {
if host.contains(':') && !host.starts_with('[') {
format!("[{host}]:{port}")
} else {
format!("{host}:{port}")
}
}
fn destination_uri(target: Target<'_>) -> Result<Uri, Error> {
Uri::builder()
.scheme(if target.tls { "https" } else { "http" })
.authority(authority(target.host, target.port))
.path_and_query("/")
.build()
.map_err(|error| Error::InvalidUri(error.to_string()))
}