Skip to main content

eggress_protocol_http/connect/
server.rs

1use std::net::IpAddr;
2
3use base64::Engine;
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5
6use crate::error::HttpError;
7use eggress_core::{BoxStream, TargetAddr, TargetHost};
8
9/// Maximum size for the HTTP request head (request line + headers).
10const MAX_HEAD_SIZE: usize = 32 * 1024;
11
12/// Maximum number of header lines.
13const MAX_HEADER_LINES: usize = 128;
14
15/// Parsed CONNECT request.
16#[derive(Debug, Clone)]
17pub struct ConnectRequest {
18    pub target: TargetAddr,
19    pub proxy_auth: Option<(String, String)>,
20}
21
22/// Handle an HTTP CONNECT request from a client stream.
23///
24/// Parses the CONNECT request, validates it, and returns the stream
25/// ready for bidirectional forwarding after sending a 200 response.
26///
27/// # Arguments
28/// * `stream` - The client stream to read the CONNECT request from
29/// * `require_auth` - Whether proxy authentication is required
30/// * `valid_credentials` - Valid (username, password) pair for auth validation
31///
32/// # Returns
33/// The parsed CONNECT request and the stream (with any bytes after the
34/// request head preserved).
35pub async fn handle_connect(
36    stream: BoxStream,
37    require_auth: bool,
38    valid_credentials: Option<(&str, &str)>,
39) -> Result<(ConnectRequest, BoxStream), HttpError> {
40    // Buffer reads so the incremental head parse does not issue one
41    // syscall per byte; unconsumed prefetch stays available to later
42    // reads on the returned stream.
43    let mut stream: BoxStream = Box::new(tokio::io::BufReader::new(stream));
44    let request = read_connect_request(&mut stream).await?;
45
46    // Validate authentication if required
47    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    // Send success response
72    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
80/// Read and parse an HTTP CONNECT request from the stream.
81async fn read_connect_request(stream: &mut BoxStream) -> Result<ConnectRequest, HttpError> {
82    // NOTE (O-02): single-byte `read` calls here are served from the
83    // `BufReader` installed by `handle_connect`, not as one syscall per byte,
84    // and stopping exactly at `\r\n\r\n` preserves pipelined post-head bytes.
85    // A chunked rewrite must use fill_buf/consume semantics to avoid
86    // over-reading into the tunneled body; left as-is deliberately.
87    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        // Check for end of headers (\r\n\r\n)
107        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            // Also count individual \r\n for header line limits
113            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    // Parse request line
130    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    // Parse authority (host:port)
154    let authority = parts[1];
155    let target = parse_authority(authority)?;
156
157    // Parse headers
158    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
173/// Parse an authority-form target (host:port).
174/// Parse an authority string (`host:port` or `[ipv6]:port`) into a [`TargetAddr`].
175///
176/// Exposed for fuzzing. Returns [`HttpError::TargetParseError`] on malformed input.
177pub fn parse_authority(authority: &str) -> Result<TargetAddr, HttpError> {
178    // Handle IPv6 bracketed addresses: [::1]:port
179    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    // Handle IPv4 or domain.
214    // A missing port implies the default HTTPS port for CONNECT targets
215    // (RFC 9110 ยง9.3.6: the authority carries host[:port]).
216    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    // Try to parse as IP first
228    if let Ok(ip) = host_str.parse::<IpAddr>() {
229        return Ok(TargetAddr {
230            host: TargetHost::Ip(ip),
231            port,
232        });
233    }
234
235    // Otherwise treat as domain
236    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
246/// Parse a header line into (name, value).
247///
248/// Exposed for fuzzing. Returns `None` if the line lacks a colon.
249pub 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
256/// Parse Basic authentication from a Proxy-Authorization header value.
257///
258/// Exposed for fuzzing. Returns `None` if the value is not a Basic auth header.
259pub 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
276/// Write an HTTP error response.
277async 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        // CONNECT without an explicit port implies the default HTTPS port.
334        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        // "user:pass" base64 encoded is "dXNlcjpwYXNz"
362        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            // Send a request line followed by headers that exceed MAX_HEAD_SIZE
425            let mut payload = b"CONNECT example.com:443 HTTP/1.1\r\n".to_vec();
426            // Add headers until we exceed the limit
427            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            // Keep connection alive briefly
434            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        // The server should reject or the client should see an error
440        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            // Send CONNECT with more than MAX_HEADER_LINES (128) header lines
454            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}