http_net/message.rs
1//! Outgoing messages: the `Response` a server sends and the `Request` a
2//! client sends.
3//!
4//! Both builders validate as they go and remember the first problem instead
5//! of failing each call, so a message can be built in one chained
6//! expression; the problem surfaces as an error when the connection writes
7//! the message. Framing headers (`content-length`, `transfer-encoding`,
8//! `connection`, `upgrade`) are owned by the connection, which derives them
9//! from the body and the exchange; setting them by hand is refused, which
10//! makes conflicting framing on the way out impossible.
11
12use alloc::{borrow::Cow, format, string::String, vec::Vec};
13use core::fmt;
14
15use crate::{
16 error::HttpError,
17 head::{check_target, is_h2c},
18 method::Method,
19 redact::{SerializedFields, serialized_fields},
20 syntax::{check_field_value, is_protocol, is_token, trim_ows},
21};
22
23/// How an outgoing message's body is supplied.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub(crate) enum OutBody {
26 /// The whole body, sent with `content-length`.
27 Full(Cow<'static, [u8]>),
28 /// Streamed afterwards with exactly this many bytes.
29 Length(u64),
30 /// Streamed afterwards with chunked encoding.
31 Chunked,
32}
33
34/// Validates a field and appends `name: value\r\n`.
35fn push_field(
36 fields: &mut Vec<u8>,
37 name: &str,
38 value: &[u8],
39 reserved: &[&str],
40) -> Result<(), HttpError> {
41 if !is_token(name.as_bytes()) {
42 return Err(HttpError::InvalidHeaderName);
43 }
44 if reserved.iter().any(|r| r.eq_ignore_ascii_case(name)) {
45 return Err(HttpError::ReservedHeader);
46 }
47 if trim_ows(value).len() != value.len() {
48 // Surrounding whitespace would be stripped by the recipient; refusing
49 // it keeps what is sent equal to what is received.
50 return Err(HttpError::InvalidHeaderValue);
51 }
52 check_field_value(value)?;
53 fields.extend_from_slice(name.as_bytes());
54 fields.extend_from_slice(b": ");
55 fields.extend_from_slice(value);
56 fields.extend_from_slice(b"\r\n");
57 Ok(())
58}
59
60const RESPONSE_RESERVED: &[&str] = &[
61 "content-length",
62 "transfer-encoding",
63 "connection",
64 "upgrade",
65];
66const REQUEST_RESERVED: &[&str] = &[
67 "content-length",
68 "transfer-encoding",
69 "connection",
70 "upgrade",
71];
72
73/// A response for a [`ServerConn`](crate::ServerConn) to send.
74///
75/// The status line, `content-length` / `transfer-encoding`, and
76/// `connection` are written by the connection, which knows the request's
77/// method and version: a `HEAD` request gets the headers without the body,
78/// an HTTP/1.0 client never gets chunked encoding, and `connection: close`
79/// is added whenever the connection will close.
80///
81/// # Examples
82///
83/// ```
84/// use http_net::Response;
85///
86/// let ok = Response::text(200, "hello");
87/// assert_eq!(ok.status(), 200);
88///
89/// let asset = Response::new(200)
90/// .header("content-type", "text/css")
91/// .header("cache-control", "max-age=31536000, immutable")
92/// .body(&b"body { margin: 0 }"[..]);
93/// assert_eq!(asset.header_value("content-type"), Some(&b"text/css"[..]));
94/// ```
95#[derive(Clone, PartialEq, Eq)]
96pub struct Response {
97 status: u16,
98 fields: Vec<u8>,
99 pub(crate) body: OutBody,
100 pub(crate) close: bool,
101 pub(crate) upgrade: bool,
102 pub(crate) has_date: bool,
103 error: Option<HttpError>,
104}
105
106impl Response {
107 /// A response with this status, no extra headers, and an empty body.
108 ///
109 /// The status is validated when the response is written (`100..=599`,
110 /// and only `101` among the 1xx codes).
111 ///
112 /// # Examples
113 ///
114 /// ```
115 /// let r = http_net::Response::new(204);
116 /// assert_eq!(r.status(), 204);
117 /// ```
118 #[must_use]
119 pub fn new(status: u16) -> Self {
120 Self {
121 status,
122 fields: Vec::new(),
123 body: OutBody::Full(Cow::Borrowed(&[])),
124 close: false,
125 upgrade: false,
126 has_date: false,
127 error: None,
128 }
129 }
130
131 /// A `text/plain; charset=utf-8` response with `text` as its body.
132 ///
133 /// # Examples
134 ///
135 /// ```
136 /// let r = http_net::Response::text(404, "no such page");
137 /// assert_eq!(r.body_bytes(), Some(&b"no such page"[..]));
138 /// ```
139 #[must_use]
140 pub fn text(status: u16, text: &str) -> Self {
141 Self::new(status)
142 .header("content-type", "text/plain; charset=utf-8")
143 .body(text.as_bytes().to_vec())
144 }
145
146 /// The response a server sends after [`ServerConn::poll`](crate::ServerConn::poll)
147 /// returned a parse error: the status from [`HttpError::status`] (400
148 /// when it has none), a short `text/plain` body with the error's
149 /// one-line description, and [`close`](Self::close). The text is static
150 /// and never includes the bytes that were refused.
151 ///
152 /// # Examples
153 ///
154 /// ```
155 /// use http_net::{HttpError, Response, ServerConn};
156 ///
157 /// let r = Response::from_error(&HttpError::HeadersTooLarge);
158 /// assert_eq!(r.status(), 431);
159 /// assert!(r.closes());
160 ///
161 /// let mut conn = ServerConn::new();
162 /// let _ = conn.feed(b"GET / HTTP/1.1\r\n\r\n");
163 /// let err = conn.poll().unwrap_err();
164 /// let mut out = Vec::new();
165 /// conn.respond(&Response::from_error(&err), &mut out).unwrap();
166 /// assert!(out.starts_with(b"HTTP/1.1 400 Bad Request\r\n"));
167 /// ```
168 #[must_use]
169 pub fn from_error(error: &HttpError) -> Self {
170 Self::new(error.status().unwrap_or(400))
171 .header("content-type", "text/plain; charset=utf-8")
172 .body(format!("{error}").into_bytes())
173 .close()
174 }
175
176 /// A `101 Switching Protocols` response handing the connection to
177 /// `protocol` (for example `websocket`, or a `name/version` product
178 /// token). The connection writes `connection: upgrade` and
179 /// `upgrade: <protocol>`; add any protocol-specific headers (such as
180 /// `sec-websocket-accept`) with [`header`](Self::header).
181 ///
182 /// The protocol must be one the request offered (RFC 9110 section 7.8),
183 /// compared ASCII case-insensitively; a name without a version matches
184 /// an offer of that name with any version. Otherwise, and always for
185 /// `h2c` (RFC 9113 section 3.1), writing it fails with
186 /// [`HttpError::UpgradeNotOffered`].
187 ///
188 /// # Examples
189 ///
190 /// ```
191 /// let r = http_net::Response::switching_protocols("websocket")
192 /// .header("sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
193 /// assert_eq!(r.status(), 101);
194 /// assert_eq!(r.header_value("upgrade"), Some(&b"websocket"[..]));
195 /// ```
196 #[must_use]
197 pub fn switching_protocols(protocol: &str) -> Self {
198 let mut r = Self::new(101);
199 r.upgrade = true;
200 if !is_protocol(protocol.as_bytes()) {
201 r.error = Some(HttpError::InvalidHeaderValue);
202 return r;
203 }
204 if is_h2c(protocol.as_bytes()) {
205 r.error = Some(HttpError::UpgradeNotOffered);
206 return r;
207 }
208 r.fields
209 .extend_from_slice(b"connection: upgrade\r\nupgrade: ");
210 r.fields.extend_from_slice(protocol.as_bytes());
211 r.fields.extend_from_slice(b"\r\n");
212 r
213 }
214
215 /// Adds a header field. The name must be a token and the value may not
216 /// contain control characters other than tab, nor leading or trailing
217 /// whitespace. Framing headers are refused with
218 /// [`HttpError::ReservedHeader`]; the first problem is reported when the
219 /// response is written.
220 ///
221 /// # Examples
222 ///
223 /// ```
224 /// use http_net::{HttpError, Response, ServerConn};
225 ///
226 /// let bad = Response::new(200).header("content-length", "5");
227 /// let mut conn = ServerConn::new();
228 /// let _ = conn.feed(b"GET / HTTP/1.1\r\nHost: a\r\n\r\n");
229 /// let _ = conn.poll();
230 /// assert_eq!(conn.respond(&bad, &mut Vec::new()), Err(HttpError::ReservedHeader));
231 /// ```
232 #[must_use]
233 pub fn header(mut self, name: &str, value: impl AsRef<[u8]>) -> Self {
234 if self.error.is_none() {
235 let value = value.as_ref();
236 if let Err(e) = push_field(&mut self.fields, name, value, RESPONSE_RESERVED) {
237 self.error = Some(e);
238 } else if name.eq_ignore_ascii_case("date") {
239 self.has_date = true;
240 }
241 }
242 self
243 }
244
245 /// Sets the whole body; it is sent with `content-length`. Static data
246 /// (an embedded asset) is borrowed rather than copied.
247 ///
248 /// # Examples
249 ///
250 /// ```
251 /// static PAGE: &[u8] = b"<!doctype html><title>x</title>";
252 /// let r = http_net::Response::new(200).body(PAGE);
253 /// assert_eq!(r.body_bytes(), Some(PAGE));
254 /// ```
255 #[must_use]
256 pub fn body(mut self, body: impl Into<Cow<'static, [u8]>>) -> Self {
257 self.body = OutBody::Full(body.into());
258 self
259 }
260
261 /// Streams the body with chunked encoding: after
262 /// [`ServerConn::respond`](crate::ServerConn::respond), send data with
263 /// [`ServerConn::send_body`](crate::ServerConn::send_body) and end it
264 /// with [`ServerConn::end_body`](crate::ServerConn::end_body). For an
265 /// HTTP/1.0 client the connection falls back to closing the connection
266 /// to end the body.
267 ///
268 /// # Examples
269 ///
270 /// ```
271 /// let r = http_net::Response::new(200).chunked();
272 /// assert_eq!(r.body_bytes(), None);
273 /// ```
274 #[must_use]
275 pub fn chunked(mut self) -> Self {
276 self.body = OutBody::Chunked;
277 self
278 }
279
280 /// Streams a body of exactly `len` bytes, sent with `content-length`;
281 /// sending more or fewer is [`HttpError::BodyLengthMismatch`].
282 ///
283 /// # Examples
284 ///
285 /// ```
286 /// let r = http_net::Response::new(200).content_length(1 << 20);
287 /// assert_eq!(r.body_bytes(), None);
288 /// ```
289 #[must_use]
290 pub fn content_length(mut self, len: u64) -> Self {
291 self.body = OutBody::Length(len);
292 self
293 }
294
295 /// Closes the connection after this response.
296 ///
297 /// # Examples
298 ///
299 /// ```
300 /// let r = http_net::Response::new(503).close();
301 /// assert!(r.closes());
302 /// ```
303 #[must_use]
304 pub fn close(mut self) -> Self {
305 self.close = true;
306 self
307 }
308
309 /// The status code.
310 ///
311 /// # Examples
312 ///
313 /// ```
314 /// assert_eq!(http_net::Response::new(418).status(), 418);
315 /// ```
316 #[must_use]
317 pub fn status(&self) -> u16 {
318 self.status
319 }
320
321 /// Whether [`close`](Self::close) was requested.
322 ///
323 /// # Examples
324 ///
325 /// ```
326 /// assert!(!http_net::Response::new(200).closes());
327 /// ```
328 #[must_use]
329 pub fn closes(&self) -> bool {
330 self.close
331 }
332
333 /// The full body, or `None` for a streamed one.
334 ///
335 /// # Examples
336 ///
337 /// ```
338 /// assert_eq!(http_net::Response::new(200).body_bytes(), Some(&[][..]));
339 /// ```
340 #[must_use]
341 pub fn body_bytes(&self) -> Option<&[u8]> {
342 match &self.body {
343 OutBody::Full(b) => Some(b),
344 _ => None,
345 }
346 }
347
348 /// The value of the first header named `name` added to this response
349 /// (ASCII case-insensitive), including the `upgrade` of
350 /// [`switching_protocols`](Self::switching_protocols).
351 ///
352 /// # Examples
353 ///
354 /// ```
355 /// let r = http_net::Response::new(200).header("x-id", "7");
356 /// assert_eq!(r.header_value("X-ID"), Some(&b"7"[..]));
357 /// ```
358 #[must_use]
359 pub fn header_value(&self, name: &str) -> Option<&[u8]> {
360 field_value(&self.fields, name)
361 }
362
363 pub(crate) fn error(&self) -> Option<HttpError> {
364 self.error
365 }
366
367 pub(crate) fn fields(&self) -> &[u8] {
368 &self.fields
369 }
370
371 /// The protocol a `101` built with
372 /// [`switching_protocols`](Self::switching_protocols) switches to.
373 pub(crate) fn upgrade_protocol(&self) -> Option<&[u8]> {
374 if self.upgrade {
375 self.header_value("upgrade")
376 } else {
377 None
378 }
379 }
380}
381
382/// Shows status, fields, body size, and flags; the values of credential
383/// fields (`set-cookie` above all) are replaced by `<redacted N bytes>`.
384impl fmt::Debug for Response {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 f.debug_struct("Response")
387 .field("status", &self.status)
388 .field("fields", &SerializedFields(&self.fields))
389 .field("body", &body_debug(&self.body))
390 .field("close", &self.close)
391 .field("error", &self.error)
392 .finish()
393 }
394}
395
396/// A request for a [`ClientConn`](crate::ClientConn) to send.
397///
398/// HTTP/1.1 requires a `Host` field; the connection refuses to send a
399/// request without one. The request line always says HTTP/1.1.
400///
401/// # Examples
402///
403/// ```
404/// use http_net::Request;
405///
406/// let get = Request::get("/index.html").header("host", "example.com");
407/// let post = Request::post("/api")
408/// .header("host", "example.com")
409/// .header("content-type", "application/json")
410/// .body(br#"{"ok":true}"#.to_vec());
411/// assert_eq!(get.method(), "GET");
412/// assert_eq!(post.target(), "/api");
413/// ```
414#[derive(Clone, PartialEq, Eq)]
415pub struct Request {
416 method: Cow<'static, str>,
417 pub(crate) method_kind: Method,
418 target: String,
419 fields: Vec<u8>,
420 pub(crate) body: OutBody,
421 pub(crate) close: bool,
422 pub(crate) upgrade: bool,
423 pub(crate) has_host: bool,
424 error: Option<HttpError>,
425}
426
427impl Request {
428 /// A request with `method` and `target`, validated when it is sent:
429 /// the method must be a token and the target must be in a form the
430 /// method allows (`/path?query`, an absolute URI, `host:port` for
431 /// `CONNECT`, or `*` for `OPTIONS`).
432 ///
433 /// # Examples
434 ///
435 /// ```
436 /// let r = http_net::Request::new("PURGE", "/cache/a");
437 /// assert_eq!(r.method(), "PURGE");
438 /// ```
439 #[must_use]
440 pub fn new(method: &str, target: &str) -> Self {
441 let method_kind = Method::from_bytes(method.as_bytes());
442 let mut error = None;
443 if !is_token(method.as_bytes()) {
444 error = Some(HttpError::InvalidMethod);
445 } else if let Err(e) = check_target(method_kind, target.as_bytes()) {
446 error = Some(e);
447 }
448 Self {
449 method: match method_kind.as_str() {
450 Some(s) => Cow::Borrowed(s),
451 None => Cow::Owned(method.into()),
452 },
453 method_kind,
454 target: target.into(),
455 fields: Vec::new(),
456 body: OutBody::Full(Cow::Borrowed(&[])),
457 close: false,
458 upgrade: false,
459 has_host: false,
460 error,
461 }
462 }
463
464 /// A `GET` request.
465 ///
466 /// # Examples
467 ///
468 /// ```
469 /// assert_eq!(http_net::Request::get("/").method(), "GET");
470 /// ```
471 #[must_use]
472 pub fn get(target: &str) -> Self {
473 Self::new("GET", target)
474 }
475
476 /// A `POST` request.
477 ///
478 /// # Examples
479 ///
480 /// ```
481 /// assert_eq!(http_net::Request::post("/").method(), "POST");
482 /// ```
483 #[must_use]
484 pub fn post(target: &str) -> Self {
485 Self::new("POST", target)
486 }
487
488 /// A `HEAD` request: its response carries headers and no body.
489 ///
490 /// # Examples
491 ///
492 /// ```
493 /// assert_eq!(http_net::Request::head("/").method(), "HEAD");
494 /// ```
495 #[must_use]
496 pub fn head(target: &str) -> Self {
497 Self::new("HEAD", target)
498 }
499
500 /// A `PUT` request.
501 ///
502 /// # Examples
503 ///
504 /// ```
505 /// assert_eq!(http_net::Request::put("/files/a").method(), "PUT");
506 /// ```
507 #[must_use]
508 pub fn put(target: &str) -> Self {
509 Self::new("PUT", target)
510 }
511
512 /// A `DELETE` request.
513 ///
514 /// # Examples
515 ///
516 /// ```
517 /// assert_eq!(http_net::Request::delete("/files/a").method(), "DELETE");
518 /// ```
519 #[must_use]
520 pub fn delete(target: &str) -> Self {
521 Self::new("DELETE", target)
522 }
523
524 /// An `OPTIONS` request; `*` asks about the server as a whole.
525 ///
526 /// # Examples
527 ///
528 /// ```
529 /// assert_eq!(http_net::Request::options("*").target(), "*");
530 /// ```
531 #[must_use]
532 pub fn options(target: &str) -> Self {
533 Self::new("OPTIONS", target)
534 }
535
536 /// Adds a header field, validated as for [`Response::header`]. A second
537 /// `host` is refused with [`HttpError::InvalidHost`].
538 ///
539 /// # Examples
540 ///
541 /// ```
542 /// let r = http_net::Request::get("/").header("host", "a").header("accept", "*/*");
543 /// assert_eq!(r.header_value("accept"), Some(&b"*/*"[..]));
544 /// ```
545 #[must_use]
546 pub fn header(mut self, name: &str, value: impl AsRef<[u8]>) -> Self {
547 if self.error.is_none() {
548 let value = value.as_ref();
549 let is_host = name.eq_ignore_ascii_case("host");
550 if is_host && (self.has_host || !crate::syntax::is_host_value(value)) {
551 self.error = Some(HttpError::InvalidHost);
552 } else if let Err(e) = push_field(&mut self.fields, name, value, REQUEST_RESERVED) {
553 self.error = Some(e);
554 } else {
555 self.has_host |= is_host;
556 }
557 }
558 self
559 }
560
561 /// Sets the whole body, sent with `content-length`.
562 ///
563 /// # Examples
564 ///
565 /// ```
566 /// let r = http_net::Request::post("/").body(&b"x=1"[..]);
567 /// assert_eq!(r.body_bytes(), Some(&b"x=1"[..]));
568 /// ```
569 #[must_use]
570 pub fn body(mut self, body: impl Into<Cow<'static, [u8]>>) -> Self {
571 self.body = OutBody::Full(body.into());
572 self
573 }
574
575 /// Streams the body with chunked encoding through
576 /// [`ClientConn::send_body`](crate::ClientConn::send_body) and
577 /// [`ClientConn::end_body`](crate::ClientConn::end_body).
578 ///
579 /// # Examples
580 ///
581 /// ```
582 /// assert_eq!(http_net::Request::post("/").chunked().body_bytes(), None);
583 /// ```
584 #[must_use]
585 pub fn chunked(mut self) -> Self {
586 self.body = OutBody::Chunked;
587 self
588 }
589
590 /// Streams a body of exactly `len` bytes.
591 ///
592 /// # Examples
593 ///
594 /// ```
595 /// assert_eq!(http_net::Request::post("/").content_length(4).body_bytes(), None);
596 /// ```
597 #[must_use]
598 pub fn content_length(mut self, len: u64) -> Self {
599 self.body = OutBody::Length(len);
600 self
601 }
602
603 /// Asks the server to close the connection after its response.
604 ///
605 /// # Examples
606 ///
607 /// ```
608 /// let _ = http_net::Request::get("/").header("host", "a").close();
609 /// ```
610 #[must_use]
611 pub fn close(mut self) -> Self {
612 self.close = true;
613 self
614 }
615
616 /// Asks to switch the connection to `protocol` (writes
617 /// `connection: upgrade` and `upgrade: <protocol>`). A `101` answer
618 /// hands the connection over; see
619 /// [`ClientEvent::Upgraded`](crate::ClientEvent::Upgraded).
620 ///
621 /// `protocol` is a token or a `name/version` product token. `h2c` is
622 /// refused with [`HttpError::UpgradeNotOffered`]: RFC 9113 section 3.1
623 /// deprecates the HTTP/2 upgrade.
624 ///
625 /// # Examples
626 ///
627 /// ```
628 /// use http_net::{ClientConn, HttpError, Request};
629 ///
630 /// let r = Request::get("/rpc").header("host", "a").upgrade("websocket");
631 /// assert_eq!(r.header_value("upgrade"), Some(&b"websocket"[..]));
632 ///
633 /// let h2c = Request::get("/").header("host", "a").upgrade("h2c");
634 /// assert_eq!(
635 /// ClientConn::new().send_request(&h2c, &mut Vec::new()),
636 /// Err(HttpError::UpgradeNotOffered)
637 /// );
638 /// ```
639 #[must_use]
640 pub fn upgrade(mut self, protocol: &str) -> Self {
641 if self.error.is_none() {
642 if is_h2c(protocol.as_bytes()) {
643 self.error = Some(HttpError::UpgradeNotOffered);
644 } else if is_protocol(protocol.as_bytes()) {
645 self.upgrade = true;
646 self.fields
647 .extend_from_slice(b"connection: upgrade\r\nupgrade: ");
648 self.fields.extend_from_slice(protocol.as_bytes());
649 self.fields.extend_from_slice(b"\r\n");
650 } else {
651 self.error = Some(HttpError::InvalidHeaderValue);
652 }
653 }
654 self
655 }
656
657 /// The method text.
658 ///
659 /// # Examples
660 ///
661 /// ```
662 /// assert_eq!(http_net::Request::new("OPTIONS", "*").method(), "OPTIONS");
663 /// ```
664 #[must_use]
665 pub fn method(&self) -> &str {
666 &self.method
667 }
668
669 /// The request target.
670 ///
671 /// # Examples
672 ///
673 /// ```
674 /// assert_eq!(http_net::Request::get("/a?b").target(), "/a?b");
675 /// ```
676 #[must_use]
677 pub fn target(&self) -> &str {
678 &self.target
679 }
680
681 /// The full body, or `None` for a streamed one.
682 ///
683 /// # Examples
684 ///
685 /// ```
686 /// assert_eq!(http_net::Request::get("/").body_bytes(), Some(&[][..]));
687 /// ```
688 #[must_use]
689 pub fn body_bytes(&self) -> Option<&[u8]> {
690 match &self.body {
691 OutBody::Full(b) => Some(b),
692 _ => None,
693 }
694 }
695
696 /// The value of the first header named `name` added to this request.
697 ///
698 /// # Examples
699 ///
700 /// ```
701 /// let r = http_net::Request::get("/").header("host", "a");
702 /// assert_eq!(r.header_value("HOST"), Some(&b"a"[..]));
703 /// ```
704 #[must_use]
705 pub fn header_value(&self, name: &str) -> Option<&[u8]> {
706 field_value(&self.fields, name)
707 }
708
709 pub(crate) fn error(&self) -> Option<HttpError> {
710 self.error
711 }
712
713 pub(crate) fn fields(&self) -> &[u8] {
714 &self.fields
715 }
716}
717
718/// Shows method, target, fields, and body size; the values of credential
719/// fields (`authorization`, `proxy-authorization`, `cookie`) are replaced by
720/// `<redacted N bytes>`.
721impl fmt::Debug for Request {
722 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
723 f.debug_struct("Request")
724 .field("method", &self.method)
725 .field("target", &self.target)
726 .field("fields", &SerializedFields(&self.fields))
727 .field("body", &body_debug(&self.body))
728 .field("error", &self.error)
729 .finish()
730 }
731}
732
733fn body_debug(body: &OutBody) -> String {
734 match body {
735 OutBody::Full(b) => format!("{} bytes", b.len()),
736 OutBody::Length(n) => format!("streamed, {n} bytes"),
737 OutBody::Chunked => "streamed, chunked".into(),
738 }
739}
740
741/// Looks a header up in serialized `name: value\r\n` lines.
742fn field_value<'a>(fields: &'a [u8], name: &str) -> Option<&'a [u8]> {
743 serialized_fields(fields)
744 .find(|(n, _)| n.eq_ignore_ascii_case(name.as_bytes()))
745 .map(|(_, v)| v)
746}
747
748#[cfg(test)]
749mod tests {
750 use alloc::string::ToString;
751
752 use super::*;
753
754 #[test]
755 fn test_response_refuses_reserved_and_invalid_headers() {
756 assert_eq!(
757 Response::new(200)
758 .header("Transfer-Encoding", "chunked")
759 .error(),
760 Some(HttpError::ReservedHeader)
761 );
762 assert_eq!(
763 Response::new(200).header("bad name", "x").error(),
764 Some(HttpError::InvalidHeaderName)
765 );
766 assert_eq!(
767 Response::new(200).header("x", "a\r\nInjected: 1").error(),
768 Some(HttpError::BareCr)
769 );
770 assert_eq!(
771 Response::new(200).header("x", " padded").error(),
772 Some(HttpError::InvalidHeaderValue)
773 );
774 // The first error sticks.
775 let r = Response::new(200)
776 .header("a b", "1")
777 .header("connection", "close");
778 assert_eq!(r.error(), Some(HttpError::InvalidHeaderName));
779 }
780
781 #[test]
782 fn test_request_validates_method_target_and_host() {
783 assert_eq!(
784 Request::new("G T", "/").error(),
785 Some(HttpError::InvalidMethod)
786 );
787 assert_eq!(
788 Request::get("no-slash").error(),
789 Some(HttpError::InvalidTarget)
790 );
791 assert_eq!(
792 Request::get("/")
793 .header("host", "a")
794 .header("Host", "b")
795 .error(),
796 Some(HttpError::InvalidHost)
797 );
798 assert_eq!(
799 Request::get("/").header("host", "a b").error(),
800 Some(HttpError::InvalidHost)
801 );
802 assert!(Request::get("/").header("host", "a").has_host);
803 assert_eq!(
804 Request::get("/").upgrade("a b").error(),
805 Some(HttpError::InvalidHeaderValue)
806 );
807 }
808
809 #[test]
810 fn test_from_error_uses_status_text_and_close() {
811 let r = Response::from_error(&HttpError::UnsupportedTransferEncoding);
812 assert_eq!(r.status(), 501);
813 assert!(r.closes());
814 assert_eq!(
815 r.header_value("content-type"),
816 Some(&b"text/plain; charset=utf-8"[..])
817 );
818 assert_eq!(
819 r.body_bytes(),
820 Some(
821 HttpError::UnsupportedTransferEncoding
822 .to_string()
823 .as_bytes()
824 )
825 );
826 // Errors without a request status still get a well-formed 400.
827 assert_eq!(
828 Response::from_error(&HttpError::UnexpectedEof).status(),
829 400
830 );
831 assert_eq!(
832 Response::from_error(&HttpError::InvalidState("x")).error(),
833 None
834 );
835 }
836
837 #[test]
838 fn test_request_shortcuts_set_the_method() {
839 for (r, m) in [
840 (Request::head("/"), "HEAD"),
841 (Request::put("/"), "PUT"),
842 (Request::delete("/"), "DELETE"),
843 (Request::options("*"), "OPTIONS"),
844 ] {
845 assert_eq!(r.method(), m);
846 assert_eq!(r.error(), None);
847 }
848 assert_eq!(Request::put("*").error(), Some(HttpError::InvalidTarget));
849 assert_eq!(Request::head("/").method_kind, Method::Head);
850 }
851
852 #[test]
853 fn test_upgrade_builders_accept_product_tokens_and_refuse_h2c() {
854 let r = Response::switching_protocols("foo/2");
855 assert_eq!(r.error(), None);
856 assert_eq!(r.upgrade_protocol(), Some(&b"foo/2"[..]));
857 assert_eq!(
858 Response::switching_protocols("h2c").error(),
859 Some(HttpError::UpgradeNotOffered)
860 );
861 assert_eq!(
862 Response::switching_protocols("H2C").error(),
863 Some(HttpError::UpgradeNotOffered)
864 );
865 assert_eq!(
866 Response::switching_protocols("foo/").error(),
867 Some(HttpError::InvalidHeaderValue)
868 );
869 assert_eq!(Response::new(200).upgrade_protocol(), None);
870 assert_eq!(
871 Request::get("/").upgrade("h2c").error(),
872 Some(HttpError::UpgradeNotOffered)
873 );
874 assert!(Request::get("/").upgrade("websocket/13").upgrade);
875 }
876
877 /// Threat T-35: credential values never reach `Debug` output.
878 #[test]
879 fn test_debug_redacts_credential_fields() {
880 let r = Response::new(200)
881 .header("Set-Cookie", "sid=s3cr3t; HttpOnly")
882 .header("x-trace", "visible");
883 let shown = alloc::format!("{r:?}");
884 assert!(!shown.contains("s3cr3t"), "{shown}");
885 assert!(shown.contains("<redacted 20 bytes>"), "{shown}");
886 assert!(shown.contains("visible"), "{shown}");
887 assert_eq!(
888 r.header_value("set-cookie"),
889 Some(&b"sid=s3cr3t; HttpOnly"[..])
890 );
891
892 let q = Request::get("/")
893 .header("host", "a")
894 .header("Authorization", "Bearer t0k3n")
895 .header("PROXY-AUTHORIZATION", "Basic cDpx")
896 .header("cookie", "a=b");
897 let shown = alloc::format!("{q:?}");
898 for secret in ["t0k3n", "cDpx", "a=b"] {
899 assert!(!shown.contains(secret), "{shown}");
900 }
901 assert!(shown.contains("\"host\", \"a\""), "{shown}");
902 }
903
904 #[test]
905 fn test_field_lookup_and_debug() {
906 let r = Response::switching_protocols("websocket").header("x", "");
907 assert_eq!(r.header_value("connection"), Some(&b"upgrade"[..]));
908 assert_eq!(r.header_value("x"), Some(&b""[..]));
909 assert!(alloc::format!("{r:?}").contains("Response"));
910 assert_eq!(
911 Response::switching_protocols("a b").error(),
912 Some(HttpError::InvalidHeaderValue)
913 );
914 }
915}