Skip to main content

http_quik/client/
proxy.rs

1use tokio::io::{AsyncReadExt, AsyncWriteExt};
2use tokio::net::TcpStream;
3use url::Url;
4
5use crate::error::{Error, Result};
6
7/// Supported proxy protocols for pre-handshake transport tunneling.
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub enum Proxy {
10    /// HTTP CONNECT proxy (RFC 7231)
11    Http(String),
12    /// SOCKSv5 proxy (RFC 1928)
13    Socks5(String),
14}
15
16impl Proxy {
17    /// Parses a proxy URL into a `Proxy` variant.
18    ///
19    /// Supports `http://` and `socks5://` schemes. Defaults to port 1080
20    /// for SOCKS5 if not specified.
21    pub fn parse(url: &str) -> Result<Self> {
22        let parsed = Url::parse(url).map_err(|e| Error::InvalidUrl(e.to_string()))?;
23        let addr = format!(
24            "{}:{}",
25            parsed.host_str().unwrap_or(""),
26            parsed.port().unwrap_or(1080)
27        );
28
29        match parsed.scheme() {
30            "http" => Ok(Proxy::Http(addr)),
31            "socks5" => Ok(Proxy::Socks5(addr)),
32            _ => Err(Error::InvalidUrl("Unsupported proxy scheme".to_string())),
33        }
34    }
35}
36
37/// Establishes a TCP tunnel through the specified proxy to the target host.
38///
39/// This occurs before the TLS handshake, ensuring that the SNI and fingerprint
40/// remain encrypted and uncompromised by the proxy server.
41pub async fn dial_proxy(proxy: &Proxy, target_host: &str, target_port: u16) -> Result<TcpStream> {
42    match proxy {
43        Proxy::Http(addr) => dial_http_proxy(addr, target_host, target_port).await,
44        Proxy::Socks5(addr) => dial_socks5_proxy(addr, target_host, target_port).await,
45    }
46}
47
48/// Implements the HTTP CONNECT tunneling protocol.
49async fn dial_http_proxy(
50    proxy_addr: &str,
51    target_host: &str,
52    target_port: u16,
53) -> Result<TcpStream> {
54    let mut stream = TcpStream::connect(proxy_addr).await?;
55
56    let connect_req = format!(
57        "CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n\r\n",
58        target_host, target_port, target_host, target_port
59    );
60
61    stream.write_all(connect_req.as_bytes()).await?;
62
63    let mut buf = [0; 4096];
64    let mut read_bytes = 0;
65
66    // Wait for the double CRLF indicating the end of the HTTP response headers.
67    loop {
68        let n = stream.read(&mut buf[read_bytes..]).await?;
69        if n == 0 {
70            return Err(Error::Connect(std::io::Error::new(
71                std::io::ErrorKind::ConnectionAborted,
72                "Proxy closed connection during CONNECT handshake",
73            )));
74        }
75        read_bytes += n;
76
77        let response = String::from_utf8_lossy(&buf[..read_bytes]);
78        if response.contains("\r\n\r\n") {
79            if response.starts_with("HTTP/1.1 200") || response.starts_with("HTTP/1.0 200") {
80                return Ok(stream);
81            } else {
82                return Err(Error::Connect(std::io::Error::other(format!(
83                    "HTTP proxy returned error status: {}",
84                    response.lines().next().unwrap_or("Unknown")
85                ))));
86            }
87        }
88    }
89}
90
91/// Implements the SOCKSv5 protocol (No Authentication mode).
92async fn dial_socks5_proxy(
93    proxy_addr: &str,
94    target_host: &str,
95    target_port: u16,
96) -> Result<TcpStream> {
97    let mut stream = TcpStream::connect(proxy_addr).await?;
98
99    // 1. Initial Greeting: Version 5, 1 supported method, No Authentication (0x00).
100    stream.write_all(&[0x05, 0x01, 0x00]).await?;
101
102    let mut response = [0; 2];
103    stream.read_exact(&mut response).await?;
104
105    if response[0] != 0x05 || response[1] != 0x00 {
106        return Err(Error::Connect(std::io::Error::other(
107            "SOCKS5 server rejected 'No Authentication' method",
108        )));
109    }
110
111    // 2. Connection Request: Version 5, Connect (0x01), Reserved (0x00), Domain (0x03).
112    let host_bytes = target_host.as_bytes();
113    let mut req = vec![0x05, 0x01, 0x00, 0x03, host_bytes.len() as u8];
114    req.extend_from_slice(host_bytes);
115    req.extend_from_slice(&target_port.to_be_bytes());
116
117    stream.write_all(&req).await?;
118
119    // 3. Response Header verification.
120    let mut resp_header = [0; 4];
121    stream.read_exact(&mut resp_header).await?;
122
123    if resp_header[0] != 0x05 || resp_header[1] != 0x00 {
124        return Err(Error::Connect(std::io::Error::other(format!(
125            "SOCKS5 connection request failed with status code: {}",
126            resp_header[1]
127        ))));
128    }
129
130    // Drain the bound address and port (variable length).
131    let addr_type = resp_header[3];
132    match addr_type {
133        0x01 => {
134            let mut addr = [0; 4];
135            stream.read_exact(&mut addr).await?;
136        }
137        0x03 => {
138            let mut len = [0; 1];
139            stream.read_exact(&mut len).await?;
140            let mut domain = vec![0; len[0] as usize];
141            stream.read_exact(&mut domain).await?;
142        }
143        0x04 => {
144            let mut addr = [0; 16];
145            stream.read_exact(&mut addr).await?;
146        }
147        _ => {
148            return Err(Error::Connect(std::io::Error::other(
149                "SOCKS5 server returned unsupported address type",
150            )))
151        }
152    }
153
154    let mut port = [0; 2];
155    stream.read_exact(&mut port).await?;
156
157    Ok(stream)
158}