Skip to main content

microsandbox_network/proxy/
http_connect.rs

1//! HTTP CONNECT outbound proxy builders and transport.
2
3use std::net::AddrParseError;
4#[cfg(feature = "engine")]
5use std::{io, net::SocketAddr, time::Duration};
6
7#[cfg(feature = "engine")]
8use tokio::{
9    io::{AsyncReadExt, AsyncWriteExt},
10    net::TcpStream,
11    time::timeout,
12};
13
14use super::types::{
15    OutboundProxy, OutboundProxyBuildError, OutboundProxyConfig, OutboundProxyProtocol,
16};
17
18//--------------------------------------------------------------------------------------------------
19// Constants
20//--------------------------------------------------------------------------------------------------
21
22#[cfg(feature = "engine")]
23const CONNECT_RESPONSE_HEADER_LIMIT: usize = 8192;
24#[cfg(feature = "engine")]
25const CONNECT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(10);
26
27//--------------------------------------------------------------------------------------------------
28// Types
29//--------------------------------------------------------------------------------------------------
30
31/// Builds an HTTP CONNECT outbound proxy.
32#[derive(Debug, Clone)]
33pub struct HttpConnectProxyBuilder {
34    address: String,
35}
36
37/// HTTP CONNECT wire protocol operations.
38#[cfg(feature = "engine")]
39pub(super) struct HttpConnectProtocol;
40
41//--------------------------------------------------------------------------------------------------
42// Methods
43//--------------------------------------------------------------------------------------------------
44
45impl HttpConnectProxyBuilder {
46    /// Creates a builder for the configured proxy address.
47    pub(super) fn new(address: impl Into<String>) -> Self {
48        Self {
49            address: address.into(),
50        }
51    }
52}
53
54#[cfg(feature = "engine")]
55impl HttpConnectProtocol {
56    /// Opens a TCP tunnel through the configured HTTP proxy.
57    pub(super) async fn connect(
58        address: SocketAddr,
59        destination: SocketAddr,
60    ) -> io::Result<TcpStream> {
61        let mut stream = TcpStream::connect(address).await?;
62        let authority = destination.to_string();
63        let request = format!("CONNECT {authority} HTTP/1.1\r\nHost: {authority}\r\n\r\n");
64
65        stream.write_all(request.as_bytes()).await?;
66        stream.flush().await?;
67
68        let status = timeout(
69            CONNECT_RESPONSE_TIMEOUT,
70            Self::read_connect_response_status(&mut stream),
71        )
72        .await
73        .map_err(|_| {
74            io::Error::new(
75                io::ErrorKind::TimedOut,
76                "HTTP CONNECT proxy did not complete its response headers within 10 seconds",
77            )
78        })??;
79
80        if (200..300).contains(&status) {
81            return Ok(stream);
82        }
83
84        let kind = if status == 407 {
85            io::ErrorKind::PermissionDenied
86        } else {
87            io::ErrorKind::ConnectionRefused
88        };
89
90        Err(io::Error::new(
91            kind,
92            format!("HTTP CONNECT proxy rejected the tunnel with status {status}"),
93        ))
94    }
95
96    async fn read_connect_response_status(stream: &mut TcpStream) -> io::Result<u16> {
97        let mut total_header_bytes = 0;
98
99        loop {
100            let mut header = Vec::with_capacity(256);
101            loop {
102                if total_header_bytes >= CONNECT_RESPONSE_HEADER_LIMIT {
103                    return Err(io::Error::new(
104                        io::ErrorKind::InvalidData,
105                        "HTTP CONNECT proxy response headers exceeded 8192 bytes",
106                    ));
107                }
108
109                header.push(stream.read_u8().await?);
110                total_header_bytes += 1;
111                if header.ends_with(b"\r\n\r\n") {
112                    break;
113                }
114            }
115
116            let status_end = header
117                .windows(2)
118                .position(|window| window == b"\r\n")
119                .ok_or_else(|| {
120                    io::Error::new(
121                        io::ErrorKind::InvalidData,
122                        "HTTP CONNECT proxy returned an invalid status line",
123                    )
124                })?;
125            let status_line = std::str::from_utf8(&header[..status_end]).map_err(|_| {
126                io::Error::new(
127                    io::ErrorKind::InvalidData,
128                    "HTTP CONNECT proxy returned a non-ASCII status line",
129                )
130            })?;
131            let mut fields = status_line.split_ascii_whitespace();
132            let version = fields.next().unwrap_or_default();
133
134            if version != "HTTP/1.0" && version != "HTTP/1.1" {
135                return Err(io::Error::new(
136                    io::ErrorKind::InvalidData,
137                    "HTTP CONNECT proxy returned an invalid HTTP status line",
138                ));
139            }
140
141            let status = fields
142                .next()
143                .ok_or_else(|| {
144                    io::Error::new(
145                        io::ErrorKind::InvalidData,
146                        "HTTP CONNECT proxy returned a status line without a status code",
147                    )
148                })?
149                .parse::<u16>()
150                .map_err(|_| {
151                    io::Error::new(
152                        io::ErrorKind::InvalidData,
153                        "HTTP CONNECT proxy returned an invalid status code",
154                    )
155                })?;
156
157            if (100..200).contains(&status) && status != 101 {
158                continue;
159            }
160
161            return Ok(status);
162        }
163    }
164}
165
166//--------------------------------------------------------------------------------------------------
167// Trait Implementations
168//--------------------------------------------------------------------------------------------------
169
170impl OutboundProxyConfig for HttpConnectProxyBuilder {
171    fn build(self) -> Result<OutboundProxy, OutboundProxyBuildError> {
172        let address = self.address.parse().map_err(|source: AddrParseError| {
173            OutboundProxyBuildError::InvalidAddress {
174                protocol: OutboundProxyProtocol::HttpConnect,
175                address: self.address,
176                source,
177            }
178        })?;
179
180        Ok(OutboundProxy::HttpConnect { address })
181    }
182}