Skip to main content

proxychains_masq/proxy/
http.rs

1use anyhow::{bail, Context, Result};
2use tokio::io::{AsyncReadExt, AsyncWriteExt};
3
4use super::{BoxStream, Target};
5
6/// Perform an HTTP CONNECT tunnel handshake on an already-connected `stream`.
7///
8/// If `username` and `password` are provided, a `Proxy-Authorization: Basic`
9/// header is included.
10///
11/// # Errors
12///
13/// Returns an error if the proxy responds with a non-2xx status code or the
14/// response headers cannot be parsed.
15pub async fn connect(
16    mut stream: BoxStream,
17    target: &Target,
18    username: Option<&str>,
19    password: Option<&str>,
20) -> Result<BoxStream> {
21    let host_port = match target {
22        Target::Ip(addr, port) => format!("{addr}:{port}"),
23        Target::Host(host, port) => format!("{host}:{port}"),
24    };
25
26    let mut req = format!("CONNECT {host_port} HTTP/1.0\r\nHost: {host_port}\r\n");
27
28    if let (Some(u), Some(p)) = (username, password) {
29        let credentials = base64_encode(&format!("{u}:{p}"));
30        req.push_str(&format!("Proxy-Authorization: Basic {credentials}\r\n"));
31    }
32    req.push_str("\r\n");
33
34    stream
35        .write_all(req.as_bytes())
36        .await
37        .context("http: write CONNECT")?;
38
39    // Read response headers byte-by-byte until \r\n\r\n
40    let mut response = Vec::<u8>::with_capacity(256);
41    loop {
42        let b = stream.read_u8().await.context("http: read response")?;
43        response.push(b);
44        if response.ends_with(b"\r\n\r\n") {
45            break;
46        }
47        if response.len() > 8192 {
48            bail!("http: response headers too large");
49        }
50    }
51
52    let header_line = response
53        .split(|&b| b == b'\n')
54        .next()
55        .context("http: empty response")?;
56    let header_str = std::str::from_utf8(header_line)
57        .context("http: non-UTF-8 status line")?
58        .trim();
59
60    // HTTP/1.x 2xx ...
61    let status_code: u16 = header_str
62        .split_whitespace()
63        .nth(1)
64        .context("http: missing status code")?
65        .parse()
66        .context("http: invalid status code")?;
67
68    if !(200..300).contains(&status_code) {
69        bail!("http: CONNECT failed with status {status_code}");
70    }
71
72    Ok(stream)
73}
74
75fn base64_encode(s: &str) -> String {
76    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
77    let input = s.as_bytes();
78    let mut out = String::new();
79    for chunk in input.chunks(3) {
80        let b0 = chunk[0] as u32;
81        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
82        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
83        let n = (b0 << 16) | (b1 << 8) | b2;
84        out.push(CHARS[((n >> 18) & 0x3f) as usize] as char);
85        out.push(CHARS[((n >> 12) & 0x3f) as usize] as char);
86        out.push(if chunk.len() > 1 {
87            CHARS[((n >> 6) & 0x3f) as usize] as char
88        } else {
89            '='
90        });
91        out.push(if chunk.len() > 2 {
92            CHARS[(n & 0x3f) as usize] as char
93        } else {
94            '='
95        });
96    }
97    out
98}