use std::io;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
pub async fn connect_via_http_proxy(proxy: &str, host: &str, port: u16) -> io::Result<TcpStream> {
let mut stream = TcpStream::connect(proxy).await?;
let request = format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n");
stream.write_all(request.as_bytes()).await?;
let mut buf = Vec::with_capacity(256);
let mut tmp = [0u8; 1];
loop {
let n = stream.read(&mut tmp).await?;
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"proxy closed connection before completing status line",
));
}
buf.push(tmp[0]);
if buf.ends_with(b"\r\n") {
break;
}
if buf.len() > 1024 {
return Err(io::Error::other(
"HTTP CONNECT status line exceeds 1024 bytes",
));
}
}
let status_line = String::from_utf8_lossy(&buf);
let status_ok = status_line.starts_with("HTTP/1.1 2") || status_line.starts_with("HTTP/1.0 2");
if !status_ok {
let line = status_line.trim_end();
return Err(io::Error::other(format!(
"HTTP CONNECT proxy rejected: {line}"
)));
}
let mut header_buf = Vec::with_capacity(512);
loop {
let n = stream.read(&mut tmp).await?;
if n == 0 {
break;
}
header_buf.push(tmp[0]);
if header_buf.len() >= 2
&& header_buf.ends_with(b"\r\n")
&& (header_buf.len() == 2 || header_buf[..header_buf.len() - 2].ends_with(b"\r\n"))
{
break;
}
if header_buf.len() > 8192 {
return Err(io::Error::other(
"HTTP CONNECT response headers exceed 8192 bytes",
));
}
}
tracing::debug!(
proxy = proxy,
target = %format!("{host}:{port}"),
"HTTP CONNECT tunnel established"
);
Ok(stream)
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::net::TcpListener;
#[tokio::test]
async fn http_connect_establishes_tunnel() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut s, _) = listener.accept().await.unwrap();
let mut buf = Vec::new();
let mut byte = [0u8; 1];
while !buf.ends_with(b"\r\n\r\n") {
s.read_exact(&mut byte).await.unwrap();
buf.push(byte[0]);
}
let req = String::from_utf8_lossy(&buf);
assert!(req.starts_with("CONNECT example.com:443 HTTP/1.1"), "{req}");
s.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
.await
.unwrap();
let mut echo = [0u8; 3];
s.read_exact(&mut echo).await.unwrap();
s.write_all(&echo).await.unwrap();
});
let mut stream = connect_via_http_proxy(&addr.to_string(), "example.com", 443)
.await
.expect("HTTP CONNECT should establish");
stream.write_all(b"abc").await.unwrap();
let mut got = [0u8; 3];
stream.read_exact(&mut got).await.unwrap();
assert_eq!(&got, b"abc");
server.await.unwrap();
}
}