microsandbox_network/proxy/
http_connect.rs1use 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#[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#[derive(Debug, Clone)]
33pub struct HttpConnectProxyBuilder {
34 address: String,
35}
36
37#[cfg(feature = "engine")]
39pub(super) struct HttpConnectProtocol;
40
41impl HttpConnectProxyBuilder {
46 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 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
166impl 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}