1use 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 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 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 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
429pub 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 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 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
599pub 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
617pub 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 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 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}