use alloc::{borrow::Cow, format, string::String, vec::Vec};
use core::fmt;
use crate::{
error::HttpError,
head::{Method, check_target},
syntax::{check_field_value, is_token, trim_ows},
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum OutBody {
Full(Cow<'static, [u8]>),
Length(u64),
Chunked,
}
fn push_field(
fields: &mut Vec<u8>,
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)?;
fields.extend_from_slice(name.as_bytes());
fields.extend_from_slice(b": ");
fields.extend_from_slice(value);
fields.extend_from_slice(b"\r\n");
Ok(())
}
const RESPONSE_RESERVED: &[&str] = &[
"content-length",
"transfer-encoding",
"connection",
"upgrade",
];
const REQUEST_RESERVED: &[&str] = &[
"content-length",
"transfer-encoding",
"connection",
"upgrade",
];
#[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 switching_protocols(protocol: &str) -> Self {
let mut r = Self::new(101);
r.upgrade = true;
if !is_token(protocol.as_bytes()) {
r.error = Some(HttpError::InvalidHeaderValue);
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
}
#[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
}
}
impl fmt::Debug for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Response")
.field("status", &self.status)
.field("fields", &String::from_utf8_lossy(&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,
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,
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 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 upgrade(mut self, protocol: &str) -> Self {
if self.error.is_none() {
if is_token(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", &String::from_utf8_lossy(&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]> {
fields
.split(|&b| b == b'\n')
.filter_map(|line| {
let line = line.strip_suffix(b"\r")?;
let colon = line.iter().position(|&b| b == b':')?;
Some((&line[..colon], line.get(colon + 2..).unwrap_or(&[])))
})
.find(|(n, _)| n.eq_ignore_ascii_case(name.as_bytes()))
.map(|(_, v)| v)
}
#[must_use]
pub const fn reason_phrase(status: u16) -> &'static str {
match status {
100 => "Continue",
101 => "Switching Protocols",
200 => "OK",
201 => "Created",
202 => "Accepted",
203 => "Non-Authoritative Information",
204 => "No Content",
205 => "Reset Content",
206 => "Partial Content",
300 => "Multiple Choices",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
307 => "Temporary Redirect",
308 => "Permanent Redirect",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
408 => "Request Timeout",
409 => "Conflict",
410 => "Gone",
411 => "Length Required",
412 => "Precondition Failed",
413 => "Content Too Large",
414 => "URI Too Long",
415 => "Unsupported Media Type",
416 => "Range Not Satisfiable",
417 => "Expectation Failed",
421 => "Misdirected Request",
422 => "Unprocessable Content",
426 => "Upgrade Required",
428 => "Precondition Required",
429 => "Too Many Requests",
431 => "Request Header Fields Too Large",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
505 => "HTTP Version Not Supported",
_ => "",
}
}
#[cfg(test)]
mod tests {
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_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)
);
}
}