eggress_protocol_http/connect/
server.rs1use std::net::IpAddr;
2
3use base64::Engine;
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5
6use crate::error::HttpError;
7use eggress_core::{BoxStream, TargetAddr, TargetHost};
8
9const MAX_HEAD_SIZE: usize = 32 * 1024;
11
12const MAX_HEADER_LINES: usize = 128;
14
15#[derive(Debug, Clone)]
17pub struct ConnectRequest {
18 pub target: TargetAddr,
19 pub proxy_auth: Option<(String, String)>,
20}
21
22pub async fn handle_connect(
36 stream: BoxStream,
37 require_auth: bool,
38 valid_credentials: Option<(&str, &str)>,
39) -> Result<(ConnectRequest, BoxStream), HttpError> {
40 let mut stream: BoxStream = Box::new(tokio::io::BufReader::new(stream));
44 let request = read_connect_request(&mut stream).await?;
45
46 if require_auth {
48 match &request.proxy_auth {
49 Some((user, pass)) => {
50 if let Some((valid_user, valid_pass)) = valid_credentials {
51 use subtle::ConstantTimeEq;
52 let user_ok: bool = user.as_bytes().ct_eq(valid_user.as_bytes()).into();
53 let pass_ok: bool = pass.as_bytes().ct_eq(valid_pass.as_bytes()).into();
54 if !user_ok || !pass_ok {
55 write_error_response(&mut stream, 407, "Proxy Authentication Required")
56 .await?;
57 return Err(HttpError::AuthRequired);
58 }
59 } else {
60 write_error_response(&mut stream, 407, "Proxy Authentication Required").await?;
61 return Err(HttpError::AuthRequired);
62 }
63 }
64 None => {
65 write_error_response(&mut stream, 407, "Proxy Authentication Required").await?;
66 return Err(HttpError::AuthRequired);
67 }
68 }
69 }
70
71 stream
73 .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
74 .await?;
75 stream.flush().await?;
76
77 Ok((request, stream))
78}
79
80async fn read_connect_request(stream: &mut BoxStream) -> Result<ConnectRequest, HttpError> {
82 let mut head_buf = Vec::with_capacity(1024);
88 let mut temp = [0u8; 1];
89 let mut header_count = 0;
90 let mut saw_request_line = false;
91
92 loop {
93 if head_buf.len() >= MAX_HEAD_SIZE {
94 return Err(HttpError::HeaderTooLarge);
95 }
96
97 let n = stream.read(&mut temp).await?;
98 if n == 0 {
99 return Err(HttpError::MalformedRequest(
100 "unexpected EOF reading request".into(),
101 ));
102 }
103
104 head_buf.push(temp[0]);
105
106 if head_buf.len() >= 4 {
108 let len = head_buf.len();
109 if &head_buf[len - 4..] == b"\r\n\r\n" {
110 break;
111 }
112 if head_buf.len() >= 2 && &head_buf[len - 2..] == b"\r\n" {
114 if saw_request_line {
115 header_count += 1;
116 } else {
117 saw_request_line = true;
118 }
119 if header_count > MAX_HEADER_LINES {
120 return Err(HttpError::TooManyHeaders);
121 }
122 }
123 }
124 }
125
126 let head_str = String::from_utf8_lossy(&head_buf);
127 let mut lines = head_str.split("\r\n");
128
129 let request_line = lines
131 .next()
132 .ok_or_else(|| HttpError::MalformedRequest("empty request".into()))?;
133
134 let parts: Vec<&str> = request_line.split_whitespace().collect();
135 if parts.len() != 3 {
136 return Err(HttpError::MalformedRequest(format!(
137 "expected 3 parts in request line, got {}",
138 parts.len()
139 )));
140 }
141
142 if parts[0] != "CONNECT" {
143 return Err(HttpError::MalformedRequest(format!(
144 "expected CONNECT method, got {}",
145 parts[0]
146 )));
147 }
148
149 if parts[2] != "HTTP/1.1" && parts[2] != "HTTP/1.0" {
150 return Err(HttpError::UnsupportedVersion(parts[2].to_string()));
151 }
152
153 let authority = parts[1];
155 let target = parse_authority(authority)?;
156
157 let mut proxy_auth = None;
159 for line in lines {
160 if line.is_empty() {
161 break;
162 }
163 if let Some((name, value)) = parse_header_line(line) {
164 if name.eq_ignore_ascii_case("Proxy-Authorization") {
165 proxy_auth = parse_basic_auth(&value);
166 }
167 }
168 }
169
170 Ok(ConnectRequest { target, proxy_auth })
171}
172
173pub fn parse_authority(authority: &str) -> Result<TargetAddr, HttpError> {
178 if authority.starts_with('[') {
180 let bracket_end = authority.find(']').ok_or_else(|| {
181 HttpError::TargetParseError("unclosed bracket in IPv6 address".into())
182 })?;
183
184 let ip_str = &authority[1..bracket_end];
185 let ip: IpAddr = ip_str
186 .parse()
187 .map_err(|e| HttpError::TargetParseError(format!("invalid IPv6 address: {}", e)))?;
188
189 let port_str = authority
190 .get(bracket_end + 2..)
191 .ok_or_else(|| HttpError::TargetParseError("missing port after IPv6 address".into()))?;
192
193 if authority
194 .as_bytes()
195 .get(bracket_end + 1)
196 .is_none_or(|&b| b != b':')
197 {
198 return Err(HttpError::TargetParseError(
199 "expected ':' between IPv6 address and port".into(),
200 ));
201 }
202
203 let port: u16 = port_str
204 .parse()
205 .map_err(|e| HttpError::TargetParseError(format!("invalid port: {}", e)))?;
206
207 return Ok(TargetAddr {
208 host: TargetHost::Ip(ip),
209 port,
210 });
211 }
212
213 const DEFAULT_CONNECT_PORT: u16 = 443;
217 let (host_str, port) = match authority.rfind(':') {
218 Some(colon_pos) => {
219 let port: u16 = authority[colon_pos + 1..]
220 .parse()
221 .map_err(|e| HttpError::TargetParseError(format!("invalid port: {}", e)))?;
222 (&authority[..colon_pos], port)
223 }
224 None => (authority, DEFAULT_CONNECT_PORT),
225 };
226
227 if let Ok(ip) = host_str.parse::<IpAddr>() {
229 return Ok(TargetAddr {
230 host: TargetHost::Ip(ip),
231 port,
232 });
233 }
234
235 if host_str.is_empty() {
237 return Err(HttpError::TargetParseError("empty host".into()));
238 }
239
240 Ok(TargetAddr {
241 host: TargetHost::Domain(host_str.to_string()),
242 port,
243 })
244}
245
246pub fn parse_header_line(line: &str) -> Option<(String, String)> {
250 let colon_pos = line.find(':')?;
251 let name = line[..colon_pos].trim().to_string();
252 let value = line[colon_pos + 1..].trim().to_string();
253 Some((name, value))
254}
255
256pub fn parse_basic_auth(value: &str) -> Option<(String, String)> {
260 let value = value.trim();
261 if !value.starts_with("Basic ") {
262 return None;
263 }
264
265 let encoded = &value[6..];
266 let decoded = base64::engine::general_purpose::STANDARD
267 .decode(encoded)
268 .ok()?;
269 let decoded_str = String::from_utf8(decoded).ok()?;
270 let colon_pos = decoded_str.find(':')?;
271 let username = decoded_str[..colon_pos].to_string();
272 let password = decoded_str[colon_pos + 1..].to_string();
273 Some((username, password))
274}
275
276async fn write_error_response(
278 stream: &mut BoxStream,
279 status: u16,
280 reason: &str,
281) -> Result<(), HttpError> {
282 let response = format!(
283 "HTTP/1.1 {} {}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
284 status, reason
285 );
286 stream.write_all(response.as_bytes()).await?;
287 stream.flush().await?;
288 Ok(())
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_parse_authority_ipv4() {
297 let target = parse_authority("192.168.1.1:8080").unwrap();
298 assert_eq!(
299 target,
300 TargetAddr {
301 host: TargetHost::Ip("192.168.1.1".parse().unwrap()),
302 port: 8080,
303 }
304 );
305 }
306
307 #[test]
308 fn test_parse_authority_ipv6() {
309 let target = parse_authority("[::1]:443").unwrap();
310 assert_eq!(
311 target,
312 TargetAddr {
313 host: TargetHost::Ip("::1".parse().unwrap()),
314 port: 443,
315 }
316 );
317 }
318
319 #[test]
320 fn test_parse_authority_domain() {
321 let target = parse_authority("example.com:443").unwrap();
322 assert_eq!(
323 target,
324 TargetAddr {
325 host: TargetHost::Domain("example.com".to_string()),
326 port: 443,
327 }
328 );
329 }
330
331 #[test]
332 fn test_parse_authority_missing_port_implies_default() {
333 let target = parse_authority("example.com").unwrap();
335 assert_eq!(
336 target,
337 TargetAddr {
338 host: TargetHost::Domain("example.com".to_string()),
339 port: 443,
340 }
341 );
342 let target = parse_authority("192.168.1.1").unwrap();
343 assert_eq!(
344 target,
345 TargetAddr {
346 host: TargetHost::Ip("192.168.1.1".parse().unwrap()),
347 port: 443,
348 }
349 );
350 }
351
352 #[test]
353 fn test_parse_header_line() {
354 let (name, value) = parse_header_line("Host: example.com").unwrap();
355 assert_eq!(name, "Host");
356 assert_eq!(value, "example.com");
357 }
358
359 #[test]
360 fn test_parse_basic_auth() {
361 let result = parse_basic_auth("Basic dXNlcjpwYXNz").unwrap();
363 assert_eq!(result, ("user".to_string(), "pass".to_string()));
364 }
365
366 #[test]
367 fn test_parse_basic_auth_no_prefix() {
368 assert!(parse_basic_auth("Bearer token").is_none());
369 }
370
371 #[test]
372 fn test_base64_decode() {
373 let decoded = base64::engine::general_purpose::STANDARD
374 .decode("dGVzdA==")
375 .unwrap();
376 assert_eq!(decoded, b"test");
377 }
378
379 #[test]
380 fn test_max_head_size_enforced() {
381 assert_eq!(MAX_HEAD_SIZE, 32 * 1024);
382 assert_eq!(MAX_HEADER_LINES, 128);
383 }
384
385 #[test]
386 fn test_parse_authority_empty_string() {
387 assert!(parse_authority("").is_err());
388 }
389
390 #[test]
391 fn test_parse_authority_empty_host_with_port() {
392 assert!(parse_authority(":80").is_err());
393 assert!(parse_authority("").is_err());
394 }
395
396 #[test]
397 fn test_parse_header_line_no_colon() {
398 assert!(parse_header_line("no-colon-here").is_none());
399 }
400
401 #[test]
402 fn test_parse_header_line_empty() {
403 assert!(parse_header_line("").is_none());
404 }
405
406 #[test]
407 fn test_parse_basic_auth_not_basic() {
408 assert!(parse_basic_auth("Bearer token123").is_none());
409 }
410
411 #[test]
412 fn test_parse_basic_auth_invalid_base64() {
413 assert!(parse_basic_auth("Basic !!!invalid!!!").is_none());
414 }
415
416 #[tokio::test]
417 async fn test_head_too_large_rejected() {
418 use tokio::io::{AsyncReadExt, AsyncWriteExt};
419
420 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
421 let addr = listener.local_addr().unwrap();
422 let jh = tokio::spawn(async move {
423 let (mut stream, _) = listener.accept().await.unwrap();
424 let mut payload = b"CONNECT example.com:443 HTTP/1.1\r\n".to_vec();
426 let header_line = b"X-Pad: AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n";
428 while payload.len() < MAX_HEAD_SIZE + header_line.len() {
429 payload.extend_from_slice(header_line);
430 }
431 payload.extend_from_slice(b"\r\n");
432 let _ = stream.write_all(&payload).await;
433 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
435 });
436
437 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
438 let mut buf = vec![0u8; 4096];
439 let _ =
441 tokio::time::timeout(std::time::Duration::from_secs(2), stream.read(&mut buf)).await;
442 jh.abort();
443 }
444
445 #[tokio::test]
446 async fn test_too_many_header_lines_rejected() {
447 use tokio::io::{AsyncReadExt, AsyncWriteExt};
448
449 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
450 let addr = listener.local_addr().unwrap();
451 let jh = tokio::spawn(async move {
452 let (mut stream, _) = listener.accept().await.unwrap();
453 let mut payload = b"CONNECT example.com:443 HTTP/1.1\r\n".to_vec();
455 for i in 0..=MAX_HEADER_LINES + 1 {
456 payload.extend_from_slice(format!("X-Header-{i}: value\r\n").as_bytes());
457 }
458 payload.extend_from_slice(b"\r\n");
459 let _ = stream.write_all(&payload).await;
460 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
461 });
462
463 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
464 let mut buf = vec![0u8; 4096];
465 let _ =
466 tokio::time::timeout(std::time::Duration::from_secs(2), stream.read(&mut buf)).await;
467 jh.abort();
468 }
469}