use alloc::{borrow::Cow, format, string::String, vec::Vec};
use core::fmt;
use crate::{
error::HttpError,
forward::is_hop_by_hop,
head::{BodyFraming, RequestHead, check_target, is_h2c},
method::Method,
redact::{SerializedFields, serialized_fields},
syntax::{check_field_value, is_protocol, is_token, trim_ows},
write::Output,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum OutBody {
Full(Cow<'static, [u8]>),
Length(u64),
Chunked,
}
const TRAILER_FORBIDDEN: &[&str] = &[
"content-length",
"transfer-encoding",
"host",
"connection",
"keep-alive",
"te",
"trailer",
"upgrade",
"proxy-connection",
"expect",
"max-forwards",
"range",
"if-match",
"if-none-match",
"if-modified-since",
"if-unmodified-since",
"if-range",
"authorization",
"proxy-authorization",
"www-authenticate",
"proxy-authenticate",
"cookie",
"set-cookie",
"content-type",
"content-encoding",
"content-range",
"cache-control",
"expires",
"date",
"location",
"retry-after",
"vary",
"age",
];
pub(crate) fn push_last_chunk_with_trailers<O: Output + ?Sized>(
out: &mut O,
trailers: &[(&str, &[u8])],
) -> Result<(), HttpError> {
check_trailers(trailers)?;
out.put(b"0\r\n");
for &(name, value) in trailers {
write_field(out, name, value);
}
out.put(b"\r\n");
Ok(())
}
pub(crate) fn check_upgrade_protocol(protocol: &[u8]) -> Result<(), HttpError> {
if !is_protocol(protocol) {
Err(HttpError::InvalidHeaderValue)
} else if is_h2c(protocol) {
Err(HttpError::UpgradeNotOffered)
} else {
Ok(())
}
}
pub(crate) fn check_response_field(name: &str, value: &[u8]) -> Result<bool, HttpError> {
check_field(name, value, RESPONSE_RESERVED)?;
Ok(name.eq_ignore_ascii_case("date"))
}
pub(crate) fn check_trailers(trailers: &[(&str, &[u8])]) -> Result<(), HttpError> {
for &(name, value) in trailers {
if TRAILER_FORBIDDEN
.iter()
.any(|f| f.eq_ignore_ascii_case(name))
{
return Err(HttpError::TrailerNotAllowed);
}
check_field(name, value, &[])?;
}
Ok(())
}
fn check_field(name: &str, value: &[u8], reserved: &[&str]) -> Result<(), HttpError> {
if !is_token(name.as_bytes()) {
return Err(HttpError::InvalidHeaderName);
}
if reserved.iter().any(|r| r.eq_ignore_ascii_case(name)) {
return Err(HttpError::ReservedHeader);
}
if trim_ows(value).len() != value.len() {
return Err(HttpError::InvalidHeaderValue);
}
check_field_value(value)
}
pub(crate) fn write_field<O: Output + ?Sized>(out: &mut O, name: &str, value: &[u8]) {
out.put(name.as_bytes());
out.put(b": ");
out.put(value);
out.put(b"\r\n");
}
fn push_field(
fields: &mut Vec<u8>,
name: &str,
value: &[u8],
reserved: &[&str],
) -> Result<(), HttpError> {
check_field(name, value, reserved)?;
write_field(fields, name, value);
Ok(())
}
const RESPONSE_RESERVED: &[&str] = &[
"content-length",
"transfer-encoding",
"connection",
"upgrade",
"trailer",
"keep-alive",
"proxy-connection",
"te",
];
const REQUEST_RESERVED: &[&str] = &[
"content-length",
"transfer-encoding",
"connection",
"upgrade",
"trailer",
"keep-alive",
"proxy-connection",
];
#[derive(Clone, PartialEq, Eq)]
pub struct Response {
status: u16,
fields: Vec<u8>,
pub(crate) body: OutBody,
pub(crate) close: bool,
pub(crate) upgrade: bool,
pub(crate) has_date: bool,
error: Option<HttpError>,
}
impl Response {
#[must_use]
pub fn new(status: u16) -> Self {
Self {
status,
fields: Vec::new(),
body: OutBody::Full(Cow::Borrowed(&[])),
close: false,
upgrade: false,
has_date: false,
error: None,
}
}
#[must_use]
pub fn text(status: u16, text: &str) -> Self {
Self::new(status)
.header("content-type", "text/plain; charset=utf-8")
.body(text.as_bytes().to_vec())
}
#[must_use]
pub fn from_error(error: &HttpError) -> Self {
Self::new(error.status().unwrap_or(400))
.header("content-type", "text/plain; charset=utf-8")
.body(format!("{error}").into_bytes())
.close()
}
#[must_use]
pub fn switching_protocols(protocol: &str) -> Self {
let mut r = Self::new(101);
r.upgrade = true;
if let Err(e) = check_upgrade_protocol(protocol.as_bytes()) {
r.error = Some(e);
return r;
}
r.fields
.extend_from_slice(b"connection: upgrade\r\nupgrade: ");
r.fields.extend_from_slice(protocol.as_bytes());
r.fields.extend_from_slice(b"\r\n");
r
}
#[must_use]
pub fn header(mut self, name: &str, value: impl AsRef<[u8]>) -> Self {
if self.error.is_none() {
let value = value.as_ref();
if let Err(e) = push_field(&mut self.fields, name, value, RESPONSE_RESERVED) {
self.error = Some(e);
} else if name.eq_ignore_ascii_case("date") {
self.has_date = true;
}
}
self
}
#[must_use]
pub fn body(mut self, body: impl Into<Cow<'static, [u8]>>) -> Self {
self.body = OutBody::Full(body.into());
self
}
#[must_use]
pub fn chunked(mut self) -> Self {
self.body = OutBody::Chunked;
self
}
#[must_use]
pub fn content_length(mut self, len: u64) -> Self {
self.body = OutBody::Length(len);
self
}
#[must_use]
pub fn close(mut self) -> Self {
self.close = true;
self
}
pub fn clear(&mut self, status: u16) {
self.status = status;
self.fields.clear();
self.body = OutBody::Full(Cow::Borrowed(&[]));
self.close = false;
self.upgrade = false;
self.has_date = false;
self.error = None;
}
#[must_use]
pub fn status(&self) -> u16 {
self.status
}
#[must_use]
pub fn closes(&self) -> bool {
self.close
}
#[must_use]
pub fn body_bytes(&self) -> Option<&[u8]> {
match &self.body {
OutBody::Full(b) => Some(b),
_ => None,
}
}
#[must_use]
pub fn header_value(&self, name: &str) -> Option<&[u8]> {
field_value(&self.fields, name)
}
pub(crate) fn error(&self) -> Option<HttpError> {
self.error
}
pub(crate) fn fields(&self) -> &[u8] {
&self.fields
}
pub(crate) fn upgrade_protocol(&self) -> Option<&[u8]> {
if self.upgrade {
self.header_value("upgrade")
} else {
None
}
}
}
impl fmt::Debug for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Response")
.field("status", &self.status)
.field("fields", &SerializedFields(&self.fields))
.field("body", &body_debug(&self.body))
.field("close", &self.close)
.field("error", &self.error)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct Request {
method: Cow<'static, str>,
pub(crate) method_kind: Method,
target: String,
fields: Vec<u8>,
pub(crate) body: OutBody,
pub(crate) close: bool,
pub(crate) upgrade: bool,
pub(crate) has_host: bool,
pub(crate) expect_continue: bool,
error: Option<HttpError>,
}
impl Request {
#[must_use]
pub fn new(method: &str, target: &str) -> Self {
let method_kind = Method::from_bytes(method.as_bytes());
let mut error = None;
if !is_token(method.as_bytes()) {
error = Some(HttpError::InvalidMethod);
} else if let Err(e) = check_target(method_kind, target.as_bytes()) {
error = Some(e);
}
Self {
method: match method_kind.as_str() {
Some(s) => Cow::Borrowed(s),
None => Cow::Owned(method.into()),
},
method_kind,
target: target.into(),
fields: Vec::new(),
body: OutBody::Full(Cow::Borrowed(&[])),
close: false,
upgrade: false,
has_host: false,
expect_continue: false,
error,
}
}
#[must_use]
pub fn get(target: &str) -> Self {
Self::new("GET", target)
}
#[must_use]
pub fn post(target: &str) -> Self {
Self::new("POST", target)
}
#[must_use]
pub fn head(target: &str) -> Self {
Self::new("HEAD", target)
}
#[must_use]
pub fn put(target: &str) -> Self {
Self::new("PUT", target)
}
#[must_use]
pub fn delete(target: &str) -> Self {
Self::new("DELETE", target)
}
#[must_use]
pub fn options(target: &str) -> Self {
Self::new("OPTIONS", target)
}
#[must_use]
pub fn forward_from(head: &RequestHead<'_>) -> Self {
let mut request = Self::new(head.method_str(), head.target());
if head.is_upgrade() {
if request.error.is_none() {
request.error = Some(HttpError::InvalidState(
"an upgrade request is hop-by-hop; tunnel it instead of forwarding it",
));
}
return request;
}
for (name, value) in head.headers() {
if !is_hop_by_hop(name, head.connection_options())
&& !name.eq_ignore_ascii_case("content-length")
{
request = request.header(name, value);
}
}
match head.framing() {
BodyFraming::Chunked => request.chunked(),
BodyFraming::Length(n) => request.content_length(n),
_ => request,
}
}
#[must_use]
pub fn header(mut self, name: &str, value: impl AsRef<[u8]>) -> Self {
if self.error.is_none() {
let value = value.as_ref();
let is_host = name.eq_ignore_ascii_case("host");
if is_host && (self.has_host || !crate::syntax::is_host_value(value)) {
self.error = Some(HttpError::InvalidHost);
} else if let Err(e) = push_field(&mut self.fields, name, value, REQUEST_RESERVED) {
self.error = Some(e);
} else {
self.has_host |= is_host;
}
}
self
}
#[must_use]
pub fn body(mut self, body: impl Into<Cow<'static, [u8]>>) -> Self {
self.body = OutBody::Full(body.into());
self
}
#[must_use]
pub fn chunked(mut self) -> Self {
self.body = OutBody::Chunked;
self
}
#[must_use]
pub fn content_length(mut self, len: u64) -> Self {
self.body = OutBody::Length(len);
self
}
#[must_use]
pub fn close(mut self) -> Self {
self.close = true;
self
}
#[must_use]
pub fn expect_continue(mut self) -> Self {
if self.error.is_none() && !self.expect_continue {
self.expect_continue = true;
self.fields.extend_from_slice(b"expect: 100-continue\r\n");
}
self
}
#[must_use]
pub fn upgrade(mut self, protocol: &str) -> Self {
if self.error.is_none() {
if is_h2c(protocol.as_bytes()) {
self.error = Some(HttpError::UpgradeNotOffered);
} else if is_protocol(protocol.as_bytes()) {
self.upgrade = true;
self.fields
.extend_from_slice(b"connection: upgrade\r\nupgrade: ");
self.fields.extend_from_slice(protocol.as_bytes());
self.fields.extend_from_slice(b"\r\n");
} else {
self.error = Some(HttpError::InvalidHeaderValue);
}
}
self
}
#[must_use]
pub fn method(&self) -> &str {
&self.method
}
#[must_use]
pub fn target(&self) -> &str {
&self.target
}
#[must_use]
pub fn body_bytes(&self) -> Option<&[u8]> {
match &self.body {
OutBody::Full(b) => Some(b),
_ => None,
}
}
#[must_use]
pub fn header_value(&self, name: &str) -> Option<&[u8]> {
field_value(&self.fields, name)
}
pub(crate) fn error(&self) -> Option<HttpError> {
self.error
}
pub(crate) fn fields(&self) -> &[u8] {
&self.fields
}
}
impl fmt::Debug for Request {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Request")
.field("method", &self.method)
.field("target", &self.target)
.field("fields", &SerializedFields(&self.fields))
.field("body", &body_debug(&self.body))
.field("error", &self.error)
.finish()
}
}
fn body_debug(body: &OutBody) -> String {
match body {
OutBody::Full(b) => format!("{} bytes", b.len()),
OutBody::Length(n) => format!("streamed, {n} bytes"),
OutBody::Chunked => "streamed, chunked".into(),
}
}
fn field_value<'a>(fields: &'a [u8], name: &str) -> Option<&'a [u8]> {
serialized_fields(fields)
.find(|(n, _)| n.eq_ignore_ascii_case(name.as_bytes()))
.map(|(_, v)| v)
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
fn test_response_refuses_reserved_and_invalid_headers() {
assert_eq!(
Response::new(200)
.header("Transfer-Encoding", "chunked")
.error(),
Some(HttpError::ReservedHeader)
);
assert_eq!(
Response::new(200).header("bad name", "x").error(),
Some(HttpError::InvalidHeaderName)
);
assert_eq!(
Response::new(200).header("x", "a\r\nInjected: 1").error(),
Some(HttpError::BareCr)
);
assert_eq!(
Response::new(200).header("x", " padded").error(),
Some(HttpError::InvalidHeaderValue)
);
let r = Response::new(200)
.header("a b", "1")
.header("connection", "close");
assert_eq!(r.error(), Some(HttpError::InvalidHeaderName));
}
#[test]
fn test_request_validates_method_target_and_host() {
assert_eq!(
Request::new("G T", "/").error(),
Some(HttpError::InvalidMethod)
);
assert_eq!(
Request::get("no-slash").error(),
Some(HttpError::InvalidTarget)
);
assert_eq!(
Request::get("/")
.header("host", "a")
.header("Host", "b")
.error(),
Some(HttpError::InvalidHost)
);
assert_eq!(
Request::get("/").header("host", "a b").error(),
Some(HttpError::InvalidHost)
);
assert!(Request::get("/").header("host", "a").has_host);
assert_eq!(
Request::get("/").upgrade("a b").error(),
Some(HttpError::InvalidHeaderValue)
);
}
#[test]
fn test_from_error_uses_status_text_and_close() {
let r = Response::from_error(&HttpError::UnsupportedTransferEncoding);
assert_eq!(r.status(), 501);
assert!(r.closes());
assert_eq!(
r.header_value("content-type"),
Some(&b"text/plain; charset=utf-8"[..])
);
assert_eq!(
r.body_bytes(),
Some(
HttpError::UnsupportedTransferEncoding
.to_string()
.as_bytes()
)
);
assert_eq!(
Response::from_error(&HttpError::UnexpectedEof).status(),
400
);
assert_eq!(
Response::from_error(&HttpError::InvalidState("x")).error(),
None
);
}
#[test]
fn test_request_shortcuts_set_the_method() {
for (r, m) in [
(Request::head("/"), "HEAD"),
(Request::put("/"), "PUT"),
(Request::delete("/"), "DELETE"),
(Request::options("*"), "OPTIONS"),
] {
assert_eq!(r.method(), m);
assert_eq!(r.error(), None);
}
assert_eq!(Request::put("*").error(), Some(HttpError::InvalidTarget));
assert_eq!(Request::head("/").method_kind, Method::Head);
}
#[test]
fn test_upgrade_builders_accept_product_tokens_and_refuse_h2c() {
let r = Response::switching_protocols("foo/2");
assert_eq!(r.error(), None);
assert_eq!(r.upgrade_protocol(), Some(&b"foo/2"[..]));
assert_eq!(
Response::switching_protocols("h2c").error(),
Some(HttpError::UpgradeNotOffered)
);
assert_eq!(
Response::switching_protocols("H2C").error(),
Some(HttpError::UpgradeNotOffered)
);
assert_eq!(
Response::switching_protocols("foo/").error(),
Some(HttpError::InvalidHeaderValue)
);
assert_eq!(Response::new(200).upgrade_protocol(), None);
assert_eq!(
Request::get("/").upgrade("h2c").error(),
Some(HttpError::UpgradeNotOffered)
);
assert!(Request::get("/").upgrade("websocket/13").upgrade);
}
#[test]
fn test_debug_redacts_credential_fields() {
let r = Response::new(200)
.header("Set-Cookie", "sid=s3cr3t; HttpOnly")
.header("x-trace", "visible");
let shown = alloc::format!("{r:?}");
assert!(!shown.contains("s3cr3t"), "{shown}");
assert!(shown.contains("<redacted 20 bytes>"), "{shown}");
assert!(shown.contains("visible"), "{shown}");
assert_eq!(
r.header_value("set-cookie"),
Some(&b"sid=s3cr3t; HttpOnly"[..])
);
let q = Request::get("/")
.header("host", "a")
.header("Authorization", "Bearer t0k3n")
.header("PROXY-AUTHORIZATION", "Basic cDpx")
.header("cookie", "a=b");
let shown = alloc::format!("{q:?}");
for secret in ["t0k3n", "cDpx", "a=b"] {
assert!(!shown.contains(secret), "{shown}");
}
assert!(shown.contains("\"host\", \"a\""), "{shown}");
}
#[test]
fn test_field_lookup_and_debug() {
let r = Response::switching_protocols("websocket").header("x", "");
assert_eq!(r.header_value("connection"), Some(&b"upgrade"[..]));
assert_eq!(r.header_value("x"), Some(&b""[..]));
assert!(alloc::format!("{r:?}").contains("Response"));
assert_eq!(
Response::switching_protocols("a b").error(),
Some(HttpError::InvalidHeaderValue)
);
}
}