Skip to main content

arcbox_proxy/
http_connect.rs

1//! HTTP CONNECT proxy client.
2//!
3//! Used when the host has a system HTTP proxy configured, to tunnel a TCP
4//! connection by hostname (from the `DnsResolutionLog`) rather than the raw IP โ€”
5//! critical for fake-IP environments where the destination IP only the proxy can
6//! resolve. The SOCKS5 equivalent lives in [`crate::socks5`].
7
8use std::io;
9
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tokio::net::TcpStream;
12
13/// Establishes a TCP tunnel via an HTTP CONNECT proxy (RFC 7231 ยง4.3.6).
14///
15/// The returned `TcpStream` is an end-to-end tunnel โ€” all subsequent reads
16/// and writes go directly to the target host through the proxy.
17pub async fn connect_via_http_proxy(proxy: &str, host: &str, port: u16) -> io::Result<TcpStream> {
18    let mut stream = TcpStream::connect(proxy).await?;
19
20    let request = format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n\r\n");
21    stream.write_all(request.as_bytes()).await?;
22
23    // Read until we find "\r\n" marking the end of the status line. The
24    // response may arrive split across multiple TCP segments, so we loop.
25    let mut buf = Vec::with_capacity(256);
26    let mut tmp = [0u8; 1];
27    loop {
28        let n = stream.read(&mut tmp).await?;
29        if n == 0 {
30            return Err(io::Error::new(
31                io::ErrorKind::UnexpectedEof,
32                "proxy closed connection before completing status line",
33            ));
34        }
35        buf.push(tmp[0]);
36        if buf.ends_with(b"\r\n") {
37            break;
38        }
39        if buf.len() > 1024 {
40            return Err(io::Error::other(
41                "HTTP CONNECT status line exceeds 1024 bytes",
42            ));
43        }
44    }
45
46    let status_line = String::from_utf8_lossy(&buf);
47    // Accept any 2xx status as success (200, 204, etc).
48    let status_ok = status_line.starts_with("HTTP/1.1 2") || status_line.starts_with("HTTP/1.0 2");
49
50    if !status_ok {
51        let line = status_line.trim_end();
52        return Err(io::Error::other(format!(
53            "HTTP CONNECT proxy rejected: {line}"
54        )));
55    }
56
57    // Consume remaining headers until a blank line. We already read the
58    // status line (including its \r\n). The headers end with \r\n\r\n, so
59    // we look for two consecutive \r\n sequences in the remaining data.
60    let mut header_buf = Vec::with_capacity(512);
61    loop {
62        let n = stream.read(&mut tmp).await?;
63        if n == 0 {
64            break;
65        }
66        header_buf.push(tmp[0]);
67        // A standalone \r\n (i.e. header_buf == b"\r\n") means the first
68        // header line is blank โ†’ end of headers. Otherwise \r\n\r\n at the
69        // tail means the previous header ended and a blank line followed.
70        if header_buf.len() >= 2
71            && header_buf.ends_with(b"\r\n")
72            && (header_buf.len() == 2 || header_buf[..header_buf.len() - 2].ends_with(b"\r\n"))
73        {
74            break;
75        }
76        if header_buf.len() > 8192 {
77            return Err(io::Error::other(
78                "HTTP CONNECT response headers exceed 8192 bytes",
79            ));
80        }
81    }
82
83    tracing::debug!(
84        proxy = proxy,
85        target = %format!("{host}:{port}"),
86        "HTTP CONNECT tunnel established"
87    );
88    Ok(stream)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use tokio::net::TcpListener;
95
96    /// Minimal HTTP CONNECT proxy: validates the request line and replies 200.
97    #[tokio::test]
98    async fn http_connect_establishes_tunnel() {
99        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
100        let addr = listener.local_addr().unwrap();
101        let server = tokio::spawn(async move {
102            let (mut s, _) = listener.accept().await.unwrap();
103            let mut buf = Vec::new();
104            let mut byte = [0u8; 1];
105            while !buf.ends_with(b"\r\n\r\n") {
106                s.read_exact(&mut byte).await.unwrap();
107                buf.push(byte[0]);
108            }
109            let req = String::from_utf8_lossy(&buf);
110            assert!(req.starts_with("CONNECT example.com:443 HTTP/1.1"), "{req}");
111            s.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
112                .await
113                .unwrap();
114            let mut echo = [0u8; 3];
115            s.read_exact(&mut echo).await.unwrap();
116            s.write_all(&echo).await.unwrap();
117        });
118
119        let mut stream = connect_via_http_proxy(&addr.to_string(), "example.com", 443)
120            .await
121            .expect("HTTP CONNECT should establish");
122        stream.write_all(b"abc").await.unwrap();
123        let mut got = [0u8; 3];
124        stream.read_exact(&mut got).await.unwrap();
125        assert_eq!(&got, b"abc");
126        server.await.unwrap();
127    }
128}