Skip to main content

hns_browser_loopback_proxy/chromium/
http1.rs

1//! Strict, bounded HTTP/1 request parsing for the loopback proxy.
2//!
3//! This module deliberately reads request heads one byte at a time. A blocking
4//! socket may deliver the head and body together, and consuming past the first
5//! `CRLFCRLF` would otherwise require a second buffering abstraction at every
6//! call site.
7
8use std::collections::HashSet;
9use std::fmt;
10use std::io::{self, Read};
11use std::net::{Ipv4Addr, Ipv6Addr};
12
13use thiserror::Error;
14
15pub const DEFAULT_MAX_REQUEST_HEAD_BYTES: usize = 64 * 1024;
16pub const DEFAULT_MAX_REQUEST_BODY_BYTES: u64 = 1024 * 1024;
17pub const MAX_REQUEST_HEADERS: usize = 256;
18pub const MAX_CHUNK_LINE_BYTES: usize = 8 * 1024;
19pub const MAX_CHUNK_TRAILER_BYTES: usize = 64 * 1024;
20
21const HEAD_END: &[u8; 4] = b"\r\n\r\n";
22
23#[derive(Debug, Error)]
24pub enum Http1Error {
25    #[error("request I/O failed")]
26    Io(#[source] io::Error),
27    #[error("request ended before it was complete")]
28    UnexpectedEof,
29    #[error("request head is too large")]
30    HeadTooLarge,
31    #[error("request has too many headers")]
32    TooManyHeaders,
33    #[error("invalid HTTP request line")]
34    InvalidRequestLine,
35    #[error("unsupported HTTP version")]
36    UnsupportedHttpVersion,
37    #[error("invalid HTTP request header")]
38    InvalidHeader,
39    #[error("duplicate Proxy-Authorization header")]
40    DuplicateProxyAuthorization,
41    #[error("invalid request target")]
42    InvalidTarget,
43    #[error("invalid request authority")]
44    InvalidAuthority,
45    #[error("request target does not match the CONNECT authority")]
46    TargetMismatch,
47    #[error("Host header is required")]
48    MissingHost,
49    #[error("Host header does not match the request target")]
50    HostMismatch,
51    #[error("invalid or duplicate Content-Length")]
52    InvalidContentLength,
53    #[error("ambiguous HTTP request body framing")]
54    AmbiguousFraming,
55    #[error("unsupported Transfer-Encoding")]
56    UnsupportedTransferEncoding,
57    #[error("request body is too large")]
58    BodyTooLarge,
59    #[error("request body ended before it was complete")]
60    UnexpectedBodyEof,
61    #[error("invalid chunked request body")]
62    InvalidChunkedBody,
63    #[error("chunked request trailers are too large")]
64    TrailersTooLarge,
65}
66
67impl Http1Error {
68    #[must_use]
69    pub const fn status_code(&self) -> u16 {
70        match self {
71            Self::HeadTooLarge | Self::TooManyHeaders => 431,
72            Self::BodyTooLarge => 413,
73            Self::UnsupportedTransferEncoding => 501,
74            _ => 400,
75        }
76    }
77
78    #[must_use]
79    pub const fn reason_phrase(&self) -> &'static str {
80        match self {
81            Self::HeadTooLarge | Self::TooManyHeaders => "Request Header Fields Too Large",
82            Self::BodyTooLarge => "Payload Too Large",
83            Self::UnsupportedTransferEncoding => "Transfer Encoding Unsupported",
84            Self::InvalidContentLength => "Bad Content-Length",
85            Self::AmbiguousFraming => "Bad Request Framing",
86            Self::MissingHost | Self::HostMismatch => "HNS Host Header Mismatch",
87            Self::TargetMismatch => "HNS Request Mismatch",
88            _ => "Bad Request",
89        }
90    }
91}
92
93impl From<io::Error> for Http1Error {
94    fn from(error: io::Error) -> Self {
95        Self::Io(error)
96    }
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub enum HttpVersion {
101    Http10,
102    Http11,
103}
104
105impl HttpVersion {
106    #[must_use]
107    pub const fn as_str(self) -> &'static str {
108        match self {
109            Self::Http10 => "HTTP/1.0",
110            Self::Http11 => "HTTP/1.1",
111        }
112    }
113}
114
115#[derive(Clone, Eq, PartialEq)]
116pub struct Header {
117    name: String,
118    value: String,
119}
120
121impl Header {
122    #[must_use]
123    pub fn name(&self) -> &str {
124        &self.name
125    }
126
127    #[must_use]
128    pub fn value(&self) -> &str {
129        &self.value
130    }
131}
132
133impl fmt::Debug for Header {
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        formatter
136            .debug_struct("Header")
137            .field("name", &self.name)
138            .field("value", &"<redacted>")
139            .field("value_len", &self.value.len())
140            .finish()
141    }
142}
143
144#[derive(Clone, Eq, PartialEq)]
145pub struct RequestHead {
146    method: String,
147    target: String,
148    version: HttpVersion,
149    headers: Vec<Header>,
150}
151
152impl RequestHead {
153    #[must_use]
154    pub fn method(&self) -> &str {
155        &self.method
156    }
157
158    #[must_use]
159    pub fn raw_target(&self) -> &str {
160        &self.target
161    }
162
163    #[must_use]
164    pub const fn version(&self) -> HttpVersion {
165        self.version
166    }
167
168    #[must_use]
169    pub fn headers(&self) -> &[Header] {
170        &self.headers
171    }
172
173    pub fn header_values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
174        self.headers
175            .iter()
176            .filter(move |header| header.name.eq_ignore_ascii_case(name))
177            .map(|header| header.value.as_str())
178    }
179
180    /// Returns the sole proxy credential field without logging or decoding it.
181    pub fn proxy_authorization(&self) -> Result<Option<&str>, Http1Error> {
182        let mut values = self.header_values("proxy-authorization");
183        let first = values.next();
184        if values.next().is_some() {
185            return Err(Http1Error::DuplicateProxyAuthorization);
186        }
187        Ok(first)
188    }
189
190    pub fn host_authority(&self, default_port: u16) -> Result<Option<Authority>, Http1Error> {
191        let mut values = self.header_values("host");
192        let Some(value) = values.next() else {
193            return Ok(None);
194        };
195        if values.next().is_some() {
196            return Err(Http1Error::HostMismatch);
197        }
198        Authority::parse(value, default_port)
199            .map(Some)
200            .map_err(|_| Http1Error::HostMismatch)
201    }
202
203    /// Parses the request target in direct-proxy or post-CONNECT context.
204    pub fn request_target(
205        &self,
206        connected_to: Option<&Authority>,
207    ) -> Result<RequestTarget, Http1Error> {
208        parse_request_target(&self.method, &self.target, connected_to)
209    }
210
211    /// Parses the target and verifies the Host field against its authority.
212    pub fn validated_target(
213        &self,
214        connected_to: Option<&Authority>,
215    ) -> Result<RequestTarget, Http1Error> {
216        let target = self.request_target(connected_to)?;
217        let (expected, default_port) = match &target {
218            RequestTarget::Absolute(target) => (&target.authority, target.scheme.default_port()),
219            RequestTarget::Connect(authority) => (authority, authority.port),
220            RequestTarget::Origin(target) => (&target.authority, 443),
221        };
222        match self.host_authority(default_port)? {
223            Some(host) if host == *expected => Ok(target),
224            Some(_) => Err(Http1Error::HostMismatch),
225            None if self.version == HttpVersion::Http10 => Ok(target),
226            None => Err(Http1Error::MissingHost),
227        }
228    }
229}
230
231impl fmt::Debug for RequestHead {
232    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
233        formatter
234            .debug_struct("RequestHead")
235            .field("method", &self.method)
236            .field("target", &"<redacted>")
237            .field("version", &self.version)
238            .field("header_count", &self.headers.len())
239            .finish()
240    }
241}
242
243#[derive(Clone, Eq, PartialEq)]
244pub struct Authority {
245    host: String,
246    port: u16,
247}
248
249impl Authority {
250    pub fn parse(value: &str, default_port: u16) -> Result<Self, Http1Error> {
251        parse_authority(value, Some(default_port), false)
252    }
253
254    #[must_use]
255    pub fn host(&self) -> &str {
256        &self.host
257    }
258
259    #[must_use]
260    pub const fn port(&self) -> u16 {
261        self.port
262    }
263
264    #[must_use]
265    pub fn host_header(&self, default_port: u16) -> String {
266        let host = if self.host.contains(':') {
267            format!("[{}]", self.host)
268        } else {
269            self.host.clone()
270        };
271        if self.port == default_port {
272            host
273        } else {
274            format!("{host}:{}", self.port)
275        }
276    }
277}
278
279impl fmt::Debug for Authority {
280    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
281        formatter
282            .debug_struct("Authority")
283            .field("host", &"<redacted>")
284            .field("port", &self.port)
285            .finish()
286    }
287}
288
289#[derive(Clone, Copy, Debug, Eq, PartialEq)]
290pub enum Scheme {
291    Http,
292    Https,
293    Ws,
294    Wss,
295}
296
297impl Scheme {
298    #[must_use]
299    pub const fn as_str(self) -> &'static str {
300        match self {
301            Self::Http => "http",
302            Self::Https => "https",
303            Self::Ws => "ws",
304            Self::Wss => "wss",
305        }
306    }
307
308    #[must_use]
309    pub const fn default_port(self) -> u16 {
310        match self {
311            Self::Http | Self::Ws => 80,
312            Self::Https | Self::Wss => 443,
313        }
314    }
315
316    #[must_use]
317    pub const fn is_secure(self) -> bool {
318        matches!(self, Self::Https | Self::Wss)
319    }
320}
321
322#[derive(Clone, Eq, PartialEq)]
323pub struct AbsoluteTarget {
324    scheme: Scheme,
325    authority: Authority,
326    path_and_query: String,
327}
328
329impl AbsoluteTarget {
330    #[must_use]
331    pub const fn scheme(&self) -> Scheme {
332        self.scheme
333    }
334
335    #[must_use]
336    pub const fn authority(&self) -> &Authority {
337        &self.authority
338    }
339
340    #[must_use]
341    pub fn path_and_query(&self) -> &str {
342        &self.path_and_query
343    }
344}
345
346impl fmt::Debug for AbsoluteTarget {
347    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
348        formatter
349            .debug_struct("AbsoluteTarget")
350            .field("scheme", &self.scheme)
351            .field("authority", &self.authority)
352            .field("path_and_query", &"<redacted>")
353            .finish()
354    }
355}
356
357#[derive(Clone, Eq, PartialEq)]
358pub struct OriginTarget {
359    authority: Authority,
360    path_and_query: String,
361}
362
363impl OriginTarget {
364    #[must_use]
365    pub const fn authority(&self) -> &Authority {
366        &self.authority
367    }
368
369    #[must_use]
370    pub fn path_and_query(&self) -> &str {
371        &self.path_and_query
372    }
373}
374
375impl fmt::Debug for OriginTarget {
376    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
377        formatter
378            .debug_struct("OriginTarget")
379            .field("authority", &self.authority)
380            .field("path_and_query", &"<redacted>")
381            .finish()
382    }
383}
384
385#[derive(Clone, Eq, PartialEq)]
386pub enum RequestTarget {
387    Absolute(AbsoluteTarget),
388    Connect(Authority),
389    Origin(OriginTarget),
390}
391
392impl RequestTarget {
393    #[must_use]
394    pub fn authority(&self) -> &Authority {
395        match self {
396            Self::Absolute(target) => &target.authority,
397            Self::Connect(authority) => authority,
398            Self::Origin(target) => &target.authority,
399        }
400    }
401
402    #[must_use]
403    pub fn path_and_query(&self) -> Option<&str> {
404        match self {
405            Self::Absolute(target) => Some(&target.path_and_query),
406            Self::Connect(_) => None,
407            Self::Origin(target) => Some(&target.path_and_query),
408        }
409    }
410}
411
412impl fmt::Debug for RequestTarget {
413    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
414        match self {
415            Self::Absolute(target) => formatter.debug_tuple("Absolute").field(target).finish(),
416            Self::Connect(authority) => formatter.debug_tuple("Connect").field(authority).finish(),
417            Self::Origin(target) => formatter.debug_tuple("Origin").field(target).finish(),
418        }
419    }
420}
421
422#[derive(Clone, Copy, Debug, Eq, PartialEq)]
423pub enum BodyFraming {
424    None,
425    ContentLength(u64),
426    Chunked,
427}
428
429/// Reads exactly one request head and leaves all body bytes unread.
430pub fn read_request_head(
431    input: &mut impl Read,
432    max_head_bytes: usize,
433) -> Result<RequestHead, Http1Error> {
434    if max_head_bytes < HEAD_END.len() {
435        return Err(Http1Error::HeadTooLarge);
436    }
437    let mut bytes = Vec::with_capacity(max_head_bytes.min(8 * 1024));
438    let mut byte = [0_u8; 1];
439    while bytes.len() < max_head_bytes {
440        let read = input.read(&mut byte)?;
441        if read == 0 {
442            return Err(Http1Error::UnexpectedEof);
443        }
444        bytes.push(byte[0]);
445        if bytes.ends_with(HEAD_END) {
446            return parse_request_head(&bytes);
447        }
448    }
449    Err(Http1Error::HeadTooLarge)
450}
451
452pub fn parse_request_head(bytes: &[u8]) -> Result<RequestHead, Http1Error> {
453    if bytes.len() > DEFAULT_MAX_REQUEST_HEAD_BYTES {
454        return Err(Http1Error::HeadTooLarge);
455    }
456    let Some(end) = find_head_end(bytes) else {
457        return Err(Http1Error::UnexpectedEof);
458    };
459    if end != bytes.len() {
460        return Err(Http1Error::InvalidHeader);
461    }
462    validate_crlf(bytes)?;
463
464    let line_end = bytes
465        .windows(2)
466        .position(|pair| pair == b"\r\n")
467        .ok_or(Http1Error::InvalidRequestLine)?;
468    let (method, target, version) = parse_request_line(&bytes[..line_end])?;
469
470    // `httparse` remains a second, independent RFC parser layer. The checks
471    // below intentionally tighten its accepted grammar for a proxy boundary.
472    let mut parsed_headers = [httparse::EMPTY_HEADER; MAX_REQUEST_HEADERS];
473    let mut parsed = httparse::Request::new(&mut parsed_headers);
474    let parsed_len = match parsed.parse(bytes) {
475        Ok(httparse::Status::Complete(length)) => length,
476        Ok(httparse::Status::Partial) => return Err(Http1Error::UnexpectedEof),
477        Err(httparse::Error::TooManyHeaders) => return Err(Http1Error::TooManyHeaders),
478        Err(_) => return Err(Http1Error::InvalidHeader),
479    };
480    if parsed_len != bytes.len()
481        || parsed.method != Some(method.as_str())
482        || parsed.path != Some(target.as_str())
483    {
484        return Err(Http1Error::InvalidRequestLine);
485    }
486    let parsed_version = match parsed.version {
487        Some(0) => HttpVersion::Http10,
488        Some(1) => HttpVersion::Http11,
489        _ => return Err(Http1Error::UnsupportedHttpVersion),
490    };
491    if parsed_version != version {
492        return Err(Http1Error::InvalidRequestLine);
493    }
494
495    // Keep the CRLF terminating the last field, but exclude the final empty
496    // line. This also yields an empty slice for a request with no fields.
497    let raw_headers = &bytes[line_end + 2..bytes.len() - 2];
498    let headers = parse_header_block(raw_headers)?;
499    if headers.len() != parsed.headers.len() {
500        return Err(Http1Error::InvalidHeader);
501    }
502    Ok(RequestHead {
503        method,
504        target,
505        version,
506        headers,
507    })
508}
509
510pub fn determine_body_framing(headers: &[Header]) -> Result<BodyFraming, Http1Error> {
511    let content_lengths: Vec<_> = headers
512        .iter()
513        .filter(|header| header.name.eq_ignore_ascii_case("content-length"))
514        .collect();
515    if content_lengths.len() > 1 {
516        return Err(Http1Error::InvalidContentLength);
517    }
518    let content_length = content_lengths
519        .first()
520        .map(|header| parse_content_length(&header.value))
521        .transpose()?;
522
523    let transfer_encodings: Vec<_> = headers
524        .iter()
525        .filter(|header| header.name.eq_ignore_ascii_case("transfer-encoding"))
526        .collect();
527    if !transfer_encodings.is_empty() && content_length.is_some() {
528        return Err(Http1Error::AmbiguousFraming);
529    }
530    if !transfer_encodings.is_empty() {
531        if transfer_encodings.len() != 1
532            || !transfer_encodings[0].value.eq_ignore_ascii_case("chunked")
533        {
534            return Err(Http1Error::UnsupportedTransferEncoding);
535        }
536        return Ok(BodyFraming::Chunked);
537    }
538    Ok(content_length.map_or(BodyFraming::None, BodyFraming::ContentLength))
539}
540
541pub fn read_request_body(
542    input: &mut impl Read,
543    framing: BodyFraming,
544    max_body_bytes: u64,
545) -> Result<Vec<u8>, Http1Error> {
546    match framing {
547        BodyFraming::None => Ok(Vec::new()),
548        BodyFraming::ContentLength(length) => read_fixed_body(input, length, max_body_bytes),
549        BodyFraming::Chunked => read_chunked_body(input, max_body_bytes),
550    }
551}
552
553pub fn read_chunked_body(
554    input: &mut impl Read,
555    max_body_bytes: u64,
556) -> Result<Vec<u8>, Http1Error> {
557    let mut output = Vec::new();
558    let mut total = 0_u64;
559    loop {
560        let line = read_crlf_line(input, MAX_CHUNK_LINE_BYTES, Http1Error::InvalidChunkedBody)?;
561        let (size_text, extensions) = match line.iter().position(|byte| *byte == b';') {
562            Some(index) => (&line[..index], &line[index..]),
563            None => (line.as_slice(), [].as_slice()),
564        };
565        if size_text.is_empty() || !size_text.iter().all(u8::is_ascii_hexdigit) {
566            return Err(Http1Error::InvalidChunkedBody);
567        }
568        validate_chunk_extensions(extensions)?;
569        let size_text =
570            std::str::from_utf8(size_text).map_err(|_| Http1Error::InvalidChunkedBody)?;
571        let size =
572            u64::from_str_radix(size_text, 16).map_err(|_| Http1Error::InvalidChunkedBody)?;
573        if size == 0 {
574            read_chunk_trailers(input)?;
575            return Ok(output);
576        }
577        total = total.checked_add(size).ok_or(Http1Error::BodyTooLarge)?;
578        if total > max_body_bytes {
579            return Err(Http1Error::BodyTooLarge);
580        }
581        let size_usize = usize::try_from(size).map_err(|_| Http1Error::BodyTooLarge)?;
582        let start = output.len();
583        let end = start
584            .checked_add(size_usize)
585            .ok_or(Http1Error::BodyTooLarge)?;
586        output
587            .try_reserve(size_usize)
588            .map_err(|_| Http1Error::BodyTooLarge)?;
589        output.resize(end, 0);
590        read_exact_body(input, &mut output[start..end])?;
591        let mut crlf = [0_u8; 2];
592        read_exact_body(input, &mut crlf)?;
593        if crlf != *b"\r\n" {
594            return Err(Http1Error::InvalidChunkedBody);
595        }
596    }
597}
598
599/// Removes proxy credentials, all `Proxy-*` and `X-HNS-*` fields, standard
600/// hop-by-hop fields, and fields nominated by `Connection`/`Proxy-Connection`.
601pub fn sanitize_forward_headers(headers: &[Header]) -> Result<Vec<Header>, Http1Error> {
602    let nominated = connection_nominated_fields(headers)?;
603
604    Ok(headers
605        .iter()
606        .filter(|header| {
607            let lower = header.name.to_ascii_lowercase();
608            !is_hop_by_hop(&lower)
609                && !lower.starts_with("proxy-")
610                && !lower.starts_with("x-hns-")
611                && !nominated.contains(&lower)
612        })
613        .cloned()
614        .collect())
615}
616
617/// Removes all proxy/internal/framing fields from a WebSocket handshake while
618/// preserving end-to-end WebSocket fields and reconstructing the two required
619/// hop-by-hop fields canonically.
620pub fn sanitize_upgrade_forward_headers(headers: &[Header]) -> Result<Vec<Header>, Http1Error> {
621    let nominated = connection_nominated_fields(headers)?;
622    let connection_upgrade = headers.iter().any(|header| {
623        header.name.eq_ignore_ascii_case("connection")
624            && header
625                .value
626                .split(',')
627                .map(str::trim)
628                .any(|token| token.eq_ignore_ascii_case("upgrade"))
629    });
630    let upgrade_values: Vec<_> = headers
631        .iter()
632        .filter(|header| header.name.eq_ignore_ascii_case("upgrade"))
633        .map(|header| header.value.as_str())
634        .collect();
635    if !connection_upgrade
636        || upgrade_values.len() != 1
637        || !upgrade_values[0].eq_ignore_ascii_case("websocket")
638    {
639        return Err(Http1Error::InvalidHeader);
640    }
641
642    let mut sanitized: Vec<_> = headers
643        .iter()
644        .filter(|header| {
645            let lower = header.name.to_ascii_lowercase();
646            !is_hop_by_hop(&lower)
647                && !lower.starts_with("proxy-")
648                && !lower.starts_with("x-hns-")
649                && !nominated.contains(&lower)
650        })
651        .cloned()
652        .collect();
653    sanitized.push(Header {
654        name: "Connection".to_owned(),
655        value: "Upgrade".to_owned(),
656    });
657    sanitized.push(Header {
658        name: "Upgrade".to_owned(),
659        value: "websocket".to_owned(),
660    });
661    Ok(sanitized)
662}
663
664fn connection_nominated_fields(headers: &[Header]) -> Result<HashSet<String>, Http1Error> {
665    let mut nominated = HashSet::new();
666    for header in headers.iter().filter(|header| {
667        header.name.eq_ignore_ascii_case("connection")
668            || header.name.eq_ignore_ascii_case("proxy-connection")
669    }) {
670        for token in header.value.split(',').map(str::trim) {
671            if token.is_empty() || !is_http_token(token.as_bytes()) {
672                return Err(Http1Error::InvalidHeader);
673            }
674            nominated.insert(token.to_ascii_lowercase());
675        }
676    }
677    Ok(nominated)
678}
679
680fn parse_request_line(bytes: &[u8]) -> Result<(String, String, HttpVersion), Http1Error> {
681    if !bytes.is_ascii() || bytes.contains(&b'\t') {
682        return Err(Http1Error::InvalidRequestLine);
683    }
684    let mut parts = bytes.split(|byte| *byte == b' ');
685    let method = parts.next().ok_or(Http1Error::InvalidRequestLine)?;
686    let target = parts.next().ok_or(Http1Error::InvalidRequestLine)?;
687    let version = parts.next().ok_or(Http1Error::InvalidRequestLine)?;
688    if parts.next().is_some()
689        || !is_http_token(method)
690        || target.is_empty()
691        || !target.iter().all(|byte| (0x21..=0x7e).contains(byte))
692    {
693        return Err(Http1Error::InvalidRequestLine);
694    }
695    let version = match version {
696        b"HTTP/1.0" => HttpVersion::Http10,
697        b"HTTP/1.1" => HttpVersion::Http11,
698        bytes if bytes.starts_with(b"HTTP/") => return Err(Http1Error::UnsupportedHttpVersion),
699        _ => return Err(Http1Error::InvalidRequestLine),
700    };
701    Ok((
702        String::from_utf8(method.to_vec()).map_err(|_| Http1Error::InvalidRequestLine)?,
703        String::from_utf8(target.to_vec()).map_err(|_| Http1Error::InvalidRequestLine)?,
704        version,
705    ))
706}
707
708fn parse_header_block(bytes: &[u8]) -> Result<Vec<Header>, Http1Error> {
709    if bytes.is_empty() {
710        return Ok(Vec::new());
711    }
712    let mut headers = Vec::new();
713    for raw_line in bytes.split(|byte| *byte == b'\n') {
714        if raw_line.is_empty() {
715            // `bytes` includes the last field's CRLF, so `split` has one
716            // terminal empty item. An earlier empty line cannot reach here:
717            // `find_head_end` would already have treated it as the head end.
718            continue;
719        }
720        let Some(line) = raw_line.strip_suffix(b"\r") else {
721            return Err(Http1Error::InvalidHeader);
722        };
723        if line.is_empty() || matches!(line.first(), Some(b' ' | b'\t')) {
724            return Err(Http1Error::InvalidHeader);
725        }
726        let separator = line
727            .iter()
728            .position(|byte| *byte == b':')
729            .ok_or(Http1Error::InvalidHeader)?;
730        let name = &line[..separator];
731        let value = trim_ows(&line[separator + 1..]);
732        if !is_http_token(name) || !is_valid_field_value(value) {
733            return Err(Http1Error::InvalidHeader);
734        }
735        if headers.len() == MAX_REQUEST_HEADERS {
736            return Err(Http1Error::TooManyHeaders);
737        }
738        headers.push(Header {
739            name: String::from_utf8(name.to_vec()).map_err(|_| Http1Error::InvalidHeader)?,
740            value: String::from_utf8(value.to_vec()).map_err(|_| Http1Error::InvalidHeader)?,
741        });
742    }
743    Ok(headers)
744}
745
746fn parse_request_target(
747    method: &str,
748    target: &str,
749    connected_to: Option<&Authority>,
750) -> Result<RequestTarget, Http1Error> {
751    if method.eq_ignore_ascii_case("CONNECT") {
752        if connected_to.is_some() {
753            return Err(Http1Error::TargetMismatch);
754        }
755        return parse_authority(target, None, true).map(RequestTarget::Connect);
756    }
757    if let Some(connected_to) = connected_to {
758        if target.starts_with('/') {
759            if target.contains('#') {
760                return Err(Http1Error::InvalidTarget);
761            }
762            return Ok(RequestTarget::Origin(OriginTarget {
763                authority: connected_to.clone(),
764                path_and_query: target.to_owned(),
765            }));
766        }
767        let absolute = parse_absolute_target(target)?;
768        if !absolute.scheme.is_secure() || absolute.authority != *connected_to {
769            return Err(Http1Error::TargetMismatch);
770        }
771        return Ok(RequestTarget::Absolute(absolute));
772    }
773    parse_absolute_target(target).map(RequestTarget::Absolute)
774}
775
776fn parse_absolute_target(target: &str) -> Result<AbsoluteTarget, Http1Error> {
777    let (scheme_text, remainder) = target.split_once("://").ok_or(Http1Error::InvalidTarget)?;
778    let scheme = if scheme_text.eq_ignore_ascii_case("http") {
779        Scheme::Http
780    } else if scheme_text.eq_ignore_ascii_case("https") {
781        Scheme::Https
782    } else if scheme_text.eq_ignore_ascii_case("ws") {
783        Scheme::Ws
784    } else if scheme_text.eq_ignore_ascii_case("wss") {
785        Scheme::Wss
786    } else {
787        return Err(Http1Error::InvalidTarget);
788    };
789    if remainder.contains('#') || remainder.contains('\\') {
790        return Err(Http1Error::InvalidTarget);
791    }
792    let authority_end = remainder.find(['/', '?']).unwrap_or(remainder.len());
793    let authority_text = &remainder[..authority_end];
794    let suffix = &remainder[authority_end..];
795    let authority = parse_authority(authority_text, Some(scheme.default_port()), false)?;
796    let path_and_query = if suffix.is_empty() {
797        "/".to_owned()
798    } else if suffix.starts_with('?') {
799        format!("/{suffix}")
800    } else {
801        suffix.to_owned()
802    };
803    Ok(AbsoluteTarget {
804        scheme,
805        authority,
806        path_and_query,
807    })
808}
809
810fn parse_authority(
811    value: &str,
812    default_port: Option<u16>,
813    require_explicit_port: bool,
814) -> Result<Authority, Http1Error> {
815    if value.is_empty()
816        || value
817            .bytes()
818            .any(|byte| byte.is_ascii_control() || byte == b' ')
819        || value.contains('@')
820        || value.contains('/')
821        || value.contains('?')
822        || value.contains('#')
823        || value.contains('\\')
824    {
825        return Err(Http1Error::InvalidAuthority);
826    }
827
828    let (host, port_text) = if let Some(bracketed) = value.strip_prefix('[') {
829        let close = bracketed.find(']').ok_or(Http1Error::InvalidAuthority)?;
830        let host = &bracketed[..close];
831        let suffix = &bracketed[close + 1..];
832        let port = if suffix.is_empty() {
833            None
834        } else {
835            Some(
836                suffix
837                    .strip_prefix(':')
838                    .ok_or(Http1Error::InvalidAuthority)?,
839            )
840        };
841        if host.parse::<Ipv6Addr>().is_err() {
842            return Err(Http1Error::InvalidAuthority);
843        }
844        (host, port)
845    } else {
846        if value.matches(':').count() > 1 {
847            return Err(Http1Error::InvalidAuthority);
848        }
849        match value.rsplit_once(':') {
850            Some((host, port)) => (host, Some(port)),
851            None => (value, None),
852        }
853    };
854    if host.is_empty() || (require_explicit_port && port_text.is_none()) {
855        return Err(Http1Error::InvalidAuthority);
856    }
857    let port = match port_text {
858        Some(text) if !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit()) => text
859            .parse::<u16>()
860            .ok()
861            .filter(|port| *port != 0)
862            .ok_or(Http1Error::InvalidAuthority)?,
863        Some(_) => return Err(Http1Error::InvalidAuthority),
864        None => default_port.ok_or(Http1Error::InvalidAuthority)?,
865    };
866    Ok(Authority {
867        host: normalize_host(host)?,
868        port,
869    })
870}
871
872fn normalize_host(host: &str) -> Result<String, Http1Error> {
873    if let Ok(address) = host.parse::<Ipv6Addr>() {
874        return Ok(address.to_string());
875    }
876    if let Ok(address) = host.parse::<Ipv4Addr>() {
877        return Ok(address.to_string());
878    }
879    if host
880        .bytes()
881        .all(|byte| byte.is_ascii_digit() || byte == b'.')
882    {
883        return Err(Http1Error::InvalidAuthority);
884    }
885    let ascii = idna::domain_to_ascii_cow(host.as_bytes(), idna::AsciiDenyList::URL)
886        .map_err(|_| Http1Error::InvalidAuthority)?;
887    let mut ascii = ascii.to_ascii_lowercase();
888    // One terminal root dot is equivalent. Multiple terminal dots contain an
889    // empty DNS label and must not collapse to the same authorization scope.
890    if ascii.ends_with('.') {
891        ascii.pop();
892    }
893    if ascii.is_empty() || ascii.len() > 253 {
894        return Err(Http1Error::InvalidAuthority);
895    }
896    for label in ascii.split('.') {
897        if label.is_empty()
898            || label.len() > 63
899            || label.starts_with('-')
900            || label.ends_with('-')
901            || !label
902                .bytes()
903                .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
904        {
905            return Err(Http1Error::InvalidAuthority);
906        }
907    }
908    Ok(ascii)
909}
910
911fn parse_content_length(value: &str) -> Result<u64, Http1Error> {
912    if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
913        return Err(Http1Error::InvalidContentLength);
914    }
915    value
916        .parse::<u64>()
917        .map_err(|_| Http1Error::InvalidContentLength)
918}
919
920fn read_fixed_body(
921    input: &mut impl Read,
922    length: u64,
923    max_body_bytes: u64,
924) -> Result<Vec<u8>, Http1Error> {
925    if length > max_body_bytes {
926        return Err(Http1Error::BodyTooLarge);
927    }
928    let length = usize::try_from(length).map_err(|_| Http1Error::BodyTooLarge)?;
929    let mut body = Vec::new();
930    body.try_reserve_exact(length)
931        .map_err(|_| Http1Error::BodyTooLarge)?;
932    body.resize(length, 0);
933    read_exact_body(input, &mut body)?;
934    Ok(body)
935}
936
937fn read_exact_body(input: &mut impl Read, mut bytes: &mut [u8]) -> Result<(), Http1Error> {
938    while !bytes.is_empty() {
939        match input.read(bytes) {
940            Ok(0) => return Err(Http1Error::UnexpectedBodyEof),
941            Ok(read) => bytes = &mut bytes[read..],
942            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
943            Err(error) => return Err(Http1Error::Io(error)),
944        }
945    }
946    Ok(())
947}
948
949fn read_crlf_line(
950    input: &mut impl Read,
951    limit: usize,
952    limit_error: Http1Error,
953) -> Result<Vec<u8>, Http1Error> {
954    let mut line = Vec::with_capacity(limit.min(256));
955    let mut byte = [0_u8; 1];
956    while line.len() < limit {
957        let read = input.read(&mut byte)?;
958        if read == 0 {
959            return Err(Http1Error::UnexpectedBodyEof);
960        }
961        line.push(byte[0]);
962        if line.ends_with(b"\r\n") {
963            line.truncate(line.len() - 2);
964            return Ok(line);
965        }
966        if byte[0] == b'\n' {
967            return Err(Http1Error::InvalidChunkedBody);
968        }
969    }
970    Err(limit_error)
971}
972
973fn read_chunk_trailers(input: &mut impl Read) -> Result<(), Http1Error> {
974    let mut consumed = 0_usize;
975    let mut count = 0_usize;
976    loop {
977        let remaining = MAX_CHUNK_TRAILER_BYTES
978            .checked_sub(consumed)
979            .ok_or(Http1Error::TrailersTooLarge)?;
980        if remaining < 2 {
981            return Err(Http1Error::TrailersTooLarge);
982        }
983        let line = read_crlf_line(
984            input,
985            remaining.min(MAX_CHUNK_LINE_BYTES),
986            Http1Error::InvalidChunkedBody,
987        )?;
988        consumed = consumed
989            .checked_add(line.len() + 2)
990            .ok_or(Http1Error::TrailersTooLarge)?;
991        if line.is_empty() {
992            return Ok(());
993        }
994        count += 1;
995        if count > MAX_REQUEST_HEADERS {
996            return Err(Http1Error::InvalidChunkedBody);
997        }
998        let header = parse_single_header_line(&line)?;
999        let lower = header.name.to_ascii_lowercase();
1000        if is_hop_by_hop(&lower)
1001            || lower == "content-length"
1002            || lower == "host"
1003            || lower.starts_with("proxy-")
1004            || lower.starts_with("x-hns-")
1005        {
1006            return Err(Http1Error::InvalidChunkedBody);
1007        }
1008    }
1009}
1010
1011fn parse_single_header_line(line: &[u8]) -> Result<Header, Http1Error> {
1012    if line.is_empty() || matches!(line.first(), Some(b' ' | b'\t')) {
1013        return Err(Http1Error::InvalidChunkedBody);
1014    }
1015    let separator = line
1016        .iter()
1017        .position(|byte| *byte == b':')
1018        .ok_or(Http1Error::InvalidChunkedBody)?;
1019    let name = &line[..separator];
1020    let value = trim_ows(&line[separator + 1..]);
1021    if !is_http_token(name) || !is_valid_field_value(value) {
1022        return Err(Http1Error::InvalidChunkedBody);
1023    }
1024    Ok(Header {
1025        name: String::from_utf8(name.to_vec()).map_err(|_| Http1Error::InvalidChunkedBody)?,
1026        value: String::from_utf8(value.to_vec()).map_err(|_| Http1Error::InvalidChunkedBody)?,
1027    })
1028}
1029
1030fn validate_chunk_extensions(mut extensions: &[u8]) -> Result<(), Http1Error> {
1031    while !extensions.is_empty() {
1032        extensions = extensions
1033            .strip_prefix(b";")
1034            .ok_or(Http1Error::InvalidChunkedBody)?;
1035        let end = extensions
1036            .iter()
1037            .position(|byte| *byte == b';')
1038            .unwrap_or(extensions.len());
1039        let extension = &extensions[..end];
1040        extensions = &extensions[end..];
1041        let (name, value) = match extension.iter().position(|byte| *byte == b'=') {
1042            Some(index) => (&extension[..index], Some(&extension[index + 1..])),
1043            None => (extension, None),
1044        };
1045        if !is_http_token(name) {
1046            return Err(Http1Error::InvalidChunkedBody);
1047        }
1048        if let Some(value) = value
1049            && !is_http_token(value)
1050            && !is_valid_quoted_string(value)
1051        {
1052            return Err(Http1Error::InvalidChunkedBody);
1053        }
1054    }
1055    Ok(())
1056}
1057
1058fn is_valid_quoted_string(value: &[u8]) -> bool {
1059    let Some(inner) = value
1060        .strip_prefix(b"\"")
1061        .and_then(|value| value.strip_suffix(b"\""))
1062    else {
1063        return false;
1064    };
1065    let mut escaped = false;
1066    for byte in inner {
1067        if escaped {
1068            if !matches!(*byte, b'\t' | b' '..=b'~') {
1069                return false;
1070            }
1071            escaped = false;
1072        } else if *byte == b'\\' {
1073            escaped = true;
1074        } else if !matches!(*byte, b'\t' | b' ' | b'!' | b'#'..=b'[' | b']'..=b'~') {
1075            return false;
1076        }
1077    }
1078    !escaped
1079}
1080
1081fn find_head_end(bytes: &[u8]) -> Option<usize> {
1082    bytes
1083        .windows(HEAD_END.len())
1084        .position(|window| window == HEAD_END)
1085        .map(|index| index + HEAD_END.len())
1086}
1087
1088fn validate_crlf(bytes: &[u8]) -> Result<(), Http1Error> {
1089    for (index, byte) in bytes.iter().enumerate() {
1090        if (*byte == b'\r' && bytes.get(index + 1) != Some(&b'\n'))
1091            || (*byte == b'\n' && index.checked_sub(1).and_then(|i| bytes.get(i)) != Some(&b'\r'))
1092        {
1093            return Err(Http1Error::InvalidHeader);
1094        }
1095    }
1096    Ok(())
1097}
1098
1099fn trim_ows(mut value: &[u8]) -> &[u8] {
1100    while matches!(value.first(), Some(b' ' | b'\t')) {
1101        value = &value[1..];
1102    }
1103    while matches!(value.last(), Some(b' ' | b'\t')) {
1104        value = &value[..value.len() - 1];
1105    }
1106    value
1107}
1108
1109fn is_http_token(value: &[u8]) -> bool {
1110    !value.is_empty()
1111        && value.iter().all(|byte| {
1112            byte.is_ascii_alphanumeric()
1113                || matches!(
1114                    byte,
1115                    b'!' | b'#'
1116                        | b'$'
1117                        | b'%'
1118                        | b'&'
1119                        | b'\''
1120                        | b'*'
1121                        | b'+'
1122                        | b'-'
1123                        | b'.'
1124                        | b'^'
1125                        | b'_'
1126                        | b'`'
1127                        | b'|'
1128                        | b'~'
1129                )
1130        })
1131}
1132
1133fn is_valid_field_value(value: &[u8]) -> bool {
1134    value
1135        .iter()
1136        .all(|byte| matches!(*byte, b'\t' | b' '..=b'~'))
1137}
1138
1139fn is_hop_by_hop(lower_name: &str) -> bool {
1140    matches!(
1141        lower_name,
1142        "connection"
1143            | "keep-alive"
1144            | "proxy-authenticate"
1145            | "proxy-authorization"
1146            | "proxy-connection"
1147            | "te"
1148            | "trailer"
1149            | "transfer-encoding"
1150            | "upgrade"
1151    )
1152}
1153
1154#[cfg(test)]
1155mod tests {
1156    use super::*;
1157    use std::io::Cursor;
1158
1159    fn parse(text: &str) -> RequestHead {
1160        parse_request_head(text.as_bytes()).unwrap()
1161    }
1162
1163    #[test]
1164    fn parses_absolute_target_and_preserves_only_routed_path_and_query() {
1165        let head = parse(
1166            "GET https://Welcome:8443/private?q=secret HTTP/1.1\r\nHost: welcome:8443\r\n\r\n",
1167        );
1168        let RequestTarget::Absolute(target) = head.validated_target(None).unwrap() else {
1169            panic!("expected absolute target");
1170        };
1171        assert_eq!(target.scheme(), Scheme::Https);
1172        assert_eq!(target.authority().host(), "welcome");
1173        assert_eq!(target.authority().port(), 8443);
1174        assert_eq!(target.path_and_query(), "/private?q=secret");
1175        assert!(!format!("{head:?}").contains("secret"));
1176        assert!(!format!("{target:?}").contains("private"));
1177    }
1178
1179    #[test]
1180    fn parses_connect_and_post_connect_origin_form() {
1181        let connect = parse("CONNECT welcome:443 HTTP/1.1\r\nHost: welcome:443\r\n\r\n");
1182        let RequestTarget::Connect(authority) = connect.validated_target(None).unwrap() else {
1183            panic!("expected CONNECT target");
1184        };
1185        let tunneled =
1186            parse("POST /submit?q=1 HTTP/1.1\r\nHost: welcome\r\nContent-Length: 2\r\n\r\n");
1187        let RequestTarget::Origin(target) = tunneled.validated_target(Some(&authority)).unwrap()
1188        else {
1189            panic!("expected origin target");
1190        };
1191        assert_eq!(target.path_and_query(), "/submit?q=1");
1192    }
1193
1194    #[test]
1195    fn rejects_direct_origin_form_and_mismatched_connected_absolute_form() {
1196        let origin = parse("GET / HTTP/1.1\r\nHost: welcome\r\n\r\n");
1197        assert!(matches!(
1198            origin.request_target(None),
1199            Err(Http1Error::InvalidTarget)
1200        ));
1201        let authority = Authority::parse("welcome", 443).unwrap();
1202        let other = parse("GET https://other/ HTTP/1.1\r\nHost: other\r\n\r\n");
1203        assert!(matches!(
1204            other.request_target(Some(&authority)),
1205            Err(Http1Error::TargetMismatch)
1206        ));
1207    }
1208
1209    #[test]
1210    fn rejects_host_mismatch_duplicates_and_missing_http11_host() {
1211        let mismatch = parse("GET http://welcome/ HTTP/1.1\r\nHost: other\r\n\r\n");
1212        assert!(matches!(
1213            mismatch.validated_target(None),
1214            Err(Http1Error::HostMismatch)
1215        ));
1216        let duplicate =
1217            parse("GET http://welcome/ HTTP/1.1\r\nHost: welcome\r\nHost: welcome\r\n\r\n");
1218        assert!(matches!(
1219            duplicate.validated_target(None),
1220            Err(Http1Error::HostMismatch)
1221        ));
1222        let missing = parse("GET http://welcome/ HTTP/1.1\r\nUser-Agent: test\r\n\r\n");
1223        assert!(matches!(
1224            missing.validated_target(None),
1225            Err(Http1Error::MissingHost)
1226        ));
1227    }
1228
1229    #[test]
1230    fn rejects_request_line_header_and_version_smuggling() {
1231        for request in [
1232            "GET  http://welcome/ HTTP/1.1\r\nHost: welcome\r\n\r\n",
1233            "GET http://welcome/ HTTP/2.0\r\nHost: welcome\r\n\r\n",
1234            "GET http://welcome/ HTTP/1.1\nHost: welcome\n\n",
1235            "GET http://welcome/ HTTP/1.1\r\nBad Name: value\r\n\r\n",
1236            "GET http://welcome/ HTTP/1.1\r\n Folded: value\r\n\r\n",
1237            "GET http://welcome/ HTTP/1.1\r\nX-Test: bad\u{7f}\r\n\r\n",
1238        ] {
1239            assert!(
1240                parse_request_head(request.as_bytes()).is_err(),
1241                "accepted {request:?}"
1242            );
1243        }
1244    }
1245
1246    #[test]
1247    fn bounded_reader_does_not_consume_a_coalesced_body() {
1248        let mut input = Cursor::new(
1249            b"POST http://welcome/upload HTTP/1.1\r\nHost: welcome\r\nContent-Length: 4\r\n\r\ndataafter"
1250                .to_vec(),
1251        );
1252        let head = read_request_head(&mut input, DEFAULT_MAX_REQUEST_HEAD_BYTES).unwrap();
1253        let framing = determine_body_framing(head.headers()).unwrap();
1254        assert_eq!(
1255            read_request_body(&mut input, framing, 1024).unwrap(),
1256            b"data"
1257        );
1258        let mut remainder = String::new();
1259        input.read_to_string(&mut remainder).unwrap();
1260        assert_eq!(remainder, "after");
1261    }
1262
1263    #[test]
1264    fn fixed_body_reader_enforces_cap_and_exact_length() {
1265        assert!(matches!(
1266            read_request_body(&mut Cursor::new(b"data"), BodyFraming::ContentLength(4), 3,),
1267            Err(Http1Error::BodyTooLarge)
1268        ));
1269        assert!(matches!(
1270            read_request_body(&mut Cursor::new(b"abc"), BodyFraming::ContentLength(4), 4,),
1271            Err(Http1Error::UnexpectedBodyEof)
1272        ));
1273    }
1274
1275    #[test]
1276    fn enforces_head_and_header_count_limits() {
1277        let request = "GET http://welcome/ HTTP/1.1\r\nHost: welcome\r\n\r\n";
1278        let mut input = Cursor::new(request.as_bytes());
1279        assert!(matches!(
1280            read_request_head(&mut input, 8),
1281            Err(Http1Error::HeadTooLarge)
1282        ));
1283
1284        let mut many = String::from("GET http://welcome/ HTTP/1.1\r\nHost: welcome\r\n");
1285        for index in 0..MAX_REQUEST_HEADERS {
1286            many.push_str(&format!("X-{index}: x\r\n"));
1287        }
1288        many.push_str("\r\n");
1289        assert!(matches!(
1290            parse_request_head(many.as_bytes()),
1291            Err(Http1Error::TooManyHeaders)
1292        ));
1293    }
1294
1295    #[test]
1296    fn rejects_duplicate_conflicting_and_ambiguous_lengths() {
1297        for request in [
1298            "POST http://welcome/ HTTP/1.1\r\nHost: welcome\r\nContent-Length: 2\r\nContent-Length: 2\r\n\r\n",
1299            "POST http://welcome/ HTTP/1.1\r\nHost: welcome\r\nContent-Length: 2\r\nContent-Length: 3\r\n\r\n",
1300            "POST http://welcome/ HTTP/1.1\r\nHost: welcome\r\nContent-Length: +2\r\n\r\n",
1301        ] {
1302            let head = parse(request);
1303            assert!(matches!(
1304                determine_body_framing(head.headers()),
1305                Err(Http1Error::InvalidContentLength)
1306            ));
1307        }
1308        let ambiguous = parse(
1309            "POST http://welcome/ HTTP/1.1\r\nHost: welcome\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n",
1310        );
1311        assert!(matches!(
1312            determine_body_framing(ambiguous.headers()),
1313            Err(Http1Error::AmbiguousFraming)
1314        ));
1315    }
1316
1317    #[test]
1318    fn only_a_single_chunked_transfer_coding_is_supported() {
1319        let chunked = parse(
1320            "POST http://welcome/ HTTP/1.1\r\nHost: welcome\r\nTransfer-Encoding: ChUnKeD\r\n\r\n",
1321        );
1322        assert_eq!(
1323            determine_body_framing(chunked.headers()).unwrap(),
1324            BodyFraming::Chunked
1325        );
1326        for value in [
1327            "gzip",
1328            "gzip, chunked",
1329            "chunked, chunked",
1330            "chunked;foo=bar",
1331            "",
1332        ] {
1333            let request = format!(
1334                "POST http://welcome/ HTTP/1.1\r\nHost: welcome\r\nTransfer-Encoding: {value}\r\n\r\n"
1335            );
1336            let head = parse(&request);
1337            assert!(matches!(
1338                determine_body_framing(head.headers()),
1339                Err(Http1Error::UnsupportedTransferEncoding)
1340            ));
1341        }
1342    }
1343
1344    #[test]
1345    fn decodes_chunked_body_and_consumes_bounded_valid_trailers() {
1346        let mut input = Cursor::new(
1347            b"4;kind=test\r\nWiki\r\n5\r\npedia\r\n0\r\nDigest: value\r\n\r\nafter".to_vec(),
1348        );
1349        assert_eq!(read_chunked_body(&mut input, 1024).unwrap(), b"Wikipedia");
1350        let mut remainder = String::new();
1351        input.read_to_string(&mut remainder).unwrap();
1352        assert_eq!(remainder, "after");
1353    }
1354
1355    #[test]
1356    fn rejects_bad_chunk_syntax_forbidden_trailers_and_body_overflow() {
1357        for body in [
1358            "x\r\n",
1359            "1\r\naX\r\n0\r\n\r\n",
1360            "1\na\r\n0\r\n\r\n",
1361            "0\r\nContent-Length: 1\r\n\r\n",
1362            "0\r\nX-HNS-Secret: value\r\n\r\n",
1363        ] {
1364            assert!(read_chunked_body(&mut Cursor::new(body.as_bytes()), 1024).is_err());
1365        }
1366        assert!(matches!(
1367            read_chunked_body(&mut Cursor::new(b"5\r\nhello\r\n0\r\n\r\n"), 4),
1368            Err(Http1Error::BodyTooLarge)
1369        ));
1370    }
1371
1372    #[test]
1373    fn retains_duplicate_proxy_credentials_for_uniform_authentication_rejection() {
1374        let request = b"GET http://welcome/ HTTP/1.1\r\nHost: welcome\r\nProxy-Authorization: first\r\nProxy-Authorization: second\r\n\r\n";
1375        let head = parse_request_head(request).unwrap();
1376
1377        assert!(matches!(
1378            head.proxy_authorization(),
1379            Err(Http1Error::DuplicateProxyAuthorization)
1380        ));
1381        assert_eq!(head.header_values("proxy-authorization").count(), 2);
1382    }
1383
1384    #[test]
1385    fn sanitizer_removes_hop_proxy_internal_and_connection_nominated_fields() {
1386        let head = parse(
1387            "GET http://welcome/ HTTP/1.1\r\nHost: welcome\r\nProxy-Authorization: secret\r\nProxy-Future: secret\r\nX-HNS-Security-Path: forged\r\nX-HNS-Future: secret\r\nConnection: keep-alive, X-Remove\r\nX-Remove: secret\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\nX-Keep: yes\r\n\r\n",
1388        );
1389        assert_eq!(head.proxy_authorization().unwrap(), Some("secret"));
1390        let sanitized = sanitize_forward_headers(head.headers()).unwrap();
1391        assert_eq!(sanitized.len(), 2);
1392        assert_eq!(sanitized[0].name(), "Host");
1393        assert_eq!(sanitized[1].name(), "X-Keep");
1394        assert!(!format!("{head:?}").contains("secret"));
1395        assert!(!format!("{:?}", head.headers()).contains("secret"));
1396    }
1397
1398    #[test]
1399    fn upgrade_sanitizer_reconstructs_websocket_hop_fields() {
1400        let head = parse(
1401            "GET ws://welcome/socket HTTP/1.1\r\nHost: welcome\r\nConnection: keep-alive, Upgrade, X-Hop\r\nUpgrade: websocket\r\nX-Hop: secret\r\nSec-WebSocket-Key: key\r\nProxy-Authorization: secret\r\nX-HNS-Trace: secret\r\n\r\n",
1402        );
1403
1404        let sanitized = sanitize_upgrade_forward_headers(head.headers()).unwrap();
1405        let pairs: Vec<_> = sanitized
1406            .iter()
1407            .map(|header| (header.name(), header.value()))
1408            .collect();
1409
1410        assert!(pairs.contains(&("Host", "welcome")));
1411        assert!(pairs.contains(&("Sec-WebSocket-Key", "key")));
1412        assert!(pairs.contains(&("Connection", "Upgrade")));
1413        assert!(pairs.contains(&("Upgrade", "websocket")));
1414        assert!(
1415            !pairs
1416                .iter()
1417                .any(|(name, _)| name.eq_ignore_ascii_case("X-Hop"))
1418        );
1419        assert!(
1420            !pairs
1421                .iter()
1422                .any(|(name, _)| name.eq_ignore_ascii_case("Proxy-Authorization"))
1423        );
1424        assert!(
1425            !pairs
1426                .iter()
1427                .any(|(name, _)| name.eq_ignore_ascii_case("X-HNS-Trace"))
1428        );
1429    }
1430
1431    #[test]
1432    fn upgrade_sanitizer_requires_a_complete_websocket_signal() {
1433        for request in [
1434            "GET / HTTP/1.1\r\nHost: welcome\r\nUpgrade: websocket\r\n\r\n",
1435            "GET / HTTP/1.1\r\nHost: welcome\r\nConnection: Upgrade\r\n\r\n",
1436            "GET / HTTP/1.1\r\nHost: welcome\r\nConnection: Upgrade\r\nUpgrade: h2c\r\n\r\n",
1437        ] {
1438            assert!(matches!(
1439                sanitize_upgrade_forward_headers(parse(request).headers()),
1440                Err(Http1Error::InvalidHeader)
1441            ));
1442        }
1443    }
1444
1445    #[test]
1446    fn normalizes_idna_trailing_dot_and_ipv6_authorities() {
1447        let unicode = Authority::parse("B\u{00fc}cher.Example.", 443).unwrap();
1448        assert_eq!(unicode.host(), "xn--bcher-kva.example");
1449        assert!(Authority::parse("welcome..", 443).is_err());
1450        let ipv6 = Authority::parse("[2001:0db8::1]:8443", 443).unwrap();
1451        assert_eq!(ipv6.host(), "2001:db8::1");
1452        assert_eq!(ipv6.host_header(443), "[2001:db8::1]:8443");
1453    }
1454
1455    #[test]
1456    fn maps_framing_errors_to_stable_http_responses() {
1457        assert_eq!(Http1Error::BodyTooLarge.status_code(), 413);
1458        assert_eq!(Http1Error::UnsupportedTransferEncoding.status_code(), 501);
1459        assert_eq!(Http1Error::AmbiguousFraming.status_code(), 400);
1460        assert_eq!(
1461            Http1Error::InvalidContentLength.reason_phrase(),
1462            "Bad Content-Length"
1463        );
1464    }
1465}