From 0f856ed70aa4323d27752a34b18ac0313cc9fed2 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 11:17:23 +0200 Subject: [PATCH 01/81] Add initial version of heph-http This current commit is rather bare-bones. It does have a (barely functional) HttpServer which can receive HTTP requests and send HTTP responses. But plenty of things are missing (not to mention tests). --- Cargo.toml | 2 + http/Cargo.toml | 15 + http/LICENSE | 1 + http/Makefile | 1 + http/examples/my_ip.rs | 128 +++++++ http/src/body.rs | 36 ++ http/src/from_bytes.rs | 35 ++ http/src/header.rs | 369 ++++++++++++++++++++ http/src/lib.rs | 82 +++++ http/src/method.rs | 117 +++++++ http/src/request.rs | 63 ++++ http/src/response.rs | 36 ++ http/src/server.rs | 754 ++++++++++++++++++++++++++++++++++++++++ http/src/status_code.rs | 235 +++++++++++++ http/src/version.rs | 55 +++ 15 files changed, 1929 insertions(+) create mode 100644 http/Cargo.toml create mode 120000 http/LICENSE create mode 120000 http/Makefile create mode 100644 http/examples/my_ip.rs create mode 100644 http/src/body.rs create mode 100644 http/src/from_bytes.rs create mode 100644 http/src/header.rs create mode 100644 http/src/lib.rs create mode 100644 http/src/method.rs create mode 100644 http/src/request.rs create mode 100644 http/src/response.rs create mode 100644 http/src/server.rs create mode 100644 http/src/status_code.rs create mode 100644 http/src/version.rs diff --git a/Cargo.toml b/Cargo.toml index 11ae514a9..73784860a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ harness = false [workspace] members = [ + "http", "tools", + "benches/timers_container", ] diff --git a/http/Cargo.toml b/http/Cargo.toml new file mode 100644 index 000000000..c73cd503d --- /dev/null +++ b/http/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "heph-http" +version = "0.1.0" +edition = "2018" + +[dependencies] +heph = { version = "0.3.0", path = "../", default-features = false } +httparse = { version = "1.4.0", default-features = false } +log = { version = "0.4.8", default-features = false } +socket2 = { version = "0.4.0", default-features = false, features = ["all"] } + +[dev-dependencies] +getrandom = { version = "0.2.2", default-features = false, features = ["std"] } +# Enable logging panics via `std-logger`. +std-logger = { version = "0.4.0", default-features = false, features = ["log-panic", "nightly"] } diff --git a/http/LICENSE b/http/LICENSE new file mode 120000 index 000000000..ea5b60640 --- /dev/null +++ b/http/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/http/Makefile b/http/Makefile new file mode 120000 index 000000000..d0b0e8e00 --- /dev/null +++ b/http/Makefile @@ -0,0 +1 @@ +../Makefile \ No newline at end of file diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs new file mode 100644 index 000000000..f2b7176bc --- /dev/null +++ b/http/examples/my_ip.rs @@ -0,0 +1,128 @@ +#![feature(never_type)] + +use std::borrow::Cow; +use std::io; +use std::net::SocketAddr; + +use heph::actor::{self, Actor, NewActor}; +use heph::net::TcpStream; +use heph::rt::{self, Runtime, ThreadLocal}; +use heph::spawn::options::{ActorOptions, Priority}; +use heph::supervisor::{Supervisor, SupervisorStrategy}; +use heph_http::{ + self as http, Header, HeaderName, Headers, HttpServer, Method, Response, StatusCode, Version, +}; +use log::{debug, error, info, warn}; + +fn main() -> Result<(), rt::Error> { + std_logger::init(); + + let actor = http_actor as fn(_, _, _) -> _; + let address = "127.0.0.1:7890".parse().unwrap(); + let server = HttpServer::setup(address, conn_supervisor, actor, ActorOptions::default()) + .map_err(rt::Error::setup)?; + + let mut runtime = Runtime::setup().use_all_cores().build()?; + runtime.run_on_workers(move |mut runtime_ref| -> io::Result<()> { + let options = ActorOptions::default().with_priority(Priority::LOW); + let server_ref = runtime_ref.try_spawn_local(ServerSupervisor, server, (), options)?; + + runtime_ref.receive_signals(server_ref.try_map()); + Ok(()) + })?; + info!("listening on {}", address); + runtime.start() +} + +/// Our supervisor for the TCP server. +#[derive(Copy, Clone, Debug)] +struct ServerSupervisor; + +impl Supervisor for ServerSupervisor +where + NA: NewActor, + NA::Actor: Actor>, +{ + fn decide(&mut self, err: http::server::Error) -> SupervisorStrategy<()> { + use http::server::Error::*; + match err { + Accept(err) => { + error!("error accepting new connection: {}", err); + SupervisorStrategy::Restart(()) + } + NewActor(_) => unreachable!(), + } + } + + fn decide_on_restart_error(&mut self, err: io::Error) -> SupervisorStrategy<()> { + error!("error restarting the TCP server: {}", err); + SupervisorStrategy::Stop + } + + fn second_restart_error(&mut self, err: io::Error) { + error!("error restarting the actor a second time: {}", err); + } +} + +fn conn_supervisor(err: io::Error) -> SupervisorStrategy<(TcpStream, SocketAddr)> { + error!("error handling connection: {}", err); + SupervisorStrategy::Stop +} + +async fn http_actor( + _: actor::Context, + mut connection: http::Connection, + address: SocketAddr, +) -> io::Result<()> { + info!("accepted connection: address={}", address); + connection.set_nodelay(true)?; + + loop { + match connection.next_request().await? { + Ok(Some(mut request)) => { + debug!("received request: {:?}", request); + let mut headers = Headers::EMPTY; + let (code, body) = if !matches!(request.method(), Method::Get | Method::Head) { + request.body_mut().ignore()?; + headers.add(Header::new(HeaderName::ALLOW, b"GET, HEAD")); + (StatusCode::METHOD_NOT_ALLOWED, "Method not allowed".into()) + } else if request.path() != "/" { + request.body_mut().ignore()?; + (StatusCode::NOT_FOUND, "Not found".into()) + } else if request.body().len() != 0 { + request.body_mut().ignore()?; + let body = Cow::from("Not expecting a body"); + (StatusCode::PAYLOAD_TOO_LARGE, body) + } else { + // This will allocate a new string which isn't the most + // efficient way to do this, but it's the easiest so we'll + // keep this for sake of example. + let body = Cow::from(address.ip().to_string()); + (StatusCode::OK, body) + }; + let version = request.version().highest_minor(); + let response = Response::new(version, code, headers, body); + debug!("sending response: {:?}", response); + connection.respond(response).await?; + } + // No more requests. + Ok(None) => return Ok(()), + Err(err) => { + warn!("error reading request: {}: source={}", err, address); + let code = err.proper_status_code(); + let body = format!("Bad request: {}", err); + let version = connection + .last_request_version() + .unwrap_or(Version::Http11) + .highest_minor(); + let response = Response::new(version, code, Headers::EMPTY, body); + debug!("sending response: {:?}", response); + connection.respond(response).await?; + if err.should_close() { + connection.close(); + return Ok(()); + } + } + } + } +} diff --git a/http/src/body.rs b/http/src/body.rs new file mode 100644 index 000000000..254ca27d3 --- /dev/null +++ b/http/src/body.rs @@ -0,0 +1,36 @@ +use std::borrow::Cow; + +// TODO: support streaming bodies. +pub trait Body { + fn as_bytes(&self) -> &[u8]; +} + +impl Body for [u8] { + fn as_bytes(&self) -> &[u8] { + self + } +} + +impl Body for Vec { + fn as_bytes(&self) -> &[u8] { + &*self + } +} + +impl Body for str { + fn as_bytes(&self) -> &[u8] { + self.as_bytes() + } +} + +impl Body for String { + fn as_bytes(&self) -> &[u8] { + self.as_bytes() + } +} + +impl Body for Cow<'_, str> { + fn as_bytes(&self) -> &[u8] { + self.as_ref().as_bytes() + } +} diff --git a/http/src/from_bytes.rs b/http/src/from_bytes.rs new file mode 100644 index 000000000..2124da83a --- /dev/null +++ b/http/src/from_bytes.rs @@ -0,0 +1,35 @@ +/// Analogous trait to the [`FromStr`] trait. +/// +/// [`FromStr`]: std::str::FromStr +pub trait FromBytes: Sized { + type Err; + + fn from_bytes(value: &[u8]) -> Result; +} + +pub struct ParseIntError; + +impl FromBytes for usize { + type Err = ParseIntError; + + fn from_bytes(src: &[u8]) -> Result { + if src.is_empty() { + return Err(ParseIntError); + } + + let mut value = 0; + for b in src.iter().copied() { + if b >= b'0' && b <= b'9' { + // TODO: check if this doesn't get compiled away. + if value >= (usize::MAX / 10) { + // Overflow. + return Err(ParseIntError); + } + value = (value * 10) + (b - b'0') as usize; + } else { + return Err(ParseIntError); + } + } + Ok(value) + } +} diff --git a/http/src/header.rs b/http/src/header.rs new file mode 100644 index 000000000..53abf2e72 --- /dev/null +++ b/http/src/header.rs @@ -0,0 +1,369 @@ +//! Module with HTTP header related types. + +// TODO: impl for `Headers`. +// * FromIterator +// * Extend + +use std::borrow::Cow; +use std::convert::AsRef; +use std::fmt; +use std::iter::FusedIterator; + +use crate::{cmp_lower_case, is_lower_case, FromBytes}; + +/// List of headers. +pub struct Headers { + /// All values appended in a single allocation. + values: Vec, + /// All parts of the headers. + parts: Vec, +} + +struct HeaderPart { + name: HeaderName, + /// Indices into `Headers.data`. + start: usize, + end: usize, +} + +impl Headers { + /// Empty list of headers. + pub const EMPTY: Headers = Headers { + values: Vec::new(), + parts: Vec::new(), + }; + + /// Creates new `Headers` from `headers`. + /// + /// Calls `F` for each header. + pub(crate) fn from_httparse_headers( + raw_headers: &[httparse::Header<'_>], + mut f: F, + ) -> Result + where + F: FnMut(&HeaderName, &[u8]) -> Result<(), E>, + { + let values_len = raw_headers.iter().map(|h| h.value.len()).sum(); + let mut headers = Headers { + values: Vec::with_capacity(values_len), + parts: Vec::with_capacity(raw_headers.len()), + }; + for header in raw_headers { + let name = HeaderName::from_str(header.name); + let value = header.value; + if let Err(err) = f(&name, value) { + return Err(err); + } + headers._add(name, value); + } + Ok(headers) + } + + /// Returns the number of headers. + pub fn len(&self) -> usize { + self.parts.len() + } + + /// Add a new `header`. + /// + /// # Notes + /// + /// This doesn't check for duplicate headers, it just adds it to the list of + /// headers. + pub fn add(&mut self, header: Header<'_>) { + self._add(header.name, header.value) + } + + fn _add(&mut self, name: HeaderName, value: &[u8]) { + let start = self.values.len(); + self.values.extend_from_slice(value); + let end = self.values.len(); + self.parts.push(HeaderPart { name, start, end }); + } + + /// Get the header with `name`, if any. + /// + /// # Notes + /// + /// This requires an owned `HeaderName` to avoid a clone. If all you need is + /// the header value you can use [`Headers::get_value`], which takes a + /// reference to `name`. + pub fn get<'a>(&'a self, name: HeaderName) -> Option> { + for part in self.parts.iter() { + if part.name == name { + return Some(Header { + name, + value: &self.values[part.start..part.end], + }); + } + } + None + } + + /// Get the header's value with `name`, if any. + pub fn get_value<'a>(&'a self, name: &HeaderName) -> Option<&[u8]> { + for part in self.parts.iter() { + if part.name == *name { + return Some(&self.values[part.start..part.end]); + } + } + None + } + + // TODO: remove header? + + /// Returns an iterator over all headers. + /// + /// The order is unspecified. + pub fn iter<'a>(&'a self) -> Iter<'a> { + Iter { + headers: self, + pos: 0, + } + } +} + +impl From> for Headers { + fn from(header: Header<'_>) -> Headers { + Headers { + values: header.value.to_vec(), + parts: vec![HeaderPart { + name: header.name, + start: 0, + end: header.value.len(), + }], + } + } +} + +/* +/// # Notes +/// +/// This clones the [`HeaderName`] in each header. For static headers, i.e. the +/// `HeaderName::*` constants, this is a cheap operation, for customer headers +/// this requires an allocation. +impl From<&'_ [Header<'_>]> for Headers { + fn from(raw_headers: &'_ [Header<'_>]) -> Headers { + let values_len = raw_headers.iter().map(|h| h.value.len()).sum(); + let mut headers = Headers { + values: Vec::with_capacity(values_len), + parts: Vec::with_capacity(raw_headers.len()), + }; + for header in raw_headers { + headers._add(header.name.clone(), header.value); + } + headers + } +} +*/ + +impl fmt::Debug for Headers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_map(); + for part in self.parts.iter() { + let value = &self.values[part.start..part.end]; + if let Ok(str) = std::str::from_utf8(value) { + f.entry(&part.name, &str); + } else { + f.entry(&part.name, &value); + } + } + f.finish() + } +} + +/// Iterator for [`Headers`], see [`Headers::iter`]. +pub struct Iter<'a> { + headers: &'a Headers, + pos: usize, +} + +impl<'a> Iterator for Iter<'a> { + type Item = Header<'a>; + + fn next(&mut self) -> Option { + self.headers.parts.get(self.pos).map(|part| { + let header = Header { + // FIXME: try to avoid this clone? + name: part.name.clone(), + value: &self.headers.values[part.start..part.end], + }; + self.pos += 1; + header + }) + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } + + fn count(self) -> usize { + self.len() + } +} + +impl<'a> ExactSizeIterator for Iter<'a> { + fn len(&self) -> usize { + self.headers.len() - self.pos + } +} + +impl<'a> FusedIterator for Iter<'a> {} + +/// HTTP header. +/// +/// RFC 7230 section 3.2. +#[derive(Clone)] +pub struct Header<'a> { + name: HeaderName, + value: &'a [u8], +} + +impl<'a> Header<'a> { + /// Create a new `Header`. + /// + /// # Notes + /// + /// `value` MUST NOT contain `\r\n`. + pub const fn new(name: HeaderName, value: &'a [u8]) -> Header<'a> { + debug_assert!(no_crlf(value)); + Header { name, value } + } + + /// Returns the name of the header. + pub const fn name(&self) -> &HeaderName { + &self.name + } + + /// Returns the value of the header. + pub const fn value(&self) -> &[u8] { + self.value + } + + /// Parse the value of the header using `T`'s [`FromBytes`] implementation. + pub fn parse(&self) -> Result + where + T: FromBytes, + { + FromBytes::from_bytes(self.value) + } +} + +impl<'a> fmt::Debug for Header<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_struct("Header"); + f.field("name", &self.name); + if let Ok(str) = std::str::from_utf8(self.value) { + f.field("value", &str); + } else { + f.field("value", &self.value); + } + f.finish() + } +} + +/// HTTP header name. +#[derive(Clone, PartialEq, Eq)] +pub struct HeaderName { + /// The value MUST be lower case. + // TODO: consider exposing this lifetime. + inner: Cow<'static, str>, +} + +/// Macro to create [`Name`] constants. +macro_rules! known_headers { + ($( ( $const_name: ident, $http_name: expr ), )+) => { + $( + pub const $const_name: HeaderName = HeaderName::from_lowercase($http_name); + )+ + + /// Create a new HTTP header `HeaderName`. + /// + /// # Notes + /// + /// If `name` is static prefer to use [`HeaderName::from_lowercase`]. + pub fn from_str(name: &str) -> HeaderName { + // TODO: check performance of this. Does the match statement + // outweight the allocation? + // TODO: match case-insensitive. + // TODO: optimise based on `name.len()`? + match name { + $( $http_name => HeaderName::$const_name, )+ + _ => HeaderName::from(name.to_string()), + } + } + } +} + +impl HeaderName { + known_headers!( + (ALLOW, "allow"), + (CONTENT_LENGTH, "content-length"), + (TRANSFER_ENCODING, "transfer-encoding"), + (USER_AGENT, "user-agent"), + (X_REQUEST_ID, "x-request-id"), + ); + + /// Create a new HTTP header `HeaderName`. + /// + /// # Panics + /// + /// Panics if `name` is not all ASCII lowercase. + pub const fn from_lowercase(name: &'static str) -> HeaderName { + assert!(is_lower_case(name)); + HeaderName { + inner: Cow::Borrowed(name), + } + } +} + +/// Returns `true` if `value` does not contain `\r\n`. +const fn no_crlf(value: &[u8]) -> bool { + if value.is_empty() { + return true; + } + let mut i = 1; + while i < value.len() - 1 { + if value[i - 1] == b'\r' && value[i] == b'\n' { + return false; + } + i += 1; + } + true +} + +impl From for HeaderName { + fn from(mut name: String) -> HeaderName { + name.make_ascii_lowercase(); + HeaderName { + inner: Cow::Owned(name), + } + } +} + +impl AsRef for HeaderName { + fn as_ref(&self) -> &str { + self.inner.as_ref() + } +} + +impl PartialEq for HeaderName { + fn eq(&self, other: &str) -> bool { + // NOTE: `self` is always lowercase, per the comment on the `inner` + // field. + cmp_lower_case(self.inner.as_ref(), other) + } +} + +impl fmt::Debug for HeaderName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_ref()) + } +} + +impl fmt::Display for HeaderName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_ref()) + } +} diff --git a/http/src/lib.rs b/http/src/lib.rs new file mode 100644 index 000000000..939eb9d45 --- /dev/null +++ b/http/src/lib.rs @@ -0,0 +1,82 @@ +#![allow( + unreachable_code, + unused_variables, + unused_mut, + dead_code, + unused_imports +)] // FIXME: remove. +#![feature( + const_eval_limit, + const_panic, + maybe_uninit_array_assume_init, + maybe_uninit_slice, + maybe_uninit_uninit_array, + maybe_uninit_write_slice +)] + +use std::convert::AsRef; +use std::fmt; +use std::str::FromStr; + +mod body; +mod from_bytes; +pub mod header; +pub mod method; +mod request; +mod response; +pub mod server; +mod status_code; +pub mod version; + +pub use body::Body; +pub use from_bytes::FromBytes; +#[doc(no_inline)] +pub use header::{Header, HeaderName, Headers}; +#[doc(no_inline)] +pub use method::Method; +pub use request::Request; +pub use response::Response; +#[doc(no_inline)] +pub use server::{Connection, HttpServer}; +pub use status_code::StatusCode; +#[doc(no_inline)] +pub use version::Version; + +/// Returns `true` if `lower_case` and `right` are a case-insensitive match. +/// +/// # Notes +/// +/// `lower_case` must be lower case! +const fn cmp_lower_case(lower_case: &str, right: &str) -> bool { + debug_assert!(is_lower_case(lower_case)); + + let left = lower_case.as_bytes(); + let right = right.as_bytes(); + let len = left.len(); + if len != right.len() { + return false; + } + + let mut i = 0; + while i < len { + if left[i] != right[i].to_ascii_lowercase() { + return false; + } + i += 1; + } + true +} + +/// Returns `true` if `value` is all ASCII lowercase. +const fn is_lower_case(value: &str) -> bool { + let value = value.as_bytes(); + let mut i = 0; + while i < value.len() { + // NOTE: allows `-` because it's used in header names. + if !matches!(value[i], b'a'..=b'z' | b'-') { + return false; + } + i += 1; + } + true +} diff --git a/http/src/method.rs b/http/src/method.rs new file mode 100644 index 000000000..d4b535290 --- /dev/null +++ b/http/src/method.rs @@ -0,0 +1,117 @@ +use std::fmt; +use std::str::FromStr; + +use crate::cmp_lower_case; + +/// HTTP method. +/// +/// RFC 7231 section 4. +#[derive(Copy, Clone, Debug)] +pub enum Method { + Get, + Head, + Post, + Put, + Delete, + Connect, + Options, + Trace, + /// RFC 5789. + Patch, +} + +impl Method { + /// Returns `true` if `self` is a HEAD method. + pub const fn is_head(self) -> bool { + matches!(self, Method::Head) + } + + /// Returns `true` if the method is safe. + /// + /// RFC 7321 section 4.2.1. + pub const fn is_safe(self) -> bool { + use Method::*; + matches!(self, Get | Head | Options | Trace) + } + + /// Returns `true` if the method is idempotent. + /// + /// RFC 7321 section 4.2.2. + pub const fn is_idempotent(self) -> bool { + matches!(self, Method::Put | Method::Delete) || self.is_safe() + } +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Method::*; + f.write_str(match self { + Options => "OPTIONS", + Get => "GET", + Post => "POST", + Put => "PUT", + Delete => "DELETE", + Head => "HEAD", + Trace => "TRACE", + Connect => "CONNECT", + Patch => "PATCH", + }) + } +} + +/// Error returned by the [`FromStr`] implementation for [`Method`]. +#[derive(Copy, Clone, Debug)] +pub struct UnknownMethod; + +impl FromStr for Method { + type Err = UnknownMethod; + + fn from_str(method: &str) -> Result { + match method.len() { + 3 => { + if cmp_lower_case("get", method) { + Ok(Method::Get) + } else if cmp_lower_case("put", method) { + Ok(Method::Put) + } else { + Err(UnknownMethod) + } + } + 4 => { + if cmp_lower_case("head", method) { + Ok(Method::Head) + } else if cmp_lower_case("post", method) { + Ok(Method::Post) + } else { + Err(UnknownMethod) + } + } + 5 => { + if cmp_lower_case("trace", method) { + Ok(Method::Trace) + } else if cmp_lower_case("patch", method) { + Ok(Method::Patch) + } else { + Err(UnknownMethod) + } + } + 6 => { + if cmp_lower_case("delete", method) { + Ok(Method::Delete) + } else { + Err(UnknownMethod) + } + } + 7 => { + if cmp_lower_case("connect", method) { + Ok(Method::Connect) + } else if cmp_lower_case("options", method) { + Ok(Method::Options) + } else { + Err(UnknownMethod) + } + } + _ => Err(UnknownMethod), + } + } +} diff --git a/http/src/request.rs b/http/src/request.rs new file mode 100644 index 000000000..91b9fb487 --- /dev/null +++ b/http/src/request.rs @@ -0,0 +1,63 @@ +// TODO: see if we can have a borrowed version of `Request`. + +use std::fmt; + +use crate::{Header, Headers, Method, Version}; + +pub struct Request { + pub(crate) method: Method, + pub(crate) path: String, + pub(crate) version: Version, + pub(crate) headers: Headers, + pub(crate) body: B, +} + +impl Request { + /// Returns the HTTP version of this request. + /// + /// # Notes + /// + /// Requests from the [`HttpServer`] will return the highest version it + /// understand, e.g. if a client used HTTP/1.2 (which doesn't exists) the + /// version would be set to HTTP/1.1 (the highest version this crate + /// understands) per RFC 7230 section 2.6. + /// + /// [`HttpServer`]: crate::HttpServer + pub fn version(&self) -> Version { + self.version + } + + /// Returns the HTTP method of this request. + pub fn method(&self) -> Method { + self.method + } + + pub fn path(&self) -> &str { + &self.path + } + + pub fn headers(&self) -> &Headers { + &self.headers + } + + pub fn body(&self) -> &B { + &self.body + } + + pub fn body_mut(&mut self) -> &mut B { + &mut self.body + } + + // TODO: maybe `fn split_body(self) -> (Request<()>, B)`? +} + +impl fmt::Debug for Request { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Request") + .field("method", &self.method) + .field("path", &self.path) + .field("version", &self.version) + .field("headers", &self.headers) + .finish() + } +} diff --git a/http/src/response.rs b/http/src/response.rs new file mode 100644 index 000000000..08f395660 --- /dev/null +++ b/http/src/response.rs @@ -0,0 +1,36 @@ +use std::fmt; + +use crate::{Header, Headers, StatusCode, Version}; + +pub struct Response { + pub(crate) version: Version, + pub(crate) status: StatusCode, + pub(crate) headers: Headers, + pub(crate) body: B, +} + +impl Response { + pub const fn new( + version: Version, + status: StatusCode, + headers: Headers, + body: B, + ) -> Response { + Response { + version, + status, + headers, + body, + } + } +} + +impl fmt::Debug for Response { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Request") + .field("version", &self.version) + .field("status", &self.status) + .field("headers", &self.headers) + .finish() + } +} diff --git a/http/src/server.rs b/http/src/server.rs new file mode 100644 index 000000000..a01220134 --- /dev/null +++ b/http/src/server.rs @@ -0,0 +1,754 @@ +// TODO: `S: Supervisor` currently uses `TcpStream` as argument due to `ArgMap`. +// Maybe disconnect `S` from `NA`? +// +// TODO: Continue reading RFC 7230 section 4 Transfer Codings. +// +// TODO: chunked encoding. +// TODO: reading request body. + +use std::convert::TryFrom; +use std::fmt; +use std::future::Future; +use std::io::{self, IoSlice, Write}; +use std::net::SocketAddr; +use std::os::raw::c_int; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{self, Poll}; + +use heph::actor::messages::Terminate; +use heph::net::{tcp, TcpListener, TcpServer, TcpStream}; +use heph::rt::Signal; +use heph::spawn::{ActorOptions, Spawn}; +use heph::{actor, rt, Actor, NewActor, Supervisor}; +use httparse::EMPTY_HEADER; +use log::debug; +use socket2::{Domain, Protocol, Socket, Type}; + +use crate::{ + FromBytes, Header, HeaderName, Headers, Method, Request, Response, StatusCode, Version, +}; + +/// Maximum size of the header (the start line and the headers). +/// +/// RFC 7230 section 3.1.1 recommends ``all HTTP senders and recipients support, +/// at a minimum, request-line lengths of 8000 octets.'' +pub const MAX_HEADER_SIZE: usize = 16384; + +/// Maximum number of headers parsed for each request. +pub const MAX_HEADERS: usize = 64; + +/// Minimum amount of bytes read from the connection or the buffer will be +/// grown. +const MIN_READ_SIZE: usize = 512; + +/// Size of the buffer used in [`Connection`]. +const BUF_SIZE: usize = 8192; + +/// A intermediate structure that implements [`NewActor`], creating +/// [`HttpServer`]. +/// +/// See [`HttpServer::setup`] to create this and [`HttpServer`] for examples. +#[derive(Debug)] +pub struct Setup { + inner: tcp::server::Setup>, +} + +impl Setup { + /// Returns the address the server is bound to. + pub fn local_addr(&self) -> SocketAddr { + self.inner.local_addr() + } +} + +impl NewActor for Setup +where + S: Supervisor> + Clone + 'static, + NA: NewActor + Clone + 'static, + NA::RuntimeAccess: rt::Access + Spawn, NA::RuntimeAccess>, +{ + type Message = Message; + type Argument = (); + type Actor = HttpServer; + type Error = io::Error; + type RuntimeAccess = NA::RuntimeAccess; + + fn new( + &mut self, + mut ctx: actor::Context, + arg: Self::Argument, + ) -> Result { + self.inner.new(ctx, arg).map(|inner| HttpServer { inner }) + } +} + +impl Clone for Setup { + fn clone(&self) -> Setup { + Setup { + inner: self.inner.clone(), + } + } +} + +/// An actor that starts a new actor for each accepted TCP connection. +/// +/// TODO: same design as TcpServer. +/// +/// This actor can start as a thread-local or thread-safe actor. When using the +/// thread-local variant one actor runs per worker thread which spawns +/// thread-local actors to handle the [`TcpStream`]s. See the first example +/// below on how to run this `TcpServer` as a thread-local actor. +/// +/// This actor can also run as thread-safe actor in which case it also spawns +/// thread-safe actors. Note however that using thread-*local* version is +/// recommended. The third example below shows how to run the `TcpServer` as +/// thread-safe actor. +/// +/// # Graceful shutdown +/// +/// Graceful shutdown is done by sending it a [`Terminate`] message, see below +/// for an example. The TCP server can also handle (shutdown) process signals, +/// see "Example 2 my ip" (in the examples directory of the source code) for an +/// example of that. +/// +/// # Examples +/// +/// TODO. +pub struct HttpServer> { + inner: TcpServer>, +} + +impl HttpServer +where + S: Supervisor> + Clone + 'static, + NA: NewActor + Clone + 'static, +{ + /// Create a new [server setup]. + /// + /// Arguments: + /// * `address`: the address to listen on. + /// * `supervisor`: the [`Supervisor`] used to supervise each started actor, + /// * `new_actor`: the [`NewActor`] implementation to start each actor, + /// and + /// * `options`: the actor options used to spawn the new actors. + /// + /// [server setup]: Setup + pub fn setup( + address: SocketAddr, + supervisor: S, + new_actor: NA, + options: ActorOptions, + ) -> io::Result> { + let new_actor = ArgMap { new_actor }; + TcpServer::setup(address, supervisor, new_actor, options).map(|inner| Setup { inner }) + } +} + +impl Actor for HttpServer +where + S: Supervisor> + Clone + 'static, + NA: NewActor + Clone + 'static, + NA::RuntimeAccess: rt::Access + Spawn, NA::RuntimeAccess>, +{ + type Error = Error; + + fn try_poll( + self: Pin<&mut Self>, + ctx: &mut task::Context<'_>, + ) -> Poll> { + let this = unsafe { self.map_unchecked_mut(|s| &mut s.inner) }; + this.try_poll(ctx) + } +} + +// TODO: better name. Like `TcpStreamToConnection`? +/// Maps `NA` to accept `(TcpStream, SocketAddr)` as argument. +#[derive(Debug, Clone)] +pub struct ArgMap { + new_actor: NA, +} + +impl NewActor for ArgMap +where + NA: NewActor, +{ + type Message = NA::Message; + type Argument = (TcpStream, SocketAddr); + type Actor = NA::Actor; + type Error = NA::Error; + type RuntimeAccess = NA::RuntimeAccess; + + fn new( + &mut self, + ctx: actor::Context, + (stream, address): Self::Argument, + ) -> Result { + let conn = Connection::new(stream); + self.new_actor.new(ctx, (conn, address)) + } + + fn name(&self) -> &'static str { + self.new_actor.name() + } +} + +pub struct Connection { + stream: TcpStream, + buf: Vec, + /// Number of bytes of `buf` that are already parsed. + parsed_bytes: usize, + /// The HTTP version of the last request. + last_version: Option, + /// The HTTP method of the last request. + last_method: Option, +} + +impl Connection { + /// Create a new `Connection`. + pub fn new(stream: TcpStream) -> Connection { + Connection { + stream, + buf: Vec::with_capacity(BUF_SIZE), + parsed_bytes: 0, + last_version: None, + last_method: None, + } + } + + pub fn peer_addr(&mut self) -> io::Result { + self.stream.peer_addr() + } + + pub fn local_addr(&mut self) -> io::Result { + self.stream.local_addr() + } + + pub fn set_ttl(&mut self, ttl: u32) -> io::Result<()> { + self.stream.set_ttl(ttl) + } + + pub fn ttl(&mut self) -> io::Result { + self.stream.ttl() + } + + pub fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { + self.stream.set_nodelay(nodelay) + } + + pub fn nodelay(&mut self) -> io::Result { + self.stream.nodelay() + } + + pub fn keepalive(&self) -> io::Result { + self.stream.keepalive() + } + + pub fn set_keepalive(&self, enable: bool) -> io::Result<()> { + self.stream.set_keepalive(enable) + } + + /// Parse the next request from the connection. + /// + /// The return is a bit complex so let's break it down. The outer type is an + /// [`io::Result`], which often needs to be handled seperately from errors + /// in the request, e.g. by using `?`. + /// + /// Next is a `Result, `[`RequestError`]`>`. `None` + /// is returned if the connections contains no more requests, i.e. all bytes + /// are read. If the connection contains a request it will return a + /// [`Request`]. If the request is somehow invalid/incomplete it will return + /// an [`RequestError`]. + /// + /// # Notes + /// + /// Most [`RequestError`]s can't be receover from and will need the + /// connection be closed, see [`RequestError::should_close`]. If the + /// connection is not closed and [`next_request`] is called again it will + /// likely return the same error (but this is not guaranteed). + /// + /// [`next_request`]: Connection::next_request + pub async fn next_request<'a>( + &'a mut self, + ) -> io::Result>>, RequestError>> { + let mut too_short = 0; + loop { + // In case of pipelined requests it could be that while reading a + // previous request's body it partially read the headers of the next + // (this) request. To handle this we attempt to parse the request if + // we have more than zero bytes in the first iteration of the loop. + if self.buf.len() <= too_short { + // Receive some more bytes. + if self.stream.recv(&mut self.buf).await? == 0 { + if self.buf.is_empty() { + // Read the entire stream, so we're done. + return Ok(Ok(None)); + } else { + // Couldn't read any more bytes, but we still have bytes in + // the buffer. This means it contains a partial request. + return Ok(Err(RequestError::IncompleteRequest)); + } + } + } + + let mut headers = [EMPTY_HEADER; MAX_HEADERS]; + let mut req = httparse::Request::new(&mut headers); + match req.parse(&self.buf[self.parsed_bytes..]) { + Ok(httparse::Status::Complete(header_length)) => { + self.parsed_bytes += header_length; + + // SAFETY: all these unwraps are safe because `parse` above + // ensures there all `Some`. + let method = match req.method.unwrap().parse() { + Ok(method) => method, + Err(_) => return Ok(Err(RequestError::UnknownMethod)), + }; + self.last_method = Some(method); + let path = req.path.unwrap().to_string(); + let version = map_version(req.version.unwrap()); + self.last_version = Some(version); + + // RFC 7230 section 3.3.3 Message Body Length. + let mut body_length: Option = None; + let res = Headers::from_httparse_headers(req.headers, |name, value| { + if *name == HeaderName::CONTENT_LENGTH { + // RFC 7230 section 3.3.3 point 4: + // > If a message is received without + // > Transfer-Encoding and with either multiple + // > Content-Length header fields having differing + // > field-values or a single Content-Length header + // > field having an invalid value, then the message + // > framing is invalid and the recipient MUST treat + // > it as an unrecoverable error. If this is a + // > request message, the server MUST respond with a + // > 400 (Bad Request) status code and then close + // > the connection. + if let Ok(length) = FromBytes::from_bytes(value) { + match body_length.as_mut() { + Some(body_length) if *body_length == length => {} + Some(_) => return Err(RequestError::DifferentContentLengths), + None => body_length = Some(length), + } + } else { + return Err(RequestError::InvalidContentLength); + } + } else if *name == HeaderName::TRANSFER_ENCODING { + todo!("transfer encoding"); + + // TODO: we can support chunked, but for other + // encoding we need external packages (for compress, + // deflate, gzip). + // Not supported transfer-encoding respond with 501 + // (Not Implemented). + // + // RFC 7230 section 3.3.3 point 3: + // > If a Transfer-Encoding header field is present + // > in a request and the chunked transfer coding is + // > not the final encoding, the message body length + // > cannot be determined reliably; the server MUST + // > respond with the 400 (Bad Request) status code + // > and then close the connection. + // > + // > If a message is received with both a + // > Transfer-Encoding and a Content-Length header + // > field, the Transfer-Encoding overrides the + // > Content-Length. [..] A sender MUST remove the + // > received Content-Length field prior to + // > forwarding such a message downstream. + } + Ok(()) + }); + let headers = match res { + Ok(headers) => headers, + Err(err) => return Ok(Err(err)), + }; + + // TODO: RFC 7230 section 3.3.3: + // > A server MAY reject a request that contains a message + // > body but not a Content-Length by responding with 411 + // > (Length Required). + // Maybe do this for POST/PUT/etc. that (usually) requires a + // body? + + // RFC 7230 section 3.3.3 point 6: + // > If this is a request message and none of the above are + // > true, then the message body length is zero (no message + // > body is present). + let size = body_length.unwrap_or(0); + + let body = Body { + conn: self, + size, + left: size, + }; + return Ok(Ok(Some(Request { + method, + version, + path, + headers, + body, + }))); + } + Ok(httparse::Status::Partial) => { + // Buffer doesn't include the entire request header, try + // reading more bytes (in the next iteration). + too_short = self.buf.len(); + self.last_method = req.method.and_then(|m| m.parse().ok()); + if let Some(version) = req.version { + self.last_version = Some(map_version(version)); + } + + if too_short >= MAX_HEADER_SIZE { + todo!("HTTP request header too large"); + } + + continue; + } + Err(err) => return Ok(Err(RequestError::from_httparse(err))), + } + } + } + + /// Returns the HTTP version of the last (partial) request. + /// + /// This can be used in cases where [`Connection::next_request`] returns a + /// [`RequestError`]. + /// + /// # Examples + /// + /// Responding to a [`RequestError`]. + /// + /// ``` + /// use heph_http::{Response, Headers, StatusCode, Version}; + /// use heph_http::server::{Connection, RequestError}; + /// + /// # return; + /// # #[allow(unreachable_code)] + /// # { + /// let mut conn: Connection = /* From HttpServer. */ + /// # todo!(); + /// + /// // Reading a request returned this error. + /// let err = RequestError::IncompleteRequest; + /// + /// // We can use `last_request_version` to determine the client prefered + /// // HTTP version, or default to the server prefered version (HTTP/1.1 + /// // here). + /// let version = conn.last_request_version().unwrap_or(Version::Http11); + /// let body = format!("Bad request: {}", err); + /// let response = Response::new(version, StatusCode::BAD_REQUEST, Headers::EMPTY, body); + /// + /// // Respond with the response. + /// conn.respond(response); + /// + /// // Close the connection if the error is fatal. + /// if err.should_close() { + /// conn.close(); + /// return; + /// } + /// # } + /// ``` + pub fn last_request_version(&self) -> Option { + self.last_version + } + + /// # Notes + /// + /// This automatically sets the "Content-Length" header if no provided in + /// `response`. + /// + /// This doesn't include the body if the response is to a HEAD request. + pub async fn respond(&mut self, response: Response) -> io::Result<()> + where + B: crate::Body, + { + use crate::Body; + + // Bytes of the (next) request. + self.clear_buffer(); + let ignore_end = self.buf.len(); + + // TODO: RFC 7230 section 3.3: + // > The presence of a message body in a response depends on + // > both the request method to which it is responding and + // > the response status code (Section 3.1.2). Responses to + // > the HEAD request method (Section 4.3.2 of [RFC7231]) + // > never include a message body because the associated + // > response header fields (e.g., Transfer-Encoding, + // > Content-Length, etc.), if present, indicate only what + // > their values would have been if the request method had + // > been GET (Section 4.3.1 of [RFC7231]). 2xx (Successful) + // > responses to a CONNECT request method (Section 4.3.6 of + // > [RFC7231]) switch to tunnel mode instead of having a + // > message body. All 1xx (Informational), 204 (No + // > Content), and 304 (Not Modified) responses do not + // > include a message body. All other responses do include + // > a message body, although the body might be of zero + // > length. + + // Format the status-line (RFC 7230 section 3.1.2). + // NOTE: we're not sending a reason-phrase, but the space is required + // before \r\n. + write!( + &mut self.buf, + "{} {} \r\n", + response.version, response.status + ) + .unwrap(); + + // Format the headers (RFC 7230 section 3.2). + let mut set_content_length_header = false; + for header in response.headers.iter() { + // Field-name: + // NOTE: spacing after the colon (`:`) is optional. + write!(&mut self.buf, "{}: ", header.name()).unwrap(); + // Append the header's value. + // NOTE: `header.value` shouldn't contain CRLF (`\r\n`). + self.buf.extend_from_slice(header.value()); + self.buf.extend_from_slice(b"\r\n"); + if *header.name() == HeaderName::CONTENT_LENGTH { + set_content_length_header = true; + } + } + + // Response body. + let body = if let Some(Method::Head) = self.last_method { + // RFC 7231 section 4.3.2: + // > The HEAD method is identical to GET except that the server MUST + // > NOT send a message body in the response (i.e., the response + // > terminates at the end of the header section). + &[] + } else { + response.body.as_bytes() + }; + + // Provide the "Conent-Length" if the user didn't. + if !set_content_length_header { + write!(&mut self.buf, "Content-Length: {}\r\n", body.len()).unwrap(); + } + + // End of the header. + self.buf.extend_from_slice(b"\r\n"); + + // Write the response to the connection. + let header = IoSlice::new(&self.buf[ignore_end..]); + let body = IoSlice::new(body); + self.stream.send_vectored_all(&mut [header, body]).await?; + + // Remove the response from the buffer. + self.buf.truncate(ignore_end); + Ok(()) + } + + /// Close the connection. + /// + /// This should be called in case of certain [`RequestError`]s, see + /// [`RequestError::should_close`]. It should also be called if a response + /// it returned without a length, that is a response with a Content-Length + /// header and not using chunked transfer encoding. + pub fn close(self) { + drop(self); + } + + /// Clear parsed request(s) from the buffer. + fn clear_buffer(&mut self) { + if self.buf.len() == self.parsed_bytes { + // Parsed all bytes in the buffer, so we can clear it. + self.buf.clear(); + self.parsed_bytes = 0; + } + + // TODO: move bytes to the start. + } +} + +const fn map_version(version: u8) -> Version { + match version { + 0 => Version::Http10, + // RFC 7230 section 2.6: + // > A server SHOULD send a response version equal to + // > the highest version to which the server is + // > conformant that has a major version less than or + // > equal to the one received in the request. + // HTTP/1.1 is the highest we support. + _ => Version::Http11, + } +} + +/// Body of HTTP [`Request`] read from a [`Connection`]. +pub struct Body<'a> { + conn: &'a mut Connection, + /// Total size of the HTTP body. + size: usize, + /// Number of unread (by the user) bytes. + left: usize, +} + +impl<'a> Body<'a> { + // TODO: RFC 7230 section 3.4 Handling Incomplete Messages. + + // TODO: RFC 7230 section 3.3.3 point 5: + // > If the sender closes the connection or the recipient + // > times out before the indicated number of octets are + // > received, the recipient MUST consider the message to be + // > incomplete and close the connection. + + /// Returns the size of the body in bytes. + /// + /// The returned value is based on the "Content-Length" header, or 0 if not + /// present. + pub fn len(&self) -> usize { + // TODO: chunked encoding. + self.size + } + + /// Returns the number of bytes left in the body. + /// + /// See [`Body::len`]. + pub fn left(&self) -> usize { + self.left + } + + /// Ignore the body, but removes it from the connection. + pub fn ignore(&mut self) -> io::Result<()> { + if self.size == 0 { + // Empty body, then we're done quickly. + return Ok(()); + } + + let ignored_len = self.conn.parsed_bytes + self.size; + if self.conn.buf.len() >= ignored_len { + // Entire body was already read we can skip the bytes. + self.conn.parsed_bytes = ignored_len; + return Ok(()); + } + + // TODO: read more bytes from the stream. + todo!("ignore the body: read more bytes") + // NOTE: conn.clear_buffer + } +} + +/* TODO: read entire body? maybe an assertion? +impl<'a> Drop for Body<'a> { + fn drop(&mut self) { + todo!() + } +} +*/ + +/// Error parsing HTTP request. +#[derive(Copy, Clone, Debug)] +pub enum RequestError { + /// Missing part of request. + IncompleteRequest, + /// HTTP Header is too large. + /// + /// Limit is defined by [`MAX_HEADER_SIZE`]. + HeaderTooLarge, + /// Value in the "Content-Length" header is invalid. + InvalidContentLength, + /// Multiple "Content-Length" headers were present with differing values. + DifferentContentLengths, + /// Invalid byte in header name. + InvalidHeaderName, + /// Invalid byte in header value. + InvalidHeaderValue, + /// Number of headers send in the request is larger than [`MAX_HEADERS`]. + TooManyHeaders, + /// Invalid byte where token is required. + InvalidToken, + /// Invalid byte in new line. + InvalidNewLine, + /// Invalid byte in HTTP version. + InvalidVersion, + /// Unknown HTTP method, not in [`Method`]. + UnknownMethod, +} + +impl RequestError { + /// Returns the proper status code for a given error. + pub fn proper_status_code(self) -> StatusCode { + use RequestError::*; + // See the parsing code for various references to the RFC(s) that + // determine the values here. + match self { + IncompleteRequest + | HeaderTooLarge + | InvalidContentLength + | DifferentContentLengths + | InvalidHeaderName + | InvalidHeaderValue + | TooManyHeaders + | InvalidToken + | InvalidNewLine + | InvalidVersion => StatusCode::BAD_REQUEST, + // RFC 7231 section 4.1: + // > When a request method is received that is unrecognized or not + // > implemented by an origin server, the origin server SHOULD + // > respond with the 501 (Not Implemented) status code. + UnknownMethod => StatusCode::NOT_IMPLEMENTED, + } + } + + /// Returns `true` if the connection should be closed based on the error + /// (after sending a error response). + pub fn should_close(self) -> bool { + use RequestError::*; + // See the parsing code for various references to the RFC(s) that + // determine the values here. + match self { + IncompleteRequest + | HeaderTooLarge + | InvalidContentLength + | DifferentContentLengths + | InvalidHeaderName + | InvalidHeaderValue + | TooManyHeaders + | InvalidToken + | InvalidNewLine + | InvalidVersion => true, + UnknownMethod => false, + } + } + + fn from_httparse(err: httparse::Error) -> RequestError { + use httparse::Error::*; + match err { + HeaderName => RequestError::InvalidHeaderName, + HeaderValue => RequestError::InvalidHeaderValue, + Token => RequestError::InvalidToken, + NewLine => RequestError::InvalidNewLine, + Version => RequestError::InvalidVersion, + TooManyHeaders => RequestError::TooManyHeaders, + // SAFETY: request never contain a status, only responses do. + Status => unreachable!(), + } + } +} + +impl fmt::Display for RequestError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use RequestError::*; + f.write_str(match self { + IncompleteRequest => "incomplete request", + HeaderTooLarge => "header too large", + InvalidContentLength => "invalid Content-Length header", + DifferentContentLengths => "different Content-Length headers", + InvalidHeaderName => "invalid header name", + InvalidHeaderValue => "invalid header value", + TooManyHeaders => "too many header", + InvalidToken | InvalidNewLine => "invalid request syntax", + InvalidVersion => "invalid version", + UnknownMethod => "unknown method", + }) + } +} + +/// The message type used by [`HttpServer`]. +/// +/// The message implements [`From`]`<`[`Terminate`]`>` and +/// [`TryFrom`]`<`[`Signal`]`>` for the message, allowing for graceful shutdown. +pub use heph::net::tcp::server::Message; + +/// Error returned by the [`HttpServer`] actor. +pub use heph::net::tcp::server::Error; diff --git a/http/src/status_code.rs b/http/src/status_code.rs new file mode 100644 index 000000000..5e9c393c4 --- /dev/null +++ b/http/src/status_code.rs @@ -0,0 +1,235 @@ +use std::fmt; + +/// Response Status Code. +/// +/// RFC 7231 section 6. +#[derive(Copy, Clone, Debug)] +pub struct StatusCode(u16); + +// TODO: Refer to RFC 7231. +// TODO: add more constants. + +impl StatusCode { + /// Continue. + /// + /// Section 6.2.1 + pub const CONTINUE: StatusCode = StatusCode(100); + + /// Switching Protocols. + /// + /// Section 6.2.2 + pub const SWITCHING_PROTOCOLS: StatusCode = StatusCode(101); + + /// OK. + /// + /// Section 6.3.1 + pub const OK: StatusCode = StatusCode(200); + + /// Created. + /// + /// Section 6.3.2 + pub const CREATED: StatusCode = StatusCode(201); + + /// Accepted. + /// + /// Section 6.3.3 + pub const ACCEPTED: StatusCode = StatusCode(202); + + /// Non-Authoritative Information. + /// + /// Section 6.3.4 + pub const NON_AUTHORITATIVE_INFORMATION: StatusCode = StatusCode(203); + + /// No Content. + /// + /// Section 6.3.5 + pub const NO_CONTENT: StatusCode = StatusCode(204); + + /// Reset Content. + /// + /// Section 6.3.6 + pub const RESET_CONTENT: StatusCode = StatusCode(205); + + /* TODO. + /// Partial Content. + /// + /// Section 4.1 of [RFC7233] + pub const AA: StatusCode = StatusCode(206); + /// Multiple Choices. + /// + /// Section 6.4.1 + pub const AA: StatusCode = StatusCode(300); + /// Moved Permanently. + /// + /// Section 6.4.2 + pub const AA: StatusCode = StatusCode(301); + /// Found. + /// + /// Section 6.4.3 + pub const AA: StatusCode = StatusCode(302); + /// See Other. + /// + /// Section 6.4.4 + pub const AA: StatusCode = StatusCode(303); + /// Not Modified. + /// + /// Section 4.1 of [RFC7232] + pub const AA: StatusCode = StatusCode(304); + /// Use Proxy. + /// + /// Section 6.4.5 + pub const AA: StatusCode = StatusCode(305); + /// Temporary Redirect. + /// + /// Section 6.4.7 + pub const AA: StatusCode = StatusCode(307); + */ + + /// Bad Request. + /// + /// Section 6.5.1 + pub const BAD_REQUEST: StatusCode = StatusCode(400); + + /* + /// Unauthorized. + /// + /// Section 3.1 of [RFC7235] + pub const AA: StatusCode = StatusCode(401); + /// Payment Required. + /// + /// Section 6.5.2 + pub const AA: StatusCode = StatusCode(402); + /// Forbidden. + /// + /// Section 6.5.3 + pub const AA: StatusCode = StatusCode(403); + */ + + /// Not Found. + /// + /// Section 6.5.4 + pub const NOT_FOUND: StatusCode = StatusCode(404); + + /// Method Not Allowed. + /// + /// Section 6.5.5 + pub const METHOD_NOT_ALLOWED: StatusCode = StatusCode(405); + + /* + /// Not Acceptable. + /// + /// Section 6.5.6 + pub const AA: StatusCode = StatusCode(406); + /// Proxy Authentication Required. + /// + /// Section 3.2 of [RFC7235] + pub const AA: StatusCode = StatusCode(407); + /// Request Timeout. + /// + /// Section 6.5.7 + pub const AA: StatusCode = StatusCode(408); + /// Conflict. + /// + /// Section 6.5.8 + pub const AA: StatusCode = StatusCode(409); + /// Gone. + /// + /// Section 6.5.9 + pub const AA: StatusCode = StatusCode(410); + /// Length Required. + /// + /// Section 6.5.10 + pub const AA: StatusCode = StatusCode(411); + /// Precondition Failed. + /// + /// Section 4.2 of [RFC7232] + pub const AA: StatusCode = StatusCode(412); + */ + + /// Payload Too Large. + /// + /// Section 6.5.11 + pub const PAYLOAD_TOO_LARGE: StatusCode = StatusCode(413); + + /* + /// URI Too Long. + /// + /// Section 6.5.12 + pub const AA: StatusCode = StatusCode(414); + /// Unsupported Media Type. + /// + /// Section 6.5.13 + pub const AA: StatusCode = StatusCode(415); + /// Range Not Satisfiable. + /// + /// Section 4.4 of [RFC7233]. + pub const AA: StatusCode = StatusCode(416); + /// Expectation Failed. + /// + /// Section 6.5.14 + pub const AA: StatusCode = StatusCode(417); + /// Upgrade Required. + /// + /// Section 6.5.15 + pub const AA: StatusCode = StatusCode(426); + /// Internal Server Error. + /// + /// Section 6.6.1 + pub const AA: StatusCode = StatusCode(500); + */ + + /// Not Implemented. + /// + /// Section 6.6.2 + pub const NOT_IMPLEMENTED: StatusCode = StatusCode(501); + + /* + /// Bad Gateway. + /// + /// Section 6.6.3 + pub const AA: StatusCode = StatusCode(502); + /// Service Unavailable. + /// + /// Section 6.6.4 + pub const AA: StatusCode = StatusCode(503); + /// Gateway Timeout. + /// + /// Section 6.6.5 + pub const AA: StatusCode = StatusCode(504); + /// HTTP Version Not Supported. + /// + /// Section 6.6.6 + pub const AA: StatusCode = StatusCode(505); + */ + + /// Returns `true` if the status code is in 1xx range. + const fn is_informational(self) -> bool { + self.0 >= 100 && self.0 < 199 + } + + /// Returns `true` if the status code is in 2xx range. + const fn is_successful(self) -> bool { + self.0 >= 200 && self.0 < 299 + } + + /// Returns `true` if the status code is in 3xx range. + const fn is_redirect(self) -> bool { + self.0 >= 300 && self.0 < 399 + } + + /// Returns `true` if the status code is in 4xx range. + const fn is_client_error(self) -> bool { + self.0 >= 400 && self.0 < 499 + } + + /// Returns `true` if the status code is in 5xx range. + const fn is_server_error(self) -> bool { + self.0 >= 500 && self.0 < 599 + } +} + +impl fmt::Display for StatusCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} diff --git a/http/src/version.rs b/http/src/version.rs new file mode 100644 index 000000000..22953ac47 --- /dev/null +++ b/http/src/version.rs @@ -0,0 +1,55 @@ +use std::fmt; +use std::str::FromStr; + +/// HTTP version. +/// +/// RFC 7231 section 2.6. +#[derive(Copy, Clone, Debug)] +pub enum Version { + /// HTTP/1.0. + Http10, + /// HTTP/1.1. + Http11, +} + +impl Version { + /// Returns the highest minor version with the same major version as `self`. + /// + /// According to RFC 7230 section 2.6: + /// > A server SHOULD send a response version equal to the highest version + /// > to which the server is conformant that has a major version less than or + /// > equal to the one received in the request. + /// + /// This function can be used to return the highest version given a major + /// version. + pub const fn highest_minor(self) -> Version { + match self { + Version::Http10 | Version::Http11 => Version::Http11, + } + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Version::Http10 => "HTTP/1.0", + Version::Http11 => "HTTP/1.1", + }) + } +} + +/// Error returned by the [`FromStr`] implementation for [`Version`]. +#[derive(Copy, Clone, Debug)] +pub struct UnknownVersion; + +impl FromStr for Version { + type Err = UnknownVersion; + + fn from_str(method: &str) -> Result { + match method { + "HTTP/1.0" => Ok(Version::Http10), + "HTTP/1.1" => Ok(Version::Http11), + _ => Err(UnknownVersion), + } + } +} From 3a56fcb7e8896640e605d950fd980415e294a063 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 11:36:54 +0200 Subject: [PATCH 02/81] Expose lifetime of HeaderName We'll mostly use a 'static lifetime, e.g. in Headers, but a short lifetime can be used in various places when we're actually borrowing data. For example in the Iterator implementation for Headers and Headers::get, neither now needs to clone the HeaderName and instead lets the name be borrowed. This also adds another lifetime to Headers, now 'n and 'v for the name and value. --- http/src/header.rs | 76 +++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index 53abf2e72..89ab7ad08 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -20,7 +20,7 @@ pub struct Headers { } struct HeaderPart { - name: HeaderName, + name: HeaderName<'static>, /// Indices into `Headers.data`. start: usize, end: usize, @@ -70,11 +70,11 @@ impl Headers { /// /// This doesn't check for duplicate headers, it just adds it to the list of /// headers. - pub fn add(&mut self, header: Header<'_>) { + pub fn add<'v>(&mut self, header: Header<'static, 'v>) { self._add(header.name, header.value) } - fn _add(&mut self, name: HeaderName, value: &[u8]) { + fn _add(&mut self, name: HeaderName<'static>, value: &[u8]) { let start = self.values.len(); self.values.extend_from_slice(value); let end = self.values.len(); @@ -85,14 +85,12 @@ impl Headers { /// /// # Notes /// - /// This requires an owned `HeaderName` to avoid a clone. If all you need is - /// the header value you can use [`Headers::get_value`], which takes a - /// reference to `name`. - pub fn get<'a>(&'a self, name: HeaderName) -> Option> { + /// If all you need is the header value you can use [`Headers::get_value`]. + pub fn get<'a>(&'a self, name: &HeaderName<'_>) -> Option> { for part in self.parts.iter() { - if part.name == name { + if part.name == *name { return Some(Header { - name, + name: part.name.borrow(), value: &self.values[part.start..part.end], }); } @@ -101,7 +99,7 @@ impl Headers { } /// Get the header's value with `name`, if any. - pub fn get_value<'a>(&'a self, name: &HeaderName) -> Option<&[u8]> { + pub fn get_value<'a>(&'a self, name: &HeaderName) -> Option<&'a [u8]> { for part in self.parts.iter() { if part.name == *name { return Some(&self.values[part.start..part.end]); @@ -123,8 +121,8 @@ impl Headers { } } -impl From> for Headers { - fn from(header: Header<'_>) -> Headers { +impl From> for Headers { + fn from(header: Header<'static, '_>) -> Headers { Headers { values: header.value.to_vec(), parts: vec![HeaderPart { @@ -179,13 +177,12 @@ pub struct Iter<'a> { } impl<'a> Iterator for Iter<'a> { - type Item = Header<'a>; + type Item = Header<'a, 'a>; fn next(&mut self) -> Option { self.headers.parts.get(self.pos).map(|part| { let header = Header { - // FIXME: try to avoid this clone? - name: part.name.clone(), + name: part.name.borrow(), value: &self.headers.values[part.start..part.end], }; self.pos += 1; @@ -215,24 +212,24 @@ impl<'a> FusedIterator for Iter<'a> {} /// /// RFC 7230 section 3.2. #[derive(Clone)] -pub struct Header<'a> { - name: HeaderName, - value: &'a [u8], +pub struct Header<'n, 'v> { + name: HeaderName<'n>, + value: &'v [u8], } -impl<'a> Header<'a> { +impl<'n, 'v> Header<'n, 'v> { /// Create a new `Header`. /// /// # Notes /// /// `value` MUST NOT contain `\r\n`. - pub const fn new(name: HeaderName, value: &'a [u8]) -> Header<'a> { + pub const fn new(name: HeaderName<'n>, value: &'v [u8]) -> Header<'n, 'v> { debug_assert!(no_crlf(value)); Header { name, value } } /// Returns the name of the header. - pub const fn name(&self) -> &HeaderName { + pub const fn name(&self) -> &HeaderName<'n> { &self.name } @@ -250,7 +247,7 @@ impl<'a> Header<'a> { } } -impl<'a> fmt::Debug for Header<'a> { +impl<'n, 'v> fmt::Debug for Header<'n, 'v> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_struct("Header"); f.field("name", &self.name); @@ -265,17 +262,16 @@ impl<'a> fmt::Debug for Header<'a> { /// HTTP header name. #[derive(Clone, PartialEq, Eq)] -pub struct HeaderName { +pub struct HeaderName<'a> { /// The value MUST be lower case. - // TODO: consider exposing this lifetime. - inner: Cow<'static, str>, + inner: Cow<'a, str>, } /// Macro to create [`Name`] constants. macro_rules! known_headers { ($( ( $const_name: ident, $http_name: expr ), )+) => { $( - pub const $const_name: HeaderName = HeaderName::from_lowercase($http_name); + pub const $const_name: HeaderName<'static> = HeaderName::from_lowercase($http_name); )+ /// Create a new HTTP header `HeaderName`. @@ -283,7 +279,7 @@ macro_rules! known_headers { /// # Notes /// /// If `name` is static prefer to use [`HeaderName::from_lowercase`]. - pub fn from_str(name: &str) -> HeaderName { + pub fn from_str(name: &str) -> HeaderName<'static> { // TODO: check performance of this. Does the match statement // outweight the allocation? // TODO: match case-insensitive. @@ -296,7 +292,7 @@ macro_rules! known_headers { } } -impl HeaderName { +impl HeaderName<'static> { known_headers!( (ALLOW, "allow"), (CONTENT_LENGTH, "content-length"), @@ -310,12 +306,22 @@ impl HeaderName { /// # Panics /// /// Panics if `name` is not all ASCII lowercase. - pub const fn from_lowercase(name: &'static str) -> HeaderName { + pub const fn from_lowercase(name: &'static str) -> HeaderName<'static> { assert!(is_lower_case(name)); HeaderName { inner: Cow::Borrowed(name), } } + + /// Borrow the header name for a shorter lifetime. + /// + /// This is used in things like [`Headers::get`] and [`Iter`] for `Headers` + /// to avoid clone heap-allocated `HeaderName`s. + fn borrow<'b>(&'b self) -> HeaderName<'b> { + HeaderName { + inner: Cow::Borrowed(self.as_ref()), + } + } } /// Returns `true` if `value` does not contain `\r\n`. @@ -333,8 +339,8 @@ const fn no_crlf(value: &[u8]) -> bool { true } -impl From for HeaderName { - fn from(mut name: String) -> HeaderName { +impl From for HeaderName<'static> { + fn from(mut name: String) -> HeaderName<'static> { name.make_ascii_lowercase(); HeaderName { inner: Cow::Owned(name), @@ -342,13 +348,13 @@ impl From for HeaderName { } } -impl AsRef for HeaderName { +impl<'a> AsRef for HeaderName<'a> { fn as_ref(&self) -> &str { self.inner.as_ref() } } -impl PartialEq for HeaderName { +impl<'a> PartialEq for HeaderName<'a> { fn eq(&self, other: &str) -> bool { // NOTE: `self` is always lowercase, per the comment on the `inner` // field. @@ -356,13 +362,13 @@ impl PartialEq for HeaderName { } } -impl fmt::Debug for HeaderName { +impl<'a> fmt::Debug for HeaderName<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_ref()) } } -impl fmt::Display for HeaderName { +impl<'a> fmt::Display for HeaderName<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_ref()) } From b1dcf4680546b55231817bbe79286117044a2878 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 12:18:51 +0200 Subject: [PATCH 03/81] Add tests for HeaderName Also fixes HeaderName::from_str to actually match (case-insensitive) the known static headers. --- http/src/header.rs | 91 ++++++++++++++++++++++---------- http/tests/functional.rs | 13 +++++ http/tests/functional/header.rs | 93 +++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 29 deletions(-) create mode 100644 http/tests/functional.rs create mode 100644 http/tests/functional/header.rs diff --git a/http/src/header.rs b/http/src/header.rs index 89ab7ad08..ae2156ecb 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -247,6 +247,21 @@ impl<'n, 'v> Header<'n, 'v> { } } +/// Returns `true` if `value` does not contain `\r\n`. +const fn no_crlf(value: &[u8]) -> bool { + if value.is_empty() { + return true; + } + let mut i = 1; + while i < value.len() - 1 { + if value[i - 1] == b'\r' && value[i] == b'\n' { + return false; + } + i += 1; + } + true +} + impl<'n, 'v> fmt::Debug for Header<'n, 'v> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_struct("Header"); @@ -269,10 +284,14 @@ pub struct HeaderName<'a> { /// Macro to create [`Name`] constants. macro_rules! known_headers { - ($( ( $const_name: ident, $http_name: expr ), )+) => { - $( + ($( + $length: tt: [ + $( ( $const_name: ident, $http_name: expr ) $(,)* ),+ + ], + )+) => { + $($( pub const $const_name: HeaderName<'static> = HeaderName::from_lowercase($http_name); - )+ + )+)+ /// Create a new HTTP header `HeaderName`. /// @@ -280,25 +299,37 @@ macro_rules! known_headers { /// /// If `name` is static prefer to use [`HeaderName::from_lowercase`]. pub fn from_str(name: &str) -> HeaderName<'static> { - // TODO: check performance of this. Does the match statement - // outweight the allocation? - // TODO: match case-insensitive. - // TODO: optimise based on `name.len()`? - match name { - $( $http_name => HeaderName::$const_name, )+ - _ => HeaderName::from(name.to_string()), + // This first matches on the length of the `name`, then does a + // case-insensitive compare of the name with all known headers with + // the same length, returning a static version if a match is found. + match name.len() { + $( + $length => { + $( + if cmp_lower_case($http_name, name) { + return HeaderName::$const_name; + } + )+ + } + )+ + _ => {} } + // If it's not a known header return a custom (heap-allocated) + // header name. + HeaderName::from(name.to_string()) } } } impl HeaderName<'static> { + // NOTE: we adding here also add to the + // `functional::header::from_str_known_headers` test. known_headers!( - (ALLOW, "allow"), - (CONTENT_LENGTH, "content-length"), - (TRANSFER_ENCODING, "transfer-encoding"), - (USER_AGENT, "user-agent"), - (X_REQUEST_ID, "x-request-id"), + 5: [ (ALLOW, "allow") ], + 10: [ (USER_AGENT, "user-agent") ], + 12: [ (X_REQUEST_ID, "x-request-id") ], + 14: [ (CONTENT_LENGTH, "content-length") ], + 17: [ (TRANSFER_ENCODING, "transfer-encoding") ], ); /// Create a new HTTP header `HeaderName`. @@ -307,7 +338,7 @@ impl HeaderName<'static> { /// /// Panics if `name` is not all ASCII lowercase. pub const fn from_lowercase(name: &'static str) -> HeaderName<'static> { - assert!(is_lower_case(name)); + assert!(is_lower_case(name), "header name not lowercase"); HeaderName { inner: Cow::Borrowed(name), } @@ -322,21 +353,17 @@ impl HeaderName<'static> { inner: Cow::Borrowed(self.as_ref()), } } -} -/// Returns `true` if `value` does not contain `\r\n`. -const fn no_crlf(value: &[u8]) -> bool { - if value.is_empty() { - return true; - } - let mut i = 1; - while i < value.len() - 1 { - if value[i - 1] == b'\r' && value[i] == b'\n' { - return false; - } - i += 1; + /// Returns `true` if `self` is heap allocated. + /// + /// # Notes + /// + /// This is only header to test [`HeaderName::from_str`], not part of the + /// stable API. + #[doc(hidden)] + pub fn is_heap_allocated(&self) -> bool { + matches!(self.inner, Cow::Owned(_)) } - true } impl From for HeaderName<'static> { @@ -362,6 +389,12 @@ impl<'a> PartialEq for HeaderName<'a> { } } +impl<'a> PartialEq<&'_ str> for HeaderName<'a> { + fn eq(&self, other: &&str) -> bool { + self.eq(*other) + } +} + impl<'a> fmt::Debug for HeaderName<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_ref()) diff --git a/http/tests/functional.rs b/http/tests/functional.rs new file mode 100644 index 000000000..e4ffd43f4 --- /dev/null +++ b/http/tests/functional.rs @@ -0,0 +1,13 @@ +//! Functional tests. + +use std::mem::size_of; + +#[track_caller] +fn assert_size(expected: usize) { + assert_eq!(size_of::(), expected); +} + +#[path = "functional"] // rustfmt can't find the files. +mod functional { + mod header; +} diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs new file mode 100644 index 000000000..96396f9ea --- /dev/null +++ b/http/tests/functional/header.rs @@ -0,0 +1,93 @@ +use heph_http::header::{Header, HeaderName, Headers}; + +use crate::assert_size; + +#[test] +fn sizes() { + assert_size::(48); + assert_size::
(48); + assert_size::>(32); +} + +#[test] +fn from_str_known_headers() { + let known_headers = &[ + "allow", + "user-agent", + "x-request-id", + "content-length", + "transfer-encoding", + ]; + for name in known_headers { + let header_name = HeaderName::from_str(name); + assert!(!header_name.is_heap_allocated(), "header: {}", name); + } +} + +#[test] +fn from_str_custom() { + let unknown_headers = &["my-header", "My-Header"]; + for name in unknown_headers { + let header_name = HeaderName::from_str(name); + assert!(header_name.is_heap_allocated(), "header: {}", name); + assert_eq!(header_name, "my-header"); + assert_eq!(header_name.as_ref(), "my-header"); + } + + let name = "bllow"; // Matches length of "Allow" header. + let header_name = HeaderName::from_str(name); + assert!(header_name.is_heap_allocated(), "header: {}", name); +} + +#[test] +fn from_lowercase() { + let header_name = HeaderName::from_lowercase("my-header"); + assert_eq!(header_name, "my-header"); + assert_eq!(header_name.as_ref(), "my-header"); +} + +#[test] +#[should_panic = "header name not lowercase"] +fn from_lowercase_not_lowercase_should_panic() { + let _name = HeaderName::from_lowercase("My-Header"); +} + +#[test] +fn from_string() { + let header_name = HeaderName::from("my-header".to_owned()); + assert_eq!(header_name, "my-header"); + assert_eq!(header_name.as_ref(), "my-header"); +} + +#[test] +fn from_string_makes_lowercase() { + let header_name = HeaderName::from("My-Header".to_owned()); + assert_eq!(header_name, "my-header"); + assert_eq!(header_name.as_ref(), "my-header"); +} + +#[test] +fn compare_is_case_insensitive() { + let tests = &[ + HeaderName::from_lowercase("my-header"), + HeaderName::from("My-Header".to_owned()), + ]; + for header_name in tests { + assert_eq!(header_name, "my-header"); + assert_eq!(header_name, "My-Header"); + assert_eq!(header_name, "mY-hEaDeR"); + assert_eq!(header_name.as_ref(), "my-header"); + } + assert_eq!(tests[0], tests[1]); +} + +#[test] +fn fmt_display() { + let tests = &[ + HeaderName::from_lowercase("my-header"), + HeaderName::from("My-Header".to_owned()), + ]; + for header_name in tests { + assert_eq!(header_name.to_string(), "my-header"); + } +} From c5189f503cbf3df1e0a61737ed25b8f473d6afbb Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 12:40:41 +0200 Subject: [PATCH 04/81] Implement fmt::{Debug, Display} for ParseIntError --- http/src/from_bytes.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/http/src/from_bytes.rs b/http/src/from_bytes.rs index 2124da83a..42dd58d33 100644 --- a/http/src/from_bytes.rs +++ b/http/src/from_bytes.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// Analogous trait to the [`FromStr`] trait. /// /// [`FromStr`]: std::str::FromStr @@ -7,8 +9,15 @@ pub trait FromBytes: Sized { fn from_bytes(value: &[u8]) -> Result; } +#[derive(Debug)] pub struct ParseIntError; +impl fmt::Display for ParseIntError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid integer") + } +} + impl FromBytes for usize { type Err = ParseIntError; From cc0de4eaa6fe9a9a2b8a50cec2828ba543de59ac Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 12:41:00 +0200 Subject: [PATCH 05/81] Add tests for Header type --- http/src/header.rs | 7 ++----- http/tests/functional/header.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index ae2156ecb..ce6d7532c 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -224,7 +224,7 @@ impl<'n, 'v> Header<'n, 'v> { /// /// `value` MUST NOT contain `\r\n`. pub const fn new(name: HeaderName<'n>, value: &'v [u8]) -> Header<'n, 'v> { - debug_assert!(no_crlf(value)); + debug_assert!(no_crlf(value), "header value contains CRLF ('\\r\\n')"); Header { name, value } } @@ -249,11 +249,8 @@ impl<'n, 'v> Header<'n, 'v> { /// Returns `true` if `value` does not contain `\r\n`. const fn no_crlf(value: &[u8]) -> bool { - if value.is_empty() { - return true; - } let mut i = 1; - while i < value.len() - 1 { + while i < value.len() { if value[i - 1] == b'\r' && value[i] == b'\n' { return false; } diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index 96396f9ea..3758e4c6f 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -9,6 +9,33 @@ fn sizes() { assert_size::>(32); } +#[test] +fn new_header() { + const _MY_HEADER: Header<'static, 'static> = + Header::new(HeaderName::USER_AGENT, b"Heph-HTTP/0.1"); + let _header = Header::new(HeaderName::USER_AGENT, b""); + // Should be fine. + let _header = Header::new(HeaderName::USER_AGENT, b"\rabc\n"); +} + +#[test] +#[should_panic = "header value contains CRLF ('\\r\\n')"] +fn new_header_with_crlf_should_panic() { + let _header = Header::new(HeaderName::USER_AGENT, b"\r\n"); +} + +#[test] +#[should_panic = "header value contains CRLF ('\\r\\n')"] +fn new_header_with_crlf_should_panic2() { + let _header = Header::new(HeaderName::USER_AGENT, b"some_text\r\n"); +} + +#[test] +fn parse_header() { + const LENGTH: Header<'static, 'static> = Header::new(HeaderName::CONTENT_LENGTH, b"100"); + assert_eq!(LENGTH.parse::().unwrap(), 100); +} + #[test] fn from_str_known_headers() { let known_headers = &[ From 51db0664e0fb35b30015117e1b7f25df2f47ac23 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 14:06:33 +0200 Subject: [PATCH 06/81] Add a lifetime to FromBytes This allow the implementation str to borrow the value, rather than making an allocation. --- http/src/from_bytes.rs | 16 ++++++++++++---- http/src/header.rs | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/http/src/from_bytes.rs b/http/src/from_bytes.rs index 42dd58d33..1c55151a4 100644 --- a/http/src/from_bytes.rs +++ b/http/src/from_bytes.rs @@ -1,12 +1,12 @@ -use std::fmt; +use std::{fmt, str}; /// Analogous trait to the [`FromStr`] trait. /// /// [`FromStr`]: std::str::FromStr -pub trait FromBytes: Sized { +pub trait FromBytes<'a>: Sized { type Err; - fn from_bytes(value: &[u8]) -> Result; + fn from_bytes(value: &'a [u8]) -> Result; } #[derive(Debug)] @@ -18,7 +18,7 @@ impl fmt::Display for ParseIntError { } } -impl FromBytes for usize { +impl FromBytes<'_> for usize { type Err = ParseIntError; fn from_bytes(src: &[u8]) -> Result { @@ -42,3 +42,11 @@ impl FromBytes for usize { Ok(value) } } + +impl<'a> FromBytes<'a> for &'a str { + type Err = str::Utf8Error; + + fn from_bytes(src: &'a [u8]) -> Result { + str::from_utf8(src) + } +} diff --git a/http/src/header.rs b/http/src/header.rs index ce6d7532c..86e1f8e74 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -241,7 +241,7 @@ impl<'n, 'v> Header<'n, 'v> { /// Parse the value of the header using `T`'s [`FromBytes`] implementation. pub fn parse(&self) -> Result where - T: FromBytes, + T: FromBytes<'v>, { FromBytes::from_bytes(self.value) } From cce8b1125609d7abbe5f7c19079c17f0e3c86955 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 14:07:13 +0200 Subject: [PATCH 07/81] Add tests for the Headers type --- http/tests/functional/header.rs | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index 3758e4c6f..a74b1ce2b 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -1,4 +1,7 @@ +use std::fmt; + use heph_http::header::{Header, HeaderName, Headers}; +use heph_http::FromBytes; use crate::assert_size; @@ -9,6 +12,90 @@ fn sizes() { assert_size::>(32); } +#[test] +fn headers_add_one_header() { + const VALUE: &[u8] = b"GET"; + + let mut headers = Headers::EMPTY; + headers.add(Header::new(HeaderName::ALLOW, VALUE)); + assert_eq!(headers.len(), 1); + + check_header(&headers, &HeaderName::ALLOW, VALUE, "GET"); + check_iter(&headers, &[(HeaderName::ALLOW, VALUE)]); +} + +#[test] +fn headers_add_multiple_headers() { + const ALLOW: &[u8] = b"GET"; + const CONTENT_LENGTH: &[u8] = b"123"; + const X_REQUEST_ID: &[u8] = b"abc-def"; + + let mut headers = Headers::EMPTY; + headers.add(Header::new(HeaderName::ALLOW, ALLOW)); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, CONTENT_LENGTH)); + headers.add(Header::new(HeaderName::X_REQUEST_ID, X_REQUEST_ID)); + assert_eq!(headers.len(), 3); + + check_header(&headers, &HeaderName::ALLOW, ALLOW, "GET"); + check_header(&headers, &HeaderName::CONTENT_LENGTH, CONTENT_LENGTH, 123); + check_header(&headers, &HeaderName::X_REQUEST_ID, X_REQUEST_ID, "abc-def"); + check_iter( + &headers, + &[ + (HeaderName::ALLOW, ALLOW), + (HeaderName::CONTENT_LENGTH, CONTENT_LENGTH), + (HeaderName::X_REQUEST_ID, X_REQUEST_ID), + ], + ); +} + +#[test] +fn headers_from_header() { + const VALUE: &[u8] = b"GET"; + let header = Header::new(HeaderName::ALLOW, VALUE); + let headers = Headers::from(header.clone()); + assert_eq!(headers.len(), 1); + + check_header(&headers, &HeaderName::ALLOW, VALUE, "GET"); + check_iter(&headers, &[(HeaderName::ALLOW, VALUE)]); +} + +fn check_header<'a, T>( + headers: &'a Headers, + name: &'_ HeaderName<'_>, + value: &'_ [u8], + parsed_value: T, +) where + T: FromBytes<'a> + PartialEq + fmt::Debug, + >::Err: fmt::Debug, +{ + let got = headers.get(name).unwrap(); + assert_eq!(got.name(), name); + assert_eq!(got.value(), value); + assert_eq!(got.parse::().unwrap(), parsed_value); + + assert_eq!(headers.get_value(name).unwrap(), value); +} + +fn check_iter(headers: &'_ Headers, expected: &[(HeaderName<'_>, &'_ [u8])]) { + let mut len = expected.len(); + let mut iter = headers.iter(); + assert_eq!(iter.len(), len); + assert_eq!(iter.size_hint(), (len, Some(len))); + for (name, value) in expected { + let got = iter.next().unwrap(); + assert_eq!(got.name(), name); + assert_eq!(got.value(), *value); + len -= 1; + assert_eq!(iter.len(), len); + assert_eq!(iter.size_hint(), (len, Some(len))); + } + assert_eq!(iter.count(), 0); + + let iter = headers.iter(); + assert_eq!(iter.count(), expected.len()); +} + #[test] fn new_header() { const _MY_HEADER: Header<'static, 'static> = From 3842ea58e5df6102684a501665186035377e5f8e Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 14:17:42 +0200 Subject: [PATCH 08/81] Add test for Header::from_str matching case-insensitive --- http/tests/functional/header.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index a74b1ce2b..cedc41f7d 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -135,6 +135,10 @@ fn from_str_known_headers() { for name in known_headers { let header_name = HeaderName::from_str(name); assert!(!header_name.is_heap_allocated(), "header: {}", name); + + // Matching should be case-insensitive. + let header_name = HeaderName::from_str(&name.to_uppercase()); + assert!(!header_name.is_heap_allocated(), "header: {}", name); } } From 14449cb4cc98d3d27f967e93ea0f091fa1fba0b2 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 14:32:15 +0200 Subject: [PATCH 09/81] Add tests for Method type --- http/src/method.rs | 36 ++++++++++++++- http/tests/functional.rs | 1 + http/tests/functional/method.rs | 80 +++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 http/tests/functional/method.rs diff --git a/http/src/method.rs b/http/src/method.rs index d4b535290..df61f9d12 100644 --- a/http/src/method.rs +++ b/http/src/method.rs @@ -1,3 +1,5 @@ +//! Module with HTTP method related types. + use std::fmt; use std::str::FromStr; @@ -6,16 +8,42 @@ use crate::cmp_lower_case; /// HTTP method. /// /// RFC 7231 section 4. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Method { + /// GET method. + /// + /// RFC 7231 section 4.3.1. Get, + /// HEAD method. + /// + /// RFC 7231 section 4.3.2. Head, + /// POST method. + /// + /// RFC 7231 section 4.3.3. Post, + /// PUT method. + /// + /// RFC 7231 section 4.3.4. Put, + /// DELETE method. + /// + /// RFC 7231 section 4.3.5. Delete, + /// CONNECT method. + /// + /// RFC 7231 section 4.3.6. Connect, + /// OPTIONS method. + /// + /// RFC 7231 section 4.3.7. Options, + /// TRACE method. + /// + /// RFC 7231 section 4.3.8. Trace, + /// PATCH method. + /// /// RFC 5789. Patch, } @@ -63,6 +91,12 @@ impl fmt::Display for Method { #[derive(Copy, Clone, Debug)] pub struct UnknownMethod; +impl fmt::Display for UnknownMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("unknown method") + } +} + impl FromStr for Method { type Err = UnknownMethod; diff --git a/http/tests/functional.rs b/http/tests/functional.rs index e4ffd43f4..0b94fdb1d 100644 --- a/http/tests/functional.rs +++ b/http/tests/functional.rs @@ -10,4 +10,5 @@ fn assert_size(expected: usize) { #[path = "functional"] // rustfmt can't find the files. mod functional { mod header; + mod method; } diff --git a/http/tests/functional/method.rs b/http/tests/functional/method.rs new file mode 100644 index 000000000..e70121529 --- /dev/null +++ b/http/tests/functional/method.rs @@ -0,0 +1,80 @@ +use heph_http::Method::{self, *}; + +use crate::assert_size; + +#[test] +fn size() { + assert_size::(1); +} + +#[test] +fn is_head() { + assert!(Head.is_head()); + let tests = &[Get, Post, Put, Delete, Connect, Options, Trace, Patch]; + for method in tests { + assert!(!method.is_head()); + } +} + +#[test] +fn is_safe() { + let safe = &[Get, Head, Options, Trace]; + for method in safe { + assert!(method.is_safe()); + } + let not_safe = &[Post, Put, Delete, Connect, Patch]; + for method in not_safe { + assert!(!method.is_safe()); + } +} +#[test] +fn is_idempotent() { + let idempotent = &[Get, Head, Put, Delete, Options, Trace]; + for method in idempotent { + assert!(method.is_idempotent()); + } + let not_idempotent = &[Post, Connect, Patch]; + for method in not_idempotent { + assert!(!method.is_idempotent()); + } +} + +#[test] +fn from_str() { + let tests = &[ + (Get, "GET"), + (Head, "HEAD"), + (Post, "POST"), + (Put, "PUT"), + (Delete, "DELETE"), + (Connect, "CONNECT"), + (Options, "OPTIONS"), + (Trace, "TRACE"), + (Patch, "PATCH"), + ]; + for (expected, input) in tests { + let got: Method = input.parse().unwrap(); + assert_eq!(got, *expected); + // Must be case-insensitive. + let got: Method = input.to_lowercase().parse().unwrap(); + assert_eq!(got, *expected); + } +} + +#[test] +fn fmt_display() { + let tests = &[ + (Get, "GET"), + (Head, "HEAD"), + (Post, "POST"), + (Put, "PUT"), + (Delete, "DELETE"), + (Connect, "CONNECT"), + (Options, "OPTIONS"), + (Trace, "TRACE"), + (Patch, "PATCH"), + ]; + for (method, expected) in tests { + assert_eq!(*method.to_string(), **expected); + } +} From d8b48ab1d0f78329336af205067425bea84b1270 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 14:39:15 +0200 Subject: [PATCH 10/81] Add tests for the Version type --- http/src/version.rs | 14 ++++++++++++- http/tests/functional.rs | 1 + http/tests/functional/method.rs | 1 + http/tests/functional/version.rs | 34 ++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 http/tests/functional/version.rs diff --git a/http/src/version.rs b/http/src/version.rs index 22953ac47..e9a1f2b62 100644 --- a/http/src/version.rs +++ b/http/src/version.rs @@ -1,14 +1,20 @@ +//! Module with HTTP version related types. + use std::fmt; use std::str::FromStr; /// HTTP version. /// /// RFC 7231 section 2.6. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Version { /// HTTP/1.0. + /// + /// RFC 1945. Http10, /// HTTP/1.1. + /// + /// RFC 7230. Http11, } @@ -42,6 +48,12 @@ impl fmt::Display for Version { #[derive(Copy, Clone, Debug)] pub struct UnknownVersion; +impl fmt::Display for UnknownVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("unknown version") + } +} + impl FromStr for Version { type Err = UnknownVersion; diff --git a/http/tests/functional.rs b/http/tests/functional.rs index 0b94fdb1d..c0748e62d 100644 --- a/http/tests/functional.rs +++ b/http/tests/functional.rs @@ -11,4 +11,5 @@ fn assert_size(expected: usize) { mod functional { mod header; mod method; + mod version; } diff --git a/http/tests/functional/method.rs b/http/tests/functional/method.rs index e70121529..aca97babb 100644 --- a/http/tests/functional/method.rs +++ b/http/tests/functional/method.rs @@ -27,6 +27,7 @@ fn is_safe() { assert!(!method.is_safe()); } } + #[test] fn is_idempotent() { let idempotent = &[Get, Head, Put, Delete, Options, Trace]; diff --git a/http/tests/functional/version.rs b/http/tests/functional/version.rs new file mode 100644 index 000000000..40de92b93 --- /dev/null +++ b/http/tests/functional/version.rs @@ -0,0 +1,34 @@ +use heph_http::Version::{self, *}; + +use crate::assert_size; + +#[test] +fn size() { + assert_size::(1); +} + +#[test] +fn is_idempotent() { + let tests = &[(Http10, Http11), (Http11, Http11)]; + for (version, expected) in tests { + assert_eq!(version.highest_minor(), *expected); + } +} + +#[test] +fn from_str() { + let tests = &[(Http10, "HTTP/1.0"), (Http11, "HTTP/1.1")]; + for (expected, input) in tests { + let got: Version = input.parse().unwrap(); + assert_eq!(got, *expected); + // NOTE: version (unlike most other types) is matched case-sensitive. + } +} + +#[test] +fn fmt_display() { + let tests = &[(Http10, "HTTP/1.0"), (Http11, "HTTP/1.1")]; + for (method, expected) in tests { + assert_eq!(*method.to_string(), **expected); + } +} From 26c6aa84a56d87f10546d3c72f0686b0b332b58e Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 15:42:54 +0200 Subject: [PATCH 11/81] Add more StatusCode constants Also some tests for the is_* functions. --- http/src/status_code.rs | 284 +++++++++++++++++---------- http/tests/functional.rs | 1 + http/tests/functional/status_code.rs | 61 ++++++ 3 files changed, 238 insertions(+), 108 deletions(-) create mode 100644 http/tests/functional/status_code.rs diff --git a/http/src/status_code.rs b/http/src/status_code.rs index 5e9c393c4..e46cccbf6 100644 --- a/http/src/status_code.rs +++ b/http/src/status_code.rs @@ -2,229 +2,297 @@ use std::fmt; /// Response Status Code. /// +/// A complete list can be found at the HTTP Status Code Registry: +/// . +/// /// RFC 7231 section 6. #[derive(Copy, Clone, Debug)] -pub struct StatusCode(u16); - -// TODO: Refer to RFC 7231. -// TODO: add more constants. +pub struct StatusCode(pub u16); impl StatusCode { + // 1xx range. /// Continue. /// - /// Section 6.2.1 + /// RFC 7231 section 6.2.1. pub const CONTINUE: StatusCode = StatusCode(100); - /// Switching Protocols. /// - /// Section 6.2.2 + /// RFC 7231 section 6.2.2. pub const SWITCHING_PROTOCOLS: StatusCode = StatusCode(101); + /// Processing. + /// + /// RFC 2518. + pub const PROCESSING: StatusCode = StatusCode(103); + /// Early Hints. + /// + /// RFC 8297. + pub const EARLY_HINTS: StatusCode = StatusCode(104); + // 2xx range. /// OK. /// - /// Section 6.3.1 + /// RFC 7231 section 6.3.1. pub const OK: StatusCode = StatusCode(200); - /// Created. /// - /// Section 6.3.2 + /// RFC 7231 section 6.3.2. pub const CREATED: StatusCode = StatusCode(201); - /// Accepted. /// - /// Section 6.3.3 + /// RFC 7231 section 6.3.3. pub const ACCEPTED: StatusCode = StatusCode(202); - /// Non-Authoritative Information. /// - /// Section 6.3.4 + /// RFC 7231 section 6.3.4. pub const NON_AUTHORITATIVE_INFORMATION: StatusCode = StatusCode(203); - /// No Content. /// - /// Section 6.3.5 + /// RFC 7231 section 6.3.5. pub const NO_CONTENT: StatusCode = StatusCode(204); - /// Reset Content. /// - /// Section 6.3.6 + /// RFC 7231 section 6.3.6. pub const RESET_CONTENT: StatusCode = StatusCode(205); - - /* TODO. /// Partial Content. /// - /// Section 4.1 of [RFC7233] - pub const AA: StatusCode = StatusCode(206); + /// RFC 7233 section 4.1. + pub const PARTIAL_CONTENT: StatusCode = StatusCode(206); + /// Multi-Status. + /// + /// RFC 4918. + pub const MULTI_STATUS: StatusCode = StatusCode(207); + /// Already Reported. + /// + /// RFC 5842. + pub const ALREADY_REPORTED: StatusCode = StatusCode(208); + /// IM Used. + /// + /// RFC 3229. + pub const IM_USED: StatusCode = StatusCode(208); + + // 3xx range. /// Multiple Choices. /// - /// Section 6.4.1 - pub const AA: StatusCode = StatusCode(300); + /// RFC 7231 section 6.4.1. + pub const MULTIPLE_CHOICES: StatusCode = StatusCode(300); /// Moved Permanently. /// - /// Section 6.4.2 - pub const AA: StatusCode = StatusCode(301); + /// RFC 7231 section 6.4.2. + pub const MOVED_PERMANENTLY: StatusCode = StatusCode(301); /// Found. /// - /// Section 6.4.3 - pub const AA: StatusCode = StatusCode(302); + /// RFC 7231 section 6.4.3. + pub const FOUND: StatusCode = StatusCode(302); /// See Other. /// - /// Section 6.4.4 - pub const AA: StatusCode = StatusCode(303); + /// RFC 7231 section 6.4.4. + pub const SEE_OTHER: StatusCode = StatusCode(303); /// Not Modified. /// - /// Section 4.1 of [RFC7232] - pub const AA: StatusCode = StatusCode(304); + /// RFC 7232 section 4.1. + pub const NOT_MODIFIED: StatusCode = StatusCode(304); + // NOTE: 306 is unused, per RFC 7231 section 6.4.6. /// Use Proxy. /// - /// Section 6.4.5 - pub const AA: StatusCode = StatusCode(305); + /// RFC 7231 section 6.4.5. + pub const USE_PROXY: StatusCode = StatusCode(305); /// Temporary Redirect. /// - /// Section 6.4.7 - pub const AA: StatusCode = StatusCode(307); - */ + /// RFC 7231 section 6.4.7. + pub const TEMPORARY_REDIRECT: StatusCode = StatusCode(307); + /// Permanent Redirect. + /// + /// RFC 7538. + pub const PERMANENT_REDIRECT: StatusCode = StatusCode(308); + // 4xx range. /// Bad Request. /// - /// Section 6.5.1 + /// RFC 7231 section 6.5.1. pub const BAD_REQUEST: StatusCode = StatusCode(400); - - /* /// Unauthorized. /// - /// Section 3.1 of [RFC7235] - pub const AA: StatusCode = StatusCode(401); + /// RFC 7235 section 3.1. + pub const UNAUTHORIZED: StatusCode = StatusCode(401); /// Payment Required. /// - /// Section 6.5.2 - pub const AA: StatusCode = StatusCode(402); + /// RFC 7231 section 6.5.2. + pub const PAYMENT_REQUIRED: StatusCode = StatusCode(402); /// Forbidden. /// - /// Section 6.5.3 - pub const AA: StatusCode = StatusCode(403); - */ - + /// RFC 7231 section 6.5.3. + pub const FORBIDDEN: StatusCode = StatusCode(403); /// Not Found. /// - /// Section 6.5.4 + /// RFC 7231 section 6.5.4. pub const NOT_FOUND: StatusCode = StatusCode(404); - /// Method Not Allowed. /// - /// Section 6.5.5 + /// RFC 7231 section 6.5.5. pub const METHOD_NOT_ALLOWED: StatusCode = StatusCode(405); - - /* /// Not Acceptable. /// - /// Section 6.5.6 - pub const AA: StatusCode = StatusCode(406); + /// RFC 7231 section 6.5.6. + pub const NOT_ACCEPTABLE: StatusCode = StatusCode(406); /// Proxy Authentication Required. /// - /// Section 3.2 of [RFC7235] - pub const AA: StatusCode = StatusCode(407); + /// RFC 7235 section 3.2. + pub const PROXY_AUTHENTICATION_REQUIRED: StatusCode = StatusCode(407); /// Request Timeout. /// - /// Section 6.5.7 - pub const AA: StatusCode = StatusCode(408); + /// RFC 7231 section 6.5.7. + pub const REQUEST_TIMEOUT: StatusCode = StatusCode(408); /// Conflict. /// - /// Section 6.5.8 - pub const AA: StatusCode = StatusCode(409); + /// RFC 7231 section 6.5.8. + pub const CONFLICT: StatusCode = StatusCode(409); /// Gone. /// - /// Section 6.5.9 - pub const AA: StatusCode = StatusCode(410); + /// RFC 7231 section 6.5.9. + pub const GONE: StatusCode = StatusCode(410); /// Length Required. /// - /// Section 6.5.10 - pub const AA: StatusCode = StatusCode(411); + /// RFC 7231 section 6.5.10. + pub const LENGTH_REQUIRED: StatusCode = StatusCode(411); /// Precondition Failed. /// - /// Section 4.2 of [RFC7232] - pub const AA: StatusCode = StatusCode(412); - */ - + /// RFC 7232 section 4.2 and RFC 8144 section 3.2. + pub const PRECONDITION_FAILED: StatusCode = StatusCode(412); /// Payload Too Large. /// - /// Section 6.5.11 + /// RFC 7231 section 6.5.11. pub const PAYLOAD_TOO_LARGE: StatusCode = StatusCode(413); - - /* /// URI Too Long. /// - /// Section 6.5.12 - pub const AA: StatusCode = StatusCode(414); + /// RFC 7231 section 6.5.12. + pub const URI_TOO_LONG: StatusCode = StatusCode(414); /// Unsupported Media Type. /// - /// Section 6.5.13 - pub const AA: StatusCode = StatusCode(415); + /// RFC 7231 section 6.5.13 and RFC 7694 section 3. + pub const UNSUPPORTED_MEDIA_TYPE: StatusCode = StatusCode(415); /// Range Not Satisfiable. /// - /// Section 4.4 of [RFC7233]. - pub const AA: StatusCode = StatusCode(416); + /// RFC 7233 section 4.4. + pub const RANGE_NOT_SATISFIABLE: StatusCode = StatusCode(416); /// Expectation Failed. /// - /// Section 6.5.14 - pub const AA: StatusCode = StatusCode(417); + /// RFC 7231 section 6.5.14. + pub const EXPECTATION_FAILED: StatusCode = StatusCode(417); + // NOTE: 418-420 are unassigned. + /// Misdirected Request. + /// + /// RFC 7540 section 9.1.2. + pub const MISDIRECTED_REQUEST: StatusCode = StatusCode(421); + /// Unprocessable Entity. + /// + /// RFC 4918. + pub const UNPROCESSABLE_ENTITY: StatusCode = StatusCode(422); + /// Locked. + /// + /// RFC 4918. + pub const LOCKED: StatusCode = StatusCode(423); + /// Failed Dependency. + /// + /// RFC 4918. + pub const FAILED_DEPENDENCY: StatusCode = StatusCode(424); + /// Too Early. + /// + /// RFC 8470. + pub const TOO_EARLY: StatusCode = StatusCode(425); /// Upgrade Required. /// - /// Section 6.5.15 - pub const AA: StatusCode = StatusCode(426); - /// Internal Server Error. + /// RFC 7231 section 6.5.15. + pub const UPGRADE_REQUIRED: StatusCode = StatusCode(426); + // NOTE: 427 is unassigned. + /// Precondition Required. + /// + /// RFC 6585. + pub const PRECONDITION_REQUIRED: StatusCode = StatusCode(428); + /// Too Many Requests. + /// + /// RFC 6585. + pub const TOO_MANY_REQUESTS: StatusCode = StatusCode(429); + // NOTE: 320 is unassigned. + /// Request Header Fields Too Large. /// - /// Section 6.6.1 - pub const AA: StatusCode = StatusCode(500); - */ + /// RFC 6585. + pub const REQUEST_HEADER_FIELDS_TOO_LARGE: StatusCode = StatusCode(431); + // NOTE: 432-450 are unassigned. + /// Unavailable For Legal Reasons. + /// + /// RFC 7725. + pub const UNAVAILABLE_FOR_LEGAL_REASONS: StatusCode = StatusCode(451); + // 5xx range. + /// Internal Server Error. + /// + /// RFC 7231 section 6.6.1. + pub const INTERNAL_SERVER_ERROR: StatusCode = StatusCode(500); /// Not Implemented. /// - /// Section 6.6.2 + /// RFC 7231 section 6.6.2. pub const NOT_IMPLEMENTED: StatusCode = StatusCode(501); - - /* /// Bad Gateway. /// - /// Section 6.6.3 - pub const AA: StatusCode = StatusCode(502); + /// RFC 7231 section 6.6.3. + pub const BAD_GATEWAY: StatusCode = StatusCode(502); /// Service Unavailable. /// - /// Section 6.6.4 - pub const AA: StatusCode = StatusCode(503); + /// RFC 7231 section 6.6.4. + pub const SERVICE_UNAVAILABLE: StatusCode = StatusCode(503); /// Gateway Timeout. /// - /// Section 6.6.5 - pub const AA: StatusCode = StatusCode(504); + /// RFC 7231 section 6.6.5. + pub const GATEWAY_TIMEOUT: StatusCode = StatusCode(504); /// HTTP Version Not Supported. /// - /// Section 6.6.6 - pub const AA: StatusCode = StatusCode(505); - */ + /// RFC 7231 section 6.6.6. + pub const HTTP_VERSION_NOT_SUPPORTED: StatusCode = StatusCode(505); + /// Variant Also Negotiates. + /// + /// RFC 2295. + pub const VARIANT_ALSO_NEGOTIATES: StatusCode = StatusCode(506); + /// Insufficient Storage. + /// + /// RFC 4918. + pub const INSUFFICIENT_STORAGE: StatusCode = StatusCode(507); + /// Loop Detected. + /// + /// RFC 5842. + pub const LOOP_DETECTED: StatusCode = StatusCode(508); + // NOTE: 509 is unassigned. + /// Not Extended. + /// + /// RFC 2774. + pub const NOT_EXTENDED: StatusCode = StatusCode(510); + /// Network Authentication Required. + /// + /// RFC 6585. + pub const NETWORK_AUTHENTICATION_REQUIRED: StatusCode = StatusCode(511); /// Returns `true` if the status code is in 1xx range. - const fn is_informational(self) -> bool { - self.0 >= 100 && self.0 < 199 + pub const fn is_informational(self) -> bool { + self.0 >= 100 && self.0 <= 199 } /// Returns `true` if the status code is in 2xx range. - const fn is_successful(self) -> bool { - self.0 >= 200 && self.0 < 299 + pub const fn is_successful(self) -> bool { + self.0 >= 200 && self.0 <= 299 } /// Returns `true` if the status code is in 3xx range. - const fn is_redirect(self) -> bool { - self.0 >= 300 && self.0 < 399 + pub const fn is_redirect(self) -> bool { + self.0 >= 300 && self.0 <= 399 } /// Returns `true` if the status code is in 4xx range. - const fn is_client_error(self) -> bool { - self.0 >= 400 && self.0 < 499 + pub const fn is_client_error(self) -> bool { + self.0 >= 400 && self.0 <= 499 } /// Returns `true` if the status code is in 5xx range. - const fn is_server_error(self) -> bool { - self.0 >= 500 && self.0 < 599 + pub const fn is_server_error(self) -> bool { + self.0 >= 500 && self.0 <= 599 } } diff --git a/http/tests/functional.rs b/http/tests/functional.rs index c0748e62d..988d9d7ef 100644 --- a/http/tests/functional.rs +++ b/http/tests/functional.rs @@ -11,5 +11,6 @@ fn assert_size(expected: usize) { mod functional { mod header; mod method; + mod status_code; mod version; } diff --git a/http/tests/functional/status_code.rs b/http/tests/functional/status_code.rs new file mode 100644 index 000000000..cd9cfdd69 --- /dev/null +++ b/http/tests/functional/status_code.rs @@ -0,0 +1,61 @@ +use heph_http::StatusCode; + +#[test] +fn is_informational() { + let informational = &[100, 101, 199]; + for status in informational { + assert!(StatusCode(*status).is_informational()); + } + let not_informational = &[0, 10, 200, 201, 400, 999]; + for status in not_informational { + assert!(!StatusCode(*status).is_informational()); + } +} + +#[test] +fn is_successful() { + let successful = &[200, 201, 299]; + for status in successful { + assert!(StatusCode(*status).is_successful()); + } + let not_successful = &[0, 10, 100, 101, 400, 999]; + for status in not_successful { + assert!(!StatusCode(*status).is_successful()); + } +} + +#[test] +fn is_redirect() { + let redirect = &[300, 301, 399]; + for status in redirect { + assert!(StatusCode(*status).is_redirect()); + } + let not_redirect = &[0, 10, 100, 101, 400, 999]; + for status in not_redirect { + assert!(!StatusCode(*status).is_redirect()); + } +} + +#[test] +fn is_client_error() { + let client_error = &[400, 401, 499]; + for status in client_error { + assert!(StatusCode(*status).is_client_error()); + } + let not_client_error = &[0, 10, 100, 101, 300, 500, 999]; + for status in not_client_error { + assert!(!StatusCode(*status).is_client_error()); + } +} + +#[test] +fn is_server_error() { + let server_error = &[500, 501, 599]; + for status in server_error { + assert!(StatusCode(*status).is_server_error()); + } + let not_server_error = &[0, 10, 100, 101, 400, 600, 999]; + for status in not_server_error { + assert!(!StatusCode(*status).is_server_error()); + } +} From aca8738a6bea7b46b0f16a1b1717681fa86893fc Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 15:47:50 +0200 Subject: [PATCH 12/81] Remove dev allow statements This removes two dependencies: socket2 and getrandom, neither are needed anymore. Also remove a number of unused imports. --- http/Cargo.toml | 2 -- http/src/lib.rs | 20 +------------------- http/src/request.rs | 2 +- http/src/response.rs | 2 +- http/src/server.rs | 19 ++++--------------- 5 files changed, 7 insertions(+), 38 deletions(-) diff --git a/http/Cargo.toml b/http/Cargo.toml index c73cd503d..134a05083 100644 --- a/http/Cargo.toml +++ b/http/Cargo.toml @@ -7,9 +7,7 @@ edition = "2018" heph = { version = "0.3.0", path = "../", default-features = false } httparse = { version = "1.4.0", default-features = false } log = { version = "0.4.8", default-features = false } -socket2 = { version = "0.4.0", default-features = false, features = ["all"] } [dev-dependencies] -getrandom = { version = "0.2.2", default-features = false, features = ["std"] } # Enable logging panics via `std-logger`. std-logger = { version = "0.4.0", default-features = false, features = ["log-panic", "nightly"] } diff --git a/http/src/lib.rs b/http/src/lib.rs index 939eb9d45..094fd5605 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -1,22 +1,4 @@ -#![allow( - unreachable_code, - unused_variables, - unused_mut, - dead_code, - unused_imports -)] // FIXME: remove. -#![feature( - const_eval_limit, - const_panic, - maybe_uninit_array_assume_init, - maybe_uninit_slice, - maybe_uninit_uninit_array, - maybe_uninit_write_slice -)] - -use std::convert::AsRef; -use std::fmt; -use std::str::FromStr; +#![feature(const_panic)] mod body; mod from_bytes; diff --git a/http/src/request.rs b/http/src/request.rs index 91b9fb487..05a63437d 100644 --- a/http/src/request.rs +++ b/http/src/request.rs @@ -2,7 +2,7 @@ use std::fmt; -use crate::{Header, Headers, Method, Version}; +use crate::{Headers, Method, Version}; pub struct Request { pub(crate) method: Method, diff --git a/http/src/response.rs b/http/src/response.rs index 08f395660..0d5830206 100644 --- a/http/src/response.rs +++ b/http/src/response.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::{Header, Headers, StatusCode, Version}; +use crate::{Headers, StatusCode, Version}; pub struct Response { pub(crate) version: Version, diff --git a/http/src/server.rs b/http/src/server.rs index a01220134..23cb04bd5 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -6,28 +6,18 @@ // TODO: chunked encoding. // TODO: reading request body. -use std::convert::TryFrom; use std::fmt; -use std::future::Future; use std::io::{self, IoSlice, Write}; use std::net::SocketAddr; -use std::os::raw::c_int; use std::pin::Pin; -use std::sync::Arc; use std::task::{self, Poll}; -use heph::actor::messages::Terminate; -use heph::net::{tcp, TcpListener, TcpServer, TcpStream}; -use heph::rt::Signal; +use heph::net::{tcp, TcpServer, TcpStream}; use heph::spawn::{ActorOptions, Spawn}; use heph::{actor, rt, Actor, NewActor, Supervisor}; use httparse::EMPTY_HEADER; -use log::debug; -use socket2::{Domain, Protocol, Socket, Type}; -use crate::{ - FromBytes, Header, HeaderName, Headers, Method, Request, Response, StatusCode, Version, -}; +use crate::{FromBytes, HeaderName, Headers, Method, Request, Response, StatusCode, Version}; /// Maximum size of the header (the start line and the headers). /// @@ -40,6 +30,7 @@ pub const MAX_HEADERS: usize = 64; /// Minimum amount of bytes read from the connection or the buffer will be /// grown. +#[allow(dead_code)] // FIXME: use this in reading. const MIN_READ_SIZE: usize = 512; /// Size of the buffer used in [`Connection`]. @@ -75,7 +66,7 @@ where fn new( &mut self, - mut ctx: actor::Context, + ctx: actor::Context, arg: Self::Argument, ) -> Result { self.inner.new(ctx, arg).map(|inner| HttpServer { inner }) @@ -461,8 +452,6 @@ impl Connection { where B: crate::Body, { - use crate::Body; - // Bytes of the (next) request. self.clear_buffer(); let ignore_end = self.buf.len(); From 8af7ef6da5944da156d20340cf9cfd4b4848c7b6 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 16:06:48 +0200 Subject: [PATCH 13/81] Fix some doc links --- http/src/server.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/http/src/server.rs b/http/src/server.rs index 23cb04bd5..ce9aed0f0 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -102,6 +102,8 @@ impl Clone for Setup { /// see "Example 2 my ip" (in the examples directory of the source code) for an /// example of that. /// +/// [`Terminate`]: heph::actor::messages::Terminate +/// /// # Examples /// /// TODO. @@ -737,6 +739,10 @@ impl fmt::Display for RequestError { /// /// The message implements [`From`]`<`[`Terminate`]`>` and /// [`TryFrom`]`<`[`Signal`]`>` for the message, allowing for graceful shutdown. +/// +/// [`Terminate`]: heph::actor::messages::Terminate +/// [`TryFrom`]: std::convert::TryFrom +/// [`Signal`]: heph::rt::Signal pub use heph::net::tcp::server::Message; /// Error returned by the [`HttpServer`] actor. From b67e54107eea5cd72b79eebbfba6128e3d29b923 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 16:19:26 +0200 Subject: [PATCH 14/81] Implement FromBytes for all unsigned integers Also expands the documentation and adds tests. --- http/src/from_bytes.rs | 52 ++++++++++++++++++----------- http/tests/functional.rs | 1 + http/tests/functional/from_bytes.rs | 48 ++++++++++++++++++++++++++ http/tests/functional/header.rs | 3 +- 4 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 http/tests/functional/from_bytes.rs diff --git a/http/src/from_bytes.rs b/http/src/from_bytes.rs index 1c55151a4..9824bd811 100644 --- a/http/src/from_bytes.rs +++ b/http/src/from_bytes.rs @@ -1,11 +1,18 @@ use std::{fmt, str}; -/// Analogous trait to the [`FromStr`] trait. +/// Analogous trait to [`FromStr`]. +/// +/// The main use case for this trait in [`Header::parse`]. Because of this the +/// implementations should expect the `value`s passed to be ASCII/UTF-8, but +/// this not true in all cases. /// /// [`FromStr`]: std::str::FromStr +/// [`Header::parse`]: crate::Header::parse pub trait FromBytes<'a>: Sized { + /// Error returned by parsing the bytes. type Err; + /// Parse the `value`. fn from_bytes(value: &'a [u8]) -> Result; } @@ -18,31 +25,38 @@ impl fmt::Display for ParseIntError { } } -impl FromBytes<'_> for usize { - type Err = ParseIntError; - - fn from_bytes(src: &[u8]) -> Result { - if src.is_empty() { - return Err(ParseIntError); - } +macro_rules! int_impl { + ($( $ty: ty ),+) => { + $( + impl FromBytes<'_> for $ty { + type Err = ParseIntError; - let mut value = 0; - for b in src.iter().copied() { - if b >= b'0' && b <= b'9' { - // TODO: check if this doesn't get compiled away. - if value >= (usize::MAX / 10) { - // Overflow. + fn from_bytes(src: &[u8]) -> Result { + if src.is_empty() { return Err(ParseIntError); } - value = (value * 10) + (b - b'0') as usize; - } else { - return Err(ParseIntError); + + let mut value: $ty = 0; + for b in src.iter().copied() { + if b >= b'0' && b <= b'9' { + if value >= (<$ty>::MAX / 10) { + // Overflow. + return Err(ParseIntError); + } + value = (value * 10) + (b - b'0') as $ty; + } else { + return Err(ParseIntError); + } + } + Ok(value) } } - Ok(value) - } + )+ + }; } +int_impl!(u8, u16, u32, u64, usize); + impl<'a> FromBytes<'a> for &'a str { type Err = str::Utf8Error; diff --git a/http/tests/functional.rs b/http/tests/functional.rs index 988d9d7ef..3755ab6b9 100644 --- a/http/tests/functional.rs +++ b/http/tests/functional.rs @@ -9,6 +9,7 @@ fn assert_size(expected: usize) { #[path = "functional"] // rustfmt can't find the files. mod functional { + mod from_bytes; mod header; mod method; mod status_code; diff --git a/http/tests/functional/from_bytes.rs b/http/tests/functional/from_bytes.rs new file mode 100644 index 000000000..b34b1a07a --- /dev/null +++ b/http/tests/functional/from_bytes.rs @@ -0,0 +1,48 @@ +use std::fmt; + +use heph_http::FromBytes; + +#[test] +fn str() { + test_parse(b"123", "123"); + test_parse(b"abc", "abc"); +} + +#[test] +fn str_not_utf8() { + test_parse_fail::<&str>(&[0, 255]); +} + +#[test] +fn integers() { + test_parse(b"123", 123u8); + test_parse(b"123", 123u16); + test_parse(b"123", 123u32); + test_parse(b"123", 123u64); + test_parse(b"123", 123usize); +} + +#[test] +fn integers_overflow() { + test_parse_fail::(b"256"); + test_parse_fail::(b"65536"); + test_parse_fail::(b"4294967296"); + test_parse_fail::(b"18446744073709551615"); + test_parse_fail::(b"18446744073709551615"); +} + +fn test_parse<'a, T>(value: &'a [u8], expected: T) +where + T: FromBytes<'a> + fmt::Debug + PartialEq, + >::Err: fmt::Debug, +{ + assert_eq!(T::from_bytes(value).unwrap(), expected); +} + +fn test_parse_fail<'a, T>(value: &'a [u8]) +where + T: FromBytes<'a> + fmt::Debug + PartialEq, + >::Err: fmt::Debug, +{ + assert!(T::from_bytes(value).is_err()); +} diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index cedc41f7d..046136704 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -37,7 +37,8 @@ fn headers_add_multiple_headers() { assert_eq!(headers.len(), 3); check_header(&headers, &HeaderName::ALLOW, ALLOW, "GET"); - check_header(&headers, &HeaderName::CONTENT_LENGTH, CONTENT_LENGTH, 123); + #[rustfmt::skip] + check_header(&headers, &HeaderName::CONTENT_LENGTH, CONTENT_LENGTH, 123usize); check_header(&headers, &HeaderName::X_REQUEST_ID, X_REQUEST_ID, "abc-def"); check_iter( &headers, From 527aa5f309ff67b81c1ec841c6b9680f685f209f Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 16:47:08 +0200 Subject: [PATCH 15/81] Check path first in my_ip example --- http/examples/my_ip.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index f2b7176bc..bfdb87fcb 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -82,13 +82,13 @@ async fn http_actor( Ok(Some(mut request)) => { debug!("received request: {:?}", request); let mut headers = Headers::EMPTY; - let (code, body) = if !matches!(request.method(), Method::Get | Method::Head) { + let (code, body) = if request.path() != "/" { + request.body_mut().ignore()?; + (StatusCode::NOT_FOUND, "Not found".into()) + } else if !matches!(request.method(), Method::Get | Method::Head) { request.body_mut().ignore()?; headers.add(Header::new(HeaderName::ALLOW, b"GET, HEAD")); (StatusCode::METHOD_NOT_ALLOWED, "Method not allowed".into()) - } else if request.path() != "/" { - request.body_mut().ignore()?; - (StatusCode::NOT_FOUND, "Not found".into()) } else if request.body().len() != 0 { request.body_mut().ignore()?; let body = Cow::from("Not expecting a body"); From 7dd45cb8080c2937ca7a0f60504dfcb424c1efba Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 16:54:38 +0200 Subject: [PATCH 16/81] Add some more tests for the integer impls of FromBytes --- http/tests/functional/from_bytes.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/http/tests/functional/from_bytes.rs b/http/tests/functional/from_bytes.rs index b34b1a07a..4b23fc264 100644 --- a/http/tests/functional/from_bytes.rs +++ b/http/tests/functional/from_bytes.rs @@ -31,6 +31,30 @@ fn integers_overflow() { test_parse_fail::(b"18446744073709551615"); } +#[test] +fn empty_integers() { + test_parse_fail::(b""); + test_parse_fail::(b""); + test_parse_fail::(b""); + test_parse_fail::(b""); + test_parse_fail::(b""); +} + +#[test] +fn invalid_integers() { + test_parse_fail::(b"abc"); + test_parse_fail::(b"abc"); + test_parse_fail::(b"abc"); + test_parse_fail::(b"abc"); + test_parse_fail::(b"abc"); + + test_parse_fail::(b"2a"); + test_parse_fail::(b"2a"); + test_parse_fail::(b"2a"); + test_parse_fail::(b"2a"); + test_parse_fail::(b"2a"); +} + fn test_parse<'a, T>(value: &'a [u8], expected: T) where T: FromBytes<'a> + fmt::Debug + PartialEq, From d7d517f27808082d5aa4721b1676cddff858d45e Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 17:10:33 +0200 Subject: [PATCH 17/81] Add Method::as_str Returns the method as string. --- http/src/method.rs | 15 ++++++++++----- http/tests/functional/method.rs | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/http/src/method.rs b/http/src/method.rs index df61f9d12..28e12aaad 100644 --- a/http/src/method.rs +++ b/http/src/method.rs @@ -68,12 +68,11 @@ impl Method { pub const fn is_idempotent(self) -> bool { matches!(self, Method::Put | Method::Delete) || self.is_safe() } -} -impl fmt::Display for Method { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + /// Returns the method as string. + pub const fn as_str(self) -> &'static str { use Method::*; - f.write_str(match self { + match self { Options => "OPTIONS", Get => "GET", Post => "POST", @@ -83,7 +82,13 @@ impl fmt::Display for Method { Trace => "TRACE", Connect => "CONNECT", Patch => "PATCH", - }) + } + } +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.as_str()) } } diff --git a/http/tests/functional/method.rs b/http/tests/functional/method.rs index aca97babb..f83346ebd 100644 --- a/http/tests/functional/method.rs +++ b/http/tests/functional/method.rs @@ -40,6 +40,24 @@ fn is_idempotent() { } } +#[test] +fn as_str() { + let tests = &[ + (Get, "GET"), + (Head, "HEAD"), + (Post, "POST"), + (Put, "PUT"), + (Delete, "DELETE"), + (Connect, "CONNECT"), + (Options, "OPTIONS"), + (Trace, "TRACE"), + (Patch, "PATCH"), + ]; + for (method, expected) in tests { + assert_eq!(method.as_str(), *expected); + } +} + #[test] fn from_str() { let tests = &[ From 3226db71b91bbede9f5f7eb7a55ac32f1aed2178 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 17:12:38 +0200 Subject: [PATCH 18/81] Add Version::as_str --- http/src/version.rs | 13 +++++++++---- http/tests/functional/version.rs | 8 ++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/http/src/version.rs b/http/src/version.rs index e9a1f2b62..e2c316dcc 100644 --- a/http/src/version.rs +++ b/http/src/version.rs @@ -33,14 +33,19 @@ impl Version { Version::Http10 | Version::Http11 => Version::Http11, } } + + /// Returns the version as string. + pub const fn as_str(self) -> &'static str { + match self { + Version::Http10 => "HTTP/1.0", + Version::Http11 => "HTTP/1.1", + } + } } impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match self { - Version::Http10 => "HTTP/1.0", - Version::Http11 => "HTTP/1.1", - }) + f.write_str(self.as_str()) } } diff --git a/http/tests/functional/version.rs b/http/tests/functional/version.rs index 40de92b93..8264b86c2 100644 --- a/http/tests/functional/version.rs +++ b/http/tests/functional/version.rs @@ -25,6 +25,14 @@ fn from_str() { } } +#[test] +fn as_str() { + let tests = &[(Http10, "HTTP/1.0"), (Http11, "HTTP/1.1")]; + for (method, expected) in tests { + assert_eq!(method.as_str(), *expected); + } +} + #[test] fn fmt_display() { let tests = &[(Http10, "HTTP/1.0"), (Http11, "HTTP/1.1")]; From 3d1d3fd2a419b8cfdd41c3e77530971d6466f696 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 17:13:40 +0200 Subject: [PATCH 19/81] Add Header::DATE The "Date" header, should be send with responses. --- http/src/header.rs | 1 + http/tests/functional/header.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/http/src/header.rs b/http/src/header.rs index 86e1f8e74..43e3f115d 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -322,6 +322,7 @@ impl HeaderName<'static> { // NOTE: we adding here also add to the // `functional::header::from_str_known_headers` test. known_headers!( + 4: [ (DATE, "date") ], 5: [ (ALLOW, "allow") ], 10: [ (USER_AGENT, "user-agent") ], 12: [ (X_REQUEST_ID, "x-request-id") ], diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index 046136704..1189fd103 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -127,6 +127,7 @@ fn parse_header() { #[test] fn from_str_known_headers() { let known_headers = &[ + "date", "allow", "user-agent", "x-request-id", From b6be5facf589216bd95be8a3f11dc91f20cb04ff Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 17:20:15 +0200 Subject: [PATCH 20/81] Add Version::{major, minor} Returns major/minor version. --- http/src/version.rs | 15 +++++++++++++++ http/tests/functional/version.rs | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/http/src/version.rs b/http/src/version.rs index e2c316dcc..656dcfb17 100644 --- a/http/src/version.rs +++ b/http/src/version.rs @@ -19,6 +19,21 @@ pub enum Version { } impl Version { + /// Returns the major version. + pub const fn major(self) -> u8 { + match self { + Version::Http10 | Version::Http11 => 1, + } + } + + /// Returns the minor version. + pub const fn minor(self) -> u8 { + match self { + Version::Http10 => 0, + Version::Http11 => 1, + } + } + /// Returns the highest minor version with the same major version as `self`. /// /// According to RFC 7230 section 2.6: diff --git a/http/tests/functional/version.rs b/http/tests/functional/version.rs index 8264b86c2..1bdb5ffcf 100644 --- a/http/tests/functional/version.rs +++ b/http/tests/functional/version.rs @@ -8,7 +8,23 @@ fn size() { } #[test] -fn is_idempotent() { +fn major() { + let tests = &[(Http10, 1), (Http11, 1)]; + for (version, expected) in tests { + assert_eq!(version.major(), *expected); + } +} + +#[test] +fn minor() { + let tests = &[(Http10, 0), (Http11, 1)]; + for (version, expected) in tests { + assert_eq!(version.minor(), *expected); + } +} + +#[test] +fn highest_minor() { let tests = &[(Http10, Http11), (Http11, Http11)]; for (version, expected) in tests { assert_eq!(version.highest_minor(), *expected); From 25a70cfc6a227a7d3a89fd6b76dc6801c0ebbf87 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 17:41:16 +0200 Subject: [PATCH 21/81] Add FromBytes implementation for SystemTime This adds a dependency on the httpdate crate. --- http/Cargo.toml | 1 + http/src/from_bytes.rs | 31 +++++++++++++++++++++++++++-- http/tests/functional/from_bytes.rs | 10 ++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/http/Cargo.toml b/http/Cargo.toml index 134a05083..52c4e0d5f 100644 --- a/http/Cargo.toml +++ b/http/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] heph = { version = "0.3.0", path = "../", default-features = false } httparse = { version = "1.4.0", default-features = false } +httpdate = { version = "1.0.0", default-features = false } log = { version = "0.4.8", default-features = false } [dev-dependencies] diff --git a/http/src/from_bytes.rs b/http/src/from_bytes.rs index 9824bd811..3994e9b55 100644 --- a/http/src/from_bytes.rs +++ b/http/src/from_bytes.rs @@ -1,5 +1,8 @@ +use std::time::SystemTime; use std::{fmt, str}; +use httpdate::parse_http_date; + /// Analogous trait to [`FromStr`]. /// /// The main use case for this trait in [`Header::parse`]. Because of this the @@ -60,7 +63,31 @@ int_impl!(u8, u16, u32, u64, usize); impl<'a> FromBytes<'a> for &'a str { type Err = str::Utf8Error; - fn from_bytes(src: &'a [u8]) -> Result { - str::from_utf8(src) + fn from_bytes(value: &'a [u8]) -> Result { + str::from_utf8(value) + } +} + +#[derive(Debug)] +pub struct ParseTimeError; + +impl fmt::Display for ParseTimeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid time") + } +} + +/// Parses the value following RFC7231 section 7.1.1.1. +impl FromBytes<'_> for SystemTime { + type Err = ParseTimeError; + + fn from_bytes(value: &[u8]) -> Result { + match str::from_utf8(value) { + Ok(value) => match parse_http_date(value) { + Ok(time) => Ok(time), + Err(_) => Err(ParseTimeError), + }, + Err(_) => Err(ParseTimeError), + } } } diff --git a/http/tests/functional/from_bytes.rs b/http/tests/functional/from_bytes.rs index 4b23fc264..3dc5e01e8 100644 --- a/http/tests/functional/from_bytes.rs +++ b/http/tests/functional/from_bytes.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::time::SystemTime; use heph_http::FromBytes; @@ -55,6 +56,14 @@ fn invalid_integers() { test_parse_fail::(b"2a"); } +#[test] +fn system_time() { + test_parse(b"Thu, 01 Jan 1970 00:00:00 GMT", SystemTime::UNIX_EPOCH); // IMF-fixdate. + test_parse(b"Thursday, 01-Jan-70 00:00:00 GMT", SystemTime::UNIX_EPOCH); // RFC 850. + test_parse(b"Thu Jan 1 00:00:00 1970", SystemTime::UNIX_EPOCH); // ANSI C’s `asctime`. +} + +#[track_caller] fn test_parse<'a, T>(value: &'a [u8], expected: T) where T: FromBytes<'a> + fmt::Debug + PartialEq, @@ -63,6 +72,7 @@ where assert_eq!(T::from_bytes(value).unwrap(), expected); } +#[track_caller] fn test_parse_fail<'a, T>(value: &'a [u8]) where T: FromBytes<'a> + fmt::Debug + PartialEq, From cfb31812e2e47b3f94921a26bc2e23d92aedf692 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 17:42:04 +0200 Subject: [PATCH 22/81] Automatically add Date header in Connection::respond --- http/src/server.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/http/src/server.rs b/http/src/server.rs index ce9aed0f0..15592b052 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -11,11 +11,13 @@ use std::io::{self, IoSlice, Write}; use std::net::SocketAddr; use std::pin::Pin; use std::task::{self, Poll}; +use std::time::SystemTime; use heph::net::{tcp, TcpServer, TcpStream}; use heph::spawn::{ActorOptions, Spawn}; use heph::{actor, rt, Actor, NewActor, Supervisor}; use httparse::EMPTY_HEADER; +use httpdate::HttpDate; use crate::{FromBytes, HeaderName, Headers, Method, Request, Response, StatusCode, Version}; @@ -488,19 +490,30 @@ impl Connection { // Format the headers (RFC 7230 section 3.2). let mut set_content_length_header = false; + let mut set_date_header = false; for header in response.headers.iter() { + let name = header.name(); // Field-name: // NOTE: spacing after the colon (`:`) is optional. - write!(&mut self.buf, "{}: ", header.name()).unwrap(); + write!(&mut self.buf, "{}: ", name).unwrap(); // Append the header's value. // NOTE: `header.value` shouldn't contain CRLF (`\r\n`). self.buf.extend_from_slice(header.value()); self.buf.extend_from_slice(b"\r\n"); - if *header.name() == HeaderName::CONTENT_LENGTH { + + if name == &HeaderName::CONTENT_LENGTH { set_content_length_header = true; + } else if name == &HeaderName::DATE { + set_date_header = true; } } + // Provide the "Date" header if the user didn't. + if !set_date_header { + let now = HttpDate::from(SystemTime::now()); + write!(&mut self.buf, "Date: {}\r\n", now).unwrap(); + } + // Response body. let body = if let Some(Method::Head) = self.last_method { // RFC 7231 section 4.3.2: @@ -512,7 +525,7 @@ impl Connection { response.body.as_bytes() }; - // Provide the "Conent-Length" if the user didn't. + // Provide the "Conent-Length" header if the user didn't. if !set_content_length_header { write!(&mut self.buf, "Content-Length: {}\r\n", body.len()).unwrap(); } From 4249526b8b32fb18e3a1a26af36cdff3fd91618c Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 19:04:16 +0200 Subject: [PATCH 23/81] Add StatusCode::phrase Returns a static reason phrase, if known. --- http/examples/my_ip.rs | 2 +- http/src/status_code.rs | 75 ++++++++++++++++++++++++- http/tests/functional/status_code.rs | 83 ++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index bfdb87fcb..351d3c331 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -80,7 +80,7 @@ async fn http_actor( loop { match connection.next_request().await? { Ok(Some(mut request)) => { - debug!("received request: {:?}", request); + info!("received request: {:?}", request); let mut headers = Headers::EMPTY; let (code, body) = if request.path() != "/" { request.body_mut().ignore()?; diff --git a/http/src/status_code.rs b/http/src/status_code.rs index e46cccbf6..701932414 100644 --- a/http/src/status_code.rs +++ b/http/src/status_code.rs @@ -68,7 +68,7 @@ impl StatusCode { /// IM Used. /// /// RFC 3229. - pub const IM_USED: StatusCode = StatusCode(208); + pub const IM_USED: StatusCode = StatusCode(226); // 3xx range. /// Multiple Choices. @@ -294,6 +294,79 @@ impl StatusCode { pub const fn is_server_error(self) -> bool { self.0 >= 500 && self.0 <= 599 } + + /// Returns the reason phrase for well known status codes. + pub const fn phrase(self) -> Option<&'static str> { + match self.0 { + 100 => Some("Continue"), + 101 => Some("Switching Protocols"), + 103 => Some("Processing"), + 104 => Some("Early Hints"), + + 200 => Some("OK"), + 201 => Some("Created"), + 202 => Some("Accepted"), + 203 => Some("Non-Authoritative Information"), + 204 => Some("No Content"), + 205 => Some("Reset Content"), + 206 => Some("Partial Content"), + 207 => Some("Multi-Status"), + 208 => Some("Already Reported"), + 226 => Some("IM Used"), + + 300 => Some("Multiple Choices"), + 301 => Some("Moved Permanently"), + 302 => Some("Found"), + 303 => Some("See Other"), + 304 => Some("Not Modified"), + 305 => Some("Use Proxy"), + 307 => Some("Temporary Redirect"), + 308 => Some("Permanent Redirect"), + + 400 => Some("Bad Request"), + 401 => Some("Unauthorized"), + 402 => Some("Payment Required"), + 403 => Some("Forbidden"), + 404 => Some("Not Found"), + 405 => Some("Method Not Allowed"), + 406 => Some("Not Acceptable"), + 407 => Some("Proxy Authentication Required"), + 408 => Some("Request Timeout"), + 409 => Some("Conflict"), + 410 => Some("Gone"), + 411 => Some("Length Required"), + 412 => Some("Precondition Failed"), + 413 => Some("Payload Too Large"), + 414 => Some("URI Too Long"), + 415 => Some("Unsupported Media Type"), + 416 => Some("Range Not Satisfiable"), + 417 => Some("Expectation Failed"), + 421 => Some("Misdirected Request"), + 422 => Some("Unprocessable Entity"), + 423 => Some("Locked"), + 424 => Some("Failed Dependency"), + 425 => Some("Too Early"), + 426 => Some("Upgrade Required"), + 428 => Some("Precondition Required"), + 429 => Some("Too Many Requests"), + 431 => Some("Request Header Fields Too Large"), + 451 => Some("Unavailable For Legal Reasons"), + + 500 => Some("Internal Server Error"), + 501 => Some("Not Implemented"), + 502 => Some("Bad Gateway"), + 503 => Some("Service Unavailable"), + 504 => Some("Gateway Timeout"), + 505 => Some("HTTP Version Not Supported"), + 506 => Some("Variant Also Negotiates"), + 507 => Some("Insufficient Storage"), + 508 => Some("Loop Detected"), + 510 => Some("Not Extended"), + 511 => Some("Network Authentication Required"), + + _ => None, + } + } } impl fmt::Display for StatusCode { diff --git a/http/tests/functional/status_code.rs b/http/tests/functional/status_code.rs index cd9cfdd69..c72a9a9d2 100644 --- a/http/tests/functional/status_code.rs +++ b/http/tests/functional/status_code.rs @@ -59,3 +59,86 @@ fn is_server_error() { assert!(!StatusCode(*status).is_server_error()); } } + +#[test] +fn phrase() { + #[rustfmt::skip] + let tests = &[ + // 1xx range. + (StatusCode::CONTINUE, "Continue"), + (StatusCode::SWITCHING_PROTOCOLS, "Switching Protocols"), + (StatusCode::PROCESSING, "Processing"), + (StatusCode::EARLY_HINTS, "Early Hints"), + + // 2xx range. + (StatusCode::OK, "OK"), + (StatusCode::CREATED, "Created"), + (StatusCode::ACCEPTED, "Accepted"), + (StatusCode::NON_AUTHORITATIVE_INFORMATION, "Non-Authoritative Information"), + (StatusCode::NO_CONTENT, "No Content"), + (StatusCode::RESET_CONTENT, "Reset Content"), + (StatusCode::PARTIAL_CONTENT, "Partial Content"), + (StatusCode::MULTI_STATUS, "Multi-Status"), + (StatusCode::ALREADY_REPORTED, "Already Reported"), + (StatusCode::IM_USED, "IM Used"), + + // 3xx range. + (StatusCode::MULTIPLE_CHOICES, "Multiple Choices"), + (StatusCode::MOVED_PERMANENTLY, "Moved Permanently"), + (StatusCode::FOUND, "Found"), + (StatusCode::SEE_OTHER, "See Other"), + (StatusCode::NOT_MODIFIED, "Not Modified"), + (StatusCode::USE_PROXY, "Use Proxy"), + (StatusCode::TEMPORARY_REDIRECT, "Temporary Redirect"), + (StatusCode::PERMANENT_REDIRECT, "Permanent Redirect"), + + // 4xx range. + (StatusCode::BAD_REQUEST, "Bad Request"), + (StatusCode::UNAUTHORIZED, "Unauthorized"), + (StatusCode::PAYMENT_REQUIRED, "Payment Required"), + (StatusCode::FORBIDDEN, "Forbidden"), + (StatusCode::NOT_FOUND, "Not Found"), + (StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed"), + (StatusCode::NOT_ACCEPTABLE, "Not Acceptable"), + (StatusCode::PROXY_AUTHENTICATION_REQUIRED, "Proxy Authentication Required"), + (StatusCode::REQUEST_TIMEOUT, "Request Timeout"), + (StatusCode::CONFLICT, "Conflict"), + (StatusCode::GONE, "Gone"), + (StatusCode::LENGTH_REQUIRED, "Length Required"), + (StatusCode::PRECONDITION_FAILED, "Precondition Failed"), + (StatusCode::PAYLOAD_TOO_LARGE, "Payload Too Large"), + (StatusCode::URI_TOO_LONG, "URI Too Long"), + (StatusCode::UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type"), + (StatusCode::RANGE_NOT_SATISFIABLE, "Range Not Satisfiable"), + (StatusCode::EXPECTATION_FAILED, "Expectation Failed"), + (StatusCode::MISDIRECTED_REQUEST, "Misdirected Request"), + (StatusCode::UNPROCESSABLE_ENTITY, "Unprocessable Entity"), + (StatusCode::LOCKED, "Locked"), + (StatusCode::FAILED_DEPENDENCY, "Failed Dependency"), + (StatusCode::TOO_EARLY, "Too Early"), + (StatusCode::UPGRADE_REQUIRED, "Upgrade Required"), + (StatusCode::PRECONDITION_REQUIRED, "Precondition Required"), + (StatusCode::TOO_MANY_REQUESTS, "Too Many Requests"), + (StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, "Request Header Fields Too Large"), + (StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS, "Unavailable For Legal Reasons"), + + // 5xx range. + (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"), + (StatusCode::NOT_IMPLEMENTED, "Not Implemented"), + (StatusCode::BAD_GATEWAY, "Bad Gateway"), + (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable"), + (StatusCode::GATEWAY_TIMEOUT, "Gateway Timeout"), + ( StatusCode::HTTP_VERSION_NOT_SUPPORTED, "HTTP Version Not Supported"), + ( StatusCode::VARIANT_ALSO_NEGOTIATES, "Variant Also Negotiates"), + (StatusCode::INSUFFICIENT_STORAGE, "Insufficient Storage"), + (StatusCode::LOOP_DETECTED, "Loop Detected"), + (StatusCode::NOT_EXTENDED, "Not Extended"), + ( StatusCode::NETWORK_AUTHENTICATION_REQUIRED, "Network Authentication Required"), + ]; + for (input, expected) in tests { + assert_eq!(input.phrase().unwrap(), *expected); + } + + assert!(StatusCode(0).phrase().is_none()); + assert!(StatusCode(999).phrase().is_none()); +} From f108912d6e5cb5a3a43d5184f007bf9055041341 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 19:10:18 +0200 Subject: [PATCH 24/81] Expand API for Response --- http/src/response.rs | 40 ++++++++++++++++++++++++++++++++++++---- http/src/server.rs | 7 ++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/http/src/response.rs b/http/src/response.rs index 0d5830206..569b2dc2c 100644 --- a/http/src/response.rs +++ b/http/src/response.rs @@ -2,14 +2,16 @@ use std::fmt; use crate::{Headers, StatusCode, Version}; +/// HTTP response. pub struct Response { - pub(crate) version: Version, - pub(crate) status: StatusCode, - pub(crate) headers: Headers, - pub(crate) body: B, + version: Version, + status: StatusCode, + headers: Headers, + body: B, } impl Response { + /// Create a new HTTP response. pub const fn new( version: Version, status: StatusCode, @@ -23,6 +25,36 @@ impl Response { body, } } + + /// Returns the HTTP version of this response. + pub const fn version(&self) -> Version { + self.version + } + + /// Returns the response code. + pub const fn status(&self) -> StatusCode { + self.status + } + + /// Returns the headers. + pub const fn headers(&self) -> &Headers { + &self.headers + } + + /// Returns mutable access to the headers. + pub fn headers_mut(&mut self) -> &mut Headers { + &mut self.headers + } + + /// The response body. + pub const fn body(&self) -> &B { + &self.body + } + + /// Mutable access to the response body. + pub fn body_mut(&mut self) -> &mut B { + &mut self.body + } } impl fmt::Debug for Response { diff --git a/http/src/server.rs b/http/src/server.rs index 15592b052..1b3e1ac93 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -484,14 +484,15 @@ impl Connection { write!( &mut self.buf, "{} {} \r\n", - response.version, response.status + response.version(), + response.status() ) .unwrap(); // Format the headers (RFC 7230 section 3.2). let mut set_content_length_header = false; let mut set_date_header = false; - for header in response.headers.iter() { + for header in response.headers().iter() { let name = header.name(); // Field-name: // NOTE: spacing after the colon (`:`) is optional. @@ -522,7 +523,7 @@ impl Connection { // > terminates at the end of the header section). &[] } else { - response.body.as_bytes() + response.body().as_bytes() }; // Provide the "Conent-Length" header if the user didn't. From eab0fbef7c08b407c0ead526d226446a2c791476 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 19:10:54 +0200 Subject: [PATCH 25/81] Add connection::Body::is_empty --- http/src/server.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/http/src/server.rs b/http/src/server.rs index 1b3e1ac93..3bf6f29eb 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -602,20 +602,23 @@ impl<'a> Body<'a> { /// The returned value is based on the "Content-Length" header, or 0 if not /// present. pub fn len(&self) -> usize { - // TODO: chunked encoding. self.size } /// Returns the number of bytes left in the body. - /// - /// See [`Body::len`]. pub fn left(&self) -> usize { self.left } + /// Returns `true` if the body is completely read (or was empty to begin + /// with). + pub fn is_empty(&self) -> bool { + self.left == 0 + } + /// Ignore the body, but removes it from the connection. pub fn ignore(&mut self) -> io::Result<()> { - if self.size == 0 { + if self.is_empty() { // Empty body, then we're done quickly. return Ok(()); } From 381d752a96123c5f40afb5192c14801938e38bae Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 19:15:40 +0200 Subject: [PATCH 26/81] Add Request::new --- http/src/request.rs | 49 +++++++++++++++++++++++++++++++++------------ http/src/server.rs | 8 +------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/http/src/request.rs b/http/src/request.rs index 05a63437d..02bfece5f 100644 --- a/http/src/request.rs +++ b/http/src/request.rs @@ -1,18 +1,34 @@ -// TODO: see if we can have a borrowed version of `Request`. - use std::fmt; use crate::{Headers, Method, Version}; +/// HTTP request. pub struct Request { - pub(crate) method: Method, - pub(crate) path: String, - pub(crate) version: Version, - pub(crate) headers: Headers, - pub(crate) body: B, + method: Method, + path: String, + version: Version, + headers: Headers, + body: B, } impl Request { + /// Create a new request. + pub const fn new( + method: Method, + path: String, + version: Version, + headers: Headers, + body: B, + ) -> Request { + Request { + method, + version, + path, + headers, + body, + } + } + /// Returns the HTTP version of this request. /// /// # Notes @@ -23,32 +39,39 @@ impl Request { /// understands) per RFC 7230 section 2.6. /// /// [`HttpServer`]: crate::HttpServer - pub fn version(&self) -> Version { + pub const fn version(&self) -> Version { self.version } /// Returns the HTTP method of this request. - pub fn method(&self) -> Method { + pub const fn method(&self) -> Method { self.method } + /// Returns the path of this request. pub fn path(&self) -> &str { &self.path } - pub fn headers(&self) -> &Headers { + /// Returns the headers. + pub const fn headers(&self) -> &Headers { &self.headers } - pub fn body(&self) -> &B { + /// Returns mutable access to the headers. + pub fn headers_mut(&mut self) -> &mut Headers { + &mut self.headers + } + + /// The request body. + pub const fn body(&self) -> &B { &self.body } + /// Mutable access to the request body. pub fn body_mut(&mut self) -> &mut B { &mut self.body } - - // TODO: maybe `fn split_body(self) -> (Request<()>, B)`? } impl fmt::Debug for Request { diff --git a/http/src/server.rs b/http/src/server.rs index 3bf6f29eb..7b2fa6b35 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -375,13 +375,7 @@ impl Connection { size, left: size, }; - return Ok(Ok(Some(Request { - method, - version, - path, - headers, - body, - }))); + return Ok(Ok(Some(Request::new(method, path, version, headers, body)))); } Ok(httparse::Status::Partial) => { // Buffer doesn't include the entire request header, try From c74ebd7762af8e704d6b518380a85d2a74f295cc Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 30 Apr 2021 19:16:07 +0200 Subject: [PATCH 27/81] Implement Body for Cow<[u8]> --- http/src/body.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/http/src/body.rs b/http/src/body.rs index 254ca27d3..da94f71d9 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -17,6 +17,12 @@ impl Body for Vec { } } +impl Body for Cow<'_, [u8]> { + fn as_bytes(&self) -> &[u8] { + self.as_ref() + } +} + impl Body for str { fn as_bytes(&self) -> &[u8] { self.as_bytes() From 59e931932ff1347f8da1a595ac627402353db2ec Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 1 May 2021 15:51:25 +0200 Subject: [PATCH 28/81] Expand the list of known HTTP headers This know includes all headers currently registered at https://www.iana.org/assignments/message-headers/message-headers.xhtml. --- http/src/header.rs | 502 +++++++++++++++++++++++++++++++- http/src/lib.rs | 2 +- http/src/parse_headers.bash | 90 ++++++ http/tests/functional/header.rs | 220 +++++++++++++- 4 files changed, 800 insertions(+), 14 deletions(-) create mode 100755 http/src/parse_headers.bash diff --git a/http/src/header.rs b/http/src/header.rs index 43e3f115d..d5ea59b9b 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -12,6 +12,9 @@ use std::iter::FusedIterator; use crate::{cmp_lower_case, is_lower_case, FromBytes}; /// List of headers. +/// +/// A complete list can be found at the "Message Headers" registry: +/// pub struct Headers { /// All values appended in a single allocation. values: Vec, @@ -283,10 +286,11 @@ pub struct HeaderName<'a> { macro_rules! known_headers { ($( $length: tt: [ - $( ( $const_name: ident, $http_name: expr ) $(,)* ),+ + $( $(#[$meta: meta])* ( $const_name: ident, $http_name: expr ) $(,)* ),+ ], )+) => { $($( + $( #[$meta] )* pub const $const_name: HeaderName<'static> = HeaderName::from_lowercase($http_name); )+)+ @@ -319,15 +323,499 @@ macro_rules! known_headers { } impl HeaderName<'static> { + // NOTE: these are automatically generated by the `parse_headers.bash` + // script. // NOTE: we adding here also add to the // `functional::header::from_str_known_headers` test. known_headers!( - 4: [ (DATE, "date") ], - 5: [ (ALLOW, "allow") ], - 10: [ (USER_AGENT, "user-agent") ], - 12: [ (X_REQUEST_ID, "x-request-id") ], - 14: [ (CONTENT_LENGTH, "content-length") ], - 17: [ (TRANSFER_ENCODING, "transfer-encoding") ], + 2: [ + #[doc = "IM.\n\nRFC 4229."] + (IM, "im"), + #[doc = "If.\n\nRFC 4918."] + (IF, "if"), + #[doc = "TE.\n\nRFC 7230 section 4.3."] + (TE, "te"), + ], + 3: [ + #[doc = "Age.\n\nRFC 7234 section 5.1."] + (AGE, "age"), + #[doc = "DAV.\n\nRFC 4918."] + (DAV, "dav"), + #[doc = "Ext.\n\nRFC 4229."] + (EXT, "ext"), + #[doc = "Man.\n\nRFC 4229."] + (MAN, "man"), + #[doc = "Opt.\n\nRFC 4229."] + (OPT, "opt"), + #[doc = "P3P.\n\nRFC 4229."] + (P3P, "p3p"), + #[doc = "PEP.\n\nRFC 4229."] + (PEP, "pep"), + #[doc = "TCN.\n\nRFC 4229."] + (TCN, "tcn"), + #[doc = "TTL.\n\nRFC 8030 section 5.2."] + (TTL, "ttl"), + #[doc = "URI.\n\nRFC 4229."] + (URI, "uri"), + #[doc = "Via.\n\nRFC 7230 section 5.7.1."] + (VIA, "via"), + ], + 4: [ + #[doc = "A-IM.\n\nRFC 4229."] + (A_IM, "a-im"), + #[doc = "ALPN.\n\nRFC 7639 section 2."] + (ALPN, "alpn"), + #[doc = "DASL.\n\nRFC 5323."] + (DASL, "dasl"), + #[doc = "Date.\n\nRFC 7231 section 7.1.1.2."] + (DATE, "date"), + #[doc = "ETag.\n\nRFC 7232 section 2.3."] + (ETAG, "etag"), + #[doc = "From.\n\nRFC 7231 section 5.5.1."] + (FROM, "from"), + #[doc = "Host.\n\nRFC 7230 section 5.4."] + (HOST, "host"), + #[doc = "Link.\n\nRFC 8288."] + (LINK, "link"), + #[doc = "Safe.\n\nRFC 4229."] + (SAFE, "safe"), + #[doc = "SLUG.\n\nRFC 5023."] + (SLUG, "slug"), + #[doc = "Vary.\n\nRFC 7231 section 7.1.4."] + (VARY, "vary"), + #[doc = "Cost.\n\nRFC 4229."] + (COST, "cost"), + ], + 5: [ + #[doc = "Allow.\n\nRFC 7231 section 7.4.1."] + (ALLOW, "allow"), + #[doc = "C-Ext.\n\nRFC 4229."] + (C_EXT, "c-ext"), + #[doc = "C-Man.\n\nRFC 4229."] + (C_MAN, "c-man"), + #[doc = "C-Opt.\n\nRFC 4229."] + (C_OPT, "c-opt"), + #[doc = "C-PEP.\n\nRFC 4229."] + (C_PEP, "c-pep"), + #[doc = "Close.\n\nRFC 7230 section 8.1."] + (CLOSE, "close"), + #[doc = "Depth.\n\nRFC 4918."] + (DEPTH, "depth"), + #[doc = "Label.\n\nRFC 4229."] + (LABEL, "label"), + #[doc = "Meter.\n\nRFC 4229."] + (METER, "meter"), + #[doc = "Range.\n\nRFC 7233 section 3.1."] + (RANGE, "range"), + #[doc = "Topic.\n\nRFC 8030 section 5.4."] + (TOPIC, "topic"), + #[doc = "SubOK.\n\nRFC 4229."] + (SUBOK, "subok"), + #[doc = "Subst.\n\nRFC 4229."] + (SUBST, "subst"), + #[doc = "Title.\n\nRFC 4229."] + (TITLE, "title"), + ], + 6: [ + #[doc = "Accept.\n\nRFC 7231 section 5.3.2."] + (ACCEPT, "accept"), + #[doc = "Cookie.\n\nRFC 6265."] + (COOKIE, "cookie"), + #[doc = "Digest.\n\nRFC 4229."] + (DIGEST, "digest"), + #[doc = "Expect.\n\nRFC 7231 section 5.1.1."] + (EXPECT, "expect"), + #[doc = "Origin.\n\nRFC 6454."] + (ORIGIN, "origin"), + #[doc = "OSCORE.\n\nRFC 8613 section 11.1."] + (OSCORE, "oscore"), + #[doc = "Pragma.\n\nRFC 7234 section 5.4."] + (PRAGMA, "pragma"), + #[doc = "Prefer.\n\nRFC 7240."] + (PREFER, "prefer"), + #[doc = "Public.\n\nRFC 4229."] + (PUBLIC, "public"), + #[doc = "Server.\n\nRFC 7231 section 7.4.2."] + (SERVER, "server"), + #[doc = "Sunset.\n\nRFC 8594."] + (SUNSET, "sunset"), + ], + 7: [ + #[doc = "Alt-Svc.\n\nRFC 7838."] + (ALT_SVC, "alt-svc"), + #[doc = "Cookie2.\n\nRFC 2965, RFC 6265."] + (COOKIE2, "cookie2"), + #[doc = "Expires.\n\nRFC 7234 section 5.3."] + (EXPIRES, "expires"), + #[doc = "Hobareg.\n\nRFC 7486 section 6.1.1."] + (HOBAREG, "hobareg"), + #[doc = "Referer.\n\nRFC 7231 section 5.5.2."] + (REFERER, "referer"), + #[doc = "Timeout.\n\nRFC 4918."] + (TIMEOUT, "timeout"), + #[doc = "Trailer.\n\nRFC 7230 section 4.4."] + (TRAILER, "trailer"), + #[doc = "Urgency.\n\nRFC 8030 section 5.3."] + (URGENCY, "urgency"), + #[doc = "Upgrade.\n\nRFC 7230 section 6.7."] + (UPGRADE, "upgrade"), + #[doc = "Warning.\n\nRFC 7234 section 5.5."] + (WARNING, "warning"), + #[doc = "Version.\n\nRFC 4229."] + (VERSION, "version"), + ], + 8: [ + #[doc = "Alt-Used.\n\nRFC 7838."] + (ALT_USED, "alt-used"), + #[doc = "CDN-Loop.\n\nRFC 8586."] + (CDN_LOOP, "cdn-loop"), + #[doc = "If-Match.\n\nRFC 7232 section 3.1."] + (IF_MATCH, "if-match"), + #[doc = "If-Range.\n\nRFC 7233 section 3.2."] + (IF_RANGE, "if-range"), + #[doc = "Location.\n\nRFC 7231 section 7.1.2."] + (LOCATION, "location"), + #[doc = "Pep-Info.\n\nRFC 4229."] + (PEP_INFO, "pep-info"), + #[doc = "Position.\n\nRFC 4229."] + (POSITION, "position"), + #[doc = "Protocol.\n\nRFC 4229."] + (PROTOCOL, "protocol"), + #[doc = "Optional.\n\nRFC 4229."] + (OPTIONAL, "optional"), + #[doc = "UA-Color.\n\nRFC 4229."] + (UA_COLOR, "ua-color"), + #[doc = "UA-Media.\n\nRFC 4229."] + (UA_MEDIA, "ua-media"), + ], + 9: [ + #[doc = "Accept-CH.\n\nRFC 8942 section 3.1."] + (ACCEPT_CH, "accept-ch"), + #[doc = "Expect-CT.\n\nRFC -ietf-httpbis-expect-ct-08."] + (EXPECT_CT, "expect-ct"), + #[doc = "Forwarded.\n\nRFC 7239."] + (FORWARDED, "forwarded"), + #[doc = "Negotiate.\n\nRFC 4229."] + (NEGOTIATE, "negotiate"), + #[doc = "Overwrite.\n\nRFC 4918."] + (OVERWRITE, "overwrite"), + #[doc = "Isolation.\n\nOData Version 4.01 Part 1: Protocol, OASIS, Chet_Ensign."] + (ISOLATION, "isolation"), + #[doc = "UA-Pixels.\n\nRFC 4229."] + (UA_PIXELS, "ua-pixels"), + ], + 10: [ + #[doc = "Alternates.\n\nRFC 4229."] + (ALTERNATES, "alternates"), + #[doc = "C-PEP-Info.\n\nRFC 4229."] + (C_PEP_INFO, "c-pep-info"), + #[doc = "Connection.\n\nRFC 7230 section 6.1."] + (CONNECTION, "connection"), + #[doc = "Content-ID.\n\nRFC 4229."] + (CONTENT_ID, "content-id"), + #[doc = "Delta-Base.\n\nRFC 4229."] + (DELTA_BASE, "delta-base"), + #[doc = "Early-Data.\n\nRFC 8470."] + (EARLY_DATA, "early-data"), + #[doc = "GetProfile.\n\nRFC 4229."] + (GETPROFILE, "getprofile"), + #[doc = "Keep-Alive.\n\nRFC 4229."] + (KEEP_ALIVE, "keep-alive"), + #[doc = "Lock-Token.\n\nRFC 4918."] + (LOCK_TOKEN, "lock-token"), + #[doc = "PICS-Label.\n\nRFC 4229."] + (PICS_LABEL, "pics-label"), + #[doc = "Set-Cookie.\n\nRFC 6265."] + (SET_COOKIE, "set-cookie"), + #[doc = "SetProfile.\n\nRFC 4229."] + (SETPROFILE, "setprofile"), + #[doc = "SoapAction.\n\nRFC 4229."] + (SOAPACTION, "soapaction"), + #[doc = "Status-URI.\n\nRFC 4229."] + (STATUS_URI, "status-uri"), + #[doc = "User-Agent.\n\nRFC 7231 section 5.5.3."] + (USER_AGENT, "user-agent"), + #[doc = "Compliance.\n\nRFC 4229."] + (COMPLIANCE, "compliance"), + #[doc = "Message-ID.\n\nRFC 4229."] + (MESSAGE_ID, "message-id"), + #[doc = "Tracestate.\n\n."] + (TRACESTATE, "tracestate"), + ], + 11: [ + #[doc = "Accept-Post.\n\n."] + (ACCEPT_POST, "accept-post"), + #[doc = "Content-MD5.\n\nRFC 4229."] + (CONTENT_MD5, "content-md5"), + #[doc = "Destination.\n\nRFC 4918."] + (DESTINATION, "destination"), + #[doc = "Retry-After.\n\nRFC 7231 section 7.1.3."] + (RETRY_AFTER, "retry-after"), + #[doc = "Set-Cookie2.\n\nRFC 2965, RFC 6265."] + (SET_COOKIE2, "set-cookie2"), + #[doc = "Want-Digest.\n\nRFC 4229."] + (WANT_DIGEST, "want-digest"), + #[doc = "Traceparent.\n\n."] + (TRACEPARENT, "traceparent"), + ], + 12: [ + #[doc = "Accept-Patch.\n\nRFC 5789."] + (ACCEPT_PATCH, "accept-patch"), + #[doc = "Content-Base.\n\nRFC 2068, RFC 2616."] + (CONTENT_BASE, "content-base"), + #[doc = "Content-Type.\n\nRFC 7231 section 3.1.1.5."] + (CONTENT_TYPE, "content-type"), + #[doc = "Derived-From.\n\nRFC 4229."] + (DERIVED_FROM, "derived-from"), + #[doc = "Max-Forwards.\n\nRFC 7231 section 5.1.2."] + (MAX_FORWARDS, "max-forwards"), + #[doc = "MIME-Version.\n\nRFC 7231, Appendix A.1."] + (MIME_VERSION, "mime-version"), + #[doc = "Redirect-Ref.\n\nRFC 4437."] + (REDIRECT_REF, "redirect-ref"), + #[doc = "Replay-Nonce.\n\nRFC 8555 section 6.5.1."] + (REPLAY_NONCE, "replay-nonce"), + #[doc = "Schedule-Tag.\n\nRFC 6638."] + (SCHEDULE_TAG, "schedule-tag"), + #[doc = "Variant-Vary.\n\nRFC 4229."] + (VARIANT_VARY, "variant-vary"), + #[doc = "Method-Check.\n\nW3C Web Application Formats Working Group."] + (METHOD_CHECK, "method-check"), + #[doc = "Referer-Root.\n\nW3C Web Application Formats Working Group."] + (REFERER_ROOT, "referer-root"), + #[doc = "X-Request-ID."] + (X_REQUEST_ID, "x-request-id"), + ], + 13: [ + #[doc = "Accept-Ranges.\n\nRFC 7233 section 2.3."] + (ACCEPT_RANGES, "accept-ranges"), + #[doc = "Authorization.\n\nRFC 7235 section 4.2."] + (AUTHORIZATION, "authorization"), + #[doc = "Cache-Control.\n\nRFC 7234 section 5.2."] + (CACHE_CONTROL, "cache-control"), + #[doc = "Content-Range.\n\nRFC 7233 section 4.2."] + (CONTENT_RANGE, "content-range"), + #[doc = "Default-Style.\n\nRFC 4229."] + (DEFAULT_STYLE, "default-style"), + #[doc = "If-None-Match.\n\nRFC 7232 section 3.2."] + (IF_NONE_MATCH, "if-none-match"), + #[doc = "Last-Modified.\n\nRFC 7232 section 2.2."] + (LAST_MODIFIED, "last-modified"), + #[doc = "OData-Version.\n\nOData Version 4.01 Part 1: Protocol, OASIS, Chet_Ensign."] + (ODATA_VERSION, "odata-version"), + #[doc = "Ordering-Type.\n\nRFC 4229."] + (ORDERING_TYPE, "ordering-type"), + #[doc = "ProfileObject.\n\nRFC 4229."] + (PROFILEOBJECT, "profileobject"), + #[doc = "Protocol-Info.\n\nRFC 4229."] + (PROTOCOL_INFO, "protocol-info"), + #[doc = "UA-Resolution.\n\nRFC 4229."] + (UA_RESOLUTION, "ua-resolution"), + ], + 14: [ + #[doc = "Accept-Charset.\n\nRFC 7231 section 5.3.3."] + (ACCEPT_CHARSET, "accept-charset"), + #[doc = "Cal-Managed-ID.\n\nRFC 8607 section 5.1."] + (CAL_MANAGED_ID, "cal-managed-id"), + #[doc = "Cert-Not-After.\n\nRFC 8739 section 3.3."] + (CERT_NOT_AFTER, "cert-not-after"), + #[doc = "Content-Length.\n\nRFC 7230 section 3.3.2."] + (CONTENT_LENGTH, "content-length"), + #[doc = "HTTP2-Settings.\n\nRFC 7540 section 3.2.1."] + (HTTP2_SETTINGS, "http2-settings"), + #[doc = "OData-EntityId.\n\nOData Version 4.01 Part 1: Protocol, OASIS, Chet_Ensign."] + (ODATA_ENTITYID, "odata-entityid"), + #[doc = "Protocol-Query.\n\nRFC 4229."] + (PROTOCOL_QUERY, "protocol-query"), + #[doc = "Proxy-Features.\n\nRFC 4229."] + (PROXY_FEATURES, "proxy-features"), + #[doc = "Schedule-Reply.\n\nRFC 6638."] + (SCHEDULE_REPLY, "schedule-reply"), + #[doc = "Access-Control.\n\nW3C Web Application Formats Working Group."] + (ACCESS_CONTROL, "access-control"), + #[doc = "Non-Compliance.\n\nRFC 4229."] + (NON_COMPLIANCE, "non-compliance"), + ], + 15: [ + #[doc = "Accept-Datetime.\n\nRFC 7089."] + (ACCEPT_DATETIME, "accept-datetime"), + #[doc = "Accept-Encoding.\n\nRFC 7231 section 5.3.4, RFC 7694 section 3."] + (ACCEPT_ENCODING, "accept-encoding"), + #[doc = "Accept-Features.\n\nRFC 4229."] + (ACCEPT_FEATURES, "accept-features"), + #[doc = "Accept-Language.\n\nRFC 7231 section 5.3.5."] + (ACCEPT_LANGUAGE, "accept-language"), + #[doc = "Cert-Not-Before.\n\nRFC 8739 section 3.3."] + (CERT_NOT_BEFORE, "cert-not-before"), + #[doc = "Content-Version.\n\nRFC 4229."] + (CONTENT_VERSION, "content-version"), + #[doc = "Differential-ID.\n\nRFC 4229."] + (DIFFERENTIAL_ID, "differential-id"), + #[doc = "OData-Isolation.\n\nOData Version 4.01 Part 1: Protocol, OASIS, Chet_Ensign."] + (ODATA_ISOLATION, "odata-isolation"), + #[doc = "Public-Key-Pins.\n\nRFC 7469."] + (PUBLIC_KEY_PINS, "public-key-pins"), + #[doc = "Security-Scheme.\n\nRFC 4229."] + (SECURITY_SCHEME, "security-scheme"), + #[doc = "X-Frame-Options.\n\nRFC 7034."] + (X_FRAME_OPTIONS, "x-frame-options"), + #[doc = "EDIINT-Features.\n\nRFC 6017."] + (EDIINT_FEATURES, "ediint-features"), + #[doc = "Resolution-Hint.\n\nRFC 4229."] + (RESOLUTION_HINT, "resolution-hint"), + #[doc = "UA-Windowpixels.\n\nRFC 4229."] + (UA_WINDOWPIXELS, "ua-windowpixels"), + #[doc = "X-Device-Accept.\n\nW3C Mobile Web Best Practices Working Group."] + (X_DEVICE_ACCEPT, "x-device-accept"), + ], + 16: [ + #[doc = "Accept-Additions.\n\nRFC 4229."] + (ACCEPT_ADDITIONS, "accept-additions"), + #[doc = "CalDAV-Timezones.\n\nRFC 7809 section 7.1."] + (CALDAV_TIMEZONES, "caldav-timezones"), + #[doc = "Content-Encoding.\n\nRFC 7231 section 3.1.2.2."] + (CONTENT_ENCODING, "content-encoding"), + #[doc = "Content-Language.\n\nRFC 7231 section 3.1.3.2."] + (CONTENT_LANGUAGE, "content-language"), + #[doc = "Content-Location.\n\nRFC 7231 section 3.1.4.2."] + (CONTENT_LOCATION, "content-location"), + #[doc = "Memento-Datetime.\n\nRFC 7089."] + (MEMENTO_DATETIME, "memento-datetime"), + #[doc = "OData-MaxVersion.\n\nOData Version 4.01 Part 1: Protocol, OASIS, Chet_Ensign."] + (ODATA_MAXVERSION, "odata-maxversion"), + #[doc = "Protocol-Request.\n\nRFC 4229."] + (PROTOCOL_REQUEST, "protocol-request"), + #[doc = "WWW-Authenticate.\n\nRFC 7235 section 4.1."] + (WWW_AUTHENTICATE, "www-authenticate"), + ], + 17: [ + #[doc = "If-Modified-Since.\n\nRFC 7232 section 3.3."] + (IF_MODIFIED_SINCE, "if-modified-since"), + #[doc = "Proxy-Instruction.\n\nRFC 4229."] + (PROXY_INSTRUCTION, "proxy-instruction"), + #[doc = "Sec-Token-Binding.\n\nRFC 8473."] + (SEC_TOKEN_BINDING, "sec-token-binding"), + #[doc = "Sec-WebSocket-Key.\n\nRFC 6455."] + (SEC_WEBSOCKET_KEY, "sec-websocket-key"), + #[doc = "Surrogate-Control.\n\nRFC 4229."] + (SURROGATE_CONTROL, "surrogate-control"), + #[doc = "Transfer-Encoding.\n\nRFC 7230 section 3.3.1."] + (TRANSFER_ENCODING, "transfer-encoding"), + #[doc = "OSLC-Core-Version.\n\nOASIS Project Specification 01, OASIS, Chet_Ensign."] + (OSLC_CORE_VERSION, "oslc-core-version"), + #[doc = "Resolver-Location.\n\nRFC 4229."] + (RESOLVER_LOCATION, "resolver-location"), + ], + 18: [ + #[doc = "Content-Style-Type.\n\nRFC 4229."] + (CONTENT_STYLE_TYPE, "content-style-type"), + #[doc = "Preference-Applied.\n\nRFC 7240."] + (PREFERENCE_APPLIED, "preference-applied"), + #[doc = "Proxy-Authenticate.\n\nRFC 7235 section 4.3."] + (PROXY_AUTHENTICATE, "proxy-authenticate"), + ], + 19: [ + #[doc = "Authentication-Info.\n\nRFC 7615 section 3."] + (AUTHENTICATION_INFO, "authentication-info"), + #[doc = "Content-Disposition.\n\nRFC 6266."] + (CONTENT_DISPOSITION, "content-disposition"), + #[doc = "Content-Script-Type.\n\nRFC 4229."] + (CONTENT_SCRIPT_TYPE, "content-script-type"), + #[doc = "If-Unmodified-Since.\n\nRFC 7232 section 3.4."] + (IF_UNMODIFIED_SINCE, "if-unmodified-since"), + #[doc = "Proxy-Authorization.\n\nRFC 7235 section 4.4."] + (PROXY_AUTHORIZATION, "proxy-authorization"), + #[doc = "AMP-Cache-Transform.\n\n."] + (AMP_CACHE_TRANSFORM, "amp-cache-transform"), + #[doc = "Timing-Allow-Origin.\n\n."] + (TIMING_ALLOW_ORIGIN, "timing-allow-origin"), + #[doc = "X-Device-User-Agent.\n\nW3C Mobile Web Best Practices Working Group."] + (X_DEVICE_USER_AGENT, "x-device-user-agent"), + ], + 20: [ + #[doc = "Sec-WebSocket-Accept.\n\nRFC 6455."] + (SEC_WEBSOCKET_ACCEPT, "sec-websocket-accept"), + #[doc = "Surrogate-Capability.\n\nRFC 4229."] + (SURROGATE_CAPABILITY, "surrogate-capability"), + #[doc = "Method-Check-Expires.\n\nW3C Web Application Formats Working Group."] + (METHOD_CHECK_EXPIRES, "method-check-expires"), + #[doc = "Repeatability-Result.\n\nRepeatable Requests Version 1.0, OASIS, Chet_Ensign."] + (REPEATABILITY_RESULT, "repeatability-result"), + ], + 21: [ + #[doc = "Apply-To-Redirect-Ref.\n\nRFC 4437."] + (APPLY_TO_REDIRECT_REF, "apply-to-redirect-ref"), + #[doc = "If-Schedule-Tag-Match.\n\nRFC 6638."] + (IF_SCHEDULE_TAG_MATCH, "if-schedule-tag-match"), + #[doc = "Sec-WebSocket-Version.\n\nRFC 6455."] + (SEC_WEBSOCKET_VERSION, "sec-websocket-version"), + ], + 22: [ + #[doc = "Authentication-Control.\n\nRFC 8053 section 4."] + (AUTHENTICATION_CONTROL, "authentication-control"), + #[doc = "Sec-WebSocket-Protocol.\n\nRFC 6455."] + (SEC_WEBSOCKET_PROTOCOL, "sec-websocket-protocol"), + #[doc = "X-Content-Type-Options.\n\n."] + (X_CONTENT_TYPE_OPTIONS, "x-content-type-options"), + #[doc = "Access-Control-Max-Age.\n\nW3C Web Application Formats Working Group."] + (ACCESS_CONTROL_MAX_AGE, "access-control-max-age"), + ], + 23: [ + #[doc = "Repeatability-Client-ID.\n\nRepeatable Requests Version 1.0, OASIS, Chet_Ensign."] + (REPEATABILITY_CLIENT_ID, "repeatability-client-id"), + #[doc = "X-Device-Accept-Charset.\n\nW3C Mobile Web Best Practices Working Group."] + (X_DEVICE_ACCEPT_CHARSET, "x-device-accept-charset"), + ], + 24: [ + #[doc = "Sec-WebSocket-Extensions.\n\nRFC 6455."] + (SEC_WEBSOCKET_EXTENSIONS, "sec-websocket-extensions"), + #[doc = "Repeatability-First-Sent.\n\nRepeatable Requests Version 1.0, OASIS, Chet_Ensign."] + (REPEATABILITY_FIRST_SENT, "repeatability-first-sent"), + #[doc = "Repeatability-Request-ID.\n\nRepeatable Requests Version 1.0, OASIS, Chet_Ensign."] + (REPEATABILITY_REQUEST_ID, "repeatability-request-id"), + #[doc = "X-Device-Accept-Encoding.\n\nW3C Mobile Web Best Practices Working Group."] + (X_DEVICE_ACCEPT_ENCODING, "x-device-accept-encoding"), + #[doc = "X-Device-Accept-Language.\n\nW3C Mobile Web Best Practices Working Group."] + (X_DEVICE_ACCEPT_LANGUAGE, "x-device-accept-language"), + ], + 25: [ + #[doc = "Optional-WWW-Authenticate.\n\nRFC 8053 section 3."] + (OPTIONAL_WWW_AUTHENTICATE, "optional-www-authenticate"), + #[doc = "Proxy-Authentication-Info.\n\nRFC 7615 section 4."] + (PROXY_AUTHENTICATION_INFO, "proxy-authentication-info"), + #[doc = "Strict-Transport-Security.\n\nRFC 6797."] + (STRICT_TRANSPORT_SECURITY, "strict-transport-security"), + #[doc = "Content-Transfer-Encoding.\n\nRFC 4229."] + (CONTENT_TRANSFER_ENCODING, "content-transfer-encoding"), + ], + 27: [ + #[doc = "Public-Key-Pins-Report-Only.\n\nRFC 7469."] + (PUBLIC_KEY_PINS_REPORT_ONLY, "public-key-pins-report-only"), + #[doc = "Access-Control-Allow-Origin.\n\nW3C Web Application Formats Working Group."] + (ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin"), + ], + 28: [ + #[doc = "Access-Control-Allow-Headers.\n\nW3C Web Application Formats Working Group."] + (ACCESS_CONTROL_ALLOW_HEADERS, "access-control-allow-headers"), + #[doc = "Access-Control-Allow-Methods.\n\nW3C Web Application Formats Working Group."] + (ACCESS_CONTROL_ALLOW_METHODS, "access-control-allow-methods"), + ], + 29: [ + #[doc = "Access-Control-Request-Method.\n\nW3C Web Application Formats Working Group."] + (ACCESS_CONTROL_REQUEST_METHOD, "access-control-request-method"), + ], + 30: [ + #[doc = "Access-Control-Request-Headers.\n\nW3C Web Application Formats Working Group."] + (ACCESS_CONTROL_REQUEST_HEADERS, "access-control-request-headers"), + ], + 32: [ + #[doc = "Access-Control-Allow-Credentials.\n\nW3C Web Application Formats Working Group."] + (ACCESS_CONTROL_ALLOW_CREDENTIALS, "access-control-allow-credentials"), + ], + 33: [ + #[doc = "Include-Referred-Token-Binding-ID.\n\nRFC 8473."] + (INCLUDE_REFERRED_TOKEN_BINDING_ID, "include-referred-token-binding-id"), + ], ); /// Create a new HTTP header `HeaderName`. diff --git a/http/src/lib.rs b/http/src/lib.rs index 094fd5605..ec6edb7ba 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -55,7 +55,7 @@ const fn is_lower_case(value: &str) -> bool { let mut i = 0; while i < value.len() { // NOTE: allows `-` because it's used in header names. - if !matches!(value[i], b'a'..=b'z' | b'-') { + if !matches!(value[i], b'0'..=b'9' | b'a'..=b'z' | b'-') { return false; } i += 1; diff --git a/http/src/parse_headers.bash b/http/src/parse_headers.bash new file mode 100755 index 000000000..a4ce63b5c --- /dev/null +++ b/http/src/parse_headers.bash @@ -0,0 +1,90 @@ +#!/usr/bin/env bash + +# Get the two csv files (permanent and provisional) from: +# https://www.iana.org/assignments/message-headers/message-headers.xhtml +# Remove the header from both file and run: +# $ cat perm-headers.csv prov-headers.csv | ./parse.bash + +set -eu + +clean_reference_partial() { + local reference="$1" + + # Remove '[' .. ']'. + if [[ "${reference:0:1}" == "[" ]]; then + if [[ "${reference: -1}" == "]" ]]; then + reference="${reference:1:-1}" + else + reference="${reference:1}" + fi + fi + + # Wrap links in '<' .. '>'. + if [[ "$reference" == http* ]]; then + reference="<$reference>" + fi + + if [[ "${reference:0:3}" == "RFC" ]]; then + # Add a space after 'RFC'. + reference="RFC ${reference:3}" + # Remove comma and lower case section. + reference="${reference/, S/ s}" + fi + + echo -n "$reference" +} + +clean_reference() { + local reference="$1" + local partial="${2:-false}" + + # Remove '"' .. '"'. + if [[ "${reference:0:1}" == "\"" ]]; then + reference="${reference:1:-1}" + fi + + # Some references are actually multiple references inside '[' .. ']'. + # Clean them one by one. + IFS="]" read -ra refs <<< "$reference" + reference="" + for ref in "${refs[@]}"; do + reference+="$(clean_reference_partial "$ref")" + reference+=', ' + done + + echo "${reference:0:-2}" # Remove last ', '. +} + +# Collect all known header name by length in `header_names`. +declare -a header_names +while IFS=$',' read -r name template protocol status reference; do + # We're only interested in HTTP headers. + if [[ "http" != "$protocol" ]]; then + continue + fi + + reference="$(clean_reference "$reference")" + const_name="${name^^}" # To uppercase. + const_name="${const_name//\-/\_}" # '-' -> '_'. + const_name="${const_name// /\_}" # ' ' -> '_'. + const_value="${name,,}" # To lowercase. + value_length="${#const_value}" # Value length. + docs="#[doc = \"$name.\\\\n\\\\n$reference.\"]" + + header_names[$value_length]+="$docs|$const_name|$const_value +" +done + +# Add non-standard headers. +# X-Request-ID. +header_names[12]+="#[doc = \"X-Request-ID.\"]|X_REQUEST_ID|x-request-id +" + +for value_length in "${!header_names[@]}"; do + values="${header_names[$value_length]}" + echo " $value_length: [" + while IFS=$'|' read -r docs const_name const_value; do + printf " $docs\n ($const_name, \"$const_value\"),\n" + done <<< "${values:0:-1}" # Remove last new line. + echo " ],"; +done diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index 1189fd103..26ca061d5 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -127,12 +127,220 @@ fn parse_header() { #[test] fn from_str_known_headers() { let known_headers = &[ - "date", - "allow", - "user-agent", - "x-request-id", - "content-length", - "transfer-encoding", + "A-IM", + "ALPN", + "AMP-Cache-Transform", + "Accept", + "Accept-Additions", + "Accept-CH", + "Accept-Charset", + "Accept-Datetime", + "Accept-Encoding", + "Accept-Features", + "Accept-Language", + "Accept-Patch", + "Accept-Post", + "Accept-Ranges", + "Access-Control", + "Access-Control-Allow-Credentials", + "Access-Control-Allow-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Origin", + "Access-Control-Max-Age", + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Age", + "Allow", + "Alt-Svc", + "Alt-Used", + "Alternates", + "Apply-To-Redirect-Ref", + "Authentication-Control", + "Authentication-Info", + "Authorization", + "C-Ext", + "C-Man", + "C-Opt", + "C-PEP", + "C-PEP-Info", + "CDN-Loop", + "Cache-Control", + "Cal-Managed-ID", + "CalDAV-Timezones", + "Cert-Not-After", + "Cert-Not-Before", + "Close", + "Compliance", + "Connection", + "Content-Base", + "Content-Disposition", + "Content-Encoding", + "Content-ID", + "Content-Language", + "Content-Length", + "Content-Location", + "Content-MD5", + "Content-Range", + "Content-Script-Type", + "Content-Style-Type", + "Content-Transfer-Encoding", + "Content-Type", + "Content-Version", + "Cookie", + "Cookie2", + "Cost", + "DASL", + "DAV", + "Date", + "Default-Style", + "Delta-Base", + "Depth", + "Derived-From", + "Destination", + "Differential-ID", + "Digest", + "EDIINT-Features", + "ETag", + "Early-Data", + "Expect", + "Expect-CT", + "Expires", + "Ext", + "Forwarded", + "From", + "GetProfile", + "HTTP2-Settings", + "Hobareg", + "Host", + "IM", + "If", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Range", + "If-Schedule-Tag-Match", + "If-Unmodified-Since", + "Include-Referred-Token-Binding-ID", + "Isolation", + "Keep-Alive", + "Label", + "Last-Modified", + "Link", + "Location", + "Lock-Token", + "MIME-Version", + "Man", + "Max-Forwards", + "Memento-Datetime", + "Message-ID", + "Meter", + "Method-Check", + "Method-Check-Expires", + "Negotiate", + "Non-Compliance", + "OData-EntityId", + "OData-Isolation", + "OData-MaxVersion", + "OData-Version", + "OSCORE", + "OSLC-Core-Version", + "Opt", + "Optional", + "Optional-WWW-Authenticate", + "Ordering-Type", + "Origin", + "Overwrite", + "P3P", + "PEP", + "PICS-Label", + "Pep-Info", + "Position", + "Pragma", + "Prefer", + "Preference-Applied", + "ProfileObject", + "Protocol", + "Protocol-Info", + "Protocol-Query", + "Protocol-Request", + "Proxy-Authenticate", + "Proxy-Authentication-Info", + "Proxy-Authorization", + "Proxy-Features", + "Proxy-Instruction", + "Public", + "Public-Key-Pins", + "Public-Key-Pins-Report-Only", + "Range", + "Redirect-Ref", + "Referer", + "Referer-Root", + "Repeatability-Client-ID", + "Repeatability-First-Sent", + "Repeatability-Request-ID", + "Repeatability-Result", + "Replay-Nonce", + "Resolution-Hint", + "Resolver-Location", + "Retry-After", + "SLUG", + "Safe", + "Schedule-Reply", + "Schedule-Tag", + "Sec-Token-Binding", + "Sec-WebSocket-Accept", + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Protocol", + "Sec-WebSocket-Version", + "Security-Scheme", + "Server", + "Set-Cookie", + "Set-Cookie2", + "SetProfile", + "SoapAction", + "Status-URI", + "Strict-Transport-Security", + "SubOK", + "Subst", + "Sunset", + "Surrogate-Capability", + "Surrogate-Control", + "TCN", + "TE", + "TTL", + "Timeout", + "Timing-Allow-Origin", + "Title", + "Topic", + "Traceparent", + "Tracestate", + "Trailer", + "Transfer-Encoding", + "UA-Color", + "UA-Media", + "UA-Pixels", + "UA-Resolution", + "UA-Windowpixels", + "URI", + "Upgrade", + "Urgency", + "User-Agent", + "Variant-Vary", + "Vary", + "Version", + "Via", + "WWW-Authenticate", + "Want-Digest", + "Warning", + "X-Content-Type-Options", + "X-Device-Accept", + "X-Device-Accept-Charset", + "X-Device-Accept-Encoding", + "X-Device-Accept-Language", + "X-Device-User-Agent", + "X-Frame-Options", + "X-Request-ID", ]; for name in known_headers { let header_name = HeaderName::from_str(name); From 3924b87f4eb8622230d5ab01679b1a76f76c0ccb Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 3 May 2021 21:35:07 +0200 Subject: [PATCH 29/81] Implement multiple HTTP Body types This commit adds four body types: * EmptyBody: no/empty body. * OneshotBody: body consisting of a single slice of bytes (&[u8]). * StreamingBody: body that is streaming, with a known length. * ChunkedBody: body that is streaming, with a unknown length. This uses HTTP chunked encoding to transfer the body. The ChunkedBody body however doesn't have an implementation yet. --- http/examples/my_ip.rs | 3 + http/src/body.rs | 300 ++++++++++++++++++++++++++++++++++++++--- http/src/lib.rs | 6 +- http/src/server.rs | 45 ++++--- 4 files changed, 311 insertions(+), 43 deletions(-) diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index 351d3c331..7c6dbc30d 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -9,6 +9,7 @@ use heph::net::TcpStream; use heph::rt::{self, Runtime, ThreadLocal}; use heph::spawn::options::{ActorOptions, Priority}; use heph::supervisor::{Supervisor, SupervisorStrategy}; +use heph_http::body::OneshotBody; use heph_http::{ self as http, Header, HeaderName, Headers, HttpServer, Method, Response, StatusCode, Version, }; @@ -101,6 +102,7 @@ async fn http_actor( (StatusCode::OK, body) }; let version = request.version().highest_minor(); + let body = OneshotBody::new(body.as_bytes()); let response = Response::new(version, code, headers, body); debug!("sending response: {:?}", response); connection.respond(response).await?; @@ -115,6 +117,7 @@ async fn http_actor( .last_request_version() .unwrap_or(Version::Http11) .highest_minor(); + let body = OneshotBody::new(body.as_bytes()); let response = Response::new(version, code, Headers::EMPTY, body); debug!("sending response: {:?}", response); connection.respond(response).await?; diff --git a/http/src/body.rs b/http/src/body.rs index da94f71d9..e6af1af09 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -1,42 +1,298 @@ -use std::borrow::Cow; +//! Module with HTTP body related types. +//! +//! See the [`Body`] trait. -// TODO: support streaming bodies. -pub trait Body { - fn as_bytes(&self) -> &[u8]; +use std::io::{self, IoSlice}; +use std::marker::PhantomData; +use std::stream::Stream; + +use heph::net::tcp::stream::{SendAll, TcpStream}; + +/// Trait that defines a HTTP body. +/// +/// The trait can't be implemented outside of this create and is implemented by +/// the following types: +/// +/// * [`EmptyBody`]: no/empty body. +/// * [`OneshotBody`]: body consisting of a single slice of bytes (`&[u8]`). +/// * [`StreamingBody`]: body that is streaming, with a known length. +/// * [`ChunkedBody`]: body that is streaming, with a *un*known length. This +/// uses HTTP chunked encoding to transfer the body. +pub trait Body: PrivateBody { + /// Length of the body, or the body will be chunked. + fn length(&self) -> BodyLength; +} + +/// Length of a body. +pub enum BodyLength { + /// Body length is known. + Known(usize), + /// Body length is unknown and the body will be transfered using chunked + /// encoding. + Chunked, } -impl Body for [u8] { - fn as_bytes(&self) -> &[u8] { - self +mod private { + use std::future::Future; + use std::io::{self, IoSlice}; + use std::pin::Pin; + use std::stream::Stream; + use std::task::{self, Poll}; + + use heph::net::TcpStream; + + /// Private extention of [`PrivateBody`]. + pub trait PrivateBody { + type WriteBody<'s, 'b>: Future>; + + /// Write the response to `stream`. + /// + /// The `http_head` buffer contains the HTTP header (i.e. status line + /// and all headers), this must still be written to the `stream` also. + fn write_response<'s, 'b>( + &'b mut self, + stream: &'s mut TcpStream, + http_head: &'b [u8], + ) -> Self::WriteBody<'s, 'b>; + } + + /// See [`OneshotBody`]. + #[derive(Debug)] + pub struct SendOneshotBody<'s, 'b> { + pub(super) stream: &'s mut TcpStream, + // HTTP head and body. + pub(super) bufs: [IoSlice<'b>; 2], + } + + impl<'s, 'b> Future for SendOneshotBody<'s, 'b> { + type Output = io::Result<()>; + + fn poll(self: Pin<&mut Self>, _: &mut task::Context<'_>) -> Poll { + let SendOneshotBody { stream, bufs } = Pin::into_inner(self); + loop { + match stream.try_send_vectored(bufs) { + Ok(0) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), + Ok(n) => { + let head_len = bufs[0].len(); + let body_len = bufs[1].len(); + if n >= head_len + body_len { + // Written everything. + return Poll::Ready(Ok(())); + } else if n <= head_len { + // Only written part of the head, advance the head + // buffer. + IoSlice::advance(&mut bufs[..1], n); + } else { + // Written entire head. + bufs[0] = IoSlice::new(&[]); + IoSlice::advance(&mut bufs[1..], n - head_len); + } + } + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + return Poll::Pending + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + } + } + + /// See [`StreamingBody`]. + #[derive(Debug)] + pub struct SendStreamingBody<'s, 'h, 'b, B> { + pub(super) stream: &'s mut TcpStream, + pub(super) head: &'h [u8], + /// Bytes left to write from `body`, not counting the HTTP head. + pub(super) left: usize, + pub(super) body: B, + /// Slice of bytes from `body`. + pub(super) body_bytes: Option<&'b [u8]>, + } + + impl<'s, 'h, 'b, B> Future for SendStreamingBody<'s, 'h, 'b, B> + where + B: Stream>, + { + type Output = io::Result<()>; + + fn poll(self: Pin<&mut Self>, ctx: &mut task::Context<'_>) -> Poll { + // SAFETY: not moving `body: B`, ensuring it's still pinned. + #[rustfmt::skip] + let SendStreamingBody { stream, head, left, body, body_bytes } = unsafe { Pin::into_inner_unchecked(self) }; + let mut body = unsafe { Pin::new_unchecked(body) }; + + // Send the HTTP head first. + // TODO: try to use vectored I/O on first call. + while !head.is_empty() { + match stream.try_send(head) { + Ok(0) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), + Ok(n) => *head = &head[n..], + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + return Poll::Pending + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + + while *left != 0 { + // We have bytes we need to send. + if let Some(bytes) = body_bytes.as_mut() { + match stream.try_send(*bytes) { + Ok(0) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), + Ok(n) if n >= bytes.len() => { + *body_bytes = None; + } + Ok(n) => { + *bytes = &bytes[n..]; + continue; + } + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + return Poll::Pending + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + + // Read some bytes from the `body` stream. + match body.as_mut().poll_next(ctx) { + Poll::Ready(Some(Ok(bytes))) => *body_bytes = Some(bytes), + Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)), + Poll::Ready(None) => { + // NOTE: this shouldn't happend. + debug_assert!(*left == 0, "short body provided to `StreamingBody`"); + return Poll::Ready(Ok(())); + } + Poll::Pending => return Poll::Pending, + } + } + + Poll::Ready(Ok(())) + } } } -impl Body for Vec { - fn as_bytes(&self) -> &[u8] { - &*self +pub(crate) use private::PrivateBody; +use private::{SendOneshotBody, SendStreamingBody}; + +/// An empty body. +#[derive(Debug)] +pub struct EmptyBody; + +impl Body for EmptyBody { + fn length(&self) -> BodyLength { + BodyLength::Known(0) } } -impl Body for Cow<'_, [u8]> { - fn as_bytes(&self) -> &[u8] { - self.as_ref() +impl PrivateBody for EmptyBody { + type WriteBody<'s, 'b> = SendAll<'s, 'b>; + + fn write_response<'s, 'b>( + &'b mut self, + stream: &'s mut TcpStream, + http_head: &'b [u8], + ) -> Self::WriteBody<'s, 'b> { + // Just need to write the HTTP head as we don't have a body. + stream.send_all(http_head) + } +} + +/// Body length and content is known in advance. Send in a single payload (i.e. +/// not chunked). +#[derive(Debug)] +pub struct OneshotBody<'b> { + bytes: &'b [u8], +} + +impl<'b> OneshotBody<'b> { + /// Create a new one-shot body. + pub const fn new(body: &'b [u8]) -> OneshotBody<'b> { + OneshotBody { bytes: body } + } +} + +impl<'b> Body for OneshotBody<'b> { + fn length(&self) -> BodyLength { + BodyLength::Known(self.bytes.len()) + } +} + +impl<'a> PrivateBody for OneshotBody<'a> { + type WriteBody<'s, 'b> = SendOneshotBody<'s, 'b>; + + fn write_response<'s, 'b>( + &'b mut self, + stream: &'s mut TcpStream, + http_head: &'b [u8], + ) -> Self::WriteBody<'s, 'b> { + let head = IoSlice::new(http_head); + let body = IoSlice::new(self.bytes); + SendOneshotBody { + stream, + bufs: [head, body], + } } } -impl Body for str { - fn as_bytes(&self) -> &[u8] { - self.as_bytes() +impl<'b> From<&'b [u8]> for OneshotBody<'b> { + fn from(body: &'b [u8]) -> Self { + OneshotBody::new(body) } } -impl Body for String { - fn as_bytes(&self) -> &[u8] { - self.as_bytes() +impl<'b> From<&'b str> for OneshotBody<'b> { + fn from(body: &'b str) -> Self { + OneshotBody::new(body.as_bytes()) } } -impl Body for Cow<'_, str> { - fn as_bytes(&self) -> &[u8] { - self.as_ref().as_bytes() +/// Streaming body with a known length. Send in a single payload (i.e. not +/// chunked). +#[derive(Debug)] +pub struct StreamingBody<'b, B> { + length: usize, + body: Option, + _body_lifetime: PhantomData<&'b [u8]>, +} + +impl<'b, B> Body for StreamingBody<'b, B> +where + B: Stream>, +{ + fn length(&self) -> BodyLength { + BodyLength::Known(self.length) } } + +impl<'b, B> PrivateBody for StreamingBody<'b, B> +where + B: Stream>, +{ + type WriteBody<'s, 'h> = SendStreamingBody<'s, 'h, 'b, B>; + + fn write_response<'s, 'h>( + &'h mut self, + stream: &'s mut TcpStream, + head: &'h [u8], + ) -> Self::WriteBody<'s, 'h> { + SendStreamingBody { + stream, + body: self.body.take().unwrap(), + head, + left: self.length, + body_bytes: None, + } + } +} + +/// Streaming body with an unknown length. Send in multiple chunks. +#[derive(Debug)] +pub struct ChunkedBody<'b, B> { + stream: B, + _body_lifetime: PhantomData<&'b [u8]>, +} + +// TODO: implement `Body` for `ChunkedBody`. diff --git a/http/src/lib.rs b/http/src/lib.rs index ec6edb7ba..d3c268215 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -1,6 +1,7 @@ -#![feature(const_panic)] +#![feature(async_stream, const_panic, generic_associated_types, io_slice_advance)] +#![allow(incomplete_features)] // NOTE: for `generic_associated_types`. -mod body; +pub mod body; mod from_bytes; pub mod header; pub mod method; @@ -10,6 +11,7 @@ pub mod server; mod status_code; pub mod version; +#[doc(no_inline)] pub use body::Body; pub use from_bytes::FromBytes; #[doc(no_inline)] diff --git a/http/src/server.rs b/http/src/server.rs index 7b2fa6b35..1abc962f1 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -7,7 +7,7 @@ // TODO: reading request body. use std::fmt; -use std::io::{self, IoSlice, Write}; +use std::io::{self, Write}; use std::net::SocketAddr; use std::pin::Pin; use std::task::{self, Poll}; @@ -19,6 +19,7 @@ use heph::{actor, rt, Actor, NewActor, Supervisor}; use httparse::EMPTY_HEADER; use httpdate::HttpDate; +use crate::body::BodyLength; use crate::{FromBytes, HeaderName, Headers, Method, Request, Response, StatusCode, Version}; /// Maximum size of the header (the start line and the headers). @@ -409,6 +410,7 @@ impl Connection { /// ``` /// use heph_http::{Response, Headers, StatusCode, Version}; /// use heph_http::server::{Connection, RequestError}; + /// use heph_http::body::OneshotBody; /// /// # return; /// # #[allow(unreachable_code)] @@ -424,6 +426,7 @@ impl Connection { /// // here). /// let version = conn.last_request_version().unwrap_or(Version::Http11); /// let body = format!("Bad request: {}", err); + /// let body = OneshotBody::new(body.as_bytes()); /// let response = Response::new(version, StatusCode::BAD_REQUEST, Headers::EMPTY, body); /// /// // Respond with the response. @@ -446,7 +449,7 @@ impl Connection { /// `response`. /// /// This doesn't include the body if the response is to a HEAD request. - pub async fn respond(&mut self, response: Response) -> io::Result<()> + pub async fn respond(&mut self, mut response: Response) -> io::Result<()> where B: crate::Body, { @@ -509,31 +512,35 @@ impl Connection { write!(&mut self.buf, "Date: {}\r\n", now).unwrap(); } - // Response body. - let body = if let Some(Method::Head) = self.last_method { - // RFC 7231 section 4.3.2: - // > The HEAD method is identical to GET except that the server MUST - // > NOT send a message body in the response (i.e., the response - // > terminates at the end of the header section). - &[] - } else { - response.body().as_bytes() - }; - // Provide the "Conent-Length" header if the user didn't. if !set_content_length_header { - write!(&mut self.buf, "Content-Length: {}\r\n", body.len()).unwrap(); + let body_length = if let Some(Method::Head) = self.last_method { + // RFC 7231 section 4.3.2: + // > The HEAD method is identical to GET except that the server + // > MUST NOT send a message body in the response (i.e., the + // > response terminates at the end of the header section). + 0 + } else { + match response.body().length() { + BodyLength::Known(length) => length, + BodyLength::Chunked => todo!("chunked response body"), + } + }; + + write!(&mut self.buf, "Content-Length: {}\r\n", body_length).unwrap(); } // End of the header. self.buf.extend_from_slice(b"\r\n"); - // Write the response to the connection. - let header = IoSlice::new(&self.buf[ignore_end..]); - let body = IoSlice::new(body); - self.stream.send_vectored_all(&mut [header, body]).await?; + // Write the response to the stream. + let head = &self.buf[ignore_end..]; + response + .body_mut() + .write_response(&mut self.stream, head) + .await?; - // Remove the response from the buffer. + // Remove the response headers from the buffer. self.buf.truncate(ignore_end); Ok(()) } From d55a82f125a1dc60d7d4b3b09a29eb0900ef4c03 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 12:01:52 +0200 Subject: [PATCH 30/81] Add FileBody body type --- http/src/body.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/http/src/body.rs b/http/src/body.rs index e6af1af09..bdd6ebc13 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -4,9 +4,10 @@ use std::io::{self, IoSlice}; use std::marker::PhantomData; +use std::num::NonZeroUsize; use std::stream::Stream; -use heph::net::tcp::stream::{SendAll, TcpStream}; +use heph::net::tcp::stream::{FileSend, SendAll, TcpStream}; /// Trait that defines a HTTP body. /// @@ -18,6 +19,8 @@ use heph::net::tcp::stream::{SendAll, TcpStream}; /// * [`StreamingBody`]: body that is streaming, with a known length. /// * [`ChunkedBody`]: body that is streaming, with a *un*known length. This /// uses HTTP chunked encoding to transfer the body. +/// * [`FileBody`]: uses a file as body, sending it's content using the +/// `sendfile(2)` system call. pub trait Body: PrivateBody { /// Length of the body, or the body will be chunked. fn length(&self) -> BodyLength; @@ -35,10 +38,12 @@ pub enum BodyLength { mod private { use std::future::Future; use std::io::{self, IoSlice}; + use std::num::NonZeroUsize; use std::pin::Pin; use std::stream::Stream; use std::task::{self, Poll}; + use heph::net::tcp::stream::FileSend; use heph::net::TcpStream; /// Private extention of [`PrivateBody`]. @@ -172,10 +177,61 @@ mod private { Poll::Ready(Ok(())) } } + + /// See [`FileBody`]. + #[derive(Debug)] + pub struct SendFileBody<'s, 'h, 'f, F> { + pub(super) stream: &'s mut TcpStream, + pub(super) head: &'h [u8], + pub(super) file: &'f F, + pub(super) offset: usize, + pub(super) end: NonZeroUsize, + } + + impl<'s, 'h, 'f, F> Future for SendFileBody<'s, 'h, 'f, F> + where + F: FileSend, + { + type Output = io::Result<()>; + + fn poll(self: Pin<&mut Self>, _: &mut task::Context<'_>) -> Poll { + #[rustfmt::skip] + let SendFileBody { stream, head, file, offset, end } = Pin::into_inner(self); + + // Send the HTTP head first. + while !head.is_empty() { + match stream.try_send(head) { + Ok(0) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), + Ok(n) => *head = &head[n..], + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + return Poll::Pending + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + + while end.get() > *offset { + let length = NonZeroUsize::new(end.get() - *offset); + match stream.try_send_file(*file, *offset, length) { + // All bytes were send. + Ok(0) => return Poll::Ready(Ok(())), + Ok(n) => *offset += n, + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + return Poll::Pending + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + + Poll::Ready(Ok(())) + } + } } pub(crate) use private::PrivateBody; -use private::{SendOneshotBody, SendStreamingBody}; +use private::{SendFileBody, SendOneshotBody, SendStreamingBody}; /// An empty body. #[derive(Debug)] @@ -296,3 +352,66 @@ pub struct ChunkedBody<'b, B> { } // TODO: implement `Body` for `ChunkedBody`. + +/// Body that sends the entire file `F`. +pub struct FileBody<'f, F> { + file: Option<&'f F>, + /// Start offset into the `file`. + offset: usize, + /// Length of the file, or the maximum number of bytes to send (minus + /// `offset`). + /// Always: `end >= offset`. + end: NonZeroUsize, +} + +impl<'f, F> FileBody<'f, F> +where + F: FileSend, +{ + /// Use a file as HTTP body + /// + /// This uses the bytes `offset..end` from `file` as HTTP body and sends + /// them using `sendfile(2)` (using [`TcpStream::send_file`]). + // TODO: make this a `const` fn once trait bounds (`FileSend`) on `const` + // functions are stable. + pub fn new(file: &'f F, offset: usize, end: NonZeroUsize) -> FileBody<'f, F> { + debug_assert!(end.get() >= offset); + FileBody { + file: Some(file), + offset, + end, + } + } +} + +impl<'f, F> Body for FileBody<'f, F> +where + F: FileSend, +{ + fn length(&self) -> BodyLength { + // NOTE: per the comment on `end`: `end >= offset`, so this can't + // underflow. + BodyLength::Known(self.end.get() - self.offset) + } +} + +impl<'f, F> PrivateBody for FileBody<'f, F> +where + F: FileSend, +{ + type WriteBody<'s, 'h> = SendFileBody<'s, 'h, 'f, F>; + + fn write_response<'s, 'h>( + &'h mut self, + stream: &'s mut TcpStream, + head: &'h [u8], + ) -> Self::WriteBody<'s, 'h> { + SendFileBody { + stream, + head, + file: self.file.take().unwrap(), + offset: self.offset, + end: self.end, + } + } +} From 2df91cd94077920fa36adfd7bf3fbcaf4903789d Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 14:27:27 +0200 Subject: [PATCH 31/81] Add StatusCode::includes_body Returns false if the status code MUST NOT include a body. This includes the entire 1xx (Informational) range, 204 (No Content), and 304 (Not Modified). See RFC 7230 section 3.3 and RFC 7231 section 6. --- http/src/status_code.rs | 18 ++++++++++++++++++ http/tests/functional/status_code.rs | 12 ++++++++++++ 2 files changed, 30 insertions(+) diff --git a/http/src/status_code.rs b/http/src/status_code.rs index 701932414..dceb1e6ce 100644 --- a/http/src/status_code.rs +++ b/http/src/status_code.rs @@ -295,6 +295,24 @@ impl StatusCode { self.0 >= 500 && self.0 <= 599 } + /// Returns `false` if the status code MUST NOT include a body. + /// + /// This includes the entire 1xx (Informational) range, 204 (No Content), + /// and 304 (Not Modified). + /// + /// Also see RFC 7230 section 3.3 and RFC 7231 section 6 (the individual + /// status codes). + pub const fn includes_body(self) -> bool { + // RFC 7230 section 3.3: + // > All 1xx (Informational), 204 (No Content), and 304 (Not Modified) + // > responses do not include a message body. All other responses do + // > include a message body, although the body might be of zero length. + match self.0 { + 100..=199 | 204 | 304 => false, + _ => true, + } + } + /// Returns the reason phrase for well known status codes. pub const fn phrase(self) -> Option<&'static str> { match self.0 { diff --git a/http/tests/functional/status_code.rs b/http/tests/functional/status_code.rs index c72a9a9d2..d32cacac6 100644 --- a/http/tests/functional/status_code.rs +++ b/http/tests/functional/status_code.rs @@ -60,6 +60,18 @@ fn is_server_error() { } } +#[test] +fn includes_body() { + let no_body = &[100, 101, 199, 204, 304]; + for status in no_body { + assert!(!StatusCode(*status).includes_body()); + } + let has_body = &[0, 10, 200, 201, 203, 205, 300, 301, 303, 305, 400, 500, 999]; + for status in has_body { + assert!(StatusCode(*status).includes_body()); + } +} + #[test] fn phrase() { #[rustfmt::skip] From 6e71fa5707cb459183b350c163be843f6818b237 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 14:28:42 +0200 Subject: [PATCH 32/81] Replace Method::is_head with expects_body Method::expects_body returns false if a response to this method MUST NOT include a body. This is only true for the HEAD method. RFC 7321 section 4.3.2. --- http/src/method.rs | 21 ++++++++++++++++----- http/tests/functional/method.rs | 21 ++++++++++++--------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/http/src/method.rs b/http/src/method.rs index 28e12aaad..0010c8bfd 100644 --- a/http/src/method.rs +++ b/http/src/method.rs @@ -49,11 +49,6 @@ pub enum Method { } impl Method { - /// Returns `true` if `self` is a HEAD method. - pub const fn is_head(self) -> bool { - matches!(self, Method::Head) - } - /// Returns `true` if the method is safe. /// /// RFC 7321 section 4.2.1. @@ -69,6 +64,22 @@ impl Method { matches!(self, Method::Put | Method::Delete) || self.is_safe() } + /// Returns `false` if a response to this method MUST NOT include a body. + /// + /// This is only true for the HEAD method. + /// + /// RFC 7321 section 4.3.2. + pub const fn expects_body(self) -> bool { + // RFC 7231 section 4.3.2: + // > The HEAD method is identical to GET except that the server MUST NOT + // > send a message body in the response (i.e., the response terminates + // > at the end of the header section). + match self { + Method::Head => false, + _ => true, + } + } + /// Returns the method as string. pub const fn as_str(self) -> &'static str { use Method::*; diff --git a/http/tests/functional/method.rs b/http/tests/functional/method.rs index f83346ebd..e3607b584 100644 --- a/http/tests/functional/method.rs +++ b/http/tests/functional/method.rs @@ -7,15 +7,6 @@ fn size() { assert_size::(1); } -#[test] -fn is_head() { - assert!(Head.is_head()); - let tests = &[Get, Post, Put, Delete, Connect, Options, Trace, Patch]; - for method in tests { - assert!(!method.is_head()); - } -} - #[test] fn is_safe() { let safe = &[Get, Head, Options, Trace]; @@ -40,6 +31,18 @@ fn is_idempotent() { } } +#[test] +fn expects_body() { + let no_body = &[Head]; + for method in no_body { + assert!(!method.expects_body()); + } + let has_body = &[Get, Post, Put, Delete, Connect, Options, Trace, Patch]; + for method in has_body { + assert!(method.expects_body()); + } +} + #[test] fn as_str() { let tests = &[ From 49c1035d1f6cc646e82eaf75468c941926423611 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 15:19:57 +0200 Subject: [PATCH 33/81] Various improvements to server::Connection Adds itoa as dependency for writing integers. And improve writing of status line and headers in Connection::send_response by using Vec's method directly. Changes Connection::respond to accept a StatusCode, Headers and a body. Adds Connection::send_response which accept a request Method and Response. The new Connection::respond method is easier to use as it handles parts of the request (i.e. the version and method) internally. Add Connection::last_request_method to get the request Method from the last request. Removes Connection::close to not confuse users about whether or not it should be called when dropping the connection. Updates the documentation of most Connection methods. Also Add StreamingBody::new, a way to actually create a streaming body (which wasn't possible before). Changes the PrivateBody trait to accept self, rather then &mut self. Removes some Options from types such as StreamingBody and FileBody. Adds Response::into_body to remove the body from a response. --- http/Cargo.toml | 1 + http/examples/my_ip.rs | 21 ++---- http/src/body.rs | 106 +++++++++++++++++----------- http/src/method.rs | 4 +- http/src/response.rs | 9 ++- http/src/server.rs | 157 ++++++++++++++++++++++++----------------- 6 files changed, 173 insertions(+), 125 deletions(-) diff --git a/http/Cargo.toml b/http/Cargo.toml index 52c4e0d5f..2dda83e0b 100644 --- a/http/Cargo.toml +++ b/http/Cargo.toml @@ -8,6 +8,7 @@ heph = { version = "0.3.0", path = "../", default-features = false } httparse = { version = "1.4.0", default-features = false } httpdate = { version = "1.0.0", default-features = false } log = { version = "0.4.8", default-features = false } +itoa = { version = "0.4.7", default-features = false } [dev-dependencies] # Enable logging panics via `std-logger`. diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index 7c6dbc30d..c09ed9470 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -10,9 +10,7 @@ use heph::rt::{self, Runtime, ThreadLocal}; use heph::spawn::options::{ActorOptions, Priority}; use heph::supervisor::{Supervisor, SupervisorStrategy}; use heph_http::body::OneshotBody; -use heph_http::{ - self as http, Header, HeaderName, Headers, HttpServer, Method, Response, StatusCode, Version, -}; +use heph_http::{self as http, Header, HeaderName, Headers, HttpServer, Method, StatusCode}; use log::{debug, error, info, warn}; fn main() -> Result<(), rt::Error> { @@ -101,11 +99,9 @@ async fn http_actor( let body = Cow::from(address.ip().to_string()); (StatusCode::OK, body) }; - let version = request.version().highest_minor(); + debug!("sending response: code={}, body='{}'", code, body); let body = OneshotBody::new(body.as_bytes()); - let response = Response::new(version, code, headers, body); - debug!("sending response: {:?}", response); - connection.respond(response).await?; + connection.respond(code, headers, body).await?; } // No more requests. Ok(None) => return Ok(()), @@ -113,16 +109,11 @@ async fn http_actor( warn!("error reading request: {}: source={}", err, address); let code = err.proper_status_code(); let body = format!("Bad request: {}", err); - let version = connection - .last_request_version() - .unwrap_or(Version::Http11) - .highest_minor(); + debug!("sending erorr response: code={}, body='{}'", code, body); let body = OneshotBody::new(body.as_bytes()); - let response = Response::new(version, code, Headers::EMPTY, body); - debug!("sending response: {:?}", response); - connection.respond(response).await?; + connection.respond(code, Headers::EMPTY, body).await?; if err.should_close() { - connection.close(); + warn!("closing connection after error: {}", err); return Ok(()); } } diff --git a/http/src/body.rs b/http/src/body.rs index bdd6ebc13..9b229aed8 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -21,7 +21,7 @@ use heph::net::tcp::stream::{FileSend, SendAll, TcpStream}; /// uses HTTP chunked encoding to transfer the body. /// * [`FileBody`]: uses a file as body, sending it's content using the /// `sendfile(2)` system call. -pub trait Body: PrivateBody { +pub trait Body<'a>: PrivateBody<'a> { /// Length of the body, or the body will be chunked. fn length(&self) -> BodyLength; } @@ -47,18 +47,20 @@ mod private { use heph::net::TcpStream; /// Private extention of [`PrivateBody`]. - pub trait PrivateBody { - type WriteBody<'s, 'b>: Future>; + pub trait PrivateBody<'a> { + type WriteBody<'stream, 'head>: Future>; /// Write the response to `stream`. /// /// The `http_head` buffer contains the HTTP header (i.e. status line /// and all headers), this must still be written to the `stream` also. - fn write_response<'s, 'b>( - &'b mut self, - stream: &'s mut TcpStream, - http_head: &'b [u8], - ) -> Self::WriteBody<'s, 'b>; + fn write_response<'stream, 'head>( + self, + stream: &'stream mut TcpStream, + http_head: &'head [u8], + ) -> Self::WriteBody<'stream, 'head> + where + 'a: 'head; } /// See [`OneshotBody`]. @@ -237,20 +239,23 @@ use private::{SendFileBody, SendOneshotBody, SendStreamingBody}; #[derive(Debug)] pub struct EmptyBody; -impl Body for EmptyBody { +impl Body<'_> for EmptyBody { fn length(&self) -> BodyLength { BodyLength::Known(0) } } -impl PrivateBody for EmptyBody { - type WriteBody<'s, 'b> = SendAll<'s, 'b>; +impl<'a> PrivateBody<'a> for EmptyBody { + type WriteBody<'s, 'h> = SendAll<'s, 'h>; - fn write_response<'s, 'b>( - &'b mut self, + fn write_response<'s, 'h>( + self, stream: &'s mut TcpStream, - http_head: &'b [u8], - ) -> Self::WriteBody<'s, 'b> { + http_head: &'h [u8], + ) -> Self::WriteBody<'s, 'h> + where + 'a: 'h, + { // Just need to write the HTTP head as we don't have a body. stream.send_all(http_head) } @@ -270,20 +275,23 @@ impl<'b> OneshotBody<'b> { } } -impl<'b> Body for OneshotBody<'b> { +impl<'b> Body<'b> for OneshotBody<'b> { fn length(&self) -> BodyLength { BodyLength::Known(self.bytes.len()) } } -impl<'a> PrivateBody for OneshotBody<'a> { - type WriteBody<'s, 'b> = SendOneshotBody<'s, 'b>; +impl<'a> PrivateBody<'a> for OneshotBody<'a> { + type WriteBody<'s, 'h> = SendOneshotBody<'s, 'h>; - fn write_response<'s, 'b>( - &'b mut self, + fn write_response<'s, 'h>( + self, stream: &'s mut TcpStream, - http_head: &'b [u8], - ) -> Self::WriteBody<'s, 'b> { + http_head: &'h [u8], + ) -> Self::WriteBody<'s, 'h> + where + 'a: 'h, + { let head = IoSlice::new(http_head); let body = IoSlice::new(self.bytes); SendOneshotBody { @@ -310,11 +318,27 @@ impl<'b> From<&'b str> for OneshotBody<'b> { #[derive(Debug)] pub struct StreamingBody<'b, B> { length: usize, - body: Option, + body: B, _body_lifetime: PhantomData<&'b [u8]>, } -impl<'b, B> Body for StreamingBody<'b, B> +impl<'b, B> StreamingBody<'b, B> +where + B: Stream>, +{ + /// Use a [`Stream`] as HTTP body. + // TODO: make this a `const` fn once trait bounds (`FileSend`) on `const` + // functions are stable. + pub fn new(length: usize, stream: B) -> StreamingBody<'b, B> { + StreamingBody { + length, + body: stream, + _body_lifetime: PhantomData, + } + } +} + +impl<'b, B> Body<'b> for StreamingBody<'b, B> where B: Stream>, { @@ -323,20 +347,23 @@ where } } -impl<'b, B> PrivateBody for StreamingBody<'b, B> +impl<'b, B> PrivateBody<'b> for StreamingBody<'b, B> where B: Stream>, { type WriteBody<'s, 'h> = SendStreamingBody<'s, 'h, 'b, B>; fn write_response<'s, 'h>( - &'h mut self, + self, stream: &'s mut TcpStream, head: &'h [u8], - ) -> Self::WriteBody<'s, 'h> { + ) -> Self::WriteBody<'s, 'h> + where + 'b: 'h, + { SendStreamingBody { stream, - body: self.body.take().unwrap(), + body: self.body, head, left: self.length, body_bytes: None, @@ -355,7 +382,7 @@ pub struct ChunkedBody<'b, B> { /// Body that sends the entire file `F`. pub struct FileBody<'f, F> { - file: Option<&'f F>, + file: &'f F, /// Start offset into the `file`. offset: usize, /// Length of the file, or the maximum number of bytes to send (minus @@ -368,7 +395,7 @@ impl<'f, F> FileBody<'f, F> where F: FileSend, { - /// Use a file as HTTP body + /// Use a file as HTTP body. /// /// This uses the bytes `offset..end` from `file` as HTTP body and sends /// them using `sendfile(2)` (using [`TcpStream::send_file`]). @@ -376,15 +403,11 @@ where // functions are stable. pub fn new(file: &'f F, offset: usize, end: NonZeroUsize) -> FileBody<'f, F> { debug_assert!(end.get() >= offset); - FileBody { - file: Some(file), - offset, - end, - } + FileBody { file, offset, end } } } -impl<'f, F> Body for FileBody<'f, F> +impl<'f, F> Body<'f> for FileBody<'f, F> where F: FileSend, { @@ -395,21 +418,24 @@ where } } -impl<'f, F> PrivateBody for FileBody<'f, F> +impl<'f, F> PrivateBody<'f> for FileBody<'f, F> where F: FileSend, { type WriteBody<'s, 'h> = SendFileBody<'s, 'h, 'f, F>; fn write_response<'s, 'h>( - &'h mut self, + self, stream: &'s mut TcpStream, head: &'h [u8], - ) -> Self::WriteBody<'s, 'h> { + ) -> Self::WriteBody<'s, 'h> + where + 'f: 'h, + { SendFileBody { stream, head, - file: self.file.take().unwrap(), + file: self.file, offset: self.offset, end: self.end, } diff --git a/http/src/method.rs b/http/src/method.rs index 0010c8bfd..dac42cb67 100644 --- a/http/src/method.rs +++ b/http/src/method.rs @@ -66,9 +66,9 @@ impl Method { /// Returns `false` if a response to this method MUST NOT include a body. /// - /// This is only true for the HEAD method. + /// This is only the case for the HEAD method. /// - /// RFC 7321 section 4.3.2. + /// RFC 7230 section 3.3 and RFC 7321 section 4.3.2. pub const fn expects_body(self) -> bool { // RFC 7231 section 4.3.2: // > The HEAD method is identical to GET except that the server MUST NOT diff --git a/http/src/response.rs b/http/src/response.rs index 569b2dc2c..d1b0cb643 100644 --- a/http/src/response.rs +++ b/http/src/response.rs @@ -46,15 +46,20 @@ impl Response { &mut self.headers } - /// The response body. + /// Returns a reference to the body. pub const fn body(&self) -> &B { &self.body } - /// Mutable access to the response body. + /// Returns a mutable reference to the body. pub fn body_mut(&mut self) -> &mut B { &mut self.body } + + /// Returns the body of the response. + pub fn into_body(self) -> B { + self.body + } } impl fmt::Debug for Response { diff --git a/http/src/server.rs b/http/src/server.rs index 1abc962f1..b6ade4a94 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -249,20 +249,22 @@ impl Connection { /// [`io::Result`], which often needs to be handled seperately from errors /// in the request, e.g. by using `?`. /// - /// Next is a `Result, `[`RequestError`]`>`. `None` - /// is returned if the connections contains no more requests, i.e. all bytes - /// are read. If the connection contains a request it will return a - /// [`Request`]. If the request is somehow invalid/incomplete it will return - /// an [`RequestError`]. + /// Next is a `Result, `[`RequestError`]`>`. + /// `Ok(None)` is returned if the connection contains no more requests, i.e. + /// when all bytes are read. If the connection contains a request it will + /// return `Ok(Some(`[`Request`]`)`. If the request is somehow invalid it + /// will return an `Err(`[`RequestError`]`)`. /// /// # Notes /// /// Most [`RequestError`]s can't be receover from and will need the /// connection be closed, see [`RequestError::should_close`]. If the - /// connection is not closed and [`next_request`] is called again it will + /// connection is not closed and `next_request` is called again it will /// likely return the same error (but this is not guaranteed). /// - /// [`next_request`]: Connection::next_request + /// Also see the [`Connection::last_request_version`] and + /// [`Connection::last_request_method`] functions to properly respond to + /// request errors. pub async fn next_request<'a>( &'a mut self, ) -> io::Result>>, RequestError>> { @@ -408,7 +410,7 @@ impl Connection { /// Responding to a [`RequestError`]. /// /// ``` - /// use heph_http::{Response, Headers, StatusCode, Version}; + /// use heph_http::{Response, Headers, StatusCode, Version, Method}; /// use heph_http::server::{Connection, RequestError}; /// use heph_http::body::OneshotBody; /// @@ -429,12 +431,14 @@ impl Connection { /// let body = OneshotBody::new(body.as_bytes()); /// let response = Response::new(version, StatusCode::BAD_REQUEST, Headers::EMPTY, body); /// + /// // We can use `last_request_method` to determine the method of the last + /// // request, which is used to determine if we need to send a body. + /// let request_method = conn.last_request_method().unwrap_or(Method::Get); /// // Respond with the response. - /// conn.respond(response); + /// conn.send_response(request_method, response); /// /// // Close the connection if the error is fatal. /// if err.should_close() { - /// conn.close(); /// return; /// } /// # } @@ -443,48 +447,82 @@ impl Connection { self.last_version } + /// Returns the HTTP method of the last (partial) request. + /// + /// This can be used in cases where [`Connection::next_request`] returns a + /// [`RequestError`]. + /// + /// # Examples + /// + /// See [`Connection::last_request_version`] for an example that responds to + /// a [`RequestError`], which uses `last_request_method`. + pub fn last_request_method(&self) -> Option { + self.last_method + } + + /// Respond to a request. + /// /// # Notes /// - /// This automatically sets the "Content-Length" header if no provided in - /// `response`. + /// This uses information from the last call to [`Connection::next_request`] + /// to respond to the request correctly. For example it uses the HTTP + /// [`Method`] to determine whether or not to send the body (as HEAD request + /// don't expect a body). When reading multiple requests from the connection + /// before responding use [`Connection::send_response`] directly. /// - /// This doesn't include the body if the response is to a HEAD request. - pub async fn respond(&mut self, mut response: Response) -> io::Result<()> + /// See the notes for [`Connection::send_response`], they apply to this + /// function also. + pub async fn respond<'b, B>( + &mut self, + status: StatusCode, + headers: Headers, + body: B, + ) -> io::Result<()> where - B: crate::Body, + B: crate::Body<'b>, { + let req_method = self.last_method.unwrap_or(Method::Get); + let version = self.last_version.unwrap_or(Version::Http11).highest_minor(); + let response = Response::new(version, status, headers, body); + self.send_response(req_method, response).await + } + + /// Send a [`Response`]. + /// + /// # Notes + /// + /// This automatically sets the "Content-Length" and "Date" headers if not + /// provided in `response`. + /// + /// If `request_method.`[`expects_body`] or + /// `response.status().`[`includes_body`] returns false this will not write + /// the body to the connection. + /// + /// [`expects_body`]: Method::expects_body + /// [`includes_body`]: StatusCode::includes_body + pub async fn send_response<'b, B>( + &mut self, + request_method: Method, + response: Response, + ) -> io::Result<()> + where + B: crate::Body<'b>, + { + let mut itoa_buf = itoa::Buffer::new(); + // Bytes of the (next) request. self.clear_buffer(); let ignore_end = self.buf.len(); - // TODO: RFC 7230 section 3.3: - // > The presence of a message body in a response depends on - // > both the request method to which it is responding and - // > the response status code (Section 3.1.2). Responses to - // > the HEAD request method (Section 4.3.2 of [RFC7231]) - // > never include a message body because the associated - // > response header fields (e.g., Transfer-Encoding, - // > Content-Length, etc.), if present, indicate only what - // > their values would have been if the request method had - // > been GET (Section 4.3.1 of [RFC7231]). 2xx (Successful) - // > responses to a CONNECT request method (Section 4.3.6 of - // > [RFC7231]) switch to tunnel mode instead of having a - // > message body. All 1xx (Informational), 204 (No - // > Content), and 304 (Not Modified) responses do not - // > include a message body. All other responses do include - // > a message body, although the body might be of zero - // > length. - // Format the status-line (RFC 7230 section 3.1.2). + self.buf + .extend_from_slice(response.version().as_str().as_bytes()); + self.buf.push(b' '); + self.buf + .extend_from_slice(itoa_buf.format(response.status().0).as_bytes()); // NOTE: we're not sending a reason-phrase, but the space is required // before \r\n. - write!( - &mut self.buf, - "{} {} \r\n", - response.version(), - response.status() - ) - .unwrap(); + self.buf.extend_from_slice(b" \r\n"); // Format the headers (RFC 7230 section 3.2). let mut set_content_length_header = false; @@ -492,8 +530,9 @@ impl Connection { for header in response.headers().iter() { let name = header.name(); // Field-name: + self.buf.extend_from_slice(name.as_ref().as_bytes()); // NOTE: spacing after the colon (`:`) is optional. - write!(&mut self.buf, "{}: ", name).unwrap(); + self.buf.extend_from_slice(b": "); // Append the header's value. // NOTE: `header.value` shouldn't contain CRLF (`\r\n`). self.buf.extend_from_slice(header.value()); @@ -514,20 +553,16 @@ impl Connection { // Provide the "Conent-Length" header if the user didn't. if !set_content_length_header { - let body_length = if let Some(Method::Head) = self.last_method { - // RFC 7231 section 4.3.2: - // > The HEAD method is identical to GET except that the server - // > MUST NOT send a message body in the response (i.e., the - // > response terminates at the end of the header section). - 0 - } else { - match response.body().length() { - BodyLength::Known(length) => length, - BodyLength::Chunked => todo!("chunked response body"), - } + let body_length = match response.body().length() { + _ if !request_method.expects_body() || !response.status().includes_body() => 0, + BodyLength::Known(length) => length, + BodyLength::Chunked => todo!("chunked response body"), }; - write!(&mut self.buf, "Content-Length: {}\r\n", body_length).unwrap(); + self.buf.extend_from_slice(b"Content-Length: "); + self.buf + .extend_from_slice(itoa_buf.format(body_length).as_bytes()); + self.buf.extend_from_slice(b"\r\n"); } // End of the header. @@ -536,7 +571,7 @@ impl Connection { // Write the response to the stream. let head = &self.buf[ignore_end..]; response - .body_mut() + .into_body() .write_response(&mut self.stream, head) .await?; @@ -545,16 +580,6 @@ impl Connection { Ok(()) } - /// Close the connection. - /// - /// This should be called in case of certain [`RequestError`]s, see - /// [`RequestError::should_close`]. It should also be called if a response - /// it returned without a length, that is a response with a Content-Length - /// header and not using chunked transfer encoding. - pub fn close(self) { - drop(self); - } - /// Clear parsed request(s) from the buffer. fn clear_buffer(&mut self) { if self.buf.len() == self.parsed_bytes { @@ -676,7 +701,7 @@ pub enum RequestError { impl RequestError { /// Returns the proper status code for a given error. - pub fn proper_status_code(self) -> StatusCode { + pub const fn proper_status_code(self) -> StatusCode { use RequestError::*; // See the parsing code for various references to the RFC(s) that // determine the values here. @@ -701,7 +726,7 @@ impl RequestError { /// Returns `true` if the connection should be closed based on the error /// (after sending a error response). - pub fn should_close(self) -> bool { + pub const fn should_close(self) -> bool { use RequestError::*; // See the parsing code for various references to the RFC(s) that // determine the values here. From 8d914c4402a78394ddb21519827563cbf296562b Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 18:16:36 +0200 Subject: [PATCH 34/81] Expand server::Body API Removes Body::ignore, which is not handled when the type is dropped. Adds an implementation of crate::body::Body for server::Body so it can be used e.g. in proxies. --- http/src/header.rs | 2 +- http/src/server.rs | 200 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 154 insertions(+), 48 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index d5ea59b9b..b8524dd0d 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -24,7 +24,7 @@ pub struct Headers { struct HeaderPart { name: HeaderName<'static>, - /// Indices into `Headers.data`. + /// Indices into `Headers.values`. start: usize, end: usize, } diff --git a/http/src/server.rs b/http/src/server.rs index b6ade4a94..4d0ff1463 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -3,6 +3,14 @@ // // TODO: Continue reading RFC 7230 section 4 Transfer Codings. // +// TODO: RFC 7230 section 3.4 Handling Incomplete Messages. +// +// TODO: RFC 7230 section 3.3.3 point 5: +// > If the sender closes the connection or the recipient +// > times out before the indicated number of octets are +// > received, the recipient MUST consider the message to be +// > incomplete and close the connection. +// // TODO: chunked encoding. // TODO: reading request body. @@ -24,17 +32,16 @@ use crate::{FromBytes, HeaderName, Headers, Method, Request, Response, StatusCod /// Maximum size of the header (the start line and the headers). /// -/// RFC 7230 section 3.1.1 recommends ``all HTTP senders and recipients support, -/// at a minimum, request-line lengths of 8000 octets.'' +/// RFC 7230 section 3.1.1 recommends "all HTTP senders and recipients support, +/// at a minimum, request-line lengths of 8000 octets." pub const MAX_HEADER_SIZE: usize = 16384; -/// Maximum number of headers parsed for each request. +/// Maximum number of headers parsed from a single request. pub const MAX_HEADERS: usize = 64; /// Minimum amount of bytes read from the connection or the buffer will be /// grown. -#[allow(dead_code)] // FIXME: use this in reading. -const MIN_READ_SIZE: usize = 512; +const MIN_READ_SIZE: usize = 4096; /// Size of the buffer used in [`Connection`]. const BUF_SIZE: usize = 8192; @@ -188,6 +195,7 @@ where } } +#[derive(Debug)] pub struct Connection { stream: TcpStream, buf: Vec, @@ -375,7 +383,6 @@ impl Connection { let body = Body { conn: self, - size, left: size, }; return Ok(Ok(Some(Request::new(method, path, version, headers, body)))); @@ -606,70 +613,172 @@ const fn map_version(version: u8) -> Version { } /// Body of HTTP [`Request`] read from a [`Connection`]. +/// +/// # Notes +/// +/// If the body is not (completely) read it's still removed from the +/// `Connection`. +#[derive(Debug)] pub struct Body<'a> { conn: &'a mut Connection, - /// Total size of the HTTP body. - size: usize, /// Number of unread (by the user) bytes. left: usize, } impl<'a> Body<'a> { - // TODO: RFC 7230 section 3.4 Handling Incomplete Messages. - - // TODO: RFC 7230 section 3.3.3 point 5: - // > If the sender closes the connection or the recipient - // > times out before the indicated number of octets are - // > received, the recipient MUST consider the message to be - // > incomplete and close the connection. - - /// Returns the size of the body in bytes. + /// Returns the length of the body (in bytes) *left*. /// /// The returned value is based on the "Content-Length" header, or 0 if not /// present. - pub fn len(&self) -> usize { - self.size - } - - /// Returns the number of bytes left in the body. - pub fn left(&self) -> usize { + pub const fn len(&self) -> usize { self.left } /// Returns `true` if the body is completely read (or was empty to begin /// with). - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.left == 0 } - /// Ignore the body, but removes it from the connection. - pub fn ignore(&mut self) -> io::Result<()> { + /// Returns the bytes currently in the buffer. + /// This is limited to the bytes of this request, i.e. it doesn't contain + fn buf_bytes(&self) -> &[u8] { + let bytes = &self.conn.buf[self.conn.parsed_bytes..]; + if bytes.len() > self.left { + &bytes[..self.left] + } else { + bytes + } + } + + /// Mark `n` bytes are processed. + fn processed(&mut self, n: usize) { + self.left -= n; + self.conn.parsed_bytes += n; + } +} + +impl<'a> crate::Body<'a> for Body<'a> { + fn length(&self) -> BodyLength { + BodyLength::Known(self.left) + } +} + +mod private { + use std::future::Future; + use std::io; + use std::pin::Pin; + use std::task::{self, Poll}; + + use heph::net::TcpStream; + + use super::{Body, MIN_READ_SIZE}; + + #[derive(Debug)] + pub struct SendBody<'c, 's, 'h> { + pub(super) body: Body<'c>, + /// Stream we're writing the body to. + pub(super) stream: &'s mut TcpStream, + /// HTTP head for the response. + pub(super) head: &'h [u8], + } + + impl<'c, 's, 'h> Future for SendBody<'c, 's, 'h> { + type Output = io::Result<()>; + + fn poll(self: Pin<&mut Self>, _: &mut task::Context<'_>) -> Poll { + let SendBody { body, stream, head } = Pin::into_inner(self); + + // Send the HTTP head first. + // TODO: try to use vectored I/O on first call. + while !head.is_empty() { + match stream.try_send(head) { + Ok(0) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), + Ok(n) => *head = &head[n..], + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + return Poll::Pending + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + + while body.left != 0 { + let bytes = body.buf_bytes(); + // TODO: maybe read first if we have less then N bytes? + if !bytes.is_empty() { + match stream.try_send(bytes) { + Ok(0) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), + Ok(n) => { + body.processed(n); + continue; + } + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + return Poll::Pending + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + + // Ensure we have space in the buffer to read into. + body.conn.clear_buffer(); + body.conn.buf.reserve(MIN_READ_SIZE); + match body.conn.stream.try_recv(&mut body.conn.buf) { + Ok(0) => return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())), + // Continue to sending the bytes above. + Ok(_) => continue, + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + return Poll::Pending + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + + Poll::Ready(Ok(())) + } + } +} + +impl<'c> crate::body::PrivateBody<'c> for Body<'c> { + type WriteBody<'s, 'h> = private::SendBody<'c, 's, 'h>; + + fn write_response<'s, 'h>( + self, + stream: &'s mut TcpStream, + head: &'h [u8], + ) -> Self::WriteBody<'s, 'h> + where + 'c: 'h, + { + private::SendBody { + body: self, + stream, + head, + } + } +} + +impl<'a> Drop for Body<'a> { + fn drop(&mut self) { if self.is_empty() { // Empty body, then we're done quickly. - return Ok(()); + return; } - let ignored_len = self.conn.parsed_bytes + self.size; + let ignored_len = self.conn.parsed_bytes + self.left; if self.conn.buf.len() >= ignored_len { // Entire body was already read we can skip the bytes. self.conn.parsed_bytes = ignored_len; - return Ok(()); + return; } - // TODO: read more bytes from the stream. + // TODO: mark more bytes as ignored in `Connection`. todo!("ignore the body: read more bytes") - // NOTE: conn.clear_buffer } } -/* TODO: read entire body? maybe an assertion? -impl<'a> Drop for Body<'a> { - fn drop(&mut self) { - todo!() - } -} -*/ - /// Error parsing HTTP request. #[derive(Copy, Clone, Debug)] pub enum RequestError { @@ -778,15 +887,12 @@ impl fmt::Display for RequestError { } } -/// The message type used by [`HttpServer`]. +/// The message type used by [`HttpServer`] (and [`TcpServer`]). /// -/// The message implements [`From`]`<`[`Terminate`]`>` and -/// [`TryFrom`]`<`[`Signal`]`>` for the message, allowing for graceful shutdown. -/// -/// [`Terminate`]: heph::actor::messages::Terminate -/// [`TryFrom`]: std::convert::TryFrom -/// [`Signal`]: heph::rt::Signal +#[doc(inline)] pub use heph::net::tcp::server::Message; -/// Error returned by the [`HttpServer`] actor. +/// Error returned by [`HttpServer`] (and [`TcpServer`]). +/// +#[doc(inline)] pub use heph::net::tcp::server::Error; From 150f8ea10c7602fa81d22f641f9c62c1e59f27ce Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 21:05:16 +0200 Subject: [PATCH 35/81] Add server::Body:{recv, recv_vectored} --- http/src/lib.rs | 10 +++- http/src/server.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/http/src/lib.rs b/http/src/lib.rs index d3c268215..51d14a552 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -1,4 +1,12 @@ -#![feature(async_stream, const_panic, generic_associated_types, io_slice_advance)] +#![feature( + async_stream, + const_fn, + const_mut_refs, + const_panic, + generic_associated_types, + io_slice_advance, + maybe_uninit_write_slice +)] #![allow(incomplete_features)] // NOTE: for `generic_associated_types`. pub mod body; diff --git a/http/src/server.rs b/http/src/server.rs index 4d0ff1463..b39b76af7 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -14,14 +14,17 @@ // TODO: chunked encoding. // TODO: reading request body. +use std::cmp::min; use std::fmt; +use std::future::Future; use std::io::{self, Write}; +use std::mem::MaybeUninit; use std::net::SocketAddr; use std::pin::Pin; use std::task::{self, Poll}; use std::time::SystemTime; -use heph::net::{tcp, TcpServer, TcpStream}; +use heph::net::{tcp, Bytes, BytesVectored, TcpServer, TcpStream}; use heph::spawn::{ActorOptions, Spawn}; use heph::{actor, rt, Actor, NewActor, Supervisor}; use httparse::EMPTY_HEADER; @@ -640,6 +643,22 @@ impl<'a> Body<'a> { self.left == 0 } + /// Receive bytes from the request body, writing them into `buf`. + pub const fn recv(&'a mut self, buf: B) -> Recv<'a, B> + where + B: Bytes, + { + Recv { body: self, buf } + } + + /// Receive bytes from the request body, writing them into `bufs`. + pub const fn recv_vectored(&'a mut self, bufs: B) -> RecvVectored<'a, B> + where + B: BytesVectored, + { + RecvVectored { body: self, bufs } + } + /// Returns the bytes currently in the buffer. /// This is limited to the bytes of this request, i.e. it doesn't contain fn buf_bytes(&self) -> &[u8] { @@ -651,6 +670,20 @@ impl<'a> Body<'a> { } } + /// Copy already read bytes. + fn copy_buf_bytes(&mut self, dst: &mut [MaybeUninit]) -> usize { + let bytes = self.buf_bytes(); + let len = bytes.len(); + if len != 0 { + let len = min(len, dst.len()); + MaybeUninit::write_slice(&mut dst[..len], &bytes[..len]); + self.processed(len); + len + } else { + 0 + } + } + /// Mark `n` bytes are processed. fn processed(&mut self, n: usize) { self.left -= n; @@ -658,6 +691,106 @@ impl<'a> Body<'a> { } } +/// The [`Future`] behind [`Body::recv`]. +#[derive(Debug)] +#[must_use = "futures do nothing unless you `.await` or poll them"] +pub struct Recv<'b, B> { + body: &'b mut Body<'b>, + buf: B, +} + +impl<'b, B> Future for Recv<'b, B> +where + B: Bytes + Unpin, +{ + type Output = io::Result; + + fn poll(self: Pin<&mut Self>, _: &mut task::Context<'_>) -> Poll { + let Recv { body, buf } = Pin::into_inner(self); + + // Copy already read bytes. + let len = body.copy_buf_bytes(buf.as_bytes()); + if len != 0 { + unsafe { buf.update_length(len) }; + } + + // Read from the stream if there is space left. + if !buf.as_bytes().is_empty() { + loop { + match body.conn.stream.try_recv(&mut *buf) { + Ok(n) => return Poll::Ready(Ok(len + n)), + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + if len != 0 { + return Poll::Ready(Ok(len)); + } else { + return Poll::Pending; + } + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + } + Poll::Ready(Ok(len)) + } +} + +/// The [`Future`] behind [`Body::recv_vectored`]. +#[derive(Debug)] +#[must_use = "futures do nothing unless you `.await` or poll them"] +pub struct RecvVectored<'b, B> { + body: &'b mut Body<'b>, + bufs: B, +} + +impl<'b, B> Future for RecvVectored<'b, B> +where + B: BytesVectored + Unpin, +{ + type Output = io::Result; + + fn poll(self: Pin<&mut Self>, _: &mut task::Context<'_>) -> Poll { + let RecvVectored { body, bufs } = Pin::into_inner(self); + + // Copy already read bytes. + let mut len = 0; + for buf in bufs.as_bufs().as_mut() { + match body.copy_buf_bytes(buf) { + 0 => break, + n => len += n, + } + } + if len != 0 { + unsafe { bufs.update_lengths(len) }; + } + + // Read from the stream if there is space left. + let buf_len = bufs + .as_bufs() + .as_mut() + .iter() + .map(|b| b.len()) + .sum::(); + if buf_len != 0 { + loop { + match body.conn.stream.try_recv_vectored(&mut *bufs) { + Ok(n) => return Poll::Ready(Ok(len + n)), + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { + if len != 0 { + return Poll::Ready(Ok(len)); + } else { + return Poll::Pending; + } + } + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + } + Poll::Ready(Ok(len)) + } +} + impl<'a> crate::Body<'a> for Body<'a> { fn length(&self) -> BodyLength { BodyLength::Known(self.left) From 0b9c2dad98af76ca044abb5fc9865093b26f33ed Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 21:06:09 +0200 Subject: [PATCH 36/81] Update my_ip HTTP example --- http/examples/my_ip.rs | 51 +++++++++++++++++++++--------------------- http/src/body.rs | 4 ++-- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index c09ed9470..9e492e0c2 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -73,50 +73,51 @@ async fn http_actor( mut connection: http::Connection, address: SocketAddr, ) -> io::Result<()> { - info!("accepted connection: address={}", address); + info!("accepted connection: source={}", address); connection.set_nodelay(true)?; loop { - match connection.next_request().await? { - Ok(Some(mut request)) => { - info!("received request: {:?}", request); - let mut headers = Headers::EMPTY; - let (code, body) = if request.path() != "/" { - request.body_mut().ignore()?; - (StatusCode::NOT_FOUND, "Not found".into()) + let mut headers = Headers::EMPTY; + let (code, body, should_close) = match connection.next_request().await? { + Ok(Some(request)) => { + info!("received request: {:?}: source={}", request, address); + if request.path() != "/" { + (StatusCode::NOT_FOUND, "Not found".into(), false) } else if !matches!(request.method(), Method::Get | Method::Head) { - request.body_mut().ignore()?; headers.add(Header::new(HeaderName::ALLOW, b"GET, HEAD")); - (StatusCode::METHOD_NOT_ALLOWED, "Method not allowed".into()) + let body = "Method not allowed".into(); + (StatusCode::METHOD_NOT_ALLOWED, body, false) } else if request.body().len() != 0 { - request.body_mut().ignore()?; let body = Cow::from("Not expecting a body"); - (StatusCode::PAYLOAD_TOO_LARGE, body) + (StatusCode::PAYLOAD_TOO_LARGE, body, true) } else { // This will allocate a new string which isn't the most // efficient way to do this, but it's the easiest so we'll // keep this for sake of example. let body = Cow::from(address.ip().to_string()); - (StatusCode::OK, body) - }; - debug!("sending response: code={}, body='{}'", code, body); - let body = OneshotBody::new(body.as_bytes()); - connection.respond(code, headers, body).await?; + (StatusCode::OK, body, true) + } } // No more requests. Ok(None) => return Ok(()), Err(err) => { warn!("error reading request: {}: source={}", err, address); let code = err.proper_status_code(); - let body = format!("Bad request: {}", err); - debug!("sending erorr response: code={}, body='{}'", code, body); - let body = OneshotBody::new(body.as_bytes()); - connection.respond(code, Headers::EMPTY, body).await?; - if err.should_close() { - warn!("closing connection after error: {}", err); - return Ok(()); - } + let body = Cow::from(format!("Bad request: {}", err)); + (code, body, err.should_close()) } + }; + + debug!( + "sending response: code={}, body='{}', source={}", + code, body, address + ); + let body = OneshotBody::new(body.as_bytes()); + connection.respond(code, headers, body).await?; + + if should_close { + warn!("closing connection: source={}", address); + return Ok(()); } } } diff --git a/http/src/body.rs b/http/src/body.rs index 9b229aed8..1af22eb23 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -232,8 +232,8 @@ mod private { } } -pub(crate) use private::PrivateBody; -use private::{SendFileBody, SendOneshotBody, SendStreamingBody}; +pub(crate) use private::{PrivateBody, SendStreamingBody}; +use private::{SendFileBody, SendOneshotBody}; /// An empty body. #[derive(Debug)] From 2a345cc5695e8fe926f82731c0e0831570955d46 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 21:27:43 +0200 Subject: [PATCH 37/81] Add Bytes::{spare_capacity, has_spare_capacity} Returns the capacity of the available buffer. --- src/net/mod.rs | 34 ++++++++++++++++++++++++++++++++++ tests/functional/bytes.rs | 15 +++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/net/mod.rs b/src/net/mod.rs index 76a7b8257..ae2082b8c 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -93,6 +93,16 @@ pub trait Bytes { /// [`update_length`]: Bytes::update_length fn as_bytes(&mut self) -> &mut [MaybeUninit]; + /// Returns the length of the buffer as returned by [`as_bytes`]. + /// + /// [`as_bytes`]: Bytes::as_bytes + fn spare_capacity(&self) -> usize; + + /// Returns `true` if the buffer has spare capacity. + fn has_spare_capacity(&self) -> bool { + self.spare_capacity() == 0 + } + /// Update the length of the byte slice, marking `n` bytes as initialised. /// /// # Safety @@ -118,6 +128,14 @@ where (&mut **self).as_bytes() } + fn spare_capacity(&self) -> usize { + (&**self).spare_capacity() + } + + fn has_spare_capacity(&self) -> bool { + (&**self).has_spare_capacity() + } + unsafe fn update_length(&mut self, n: usize) { (&mut **self).update_length(n) } @@ -160,6 +178,14 @@ impl Bytes for Vec { self.spare_capacity_mut() } + fn spare_capacity(&self) -> usize { + self.capacity() - self.len() + } + + fn has_spare_capacity(&self) -> bool { + self.capacity() != self.len() + } + unsafe fn update_length(&mut self, n: usize) { let new = self.len() + n; debug_assert!(self.capacity() >= new); @@ -351,6 +377,14 @@ impl<'a> DerefMut for MaybeUninitSlice<'a> { /// &mut self.bytes[self.initialised..] /// } /// +/// fn spare_capacity(&self) -> usize { +/// self.bytes.len() - self.initialised +/// } +/// +/// fn has_spare_capacity(&self) -> bool { +/// self.bytes.len() != self.initialised +/// } +/// /// unsafe fn update_length(&mut self, n: usize) { /// self.initialised += n; /// } diff --git a/tests/functional/bytes.rs b/tests/functional/bytes.rs index d9c656c90..dfa7b6037 100644 --- a/tests/functional/bytes.rs +++ b/tests/functional/bytes.rs @@ -12,7 +12,9 @@ fn write_bytes(src: &[u8], mut buf: B) -> usize where B: Bytes, { + let spare_capacity = buf.spare_capacity(); let dst = buf.as_bytes(); + assert_eq!(dst.len(), spare_capacity); let len = min(src.len(), dst.len()); // Safety: both the `src` and `dst` pointers are good. And we've ensured // that the length is correct, not overwriting data we don't own or reading @@ -52,22 +54,35 @@ where #[test] fn impl_for_vec() { let mut buf = Vec::::with_capacity(2 * DATA.len()); + assert_eq!(buf.spare_capacity(), 2 * DATA.len()); + assert!(buf.has_spare_capacity()); let n = write_bytes(DATA, &mut buf); assert_eq!(n, DATA.len()); assert_eq!(buf.len(), DATA.len()); assert_eq!(&*buf, DATA); + assert_eq!(buf.spare_capacity(), DATA.len()); + assert!(buf.has_spare_capacity()); } #[test] fn dont_overwrite_existing_bytes_in_vec() { let mut buf = Vec::::with_capacity(2 * DATA.len()); + assert_eq!(buf.spare_capacity(), 2 * DATA.len()); + assert!(buf.has_spare_capacity()); buf.extend(DATA2); + assert_eq!(buf.spare_capacity(), 2 * DATA.len() - DATA2.len()); + assert!(buf.has_spare_capacity()); let start = buf.len(); let n = write_bytes(DATA, &mut buf); assert_eq!(n, DATA.len()); assert_eq!(buf.len(), DATA2.len() + DATA.len()); assert_eq!(&buf[..start], DATA2); // Original bytes untouched. assert_eq!(&buf[start..start + n], DATA); + assert_eq!(buf.spare_capacity(), 1); + assert!(buf.has_spare_capacity()); + buf.push(b'a'); + assert_eq!(buf.spare_capacity(), 0); + assert!(!buf.has_spare_capacity()); } #[test] From 8506056e3f558b5005979798dea57df13803b635 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 21:39:57 +0200 Subject: [PATCH 38/81] Add BytesVectored::{spare_capacity, has_spare_capacity} Returns the total capacity of the available buffers. --- src/net/mod.rs | 34 ++++++++++++++++++++++++++++++++++ tests/functional/bytes.rs | 14 ++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/net/mod.rs b/src/net/mod.rs index ae2082b8c..8aefd5ea2 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -397,6 +397,16 @@ pub trait BytesVectored { /// Returns itself as a slice of [`MaybeUninitSlice`]. fn as_bufs<'b>(&'b mut self) -> Self::Bufs<'b>; + /// Returns the total length of the buffers as returned by [`as_bufs`]. + /// + /// [`as_bufs`]: BytesVectored::as_bufs + fn spare_capacity(&self) -> usize; + + /// Returns `true` if (one of) the buffers has spare capacity. + fn has_spare_capacity(&self) -> bool { + self.spare_capacity() == 0 + } + /// Update the length of the buffers in the slice. /// /// # Safety @@ -425,6 +435,14 @@ where (&mut **self).as_bufs() } + fn spare_capacity(&self) -> usize { + (&**self).spare_capacity() + } + + fn has_spare_capacity(&self) -> bool { + (&**self).has_spare_capacity() + } + unsafe fn update_lengths(&mut self, n: usize) { (&mut **self).update_lengths(n) } @@ -445,6 +463,14 @@ where unsafe { MaybeUninit::array_assume_init(bufs) } } + fn spare_capacity(&self) -> usize { + self.iter().map(|b| b.spare_capacity()).sum() + } + + fn has_spare_capacity(&self) -> bool { + self.iter().any(|b| b.has_spare_capacity()) + } + unsafe fn update_lengths(&mut self, n: usize) { let mut left = n; for buf in self.iter_mut() { @@ -474,6 +500,14 @@ macro_rules! impl_vectored_bytes_tuple { unsafe { MaybeUninit::array_assume_init(bufs) } } + fn spare_capacity(&self) -> usize { + $( self.$idx.spare_capacity() + )+ 0 + } + + fn has_spare_capacity(&self) -> bool { + $( self.$idx.has_spare_capacity() || )+ false + } + unsafe fn update_lengths(&mut self, n: usize) { let mut left = n; $( diff --git a/tests/functional/bytes.rs b/tests/functional/bytes.rs index dfa7b6037..c19d776e3 100644 --- a/tests/functional/bytes.rs +++ b/tests/functional/bytes.rs @@ -88,12 +88,19 @@ fn dont_overwrite_existing_bytes_in_vec() { #[test] fn vectored_array() { let mut bufs = [Vec::with_capacity(1), Vec::with_capacity(DATA.len())]; + assert_eq!(bufs.spare_capacity(), 1 + DATA.len()); + assert!(bufs.has_spare_capacity()); let n = write_bytes_vectored(DATA, &mut bufs); assert_eq!(n, DATA.len()); assert_eq!(bufs[0].len(), 1); assert_eq!(bufs[1].len(), DATA.len() - 1); assert_eq!(bufs[0], &DATA[..1]); assert_eq!(bufs[1], &DATA[1..]); + assert_eq!(bufs.spare_capacity(), 1); + assert!(bufs.has_spare_capacity()); + bufs[1].push(b'a'); + assert_eq!(bufs.spare_capacity(), 0); + assert!(!bufs.has_spare_capacity()); } #[test] @@ -103,6 +110,8 @@ fn vectored_tuple() { Vec::with_capacity(3), Vec::with_capacity(DATA.len()), ); + assert_eq!(bufs.spare_capacity(), 1 + 3 + DATA.len()); + assert!(bufs.has_spare_capacity()); let n = write_bytes_vectored(DATA, &mut bufs); assert_eq!(n, DATA.len()); assert_eq!(bufs.0.len(), 1); @@ -111,4 +120,9 @@ fn vectored_tuple() { assert_eq!(bufs.0, &DATA[..1]); assert_eq!(bufs.1, &DATA[1..4]); assert_eq!(bufs.2, &DATA[4..]); + assert_eq!(bufs.spare_capacity(), 4); + assert!(bufs.has_spare_capacity()); + bufs.2.extend_from_slice(b"aaaa"); + assert_eq!(bufs.spare_capacity(), 0); + assert!(!bufs.has_spare_capacity()); } From 2139caf84a68f03c849765d4f45c72638d9ca109 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 May 2021 21:47:15 +0200 Subject: [PATCH 39/81] Use BytesVectored::has_spare_capacity when possible --- http/src/server.rs | 8 +------- src/net/tcp/stream.rs | 11 ++++++----- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/http/src/server.rs b/http/src/server.rs index b39b76af7..6f06874c1 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -765,13 +765,7 @@ where } // Read from the stream if there is space left. - let buf_len = bufs - .as_bufs() - .as_mut() - .iter() - .map(|b| b.len()) - .sum::(); - if buf_len != 0 { + if bufs.has_spare_capacity() { loop { match body.conn.stream.try_recv_vectored(&mut *bufs) { Ok(n) => return Poll::Ready(Ok(len + n)), diff --git a/src/net/tcp/stream.rs b/src/net/tcp/stream.rs index 9b9ddd801..fd1e6e66f 100644 --- a/src/net/tcp/stream.rs +++ b/src/net/tcp/stream.rs @@ -365,19 +365,20 @@ impl TcpStream { where B: BytesVectored, { + debug_assert!( + bufs.has_spare_capacity(), + "called `TcpStream::recv_vectored` with an empty buffer" + ); RecvVectored { stream: self, bufs } } /// Receive at least `n` bytes from the stream, writing them into `bufs`. - pub fn recv_n_vectored(&mut self, mut bufs: B, n: usize) -> RecvNVectored<'_, B> + pub fn recv_n_vectored(&mut self, bufs: B, n: usize) -> RecvNVectored<'_, B> where B: BytesVectored, { debug_assert!( - { - let mut dst = bufs.as_bufs(); - !dst.as_mut().iter().map(|buf| buf.len()).sum::() >= n - }, + !bufs.spare_capacity() >= n, "called `TcpStream::recv_n_vectored` with a buffer smaller then `n`" ); RecvNVectored { From 397707b6f98b3d68f6d4a4c4c96601a23d3e1313 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 7 May 2021 12:07:41 +0200 Subject: [PATCH 40/81] Use Bytes(Vectored)::{spare_capacity, has_spare_capacity} more --- src/net/mod.rs | 4 +-- src/net/tcp/stream.rs | 60 ++++++++++++++++----------------- src/net/udp.rs | 78 ++++++++++++++++++++----------------------- 3 files changed, 68 insertions(+), 74 deletions(-) diff --git a/src/net/mod.rs b/src/net/mod.rs index 8aefd5ea2..511f7a771 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -474,7 +474,7 @@ where unsafe fn update_lengths(&mut self, n: usize) { let mut left = n; for buf in self.iter_mut() { - let n = min(left, buf.as_bytes().len()); + let n = min(left, buf.spare_capacity()); buf.update_length(n); left -= n; if left == 0 { @@ -511,7 +511,7 @@ macro_rules! impl_vectored_bytes_tuple { unsafe fn update_lengths(&mut self, n: usize) { let mut left = n; $( - let n = min(left, self.$idx.as_bytes().len()); + let n = min(left, self.$idx.spare_capacity()); self.$idx.update_length(n); left -= n; if left == 0 { diff --git a/src/net/tcp/stream.rs b/src/net/tcp/stream.rs index fd1e6e66f..fd0336f63 100644 --- a/src/net/tcp/stream.rs +++ b/src/net/tcp/stream.rs @@ -236,16 +236,17 @@ impl TcpStream { where B: Bytes, { - let dst = buf.as_bytes(); debug_assert!( - !dst.is_empty(), + buf.has_spare_capacity(), "called `TcpStream::try_recv with an empty buffer" ); - SockRef::from(&self.socket).recv(dst).map(|read| { - // Safety: just read the bytes. - unsafe { buf.update_length(read) } - read - }) + SockRef::from(&self.socket) + .recv(buf.as_bytes()) + .map(|read| { + // Safety: just read the bytes. + unsafe { buf.update_length(read) } + read + }) } /// Receive messages from the stream, writing them into `buf`. @@ -314,12 +315,12 @@ impl TcpStream { /// # /// # drop(actor); // Silent dead code warnings. /// ``` - pub fn recv_n<'a, B>(&'a mut self, mut buf: B, n: usize) -> RecvN<'a, B> + pub fn recv_n<'a, B>(&'a mut self, buf: B, n: usize) -> RecvN<'a, B> where B: Bytes, { debug_assert!( - buf.as_bytes().len() >= n, + buf.spare_capacity() >= n, "called `TcpStream::recv_n` with a buffer smaller then `n`" ); RecvN { @@ -342,16 +343,14 @@ impl TcpStream { where B: BytesVectored, { - let mut dst = bufs.as_bufs(); debug_assert!( - dst.as_mut().iter().any(|buf| !buf.is_empty()), - "called `UdpSocket::try_recv_vectored` with an empty buffers" + bufs.has_spare_capacity(), + "called `UdpSocket::try_recv_vectored` with empty buffers" ); - let res = - SockRef::from(&self.socket).recv_vectored(MaybeUninitSlice::as_socket2(dst.as_mut())); + let res = SockRef::from(&self.socket) + .recv_vectored(MaybeUninitSlice::as_socket2(bufs.as_bufs().as_mut())); match res { Ok((read, _)) => { - drop(dst); // Safety: just read the bytes. unsafe { bufs.update_lengths(read) } Ok(read) @@ -367,7 +366,7 @@ impl TcpStream { { debug_assert!( bufs.has_spare_capacity(), - "called `TcpStream::recv_vectored` with an empty buffer" + "called `TcpStream::recv_vectored` with empty buffers" ); RecvVectored { stream: self, bufs } } @@ -378,7 +377,7 @@ impl TcpStream { B: BytesVectored, { debug_assert!( - !bufs.spare_capacity() >= n, + bufs.spare_capacity() >= n, "called `TcpStream::recv_n_vectored` with a buffer smaller then `n`" ); RecvNVectored { @@ -395,16 +394,17 @@ impl TcpStream { where B: Bytes, { - let dst = buf.as_bytes(); debug_assert!( - !dst.is_empty(), + buf.has_spare_capacity(), "called `TcpStream::try_peek with an empty buffer" ); - SockRef::from(&self.socket).peek(dst).map(|read| { - // Safety: just read the bytes. - unsafe { buf.update_length(read) } - read - }) + SockRef::from(&self.socket) + .peek(buf.as_bytes()) + .map(|read| { + // Safety: just read the bytes. + unsafe { buf.update_length(read) } + read + }) } /// Receive messages from the stream, writing them into `buf`, without @@ -424,16 +424,16 @@ impl TcpStream { where B: BytesVectored, { - let mut dst = bufs.as_bufs(); debug_assert!( - dst.as_mut().iter().any(|buf| !buf.is_empty()), - "called `UdpSocket::try_peek_vectored` with an empty buffer" + bufs.has_spare_capacity(), + "called `UdpSocket::try_peek_vectored` with empty buffers" + ); + let res = SockRef::from(&self.socket).recv_vectored_with_flags( + MaybeUninitSlice::as_socket2(bufs.as_bufs().as_mut()), + libc::MSG_PEEK, ); - let res = SockRef::from(&self.socket) - .recv_vectored_with_flags(MaybeUninitSlice::as_socket2(dst.as_mut()), libc::MSG_PEEK); match res { Ok((read, _)) => { - drop(dst); // Safety: just read the bytes. unsafe { bufs.update_lengths(read) } Ok(read) diff --git a/src/net/udp.rs b/src/net/udp.rs index b63a9f55b..c380c5f72 100644 --- a/src/net/udp.rs +++ b/src/net/udp.rs @@ -263,13 +263,12 @@ impl UdpSocket { where B: Bytes, { - let dst = buf.as_bytes(); debug_assert!( - !dst.is_empty(), + buf.has_spare_capacity(), "called `UdpSocket::try_recv_from` with an empty buffer" ); SockRef::from(&self.socket) - .recv_from(dst) + .recv_from(buf.as_bytes()) .and_then(|(read, address)| { // Safety: just read the bytes. unsafe { buf.update_length(read) } @@ -300,16 +299,14 @@ impl UdpSocket { where B: BytesVectored, { - let mut dst = bufs.as_bufs(); debug_assert!( - !dst.as_mut().first().map_or(true, |buf| buf.is_empty()), - "called `UdpSocket::try_recv_from` with an empty buffer" + bufs.has_spare_capacity(), + "called `UdpSocket::try_recv_from` with empty buffers" ); let res = SockRef::from(&self.socket) - .recv_from_vectored(MaybeUninitSlice::as_socket2(dst.as_mut())); + .recv_from_vectored(MaybeUninitSlice::as_socket2(bufs.as_bufs().as_mut())); match res { Ok((read, _, address)) => { - drop(dst); // Safety: just read the bytes. unsafe { bufs.update_lengths(read) } let address = convert_address(address)?; @@ -341,13 +338,12 @@ impl UdpSocket { where B: Bytes, { - let dst = buf.as_bytes(); debug_assert!( - !dst.is_empty(), + buf.has_spare_capacity(), "called `UdpSocket::try_peek_from` with an empty buffer" ); SockRef::from(&self.socket) - .peek_from(dst) + .peek_from(buf.as_bytes()) .and_then(|(read, address)| { // Safety: just read the bytes. unsafe { buf.update_length(read) } @@ -379,18 +375,16 @@ impl UdpSocket { where B: BytesVectored, { - let mut dst = bufs.as_bufs(); debug_assert!( - !dst.as_mut().first().map_or(true, |buf| buf.is_empty()), - "called `UdpSocket::try_peek_from_vectored` with an empty buffer" + bufs.has_spare_capacity(), + "called `UdpSocket::try_peek_from_vectored` with empty buffers" ); let res = SockRef::from(&self.socket).recv_from_vectored_with_flags( - MaybeUninitSlice::as_socket2(dst.as_mut()), + MaybeUninitSlice::as_socket2(bufs.as_bufs().as_mut()), libc::MSG_PEEK, ); match res { Ok((read, _, address)) => { - drop(dst); // Safety: just read the bytes. unsafe { bufs.update_lengths(read) } let address = convert_address(address)?; @@ -591,16 +585,17 @@ impl UdpSocket { where B: Bytes, { - let dst = buf.as_bytes(); debug_assert!( - !dst.is_empty(), + buf.has_spare_capacity(), "called `UdpSocket::try_recv` with an empty buffer" ); - SockRef::from(&self.socket).recv(dst).map(|read| { - // Safety: just read the bytes. - unsafe { buf.update_length(read) } - read - }) + SockRef::from(&self.socket) + .recv(buf.as_bytes()) + .map(|read| { + // Safety: just read the bytes. + unsafe { buf.update_length(read) } + read + }) } /// Receives data from the socket. Returns a [`Future`] that on success @@ -624,16 +619,14 @@ impl UdpSocket { where B: BytesVectored, { - let mut dst = bufs.as_bufs(); debug_assert!( - !dst.as_mut().first().map_or(true, |buf| buf.is_empty()), - "called `UdpSocket::try_recv_vectored` with an empty buffer" + bufs.has_spare_capacity(), + "called `UdpSocket::try_recv_vectored` with empty buffers" ); - let res = - SockRef::from(&self.socket).recv_vectored(MaybeUninitSlice::as_socket2(dst.as_mut())); + let res = SockRef::from(&self.socket) + .recv_vectored(MaybeUninitSlice::as_socket2(bufs.as_bufs().as_mut())); match res { Ok((read, _)) => { - drop(dst); // Safety: just read the bytes. unsafe { bufs.update_lengths(read) } Ok(read) @@ -663,16 +656,17 @@ impl UdpSocket { where B: Bytes, { - let dst = buf.as_bytes(); debug_assert!( - !dst.is_empty(), + buf.has_spare_capacity(), "called `UdpSocket::try_peek` with an empty buffer" ); - SockRef::from(&self.socket).peek(dst).map(|read| { - // Safety: just read the bytes. - unsafe { buf.update_length(read) } - read - }) + SockRef::from(&self.socket) + .peek(buf.as_bytes()) + .map(|read| { + // Safety: just read the bytes. + unsafe { buf.update_length(read) } + read + }) } /// Receives data from the socket, without removing it from the input queue. @@ -697,16 +691,16 @@ impl UdpSocket { where B: BytesVectored, { - let mut dst = bufs.as_bufs(); debug_assert!( - !dst.as_mut().first().map_or(true, |buf| buf.is_empty()), - "called `UdpSocket::try_peek_vectored` with an empty buffer" + bufs.has_spare_capacity(), + "called `UdpSocket::try_peek_vectored` with empty buffers" + ); + let res = SockRef::from(&self.socket).recv_vectored_with_flags( + MaybeUninitSlice::as_socket2(bufs.as_bufs().as_mut()), + libc::MSG_PEEK, ); - let res = SockRef::from(&self.socket) - .recv_vectored_with_flags(MaybeUninitSlice::as_socket2(dst.as_mut()), libc::MSG_PEEK); match res { Ok((read, _)) => { - drop(dst); // Safety: just read the bytes. unsafe { bufs.update_lengths(read) } Ok(read) From 1726e2d082d3327b208d8e4793b3377eee8bb9ca Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 7 May 2021 12:09:31 +0200 Subject: [PATCH 41/81] Add const_fn_trait_bound feature Split off from const_fn. --- http/src/lib.rs | 1 + http/src/server.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/http/src/lib.rs b/http/src/lib.rs index 51d14a552..99b7eb95c 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -1,6 +1,7 @@ #![feature( async_stream, const_fn, + const_fn_trait_bound, const_mut_refs, const_panic, generic_associated_types, diff --git a/http/src/server.rs b/http/src/server.rs index 6f06874c1..fae8aebee 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -168,7 +168,8 @@ where } // TODO: better name. Like `TcpStreamToConnection`? -/// Maps `NA` to accept `(TcpStream, SocketAddr)` as argument. +/// Maps `NA` to accept `(TcpStream, SocketAddr)` as argument, creating a +/// [`Connection`]. #[derive(Debug, Clone)] pub struct ArgMap { new_actor: NA, @@ -292,8 +293,9 @@ impl Connection { // Read the entire stream, so we're done. return Ok(Ok(None)); } else { - // Couldn't read any more bytes, but we still have bytes in - // the buffer. This means it contains a partial request. + // Couldn't read any more bytes, but we still have bytes + // in the buffer. This means it contains a partial + // request. return Ok(Err(RequestError::IncompleteRequest)); } } @@ -715,7 +717,7 @@ where } // Read from the stream if there is space left. - if !buf.as_bytes().is_empty() { + if buf.has_spare_capacity() { loop { match body.conn.stream.try_recv(&mut *buf) { Ok(n) => return Poll::Ready(Ok(len + n)), From 225fa078c4cd8cfde393851b85f3dd3492e967e6 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 7 May 2021 15:40:55 +0200 Subject: [PATCH 42/81] Set Connection header for HTTP/1.0 responses --- http/examples/my_ip.rs | 6 +++++- http/src/server.rs | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index 9e492e0c2..8b6257f31 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -95,7 +95,7 @@ async fn http_actor( // efficient way to do this, but it's the easiest so we'll // keep this for sake of example. let body = Cow::from(address.ip().to_string()); - (StatusCode::OK, body, true) + (StatusCode::OK, body, false) } } // No more requests. @@ -108,6 +108,10 @@ async fn http_actor( } }; + if should_close { + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + } + debug!( "sending response: code={}, body='{}', source={}", code, body, address diff --git a/http/src/server.rs b/http/src/server.rs index fae8aebee..cae128997 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -12,7 +12,6 @@ // > incomplete and close the connection. // // TODO: chunked encoding. -// TODO: reading request body. use std::cmp::min; use std::fmt; @@ -199,6 +198,13 @@ where } } +/// HTTP connection. +/// +/// This a TCP stream from which [HTTP requests] are read and [HTTP responses] +/// are send to. +/// +/// [HTTP requests]: Request +/// [HTTP responses]: Response #[derive(Debug)] pub struct Connection { stream: TcpStream, @@ -503,8 +509,8 @@ impl Connection { /// /// # Notes /// - /// This automatically sets the "Content-Length" and "Date" headers if not - /// provided in `response`. + /// This automatically sets the "Content-Length", "Connection" and "Date" + /// headers if not provided in `response`. /// /// If `request_method.`[`expects_body`] or /// `response.status().`[`includes_body`] returns false this will not write @@ -537,6 +543,7 @@ impl Connection { self.buf.extend_from_slice(b" \r\n"); // Format the headers (RFC 7230 section 3.2). + let mut set_connection_header = false; let mut set_content_length_header = false; let mut set_date_header = false; for header in response.headers().iter() { @@ -550,13 +557,23 @@ impl Connection { self.buf.extend_from_slice(header.value()); self.buf.extend_from_slice(b"\r\n"); - if name == &HeaderName::CONTENT_LENGTH { + if name == &HeaderName::CONNECTION { + set_connection_header = true; + } else if name == &HeaderName::CONTENT_LENGTH { set_content_length_header = true; } else if name == &HeaderName::DATE { set_date_header = true; } } + // Provide the "Connection" header if the user didn't. + if !set_connection_header && matches!(response.version(), Version::Http10) { + // Per RFC 7230 section 6.3, HTTP/1.0 needs the "Connection: + // keep-alive" header to persistent the connection. Connections + // using HTTP/1.1 persistent by default. + self.buf.extend_from_slice(b"Connection: keep-alive\r\n"); + } + // Provide the "Date" header if the user didn't. if !set_date_header { let now = HttpDate::from(SystemTime::now()); @@ -621,8 +638,8 @@ const fn map_version(version: u8) -> Version { /// /// # Notes /// -/// If the body is not (completely) read it's still removed from the -/// `Connection`. +/// If the body is not (completely) read before this is dropped it will still +/// removed from the `Connection`. #[derive(Debug)] pub struct Body<'a> { conn: &'a mut Connection, @@ -633,8 +650,15 @@ pub struct Body<'a> { impl<'a> Body<'a> { /// Returns the length of the body (in bytes) *left*. /// - /// The returned value is based on the "Content-Length" header, or 0 if not - /// present. + /// Calling this before [`recv`] or [`recv_vectored`] will return the + /// original body length, after removing bytes from the body this will + /// return the remaining length. + /// + /// The body length is determined by the "Content-Length" header, or 0 if + /// not present. + /// + /// [`recv`]: Body::recv + /// [`recv_vectored`]: Body::recv_vectored pub const fn len(&self) -> usize { self.left } From 61b1617c2168e65773d6208367305e01786da55b Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 7 May 2021 16:09:12 +0200 Subject: [PATCH 43/81] Use checked addition and multiplication in FromBytes for uints --- http/src/from_bytes.rs | 11 +++++++---- http/tests/functional/from_bytes.rs | 16 +++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/http/src/from_bytes.rs b/http/src/from_bytes.rs index 3994e9b55..c69f6bcd8 100644 --- a/http/src/from_bytes.rs +++ b/http/src/from_bytes.rs @@ -42,11 +42,14 @@ macro_rules! int_impl { let mut value: $ty = 0; for b in src.iter().copied() { if b >= b'0' && b <= b'9' { - if value >= (<$ty>::MAX / 10) { - // Overflow. - return Err(ParseIntError); + match value.checked_mul(10) { + Some(v) => value = v, + None => return Err(ParseIntError), + } + match value.checked_add((b - b'0') as $ty) { + Some(v) => value = v, + None => return Err(ParseIntError), } - value = (value * 10) + (b - b'0') as $ty; } else { return Err(ParseIntError); } diff --git a/http/tests/functional/from_bytes.rs b/http/tests/functional/from_bytes.rs index 3dc5e01e8..ba2daae09 100644 --- a/http/tests/functional/from_bytes.rs +++ b/http/tests/functional/from_bytes.rs @@ -21,15 +21,21 @@ fn integers() { test_parse(b"123", 123u32); test_parse(b"123", 123u64); test_parse(b"123", 123usize); + + test_parse(b"255", u8::MAX); + test_parse(b"65535", u16::MAX); + test_parse(b"4294967295", u32::MAX); + test_parse(b"18446744073709551615", u64::MAX); + test_parse(b"18446744073709551615", usize::MAX); } #[test] fn integers_overflow() { - test_parse_fail::(b"256"); - test_parse_fail::(b"65536"); - test_parse_fail::(b"4294967296"); - test_parse_fail::(b"18446744073709551615"); - test_parse_fail::(b"18446744073709551615"); + test_parse_fail::(b"257"); + test_parse_fail::(b"65537"); + test_parse_fail::(b"4294967297"); + test_parse_fail::(b"18446744073709551616"); + test_parse_fail::(b"18446744073709551616"); } #[test] From f5a8d1d3d90ece1d6d4c08c9ce14c5c27f404123 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Fri, 7 May 2021 18:46:08 +0200 Subject: [PATCH 44/81] Always remove complete request Body from Connection --- http/src/server.rs | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/http/src/server.rs b/http/src/server.rs index cae128997..9c4ca8412 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -210,6 +210,8 @@ pub struct Connection { stream: TcpStream, buf: Vec, /// Number of bytes of `buf` that are already parsed. + /// NOTE: this may be larger then `buf.len()`, which case a `Body` was + /// dropped without reading it entirely. parsed_bytes: usize, /// The HTTP version of the last request. last_version: Option, @@ -289,11 +291,19 @@ impl Connection { let mut too_short = 0; loop { // In case of pipelined requests it could be that while reading a - // previous request's body it partially read the headers of the next - // (this) request. To handle this we attempt to parse the request if - // we have more than zero bytes in the first iteration of the loop. - if self.buf.len() <= too_short { - // Receive some more bytes. + // previous request's body it partially read the head of the next + // (this) request. To handle this we first attempt to parse the + // request if we have more than zero bytes (of the next request) in + // the first iteration of the loop. + while self.parsed_bytes >= self.buf.len() + || self.buf.len() - self.parsed_bytes <= too_short + { + // While we didn't read the entire previous request body, or + // while we have less than `too_short` bytes we try to receive + // some more bytes. + + self.clear_buffer(); + self.buf.reserve(MIN_READ_SIZE); if self.stream.recv(&mut self.buf).await? == 0 { if self.buf.is_empty() { // Read the entire stream, so we're done. @@ -309,6 +319,8 @@ impl Connection { let mut headers = [EMPTY_HEADER; MAX_HEADERS]; let mut req = httparse::Request::new(&mut headers); + // SAFETY: because we received until at least `self.parsed_bytes >= + // self.buf.len()` above, we can safely slice the buffer.. match req.parse(&self.buf[self.parsed_bytes..]) { Ok(httparse::Status::Complete(header_length)) => { self.parsed_bytes += header_length; @@ -528,7 +540,8 @@ impl Connection { { let mut itoa_buf = itoa::Buffer::new(); - // Bytes of the (next) request. + // Clear bytes from the previous request, keeping the bytes of the + // request. self.clear_buffer(); let ignore_end = self.buf.len(); @@ -611,10 +624,11 @@ impl Connection { /// Clear parsed request(s) from the buffer. fn clear_buffer(&mut self) { - if self.buf.len() == self.parsed_bytes { + let buf_len = self.buf.len(); + if self.parsed_bytes >= buf_len { // Parsed all bytes in the buffer, so we can clear it. self.buf.clear(); - self.parsed_bytes = 0; + self.parsed_bytes -= buf_len; } // TODO: move bytes to the start. @@ -920,15 +934,10 @@ impl<'a> Drop for Body<'a> { return; } - let ignored_len = self.conn.parsed_bytes + self.left; - if self.conn.buf.len() >= ignored_len { - // Entire body was already read we can skip the bytes. - self.conn.parsed_bytes = ignored_len; - return; - } - - // TODO: mark more bytes as ignored in `Connection`. - todo!("ignore the body: read more bytes") + // Mark the entire body as parsed. + // NOTE: `Connection` handles the case where we didn't read the entire + // body yet. + self.conn.parsed_bytes += self.left; } } From 539081e43fa31fea729f0e7a8fbe343a09128f4c Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 8 May 2021 18:33:01 +0200 Subject: [PATCH 45/81] Rename FromBytes to FromHeaderValue To show its intended purpose is to parse header values. Also move it to the header module. --- http/src/from_bytes.rs | 96 ---------------- http/src/header.rs | 105 +++++++++++++++++- http/src/lib.rs | 2 - http/src/server.rs | 5 +- http/tests/functional.rs | 2 +- .../{from_bytes.rs => from_header_value.rs} | 10 +- http/tests/functional/header.rs | 7 +- 7 files changed, 112 insertions(+), 115 deletions(-) delete mode 100644 http/src/from_bytes.rs rename http/tests/functional/{from_bytes.rs => from_header_value.rs} (89%) diff --git a/http/src/from_bytes.rs b/http/src/from_bytes.rs deleted file mode 100644 index c69f6bcd8..000000000 --- a/http/src/from_bytes.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::time::SystemTime; -use std::{fmt, str}; - -use httpdate::parse_http_date; - -/// Analogous trait to [`FromStr`]. -/// -/// The main use case for this trait in [`Header::parse`]. Because of this the -/// implementations should expect the `value`s passed to be ASCII/UTF-8, but -/// this not true in all cases. -/// -/// [`FromStr`]: std::str::FromStr -/// [`Header::parse`]: crate::Header::parse -pub trait FromBytes<'a>: Sized { - /// Error returned by parsing the bytes. - type Err; - - /// Parse the `value`. - fn from_bytes(value: &'a [u8]) -> Result; -} - -#[derive(Debug)] -pub struct ParseIntError; - -impl fmt::Display for ParseIntError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("invalid integer") - } -} - -macro_rules! int_impl { - ($( $ty: ty ),+) => { - $( - impl FromBytes<'_> for $ty { - type Err = ParseIntError; - - fn from_bytes(src: &[u8]) -> Result { - if src.is_empty() { - return Err(ParseIntError); - } - - let mut value: $ty = 0; - for b in src.iter().copied() { - if b >= b'0' && b <= b'9' { - match value.checked_mul(10) { - Some(v) => value = v, - None => return Err(ParseIntError), - } - match value.checked_add((b - b'0') as $ty) { - Some(v) => value = v, - None => return Err(ParseIntError), - } - } else { - return Err(ParseIntError); - } - } - Ok(value) - } - } - )+ - }; -} - -int_impl!(u8, u16, u32, u64, usize); - -impl<'a> FromBytes<'a> for &'a str { - type Err = str::Utf8Error; - - fn from_bytes(value: &'a [u8]) -> Result { - str::from_utf8(value) - } -} - -#[derive(Debug)] -pub struct ParseTimeError; - -impl fmt::Display for ParseTimeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("invalid time") - } -} - -/// Parses the value following RFC7231 section 7.1.1.1. -impl FromBytes<'_> for SystemTime { - type Err = ParseTimeError; - - fn from_bytes(value: &[u8]) -> Result { - match str::from_utf8(value) { - Ok(value) => match parse_http_date(value) { - Ok(time) => Ok(time), - Err(_) => Err(ParseTimeError), - }, - Err(_) => Err(ParseTimeError), - } - } -} diff --git a/http/src/header.rs b/http/src/header.rs index b8524dd0d..5b6b50a1c 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -6,10 +6,13 @@ use std::borrow::Cow; use std::convert::AsRef; -use std::fmt; use std::iter::FusedIterator; +use std::time::SystemTime; +use std::{fmt, str}; -use crate::{cmp_lower_case, is_lower_case, FromBytes}; +use httpdate::parse_http_date; + +use crate::{cmp_lower_case, is_lower_case}; /// List of headers. /// @@ -241,12 +244,13 @@ impl<'n, 'v> Header<'n, 'v> { self.value } - /// Parse the value of the header using `T`'s [`FromBytes`] implementation. + /// Parse the value of the header using `T`'s [`FromHeaderValue`] + /// implementation. pub fn parse(&self) -> Result where - T: FromBytes<'v>, + T: FromHeaderValue<'v>, { - FromBytes::from_bytes(self.value) + FromHeaderValue::from_bytes(self.value) } } @@ -892,3 +896,94 @@ impl<'a> fmt::Display for HeaderName<'a> { f.write_str(self.as_ref()) } } + +/// Analogous trait to [`FromStr`]. +/// +/// The main use case for this trait in [`Header::parse`]. Because of this the +/// implementations should expect the `value`s passed to be ASCII/UTF-8, but +/// this not true in all cases. +/// +/// [`FromStr`]: std::str::FromStr +pub trait FromHeaderValue<'a>: Sized { + /// Error returned by parsing the bytes. + type Err; + + /// Parse the `value`. + fn from_bytes(value: &'a [u8]) -> Result; +} + +#[derive(Debug)] +pub struct ParseIntError; + +impl fmt::Display for ParseIntError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid integer") + } +} + +macro_rules! int_impl { + ($( $ty: ty ),+) => { + $( + impl FromHeaderValue<'_> for $ty { + type Err = ParseIntError; + + fn from_bytes(src: &[u8]) -> Result { + if src.is_empty() { + return Err(ParseIntError); + } + + let mut value: $ty = 0; + for b in src.iter().copied() { + if b >= b'0' && b <= b'9' { + match value.checked_mul(10) { + Some(v) => value = v, + None => return Err(ParseIntError), + } + match value.checked_add((b - b'0') as $ty) { + Some(v) => value = v, + None => return Err(ParseIntError), + } + } else { + return Err(ParseIntError); + } + } + Ok(value) + } + } + )+ + }; +} + +int_impl!(u8, u16, u32, u64, usize); + +impl<'a> FromHeaderValue<'a> for &'a str { + type Err = str::Utf8Error; + + fn from_bytes(value: &'a [u8]) -> Result { + str::from_utf8(value) + } +} + +#[derive(Debug)] +pub struct ParseTimeError; + +impl fmt::Display for ParseTimeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid time") + } +} + +/// Parses the value following RFC7231 section 7.1.1.1. +impl FromHeaderValue<'_> for SystemTime { + type Err = ParseTimeError; + + fn from_bytes(value: &[u8]) -> Result { + match str::from_utf8(value) { + Ok(value) => match parse_http_date(value) { + Ok(time) => Ok(time), + Err(_) => Err(ParseTimeError), + }, + Err(_) => Err(ParseTimeError), + } + } +} diff --git a/http/src/lib.rs b/http/src/lib.rs index 99b7eb95c..e23b392b0 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -11,7 +11,6 @@ #![allow(incomplete_features)] // NOTE: for `generic_associated_types`. pub mod body; -mod from_bytes; pub mod header; pub mod method; mod request; @@ -22,7 +21,6 @@ pub mod version; #[doc(no_inline)] pub use body::Body; -pub use from_bytes::FromBytes; #[doc(no_inline)] pub use header::{Header, HeaderName, Headers}; #[doc(no_inline)] diff --git a/http/src/server.rs b/http/src/server.rs index 9c4ca8412..44104036c 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -30,7 +30,8 @@ use httparse::EMPTY_HEADER; use httpdate::HttpDate; use crate::body::BodyLength; -use crate::{FromBytes, HeaderName, Headers, Method, Request, Response, StatusCode, Version}; +use crate::header::{FromHeaderValue, HeaderName, Headers}; +use crate::{Method, Request, Response, StatusCode, Version}; /// Maximum size of the header (the start line and the headers). /// @@ -351,7 +352,7 @@ impl Connection { // > request message, the server MUST respond with a // > 400 (Bad Request) status code and then close // > the connection. - if let Ok(length) = FromBytes::from_bytes(value) { + if let Ok(length) = FromHeaderValue::from_bytes(value) { match body_length.as_mut() { Some(body_length) if *body_length == length => {} Some(_) => return Err(RequestError::DifferentContentLengths), diff --git a/http/tests/functional.rs b/http/tests/functional.rs index 3755ab6b9..bce0dcb64 100644 --- a/http/tests/functional.rs +++ b/http/tests/functional.rs @@ -9,7 +9,7 @@ fn assert_size(expected: usize) { #[path = "functional"] // rustfmt can't find the files. mod functional { - mod from_bytes; + mod from_header_value; mod header; mod method; mod status_code; diff --git a/http/tests/functional/from_bytes.rs b/http/tests/functional/from_header_value.rs similarity index 89% rename from http/tests/functional/from_bytes.rs rename to http/tests/functional/from_header_value.rs index ba2daae09..fb43da5c2 100644 --- a/http/tests/functional/from_bytes.rs +++ b/http/tests/functional/from_header_value.rs @@ -1,7 +1,7 @@ use std::fmt; use std::time::SystemTime; -use heph_http::FromBytes; +use heph_http::header::FromHeaderValue; #[test] fn str() { @@ -72,8 +72,8 @@ fn system_time() { #[track_caller] fn test_parse<'a, T>(value: &'a [u8], expected: T) where - T: FromBytes<'a> + fmt::Debug + PartialEq, - >::Err: fmt::Debug, + T: FromHeaderValue<'a> + fmt::Debug + PartialEq, + >::Err: fmt::Debug, { assert_eq!(T::from_bytes(value).unwrap(), expected); } @@ -81,8 +81,8 @@ where #[track_caller] fn test_parse_fail<'a, T>(value: &'a [u8]) where - T: FromBytes<'a> + fmt::Debug + PartialEq, - >::Err: fmt::Debug, + T: FromHeaderValue<'a> + fmt::Debug + PartialEq, + >::Err: fmt::Debug, { assert!(T::from_bytes(value).is_err()); } diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index 26ca061d5..92110a826 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -1,7 +1,6 @@ use std::fmt; -use heph_http::header::{Header, HeaderName, Headers}; -use heph_http::FromBytes; +use heph_http::header::{FromHeaderValue, Header, HeaderName, Headers}; use crate::assert_size; @@ -67,8 +66,8 @@ fn check_header<'a, T>( value: &'_ [u8], parsed_value: T, ) where - T: FromBytes<'a> + PartialEq + fmt::Debug, - >::Err: fmt::Debug, + T: FromHeaderValue<'a> + PartialEq + fmt::Debug, + >::Err: fmt::Debug, { let got = headers.get(name).unwrap(); assert_eq!(got.name(), name); From 4ade7f35e0c7a809f1ce140c7a7ba47a3f0a24c4 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 8 May 2021 18:40:11 +0200 Subject: [PATCH 46/81] Add timeouts to the my_ip example --- http/examples/my_ip.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index 8b6257f31..ac2760a5a 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -3,12 +3,14 @@ use std::borrow::Cow; use std::io; use std::net::SocketAddr; +use std::time::Duration; use heph::actor::{self, Actor, NewActor}; use heph::net::TcpStream; use heph::rt::{self, Runtime, ThreadLocal}; use heph::spawn::options::{ActorOptions, Priority}; use heph::supervisor::{Supervisor, SupervisorStrategy}; +use heph::timer::Deadline; use heph_http::body::OneshotBody; use heph_http::{self as http, Header, HeaderName, Headers, HttpServer, Method, StatusCode}; use log::{debug, error, info, warn}; @@ -68,17 +70,23 @@ fn conn_supervisor(err: io::Error) -> SupervisorStrategy<(TcpStream, SocketAddr) SupervisorStrategy::Stop } +const READ_TIMEOUT: Duration = Duration::from_secs(10); +const ALIVE_TIMEOUT: Duration = Duration::from_secs(120); +const WRITE_TIMEOUT: Duration = Duration::from_secs(10); + async fn http_actor( - _: actor::Context, + mut ctx: actor::Context, mut connection: http::Connection, address: SocketAddr, ) -> io::Result<()> { info!("accepted connection: source={}", address); connection.set_nodelay(true)?; + let mut read_timeout = READ_TIMEOUT; loop { let mut headers = Headers::EMPTY; - let (code, body, should_close) = match connection.next_request().await? { + let fut = Deadline::after(&mut ctx, read_timeout, connection.next_request()); + let (code, body, should_close) = match fut.await? { Ok(Some(request)) => { info!("received request: {:?}: source={}", request, address); if request.path() != "/" { @@ -117,11 +125,16 @@ async fn http_actor( code, body, address ); let body = OneshotBody::new(body.as_bytes()); - connection.respond(code, headers, body).await?; + let write_response = connection.respond(code, headers, body); + Deadline::after(&mut ctx, WRITE_TIMEOUT, write_response).await?; if should_close { warn!("closing connection: source={}", address); return Ok(()); } + + // Now that we've read a single request we can wait a little for the + // next one so that we can reuse the resources for the next request. + read_timeout = ALIVE_TIMEOUT; } } From fa8c8beb7b83e9c5e7d525ff61cad9b0c4bf7654 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Thu, 13 May 2021 20:33:20 +0200 Subject: [PATCH 47/81] Add --workspace to all Cargo commands So it will test/check/doc/etc. the entire workspace. --- Makefile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index e29736c2d..b01a9bcf5 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ TARGETS ?= x86_64-apple-darwin x86_64-unknown-linux-gnu x86_64-unknown-freebsd RUN ?= test test: - cargo test --all-features + cargo test --all-features --workspace # NOTE: Keep `RUSTFLAGS` and `RUSTDOCFLAGS` in sync to ensure the doc tests # compile correctly. @@ -27,14 +27,14 @@ test_sanitiser: @if [ -z $${SAN+x} ]; then echo "Required '\$$SAN' variable is not set" 1>&2; exit 1; fi RUSTFLAGS="-Z sanitizer=$$SAN -Z sanitizer-memory-track-origins" \ RUSTDOCFLAGS="-Z sanitizer=$$SAN -Z sanitizer-memory-track-origins" \ - cargo test -Z build-std --all-features --target $(RUSTUP_TARGET) + cargo test -Z build-std --all-features --workspace --target $(RUSTUP_TARGET) check: - cargo check --all-features --all-targets + cargo check --all-features --workspace --all-targets check_all_targets: $(TARGETS) $(TARGETS): - cargo check --all-features --all-targets --target $@ + cargo check --all-features --workspace --all-targets --target $@ # NOTE: when using this command you might want to change the `test` target to # only run a subset of the tests you're actively working on. @@ -47,7 +47,7 @@ dev: # multiple-crate-versions: socket2 is included twice? But `cargo tree` disagrees. clippy: lint lint: - cargo clippy --all-features -- \ + cargo clippy --all-features --workspace -- \ --deny clippy::all \ --deny clippy::correctness \ --deny clippy::style \ @@ -110,10 +110,10 @@ install_llvm_tools: rustup component add llvm-tools-preview doc: - cargo doc --all-features + cargo doc --all-features --workspace doc_private: - cargo doc --all-features --document-private-items + cargo doc --all-features --workspace --document-private-items clean: cargo clean From 26414646b907098865d57b15c9670bfe02f03807 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Thu, 13 May 2021 20:39:48 +0200 Subject: [PATCH 48/81] Fix some Clippy warnings in convert_trace tool --- tools/src/bin/convert_trace.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tools/src/bin/convert_trace.rs b/tools/src/bin/convert_trace.rs index 3d41aa25e..e646caafd 100644 --- a/tools/src/bin/convert_trace.rs +++ b/tools/src/bin/convert_trace.rs @@ -19,7 +19,7 @@ fn main() { let output = match args.next() { Some(output) => PathBuf::from(output), None => { - let end_idx = input.rfind('.').unwrap_or(input.len()); + let end_idx = input.rfind('.').unwrap_or_else(|| input.len()); let mut output = PathBuf::from(&input[..end_idx]); // If the input has a single extension this will add `json` to it. // If however it has two extensions, e.g. `.bin.log` this will @@ -60,10 +60,11 @@ fn main() { .as_micros(); let mut duration = event.end.duration_since(event.start).unwrap().as_micros(); - let pid = event.stream_id; - let tid = event.substream_id; + let process_id = event.stream_id; + let thread_id = event.substream_id; loop { - match times.entry((pid, tid)).or_default().entry(timestamp) { + let key = (process_id, thread_id); + match times.entry(key).or_default().entry(timestamp) { Entry::Vacant(entry) => { entry.insert(duration); break; @@ -88,8 +89,8 @@ fn main() { output, "{}\t\t{{\"pid\": {}, \"tid\": {}, \"ts\": {}, \"dur\": {}, \"name\": \"{}\"", if first { "" } else { ",\n" }, - pid, - tid, + process_id, + thread_id, timestamp, duration, event.description, @@ -101,7 +102,7 @@ fn main() { output .write_all(b", \"args\": {") .expect("failed to write event to output"); - for (name, value) in event.attributes.iter() { + for (name, value) in &event.attributes { let fmt_args = match value { // NOTE: `format_args!` is useless. Value::Unsigned(value) => format!("\"{}\": {}", name, value), @@ -194,7 +195,9 @@ pub struct TraceEvents<'t, R> { trace: &'t mut Trace, } +#[allow(clippy::unreadable_literal)] const METADATA_MAGIC: u32 = 0x75D11D4D; +#[allow(clippy::unreadable_literal)] const EVENT_MAGIC: u32 = 0xC1FC1FB7; /// Minimum amount of bytes in the buffer before we read again. @@ -237,12 +240,12 @@ where match magic { METADATA_MAGIC => { if let Err(err) = self.apply_metadata_packet() { - Some(Err(err.into())) + Some(Err(err)) } else { self.next() } } - EVENT_MAGIC => Some(self.parse_event_packet().map_err(|e| e.into())), + EVENT_MAGIC => Some(self.parse_event_packet()), magic => Some(Err(ParseError::InvalidMagic(magic))), } } From 3d092de99a61118df039faef67ddc5a5b57d4515 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Thu, 13 May 2021 21:00:45 +0200 Subject: [PATCH 49/81] Fix and ignore some Clippy warnings --- http/src/header.rs | 9 +++++---- http/src/method.rs | 5 +---- http/src/request.rs | 2 +- http/src/server.rs | 44 ++++++++++++++++++++--------------------- http/src/status_code.rs | 5 +---- 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index 5b6b50a1c..f82eff37a 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -93,7 +93,7 @@ impl Headers { /// /// If all you need is the header value you can use [`Headers::get_value`]. pub fn get<'a>(&'a self, name: &HeaderName<'_>) -> Option> { - for part in self.parts.iter() { + for part in &self.parts { if part.name == *name { return Some(Header { name: part.name.borrow(), @@ -106,7 +106,7 @@ impl Headers { /// Get the header's value with `name`, if any. pub fn get_value<'a>(&'a self, name: &HeaderName) -> Option<&'a [u8]> { - for part in self.parts.iter() { + for part in &self.parts { if part.name == *name { return Some(&self.values[part.start..part.end]); } @@ -164,7 +164,7 @@ impl From<&'_ [Header<'_>]> for Headers { impl fmt::Debug for Headers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_map(); - for part in self.parts.iter() { + for part in &self.parts { let value = &self.values[part.start..part.end]; if let Ok(str) = std::str::from_utf8(value) { f.entry(&part.name, &str); @@ -303,6 +303,7 @@ macro_rules! known_headers { /// # Notes /// /// If `name` is static prefer to use [`HeaderName::from_lowercase`]. + #[allow(clippy::should_implement_trait)] pub fn from_str(name: &str) -> HeaderName<'static> { // This first matches on the length of the `name`, then does a // case-insensitive compare of the name with all known headers with @@ -934,7 +935,7 @@ macro_rules! int_impl { let mut value: $ty = 0; for b in src.iter().copied() { - if b >= b'0' && b <= b'9' { + if (b'0'..=b'9').contains(&b) { match value.checked_mul(10) { Some(v) => value = v, None => return Err(ParseIntError), diff --git a/http/src/method.rs b/http/src/method.rs index dac42cb67..6f849a98c 100644 --- a/http/src/method.rs +++ b/http/src/method.rs @@ -74,10 +74,7 @@ impl Method { // > The HEAD method is identical to GET except that the server MUST NOT // > send a message body in the response (i.e., the response terminates // > at the end of the header section). - match self { - Method::Head => false, - _ => true, - } + !matches!(self, Method::Head) } /// Returns the method as string. diff --git a/http/src/request.rs b/http/src/request.rs index 02bfece5f..36c1f43a6 100644 --- a/http/src/request.rs +++ b/http/src/request.rs @@ -22,8 +22,8 @@ impl Request { ) -> Request { Request { method, - version, path, + version, headers, body, } diff --git a/http/src/server.rs b/http/src/server.rs index 44104036c..1c3d69b94 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -96,7 +96,7 @@ impl Clone for Setup { /// An actor that starts a new actor for each accepted TCP connection. /// -/// TODO: same design as TcpServer. +/// TODO: same design as `TcpServer`. /// /// This actor can start as a thread-local or thread-safe actor. When using the /// thread-local variant one actor runs per worker thread which spawns @@ -306,40 +306,40 @@ impl Connection { self.clear_buffer(); self.buf.reserve(MIN_READ_SIZE); if self.stream.recv(&mut self.buf).await? == 0 { - if self.buf.is_empty() { + return if self.buf.is_empty() { // Read the entire stream, so we're done. - return Ok(Ok(None)); + Ok(Ok(None)) } else { // Couldn't read any more bytes, but we still have bytes // in the buffer. This means it contains a partial // request. - return Ok(Err(RequestError::IncompleteRequest)); - } + Ok(Err(RequestError::IncompleteRequest)) + }; } } let mut headers = [EMPTY_HEADER; MAX_HEADERS]; - let mut req = httparse::Request::new(&mut headers); + let mut request = httparse::Request::new(&mut headers); // SAFETY: because we received until at least `self.parsed_bytes >= // self.buf.len()` above, we can safely slice the buffer.. - match req.parse(&self.buf[self.parsed_bytes..]) { + match request.parse(&self.buf[self.parsed_bytes..]) { Ok(httparse::Status::Complete(header_length)) => { self.parsed_bytes += header_length; // SAFETY: all these unwraps are safe because `parse` above // ensures there all `Some`. - let method = match req.method.unwrap().parse() { + let method = match request.method.unwrap().parse() { Ok(method) => method, Err(_) => return Ok(Err(RequestError::UnknownMethod)), }; self.last_method = Some(method); - let path = req.path.unwrap().to_string(); - let version = map_version(req.version.unwrap()); + let path = request.path.unwrap().to_string(); + let version = map_version(request.version.unwrap()); self.last_version = Some(version); // RFC 7230 section 3.3.3 Message Body Length. let mut body_length: Option = None; - let res = Headers::from_httparse_headers(req.headers, |name, value| { + let res = Headers::from_httparse_headers(request.headers, |name, value| { if *name == HeaderName::CONTENT_LENGTH { // RFC 7230 section 3.3.3 point 4: // > If a message is received without @@ -415,8 +415,8 @@ impl Connection { // Buffer doesn't include the entire request header, try // reading more bytes (in the next iteration). too_short = self.buf.len(); - self.last_method = req.method.and_then(|m| m.parse().ok()); - if let Some(version) = req.version { + self.last_method = request.method.and_then(|m| m.parse().ok()); + if let Some(version) = request.version { self.last_version = Some(map_version(version)); } @@ -503,6 +503,7 @@ impl Connection { /// /// See the notes for [`Connection::send_response`], they apply to this /// function also. + #[allow(clippy::future_not_send)] pub async fn respond<'b, B>( &mut self, status: StatusCode, @@ -531,6 +532,7 @@ impl Connection { /// /// [`expects_body`]: Method::expects_body /// [`includes_body`]: StatusCode::includes_body + #[allow(clippy::future_not_send)] pub async fn send_response<'b, B>( &mut self, request_method: Method, @@ -719,10 +721,8 @@ impl<'a> Body<'a> { let len = min(len, dst.len()); MaybeUninit::write_slice(&mut dst[..len], &bytes[..len]); self.processed(len); - len - } else { - 0 } + len } /// Mark `n` bytes are processed. @@ -761,10 +761,10 @@ where match body.conn.stream.try_recv(&mut *buf) { Ok(n) => return Poll::Ready(Ok(len + n)), Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { - if len != 0 { - return Poll::Ready(Ok(len)); + return if len == 0 { + Poll::Pending } else { - return Poll::Pending; + Poll::Ready(Ok(len)) } } Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, @@ -811,10 +811,10 @@ where match body.conn.stream.try_recv_vectored(&mut *bufs) { Ok(n) => return Poll::Ready(Ok(len + n)), Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { - if len != 0 { - return Poll::Ready(Ok(len)); + return if len == 0 { + Poll::Pending } else { - return Poll::Pending; + Poll::Ready(Ok(len)) } } Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, diff --git a/http/src/status_code.rs b/http/src/status_code.rs index dceb1e6ce..f4b2f7574 100644 --- a/http/src/status_code.rs +++ b/http/src/status_code.rs @@ -307,10 +307,7 @@ impl StatusCode { // > All 1xx (Informational), 204 (No Content), and 304 (Not Modified) // > responses do not include a message body. All other responses do // > include a message body, although the body might be of zero length. - match self.0 { - 100..=199 | 204 | 304 => false, - _ => true, - } + !matches!(self.0, 100..=199 | 204 | 304) } /// Returns the reason phrase for well known status codes. From c43492a62cd3fb87879129179f37c3f456b7fc2c Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Thu, 13 May 2021 21:01:09 +0200 Subject: [PATCH 50/81] Add Headers::is_empty --- http/src/header.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/http/src/header.rs b/http/src/header.rs index f82eff37a..59c17a940 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -70,6 +70,11 @@ impl Headers { self.parts.len() } + /// Returns `true` if this is empty. + pub fn is_empty(&self) -> bool { + self.parts.is_empty() + } + /// Add a new `header`. /// /// # Notes From 9e1f55250f7d73f29fe99120ffe82699e7baf441 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Thu, 13 May 2021 21:01:53 +0200 Subject: [PATCH 51/81] Fix StreamingBody's Body implementation This didn't check the number of bytes left to send before, but it still needs tests. --- http/src/body.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/http/src/body.rs b/http/src/body.rs index 1af22eb23..63ac8923a 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -146,14 +146,17 @@ mod private { while *left != 0 { // We have bytes we need to send. if let Some(bytes) = body_bytes.as_mut() { + // TODO: check `bytes.len()` <= `left`. match stream.try_send(*bytes) { Ok(0) => return Poll::Ready(Err(io::ErrorKind::WriteZero.into())), - Ok(n) if n >= bytes.len() => { - *body_bytes = None; - } Ok(n) => { - *bytes = &bytes[n..]; - continue; + *left -= n; + if n >= bytes.len() { + *body_bytes = None; + } else { + *bytes = &bytes[n..]; + continue; + } } Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { return Poll::Pending From 7a2b672168fff96eaffe89f61b3f9bfe5eb540fc Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Thu, 13 May 2021 21:07:49 +0200 Subject: [PATCH 52/81] Remove const_fn feature It's been split into smaller features. --- http/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/http/src/lib.rs b/http/src/lib.rs index e23b392b0..13c9b6d26 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -1,6 +1,5 @@ #![feature( async_stream, - const_fn, const_fn_trait_bound, const_mut_refs, const_panic, From a6f06532d7f0c2234a5f498f30d1cc09b2ef1614 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 18 May 2021 11:28:40 +0200 Subject: [PATCH 53/81] Return RequestError::HeadTooLarge if the HTTP head is too large Wasn't implemented before. --- http/src/server.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/http/src/server.rs b/http/src/server.rs index 1c3d69b94..9c918786a 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -37,7 +37,7 @@ use crate::{Method, Request, Response, StatusCode, Version}; /// /// RFC 7230 section 3.1.1 recommends "all HTTP senders and recipients support, /// at a minimum, request-line lengths of 8000 octets." -pub const MAX_HEADER_SIZE: usize = 16384; +pub const MAX_HEAD_SIZE: usize = 16384; /// Maximum number of headers parsed from a single request. pub const MAX_HEADERS: usize = 64; @@ -420,8 +420,8 @@ impl Connection { self.last_version = Some(map_version(version)); } - if too_short >= MAX_HEADER_SIZE { - todo!("HTTP request header too large"); + if too_short >= MAX_HEAD_SIZE { + return Ok(Err(RequestError::HeadTooLarge)); } continue; @@ -947,10 +947,10 @@ impl<'a> Drop for Body<'a> { pub enum RequestError { /// Missing part of request. IncompleteRequest, - /// HTTP Header is too large. + /// HTTP Head (start line and headers) is too large. /// - /// Limit is defined by [`MAX_HEADER_SIZE`]. - HeaderTooLarge, + /// Limit is defined by [`MAX_HEAD_SIZE`]. + HeadTooLarge, /// Value in the "Content-Length" header is invalid. InvalidContentLength, /// Multiple "Content-Length" headers were present with differing values. @@ -979,7 +979,7 @@ impl RequestError { // determine the values here. match self { IncompleteRequest - | HeaderTooLarge + | HeadTooLarge | InvalidContentLength | DifferentContentLengths | InvalidHeaderName @@ -1004,7 +1004,7 @@ impl RequestError { // determine the values here. match self { IncompleteRequest - | HeaderTooLarge + | HeadTooLarge | InvalidContentLength | DifferentContentLengths | InvalidHeaderName @@ -1037,7 +1037,7 @@ impl fmt::Display for RequestError { use RequestError::*; f.write_str(match self { IncompleteRequest => "incomplete request", - HeaderTooLarge => "header too large", + HeadTooLarge => "head too large", InvalidContentLength => "invalid Content-Length header", DifferentContentLengths => "different Content-Length headers", InvalidHeaderName => "invalid header name", From ff4b43974408a79028af83abafbbd23771cae468 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 18 May 2021 11:46:52 +0200 Subject: [PATCH 54/81] Document HttpServer and the server module --- http/src/server.rs | 161 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 145 insertions(+), 16 deletions(-) diff --git a/http/src/server.rs b/http/src/server.rs index 9c918786a..9d935597d 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -13,6 +13,8 @@ // // TODO: chunked encoding. +//! Module with the HTTP server implementation. + use std::cmp::min; use std::fmt; use std::future::Future; @@ -94,32 +96,159 @@ impl Clone for Setup { } } -/// An actor that starts a new actor for each accepted TCP connection. -/// -/// TODO: same design as `TcpServer`. +/// An actor that starts a new actor for each accepted HTTP [`Connection`]. /// -/// This actor can start as a thread-local or thread-safe actor. When using the -/// thread-local variant one actor runs per worker thread which spawns -/// thread-local actors to handle the [`TcpStream`]s. See the first example -/// below on how to run this `TcpServer` as a thread-local actor. +/// `HttpServer` has the same design as [`TcpServer`]. It accept `TcpStream`s +/// and converts those into HTTP [`Connection`]s, from which HTTP [`Request`]s +/// can be read and HTTP [`Response`]s can be written. /// -/// This actor can also run as thread-safe actor in which case it also spawns -/// thread-safe actors. Note however that using thread-*local* version is -/// recommended. The third example below shows how to run the `TcpServer` as -/// thread-safe actor. +/// Similar to `TcpServer` this type works with thread-safe and thread-local +/// actors. /// /// # Graceful shutdown /// -/// Graceful shutdown is done by sending it a [`Terminate`] message, see below -/// for an example. The TCP server can also handle (shutdown) process signals, -/// see "Example 2 my ip" (in the examples directory of the source code) for an -/// example of that. +/// Graceful shutdown is done by sending it a [`Terminate`] message. The HTTP +/// server can also handle (shutdown) process signals, see below for an example. /// /// [`Terminate`]: heph::actor::messages::Terminate /// /// # Examples /// -/// TODO. +/// ```rust +/// # #![feature(never_type)] +/// use std::borrow::Cow; +/// use std::io; +/// use std::net::SocketAddr; +/// use std::time::Duration; +/// +/// use heph::actor::{self, Actor, NewActor}; +/// use heph::net::TcpStream; +/// use heph::rt::{self, Runtime, ThreadLocal}; +/// use heph::spawn::options::{ActorOptions, Priority}; +/// use heph::supervisor::{Supervisor, SupervisorStrategy}; +/// use heph::timer::Deadline; +/// use heph_http::body::OneshotBody; +/// use heph_http::{self as http, Header, HeaderName, Headers, HttpServer, Method, StatusCode}; +/// use log::error; +/// +/// fn main() -> Result<(), rt::Error> { +/// // Setup the HTTP server. +/// let actor = http_actor as fn(_, _, _) -> _; +/// let address = "127.0.0.1:7890".parse().unwrap(); +/// let server = HttpServer::setup(address, conn_supervisor, actor, ActorOptions::default()) +/// .map_err(rt::Error::setup)?; +/// +/// // Build the runtime. +/// let mut runtime = Runtime::setup().use_all_cores().build()?; +/// // On each worker thread start our HTTP server. +/// runtime.run_on_workers(move |mut runtime_ref| -> io::Result<()> { +/// let options = ActorOptions::default().with_priority(Priority::LOW); +/// let server_ref = runtime_ref.try_spawn_local(ServerSupervisor, server, (), options)?; +/// +/// # server_ref.try_send(heph::actor::messages::Terminate).unwrap(); +/// +/// // Allow graceful shutdown by responding to process signals. +/// runtime_ref.receive_signals(server_ref.try_map()); +/// Ok(()) +/// })?; +/// runtime.start() +/// } +/// +/// /// Our supervisor for the TCP server. +/// #[derive(Copy, Clone, Debug)] +/// struct ServerSupervisor; +/// +/// impl Supervisor for ServerSupervisor +/// where +/// NA: NewActor, +/// NA::Actor: Actor>, +/// { +/// fn decide(&mut self, err: http::server::Error) -> SupervisorStrategy<()> { +/// use http::server::Error::*; +/// match err { +/// Accept(err) => { +/// error!("error accepting new connection: {}", err); +/// SupervisorStrategy::Restart(()) +/// } +/// NewActor(_) => unreachable!(), +/// } +/// } +/// +/// fn decide_on_restart_error(&mut self, err: io::Error) -> SupervisorStrategy<()> { +/// error!("error restarting the TCP server: {}", err); +/// SupervisorStrategy::Stop +/// } +/// +/// fn second_restart_error(&mut self, err: io::Error) { +/// error!("error restarting the actor a second time: {}", err); +/// } +/// } +/// +/// fn conn_supervisor(err: io::Error) -> SupervisorStrategy<(TcpStream, SocketAddr)> { +/// error!("error handling connection: {}", err); +/// SupervisorStrategy::Stop +/// } +/// +/// /// Our actor that handles a single HTTP connection. +/// async fn http_actor( +/// mut ctx: actor::Context, +/// mut connection: http::Connection, +/// address: SocketAddr, +/// ) -> io::Result<()> { +/// // Set `TCP_NODELAY` on the `TcpStream`. +/// connection.set_nodelay(true)?; +/// +/// loop { +/// let mut headers = Headers::EMPTY; +/// // Read the next request. +/// let (code, body, should_close) = match connection.next_request().await? { +/// Ok(Some(request)) => { +/// // Only support GET/HEAD to "/", with an empty body. +/// if request.path() != "/" { +/// (StatusCode::NOT_FOUND, "Not found".into(), false) +/// } else if !matches!(request.method(), Method::Get | Method::Head) { +/// // Add the "Allow" header to show the HTTP methods we do +/// // support. +/// headers.add(Header::new(HeaderName::ALLOW, b"GET, HEAD")); +/// let body = "Method not allowed".into(); +/// (StatusCode::METHOD_NOT_ALLOWED, body, false) +/// } else if request.body().len() != 0 { +/// (StatusCode::PAYLOAD_TOO_LARGE, "Not expecting a body".into(), true) +/// } else { +/// // Use the IP address as body. +/// let body = Cow::from(address.ip().to_string()); +/// (StatusCode::OK, body, false) +/// } +/// } +/// // No more requests. +/// Ok(None) => return Ok(()), +/// // Error parsing request. +/// Err(err) => { +/// // Determine the correct status code to return. +/// let code = err.proper_status_code(); +/// // Create a useful error message as body. +/// let body = Cow::from(format!("Bad request: {}", err)); +/// (code, body, err.should_close()) +/// } +/// }; +/// +/// // If we want to close the connection add the "Connection: close" +/// // header. +/// if should_close { +/// headers.add(Header::new(HeaderName::CONNECTION, b"close")); +/// } +/// +/// // Send the body as a single payload. +/// let body = OneshotBody::new(body.as_bytes()); +/// // Respond to the request. +/// connection.respond(code, headers, body).await?; +/// +/// if should_close { +/// return Ok(()); +/// } +/// } +/// } +/// ``` pub struct HttpServer> { inner: TcpServer>, } From 73f52327fd434cc21da86f57a48ed1f75c3fd6cc Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 18 May 2021 11:50:16 +0200 Subject: [PATCH 55/81] Add Headers::clear Removes all headers from the list. --- http/src/header.rs | 8 ++++++++ http/tests/functional/header.rs | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/http/src/header.rs b/http/src/header.rs index 59c17a940..07d27d5b5 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -75,6 +75,14 @@ impl Headers { self.parts.is_empty() } + /// Clear the headers. + /// + /// Removes all headers from the list. + pub fn clear(&mut self) { + self.parts.clear(); + self.values.clear(); + } + /// Add a new `header`. /// /// # Notes diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index 92110a826..6a2c364f3 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -60,6 +60,22 @@ fn headers_from_header() { check_iter(&headers, &[(HeaderName::ALLOW, VALUE)]); } +#[test] +fn clear_headers() { + const ALLOW: &[u8] = b"GET"; + const CONTENT_LENGTH: &[u8] = b"123"; + + let mut headers = Headers::EMPTY; + headers.add(Header::new(HeaderName::ALLOW, ALLOW)); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, CONTENT_LENGTH)); + assert_eq!(headers.len(), 2); + + headers.clear(); + assert_eq!(headers.len(), 0); + assert!(headers.get(&HeaderName::ALLOW).is_none()); + assert!(headers.get(&HeaderName::CONTENT_LENGTH).is_none()); +} + fn check_header<'a, T>( headers: &'a Headers, name: &'_ HeaderName<'_>, From 019c53b1d1b94084c0d9d107b93399edd6f0d222 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 18 May 2021 14:57:06 +0200 Subject: [PATCH 56/81] Add Bytes::limit and LimitedBytes A wrapper struct to limit the amount of bytes used in the Bytes implementation. --- src/net/mod.rs | 57 ++++++++++++++++++++++++++++++++++++++- tests/functional/bytes.rs | 16 +++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/net/mod.rs b/src/net/mod.rs index 511f7a771..da9496a0a 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -118,6 +118,19 @@ pub trait Bytes { /// [`TcpStream::recv_n`] will not work correctly (as the buffer will /// overwrite itself on successive reads). unsafe fn update_length(&mut self, n: usize); + + /// Wrap the buffer in `LimitedBytes`, which limits the amount of bytes used + /// to `limit`. + /// + /// [`LimitedBytes::into_inner`] can be used to retrieve the buffer again, + /// or a mutable reference to the buffer can be used and the limited buffer + /// be dropped after usage. + fn limit(self, limit: usize) -> LimitedBytes + where + Self: Sized, + { + LimitedBytes { buf: self, limit } + } } impl Bytes for &mut B @@ -173,7 +186,6 @@ where /// } /// ``` impl Bytes for Vec { - // NOTE: keep this function in sync with the impl below. fn as_bytes(&mut self) -> &mut [MaybeUninit] { self.spare_capacity_mut() } @@ -193,6 +205,49 @@ impl Bytes for Vec { } } +/// Wrapper to limit the number of bytes `B` can use. +/// +/// See [`Bytes::limit`]. +#[derive(Debug)] +pub struct LimitedBytes { + buf: B, + limit: usize, +} + +impl LimitedBytes { + /// Returns the underlying buffer. + pub fn into_inner(self) -> B { + self.buf + } +} + +impl Bytes for LimitedBytes +where + B: Bytes, +{ + fn as_bytes(&mut self) -> &mut [MaybeUninit] { + let bytes = self.buf.as_bytes(); + if bytes.len() > self.limit { + &mut bytes[..self.limit] + } else { + bytes + } + } + + fn spare_capacity(&self) -> usize { + min(self.buf.spare_capacity(), self.limit) + } + + fn has_spare_capacity(&self) -> bool { + self.spare_capacity() > 0 + } + + unsafe fn update_length(&mut self, n: usize) { + self.buf.update_length(n); + self.limit -= n; + } +} + /// A version of [`IoSliceMut`] that allows the buffer to be uninitialised. /// /// [`IoSliceMut`]: std::io::IoSliceMut diff --git a/tests/functional/bytes.rs b/tests/functional/bytes.rs index c19d776e3..6d2cd5e4c 100644 --- a/tests/functional/bytes.rs +++ b/tests/functional/bytes.rs @@ -85,6 +85,22 @@ fn dont_overwrite_existing_bytes_in_vec() { assert!(!buf.has_spare_capacity()); } +#[test] +fn limited_bytes() { + const LIMIT: usize = 5; + let mut buf = Vec::::with_capacity(2 * DATA.len()).limit(LIMIT); + assert_eq!(buf.spare_capacity(), 5); + assert!(buf.has_spare_capacity()); + + let n = write_bytes(DATA, &mut buf); + assert_eq!(n, LIMIT); + assert_eq!(buf.spare_capacity(), 0); + assert!(!buf.has_spare_capacity()); + let buf = buf.into_inner(); + assert_eq!(&*buf, &DATA[..LIMIT]); + assert_eq!(buf.len(), LIMIT); +} + #[test] fn vectored_array() { let mut bufs = [Vec::with_capacity(1), Vec::with_capacity(DATA.len())]; From 792cb9c5aed14cc95ba9f29b1a4f1ccaf3519cce Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Wed, 19 May 2021 12:19:42 +0200 Subject: [PATCH 57/81] Add BytesVectored::limit Same as Bytes::limit, but then for vectored I/O. --- src/net/mod.rs | 72 +++++++++++++++++++++++++++++++++++++-- tests/functional/bytes.rs | 26 ++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/net/mod.rs b/src/net/mod.rs index da9496a0a..4fcee32e3 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -48,7 +48,7 @@ use std::cmp::min; use std::mem::MaybeUninit; use std::net::SocketAddr; use std::ops::{Deref, DerefMut}; -use std::{fmt, io}; +use std::{fmt, io, slice}; use socket2::SockAddr; @@ -207,7 +207,7 @@ impl Bytes for Vec { /// Wrapper to limit the number of bytes `B` can use. /// -/// See [`Bytes::limit`]. +/// See [`Bytes::limit`] and [`BytesVectored::limit`]. #[derive(Debug)] pub struct LimitedBytes { buf: B, @@ -248,6 +248,49 @@ where } } +impl BytesVectored for LimitedBytes +where + B: BytesVectored, +{ + type Bufs<'b> = B::Bufs<'b>; + + fn as_bufs<'b>(&'b mut self) -> Self::Bufs<'b> { + let mut bufs = self.buf.as_bufs(); + let mut left = self.limit; + let mut iter = bufs.as_mut().iter_mut(); + while let Some(buf) = iter.next() { + let len = buf.len(); + if left > len { + left -= len; + } else { + buf.limit(left); + for buf in iter { + *buf = MaybeUninitSlice::new(&mut []); + } + break; + } + } + bufs + } + + fn spare_capacity(&self) -> usize { + if self.limit == 0 { + 0 + } else { + min(self.buf.spare_capacity(), self.limit) + } + } + + fn has_spare_capacity(&self) -> bool { + self.limit != 0 && self.buf.has_spare_capacity() + } + + unsafe fn update_lengths(&mut self, n: usize) { + self.buf.update_lengths(n); + self.limit -= n; + } +} + /// A version of [`IoSliceMut`] that allows the buffer to be uninitialised. /// /// [`IoSliceMut`]: std::io::IoSliceMut @@ -283,6 +326,18 @@ impl<'a> MaybeUninitSlice<'a> { MaybeUninitSlice(socket2::MaybeUninitSlice::new(buf.as_bytes())) } + fn limit(&mut self, limit: usize) { + let len = self.len(); + assert!(len >= limit); + self.0 = unsafe { + // SAFETY: this should be the line below, but I couldn't figure out + // the lifetime. Since we're only making the slices smaller (as + // checked by the assert above) this should be safe. + //self.0 = socket2::MaybeUninitSlice::new(&mut self[..limit]); + socket2::MaybeUninitSlice::new(slice::from_raw_parts_mut(self.0.as_mut_ptr(), limit)) + }; + } + /// Returns `bufs` as [`socket2::MaybeUninitSlice`]. #[allow(clippy::wrong_self_convention)] fn as_socket2<'b>( @@ -478,6 +533,19 @@ pub trait BytesVectored { /// [`TcpStream::recv_n_vectored`] will not work correctly (as the buffer /// will overwrite itself on successive reads). unsafe fn update_lengths(&mut self, n: usize); + + /// Wrap the buffer in `LimitedBytes`, which limits the amount of bytes used + /// to `limit`. + /// + /// [`LimitedBytes::into_inner`] can be used to retrieve the buffer again, + /// or a mutable reference to the buffer can be used and the limited buffer + /// be dropped after usage. + fn limit(self, limit: usize) -> LimitedBytes + where + Self: Sized, + { + LimitedBytes { buf: self, limit } + } } impl BytesVectored for &mut B diff --git a/tests/functional/bytes.rs b/tests/functional/bytes.rs index 6d2cd5e4c..6718679a7 100644 --- a/tests/functional/bytes.rs +++ b/tests/functional/bytes.rs @@ -142,3 +142,29 @@ fn vectored_tuple() { assert_eq!(bufs.spare_capacity(), 0); assert!(!bufs.has_spare_capacity()); } + +#[test] +fn limited_bytes_vectored() { + const LIMIT: usize = 5; + + let mut bufs = [ + Vec::with_capacity(1), + Vec::with_capacity(DATA.len()), + Vec::with_capacity(10), + ] + .limit(LIMIT); + assert_eq!(bufs.spare_capacity(), LIMIT); + assert!(bufs.has_spare_capacity()); + + let n = write_bytes_vectored(DATA, &mut bufs); + assert_eq!(n, LIMIT); + assert_eq!(bufs.spare_capacity(), 0); + assert!(!bufs.has_spare_capacity()); + let bufs = bufs.into_inner(); + assert_eq!(bufs[0].len(), 1); + assert_eq!(bufs[1].len(), LIMIT - 1); + assert_eq!(bufs[2].len(), 0); + assert_eq!(bufs[0], &DATA[..1]); + assert_eq!(bufs[1], &DATA[1..LIMIT]); + assert_eq!(bufs[2], &[]); +} From 1b5cb7c6a6f1748584e64fb0de0083984734f802 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Wed, 19 May 2021 12:20:43 +0200 Subject: [PATCH 58/81] Move LimitedBytes after BytesVectored trait Since it's now used in both Bytes and BytesVectored. --- src/net/mod.rs | 172 ++++++++++++++++++++++++------------------------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/src/net/mod.rs b/src/net/mod.rs index 4fcee32e3..8ae8719a4 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -205,92 +205,6 @@ impl Bytes for Vec { } } -/// Wrapper to limit the number of bytes `B` can use. -/// -/// See [`Bytes::limit`] and [`BytesVectored::limit`]. -#[derive(Debug)] -pub struct LimitedBytes { - buf: B, - limit: usize, -} - -impl LimitedBytes { - /// Returns the underlying buffer. - pub fn into_inner(self) -> B { - self.buf - } -} - -impl Bytes for LimitedBytes -where - B: Bytes, -{ - fn as_bytes(&mut self) -> &mut [MaybeUninit] { - let bytes = self.buf.as_bytes(); - if bytes.len() > self.limit { - &mut bytes[..self.limit] - } else { - bytes - } - } - - fn spare_capacity(&self) -> usize { - min(self.buf.spare_capacity(), self.limit) - } - - fn has_spare_capacity(&self) -> bool { - self.spare_capacity() > 0 - } - - unsafe fn update_length(&mut self, n: usize) { - self.buf.update_length(n); - self.limit -= n; - } -} - -impl BytesVectored for LimitedBytes -where - B: BytesVectored, -{ - type Bufs<'b> = B::Bufs<'b>; - - fn as_bufs<'b>(&'b mut self) -> Self::Bufs<'b> { - let mut bufs = self.buf.as_bufs(); - let mut left = self.limit; - let mut iter = bufs.as_mut().iter_mut(); - while let Some(buf) = iter.next() { - let len = buf.len(); - if left > len { - left -= len; - } else { - buf.limit(left); - for buf in iter { - *buf = MaybeUninitSlice::new(&mut []); - } - break; - } - } - bufs - } - - fn spare_capacity(&self) -> usize { - if self.limit == 0 { - 0 - } else { - min(self.buf.spare_capacity(), self.limit) - } - } - - fn has_spare_capacity(&self) -> bool { - self.limit != 0 && self.buf.has_spare_capacity() - } - - unsafe fn update_lengths(&mut self, n: usize) { - self.buf.update_lengths(n); - self.limit -= n; - } -} - /// A version of [`IoSliceMut`] that allows the buffer to be uninitialised. /// /// [`IoSliceMut`]: std::io::IoSliceMut @@ -658,6 +572,92 @@ impl_vectored_bytes_tuple! { 4: B0 0, B1 1, B2 2, B3 3 } impl_vectored_bytes_tuple! { 3: B0 0, B1 1, B2 2 } impl_vectored_bytes_tuple! { 2: B0 0, B1 1 } +/// Wrapper to limit the number of bytes `B` can use. +/// +/// See [`Bytes::limit`] and [`BytesVectored::limit`]. +#[derive(Debug)] +pub struct LimitedBytes { + buf: B, + limit: usize, +} + +impl LimitedBytes { + /// Returns the underlying buffer. + pub fn into_inner(self) -> B { + self.buf + } +} + +impl Bytes for LimitedBytes +where + B: Bytes, +{ + fn as_bytes(&mut self) -> &mut [MaybeUninit] { + let bytes = self.buf.as_bytes(); + if bytes.len() > self.limit { + &mut bytes[..self.limit] + } else { + bytes + } + } + + fn spare_capacity(&self) -> usize { + min(self.buf.spare_capacity(), self.limit) + } + + fn has_spare_capacity(&self) -> bool { + self.spare_capacity() > 0 + } + + unsafe fn update_length(&mut self, n: usize) { + self.buf.update_length(n); + self.limit -= n; + } +} + +impl BytesVectored for LimitedBytes +where + B: BytesVectored, +{ + type Bufs<'b> = B::Bufs<'b>; + + fn as_bufs<'b>(&'b mut self) -> Self::Bufs<'b> { + let mut bufs = self.buf.as_bufs(); + let mut left = self.limit; + let mut iter = bufs.as_mut().iter_mut(); + while let Some(buf) = iter.next() { + let len = buf.len(); + if left > len { + left -= len; + } else { + buf.limit(left); + for buf in iter { + *buf = MaybeUninitSlice::new(&mut []); + } + break; + } + } + bufs + } + + fn spare_capacity(&self) -> usize { + if self.limit == 0 { + 0 + } else { + min(self.buf.spare_capacity(), self.limit) + } + } + + fn has_spare_capacity(&self) -> bool { + self.limit != 0 && self.buf.has_spare_capacity() + } + + unsafe fn update_lengths(&mut self, n: usize) { + self.buf.update_lengths(n); + self.limit -= n; + } +} + /// Convert a `socket2:::SockAddr` into a `std::net::SocketAddr`. #[allow(clippy::needless_pass_by_value)] fn convert_address(address: SockAddr) -> io::Result { From aadc921c68b3c4bcd064615f699fc6e0a5c32d87 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Wed, 19 May 2021 13:43:23 +0200 Subject: [PATCH 59/81] Support chunked transfer encoding in HTTP server Still left to do is dropping an unread chunked Body, ensuring that the bytes are all removed from the connection. --- http/examples/my_ip.rs | 2 +- http/src/lib.rs | 3 +- http/src/server.rs | 514 ++++++++++++++++++++++++++++++++--------- 3 files changed, 405 insertions(+), 114 deletions(-) diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index ac2760a5a..2249ee16d 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -95,7 +95,7 @@ async fn http_actor( headers.add(Header::new(HeaderName::ALLOW, b"GET, HEAD")); let body = "Method not allowed".into(); (StatusCode::METHOD_NOT_ALLOWED, body, false) - } else if request.body().len() != 0 { + } else if !request.body().is_empty() { let body = Cow::from("Not expecting a body"); (StatusCode::PAYLOAD_TOO_LARGE, body, true) } else { diff --git a/http/src/lib.rs b/http/src/lib.rs index 13c9b6d26..a35df8678 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -5,7 +5,8 @@ const_panic, generic_associated_types, io_slice_advance, - maybe_uninit_write_slice + maybe_uninit_write_slice, + ready_macro )] #![allow(incomplete_features)] // NOTE: for `generic_associated_types`. diff --git a/http/src/server.rs b/http/src/server.rs index 9d935597d..f39808219 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -22,13 +22,13 @@ use std::io::{self, Write}; use std::mem::MaybeUninit; use std::net::SocketAddr; use std::pin::Pin; +use std::task::ready; use std::task::{self, Poll}; use std::time::SystemTime; use heph::net::{tcp, Bytes, BytesVectored, TcpServer, TcpStream}; use heph::spawn::{ActorOptions, Spawn}; use heph::{actor, rt, Actor, NewActor, Supervisor}; -use httparse::EMPTY_HEADER; use httpdate::HttpDate; use crate::body::BodyLength; @@ -212,7 +212,7 @@ impl Clone for Setup { /// headers.add(Header::new(HeaderName::ALLOW, b"GET, HEAD")); /// let body = "Method not allowed".into(); /// (StatusCode::METHOD_NOT_ALLOWED, body, false) -/// } else if request.body().len() != 0 { +/// } else if !request.body().is_empty() { /// (StatusCode::PAYLOAD_TOO_LARGE, "Not expecting a body".into(), true) /// } else { /// // Use the IP address as body. @@ -340,7 +340,7 @@ pub struct Connection { stream: TcpStream, buf: Vec, /// Number of bytes of `buf` that are already parsed. - /// NOTE: this may be larger then `buf.len()`, which case a `Body` was + /// NOTE: this may be larger then `buf.len()`, in which case a `Body` was /// dropped without reading it entirely. parsed_bytes: usize, /// The HTTP version of the last request. @@ -351,7 +351,7 @@ pub struct Connection { impl Connection { /// Create a new `Connection`. - pub fn new(stream: TcpStream) -> Connection { + fn new(stream: TcpStream) -> Connection { Connection { stream, buf: Vec::with_capacity(BUF_SIZE), @@ -447,7 +447,7 @@ impl Connection { } } - let mut headers = [EMPTY_HEADER; MAX_HEADERS]; + let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS]; let mut request = httparse::Request::new(&mut headers); // SAFETY: because we received until at least `self.parsed_bytes >= // self.buf.len()` above, we can safely slice the buffer.. @@ -467,7 +467,7 @@ impl Connection { self.last_version = Some(version); // RFC 7230 section 3.3.3 Message Body Length. - let mut body_length: Option = None; + let mut body_length: Option = None; let res = Headers::from_httparse_headers(request.headers, |name, value| { if *name == HeaderName::CONTENT_LENGTH { // RFC 7230 section 3.3.3 point 4: @@ -483,36 +483,64 @@ impl Connection { // > the connection. if let Ok(length) = FromHeaderValue::from_bytes(value) { match body_length.as_mut() { - Some(body_length) if *body_length == length => {} - Some(_) => return Err(RequestError::DifferentContentLengths), - None => body_length = Some(length), + Some(BodyLength::Known(body_length)) + if *body_length == length => {} + Some(BodyLength::Known(_)) => { + return Err(RequestError::DifferentContentLengths) + } + Some(BodyLength::Chunked) => { + return Err(RequestError::ContentLengthAndTransferEncoding) + } + None => body_length = Some(BodyLength::Known(length)), } } else { return Err(RequestError::InvalidContentLength); } } else if *name == HeaderName::TRANSFER_ENCODING { - todo!("transfer encoding"); - - // TODO: we can support chunked, but for other - // encoding we need external packages (for compress, - // deflate, gzip). - // Not supported transfer-encoding respond with 501 - // (Not Implemented). - // - // RFC 7230 section 3.3.3 point 3: - // > If a Transfer-Encoding header field is present - // > in a request and the chunked transfer coding is - // > not the final encoding, the message body length - // > cannot be determined reliably; the server MUST - // > respond with the 400 (Bad Request) status code - // > and then close the connection. - // > - // > If a message is received with both a - // > Transfer-Encoding and a Content-Length header - // > field, the Transfer-Encoding overrides the - // > Content-Length. [..] A sender MUST remove the - // > received Content-Length field prior to - // > forwarding such a message downstream. + let mut encodings = value.split(|b| *b == b',').peekable(); + while let Some(encoding) = encodings.next() { + match trim_ws(encoding) { + b"chunked" => { + // RFC 7230 section 3.3.3 point 3: + // > If a Transfer-Encoding header field + // > is present in a request and the + // > chunked transfer coding is not the + // > final encoding, the message body + // > length cannot be determined + // > reliably; the server MUST respond + // > with the 400 (Bad Request) status + // > code and then close the connection. + if encodings.peek().is_some() { + return Err( + RequestError::ChunkedNotLastTransferEncoding, + ); + } + + // RFC 7230 section 3.3.3 point 3: + // > If a message is received with both + // > a Transfer-Encoding and a + // > Content-Length header field, the + // > Transfer-Encoding overrides the + // > Content-Length. Such a message + // > might indicate an attempt to + // > perform request smuggling (Section + // > 9.5) or response splitting (Section + // > 9.4) and ought to be handled as an + // > error. + if body_length.is_some() { + return Err( + RequestError::ContentLengthAndTransferEncoding, + ); + } + + body_length = Some(BodyLength::Chunked); + } + b"identity" => {} // No changes. + // TODO: support "compress", "deflate" and + // "gzip". + _ => return Err(RequestError::UnsupportedTransferEncoding), + } + } } Ok(()) }); @@ -521,23 +549,34 @@ impl Connection { Err(err) => return Ok(Err(err)), }; - // TODO: RFC 7230 section 3.3.3: - // > A server MAY reject a request that contains a message - // > body but not a Content-Length by responding with 411 - // > (Length Required). - // Maybe do this for POST/PUT/etc. that (usually) requires a - // body? - - // RFC 7230 section 3.3.3 point 6: - // > If this is a request message and none of the above are - // > true, then the message body length is zero (no message - // > body is present). - let size = body_length.unwrap_or(0); - - let body = Body { - conn: self, - left: size, + let kind = match body_length { + Some(BodyLength::Known(left)) => BodyKind::Oneshot { left }, + Some(BodyLength::Chunked) => { + match httparse::parse_chunk_size(&self.buf[self.parsed_bytes..]) { + Ok(httparse::Status::Complete((idx, chunk_size))) => { + self.parsed_bytes += idx; + let read_complete = if chunk_size == 0 { true } else { false }; + BodyKind::Chunked { + // FIXME: add check here. It's fine on + // 64 bit (only currently supported). + left_in_chunk: chunk_size as usize, + read_complete, + } + } + Ok(httparse::Status::Partial) => BodyKind::Chunked { + left_in_chunk: 0, + read_complete: false, + }, + Err(_) => return Ok(Err(RequestError::InvalidChunkSize)), + } + } + // RFC 7230 section 3.3.3 point 6: + // > If this is a request message and none of the above + // > are true, then the message body length is zero (no + // > message body is present). + None => BodyKind::Oneshot { left: 0 }, }; + let body = Body { conn: self, kind }; return Ok(Ok(Some(Request::new(method, path, version, headers, body)))); } Ok(httparse::Status::Partial) => { @@ -578,7 +617,7 @@ impl Connection { /// # #[allow(unreachable_code)] /// # { /// let mut conn: Connection = /* From HttpServer. */ - /// # todo!(); + /// # panic!("can't actually run example"); /// /// // Reading a request returned this error. /// let err = RequestError::IncompleteRequest; @@ -690,6 +729,7 @@ impl Connection { // Format the headers (RFC 7230 section 3.2). let mut set_connection_header = false; let mut set_content_length_header = false; + let mut set_transfer_encoding_header = false; let mut set_date_header = false; for header in response.headers().iter() { let name = header.name(); @@ -706,6 +746,8 @@ impl Connection { set_connection_header = true; } else if name == &HeaderName::CONTENT_LENGTH { set_content_length_header = true; + } else if name == &HeaderName::TRANSFER_ENCODING { + set_transfer_encoding_header = true; } else if name == &HeaderName::DATE { set_date_header = true; } @@ -725,18 +767,21 @@ impl Connection { write!(&mut self.buf, "Date: {}\r\n", now).unwrap(); } - // Provide the "Conent-Length" header if the user didn't. - if !set_content_length_header { - let body_length = match response.body().length() { - _ if !request_method.expects_body() || !response.status().includes_body() => 0, - BodyLength::Known(length) => length, - BodyLength::Chunked => todo!("chunked response body"), - }; - - self.buf.extend_from_slice(b"Content-Length: "); - self.buf - .extend_from_slice(itoa_buf.format(body_length).as_bytes()); - self.buf.extend_from_slice(b"\r\n"); + // Provide the "Conent-Length" or "Transfer-Encoding" header if the user + // didn't. + if !set_content_length_header && !set_transfer_encoding_header { + match response.body().length() { + _ if !request_method.expects_body() || !response.status().includes_body() => { + extend_content_length_header(&mut self.buf, &mut itoa_buf, 0) + } + BodyLength::Known(length) => { + extend_content_length_header(&mut self.buf, &mut itoa_buf, length) + } + BodyLength::Chunked => { + self.buf + .extend_from_slice(b"Transfer-Encoding: chunked\r\n"); + } + } } // End of the header. @@ -765,6 +810,25 @@ impl Connection { // TODO: move bytes to the start. } + + /// Recv bytes from the underlying stream, reading into `self.buf`. + /// + /// Returns an `UnexpectedEof` error if zero bytes are received. + fn recv(&mut self) -> Poll> { + // Ensure we have space in the buffer to read into. + self.clear_buffer(); + self.buf.reserve(MIN_READ_SIZE); + + loop { + match self.stream.try_recv(&mut self.buf) { + Ok(0) => return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())), + Ok(n) => return Poll::Ready(Ok(n)), + Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => return Poll::Pending, + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, + Err(err) => return Poll::Ready(Err(err)), + } + } + } } const fn map_version(version: u8) -> Version { @@ -780,6 +844,28 @@ const fn map_version(version: u8) -> Version { } } +/// Trim whitespace from `value`. +fn trim_ws(value: &[u8]) -> &[u8] { + let start = value.iter().position(|b| !b.is_ascii_whitespace()); + let end = value.iter().rposition(|b| !b.is_ascii_whitespace()); + if let (Some(start), Some(end)) = (start, end) { + &value[start..end] + } else { + &[] + } +} + +/// Add "Content-Length" header to `buf`. +fn extend_content_length_header( + buf: &mut Vec, + itoa_buf: &mut itoa::Buffer, + content_length: usize, +) { + buf.extend_from_slice(b"Content-Length: "); + buf.extend_from_slice(itoa_buf.format(content_length).as_bytes()); + buf.extend_from_slice(b"\r\n"); +} + /// Body of HTTP [`Request`] read from a [`Connection`]. /// /// # Notes @@ -789,30 +875,79 @@ const fn map_version(version: u8) -> Version { #[derive(Debug)] pub struct Body<'a> { conn: &'a mut Connection, - /// Number of unread (by the user) bytes. - left: usize, + kind: BodyKind, +} + +#[derive(Debug)] +enum BodyKind { + /// No encoding. + Oneshot { + /// Number of unread (by the user) bytes. + left: usize, + }, + /// Chunked transfer encoding. + Chunked { + /// Number of unread (by the user) bytes in this chunk. + left_in_chunk: usize, + /// Read all chunks. + read_complete: bool, + }, } impl<'a> Body<'a> { - /// Returns the length of the body (in bytes) *left*. + /// Returns the length of the body (in bytes) *left*, or a /// /// Calling this before [`recv`] or [`recv_vectored`] will return the /// original body length, after removing bytes from the body this will - /// return the remaining length. + /// return the *remaining* length. /// - /// The body length is determined by the "Content-Length" header, or 0 if - /// not present. + /// The body length is determined by the "Content-Length" or + /// "Transfer-Encoding" header, or 0 if neither are present. /// /// [`recv`]: Body::recv /// [`recv_vectored`]: Body::recv_vectored - pub const fn len(&self) -> usize { - self.left + pub fn len(&self) -> BodyLength { + match self.kind { + BodyKind::Oneshot { left } => BodyLength::Known(left), + BodyKind::Chunked { .. } => BodyLength::Chunked, + } + } + + /// Return the length of this chunk *left*, or the entire body in case of a + /// oneshot body. + fn chunk_len(&self) -> usize { + match self.kind { + BodyKind::Oneshot { left } => left, + BodyKind::Chunked { left_in_chunk, .. } => left_in_chunk, + } } /// Returns `true` if the body is completely read (or was empty to begin /// with). - pub const fn is_empty(&self) -> bool { - self.left == 0 + /// + /// # Notes + /// + /// This can return `false` for empty bodies using chunked encoding if not + /// enough bytes have been read yet. Using chunked encoding we don't know + /// the length upfront as it it's determined by reading the length of each + /// chunk. If the send request only contained the HTTP head (i.e. no body) + /// and uses chunked encoding this would return `false`, as body length is + /// unkown and thus not empty. However if the body would then send a single + /// empty chunk (signaling the end of the body), this would return `true` as + /// it turns out the body is indeed empty. + pub fn is_empty(&self) -> bool { + match self.kind { + BodyKind::Oneshot { left } => left == 0, + BodyKind::Chunked { + left_in_chunk, + read_complete, + } => read_complete && left_in_chunk == 0, + } + } + + /// Returns `true` if the body is chunked. + pub fn is_chunked(&self) -> bool { + matches!(self.kind, BodyKind::Chunked { .. }) } /// Receive bytes from the request body, writing them into `buf`. @@ -832,22 +967,30 @@ impl<'a> Body<'a> { } /// Returns the bytes currently in the buffer. - /// This is limited to the bytes of this request, i.e. it doesn't contain + /// + /// This is limited to the bytes of this request/chunk, i.e. it doesn't + /// contain the next request/chunk. fn buf_bytes(&self) -> &[u8] { let bytes = &self.conn.buf[self.conn.parsed_bytes..]; - if bytes.len() > self.left { - &bytes[..self.left] + let left = match self.kind { + BodyKind::Oneshot { left } => left, + BodyKind::Chunked { left_in_chunk, .. } => left_in_chunk, + }; + if bytes.len() > left { + &bytes[..left] } else { bytes } } /// Copy already read bytes. + /// + /// Same as [`Body::buf_bytes`] this is limited to the bytes of this + /// request/chunk, i.e. it doesn't contain the next request/chunk. fn copy_buf_bytes(&mut self, dst: &mut [MaybeUninit]) -> usize { let bytes = self.buf_bytes(); - let len = bytes.len(); + let len = min(bytes.len(), dst.len()); if len != 0 { - let len = min(len, dst.len()); MaybeUninit::write_slice(&mut dst[..len], &bytes[..len]); self.processed(len); } @@ -856,11 +999,55 @@ impl<'a> Body<'a> { /// Mark `n` bytes are processed. fn processed(&mut self, n: usize) { - self.left -= n; + // TODO: should this be `unsafe`? We don't do underflow checks... + match &mut self.kind { + BodyKind::Oneshot { left } => *left -= n, + BodyKind::Chunked { left_in_chunk, .. } => *left_in_chunk -= n, + } self.conn.parsed_bytes += n; } } +/// Read a chunk form `conn`. +/// +/// Returns an I/O error, or an `InvalidData` error if the chunk size is +/// invalid. +fn read_chunk( + conn: &mut Connection, + // Fields of `BodyKind::Chunked`: + left_in_chunk: &mut usize, + read_complete: &mut bool, +) -> Poll> { + // TODO: check buffer, might contains chunk. + + // Ensure we have space in the buffer to read into. + conn.clear_buffer(); + conn.buf.reserve(MIN_READ_SIZE); + + loop { + ready!(conn.recv())?; + match httparse::parse_chunk_size(&conn.buf) { + Ok(httparse::Status::Complete((idx, chunk_size))) => { + conn.parsed_bytes += idx; + if chunk_size == 0 { + *read_complete = true; + } + // FIXME: add check here. It's fine on 64 bit (only currently + // supported). + *left_in_chunk = chunk_size as usize; + return Poll::Ready(Ok(())); + } + Ok(httparse::Status::Partial) => continue, // Read some more data. + Err(_) => { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid chunk size", + ))) + } + } + } +} + /// The [`Future`] behind [`Body::recv`]. #[derive(Debug)] #[must_use = "futures do nothing unless you `.await` or poll them"] @@ -878,16 +1065,42 @@ where fn poll(self: Pin<&mut Self>, _: &mut task::Context<'_>) -> Poll { let Recv { body, buf } = Pin::into_inner(self); - // Copy already read bytes. - let len = body.copy_buf_bytes(buf.as_bytes()); - if len != 0 { - unsafe { buf.update_length(len) }; + let mut len = 0; + loop { + // Copy bytes in our buffer. + len += body.copy_buf_bytes(buf.as_bytes()); + if len != 0 { + unsafe { buf.update_length(len) }; + } + + let limit = body.chunk_len(); + if limit == 0 { + match &mut body.kind { + // Read all the bytes from the oneshot body. + BodyKind::Oneshot { .. } => return Poll::Ready(Ok(len)), + // Read all the bytes in the chunk, so need to read another + // chunk. + BodyKind::Chunked { + left_in_chunk, + read_complete, + } => { + ready!(read_chunk(&mut body.conn, left_in_chunk, read_complete))?; + // Copy read bytes again. + continue; + } + } + } else { + // Continue to reading below. + break; + } } // Read from the stream if there is space left. if buf.has_spare_capacity() { + // Limit the read until the end of the chunk/body. + let limit = body.chunk_len(); loop { - match body.conn.stream.try_recv(&mut *buf) { + match body.conn.stream.try_recv(buf.limit(limit)) { Ok(n) => return Poll::Ready(Ok(len + n)), Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { return if len == 0 { @@ -900,8 +1113,9 @@ where Err(err) => return Poll::Ready(Err(err)), } } + } else { + Poll::Ready(Ok(len)) } - Poll::Ready(Ok(len)) } } @@ -922,22 +1136,47 @@ where fn poll(self: Pin<&mut Self>, _: &mut task::Context<'_>) -> Poll { let RecvVectored { body, bufs } = Pin::into_inner(self); - // Copy already read bytes. let mut len = 0; - for buf in bufs.as_bufs().as_mut() { - match body.copy_buf_bytes(buf) { - 0 => break, - n => len += n, + loop { + // Copy bytes in our buffer. + for buf in bufs.as_bufs().as_mut() { + match body.copy_buf_bytes(buf) { + 0 => break, + n => len += n, + } + } + if len != 0 { + unsafe { bufs.update_lengths(len) }; + } + + let limit = body.chunk_len(); + if limit == 0 { + match &mut body.kind { + // Read all the bytes from the oneshot body. + BodyKind::Oneshot { .. } => return Poll::Ready(Ok(len)), + // Read all the bytes in the chunk, so need to read another + // chunk. + BodyKind::Chunked { + left_in_chunk, + read_complete, + } => { + ready!(read_chunk(&mut body.conn, left_in_chunk, read_complete))?; + // Copy read bytes again. + continue; + } + } + } else { + // Continue to reading below. + break; } - } - if len != 0 { - unsafe { bufs.update_lengths(len) }; } // Read from the stream if there is space left. if bufs.has_spare_capacity() { + // Limit the read until the end of the chunk/body. + let limit = body.chunk_len(); loop { - match body.conn.stream.try_recv_vectored(&mut *bufs) { + match body.conn.stream.try_recv_vectored(bufs.limit(limit)) { Ok(n) => return Poll::Ready(Ok(len + n)), Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { return if len == 0 { @@ -950,14 +1189,15 @@ where Err(err) => return Poll::Ready(Err(err)), } } + } else { + Poll::Ready(Ok(len)) } - Poll::Ready(Ok(len)) } } impl<'a> crate::Body<'a> for Body<'a> { fn length(&self) -> BodyLength { - BodyLength::Known(self.left) + self.len() } } @@ -965,11 +1205,11 @@ mod private { use std::future::Future; use std::io; use std::pin::Pin; - use std::task::{self, Poll}; + use std::task::{self, ready, Poll}; use heph::net::TcpStream; - use super::{Body, MIN_READ_SIZE}; + use super::{read_chunk, Body, BodyKind}; #[derive(Debug)] pub struct SendBody<'c, 's, 'h> { @@ -1000,8 +1240,14 @@ mod private { } } - while body.left != 0 { + while !body.is_empty() { + let limit = body.chunk_len(); let bytes = body.buf_bytes(); + let bytes = if bytes.len() > limit { + &bytes[..limit] + } else { + bytes + }; // TODO: maybe read first if we have less then N bytes? if !bytes.is_empty() { match stream.try_send(bytes) { @@ -1016,20 +1262,25 @@ mod private { Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, Err(err) => return Poll::Ready(Err(err)), } + // NOTE: we don't continue here, we always return on start + // the next iteration of the loop. } - // Ensure we have space in the buffer to read into. - body.conn.clear_buffer(); - body.conn.buf.reserve(MIN_READ_SIZE); - match body.conn.stream.try_recv(&mut body.conn.buf) { - Ok(0) => return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into())), - // Continue to sending the bytes above. - Ok(_) => continue, - Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { - return Poll::Pending + // Read some more data, or the next chunk. + match &mut body.kind { + BodyKind::Oneshot { .. } => { + let _ = ready!(body.conn.recv())?; + } + BodyKind::Chunked { + left_in_chunk, + read_complete, + } => { + if *left_in_chunk == 0 { + ready!(read_chunk(&mut body.conn, left_in_chunk, read_complete))?; + } else { + ready!(body.conn.recv())?; + } } - Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue, - Err(err) => return Poll::Ready(Err(err)), } } @@ -1067,7 +1318,11 @@ impl<'a> Drop for Body<'a> { // Mark the entire body as parsed. // NOTE: `Connection` handles the case where we didn't read the entire // body yet. - self.conn.parsed_bytes += self.left; + match self.kind { + BodyKind::Oneshot { left } => self.conn.parsed_bytes += left, + // FIXME: don't panic here. + BodyKind::Chunked { .. } => todo!("remove chunked body from connection"), + } } } @@ -1090,6 +1345,22 @@ pub enum RequestError { InvalidHeaderValue, /// Number of headers send in the request is larger than [`MAX_HEADERS`]. TooManyHeaders, + /// Unsupported "Transfer-Encoding" header. + UnsupportedTransferEncoding, + /// Request has a "Transfer-Encoding" header with a chunked encoding, but + /// it's not the final encoding, then the message body length cannot be + /// determined reliably. + /// + /// See RFC 7230 section 3.3.3 point 3. + ChunkedNotLastTransferEncoding, + /// Request contains both "Content-Length" and "Transfer-Encoding" headers. + /// + /// An attacker might attempt to "smuggle a request" ("HTTP Request + /// Smuggling", Linhart et al., June 2005) or "split a response" ("Divide + /// and Conquer - HTTP Response Splitting, Web Cache Poisoning Attacks, and + /// Related Topics", Klein, March 2004). RFC 7230 (see section 3.3.3 point + /// 3) says that this "ought to be handled as an error", and so we do. + ContentLengthAndTransferEncoding, /// Invalid byte where token is required. InvalidToken, /// Invalid byte in new line. @@ -1098,6 +1369,8 @@ pub enum RequestError { InvalidVersion, /// Unknown HTTP method, not in [`Method`]. UnknownMethod, + /// Chunk size is invalid. + InvalidChunkSize, } impl RequestError { @@ -1114,14 +1387,22 @@ impl RequestError { | InvalidHeaderName | InvalidHeaderValue | TooManyHeaders + | ChunkedNotLastTransferEncoding + | ContentLengthAndTransferEncoding | InvalidToken | InvalidNewLine - | InvalidVersion => StatusCode::BAD_REQUEST, + | InvalidVersion + | InvalidChunkSize=> StatusCode::BAD_REQUEST, + // RFC 7230 section 3.3.1: + // > A server that receives a request message with a transfer coding + // > it does not understand SHOULD respond with 501 (Not + // > Implemented). + UnsupportedTransferEncoding // RFC 7231 section 4.1: // > When a request method is received that is unrecognized or not // > implemented by an origin server, the origin server SHOULD // > respond with the 501 (Not Implemented) status code. - UnknownMethod => StatusCode::NOT_IMPLEMENTED, + | UnknownMethod => StatusCode::NOT_IMPLEMENTED, } } @@ -1139,10 +1420,13 @@ impl RequestError { | InvalidHeaderName | InvalidHeaderValue | TooManyHeaders + | ChunkedNotLastTransferEncoding + | ContentLengthAndTransferEncoding | InvalidToken | InvalidNewLine - | InvalidVersion => true, - UnknownMethod => false, + | InvalidVersion + | InvalidChunkSize => true, + UnsupportedTransferEncoding | UnknownMethod => false, } } @@ -1172,9 +1456,15 @@ impl fmt::Display for RequestError { InvalidHeaderName => "invalid header name", InvalidHeaderValue => "invalid header value", TooManyHeaders => "too many header", + UnsupportedTransferEncoding => "unsupported Transfer-Encoding header", + ChunkedNotLastTransferEncoding => "invalid Transfer-Encoding header", + ContentLengthAndTransferEncoding => { + "provided both Content-Length and Transfer-Encoding headers" + } InvalidToken | InvalidNewLine => "invalid request syntax", InvalidVersion => "invalid version", UnknownMethod => "unknown method", + InvalidChunkSize => "invalid chunk size", }) } } From b5a55c3366890b68a989f450e928b791facfd883 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Wed, 19 May 2021 13:44:14 +0200 Subject: [PATCH 60/81] Simplify Headers::from_httparse_headers --- http/src/header.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index 07d27d5b5..4023381f3 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -57,9 +57,7 @@ impl Headers { for header in raw_headers { let name = HeaderName::from_str(header.name); let value = header.value; - if let Err(err) = f(&name, value) { - return Err(err); - } + f(&name, value)?; headers._add(name, value); } Ok(headers) From 94da933d578bfcd426ea6e481fb1dde8d7be7151 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Wed, 19 May 2021 14:01:52 +0200 Subject: [PATCH 61/81] Change Connection::{send_response,respond} API Connection::respond now accepts a reference to the Headers, allowing the allocating to be reused (see the my_ip example changes). Connection::send_response now accepts the fields of the Response as arguments because Headers is owned in it, but accepting a &Response doesn't work because we do need ownership of the body. --- http/examples/my_ip.rs | 5 ++-- http/src/server.rs | 68 +++++++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/http/examples/my_ip.rs b/http/examples/my_ip.rs index 2249ee16d..0a3a1509e 100644 --- a/http/examples/my_ip.rs +++ b/http/examples/my_ip.rs @@ -83,8 +83,8 @@ async fn http_actor( connection.set_nodelay(true)?; let mut read_timeout = READ_TIMEOUT; + let mut headers = Headers::EMPTY; loop { - let mut headers = Headers::EMPTY; let fut = Deadline::after(&mut ctx, read_timeout, connection.next_request()); let (code, body, should_close) = match fut.await? { Ok(Some(request)) => { @@ -125,7 +125,7 @@ async fn http_actor( code, body, address ); let body = OneshotBody::new(body.as_bytes()); - let write_response = connection.respond(code, headers, body); + let write_response = connection.respond(code, &headers, body); Deadline::after(&mut ctx, WRITE_TIMEOUT, write_response).await?; if should_close { @@ -136,5 +136,6 @@ async fn http_actor( // Now that we've read a single request we can wait a little for the // next one so that we can reuse the resources for the next request. read_timeout = ALIVE_TIMEOUT; + headers.clear(); } } diff --git a/http/src/server.rs b/http/src/server.rs index f39808219..da32a5730 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -33,7 +33,7 @@ use httpdate::HttpDate; use crate::body::BodyLength; use crate::header::{FromHeaderValue, HeaderName, Headers}; -use crate::{Method, Request, Response, StatusCode, Version}; +use crate::{Method, Request, StatusCode, Version}; /// Maximum size of the header (the start line and the headers). /// @@ -105,6 +105,8 @@ impl Clone for Setup { /// Similar to `TcpServer` this type works with thread-safe and thread-local /// actors. /// +/// [`Response`]: crate::Response +/// /// # Graceful shutdown /// /// Graceful shutdown is done by sending it a [`Terminate`] message. The HTTP @@ -198,8 +200,8 @@ impl Clone for Setup { /// // Set `TCP_NODELAY` on the `TcpStream`. /// connection.set_nodelay(true)?; /// +/// let mut headers = Headers::EMPTY; /// loop { -/// let mut headers = Headers::EMPTY; /// // Read the next request. /// let (code, body, should_close) = match connection.next_request().await? { /// Ok(Some(request)) => { @@ -241,11 +243,12 @@ impl Clone for Setup { /// // Send the body as a single payload. /// let body = OneshotBody::new(body.as_bytes()); /// // Respond to the request. -/// connection.respond(code, headers, body).await?; +/// connection.respond(code, &headers, body).await?; /// /// if should_close { /// return Ok(()); /// } +/// headers.clear(); /// } /// } /// ``` @@ -334,7 +337,7 @@ where /// are send to. /// /// [HTTP requests]: Request -/// [HTTP responses]: Response +/// [HTTP responses]: crate::Response #[derive(Debug)] pub struct Connection { stream: TcpStream, @@ -628,13 +631,12 @@ impl Connection { /// let version = conn.last_request_version().unwrap_or(Version::Http11); /// let body = format!("Bad request: {}", err); /// let body = OneshotBody::new(body.as_bytes()); - /// let response = Response::new(version, StatusCode::BAD_REQUEST, Headers::EMPTY, body); /// /// // We can use `last_request_method` to determine the method of the last /// // request, which is used to determine if we need to send a body. /// let request_method = conn.last_request_method().unwrap_or(Method::Get); /// // Respond with the response. - /// conn.send_response(request_method, response); + /// conn.send_response(request_method, version, StatusCode::BAD_REQUEST, &Headers::EMPTY, body); /// /// // Close the connection if the error is fatal. /// if err.should_close() { @@ -675,7 +677,7 @@ impl Connection { pub async fn respond<'b, B>( &mut self, status: StatusCode, - headers: Headers, + headers: &Headers, body: B, ) -> io::Result<()> where @@ -683,28 +685,42 @@ impl Connection { { let req_method = self.last_method.unwrap_or(Method::Get); let version = self.last_version.unwrap_or(Version::Http11).highest_minor(); - let response = Response::new(version, status, headers, body); - self.send_response(req_method, response).await + self.send_response(req_method, version, status, headers, body) + .await } /// Send a [`Response`]. /// + /// Arguments: + /// * `request_method` is the method used by the [`Request`], used to + /// determine if a body needs to be send. + /// * `version`, `status`, `headers` and `body` make up the HTTP + /// [`Response`]. + /// + /// In most cases it's easier to use [`Connection::respond`], only when + /// reading two requests before responding is this function useful. + /// + /// [`Response`]: crate::Response + /// /// # Notes /// - /// This automatically sets the "Content-Length", "Connection" and "Date" - /// headers if not provided in `response`. + /// This automatically sets the "Content-Length" or "Transfer-Encoding", + /// "Connection" and "Date" headers if not provided in `headers`. /// - /// If `request_method.`[`expects_body`] or - /// `response.status().`[`includes_body`] returns false this will not write - /// the body to the connection. + /// If `request_method.`[`expects_body()`] or `status.`[`includes_body()`] + /// returns `false` this will not write the body to the connection. /// - /// [`expects_body`]: Method::expects_body - /// [`includes_body`]: StatusCode::includes_body + /// [`expects_body()`]: Method::expects_body + /// [`includes_body()`]: StatusCode::includes_body #[allow(clippy::future_not_send)] pub async fn send_response<'b, B>( &mut self, request_method: Method, - response: Response, + // Response data: + version: Version, + status: StatusCode, + headers: &Headers, + body: B, ) -> io::Result<()> where B: crate::Body<'b>, @@ -717,11 +733,10 @@ impl Connection { let ignore_end = self.buf.len(); // Format the status-line (RFC 7230 section 3.1.2). - self.buf - .extend_from_slice(response.version().as_str().as_bytes()); + self.buf.extend_from_slice(version.as_str().as_bytes()); self.buf.push(b' '); self.buf - .extend_from_slice(itoa_buf.format(response.status().0).as_bytes()); + .extend_from_slice(itoa_buf.format(status.0).as_bytes()); // NOTE: we're not sending a reason-phrase, but the space is required // before \r\n. self.buf.extend_from_slice(b" \r\n"); @@ -731,7 +746,7 @@ impl Connection { let mut set_content_length_header = false; let mut set_transfer_encoding_header = false; let mut set_date_header = false; - for header in response.headers().iter() { + for header in headers.iter() { let name = header.name(); // Field-name: self.buf.extend_from_slice(name.as_ref().as_bytes()); @@ -754,7 +769,7 @@ impl Connection { } // Provide the "Connection" header if the user didn't. - if !set_connection_header && matches!(response.version(), Version::Http10) { + if !set_connection_header && matches!(version, Version::Http10) { // Per RFC 7230 section 6.3, HTTP/1.0 needs the "Connection: // keep-alive" header to persistent the connection. Connections // using HTTP/1.1 persistent by default. @@ -770,8 +785,8 @@ impl Connection { // Provide the "Conent-Length" or "Transfer-Encoding" header if the user // didn't. if !set_content_length_header && !set_transfer_encoding_header { - match response.body().length() { - _ if !request_method.expects_body() || !response.status().includes_body() => { + match body.length() { + _ if !request_method.expects_body() || !status.includes_body() => { extend_content_length_header(&mut self.buf, &mut itoa_buf, 0) } BodyLength::Known(length) => { @@ -789,10 +804,7 @@ impl Connection { // Write the response to the stream. let head = &self.buf[ignore_end..]; - response - .into_body() - .write_response(&mut self.stream, head) - .await?; + body.write_response(&mut self.stream, head).await?; // Remove the response headers from the buffer. self.buf.truncate(ignore_end); From be251cf746802fa467531e0f97c08761af50a336 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Wed, 19 May 2021 14:21:24 +0200 Subject: [PATCH 62/81] Mark some functions as constant --- http/src/body.rs | 8 ++------ http/src/header.rs | 4 ++-- http/src/request.rs | 4 ++-- http/src/response.rs | 4 ++-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/http/src/body.rs b/http/src/body.rs index 63ac8923a..e88263b65 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -330,9 +330,7 @@ where B: Stream>, { /// Use a [`Stream`] as HTTP body. - // TODO: make this a `const` fn once trait bounds (`FileSend`) on `const` - // functions are stable. - pub fn new(length: usize, stream: B) -> StreamingBody<'b, B> { + pub const fn new(length: usize, stream: B) -> StreamingBody<'b, B> { StreamingBody { length, body: stream, @@ -402,9 +400,7 @@ where /// /// This uses the bytes `offset..end` from `file` as HTTP body and sends /// them using `sendfile(2)` (using [`TcpStream::send_file`]). - // TODO: make this a `const` fn once trait bounds (`FileSend`) on `const` - // functions are stable. - pub fn new(file: &'f F, offset: usize, end: NonZeroUsize) -> FileBody<'f, F> { + pub const fn new(file: &'f F, offset: usize, end: NonZeroUsize) -> FileBody<'f, F> { debug_assert!(end.get() >= offset); FileBody { file, offset, end } } diff --git a/http/src/header.rs b/http/src/header.rs index 4023381f3..3dbe462ed 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -130,7 +130,7 @@ impl Headers { /// Returns an iterator over all headers. /// /// The order is unspecified. - pub fn iter<'a>(&'a self) -> Iter<'a> { + pub const fn iter<'a>(&'a self) -> Iter<'a> { Iter { headers: self, pos: 0, @@ -863,7 +863,7 @@ impl HeaderName<'static> { /// This is only header to test [`HeaderName::from_str`], not part of the /// stable API. #[doc(hidden)] - pub fn is_heap_allocated(&self) -> bool { + pub const fn is_heap_allocated(&self) -> bool { matches!(self.inner, Cow::Owned(_)) } } diff --git a/http/src/request.rs b/http/src/request.rs index 36c1f43a6..5f4030c64 100644 --- a/http/src/request.rs +++ b/http/src/request.rs @@ -59,7 +59,7 @@ impl Request { } /// Returns mutable access to the headers. - pub fn headers_mut(&mut self) -> &mut Headers { + pub const fn headers_mut(&mut self) -> &mut Headers { &mut self.headers } @@ -69,7 +69,7 @@ impl Request { } /// Mutable access to the request body. - pub fn body_mut(&mut self) -> &mut B { + pub const fn body_mut(&mut self) -> &mut B { &mut self.body } } diff --git a/http/src/response.rs b/http/src/response.rs index d1b0cb643..affafa607 100644 --- a/http/src/response.rs +++ b/http/src/response.rs @@ -42,7 +42,7 @@ impl Response { } /// Returns mutable access to the headers. - pub fn headers_mut(&mut self) -> &mut Headers { + pub const fn headers_mut(&mut self) -> &mut Headers { &mut self.headers } @@ -52,7 +52,7 @@ impl Response { } /// Returns a mutable reference to the body. - pub fn body_mut(&mut self) -> &mut B { + pub const fn body_mut(&mut self) -> &mut B { &mut self.body } From a48630c9f6a720592b0961dd083386c147e79720 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Wed, 19 May 2021 14:37:35 +0200 Subject: [PATCH 63/81] Fix Clippy warnings --- http/src/lib.rs | 3 ++- http/src/server.rs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/http/src/lib.rs b/http/src/lib.rs index a35df8678..f60197084 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -6,7 +6,8 @@ generic_associated_types, io_slice_advance, maybe_uninit_write_slice, - ready_macro + ready_macro, + stmt_expr_attributes )] #![allow(incomplete_features)] // NOTE: for `generic_associated_types`. diff --git a/http/src/server.rs b/http/src/server.rs index da32a5730..13a2b06c6 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -418,6 +418,7 @@ impl Connection { /// Also see the [`Connection::last_request_version`] and /// [`Connection::last_request_method`] functions to properly respond to /// request errors. + #[allow(clippy::too_many_lines)] // TODO. pub async fn next_request<'a>( &'a mut self, ) -> io::Result>>, RequestError>> { @@ -555,15 +556,15 @@ impl Connection { let kind = match body_length { Some(BodyLength::Known(left)) => BodyKind::Oneshot { left }, Some(BodyLength::Chunked) => { + #[allow(clippy::cast_possible_truncation)] // For truncate below. match httparse::parse_chunk_size(&self.buf[self.parsed_bytes..]) { Ok(httparse::Status::Complete((idx, chunk_size))) => { self.parsed_bytes += idx; - let read_complete = if chunk_size == 0 { true } else { false }; BodyKind::Chunked { // FIXME: add check here. It's fine on // 64 bit (only currently supported). left_in_chunk: chunk_size as usize, - read_complete, + read_complete: chunk_size == 0, } } Ok(httparse::Status::Partial) => BodyKind::Chunked { @@ -1039,6 +1040,7 @@ fn read_chunk( loop { ready!(conn.recv())?; match httparse::parse_chunk_size(&conn.buf) { + #[allow(clippy::cast_possible_truncation)] // For truncate below. Ok(httparse::Status::Complete((idx, chunk_size))) => { conn.parsed_bytes += idx; if chunk_size == 0 { @@ -1101,10 +1103,9 @@ where continue; } } - } else { - // Continue to reading below. - break; } + // Continue to reading below. + break; } // Read from the stream if there is space left. @@ -1177,10 +1178,9 @@ where continue; } } - } else { - // Continue to reading below. - break; } + // Continue to reading below. + break; } // Read from the stream if there is space left. From 2567c125327b4d5771789b84f75aa272261ba7e7 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 22 May 2021 22:30:55 +0200 Subject: [PATCH 64/81] Derive common traits for BodyLength Also clean up some of the lifetime naming in the body module. --- http/src/body.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/http/src/body.rs b/http/src/body.rs index e88263b65..38ce0bee1 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -27,6 +27,7 @@ pub trait Body<'a>: PrivateBody<'a> { } /// Length of a body. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum BodyLength { /// Body length is known. Known(usize), @@ -46,8 +47,10 @@ mod private { use heph::net::tcp::stream::FileSend; use heph::net::TcpStream; - /// Private extention of [`PrivateBody`]. - pub trait PrivateBody<'a> { + /// Private extention of [`Body`]. + /// + /// [`Body`]: super::Body + pub trait PrivateBody<'body> { type WriteBody<'stream, 'head>: Future>; /// Write the response to `stream`. @@ -60,7 +63,7 @@ mod private { http_head: &'head [u8], ) -> Self::WriteBody<'stream, 'head> where - 'a: 'head; + 'body: 'head; } /// See [`OneshotBody`]. @@ -242,13 +245,13 @@ use private::{SendFileBody, SendOneshotBody}; #[derive(Debug)] pub struct EmptyBody; -impl Body<'_> for EmptyBody { +impl<'b> Body<'b> for EmptyBody { fn length(&self) -> BodyLength { BodyLength::Known(0) } } -impl<'a> PrivateBody<'a> for EmptyBody { +impl<'b> PrivateBody<'b> for EmptyBody { type WriteBody<'s, 'h> = SendAll<'s, 'h>; fn write_response<'s, 'h>( @@ -257,7 +260,7 @@ impl<'a> PrivateBody<'a> for EmptyBody { http_head: &'h [u8], ) -> Self::WriteBody<'s, 'h> where - 'a: 'h, + 'b: 'h, { // Just need to write the HTTP head as we don't have a body. stream.send_all(http_head) @@ -284,7 +287,7 @@ impl<'b> Body<'b> for OneshotBody<'b> { } } -impl<'a> PrivateBody<'a> for OneshotBody<'a> { +impl<'b> PrivateBody<'b> for OneshotBody<'b> { type WriteBody<'s, 'h> = SendOneshotBody<'s, 'h>; fn write_response<'s, 'h>( @@ -293,7 +296,7 @@ impl<'a> PrivateBody<'a> for OneshotBody<'a> { http_head: &'h [u8], ) -> Self::WriteBody<'s, 'h> where - 'a: 'h, + 'b: 'h, { let head = IoSlice::new(http_head); let body = IoSlice::new(self.bytes); From aaf61693500aaf0c316e4ed539f73aeac536ec4e Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 22 May 2021 22:32:06 +0200 Subject: [PATCH 65/81] Derive Eq and Partial for StatusCode --- http/src/header.rs | 2 +- http/src/status_code.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index 3dbe462ed..816129ef2 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -251,7 +251,7 @@ impl<'n, 'v> Header<'n, 'v> { } /// Returns the value of the header. - pub const fn value(&self) -> &[u8] { + pub const fn value(&self) -> &'v [u8] { self.value } diff --git a/http/src/status_code.rs b/http/src/status_code.rs index f4b2f7574..1a2238499 100644 --- a/http/src/status_code.rs +++ b/http/src/status_code.rs @@ -6,7 +6,7 @@ use std::fmt; /// . /// /// RFC 7231 section 6. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct StatusCode(pub u16); impl StatusCode { From 0a5714e16677712fcf00f058992cc1ca165cd8c3 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 22 May 2021 22:32:34 +0200 Subject: [PATCH 66/81] Add some more tests for Headers For Headers::is_empty and Headers::{get,get_value} with the header not in the list. --- http/tests/functional/header.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index 6a2c364f3..b6f6bbb24 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -18,6 +18,7 @@ fn headers_add_one_header() { let mut headers = Headers::EMPTY; headers.add(Header::new(HeaderName::ALLOW, VALUE)); assert_eq!(headers.len(), 1); + assert!(!headers.is_empty()); check_header(&headers, &HeaderName::ALLOW, VALUE, "GET"); check_iter(&headers, &[(HeaderName::ALLOW, VALUE)]); @@ -34,6 +35,7 @@ fn headers_add_multiple_headers() { headers.add(Header::new(HeaderName::CONTENT_LENGTH, CONTENT_LENGTH)); headers.add(Header::new(HeaderName::X_REQUEST_ID, X_REQUEST_ID)); assert_eq!(headers.len(), 3); + assert!(!headers.is_empty()); check_header(&headers, &HeaderName::ALLOW, ALLOW, "GET"); #[rustfmt::skip] @@ -55,11 +57,23 @@ fn headers_from_header() { let header = Header::new(HeaderName::ALLOW, VALUE); let headers = Headers::from(header.clone()); assert_eq!(headers.len(), 1); + assert!(!headers.is_empty()); check_header(&headers, &HeaderName::ALLOW, VALUE, "GET"); check_iter(&headers, &[(HeaderName::ALLOW, VALUE)]); } +#[test] +fn headers_get_not_found() { + let mut headers = Headers::EMPTY; + assert!(headers.get(&HeaderName::DATE).is_none()); + assert!(headers.get_value(&HeaderName::DATE).is_none()); + + headers.add(Header::new(HeaderName::ALLOW, b"GET")); + assert!(headers.get(&HeaderName::DATE).is_none()); + assert!(headers.get_value(&HeaderName::DATE).is_none()); +} + #[test] fn clear_headers() { const ALLOW: &[u8] = b"GET"; @@ -69,9 +83,11 @@ fn clear_headers() { headers.add(Header::new(HeaderName::ALLOW, ALLOW)); headers.add(Header::new(HeaderName::CONTENT_LENGTH, CONTENT_LENGTH)); assert_eq!(headers.len(), 2); + assert!(!headers.is_empty()); headers.clear(); assert_eq!(headers.len(), 0); + assert!(headers.is_empty()); assert!(headers.get(&HeaderName::ALLOW).is_none()); assert!(headers.get(&HeaderName::CONTENT_LENGTH).is_none()); } @@ -367,6 +383,12 @@ fn from_str_known_headers() { } } +#[test] +fn from_str_unknown_header() { + let header_name = HeaderName::from_str("EXTRA_LONG_UNKNOWN_HEADER_NAME_REALLY_LONG"); + assert!(header_name.is_heap_allocated()); +} + #[test] fn from_str_custom() { let unknown_headers = &["my-header", "My-Header"]; From 11c1dc0abe5d136256eb28d0a7106b8ea8d71c39 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 22 May 2021 22:33:54 +0200 Subject: [PATCH 67/81] Add some tests for the HttpServer This doesn't test everything yet, but it's a good start. Also cleans up the implementation in places. --- http/src/server.rs | 223 +++++++--- http/src/version.rs | 2 +- http/tests/functional.rs | 3 + http/tests/functional/server.rs | 766 ++++++++++++++++++++++++++++++++ 4 files changed, 937 insertions(+), 57 deletions(-) create mode 100644 http/tests/functional/server.rs diff --git a/http/src/server.rs b/http/src/server.rs index 13a2b06c6..b134c3dc6 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -422,6 +422,10 @@ impl Connection { pub async fn next_request<'a>( &'a mut self, ) -> io::Result>>, RequestError>> { + // NOTE: not resetting the version as that doesn't change between + // requests. + self.last_method = None; + let mut too_short = 0; loop { // In case of pipelined requests it could be that while reading a @@ -785,9 +789,11 @@ impl Connection { // Provide the "Conent-Length" or "Transfer-Encoding" header if the user // didn't. + let mut send_body = true; if !set_content_length_header && !set_transfer_encoding_header { match body.length() { _ if !request_method.expects_body() || !status.includes_body() => { + send_body = false; extend_content_length_header(&mut self.buf, &mut itoa_buf, 0) } BodyLength::Known(length) => { @@ -804,10 +810,14 @@ impl Connection { self.buf.extend_from_slice(b"\r\n"); // Write the response to the stream. - let head = &self.buf[ignore_end..]; - body.write_response(&mut self.stream, head).await?; + let http_head = &self.buf[ignore_end..]; + if send_body { + body.write_response(&mut self.stream, http_head).await?; + } else { + self.stream.send(http_head).await?; + } - // Remove the response headers from the buffer. + // Remove the response head from the buffer. self.buf.truncate(ignore_end); Ok(()) } @@ -827,7 +837,7 @@ impl Connection { /// Recv bytes from the underlying stream, reading into `self.buf`. /// /// Returns an `UnexpectedEof` error if zero bytes are received. - fn recv(&mut self) -> Poll> { + fn try_recv(&mut self) -> Poll> { // Ensure we have space in the buffer to read into. self.clear_buffer(); self.buf.reserve(MIN_READ_SIZE); @@ -842,6 +852,80 @@ impl Connection { } } } + + /// Read a HTTP body chunk. + /// + /// Returns an I/O error, or an `InvalidData` error if the chunk size is + /// invalid. + fn try_read_chunk( + &mut self, + // Fields of `BodyKind::Chunked`: + left_in_chunk: &mut usize, + read_complete: &mut bool, + ) -> Poll> { + loop { + match httparse::parse_chunk_size(&self.buf[self.parsed_bytes..]) { + #[allow(clippy::cast_possible_truncation)] // For truncate below. + Ok(httparse::Status::Complete((idx, chunk_size))) => { + self.parsed_bytes += idx; + if chunk_size == 0 { + *read_complete = true; + } + // FIXME: add check here. It's fine on 64 bit (only currently + // supported). + *left_in_chunk = chunk_size as usize; + return Poll::Ready(Ok(())); + } + Ok(httparse::Status::Partial) => {} // Read some more data below. + Err(_) => { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid chunk size", + ))) + } + } + + ready!(self.try_recv())?; + } + } + + async fn read_chunk( + &mut self, + // Fields of `BodyKind::Chunked`: + left_in_chunk: &mut usize, + read_complete: &mut bool, + ) -> io::Result<()> { + loop { + match httparse::parse_chunk_size(&self.buf[self.parsed_bytes..]) { + #[allow(clippy::cast_possible_truncation)] // For truncate below. + Ok(httparse::Status::Complete((idx, chunk_size))) => { + self.parsed_bytes += idx; + if chunk_size == 0 { + *read_complete = true; + } + // FIXME: add check here. It's fine on 64 bit (only currently + // supported). + *left_in_chunk = chunk_size as usize; + return Ok(()); + } + Ok(httparse::Status::Partial) => {} // Read some more data below. + Err(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid chunk size", + )) + } + } + + // Ensure we have space in the buffer to read into. + self.clear_buffer(); + self.buf.reserve(MIN_READ_SIZE); + + if self.stream.recv(&mut self.buf).await? == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + } + } } const fn map_version(version: u8) -> Version { @@ -862,7 +946,7 @@ fn trim_ws(value: &[u8]) -> &[u8] { let start = value.iter().position(|b| !b.is_ascii_whitespace()); let end = value.iter().rposition(|b| !b.is_ascii_whitespace()); if let (Some(start), Some(end)) = (start, end) { - &value[start..end] + &value[start..=end] } else { &[] } @@ -979,6 +1063,63 @@ impl<'a> Body<'a> { RecvVectored { body: self, bufs } } + /// Read the entire body into `buf`, up to `limit` bytes. + /// + /// If the body is larger then `limit` bytes it return an `io::Error`. + pub async fn read_all(&mut self, buf: &mut Vec, limit: usize) -> io::Result<()> { + let mut total = 0; + loop { + // Copy bytes in our buffer. + let bytes = self.buf_bytes(); + let len = bytes.len(); + if limit < total + len { + return Err(io::Error::new(io::ErrorKind::Other, "body too large")); + } + + buf.extend_from_slice(bytes); + self.processed(len); + total += len; + + let chunk_len = self.chunk_len(); + if chunk_len == 0 { + match &mut self.kind { + // Read all the bytes from the oneshot body. + BodyKind::Oneshot { .. } => return Ok(()), + // Read all the bytes in the chunk, so need to read another + // chunk. + BodyKind::Chunked { + left_in_chunk, + read_complete, + } => { + if *read_complete { + return Ok(()); + } + + self.conn.read_chunk(left_in_chunk, read_complete).await?; + // Copy read bytes again. + continue; + } + } + } + // Continue to reading below. + break; + } + + loop { + // Limit the read until the end of the chunk/body. + let chunk_len = self.chunk_len(); + if chunk_len == 0 { + return Ok(()); + } else if limit < total + chunk_len { + return Err(io::Error::new(io::ErrorKind::Other, "body too large")); + } + + (&mut *buf).reserve(chunk_len); + self.conn.stream.recv_n(&mut *buf, chunk_len).await?; + total += chunk_len; + } + } + /// Returns the bytes currently in the buffer. /// /// This is limited to the bytes of this request/chunk, i.e. it doesn't @@ -1021,47 +1162,6 @@ impl<'a> Body<'a> { } } -/// Read a chunk form `conn`. -/// -/// Returns an I/O error, or an `InvalidData` error if the chunk size is -/// invalid. -fn read_chunk( - conn: &mut Connection, - // Fields of `BodyKind::Chunked`: - left_in_chunk: &mut usize, - read_complete: &mut bool, -) -> Poll> { - // TODO: check buffer, might contains chunk. - - // Ensure we have space in the buffer to read into. - conn.clear_buffer(); - conn.buf.reserve(MIN_READ_SIZE); - - loop { - ready!(conn.recv())?; - match httparse::parse_chunk_size(&conn.buf) { - #[allow(clippy::cast_possible_truncation)] // For truncate below. - Ok(httparse::Status::Complete((idx, chunk_size))) => { - conn.parsed_bytes += idx; - if chunk_size == 0 { - *read_complete = true; - } - // FIXME: add check here. It's fine on 64 bit (only currently - // supported). - *left_in_chunk = chunk_size as usize; - return Poll::Ready(Ok(())); - } - Ok(httparse::Status::Partial) => continue, // Read some more data. - Err(_) => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid chunk size", - ))) - } - } - } -} - /// The [`Future`] behind [`Body::recv`]. #[derive(Debug)] #[must_use = "futures do nothing unless you `.await` or poll them"] @@ -1098,7 +1198,7 @@ where left_in_chunk, read_complete, } => { - ready!(read_chunk(&mut body.conn, left_in_chunk, read_complete))?; + ready!(body.conn.try_read_chunk(left_in_chunk, read_complete))?; // Copy read bytes again. continue; } @@ -1173,7 +1273,7 @@ where left_in_chunk, read_complete, } => { - ready!(read_chunk(&mut body.conn, left_in_chunk, read_complete))?; + ready!(body.conn.try_read_chunk(left_in_chunk, read_complete))?; // Copy read bytes again. continue; } @@ -1221,7 +1321,7 @@ mod private { use heph::net::TcpStream; - use super::{read_chunk, Body, BodyKind}; + use super::{Body, BodyKind}; #[derive(Debug)] pub struct SendBody<'c, 's, 'h> { @@ -1281,16 +1381,16 @@ mod private { // Read some more data, or the next chunk. match &mut body.kind { BodyKind::Oneshot { .. } => { - let _ = ready!(body.conn.recv())?; + let _ = ready!(body.conn.try_recv())?; } BodyKind::Chunked { left_in_chunk, read_complete, } => { if *left_in_chunk == 0 { - ready!(read_chunk(&mut body.conn, left_in_chunk, read_complete))?; + ready!(body.conn.try_read_chunk(left_in_chunk, read_complete))?; } else { - ready!(body.conn.recv())?; + ready!(body.conn.try_recv())?; } } } @@ -1332,8 +1432,18 @@ impl<'a> Drop for Body<'a> { // body yet. match self.kind { BodyKind::Oneshot { left } => self.conn.parsed_bytes += left, - // FIXME: don't panic here. - BodyKind::Chunked { .. } => todo!("remove chunked body from connection"), + BodyKind::Chunked { + left_in_chunk, + read_complete, + } => { + if read_complete { + // Read all chunks. + debug_assert_eq!(left_in_chunk, 0); + } else { + // FIXME: don't panic here. + todo!("remove chunked body from connection"); + } + } } } } @@ -1431,6 +1541,7 @@ impl RequestError { | DifferentContentLengths | InvalidHeaderName | InvalidHeaderValue + | UnsupportedTransferEncoding | TooManyHeaders | ChunkedNotLastTransferEncoding | ContentLengthAndTransferEncoding @@ -1438,7 +1549,7 @@ impl RequestError { | InvalidNewLine | InvalidVersion | InvalidChunkSize => true, - UnsupportedTransferEncoding | UnknownMethod => false, + UnknownMethod => false, } } @@ -1468,7 +1579,7 @@ impl fmt::Display for RequestError { InvalidHeaderName => "invalid header name", InvalidHeaderValue => "invalid header value", TooManyHeaders => "too many header", - UnsupportedTransferEncoding => "unsupported Transfer-Encoding header", + UnsupportedTransferEncoding => "unsupported Transfer-Encoding", ChunkedNotLastTransferEncoding => "invalid Transfer-Encoding header", ContentLengthAndTransferEncoding => { "provided both Content-Length and Transfer-Encoding headers" diff --git a/http/src/version.rs b/http/src/version.rs index 656dcfb17..c6a071a0c 100644 --- a/http/src/version.rs +++ b/http/src/version.rs @@ -70,7 +70,7 @@ pub struct UnknownVersion; impl fmt::Display for UnknownVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("unknown version") + f.write_str("unknown HTTP version") } } diff --git a/http/tests/functional.rs b/http/tests/functional.rs index bce0dcb64..f5c7693e3 100644 --- a/http/tests/functional.rs +++ b/http/tests/functional.rs @@ -1,5 +1,7 @@ //! Functional tests. +#![feature(async_stream, never_type, once_cell)] + use std::mem::size_of; #[track_caller] @@ -12,6 +14,7 @@ mod functional { mod from_header_value; mod header; mod method; + mod server; mod status_code; mod version; } diff --git a/http/tests/functional/server.rs b/http/tests/functional/server.rs new file mode 100644 index 000000000..aedafa15b --- /dev/null +++ b/http/tests/functional/server.rs @@ -0,0 +1,766 @@ +use std::borrow::Cow; +use std::io::{self, Read, Write}; +use std::lazy::SyncLazy; +use std::net::{Shutdown, SocketAddr, TcpStream}; +use std::str; +use std::sync::{Arc, Condvar, Mutex, Weak}; +use std::thread::{self, sleep}; +use std::time::{Duration, SystemTime}; + +use heph::actor::messages::Terminate; +use heph::rt::{self, Runtime, ThreadLocal}; +use heph::spawn::options::{ActorOptions, Priority}; +use heph::{actor, Actor, ActorRef, NewActor, Supervisor, SupervisorStrategy}; +use heph_http::body::OneshotBody; +use heph_http::server::{HttpServer, RequestError}; +use heph_http::{self as http, Header, HeaderName, Headers, Method, StatusCode, Version}; +use httpdate::fmt_http_date; + +/// Macro to run with a test server. +macro_rules! with_test_server { + (|$stream: ident| $test: block) => { + let test_server = TestServer::spawn(); + // NOTE: we put `test` in a block to ensure all connections to the + // server are dropped before we call `test_server.join()` below (which + // would block a shutdown. + { + let mut $stream = TcpStream::connect(test_server.address).unwrap(); + $stream.set_nodelay(true).unwrap(); + $stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + $stream + .set_write_timeout(Some(Duration::from_secs(1))) + .unwrap(); + $test + } + test_server.join(); + }; +} + +#[test] +fn get() { + with_test_server!(|stream| { + stream.write_all(b"GET / HTTP/1.1\r\n\r\n").unwrap(); + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"2")); + let body = b"OK"; + expect_response(&mut stream, Version::Http11, StatusCode::OK, &headers, body); + }); +} + +#[test] +fn head() { + with_test_server!(|stream| { + stream.write_all(b"HEAD / HTTP/1.1\r\n\r\n").unwrap(); + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"0")); + let body = b""; + expect_response(&mut stream, Version::Http11, StatusCode::OK, &headers, body); + }); +} + +#[test] +fn post() { + with_test_server!(|stream| { + stream + .write_all(b"POST /echo-body HTTP/1.1\r\nContent-Length: 11\r\n\r\nHello world") + .unwrap(); + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"11")); + let body = b"Hello world"; + expect_response(&mut stream, Version::Http11, StatusCode::OK, &headers, body); + }); +} + +#[test] +fn with_request_header() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nUser-Agent:heph-http\r\n\r\n") + .unwrap(); + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"2")); + let body = b"OK"; + expect_response(&mut stream, Version::Http11, StatusCode::OK, &headers, body); + }); +} + +#[test] +fn with_multiple_request_headers() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nUser-Agent:heph-http\r\nAccept: */*\r\n\r\n") + .unwrap(); + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"2")); + let body = b"OK"; + expect_response(&mut stream, Version::Http11, StatusCode::OK, &headers, body); + }); +} + +#[test] +fn deny_incomplete_request() { + with_test_server!(|stream| { + // NOTE: missing `\r\n`. + stream.write_all(b"GET / HTTP/1.1\r\n").unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"31")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: incomplete request"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_unknown_method() { + with_test_server!(|stream| { + stream.write_all(b"MY_GET / HTTP/1.1\r\n\r\n").unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::NOT_IMPLEMENTED; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"27")); + let body = b"Bad request: unknown method"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_invalid_method() { + with_test_server!(|stream| { + stream.write_all(b"G\nE\rT / HTTP/1.1\r\n\r\n").unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"35")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: invalid request syntax"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn accept_same_content_length_headers() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nContent-Length: 0\r\nContent-Length: 0\r\n\r\n") + .unwrap(); + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"2")); + let body = b"OK"; + expect_response(&mut stream, Version::Http11, StatusCode::OK, &headers, body); + }); +} + +#[test] +fn deny_different_content_length_headers() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nContent-Length: 0\r\nContent-Length: 1\r\n\r\nA") + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"45")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: different Content-Length headers"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_content_length_and_chunked_transfer_encoding_request() { + // NOTE: similar to + // `deny_chunked_transfer_encoding_and_content_length_request`, but + // Transfer-Encoding goes first. + with_test_server!(|stream| { + stream + .write_all( + b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 1\r\n\r\nA", + ) + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"71")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: provided both Content-Length and Transfer-Encoding headers"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_invalid_content_length_headers() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nContent-Length: ABC\r\n\r\n") + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"42")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: invalid Content-Length header"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn identity_transfer_encoding() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nTransfer-Encoding: identity\r\n\r\n") + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::OK; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"2")); + let body = b"OK"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn identity_transfer_encoding_with_content_length() { + with_test_server!(|stream| { + stream + .write_all( + b"POST /echo-body HTTP/1.1\r\nTransfer-Encoding: identity\r\nContent-Length: 1\r\n\r\nA", + ) + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::OK; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"1")); + let body = b"A"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_unsupported_transfer_encoding() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nTransfer-Encoding: Nah\r\n\r\nA") + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::NOT_IMPLEMENTED; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"42")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: unsupported Transfer-Encoding"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_empty_transfer_encoding() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nTransfer-Encoding:\r\n\r\n") + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::NOT_IMPLEMENTED; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"42")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: unsupported Transfer-Encoding"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_chunked_transfer_encoding_not_last() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked, gzip\r\n\r\nA") + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"45")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: invalid Transfer-Encoding header"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_chunked_transfer_encoding_and_content_length_request() { + // NOTE: similar to + // `deny_content_length_and_chunked_transfer_encoding_request`, but + // Content-Length goes first. + with_test_server!(|stream| { + stream + .write_all( + b"GET / HTTP/1.1\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\nA", + ) + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"71")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: provided both Content-Length and Transfer-Encoding headers"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn empty_body_chunked_transfer_encoding() { + with_test_server!(|stream| { + stream + .write_all(b"POST /echo-body HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n") + .unwrap(); + let status = StatusCode::OK; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"0")); + let body = b""; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn deny_invalid_chunk_size() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nZ\r\nAbc0\r\n") + .unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"31")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: invalid chunk size"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn read_partial_chunk_size_chunked_transfer_encoding() { + // Test `Connection::next_request` handling reading the HTTP head, but not + // the chunk size yet. + with_test_server!(|stream| { + stream + .write_all(b"POST /echo-body HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n") + .unwrap(); + sleep(Duration::from_millis(200)); + stream.write_all(b"0\r\n").unwrap(); + let status = StatusCode::OK; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"0")); + let body = b""; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn too_large_http_head() { + // Tests `heph_http::server::MAX_HEAD_SIZE`. + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nSOME_HEADER: ") + .unwrap(); + let mut header_value = Vec::with_capacity(heph_http::server::MAX_HEAD_SIZE); + header_value.resize(heph_http::server::MAX_HEAD_SIZE, b'a'); + stream.write_all(&header_value).unwrap(); + stream.write_all(b"\r\n\r\n").unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"27")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: head too large"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn invalid_header_name() { + with_test_server!(|stream| { + stream.write_all(b"GET / HTTP/1.1\r\n\0: \r\n\r\n").unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"32")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: invalid header name"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn invalid_header_value() { + with_test_server!(|stream| { + stream + .write_all(b"GET / HTTP/1.1\r\nAbc: Header\rvalue\r\n\r\n") + .unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"33")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: invalid header value"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn invalid_carriage_return() { + with_test_server!(|stream| { + stream.write_all(b"\rGET / HTTP/1.1\r\n\r\n").unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"35")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: invalid request syntax"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn invalid_http_version() { + with_test_server!(|stream| { + stream.write_all(b"GET / HTTPS/1.1\r\n\r\n").unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"28")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: invalid version"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +#[test] +fn too_many_header() { + with_test_server!(|stream| { + stream.write_all(b"GET / HTTP/1.1\r\n").unwrap(); + for _ in 0..=http::server::MAX_HEADERS { + stream.write_all(b"Some-Header: Abc\r\n").unwrap(); + } + stream.write_all(b"\r\n").unwrap(); + let status = StatusCode::BAD_REQUEST; + let mut headers = Headers::EMPTY; + let now = fmt_http_date(SystemTime::now()); + headers.add(Header::new(HeaderName::DATE, now.as_bytes())); + headers.add(Header::new(HeaderName::CONTENT_LENGTH, b"28")); + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + let body = b"Bad request: too many header"; + expect_response(&mut stream, Version::Http11, status, &headers, body); + }); +} + +fn expect_response( + stream: &mut TcpStream, + // Expected values: + version: Version, + status: StatusCode, + headers: &Headers, + body: &[u8], +) { + let mut buf = [0; 1024]; + let n = stream.read(&mut buf).unwrap(); + let buf = &buf[..n]; + + eprintln!("read response: {:?}", str::from_utf8(&buf[..n])); + + let mut h = [httparse::EMPTY_HEADER; 64]; + let mut response = httparse::Response::new(&mut h); + let parsed_n = response.parse(&buf).unwrap().unwrap(); + + assert_eq!(response.version, Some(version.minor())); + assert_eq!(response.code.unwrap(), status.0); + assert!(response.reason.unwrap().is_empty()); // We don't send a reason-phrase. + assert_eq!( + response.headers.len(), + headers.len(), + "mismatch headers lengths, got: {:?}, expected: {:?}", + response.headers, + headers + ); + for got_header in response.headers { + let got_header_name = HeaderName::from_str(got_header.name); + let got = headers.get_value(&got_header_name).unwrap(); + assert_eq!( + got_header.value, + got, + "different header values for '{}' header, got: '{:?}', expected: '{:?}'", + got_header_name, + str::from_utf8(got_header.value), + str::from_utf8(got) + ); + } + assert_eq!(&buf[parsed_n..], body, "different bodies"); + assert_eq!(parsed_n, n - body.len(), "unexpected extra bytes"); +} + +struct TestServer { + address: SocketAddr, + server_ref: ActorRef, + handle: Option>, +} + +impl TestServer { + fn spawn() -> Arc { + static TEST_SERVER: SyncLazy>> = + SyncLazy::new(|| Mutex::new(Weak::new())); + + let mut test_server = TEST_SERVER.lock().unwrap(); + if let Some(test_server) = test_server.upgrade() { + // Use an existing running server. + test_server + } else { + // Start a new server. + let new_server = Arc::new(TestServer::new()); + *test_server = Arc::downgrade(&new_server); + new_server + } + } + + fn new() -> TestServer { + const TIMEOUT: Duration = Duration::from_secs(1); + + let actor = http_actor as fn(_, _, _) -> _; + let address = "127.0.0.1:7890".parse().unwrap(); + let server = HttpServer::setup(address, conn_supervisor, actor, ActorOptions::default()) + .map_err(rt::Error::setup) + .unwrap(); + let address = server.local_addr(); + + let mut runtime = Runtime::setup().num_threads(1).build().unwrap(); + let server_ref = Arc::new(Mutex::new(None)); + let set_ref = Arc::new(Condvar::new()); + let srv_ref = server_ref.clone(); + let set_ref2 = set_ref.clone(); + runtime + .run_on_workers(move |mut runtime_ref| -> Result<(), !> { + let mut server_ref = srv_ref.lock().unwrap(); + let options = ActorOptions::default().with_priority(Priority::LOW); + *server_ref = Some( + runtime_ref + .try_spawn_local(ServerSupervisor, server, (), options) + .unwrap() + .map(), + ); + set_ref2.notify_all(); + Ok(()) + }) + .unwrap(); + + let handle = thread::spawn(move || runtime.start().unwrap()); + let mut server_ref = set_ref + .wait_timeout_while(server_ref.lock().unwrap(), TIMEOUT, |r| r.is_none()) + .unwrap() + .0; + let server_ref = server_ref.take().unwrap(); + TestServer { + address, + server_ref, + handle: Some(handle), + } + } + + fn join(mut self: Arc) { + if let Some(this) = Arc::get_mut(&mut self) { + this.server_ref.try_send(Terminate).unwrap(); + this.handle.take().unwrap().join().unwrap() + } + } +} + +#[derive(Copy, Clone, Debug)] +struct ServerSupervisor; + +impl Supervisor for ServerSupervisor +where + NA: NewActor, + NA::Actor: Actor>, +{ + fn decide(&mut self, err: http::server::Error) -> SupervisorStrategy<()> { + use http::server::Error::*; + match err { + Accept(err) => panic!("error accepting new connection: {}", err), + NewActor(_) => unreachable!(), + } + } + + fn decide_on_restart_error(&mut self, err: io::Error) -> SupervisorStrategy<()> { + panic!("error restarting the TCP server: {}", err); + } + + fn second_restart_error(&mut self, err: io::Error) { + panic!("error restarting the actor a second time: {}", err); + } +} + +fn conn_supervisor(err: io::Error) -> SupervisorStrategy<(heph::net::TcpStream, SocketAddr)> { + panic!("error handling connection: {}", err) +} + +/// Routes: +/// GET / => 200, OK. +/// POST /echo-body => 200, $request_body. +/// * => 404, Not found. +async fn http_actor( + _: actor::Context, + mut connection: http::Connection, + _: SocketAddr, +) -> io::Result<()> { + connection.set_nodelay(true)?; + + let mut headers = Headers::EMPTY; + loop { + let mut got_version = None; + let mut got_method = None; + let (code, body, should_close) = match connection.next_request().await? { + Ok(Some(mut request)) => { + got_version = Some(request.version()); + got_method = Some(request.method()); + + match (request.method(), request.path()) { + (Method::Get | Method::Head, "/") => (StatusCode::OK, "OK".into(), false), + (Method::Post, "/echo-body") => { + let body_len = request.body().len(); + let mut buf = Vec::with_capacity(128); + request.body_mut().read_all(&mut buf, 1024).await?; + assert!(request.body().is_empty()); + if let http::body::BodyLength::Known(length) = body_len { + assert_eq!(length, buf.len()); + } else { + assert!(request.body().is_chunked()); + } + let body = String::from_utf8(buf).unwrap().into(); + (StatusCode::OK, body, false) + } + _ => (StatusCode::NOT_FOUND, "Not found".into(), false), + } + } + // No more requests. + Ok(None) => return Ok(()), + Err(err) => { + let code = err.proper_status_code(); + let body = Cow::from(format!("Bad request: {}", err)); + (code, body, err.should_close()) + } + }; + if let Some(got_version) = got_version { + assert_eq!(connection.last_request_version().unwrap(), got_version); + } + if let Some(got_method) = got_method { + assert_eq!(connection.last_request_method().unwrap(), got_method); + } + + if should_close { + headers.add(Header::new(HeaderName::CONNECTION, b"close")); + } + + let body = OneshotBody::new(body.as_bytes()); + connection.respond(code, &headers, body).await?; + if should_close { + return Ok(()); + } + + headers.clear(); + } +} + +#[test] +fn request_error_proper_status_code() { + use RequestError::*; + let tests = &[ + (IncompleteRequest, StatusCode::BAD_REQUEST), + (HeadTooLarge, StatusCode::BAD_REQUEST), + (InvalidContentLength, StatusCode::BAD_REQUEST), + (DifferentContentLengths, StatusCode::BAD_REQUEST), + (InvalidHeaderName, StatusCode::BAD_REQUEST), + (InvalidHeaderValue, StatusCode::BAD_REQUEST), + (TooManyHeaders, StatusCode::BAD_REQUEST), + (ChunkedNotLastTransferEncoding, StatusCode::BAD_REQUEST), + (ContentLengthAndTransferEncoding, StatusCode::BAD_REQUEST), + (InvalidToken, StatusCode::BAD_REQUEST), + (InvalidNewLine, StatusCode::BAD_REQUEST), + (InvalidVersion, StatusCode::BAD_REQUEST), + (InvalidChunkSize, StatusCode::BAD_REQUEST), + (UnsupportedTransferEncoding, StatusCode::NOT_IMPLEMENTED), + (UnknownMethod, StatusCode::NOT_IMPLEMENTED), + ]; + + for (error, expected) in tests { + assert_eq!(error.proper_status_code(), *expected); + } +} + +#[test] +fn request_should_close() { + use RequestError::*; + let tests = &[ + (IncompleteRequest, true), + (HeadTooLarge, true), + (InvalidContentLength, true), + (DifferentContentLengths, true), + (InvalidHeaderName, true), + (InvalidHeaderValue, true), + (UnsupportedTransferEncoding, true), + (TooManyHeaders, true), + (ChunkedNotLastTransferEncoding, true), + (ContentLengthAndTransferEncoding, true), + (InvalidToken, true), + (InvalidNewLine, true), + (InvalidVersion, true), + (InvalidChunkSize, true), + (UnknownMethod, false), + ]; + + for (error, expected) in tests { + assert_eq!(error.should_close(), *expected); + } +} From 98f8d3239a5da1a17bd764e605d8ce34a9e41ba4 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 22 May 2021 23:03:53 +0200 Subject: [PATCH 68/81] Cleanup Connection docs --- http/src/server.rs | 106 +++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/http/src/server.rs b/http/src/server.rs index b134c3dc6..dfee48c40 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -3,15 +3,11 @@ // // TODO: Continue reading RFC 7230 section 4 Transfer Codings. // -// TODO: RFC 7230 section 3.4 Handling Incomplete Messages. -// // TODO: RFC 7230 section 3.3.3 point 5: // > If the sender closes the connection or the recipient // > times out before the indicated number of octets are // > received, the recipient MUST consider the message to be // > incomplete and close the connection. -// -// TODO: chunked encoding. //! Module with the HTTP server implementation. @@ -333,8 +329,12 @@ where /// HTTP connection. /// -/// This a TCP stream from which [HTTP requests] are read and [HTTP responses] -/// are send to. +/// This wraps a TCP stream from which [HTTP requests] are read and [HTTP +/// responses] are send to. +/// +/// It's advisable to set `TCP_NODELAY` using [`Connection::set_nodelay`] as the +/// `Connection` uses internally buffering, meaning only bodies with small +/// chunks would benefit from `TCP_NODELAY`. /// /// [HTTP requests]: Request /// [HTTP responses]: crate::Response @@ -364,38 +364,6 @@ impl Connection { } } - pub fn peer_addr(&mut self) -> io::Result { - self.stream.peer_addr() - } - - pub fn local_addr(&mut self) -> io::Result { - self.stream.local_addr() - } - - pub fn set_ttl(&mut self, ttl: u32) -> io::Result<()> { - self.stream.set_ttl(ttl) - } - - pub fn ttl(&mut self) -> io::Result { - self.stream.ttl() - } - - pub fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.stream.set_nodelay(nodelay) - } - - pub fn nodelay(&mut self) -> io::Result { - self.stream.nodelay() - } - - pub fn keepalive(&self) -> io::Result { - self.stream.keepalive() - } - - pub fn set_keepalive(&self, enable: bool) -> io::Result<()> { - self.stream.set_keepalive(enable) - } - /// Parse the next request from the connection. /// /// The return is a bit complex so let's break it down. The outer type is an @@ -410,8 +378,8 @@ impl Connection { /// /// # Notes /// - /// Most [`RequestError`]s can't be receover from and will need the - /// connection be closed, see [`RequestError::should_close`]. If the + /// Most [`RequestError`]s can't be receover from and the connection should + /// be closed when hitting them, see [`RequestError::should_close`]. If the /// connection is not closed and `next_request` is called again it will /// likely return the same error (but this is not guaranteed). /// @@ -630,16 +598,18 @@ impl Connection { /// // Reading a request returned this error. /// let err = RequestError::IncompleteRequest; /// - /// // We can use `last_request_version` to determine the client prefered - /// // HTTP version, or default to the server prefered version (HTTP/1.1 - /// // here). - /// let version = conn.last_request_version().unwrap_or(Version::Http11); - /// let body = format!("Bad request: {}", err); - /// let body = OneshotBody::new(body.as_bytes()); - /// /// // We can use `last_request_method` to determine the method of the last /// // request, which is used to determine if we need to send a body. /// let request_method = conn.last_request_method().unwrap_or(Method::Get); + /// + /// // We can use `last_request_version` to determine the client preferred + /// // HTTP version, or default to the server's preferred version (HTTP/1.1 + /// // here). + /// let version = conn.last_request_version().unwrap_or(Version::Http11); + /// + /// let msg = format!("Bad request: {}", err); + /// let body = OneshotBody::new(msg.as_bytes()); + /// /// // Respond with the response. /// conn.send_response(request_method, version, StatusCode::BAD_REQUEST, &Headers::EMPTY, body); /// @@ -666,7 +636,7 @@ impl Connection { self.last_method } - /// Respond to a request. + /// Respond to the last parsed request. /// /// # Notes /// @@ -822,6 +792,46 @@ impl Connection { Ok(()) } + /// See [`TcpStream::peer_addr`]. + pub fn peer_addr(&mut self) -> io::Result { + self.stream.peer_addr() + } + + /// See [`TcpStream::local_addr`]. + pub fn local_addr(&mut self) -> io::Result { + self.stream.local_addr() + } + + /// See [`TcpStream::set_ttl`]. + pub fn set_ttl(&mut self, ttl: u32) -> io::Result<()> { + self.stream.set_ttl(ttl) + } + + /// See [`TcpStream::ttl`]. + pub fn ttl(&mut self) -> io::Result { + self.stream.ttl() + } + + /// See [`TcpStream::set_nodelay`]. + pub fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { + self.stream.set_nodelay(nodelay) + } + + /// See [`TcpStream::nodelay`]. + pub fn nodelay(&mut self) -> io::Result { + self.stream.nodelay() + } + + /// See [`TcpStream::keepalive`]. + pub fn keepalive(&self) -> io::Result { + self.stream.keepalive() + } + + /// See [`TcpStream::set_keepalive`]. + pub fn set_keepalive(&self, enable: bool) -> io::Result<()> { + self.stream.set_keepalive(enable) + } + /// Clear parsed request(s) from the buffer. fn clear_buffer(&mut self) { let buf_len = self.buf.len(); From d428a06182b03a0a9223b22231984c868f573875 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sat, 22 May 2021 23:07:10 +0200 Subject: [PATCH 69/81] Enable some warnings Same ones from the main Heph crate. --- http/src/header.rs | 3 +++ http/src/lib.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/http/src/header.rs b/http/src/header.rs index 816129ef2..d4980cb5a 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -924,6 +924,8 @@ pub trait FromHeaderValue<'a>: Sized { fn from_bytes(value: &'a [u8]) -> Result; } +/// Error returned by the [`FromHeaderValue`] implementation for numbers, e.g. +/// `usize`. #[derive(Debug)] pub struct ParseIntError; @@ -976,6 +978,7 @@ impl<'a> FromHeaderValue<'a> for &'a str { } } +/// Error returned by the [`FromHeaderValue`] implementation for [`SystemTime`]. #[derive(Debug)] pub struct ParseTimeError; diff --git a/http/src/lib.rs b/http/src/lib.rs index f60197084..d0369473d 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -1,3 +1,5 @@ +//! HTTP/1.1 implementation for Heph. + #![feature( async_stream, const_fn_trait_bound, @@ -10,6 +12,19 @@ stmt_expr_attributes )] #![allow(incomplete_features)] // NOTE: for `generic_associated_types`. +#![warn( + anonymous_parameters, + bare_trait_objects, + missing_debug_implementations, + missing_docs, + rust_2018_idioms, + trivial_numeric_casts, + unused_extern_crates, + unused_import_braces, + unused_qualifications, + unused_results, + variant_size_differences +)] pub mod body; pub mod header; From fbcb2e97e8f292ee08c09609a8be87108be2fe52 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sun, 23 May 2021 13:20:46 +0200 Subject: [PATCH 70/81] Don't link LICENSE file Use a proper LICENSE file for the Heph-HTTP crate. --- http/LICENSE | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) mode change 120000 => 100644 http/LICENSE diff --git a/http/LICENSE b/http/LICENSE deleted file mode 120000 index ea5b60640..000000000 --- a/http/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file diff --git a/http/LICENSE b/http/LICENSE new file mode 100644 index 000000000..1cc94c7c8 --- /dev/null +++ b/http/LICENSE @@ -0,0 +1,20 @@ +Copyright (C) 2021 Thomas de Zeeuw + + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 2d94dc494f658dc815d9834f0daaf32a1df14866 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sun, 23 May 2021 13:39:02 +0200 Subject: [PATCH 71/81] Fix all warnings Implements fmt::Debug for all public types, ignores some return arguments that aren't useful. --- http/src/body.rs | 5 +++-- http/src/header.rs | 20 +++++++++++--------- http/src/method.rs | 2 +- http/src/server.rs | 23 ++++++++++++++++++----- http/src/status_code.rs | 2 +- http/src/version.rs | 4 ++-- 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/http/src/body.rs b/http/src/body.rs index 38ce0bee1..70b309c6d 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -91,11 +91,11 @@ mod private { } else if n <= head_len { // Only written part of the head, advance the head // buffer. - IoSlice::advance(&mut bufs[..1], n); + let _ = IoSlice::advance(&mut bufs[..1], n); } else { // Written entire head. bufs[0] = IoSlice::new(&[]); - IoSlice::advance(&mut bufs[1..], n - head_len); + let _ = IoSlice::advance(&mut bufs[1..], n - head_len); } } Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => { @@ -385,6 +385,7 @@ pub struct ChunkedBody<'b, B> { // TODO: implement `Body` for `ChunkedBody`. /// Body that sends the entire file `F`. +#[derive(Debug)] pub struct FileBody<'f, F> { file: &'f F, /// Start offset into the `file`. diff --git a/http/src/header.rs b/http/src/header.rs index d4980cb5a..768b9bc14 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -47,7 +47,7 @@ impl Headers { mut f: F, ) -> Result where - F: FnMut(&HeaderName, &[u8]) -> Result<(), E>, + F: FnMut(&HeaderName<'_>, &[u8]) -> Result<(), E>, { let values_len = raw_headers.iter().map(|h| h.value.len()).sum(); let mut headers = Headers { @@ -87,7 +87,7 @@ impl Headers { /// /// This doesn't check for duplicate headers, it just adds it to the list of /// headers. - pub fn add<'v>(&mut self, header: Header<'static, 'v>) { + pub fn add(&mut self, header: Header<'static, '_>) { self._add(header.name, header.value) } @@ -103,7 +103,7 @@ impl Headers { /// # Notes /// /// If all you need is the header value you can use [`Headers::get_value`]. - pub fn get<'a>(&'a self, name: &HeaderName<'_>) -> Option> { + pub fn get(&self, name: &HeaderName<'_>) -> Option> { for part in &self.parts { if part.name == *name { return Some(Header { @@ -116,7 +116,7 @@ impl Headers { } /// Get the header's value with `name`, if any. - pub fn get_value<'a>(&'a self, name: &HeaderName) -> Option<&'a [u8]> { + pub fn get_value<'a>(&'a self, name: &HeaderName<'_>) -> Option<&'a [u8]> { for part in &self.parts { if part.name == *name { return Some(&self.values[part.start..part.end]); @@ -178,9 +178,9 @@ impl fmt::Debug for Headers { for part in &self.parts { let value = &self.values[part.start..part.end]; if let Ok(str) = std::str::from_utf8(value) { - f.entry(&part.name, &str); + let _ = f.entry(&part.name, &str); } else { - f.entry(&part.name, &value); + let _ = f.entry(&part.name, &value); } } f.finish() @@ -188,6 +188,7 @@ impl fmt::Debug for Headers { } /// Iterator for [`Headers`], see [`Headers::iter`]. +#[derive(Debug)] pub struct Iter<'a> { headers: &'a Headers, pos: usize, @@ -280,11 +281,11 @@ const fn no_crlf(value: &[u8]) -> bool { impl<'n, 'v> fmt::Debug for Header<'n, 'v> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_struct("Header"); - f.field("name", &self.name); + let _ = f.field("name", &self.name); if let Ok(str) = std::str::from_utf8(self.value) { - f.field("value", &str); + let _ = f.field("value", &str); } else { - f.field("value", &self.value); + let _ = f.field("value", &self.value); } f.finish() } @@ -953,6 +954,7 @@ macro_rules! int_impl { Some(v) => value = v, None => return Err(ParseIntError), } + #[allow(trivial_numeric_casts)] // For `u8 as u8`. match value.checked_add((b - b'0') as $ty) { Some(v) => value = v, None => return Err(ParseIntError), diff --git a/http/src/method.rs b/http/src/method.rs index 6f849a98c..14ceb7648 100644 --- a/http/src/method.rs +++ b/http/src/method.rs @@ -95,7 +95,7 @@ impl Method { } impl fmt::Display for Method { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } diff --git a/http/src/server.rs b/http/src/server.rs index dfee48c40..e32b95009 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -295,6 +295,19 @@ where } } +impl fmt::Debug for HttpServer +where + S: fmt::Debug, + NA: NewActor + fmt::Debug, + NA::RuntimeAccess: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HttpServer") + .field("inner", &self.inner) + .finish() + } +} + // TODO: better name. Like `TcpStreamToConnection`? /// Maps `NA` to accept `(TcpStream, SocketAddr)` as argument, creating a /// [`Connection`]. @@ -784,7 +797,7 @@ impl Connection { if send_body { body.write_response(&mut self.stream, http_head).await?; } else { - self.stream.send(http_head).await?; + self.stream.send_all(http_head).await?; } // Remove the response head from the buffer. @@ -895,7 +908,7 @@ impl Connection { } } - ready!(self.try_recv())?; + let _ = ready!(self.try_recv())?; } } @@ -1155,7 +1168,7 @@ impl<'a> Body<'a> { let bytes = self.buf_bytes(); let len = min(bytes.len(), dst.len()); if len != 0 { - MaybeUninit::write_slice(&mut dst[..len], &bytes[..len]); + let _ = MaybeUninit::write_slice(&mut dst[..len], &bytes[..len]); self.processed(len); } len @@ -1400,7 +1413,7 @@ mod private { if *left_in_chunk == 0 { ready!(body.conn.try_read_chunk(left_in_chunk, read_complete))?; } else { - ready!(body.conn.try_recv())?; + let _ = ready!(body.conn.try_recv())?; } } } @@ -1579,7 +1592,7 @@ impl RequestError { } impl fmt::Display for RequestError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use RequestError::*; f.write_str(match self { IncompleteRequest => "incomplete request", diff --git a/http/src/status_code.rs b/http/src/status_code.rs index 1a2238499..201018172 100644 --- a/http/src/status_code.rs +++ b/http/src/status_code.rs @@ -385,7 +385,7 @@ impl StatusCode { } impl fmt::Display for StatusCode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } diff --git a/http/src/version.rs b/http/src/version.rs index c6a071a0c..bf8e69a1d 100644 --- a/http/src/version.rs +++ b/http/src/version.rs @@ -59,7 +59,7 @@ impl Version { } impl fmt::Display for Version { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } @@ -69,7 +69,7 @@ impl fmt::Display for Version { pub struct UnknownVersion; impl fmt::Display for UnknownVersion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("unknown HTTP version") } } From 3b21bea897da6419f29958813761930618b18d14 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sun, 23 May 2021 13:49:17 +0200 Subject: [PATCH 72/81] Relax lifetime in HeaderName::from_lowercase --- http/src/header.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index 768b9bc14..186c6f4ca 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -310,7 +310,7 @@ macro_rules! known_headers { pub const $const_name: HeaderName<'static> = HeaderName::from_lowercase($http_name); )+)+ - /// Create a new HTTP header `HeaderName`. + /// Create a new HTTP `HeaderName`. /// /// # Notes /// @@ -339,7 +339,7 @@ macro_rules! known_headers { } } -impl HeaderName<'static> { +impl<'n> HeaderName<'n> { // NOTE: these are automatically generated by the `parse_headers.bash` // script. // NOTE: we adding here also add to the @@ -835,12 +835,12 @@ impl HeaderName<'static> { ], ); - /// Create a new HTTP header `HeaderName`. + /// Create a new HTTP `HeaderName`. /// /// # Panics /// /// Panics if `name` is not all ASCII lowercase. - pub const fn from_lowercase(name: &'static str) -> HeaderName<'static> { + pub const fn from_lowercase(name: &'n str) -> HeaderName<'n> { assert!(is_lower_case(name), "header name not lowercase"); HeaderName { inner: Cow::Borrowed(name), From e5457a586503e37f250e4a6e416fd1f404dc45ea Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sun, 23 May 2021 13:54:35 +0200 Subject: [PATCH 73/81] Implement From<&Header> for Headers --- http/src/header.rs | 11 ++--------- http/tests/functional/header.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index 186c6f4ca..9f027ab4f 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -151,14 +151,8 @@ impl From> for Headers { } } -/* -/// # Notes -/// -/// This clones the [`HeaderName`] in each header. For static headers, i.e. the -/// `HeaderName::*` constants, this is a cheap operation, for customer headers -/// this requires an allocation. -impl From<&'_ [Header<'_>]> for Headers { - fn from(raw_headers: &'_ [Header<'_>]) -> Headers { +impl From<&[Header<'static, '_>]> for Headers { + fn from(raw_headers: &[Header<'static, '_>]) -> Headers { let values_len = raw_headers.iter().map(|h| h.value.len()).sum(); let mut headers = Headers { values: Vec::with_capacity(values_len), @@ -170,7 +164,6 @@ impl From<&'_ [Header<'_>]> for Headers { headers } } -*/ impl fmt::Debug for Headers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index b6f6bbb24..7bda15344 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -55,13 +55,42 @@ fn headers_add_multiple_headers() { fn headers_from_header() { const VALUE: &[u8] = b"GET"; let header = Header::new(HeaderName::ALLOW, VALUE); - let headers = Headers::from(header.clone()); + let headers = Headers::from(header); assert_eq!(headers.len(), 1); assert!(!headers.is_empty()); check_header(&headers, &HeaderName::ALLOW, VALUE, "GET"); check_iter(&headers, &[(HeaderName::ALLOW, VALUE)]); } +#[test] +fn headers_from_slice() { + const ALLOW: &[u8] = b"GET"; + const CONTENT_LENGTH: &[u8] = b"123"; + const X_REQUEST_ID: &[u8] = b"abc-def"; + + let expected_headers: &[_] = &[ + Header::new(HeaderName::ALLOW, ALLOW), + Header::new(HeaderName::CONTENT_LENGTH, CONTENT_LENGTH), + Header::new(HeaderName::X_REQUEST_ID, X_REQUEST_ID), + ]; + + let headers = Headers::from(expected_headers); + assert_eq!(headers.len(), 3); + assert!(!headers.is_empty()); + + check_header(&headers, &HeaderName::ALLOW, ALLOW, "GET"); + #[rustfmt::skip] + check_header(&headers, &HeaderName::CONTENT_LENGTH, CONTENT_LENGTH, 123usize); + check_header(&headers, &HeaderName::X_REQUEST_ID, X_REQUEST_ID, "abc-def"); + check_iter( + &headers, + &[ + (HeaderName::ALLOW, ALLOW), + (HeaderName::CONTENT_LENGTH, CONTENT_LENGTH), + (HeaderName::X_REQUEST_ID, X_REQUEST_ID), + ], + ); +} #[test] fn headers_get_not_found() { From bb9bec9625b14430760c67ba2c299f0ae1042f22 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sun, 23 May 2021 14:07:09 +0200 Subject: [PATCH 74/81] Implement FromIterator and Extend for Headers --- http/src/header.rs | 33 ++++++++++++++++++++++++++++----- http/tests/functional/header.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/http/src/header.rs b/http/src/header.rs index 9f027ab4f..e6959c2fa 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -1,12 +1,8 @@ //! Module with HTTP header related types. -// TODO: impl for `Headers`. -// * FromIterator -// * Extend - use std::borrow::Cow; use std::convert::AsRef; -use std::iter::FusedIterator; +use std::iter::{FromIterator, FusedIterator}; use std::time::SystemTime; use std::{fmt, str}; @@ -165,6 +161,33 @@ impl From<&[Header<'static, '_>]> for Headers { } } +impl<'v> FromIterator> for Headers { + fn from_iter(iter: I) -> Headers + where + I: IntoIterator>, + { + let mut headers = Headers::EMPTY; + headers.extend(iter); + headers + } +} + +impl<'v> Extend> for Headers { + fn extend(&mut self, iter: I) + where + I: IntoIterator>, + { + let iter = iter.into_iter(); + let (iter_len, _) = iter.size_hint(); + // Make a guess of 10 bytes per header value on average. + self.values.reserve(iter_len * 10); + self.parts.reserve(iter_len); + for header in iter { + self._add(header.name, header.value); + } + } +} + impl fmt::Debug for Headers { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_map(); diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index 7bda15344..f5832c872 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::iter::FromIterator; use heph_http::header::{FromHeaderValue, Header, HeaderName, Headers}; @@ -62,6 +63,7 @@ fn headers_from_header() { check_header(&headers, &HeaderName::ALLOW, VALUE, "GET"); check_iter(&headers, &[(HeaderName::ALLOW, VALUE)]); } + #[test] fn headers_from_slice() { const ALLOW: &[u8] = b"GET"; @@ -92,6 +94,37 @@ fn headers_from_slice() { ); } +#[test] +fn headers_from_iter_and_extend() { + const ALLOW: &[u8] = b"GET"; + const CONTENT_LENGTH: &[u8] = b"123"; + const X_REQUEST_ID: &[u8] = b"abc-def"; + + let mut headers = Headers::from_iter([ + Header::new(HeaderName::ALLOW, ALLOW), + Header::new(HeaderName::CONTENT_LENGTH, CONTENT_LENGTH), + ]); + assert_eq!(headers.len(), 2); + assert!(!headers.is_empty()); + + headers.extend([Header::new(HeaderName::X_REQUEST_ID, X_REQUEST_ID)]); + assert_eq!(headers.len(), 3); + assert!(!headers.is_empty()); + + check_header(&headers, &HeaderName::ALLOW, ALLOW, "GET"); + #[rustfmt::skip] + check_header(&headers, &HeaderName::CONTENT_LENGTH, CONTENT_LENGTH, 123usize); + check_header(&headers, &HeaderName::X_REQUEST_ID, X_REQUEST_ID, "abc-def"); + check_iter( + &headers, + &[ + (HeaderName::ALLOW, ALLOW), + (HeaderName::CONTENT_LENGTH, CONTENT_LENGTH), + (HeaderName::X_REQUEST_ID, X_REQUEST_ID), + ], + ); +} + #[test] fn headers_get_not_found() { let mut headers = Headers::EMPTY; From 86cff1cbd5a7c388d15368adc1a2918ec3edb9ef Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sun, 23 May 2021 18:45:18 +0200 Subject: [PATCH 75/81] Rename Body::write_response to write_message So it can be used in the HTTP client as well. --- http/src/body.rs | 19 ++++++++++--------- http/src/server.rs | 6 +++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/http/src/body.rs b/http/src/body.rs index 70b309c6d..bc707a44f 100644 --- a/http/src/body.rs +++ b/http/src/body.rs @@ -53,11 +53,12 @@ mod private { pub trait PrivateBody<'body> { type WriteBody<'stream, 'head>: Future>; - /// Write the response to `stream`. + /// Write a HTTP message to `stream`. /// - /// The `http_head` buffer contains the HTTP header (i.e. status line - /// and all headers), this must still be written to the `stream` also. - fn write_response<'stream, 'head>( + /// The `http_head` buffer contains the HTTP header (i.e. request/status + /// line and all headers), this must still be written to the `stream` + /// also. + fn write_message<'stream, 'head>( self, stream: &'stream mut TcpStream, http_head: &'head [u8], @@ -254,7 +255,7 @@ impl<'b> Body<'b> for EmptyBody { impl<'b> PrivateBody<'b> for EmptyBody { type WriteBody<'s, 'h> = SendAll<'s, 'h>; - fn write_response<'s, 'h>( + fn write_message<'s, 'h>( self, stream: &'s mut TcpStream, http_head: &'h [u8], @@ -290,7 +291,7 @@ impl<'b> Body<'b> for OneshotBody<'b> { impl<'b> PrivateBody<'b> for OneshotBody<'b> { type WriteBody<'s, 'h> = SendOneshotBody<'s, 'h>; - fn write_response<'s, 'h>( + fn write_message<'s, 'h>( self, stream: &'s mut TcpStream, http_head: &'h [u8], @@ -332,7 +333,7 @@ impl<'b, B> StreamingBody<'b, B> where B: Stream>, { - /// Use a [`Stream`] as HTTP body. + /// Use a [`Stream`] as HTTP body with a known length. pub const fn new(length: usize, stream: B) -> StreamingBody<'b, B> { StreamingBody { length, @@ -357,7 +358,7 @@ where { type WriteBody<'s, 'h> = SendStreamingBody<'s, 'h, 'b, B>; - fn write_response<'s, 'h>( + fn write_message<'s, 'h>( self, stream: &'s mut TcpStream, head: &'h [u8], @@ -427,7 +428,7 @@ where { type WriteBody<'s, 'h> = SendFileBody<'s, 'h, 'f, F>; - fn write_response<'s, 'h>( + fn write_message<'s, 'h>( self, stream: &'s mut TcpStream, head: &'h [u8], diff --git a/http/src/server.rs b/http/src/server.rs index e32b95009..7e8814151 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -789,13 +789,13 @@ impl Connection { } } - // End of the header. + // End of the HTTP head. self.buf.extend_from_slice(b"\r\n"); // Write the response to the stream. let http_head = &self.buf[ignore_end..]; if send_body { - body.write_response(&mut self.stream, http_head).await?; + body.write_message(&mut self.stream, http_head).await?; } else { self.stream.send_all(http_head).await?; } @@ -1427,7 +1427,7 @@ mod private { impl<'c> crate::body::PrivateBody<'c> for Body<'c> { type WriteBody<'s, 'h> = private::SendBody<'c, 's, 'h>; - fn write_response<'s, 'h>( + fn write_message<'s, 'h>( self, stream: &'s mut TcpStream, head: &'h [u8], From d731331ce4b34d18cf181091ce44e06846823094 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sun, 23 May 2021 21:54:15 +0200 Subject: [PATCH 76/81] Implement From<[Header; N]> for Headers --- http/src/header.rs | 14 ++++++++++++++ http/tests/functional/header.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/http/src/header.rs b/http/src/header.rs index e6959c2fa..b77a4e830 100644 --- a/http/src/header.rs +++ b/http/src/header.rs @@ -147,6 +147,20 @@ impl From> for Headers { } } +impl From<[Header<'static, '_>; N]> for Headers { + fn from(raw_headers: [Header<'static, '_>; N]) -> Headers { + let values_len = raw_headers.iter().map(|h| h.value.len()).sum(); + let mut headers = Headers { + values: Vec::with_capacity(values_len), + parts: Vec::with_capacity(raw_headers.len()), + }; + for header in raw_headers { + headers._add(header.name.clone(), header.value); + } + headers + } +} + impl From<&[Header<'static, '_>]> for Headers { fn from(raw_headers: &[Header<'static, '_>]) -> Headers { let values_len = raw_headers.iter().map(|h| h.value.len()).sum(); diff --git a/http/tests/functional/header.rs b/http/tests/functional/header.rs index f5832c872..c6263b382 100644 --- a/http/tests/functional/header.rs +++ b/http/tests/functional/header.rs @@ -64,6 +64,34 @@ fn headers_from_header() { check_iter(&headers, &[(HeaderName::ALLOW, VALUE)]); } +#[test] +fn headers_from_array() { + const ALLOW: &[u8] = b"GET"; + const CONTENT_LENGTH: &[u8] = b"123"; + const X_REQUEST_ID: &[u8] = b"abc-def"; + + let headers = Headers::from([ + Header::new(HeaderName::ALLOW, ALLOW), + Header::new(HeaderName::CONTENT_LENGTH, CONTENT_LENGTH), + Header::new(HeaderName::X_REQUEST_ID, X_REQUEST_ID), + ]); + assert_eq!(headers.len(), 3); + assert!(!headers.is_empty()); + + check_header(&headers, &HeaderName::ALLOW, ALLOW, "GET"); + #[rustfmt::skip] + check_header(&headers, &HeaderName::CONTENT_LENGTH, CONTENT_LENGTH, 123usize); + check_header(&headers, &HeaderName::X_REQUEST_ID, X_REQUEST_ID, "abc-def"); + check_iter( + &headers, + &[ + (HeaderName::ALLOW, ALLOW), + (HeaderName::CONTENT_LENGTH, CONTENT_LENGTH), + (HeaderName::X_REQUEST_ID, X_REQUEST_ID), + ], + ); +} + #[test] fn headers_from_slice() { const ALLOW: &[u8] = b"GET"; From 81f54309a9bfc096ea8bb5bc5f5d1283a75c6717 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Sun, 23 May 2021 21:54:50 +0200 Subject: [PATCH 77/81] Small fixes --- http/src/server.rs | 17 ++++++++++------- http/tests/functional/server.rs | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/http/src/server.rs b/http/src/server.rs index 7e8814151..690f7cd17 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -31,7 +31,7 @@ use crate::body::BodyLength; use crate::header::{FromHeaderValue, HeaderName, Headers}; use crate::{Method, Request, StatusCode, Version}; -/// Maximum size of the header (the start line and the headers). +/// Maximum size of the head (the start line and the headers). /// /// RFC 7230 section 3.1.1 recommends "all HTTP senders and recipients support, /// at a minimum, request-line lengths of 8000 octets." @@ -414,9 +414,7 @@ impl Connection { // (this) request. To handle this we first attempt to parse the // request if we have more than zero bytes (of the next request) in // the first iteration of the loop. - while self.parsed_bytes >= self.buf.len() - || self.buf.len() - self.parsed_bytes <= too_short - { + while self.parsed_bytes >= self.buf.len() || self.buf.len() <= too_short { // While we didn't read the entire previous request body, or // while we have less than `too_short` bytes we try to receive // some more bytes. @@ -441,8 +439,8 @@ impl Connection { // SAFETY: because we received until at least `self.parsed_bytes >= // self.buf.len()` above, we can safely slice the buffer.. match request.parse(&self.buf[self.parsed_bytes..]) { - Ok(httparse::Status::Complete(header_length)) => { - self.parsed_bytes += header_length; + Ok(httparse::Status::Complete(head_length)) => { + self.parsed_bytes += head_length; // SAFETY: all these unwraps are safe because `parse` above // ensures there all `Some`. @@ -480,6 +478,11 @@ impl Connection { Some(BodyLength::Chunked) => { return Err(RequestError::ContentLengthAndTransferEncoding) } + // RFC 7230 section 3.3.3 point 5: + // > If a valid Content-Length header field + // > is present without Transfer-Encoding, + // > its decimal value defines the expected + // > message body length in octets. None => body_length = Some(BodyLength::Known(length)), } } else { @@ -569,7 +572,7 @@ impl Connection { return Ok(Ok(Some(Request::new(method, path, version, headers, body)))); } Ok(httparse::Status::Partial) => { - // Buffer doesn't include the entire request header, try + // Buffer doesn't include the entire request head, try // reading more bytes (in the next iteration). too_short = self.buf.len(); self.last_method = request.method.and_then(|m| m.parse().ok()); diff --git a/http/tests/functional/server.rs b/http/tests/functional/server.rs index aedafa15b..e1551a090 100644 --- a/http/tests/functional/server.rs +++ b/http/tests/functional/server.rs @@ -570,7 +570,7 @@ impl TestServer { const TIMEOUT: Duration = Duration::from_secs(1); let actor = http_actor as fn(_, _, _) -> _; - let address = "127.0.0.1:7890".parse().unwrap(); + let address = "127.0.0.1:0".parse().unwrap(); let server = HttpServer::setup(address, conn_supervisor, actor, ActorOptions::default()) .map_err(rt::Error::setup) .unwrap(); From 25e57777a94cf976af267c16bd37d0892e1d2d46 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 24 May 2021 12:00:10 +0200 Subject: [PATCH 78/81] Move some constants and function to the crate root These will also be used in the client module. --- http/src/lib.rs | 112 ++++++++++++++++++++++++++++++++ http/src/server.rs | 49 ++------------ http/tests/functional/server.rs | 8 +-- 3 files changed, 122 insertions(+), 47 deletions(-) diff --git a/http/src/lib.rs b/http/src/lib.rs index d0369473d..95776d711 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -49,6 +49,60 @@ pub use status_code::StatusCode; #[doc(no_inline)] pub use version::Version; +/// Maximum size of the HTTP head (the start line and the headers). +/// +/// RFC 7230 section 3.1.1 recommends "all HTTP senders and recipients support, +/// at a minimum, request-line lengths of 8000 octets." +pub const MAX_HEAD_SIZE: usize = 16384; + +/// Maximum number of headers parsed from a single [`Request`]/[`Response`]. +pub const MAX_HEADERS: usize = 64; + +/// Minimum amount of bytes read from the connection or the buffer will be +/// grown. +const MIN_READ_SIZE: usize = 4096; + +/// Size of the buffer used in [`server::Connection`] and [`Client`]. +const BUF_SIZE: usize = 8192; + +/// Map a `version` byte to a [`Version`]. +const fn map_version_byte(version: u8) -> Version { + match version { + 0 => Version::Http10, + // RFC 7230 section 2.6: + // > A server SHOULD send a response version equal to + // > the highest version to which the server is + // > conformant that has a major version less than or + // > equal to the one received in the request. + // HTTP/1.1 is the highest we support. + _ => Version::Http11, + } +} + +/// Trim whitespace from `value`. +fn trim_ws(value: &[u8]) -> &[u8] { + let len = value.len(); + if len == 0 { + return value; + } + let mut start = 0; + while start < len { + if !value[start].is_ascii_whitespace() { + break; + } + start += 1; + } + let mut end = len - 1; + while end > start { + if !value[end].is_ascii_whitespace() { + break; + } + end -= 1; + } + // TODO: make this `const`. + &value[start..=end] +} + /// Returns `true` if `lower_case` and `right` are a case-insensitive match. /// /// # Notes @@ -87,3 +141,61 @@ const fn is_lower_case(value: &str) -> bool { } true } + +#[cfg(test)] +mod tests { + use super::{cmp_lower_case, is_lower_case, trim_ws}; + + #[test] + fn test_trim_ws() { + let tests = &[ + ("", ""), + ("abc", "abc"), + (" abc", "abc"), + (" abc ", "abc"), + (" gzip, chunked ", "gzip, chunked"), + ]; + for (input, expected) in tests { + let got = trim_ws(input.as_bytes()); + assert_eq!(got, expected.as_bytes(), "input: {}", input); + } + } + + #[test] + fn test_is_lower_case() { + let tests = &[ + ("", true), + ("abc", true), + ("Abc", false), + ("aBc", false), + ("AbC", false), + ("ABC", false), + ]; + for (input, expected) in tests { + let got = is_lower_case(input); + assert_eq!(got, *expected, "input: {}", input); + } + } + + #[test] + fn test_cmp_lower_case() { + let tests = &[ + ("", "", true), + ("abc", "abc", true), + ("abc", "Abc", true), + ("abc", "aBc", true), + ("abc", "abC", true), + ("abc", "ABC", true), + ("a", "", false), + ("", "a", false), + ("abc", "", false), + ("abc", "d", false), + ("abc", "de", false), + ("abc", "def", false), + ]; + for (lower_case, right, expected) in tests { + let got = cmp_lower_case(lower_case, right); + assert_eq!(got, *expected, "input: '{}', '{}'", lower_case, right); + } + } +} diff --git a/http/src/server.rs b/http/src/server.rs index 690f7cd17..a548a0857 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -29,23 +29,10 @@ use httpdate::HttpDate; use crate::body::BodyLength; use crate::header::{FromHeaderValue, HeaderName, Headers}; -use crate::{Method, Request, StatusCode, Version}; - -/// Maximum size of the head (the start line and the headers). -/// -/// RFC 7230 section 3.1.1 recommends "all HTTP senders and recipients support, -/// at a minimum, request-line lengths of 8000 octets." -pub const MAX_HEAD_SIZE: usize = 16384; - -/// Maximum number of headers parsed from a single request. -pub const MAX_HEADERS: usize = 64; - -/// Minimum amount of bytes read from the connection or the buffer will be -/// grown. -const MIN_READ_SIZE: usize = 4096; - -/// Size of the buffer used in [`Connection`]. -const BUF_SIZE: usize = 8192; +use crate::{ + map_version_byte, trim_ws, Method, Request, StatusCode, Version, BUF_SIZE, MAX_HEADERS, + MAX_HEAD_SIZE, MIN_READ_SIZE, +}; /// A intermediate structure that implements [`NewActor`], creating /// [`HttpServer`]. @@ -450,7 +437,7 @@ impl Connection { }; self.last_method = Some(method); let path = request.path.unwrap().to_string(); - let version = map_version(request.version.unwrap()); + let version = map_version_byte(request.version.unwrap()); self.last_version = Some(version); // RFC 7230 section 3.3.3 Message Body Length. @@ -577,7 +564,7 @@ impl Connection { too_short = self.buf.len(); self.last_method = request.method.and_then(|m| m.parse().ok()); if let Some(version) = request.version { - self.last_version = Some(map_version(version)); + self.last_version = Some(map_version_byte(version)); } if too_short >= MAX_HEAD_SIZE { @@ -954,30 +941,6 @@ impl Connection { } } -const fn map_version(version: u8) -> Version { - match version { - 0 => Version::Http10, - // RFC 7230 section 2.6: - // > A server SHOULD send a response version equal to - // > the highest version to which the server is - // > conformant that has a major version less than or - // > equal to the one received in the request. - // HTTP/1.1 is the highest we support. - _ => Version::Http11, - } -} - -/// Trim whitespace from `value`. -fn trim_ws(value: &[u8]) -> &[u8] { - let start = value.iter().position(|b| !b.is_ascii_whitespace()); - let end = value.iter().rposition(|b| !b.is_ascii_whitespace()); - if let (Some(start), Some(end)) = (start, end) { - &value[start..=end] - } else { - &[] - } -} - /// Add "Content-Length" header to `buf`. fn extend_content_length_header( buf: &mut Vec, diff --git a/http/tests/functional/server.rs b/http/tests/functional/server.rs index e1551a090..029085ac1 100644 --- a/http/tests/functional/server.rs +++ b/http/tests/functional/server.rs @@ -398,13 +398,13 @@ fn read_partial_chunk_size_chunked_transfer_encoding() { #[test] fn too_large_http_head() { - // Tests `heph_http::server::MAX_HEAD_SIZE`. + // Tests `heph_http::MAX_HEAD_SIZE`. with_test_server!(|stream| { stream .write_all(b"GET / HTTP/1.1\r\nSOME_HEADER: ") .unwrap(); - let mut header_value = Vec::with_capacity(heph_http::server::MAX_HEAD_SIZE); - header_value.resize(heph_http::server::MAX_HEAD_SIZE, b'a'); + let mut header_value = Vec::with_capacity(heph_http::MAX_HEAD_SIZE); + header_value.resize(heph_http::MAX_HEAD_SIZE, b'a'); stream.write_all(&header_value).unwrap(); stream.write_all(b"\r\n\r\n").unwrap(); let status = StatusCode::BAD_REQUEST; @@ -484,7 +484,7 @@ fn invalid_http_version() { fn too_many_header() { with_test_server!(|stream| { stream.write_all(b"GET / HTTP/1.1\r\n").unwrap(); - for _ in 0..=http::server::MAX_HEADERS { + for _ in 0..=http::MAX_HEADERS { stream.write_all(b"Some-Header: Abc\r\n").unwrap(); } stream.write_all(b"\r\n").unwrap(); From a11c475b6d8bab000ffa5f9bba9f72d77a3ab1b4 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 24 May 2021 12:59:50 +0200 Subject: [PATCH 79/81] Expand testing Various edge cases and some fmt::Display implementation tests. --- http/src/method.rs | 33 ++++++++-------------- http/src/server.rs | 2 +- http/tests/functional/from_header_value.rs | 26 ++++++++++++++++- http/tests/functional/method.rs | 14 +++++++++ http/tests/functional/status_code.rs | 12 ++++++++ http/tests/functional/version.rs | 7 +++++ 6 files changed, 71 insertions(+), 23 deletions(-) diff --git a/http/src/method.rs b/http/src/method.rs index 14ceb7648..2ed81a179 100644 --- a/http/src/method.rs +++ b/http/src/method.rs @@ -106,7 +106,7 @@ pub struct UnknownMethod; impl fmt::Display for UnknownMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("unknown method") + f.write_str("unknown HTTP method") } } @@ -117,48 +117,39 @@ impl FromStr for Method { match method.len() { 3 => { if cmp_lower_case("get", method) { - Ok(Method::Get) + return Ok(Method::Get); } else if cmp_lower_case("put", method) { - Ok(Method::Put) - } else { - Err(UnknownMethod) + return Ok(Method::Put); } } 4 => { if cmp_lower_case("head", method) { - Ok(Method::Head) + return Ok(Method::Head); } else if cmp_lower_case("post", method) { - Ok(Method::Post) - } else { - Err(UnknownMethod) + return Ok(Method::Post); } } 5 => { if cmp_lower_case("trace", method) { - Ok(Method::Trace) + return Ok(Method::Trace); } else if cmp_lower_case("patch", method) { - Ok(Method::Patch) - } else { - Err(UnknownMethod) + return Ok(Method::Patch); } } 6 => { if cmp_lower_case("delete", method) { - Ok(Method::Delete) - } else { - Err(UnknownMethod) + return Ok(Method::Delete); } } 7 => { if cmp_lower_case("connect", method) { - Ok(Method::Connect) + return Ok(Method::Connect); } else if cmp_lower_case("options", method) { - Ok(Method::Options) - } else { - Err(UnknownMethod) + return Ok(Method::Options); } } - _ => Err(UnknownMethod), + _ => {} } + Err(UnknownMethod) } } diff --git a/http/src/server.rs b/http/src/server.rs index a548a0857..1adaae4db 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -1018,7 +1018,7 @@ impl<'a> Body<'a> { /// the length upfront as it it's determined by reading the length of each /// chunk. If the send request only contained the HTTP head (i.e. no body) /// and uses chunked encoding this would return `false`, as body length is - /// unkown and thus not empty. However if the body would then send a single + /// unknown and thus not empty. However if the body would then send a single /// empty chunk (signaling the end of the body), this would return `true` as /// it turns out the body is indeed empty. pub fn is_empty(&self) -> bool { diff --git a/http/tests/functional/from_header_value.rs b/http/tests/functional/from_header_value.rs index fb43da5c2..0a3a87247 100644 --- a/http/tests/functional/from_header_value.rs +++ b/http/tests/functional/from_header_value.rs @@ -1,7 +1,7 @@ use std::fmt; use std::time::SystemTime; -use heph_http::header::FromHeaderValue; +use heph_http::header::{FromHeaderValue, ParseIntError, ParseTimeError}; #[test] fn str() { @@ -31,6 +31,14 @@ fn integers() { #[test] fn integers_overflow() { + // In multiplication. + test_parse_fail::(b"300"); + test_parse_fail::(b"70000"); + test_parse_fail::(b"5000000000"); + test_parse_fail::(b"20000000000000000000"); + test_parse_fail::(b"20000000000000000000"); + + // In addition. test_parse_fail::(b"257"); test_parse_fail::(b"65537"); test_parse_fail::(b"4294967297"); @@ -69,6 +77,12 @@ fn system_time() { test_parse(b"Thu Jan 1 00:00:00 1970", SystemTime::UNIX_EPOCH); // ANSI C’s `asctime`. } +#[test] +fn invalid_system_time() { + test_parse_fail::(b"\xa0\xa1"); // Invalid UTF-8. + test_parse_fail::(b"ABC, 01 Jan 1970 00:00:00 GMT"); // Invalid format. +} + #[track_caller] fn test_parse<'a, T>(value: &'a [u8], expected: T) where @@ -86,3 +100,13 @@ where { assert!(T::from_bytes(value).is_err()); } + +#[test] +fn parse_int_error_fmt_display() { + assert_eq!(ParseIntError.to_string(), "invalid integer"); +} + +#[test] +fn parse_time_error_fmt_display() { + assert_eq!(ParseTimeError.to_string(), "invalid time"); +} diff --git a/http/tests/functional/method.rs b/http/tests/functional/method.rs index e3607b584..60961e688 100644 --- a/http/tests/functional/method.rs +++ b/http/tests/functional/method.rs @@ -1,3 +1,4 @@ +use heph_http::method::UnknownMethod; use heph_http::Method::{self, *}; use crate::assert_size; @@ -83,6 +84,14 @@ fn from_str() { } } +#[test] +fn from_invalid_str() { + let tests = &["abc", "abcd", "abcde", "abcdef", "abcdefg", "abcdefgh"]; + for input in tests { + assert!(input.parse::().is_err()); + } +} + #[test] fn fmt_display() { let tests = &[ @@ -100,3 +109,8 @@ fn fmt_display() { assert_eq!(*method.to_string(), **expected); } } + +#[test] +fn unknown_method_fmt_display() { + assert_eq!(UnknownMethod.to_string(), "unknown HTTP method"); +} diff --git a/http/tests/functional/status_code.rs b/http/tests/functional/status_code.rs index d32cacac6..3824a619d 100644 --- a/http/tests/functional/status_code.rs +++ b/http/tests/functional/status_code.rs @@ -154,3 +154,15 @@ fn phrase() { assert!(StatusCode(0).phrase().is_none()); assert!(StatusCode(999).phrase().is_none()); } + +#[test] +fn fmt_display() { + let tests = &[ + (StatusCode::OK, "200"), + (StatusCode::BAD_REQUEST, "400"), + (StatusCode(999), "999"), + ]; + for (method, expected) in tests { + assert_eq!(*method.to_string(), **expected); + } +} diff --git a/http/tests/functional/version.rs b/http/tests/functional/version.rs index 1bdb5ffcf..0569227a8 100644 --- a/http/tests/functional/version.rs +++ b/http/tests/functional/version.rs @@ -1,3 +1,4 @@ +use heph_http::version::UnknownVersion; use heph_http::Version::{self, *}; use crate::assert_size; @@ -39,6 +40,7 @@ fn from_str() { assert_eq!(got, *expected); // NOTE: version (unlike most other types) is matched case-sensitive. } + assert!("HTTP/1.2".parse::().is_err()); } #[test] @@ -56,3 +58,8 @@ fn fmt_display() { assert_eq!(*method.to_string(), **expected); } } + +#[test] +fn unknown_version_fmt_display() { + assert_eq!(UnknownVersion.to_string(), "unknown HTTP version"); +} From cfff03c9fb622e3361055447f6cf68f39a209356 Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 24 May 2021 13:36:59 +0200 Subject: [PATCH 80/81] Add initial implementation of the client module Still has plenty of thing to do: * Fix number of TODO/todo!s. * Add tests. --- http/Cargo.toml | 4 + http/src/client.rs | 733 ++++++++++++++++++++++++++++++++ http/src/lib.rs | 3 + http/tests/functional.rs | 1 + http/tests/functional/client.rs | 222 ++++++++++ 5 files changed, 963 insertions(+) create mode 100644 http/src/client.rs create mode 100644 http/tests/functional/client.rs diff --git a/http/Cargo.toml b/http/Cargo.toml index 2dda83e0b..d72cf2707 100644 --- a/http/Cargo.toml +++ b/http/Cargo.toml @@ -13,3 +13,7 @@ itoa = { version = "0.4.7", default-features = false } [dev-dependencies] # Enable logging panics via `std-logger`. std-logger = { version = "0.4.0", default-features = false, features = ["log-panic", "nightly"] } + +[dev-dependencies.heph] +path = "../" +features = ["test"] diff --git a/http/src/client.rs b/http/src/client.rs new file mode 100644 index 000000000..b94c635d4 --- /dev/null +++ b/http/src/client.rs @@ -0,0 +1,733 @@ +//! Module with the HTTP client implementation. + +// FIXME: remove. +#![allow(missing_docs)] + +use std::future::Future; +use std::net::SocketAddr; +use std::pin::Pin; +use std::task::{self, Poll}; +use std::{fmt, io}; + +use heph::net::tcp::stream::{self, TcpStream}; +use heph::{actor, rt}; + +use crate::body::{BodyLength, EmptyBody}; +use crate::header::{FromHeaderValue, HeaderName, Headers}; +use crate::{ + map_version_byte, trim_ws, Method, Response, StatusCode, BUF_SIZE, MAX_HEADERS, MAX_HEAD_SIZE, + MIN_READ_SIZE, +}; + +#[derive(Debug)] +pub struct Client { + stream: TcpStream, + buf: Vec, + /// Number of bytes of `buf` that are already parsed. + /// NOTE: this may be larger then `buf.len()`, in which case a `Body` was + /// dropped without reading it entirely. + parsed_bytes: usize, +} + +impl Client { + /// Create a new HTTP client, connected to `address`. + pub fn connect( + ctx: &mut actor::Context, + address: SocketAddr, + ) -> io::Result + where + RT: rt::Access, + { + TcpStream::connect(ctx, address).map(|connect| Connect { connect }) + } + + /// Send a GET request. + /// + /// # Notes + /// + /// Any [`ResponseError`] are turned into [`io::Error`]. If you want to + /// handle the `ResponseError`s separately use [`Client::request`]. + pub async fn get<'c, 'p>(&'c mut self, path: &'p str) -> io::Result>> { + let res = self + .request(Method::Get, path, &Headers::EMPTY, EmptyBody) + .await; + match res { + Ok(Ok(response)) => Ok(response), + Ok(Err(err)) => Err(err.into()), + Err(err) => Err(err), + } + } + + /// Make a [`Request`] and wait (non-blocking) for a [`Response`]. + /// + /// [`Request`]: crate::Request + /// + /// # Notes + /// + /// This always uses HTTP/1.1 to make the requests. + /// + /// If the server doesn't respond this return an [`io::Error`] with + /// [`io::ErrorKind::UnexpectedEof`]. + pub async fn request<'c, 'b, B>( + &'c mut self, + method: Method, + path: &str, + headers: &Headers, + body: B, + ) -> io::Result>, ResponseError>> + where + B: crate::Body<'b>, + { + self.send_request(method, path, headers, body).await?; + match self.read_response(method).await { + Ok(Ok(Some(request))) => Ok(Ok(request)), + Ok(Ok(None)) => Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "no HTTP response", + )), + Ok(Err(err)) => Ok(Err(err)), + Err(err) => Err(err), + } + } + + pub async fn send_request<'b, B>( + &mut self, + method: Method, + path: &str, + headers: &Headers, + body: B, + ) -> io::Result<()> + where + B: crate::Body<'b>, + { + // Clear bytes from the previous request, keeping the bytes of the + // response. + self.clear_buffer(); + let ignore_end = self.buf.len(); + + // Request line. + self.buf.extend_from_slice(method.as_str().as_bytes()); + self.buf.push(b' '); + self.buf.extend_from_slice(path.as_bytes()); + self.buf.extend_from_slice(b" HTTP/1.1\r\n"); + + // Headers. + let mut set_user_agent_header = false; + let mut set_content_length_header = false; + let mut set_transfer_encoding_header = false; + for header in headers.iter() { + let name = header.name(); + // Field-name: + self.buf.extend_from_slice(name.as_ref().as_bytes()); + // NOTE: spacing after the colon (`:`) is optional. + self.buf.extend_from_slice(b": "); + // Append the header's value. + // NOTE: `header.value` shouldn't contain CRLF (`\r\n`). + self.buf.extend_from_slice(header.value()); + self.buf.extend_from_slice(b"\r\n"); + + if name == &HeaderName::USER_AGENT { + set_user_agent_header = true; + } else if name == &HeaderName::CONTENT_LENGTH { + set_content_length_header = true; + } else if name == &HeaderName::TRANSFER_ENCODING { + set_transfer_encoding_header = true; + } + } + + /* TODO: set "Host" header. + // Provide the "Host" header if the user didn't. + if !set_host_header { + write!(&mut self.buf, "Host: {}\r\n", self.host).unwrap(); + } + */ + + // Provide the "User-Agent" header if the user didn't. + if !set_user_agent_header { + self.buf.extend_from_slice( + concat!("User-Agent: Heph-HTTP/", env!("CARGO_PKG_VERSION"), "\r\n").as_bytes(), + ); + } + + if !set_content_length_header && !set_transfer_encoding_header { + match body.length() { + BodyLength::Known(0) => {} // No need for a "Content-Length" header. + BodyLength::Known(length) => { + let mut itoa_buf = itoa::Buffer::new(); + self.buf.extend_from_slice(b"Content-Length: "); + self.buf + .extend_from_slice(itoa_buf.format(length).as_bytes()); + self.buf.extend_from_slice(b"\r\n"); + } + BodyLength::Chunked => { + self.buf + .extend_from_slice(b"Transfer-Encoding: chunked\r\n"); + } + } + } + + // End of the HTTP head. + self.buf.extend_from_slice(b"\r\n"); + + // Write the request to the stream. + let http_head = &self.buf[ignore_end..]; + body.write_message(&mut self.stream, http_head).await?; + + // Remove the request from the buffer. + self.buf.truncate(ignore_end); + Ok(()) + } + + pub async fn read_response<'a>( + &'a mut self, + request_method: Method, + ) -> io::Result>>, ResponseError>> { + let mut too_short = 0; + loop { + // In case of pipelined responses it could be that while reading a + // previous response's body it partially read the head of the next + // (this) response. To handle this we first attempt to parse the + // response if we have more than zero bytes (of the next response) + // in the first iteration of the loop. + while self.parsed_bytes >= self.buf.len() || self.buf.len() <= too_short { + // While we didn't read the entire previous response body, or + // while we have less than `too_short` bytes we try to receive + // some more bytes. + + self.clear_buffer(); + self.buf.reserve(MIN_READ_SIZE); + if self.stream.recv(&mut self.buf).await? == 0 { + return if self.buf.is_empty() { + // Read the entire stream, so we're done. + Ok(Ok(None)) + } else { + // Couldn't read any more bytes, but we still have bytes + // in the buffer. This means it contains a partial + // response. + Ok(Err(ResponseError::IncompleteResponse)) + }; + } + } + + let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS]; + let mut response = httparse::Response::new(&mut headers); + // SAFETY: because we received until at least `self.parsed_bytes >= + // self.buf.len()` above, we can safely slice the buffer.. + match response.parse(&self.buf[self.parsed_bytes..]) { + Ok(httparse::Status::Complete(head_length)) => { + self.parsed_bytes += head_length; + + // SAFETY: all these unwraps are safe because `parse` above + // ensures there all `Some`. + let version = map_version_byte(response.version.unwrap()); + let status = StatusCode(response.code.unwrap()); + // NOTE: don't care about the reason. + + // RFC 7230 section 3.3.3 Message Body Length. + let mut body_length: Option = None; + let res = Headers::from_httparse_headers(response.headers, |name, value| { + if *name == HeaderName::CONTENT_LENGTH { + // RFC 7230 section 3.3.3 point 4: + // > If a message is received without + // > Transfer-Encoding and with either multiple + // > Content-Length header fields having differing + // > field-values or a single Content-Length header + // > field having an invalid value, then the message + // > framing is invalid and the recipient MUST treat + // > it as an unrecoverable error. [..] If this is a + // > response message received by a user agent, the + // > user agent MUST close the connection to the + // > server and discard the received response. + if let Ok(length) = FromHeaderValue::from_bytes(value) { + match body_length.as_mut() { + Some(ResponseBodyLength::Known(body_length)) + if *body_length == length => {} + Some(ResponseBodyLength::Known(_)) => { + return Err(ResponseError::DifferentContentLengths) + } + Some( + ResponseBodyLength::Chunked | ResponseBodyLength::ReadToEnd, + ) => { + return Err(ResponseError::ContentLengthAndTransferEncoding) + } + // RFC 7230 section 3.3.3 point 5: + // > If a valid Content-Length header field + // > is present without Transfer-Encoding, + // > its decimal value defines the expected + // > message body length in octets. + None => body_length = Some(ResponseBodyLength::Known(length)), + } + } else { + return Err(ResponseError::InvalidContentLength); + } + } else if *name == HeaderName::TRANSFER_ENCODING { + let mut encodings = value.split(|b| *b == b',').peekable(); + while let Some(encoding) = encodings.next() { + match trim_ws(encoding) { + b"chunked" => { + // RFC 7230 section 3.3.3 point 3: + // > If a message is received with both + // > a Transfer-Encoding and a + // > Content-Length header field, the + // > Transfer-Encoding overrides the + // > Content-Length. Such a message + // > might indicate an attempt to + // > perform request smuggling (Section + // > 9.5) or response splitting (Section + // > 9.4) and ought to be handled as an + // > error. + if body_length.is_some() { + return Err( + ResponseError::ContentLengthAndTransferEncoding, + ); + } + + // RFC 7230 section 3.3.3 point 3: + // > If a Transfer-Encoding header field + // > is present in a response and the + // > chunked transfer coding is not the + // > final encoding, the message body + // > length is determined by reading the + // > connection until it is closed by + // > the server. + if encodings.peek().is_some() { + body_length = Some(ResponseBodyLength::ReadToEnd) + } else { + body_length = Some(ResponseBodyLength::Chunked); + } + } + b"identity" => {} // No changes. + // TODO: support "compress", "deflate" and + // "gzip". + _ => return Err(ResponseError::UnsupportedTransferEncoding), + } + } + } + Ok(()) + }); + let headers = match res { + Ok(headers) => headers, + Err(err) => return Ok(Err(err)), + }; + + let kind = match body_length { + // RFC 7230 section 3.3.3 point 2: + // > Any 2xx (Successful) response to a CONNECT request + // > implies that the connection will become a tunnel + // > immediately after the empty line that concludes the + // > header fields. A client MUST ignore any + // > Content-Length or Transfer-Encoding header fields + // > received in such a message. + _ if matches!(request_method, Method::Connect) + && status.is_successful() => + { + BodyKind::Known { left: 0 } + } + Some(ResponseBodyLength::Known(left)) => BodyKind::Known { left }, + Some(ResponseBodyLength::Chunked) => { + #[allow(clippy::cast_possible_truncation)] // For truncate below. + match httparse::parse_chunk_size(&self.buf[self.parsed_bytes..]) { + Ok(httparse::Status::Complete((idx, chunk_size))) => { + self.parsed_bytes += idx; + BodyKind::Chunked { + // FIXME: add check here. It's fine on + // 64 bit (only currently supported). + left_in_chunk: chunk_size as usize, + read_complete: chunk_size == 0, + } + } + Ok(httparse::Status::Partial) => BodyKind::Chunked { + left_in_chunk: 0, + read_complete: false, + }, + Err(_) => return Ok(Err(ResponseError::InvalidChunkSize)), + } + } + Some(ResponseBodyLength::ReadToEnd) => BodyKind::Unknown, + // RFC 7230 section 3.3.3 point 1: + // > Any response to a HEAD request and any response + // > with a 1xx (Informational), 204 (No Content), or + // > 304 (Not Modified) status code is always terminated + // > by the first empty line after the header fields, + // > regardless of the header fields present in the + // > message, and thus cannot contain a message body. + // NOTE: we don't follow this strictly as a server might + // not be implemented corretly, in which case we follow + // the "Content-Length"/"Transfer-Encoding" header + // instead (above). + None if !request_method.expects_body() || !status.includes_body() => { + BodyKind::Known { left: 0 } + } + // RFC 7230 section 3.3.3 point 7: + // > Otherwise, this is a response message without a + // > declared message body length, so the message body + // > length is determined by the number of octets + // > received prior to the server closing the + // > connection. + None => BodyKind::Unknown, + }; + let body = Body { client: self, kind }; + return Ok(Ok(Some(Response::new(version, status, headers, body)))); + } + Ok(httparse::Status::Partial) => { + // Buffer doesn't include the entire response head, try + // reading more bytes (in the next iteration). + too_short = self.buf.len(); + if too_short >= MAX_HEAD_SIZE { + return Ok(Err(ResponseError::HeadTooLarge)); + } + + continue; + } + Err(err) => return Ok(Err(ResponseError::from_httparse(err))), + } + } + } + + async fn read_chunk( + &mut self, + // Fields of `BodyKind::Chunked`: + left_in_chunk: &mut usize, + read_complete: &mut bool, + ) -> io::Result<()> { + loop { + match httparse::parse_chunk_size(&self.buf[self.parsed_bytes..]) { + #[allow(clippy::cast_possible_truncation)] // For truncate below. + Ok(httparse::Status::Complete((idx, chunk_size))) => { + self.parsed_bytes += idx; + if chunk_size == 0 { + *read_complete = true; + } + // FIXME: add check here. It's fine on 64 bit (only currently + // supported). + *left_in_chunk = chunk_size as usize; + return Ok(()); + } + Ok(httparse::Status::Partial) => {} // Read some more data below. + Err(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid chunk size", + )) + } + } + + // Ensure we have space in the buffer to read into. + self.clear_buffer(); + self.buf.reserve(MIN_READ_SIZE); + + if self.stream.recv(&mut self.buf).await? == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + } + } + + /// Clear parsed request(s) from the buffer. + fn clear_buffer(&mut self) { + let buf_len = self.buf.len(); + if self.parsed_bytes >= buf_len { + // Parsed all bytes in the buffer, so we can clear it. + self.buf.clear(); + self.parsed_bytes -= buf_len; + } + + // TODO: move bytes to the start. + } +} + +enum ResponseBodyLength { + /// Body length is known. + Known(usize), + /// Body length is unknown and the body will be transfered using chunked + /// encoding. + Chunked, + /// Body length is unknown, but the response is not chunked. Read until the + /// connection is closed. + ReadToEnd, +} + +/// [`Future`] behind [`Client::connect`]. +#[derive(Debug)] +pub struct Connect { + connect: stream::Connect, +} + +impl Future for Connect { + type Output = io::Result; + + #[track_caller] + fn poll(mut self: Pin<&mut Self>, ctx: &mut task::Context<'_>) -> Poll { + match Pin::new(&mut self.connect).poll(ctx) { + Poll::Ready(Ok(mut stream)) => { + stream.set_nodelay(true)?; + Poll::Ready(Ok(Client { + stream, + buf: Vec::with_capacity(BUF_SIZE), + parsed_bytes: 0, + })) + } + Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), + Poll::Pending => Poll::Pending, + } + } +} + +#[derive(Debug)] +pub struct Body<'c> { + client: &'c mut Client, + kind: BodyKind, +} + +#[derive(Debug)] +enum BodyKind { + /// Known body length. + Known { + /// Number of unread (by the user) bytes. + left: usize, + }, + /// Chunked transfer encoding. + Chunked { + /// Number of unread (by the user) bytes in this chunk. + left_in_chunk: usize, + /// Read all chunks. + read_complete: bool, + }, + /// Body length is not known, read the body until the server closes the + /// connection. + Unknown, +} + +impl<'c> Body<'c> { + /* + /// Returns `true` if the body is completely read (or was empty to begin + /// with). + /// + /// # Notes + /// + /// This can return `false` for empty bodies using chunked encoding if not + /// enough bytes have been read yet. Using chunked encoding we don't know + /// the length upfront as it it's determined by reading the length of each + /// chunk. If the send request only contained the HTTP head (i.e. no body) + /// and uses chunked encoding this would return `false`, as body length is + /// unknown and thus not empty. However if the body would then send a single + /// empty chunk (signaling the end of the body), this would return `true` as + /// it turns out the body is indeed empty. + pub fn is_empty(&self) -> bool { + match self.kind { + BodyKind::Known { left } => left == 0, + BodyKind::Chunked { + left_in_chunk, + read_complete, + } => read_complete && left_in_chunk == 0, + } + } + + /// Returns `true` if the body is chunked. + pub fn is_chunked(&self) -> bool { + matches!(self.kind, BodyKind::Chunked { .. }) + } + */ + + /* + TODO: RFC 7230 section 3.3.3 point 5: + [..] If the sender closes the connection or the recipient times out + before the indicated number of octets are received, the recipient MUST + consider the message to be incomplete and close the connection. + */ + + pub async fn read_all(&mut self, buf: &mut Vec, limit: usize) -> io::Result<()> { + let mut total = 0; + loop { + // Copy bytes in our buffer. + let bytes = self.buf_bytes(); + let len = bytes.len(); + if limit < total + len { + return Err(io::Error::new(io::ErrorKind::Other, "body too large")); + } + + buf.extend_from_slice(bytes); + self.processed(len); + total += len; + + match &mut self.kind { + // Read all the bytes from the body. + BodyKind::Known { left: 0 } => return Ok(()), + // Read all the bytes in the chunk, so need to read another + // chunk. + BodyKind::Chunked { + left_in_chunk, + read_complete, + } if *left_in_chunk == 0 => { + if *read_complete { + return Ok(()); + } + + self.client.read_chunk(left_in_chunk, read_complete).await?; + // Copy read bytes again. + continue; + } + // Continue to reading below. + BodyKind::Known { .. } | BodyKind::Chunked { .. } | BodyKind::Unknown => break, + } + } + + todo!("client::Body::read_all") + /* + loop { + // Limit the read until the end of the chunk/body. + let chunk_len = self.chunk_len(); + if chunk_len == 0 { + return Ok(()); + } else if limit < total + chunk_len { + return Err(io::Error::new(io::ErrorKind::Other, "body too large")); + } + + (&mut *buf).reserve(chunk_len); + self.conn.stream.recv_n(&mut *buf, chunk_len).await?; + total += chunk_len; + } + */ + } + + /// Returns the bytes currently in the buffer. + /// + /// This is limited to the bytes of this request/chunk, i.e. it doesn't + /// contain the next request/chunk. + fn buf_bytes(&self) -> &[u8] { + let bytes = &self.client.buf[self.client.parsed_bytes..]; + match self.kind { + BodyKind::Known { left } + | BodyKind::Chunked { + left_in_chunk: left, + .. + } if bytes.len() > left => &bytes[..left], + _ => bytes, + } + } + + /// Mark `n` bytes are processed. + fn processed(&mut self, n: usize) { + // TODO: should this be `unsafe`? We don't do underflow checks... + match &mut self.kind { + BodyKind::Known { left } => *left -= n, + BodyKind::Chunked { left_in_chunk, .. } => *left_in_chunk -= n, + BodyKind::Unknown => {} + } + self.client.parsed_bytes += n; + } +} + +/// Error parsing HTTP response. +#[derive(Copy, Clone, Debug)] +pub enum ResponseError { + /// Missing part of response. + IncompleteResponse, + /// HTTP Head (start line and headers) is too large. + /// + /// Limit is defined by [`MAX_HEAD_SIZE`]. + HeadTooLarge, + /// Value in the "Content-Length" header is invalid. + InvalidContentLength, + /// Multiple "Content-Length" headers were present with differing values. + DifferentContentLengths, + /// Invalid byte in header name. + InvalidHeaderName, + /// Invalid byte in header value. + InvalidHeaderValue, + /// Number of headers send in the request is larger than [`MAX_HEADERS`]. + TooManyHeaders, + /// Unsupported "Transfer-Encoding" header. + UnsupportedTransferEncoding, + /// Response contains both "Content-Length" and "Transfer-Encoding" headers. + /// + /// An attacker might attempt to "smuggle a request" ("HTTP Response + /// Smuggling", Linhart et al., June 2005) or "split a response" ("Divide + /// and Conquer - HTTP Response Splitting, Web Cache Poisoning Attacks, and + /// Related Topics", Klein, March 2004). RFC 7230 (see section 3.3.3 point + /// 3) says that this "ought to be handled as an error", and so we do. + ContentLengthAndTransferEncoding, + /// Invalid byte where token is required. + InvalidToken, + /// Invalid byte in new line. + InvalidNewLine, + /// Invalid byte in HTTP version. + InvalidVersion, + /// Invalid byte in status code. + InvalidStatus, + /// Chunk size is invalid. + InvalidChunkSize, +} + +impl ResponseError { + /* TODO. + /// Returns `true` if the connection should be closed based on the error + /// (after sending a error response). + pub const fn should_close(self) -> bool { + use RequestError::*; + // See the parsing code for various references to the RFC(s) that + // determine the values here. + match self { + IncompleteRequest + | HeadTooLarge + | InvalidContentLength + | DifferentContentLengths + | InvalidHeaderName + | InvalidHeaderValue + | UnsupportedTransferEncoding + | TooManyHeaders + | ContentLengthAndTransferEncoding + | InvalidToken + | InvalidNewLine + | InvalidVersion + | InvalidChunkSize => true, + UnknownMethod => false, + } + } + */ + + fn from_httparse(err: httparse::Error) -> ResponseError { + use httparse::Error::*; + match err { + HeaderName => ResponseError::InvalidHeaderName, + HeaderValue => ResponseError::InvalidHeaderValue, + Token => ResponseError::InvalidToken, + NewLine => ResponseError::InvalidNewLine, + Version => ResponseError::InvalidVersion, + TooManyHeaders => ResponseError::TooManyHeaders, + Status => ResponseError::InvalidStatus, + } + } + + fn as_str(self) -> &'static str { + use ResponseError::*; + match self { + IncompleteResponse => "incomplete response", + HeadTooLarge => "head too large", + InvalidContentLength => "invalid Content-Length header", + DifferentContentLengths => "different Content-Length headers", + InvalidHeaderName => "invalid header name", + InvalidHeaderValue => "invalid header value", + TooManyHeaders => "too many header", + UnsupportedTransferEncoding => "unsupported Transfer-Encoding", + ContentLengthAndTransferEncoding => { + "contained both Content-Length and Transfer-Encoding headers" + } + InvalidToken | InvalidNewLine => "invalid request syntax", + InvalidVersion => "invalid version", + InvalidStatus => "invalid status", + InvalidChunkSize => "invalid chunk size", + } + } +} + +impl From for io::Error { + fn from(err: ResponseError) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, err.as_str()) + } +} + +impl fmt::Display for ResponseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/http/src/lib.rs b/http/src/lib.rs index 95776d711..8e8d8ec1c 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -27,6 +27,7 @@ )] pub mod body; +pub mod client; pub mod header; pub mod method; mod request; @@ -38,6 +39,8 @@ pub mod version; #[doc(no_inline)] pub use body::Body; #[doc(no_inline)] +pub use client::Client; +#[doc(no_inline)] pub use header::{Header, HeaderName, Headers}; #[doc(no_inline)] pub use method::Method; diff --git a/http/tests/functional.rs b/http/tests/functional.rs index f5c7693e3..c1fe31517 100644 --- a/http/tests/functional.rs +++ b/http/tests/functional.rs @@ -11,6 +11,7 @@ fn assert_size(expected: usize) { #[path = "functional"] // rustfmt can't find the files. mod functional { + mod client; mod from_header_value; mod header; mod method; diff --git a/http/tests/functional/client.rs b/http/tests/functional/client.rs new file mode 100644 index 000000000..777f3fed1 --- /dev/null +++ b/http/tests/functional/client.rs @@ -0,0 +1,222 @@ +#![allow(unused_imports)] + +use std::borrow::Cow; +use std::io::{self, Read, Write}; +use std::lazy::SyncLazy; +use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream}; +use std::sync::{Arc, Condvar, Mutex, Weak}; +use std::task::Poll; +use std::thread::{self, sleep}; +use std::time::{Duration, SystemTime}; +use std::{fmt, str}; + +use heph::actor::messages::Terminate; +use heph::rt::{self, Runtime, ThreadSafe}; +use heph::spawn::options::{ActorOptions, Priority}; +use heph::test::{init_actor, poll_actor}; +use heph::{actor, Actor, ActorRef, NewActor, Supervisor, SupervisorStrategy}; +use heph_http::body::OneshotBody; +use heph_http::server::{HttpServer, RequestError}; +use heph_http::{ + self as http, Client, Header, HeaderName, Headers, Method, Response, StatusCode, Version, +}; +use httpdate::fmt_http_date; + +const USER_AGENT: &[u8] = b"Heph-HTTP/0.1.0"; + +/// Macro to run with a test server. +macro_rules! with_test_server { + (|$test_server: ident| $test: block) => { + let test_server = TestServer::spawn(); + let $test_server = test_server; + $test + }; +} + +#[test] +fn get() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client.get("/").await?; + let headers = Headers::from([Header::new(HeaderName::CONTENT_LENGTH, b"2")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +fn expect_request( + stream: &mut TcpStream, + // Expected values: + method: Method, + path: &str, + version: Version, + headers: &Headers, + body: &[u8], +) { + let mut buf = [0; 1024]; + let n = stream.read(&mut buf).unwrap(); + let buf = &buf[..n]; + + eprintln!("read response: {:?}", str::from_utf8(&buf[..n])); + + let mut h = [httparse::EMPTY_HEADER; 64]; + let mut request = httparse::Request::new(&mut h); + let parsed_n = request.parse(&buf).unwrap().unwrap(); + + assert_eq!(request.method, Some(method.as_str())); + assert_eq!(request.path, Some(path)); + assert_eq!(request.version, Some(version.minor())); + assert_eq!( + request.headers.len(), + headers.len(), + "mismatch headers lengths, got: {:?}, expected: {:?}", + request.headers, + headers + ); + for got_header in request.headers { + let got_header_name = HeaderName::from_str(got_header.name); + let got = headers.get_value(&got_header_name).unwrap(); + assert_eq!( + got_header.value, + got, + "different header values for '{}' header, got: '{:?}', expected: '{:?}'", + got_header_name, + str::from_utf8(got_header.value), + str::from_utf8(got) + ); + } + assert_eq!(&buf[parsed_n..], body, "different bodies"); + assert_eq!(parsed_n, n - body.len(), "unexpected extra bytes"); +} + +async fn expect_response( + mut response: Response>, + // Expected values: + version: Version, + status: StatusCode, + headers: &Headers, + body: &[u8], +) { + eprintln!("read response: {:?}", response); + assert_eq!(response.version(), version); + assert_eq!(response.status(), status); + assert_eq!( + response.headers().len(), + headers.len(), + "mismatch headers lengths, got: {:?}, expected: {:?}", + response.headers(), + headers + ); + for got_header in response.headers().iter() { + let expected = headers.get_value(&got_header.name()).unwrap(); + assert_eq!( + got_header.value(), + expected, + "different header values for '{}' header, got: '{:?}', expected: '{:?}'", + got_header.name(), + str::from_utf8(got_header.value()), + str::from_utf8(expected) + ); + } + let mut got_body = Vec::new(); + response + .body_mut() + .read_all(&mut got_body, 1024) + .await + .unwrap(); + assert_eq!(got_body, body, "different bodies"); +} + +struct TestServer { + address: SocketAddr, + listener: Mutex, +} + +impl TestServer { + fn spawn() -> Arc { + static TEST_SERVER: SyncLazy>> = + SyncLazy::new(|| Mutex::new(Weak::new())); + + let mut test_server = TEST_SERVER.lock().unwrap(); + if let Some(test_server) = test_server.upgrade() { + // Use an existing running server. + test_server + } else { + // Start a new server. + let new_server = Arc::new(TestServer::new()); + *test_server = Arc::downgrade(&new_server); + new_server + } + } + + fn new() -> TestServer { + let address: SocketAddr = "127.0.0.1:0".parse().unwrap(); + let listener = TcpListener::bind(address).unwrap(); + let address = listener.local_addr().unwrap(); + + TestServer { + address, + listener: Mutex::new(listener), + } + } + + #[track_caller] + fn accept(&self, spawn: F) -> (TcpStream, thread::JoinHandle<()>) + where + F: FnOnce(SocketAddr) -> A, + A: Actor + Send + 'static, + A::Error: fmt::Display, + { + let listener = self.listener.lock().unwrap(); + let actor = spawn(self.address); + let mut actor = Box::pin(actor); + let handle = thread::spawn(move || { + for _ in 0..100 { + match poll_actor(actor.as_mut()) { + Poll::Pending => {} + Poll::Ready(Ok(())) => return, + Poll::Ready(Err(err)) => panic!("error in actor: {}", err), + } + sleep(Duration::from_millis(10)); + } + panic!("looped too many times"); + }); + let (stream, _) = listener.accept().unwrap(); + drop(listener); + stream.set_nodelay(true).unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + stream + .set_write_timeout(Some(Duration::from_secs(1))) + .unwrap(); + (stream, handle) + } +} From 7fd3a60e0defe3d7dace178feeb00b7afec7b9ea Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Mon, 24 May 2021 17:00:44 +0200 Subject: [PATCH 81/81] Add tests for the http::Client --- http/src/client.rs | 97 ++- http/src/server.rs | 6 +- http/tests/functional/client.rs | 1266 ++++++++++++++++++++++++++++++- 3 files changed, 1313 insertions(+), 56 deletions(-) diff --git a/http/src/client.rs b/http/src/client.rs index b94c635d4..e197b059a 100644 --- a/http/src/client.rs +++ b/http/src/client.rs @@ -3,6 +3,7 @@ // FIXME: remove. #![allow(missing_docs)] +use std::cmp::min; use std::future::Future; use std::net::SocketAddr; use std::pin::Pin; @@ -352,7 +353,7 @@ impl Client { // > regardless of the header fields present in the // > message, and thus cannot contain a message body. // NOTE: we don't follow this strictly as a server might - // not be implemented corretly, in which case we follow + // not be implemented correctly, in which case we follow // the "Content-Length"/"Transfer-Encoding" header // instead (above). None if !request_method.expects_body() || !status.includes_body() => { @@ -571,22 +572,39 @@ impl<'c> Body<'c> { } } - todo!("client::Body::read_all") - /* loop { // Limit the read until the end of the chunk/body. - let chunk_len = self.chunk_len(); - if chunk_len == 0 { - return Ok(()); - } else if limit < total + chunk_len { - return Err(io::Error::new(io::ErrorKind::Other, "body too large")); + let chunk_len = match self.kind { + BodyKind::Known { left } => Some(left), + BodyKind::Chunked { left_in_chunk, .. } => Some(left_in_chunk), + BodyKind::Unknown => None, + }; + + if let Some(chunk_len) = chunk_len { + if chunk_len == 0 { + return Ok(()); + } else if total + chunk_len > limit { + return Err(io::Error::new(io::ErrorKind::Other, "body too large")); + } } - (&mut *buf).reserve(chunk_len); - self.conn.stream.recv_n(&mut *buf, chunk_len).await?; - total += chunk_len; + let capacity = chunk_len + .unwrap_or_else(|| min(MIN_READ_SIZE, limit.saturating_sub(buf.capacity()))); + (&mut *buf).reserve(capacity); + if let Some(chunk_len) = chunk_len { + // FIXME: doesn't deal with chunked bodies. + return self.client.stream.recv_n(&mut *buf, chunk_len).await; + } else { + let n = self.client.stream.recv(&mut *buf).await?; + if n == 0 { + return Ok(()); + } + total += n; + if total > limit { + return Err(io::Error::new(io::ErrorKind::Other, "body too large")); + } + } } - */ } /// Returns the bytes currently in the buffer. @@ -617,8 +635,10 @@ impl<'c> Body<'c> { } } +// FIXME: remove body from `Client` if it's dropped before it's fully read. + /// Error parsing HTTP response. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ResponseError { /// Missing part of response. IncompleteResponse, @@ -646,8 +666,6 @@ pub enum ResponseError { /// Related Topics", Klein, March 2004). RFC 7230 (see section 3.3.3 point /// 3) says that this "ought to be handled as an error", and so we do. ContentLengthAndTransferEncoding, - /// Invalid byte where token is required. - InvalidToken, /// Invalid byte in new line. InvalidNewLine, /// Invalid byte in HTTP version. @@ -659,38 +677,19 @@ pub enum ResponseError { } impl ResponseError { - /* TODO. /// Returns `true` if the connection should be closed based on the error /// (after sending a error response). pub const fn should_close(self) -> bool { - use RequestError::*; - // See the parsing code for various references to the RFC(s) that - // determine the values here. - match self { - IncompleteRequest - | HeadTooLarge - | InvalidContentLength - | DifferentContentLengths - | InvalidHeaderName - | InvalidHeaderValue - | UnsupportedTransferEncoding - | TooManyHeaders - | ContentLengthAndTransferEncoding - | InvalidToken - | InvalidNewLine - | InvalidVersion - | InvalidChunkSize => true, - UnknownMethod => false, - } + // Currently all errors are fatal for the connection. + true } - */ fn from_httparse(err: httparse::Error) -> ResponseError { use httparse::Error::*; match err { HeaderName => ResponseError::InvalidHeaderName, HeaderValue => ResponseError::InvalidHeaderValue, - Token => ResponseError::InvalidToken, + Token => unreachable!(), NewLine => ResponseError::InvalidNewLine, Version => ResponseError::InvalidVersion, TooManyHeaders => ResponseError::TooManyHeaders, @@ -702,20 +701,20 @@ impl ResponseError { use ResponseError::*; match self { IncompleteResponse => "incomplete response", - HeadTooLarge => "head too large", - InvalidContentLength => "invalid Content-Length header", - DifferentContentLengths => "different Content-Length headers", - InvalidHeaderName => "invalid header name", - InvalidHeaderValue => "invalid header value", - TooManyHeaders => "too many header", - UnsupportedTransferEncoding => "unsupported Transfer-Encoding", + HeadTooLarge => "response head too large", + InvalidContentLength => "invalid response Content-Length header", + DifferentContentLengths => "response has different Content-Length headers", + InvalidHeaderName => "invalid response header name", + InvalidHeaderValue => "invalid response header value", + TooManyHeaders => "too many response headers", + UnsupportedTransferEncoding => "response has unsupported Transfer-Encoding header", ContentLengthAndTransferEncoding => { - "contained both Content-Length and Transfer-Encoding headers" + "response contained both Content-Length and Transfer-Encoding headers" } - InvalidToken | InvalidNewLine => "invalid request syntax", - InvalidVersion => "invalid version", - InvalidStatus => "invalid status", - InvalidChunkSize => "invalid chunk size", + InvalidNewLine => "invalid response syntax", + InvalidVersion => "invalid HTTP response version", + InvalidStatus => "invalid HTTP response status", + InvalidChunkSize => "invalid response chunk size", } } } diff --git a/http/src/server.rs b/http/src/server.rs index 1adaae4db..70441d5e8 100644 --- a/http/src/server.rs +++ b/http/src/server.rs @@ -1099,13 +1099,15 @@ impl<'a> Body<'a> { let chunk_len = self.chunk_len(); if chunk_len == 0 { return Ok(()); - } else if limit < total + chunk_len { + } else if total + chunk_len > limit { return Err(io::Error::new(io::ErrorKind::Other, "body too large")); } (&mut *buf).reserve(chunk_len); self.conn.stream.recv_n(&mut *buf, chunk_len).await?; total += chunk_len; + + // FIXME: doesn't deal with chunked bodies. } } @@ -1438,7 +1440,7 @@ impl<'a> Drop for Body<'a> { } /// Error parsing HTTP request. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum RequestError { /// Missing part of request. IncompleteRequest, diff --git a/http/tests/functional/client.rs b/http/tests/functional/client.rs index 777f3fed1..a99c3d9d9 100644 --- a/http/tests/functional/client.rs +++ b/http/tests/functional/client.rs @@ -15,11 +15,10 @@ use heph::rt::{self, Runtime, ThreadSafe}; use heph::spawn::options::{ActorOptions, Priority}; use heph::test::{init_actor, poll_actor}; use heph::{actor, Actor, ActorRef, NewActor, Supervisor, SupervisorStrategy}; -use heph_http::body::OneshotBody; +use heph_http::body::{EmptyBody, OneshotBody}; +use heph_http::client::{Client, ResponseError}; use heph_http::server::{HttpServer, RequestError}; -use heph_http::{ - self as http, Client, Header, HeaderName, Headers, Method, Response, StatusCode, Version, -}; +use heph_http::{self as http, Header, HeaderName, Headers, Method, Response, StatusCode, Version}; use httpdate::fmt_http_date; const USER_AGENT: &[u8] = b"Heph-HTTP/0.1.0"; @@ -71,6 +70,1262 @@ fn get() { }); } +#[test] +fn get_no_response() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client.get("/").await.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof); + assert_eq!(err.to_string(), "no HTTP response"); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // No response. + drop(stream); + + handle.join().unwrap(); + }); +} + +#[test] +fn get_invalid_response() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client.get("/").await.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + assert_eq!(err.to_string(), "invalid HTTP response status"); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream + .write_all(b"HTTP/1.1 a00\r\nContent-Length: 2\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn request_with_headers() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let headers = Headers::from([Header::new(HeaderName::HOST, b"localhost")]); + let response = client + .request(Method::Get, "/", &headers, EmptyBody) + .await? + .unwrap(); + let headers = Headers::from([Header::new(HeaderName::CONTENT_LENGTH, b"2")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([ + Header::new(HeaderName::USER_AGENT, USER_AGENT), + Header::new(HeaderName::HOST, b"localhost"), + ]), + b"", + ); + + // Write response. + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn request_with_user_agent_header() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let headers = Headers::from([Header::new(HeaderName::USER_AGENT, b"my-user-agent")]); + let response = client + .request(Method::Get, "/", &headers, EmptyBody) + .await? + .unwrap(); + let headers = Headers::from([Header::new(HeaderName::CONTENT_LENGTH, b"2")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, b"my-user-agent")]), + b"", + ); + + // Write response. + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +/* FIXME: The following tests have the following problem: +error: implementation of `body::private::PrivateBody` is not general enough + --> http/tests/functional/client.rs:255:48 + | +255 | let (mut stream, handle) = test_server.accept(|address| { + | ^^^^^^ implementation of `body::private::PrivateBody` is not general enough + | + = note: `body::private::PrivateBody<'1>` would have to be implemented for the type `OneshotBody<'0>`, for any two lifetimes `'0` and `'1`... + = note: ...but `body::private::PrivateBody<'2>` is actually implemented for the type `OneshotBody<'2>`, for some specific lifetime `'2` + +#[test] +fn request_with_content_length_header() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let body = OneshotBody::new(b"Hi"); + // NOTE: Content-Length is incorrect for this test! + let headers = Headers::from([Header::new(HeaderName::CONTENT_LENGTH, b"3")]); + let response = client + .request(Method::Get, "/", &headers, body) + .await? + .unwrap(); + let headers = Headers::from([Header::new(HeaderName::CONTENT_LENGTH, b"2")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([ + Header::new(HeaderName::USER_AGENT, USER_AGENT), + Header::new(HeaderName::CONTENT_LENGTH, b"3"), + ]), + b"hi", + ); + + // Write response. + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn request_with_transfer_encoding_header() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let headers = Headers::from([Header::new(HeaderName::TRANSFER_ENCODING, b"identify")]); + let body = OneshotBody::new(b"Hi"); + let response = client + .request(Method::Get, "/", &headers, body) + .await? + .unwrap(); + let headers = Headers::from([Header::new(HeaderName::CONTENT_LENGTH, b"2")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([ + Header::new(HeaderName::USER_AGENT, USER_AGENT), + Header::new(HeaderName::TRANSFER_ENCODING, b"identify"), + ]), + b"hi", + ); + + // Write response. + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn request_sets_content_length_header() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let body = OneshotBody::new(b"Hello"); + let response = client + .request(Method::Get, "/", &Headers::EMPTY, body) + .await? + .unwrap(); + let headers = Headers::from([Header::new(HeaderName::CONTENT_LENGTH, b"2")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([ + Header::new(HeaderName::USER_AGENT, USER_AGENT), + Header::new(HeaderName::CONTENT_LENGTH, b"4"), + ]), + b"Hello", + ); + + // Write response. + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} +*/ + +// TODO: add test with `ChunkedBody`. + +#[test] +fn partial_response() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::IncompleteResponse); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Partal response, missing last `\r\n`. + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\n") + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn same_content_length() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client.get("/").await?; + let headers = Headers::from([ + Header::new(HeaderName::CONTENT_LENGTH, b"2"), + Header::new(HeaderName::CONTENT_LENGTH, b"2"), + ]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\nContent-Length: 2\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn different_content_length() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::DifferentContentLengths); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\nContent-Length: 4\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn transfer_encoding_and_content_length_and() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::ContentLengthAndTransferEncoding); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nContent-Length: 2\r\n\r\n") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn invalid_content_length() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::InvalidContentLength); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: abc\r\n\r\nOk") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn chunked_transfer_encoding() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client.get("/").await?; + let headers = Headers::from([Header::new(HeaderName::TRANSFER_ENCODING, b"chunked")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nOk0\r\n") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn slow_chunked_transfer_encoding() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client.get("/").await?; + let headers = Headers::from([Header::new(HeaderName::TRANSFER_ENCODING, b"chunked")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n") + .unwrap(); + sleep(Duration::from_millis(100)); + stream.write_all(b"2\r\nOk0\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn empty_chunked_transfer_encoding() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client.get("/").await?; + let headers = Headers::from([Header::new(HeaderName::TRANSFER_ENCODING, b"chunked")]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn content_length_and_identity_transfer_encoding() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client.get("/").await?; + let headers = Headers::from([ + Header::new(HeaderName::CONTENT_LENGTH, b"2"), + Header::new(HeaderName::TRANSFER_ENCODING, b"identity"), + ]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all( + b"HTTP/1.1 200\r\nContent-Length: 2\r\nTransfer-Encoding: identity\r\n\r\nOk", + ) + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn unsupported_transfer_encoding() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::UnsupportedTransferEncoding); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nTransfer-Encoding: gzip\r\n\r\n") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn chunked_not_last_transfer_encoding() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client.get("/").await?; + let headers = Headers::from([Header::new( + HeaderName::TRANSFER_ENCODING, + b"chunked, identity", + )]); + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nTransfer-Encoding: chunked, identity\r\n\r\nOk") + .unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn content_length_and_transfer_encoding() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::ContentLengthAndTransferEncoding); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nContent-Length: 2\r\nTransfer-Encoding: chunked\r\n\r\n") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn invalid_chunk_size() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::InvalidChunkSize); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + stream + .write_all(b"HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nQ\r\nOk0\r\n") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn connect() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client + .request(Method::Connect, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap(); + let headers = Headers::EMPTY; + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Connect, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTP/1.1 200\r\n\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn head() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client + .request(Method::Head, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap(); + let headers = Headers::EMPTY; + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Head, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTP/1.1 200\r\n\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn response_status_204() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap(); + let headers = Headers::EMPTY; + let status = StatusCode::NO_CONTENT; + expect_response(response, Version::Http11, status, &headers, b"").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTP/1.1 204\r\n\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn no_content_length_no_transfer_encoding_response() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let response = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap(); + let headers = Headers::EMPTY; + expect_response(response, Version::Http11, StatusCode::OK, &headers, b"Ok").await; + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTP/1.1 200\r\n\r\nOk").unwrap(); + stream.shutdown(Shutdown::Write).unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn response_head_too_large() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::HeadTooLarge); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTP/1.1 200\r\n").unwrap(); + let buf = [b'a'; http::MAX_HEAD_SIZE]; + stream.write_all(&buf).unwrap(); + stream.write_all(b"\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn invalid_header_name() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::InvalidHeaderName); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTP/1.1 200\r\n\0: \r\n\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn invalid_header_value() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::InvalidHeaderValue); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream + .write_all(b"HTTP/1.1 200\r\nAbc: Header\rvalue\r\n\r\n") + .unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn invalid_new_line() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::InvalidNewLine); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"\rHTTP/1.1 200\r\n\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn invalid_version() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::InvalidVersion); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTPS/1.1 200\r\n\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn invalid_status() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::InvalidStatus); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTP/1.1 2009\r\n\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + +#[test] +fn too_many_headers() { + with_test_server!(|test_server| { + async fn http_actor( + mut ctx: actor::Context, + address: SocketAddr, + ) -> io::Result<()> { + let mut client = Client::connect(&mut ctx, address)?.await?; + let err = client + .request(Method::Get, "/", &Headers::EMPTY, EmptyBody) + .await? + .unwrap_err(); + assert_eq!(err, ResponseError::TooManyHeaders); + Ok(()) + } + + let (mut stream, handle) = test_server.accept(|address| { + let http_actor = http_actor as fn(_, _) -> _; + let (actor, _) = init_actor(http_actor, address).unwrap(); + actor + }); + + expect_request( + &mut stream, + Method::Get, + "/", + Version::Http11, + &Headers::from([Header::new(HeaderName::USER_AGENT, USER_AGENT)]), + b"", + ); + + // Write response. + stream.write_all(b"HTTP/1.1 200\r\n").unwrap(); + for _ in 0..=http::MAX_HEADERS { + stream.write_all(b"Some-Header: Abc\r\n").unwrap(); + } + stream.write_all(b"\r\n").unwrap(); + + handle.join().unwrap(); + }); +} + fn expect_request( stream: &mut TcpStream, // Expected values: @@ -84,7 +1339,7 @@ fn expect_request( let n = stream.read(&mut buf).unwrap(); let buf = &buf[..n]; - eprintln!("read response: {:?}", str::from_utf8(&buf[..n])); + eprintln!("read request: {:?}", str::from_utf8(&buf[..n])); let mut h = [httparse::EMPTY_HEADER; 64]; let mut request = httparse::Request::new(&mut h); @@ -197,6 +1452,7 @@ impl TestServer { let listener = self.listener.lock().unwrap(); let actor = spawn(self.address); let mut actor = Box::pin(actor); + // TODO: don't run this on a different thread, use a test Heph runtime. let handle = thread::spawn(move || { for _ in 0..100 { match poll_actor(actor.as_mut()) {