diff options
Diffstat (limited to 'third_party/rust/bhttp')
-rw-r--r-- | third_party/rust/bhttp/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/bhttp/Cargo.toml | 45 | ||||
-rw-r--r-- | third_party/rust/bhttp/README.md | 25 | ||||
-rw-r--r-- | third_party/rust/bhttp/src/err.rs | 38 | ||||
-rw-r--r-- | third_party/rust/bhttp/src/lib.rs | 797 | ||||
-rw-r--r-- | third_party/rust/bhttp/src/parse.rs | 81 | ||||
-rw-r--r-- | third_party/rust/bhttp/src/rw.rs | 106 | ||||
-rw-r--r-- | third_party/rust/bhttp/tests/test.rs | 225 |
8 files changed, 1318 insertions, 0 deletions
diff --git a/third_party/rust/bhttp/.cargo-checksum.json b/third_party/rust/bhttp/.cargo-checksum.json new file mode 100644 index 0000000000..aef4088653 --- /dev/null +++ b/third_party/rust/bhttp/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"50cee393fec5ecf4c7c34fbbaefa37c3c5a6bf6c3acee2d035ac49d691c74f16","README.md":"30c2c4a78875fd78a6b007954ed0aceed4004983e5ff62a38261bbcd1a9c36a3","src/err.rs":"0b605edff622d043c9269675c74e412f3363a04060f2921356d0afcd27987c02","src/lib.rs":"0aff5fd18b0f90f3228a8f4c643ebb679fca8c909f497cbeb2f7e18009d8e932","src/parse.rs":"a2428cf6736cb6d251bb2c95d80d84f3c3fd0197a831f94045408ac592fd7013","src/rw.rs":"a667035f34b41e2492a417d1aad6454422f6769ece578faa75205a6335449511","tests/test.rs":"ec171a6a4d469de8d4fe921872a9c0e15b27ae9ca2c19ff4e756b5941e0a8b57"},"package":"1300dca7a20730cce82c33fbf8795862556645ef5e9ee835390278c3fe1eb1d0"}
\ No newline at end of file diff --git a/third_party/rust/bhttp/Cargo.toml b/third_party/rust/bhttp/Cargo.toml new file mode 100644 index 0000000000..9b3a01d305 --- /dev/null +++ b/third_party/rust/bhttp/Cargo.toml @@ -0,0 +1,45 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "bhttp" +version = "0.3.1" +authors = ["Martin Thomson <mt@lowentropy.net>"] +description = "Binary HTTP messages (draft-ietf-httpbis-binary-message)" +readme = "README.md" +license = "MIT OR Apache-2.0" +repository = "https://github.com/martinthomson/ohttp" + +[dependencies.thiserror] +version = "1" + +[dependencies.url] +version = "2" +optional = true + +[dev-dependencies.hex] +version = "0.4" + +[features] +bhttp = [ + "read-bhttp", + "write-bhttp", +] +default = ["bhttp"] +http = [ + "read-http", + "write-http", +] +read-bhttp = [] +read-http = ["url"] +write-bhttp = [] +write-http = [] diff --git a/third_party/rust/bhttp/README.md b/third_party/rust/bhttp/README.md new file mode 100644 index 0000000000..2a6bc21b6e --- /dev/null +++ b/third_party/rust/bhttp/README.md @@ -0,0 +1,25 @@ +# Binary HTTP Messages + +This is a rust implementation of [Binary HTTP +Messages](https://www.rfc-editor.org/rfc/rfc9292.html). + +## Using + +The API documentation is currently sparse, but the API is fairly small and +descriptive. + +The `bhttp` crate has the following features: + +- `read-bhttp` enables parsing of binary HTTP messages. This is enabled by + default. + +- `write-bhttp` enables writing of binary HTTP messages. This is enabled by + default. + +- `read-http` enables a simple HTTP/1.1 message parser. This parser is fairly + basic and is not recommended for production use. Getting an HTTP/1.1 parser + right is a massive enterprise; this one only does the basics. This is + disabled by default. + +- `write-http` enables writing of HTTP/1.1 messages. This is disabled by + default. diff --git a/third_party/rust/bhttp/src/err.rs b/third_party/rust/bhttp/src/err.rs new file mode 100644 index 0000000000..5ee136f960 --- /dev/null +++ b/third_party/rust/bhttp/src/err.rs @@ -0,0 +1,38 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("a request used the CONNECT method")] + ConnectUnsupported, + #[error("a field contained invalid Unicode: {0}")] + CharacterEncoding(#[from] std::string::FromUtf8Error), + #[error("a field contained an integer value that was out of range: {0}")] + IntRange(#[from] std::num::TryFromIntError), + #[error("the mode of the message was invalid")] + InvalidMode, + #[error("IO error {0}")] + Io(#[from] std::io::Error), + #[error("a field or line was missing a necessary character 0x{0:x}")] + Missing(u8), + #[error("a URL was missing a key component")] + MissingUrlComponent, + #[error("an obs-fold line was the first line of a field section")] + ObsFold, + #[error("a field contained a non-integer value: {0}")] + ParseInt(#[from] std::num::ParseIntError), + #[error("a field was truncated")] + Truncated, + #[error("a message included the Upgrade field")] + UpgradeUnsupported, + #[error("a URL could not be parsed into components: {0}")] + #[cfg(feature = "read-http")] + UrlParse(#[from] url::ParseError), +} + +#[cfg(any( + feature = "read-http", + feature = "write-http", + feature = "read-bhttp", + feature = "write-bhttp" +))] +pub type Res<T> = Result<T, Error>; diff --git a/third_party/rust/bhttp/src/lib.rs b/third_party/rust/bhttp/src/lib.rs new file mode 100644 index 0000000000..205cbf7202 --- /dev/null +++ b/third_party/rust/bhttp/src/lib.rs @@ -0,0 +1,797 @@ +#![deny(warnings, clippy::pedantic)] +#![allow(clippy::missing_errors_doc)] // Too lazy to document these. + +#[cfg(feature = "read-bhttp")] +use std::convert::TryFrom; +#[cfg(any( + feature = "read-http", + feature = "write-http", + feature = "read-bhttp", + feature = "write-bhttp" +))] +use std::io; +#[cfg(feature = "read-http")] +use url::Url; + +mod err; +mod parse; +#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))] +mod rw; + +pub use err::Error; +#[cfg(any( + feature = "read-http", + feature = "write-http", + feature = "read-bhttp", + feature = "write-bhttp" +))] +use err::Res; +#[cfg(feature = "read-http")] +use parse::{downcase, is_ows, read_line, split_at, COLON, SEMICOLON, SLASH, SP}; +use parse::{index_of, trim_ows, COMMA}; +#[cfg(feature = "read-bhttp")] +use rw::{read_varint, read_vec}; +#[cfg(feature = "write-bhttp")] +use rw::{write_len, write_varint, write_vec}; +#[cfg(any(feature = "read-http", feature = "read-bhttp",))] +use std::borrow::BorrowMut; + +#[cfg(feature = "read-http")] +const CONTENT_LENGTH: &[u8] = b"content-length"; +#[cfg(feature = "read-bhttp")] +const COOKIE: &[u8] = b"cookie"; +const TRANSFER_ENCODING: &[u8] = b"transfer-encoding"; +const CHUNKED: &[u8] = b"chunked"; + +pub type StatusCode = u16; + +pub trait ReadSeek: io::BufRead + io::Seek {} +impl<T> ReadSeek for io::Cursor<T> where T: AsRef<[u8]> {} +impl<T> ReadSeek for io::BufReader<T> where T: io::Read + io::Seek {} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))] +pub enum Mode { + KnownLength, + IndefiniteLength, +} + +pub struct Field { + name: Vec<u8>, + value: Vec<u8>, +} + +impl Field { + #[must_use] + pub fn new(name: Vec<u8>, value: Vec<u8>) -> Self { + Self { name, value } + } + + #[must_use] + pub fn name(&self) -> &[u8] { + &self.name + } + + #[must_use] + pub fn value(&self) -> &[u8] { + &self.value + } + + #[cfg(feature = "write-http")] + pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { + w.write_all(&self.name)?; + w.write_all(b": ")?; + w.write_all(&self.value)?; + w.write_all(b"\r\n")?; + Ok(()) + } + + #[cfg(feature = "write-bhttp")] + pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> { + write_vec(&self.name, w)?; + write_vec(&self.value, w)?; + Ok(()) + } + + #[cfg(feature = "read-http")] + pub fn obs_fold(&mut self, extra: &[u8]) { + self.value.push(SP); + self.value.extend(trim_ows(extra)); + } +} + +#[derive(Default)] +pub struct FieldSection(Vec<Field>); +impl FieldSection { + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Gets the value from the first instance of the field. + #[must_use] + pub fn get(&self, n: &[u8]) -> Option<&[u8]> { + for f in &self.0 { + if &f.name[..] == n { + return Some(&f.value); + } + } + None + } + + pub fn put(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) { + self.0.push(Field::new(name.into(), value.into())); + } + + pub fn iter(&self) -> impl Iterator<Item = &Field> { + self.0.iter() + } + + #[must_use] + pub fn fields(&self) -> &[Field] { + &self.0 + } + + #[must_use] + pub fn is_chunked(&self) -> bool { + // Look at the last symbol in Transfer-Encoding. + // This is very primitive decoding; structured field this is not. + if let Some(te) = self.get(TRANSFER_ENCODING) { + let mut slc = te; + while let Some(i) = index_of(COMMA, slc) { + slc = trim_ows(&slc[i + 1..]); + } + slc == CHUNKED + } else { + false + } + } + + /// As required by the HTTP specification, remove the Connection header + /// field, everything it refers to, and a few extra fields. + #[cfg(feature = "read-http")] + fn strip_connection_headers(&mut self) { + const CONNECTION: &[u8] = b"connection"; + const PROXY_CONNECTION: &[u8] = b"proxy-connection"; + const SHOULD_REMOVE: &[&[u8]] = &[ + CONNECTION, + PROXY_CONNECTION, + b"keep-alive", + b"te", + b"trailer", + b"transfer-encoding", + b"upgrade", + ]; + let mut listed = Vec::new(); + let mut track = |n| { + let mut name = Vec::from(trim_ows(n)); + downcase(&mut name); + if !listed.contains(&name) { + listed.push(name); + } + }; + + for f in self + .0 + .iter() + .filter(|f| f.name() == CONNECTION || f.name == PROXY_CONNECTION) + { + let mut v = f.value(); + while let Some(i) = index_of(COMMA, v) { + track(&v[..i]); + v = &v[i + 1..]; + } + track(v); + } + + self.0.retain(|f| { + !SHOULD_REMOVE.contains(&f.name()) && listed.iter().all(|x| &x[..] != f.name()) + }); + } + + #[cfg(feature = "read-http")] + fn parse_line(fields: &mut Vec<Field>, line: Vec<u8>) -> Res<()> { + // obs-fold is helpful in specs, so support it here too + let f = if is_ows(line[0]) { + let mut e = fields.pop().ok_or(Error::ObsFold)?; + e.obs_fold(&line); + e + } else if let Some((n, v)) = split_at(COLON, line) { + let mut name = Vec::from(trim_ows(&n)); + downcase(&mut name); + let value = Vec::from(trim_ows(&v)); + Field::new(name, value) + } else { + return Err(Error::Missing(COLON)); + }; + fields.push(f); + Ok(()) + } + + #[cfg(feature = "read-http")] + pub fn read_http<T, R>(r: &mut T) -> Res<Self> + where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, + { + let mut fields = Vec::new(); + loop { + let line = read_line(r)?; + if trim_ows(&line).is_empty() { + return Ok(Self(fields)); + } + Self::parse_line(&mut fields, line)?; + } + } + + #[cfg(feature = "read-bhttp")] + fn read_bhttp_fields<T, R>(terminator: bool, r: &mut T) -> Res<Vec<Field>> + where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, + { + let r = r.borrow_mut(); + let mut fields = Vec::new(); + let mut cookie_index: Option<usize> = None; + loop { + if let Some(n) = read_vec(r)? { + if n.is_empty() { + if terminator { + return Ok(fields); + } + return Err(Error::Truncated); + } + let mut v = read_vec(r)?.ok_or(Error::Truncated)?; + if n == COOKIE { + if let Some(i) = &cookie_index { + fields[*i].value.extend_from_slice(b"; "); + fields[*i].value.append(&mut v); + continue; + } + cookie_index = Some(fields.len()); + } + fields.push(Field::new(n, v)); + } else if terminator { + return Err(Error::Truncated); + } else { + return Ok(fields); + } + } + } + + #[cfg(feature = "read-bhttp")] + pub fn read_bhttp<T, R>(mode: Mode, r: &mut T) -> Res<Self> + where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, + { + let fields = if mode == Mode::KnownLength { + if let Some(buf) = read_vec(r)? { + Self::read_bhttp_fields(false, &mut io::Cursor::new(&buf[..]))? + } else { + Vec::new() + } + } else { + Self::read_bhttp_fields(true, r)? + }; + Ok(Self(fields)) + } + + #[cfg(feature = "write-bhttp")] + fn write_bhttp_headers(&self, w: &mut impl io::Write) -> Res<()> { + for f in &self.0 { + f.write_bhttp(w)?; + } + Ok(()) + } + + #[cfg(feature = "write-bhttp")] + pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { + if mode == Mode::KnownLength { + let mut buf = Vec::new(); + self.write_bhttp_headers(&mut buf)?; + write_vec(&buf, w)?; + } else { + self.write_bhttp_headers(w)?; + write_len(0, w)?; + } + Ok(()) + } + + #[cfg(feature = "write-http")] + pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { + for f in &self.0 { + f.write_http(w)?; + } + w.write_all(b"\r\n")?; + Ok(()) + } +} + +pub enum ControlData { + Request { + method: Vec<u8>, + scheme: Vec<u8>, + authority: Vec<u8>, + path: Vec<u8>, + }, + Response(StatusCode), +} + +impl ControlData { + #[must_use] + pub fn is_request(&self) -> bool { + matches!(self, Self::Request { .. }) + } + + #[must_use] + pub fn method(&self) -> Option<&[u8]> { + if let Self::Request { method, .. } = self { + Some(method) + } else { + None + } + } + + #[must_use] + pub fn scheme(&self) -> Option<&[u8]> { + if let Self::Request { scheme, .. } = self { + Some(scheme) + } else { + None + } + } + + #[must_use] + pub fn authority(&self) -> Option<&[u8]> { + if let Self::Request { authority, .. } = self { + if authority.is_empty() { + None + } else { + Some(authority) + } + } else { + None + } + } + + #[must_use] + pub fn path(&self) -> Option<&[u8]> { + if let Self::Request { path, .. } = self { + if path.is_empty() { + None + } else { + Some(path) + } + } else { + None + } + } + + #[must_use] + pub fn status(&self) -> Option<StatusCode> { + if let Self::Response(code) = self { + Some(*code) + } else { + None + } + } + + #[cfg(feature = "read-http")] + pub fn read_http(line: Vec<u8>) -> Res<Self> { + // request-line = method SP request-target SP HTTP-version + // status-line = HTTP-version SP status-code SP [reason-phrase] + let (a, r) = split_at(SP, line).ok_or(Error::Missing(SP))?; + let (b, _) = split_at(SP, r).ok_or(Error::Missing(SP))?; + if index_of(SLASH, &a).is_some() { + // Probably a response, so treat it as such. + let status_str = String::from_utf8(b)?; + let code = status_str.parse::<u16>()?; + Ok(Self::Response(code)) + } else if index_of(COLON, &b).is_some() { + // Now try to parse the URL. + let url_str = String::from_utf8(b)?; + let parsed = Url::parse(&url_str)?; + let authority = parsed.host_str().map_or_else(String::new, |host| { + let mut authority = String::from(host); + if let Some(port) = parsed.port() { + authority.push(':'); + authority.push_str(&port.to_string()); + } + authority + }); + let mut path = String::from(parsed.path()); + if let Some(q) = parsed.query() { + path.push('?'); + path.push_str(q); + } + Ok(Self::Request { + method: a, + scheme: Vec::from(parsed.scheme().as_bytes()), + authority: Vec::from(authority.as_bytes()), + path: Vec::from(path.as_bytes()), + }) + } else { + if a == b"CONNECT" { + return Err(Error::ConnectUnsupported); + } + Ok(Self::Request { + method: a, + scheme: Vec::from(&b"https"[..]), + authority: Vec::new(), + path: b, + }) + } + } + + #[cfg(feature = "read-bhttp")] + pub fn read_bhttp<T, R>(request: bool, r: &mut T) -> Res<Self> + where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, + { + let v = if request { + let method = read_vec(r)?.ok_or(Error::Truncated)?; + let scheme = read_vec(r)?.ok_or(Error::Truncated)?; + let authority = read_vec(r)?.ok_or(Error::Truncated)?; + let path = read_vec(r)?.ok_or(Error::Truncated)?; + Self::Request { + method, + scheme, + authority, + path, + } + } else { + Self::Response(u16::try_from(read_varint(r)?.ok_or(Error::Truncated)?)?) + }; + Ok(v) + } + + /// If this is an informational response. + #[cfg(any(feature = "read-bhttp", feature = "read-http"))] + #[must_use] + fn informational(&self) -> Option<StatusCode> { + match self { + Self::Response(v) if *v >= 100 && *v < 200 => Some(*v), + _ => None, + } + } + + #[cfg(feature = "write-bhttp")] + #[must_use] + fn code(&self, mode: Mode) -> u64 { + match (self, mode) { + (Self::Request { .. }, Mode::KnownLength) => 0, + (Self::Response(_), Mode::KnownLength) => 1, + (Self::Request { .. }, Mode::IndefiniteLength) => 2, + (Self::Response(_), Mode::IndefiniteLength) => 3, + } + } + + #[cfg(feature = "write-bhttp")] + pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> { + match self { + Self::Request { + method, + scheme, + authority, + path, + } => { + write_vec(method, w)?; + write_vec(scheme, w)?; + write_vec(authority, w)?; + write_vec(path, w)?; + } + Self::Response(status) => write_varint(*status, w)?, + } + Ok(()) + } + + #[cfg(feature = "write-http")] + pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { + match self { + Self::Request { + method, + scheme, + authority, + path, + } => { + w.write_all(method)?; + w.write_all(b" ")?; + if !authority.is_empty() { + w.write_all(scheme)?; + w.write_all(b"://")?; + w.write_all(authority)?; + } + w.write_all(path)?; + w.write_all(b" HTTP/1.1\r\n")?; + } + Self::Response(status) => { + let buf = format!("HTTP/1.1 {} Reason\r\n", *status); + w.write_all(buf.as_bytes())?; + } + } + Ok(()) + } +} + +pub struct InformationalResponse { + status: StatusCode, + fields: FieldSection, +} + +impl InformationalResponse { + #[must_use] + pub fn new(status: StatusCode, fields: FieldSection) -> Self { + Self { status, fields } + } + + #[must_use] + pub fn status(&self) -> StatusCode { + self.status + } + + #[must_use] + pub fn fields(&self) -> &FieldSection { + &self.fields + } + + #[cfg(feature = "write-bhttp")] + fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { + write_varint(self.status, w)?; + self.fields.write_bhttp(mode, w)?; + Ok(()) + } +} + +pub struct Message { + informational: Vec<InformationalResponse>, + control: ControlData, + header: FieldSection, + content: Vec<u8>, + trailer: FieldSection, +} + +impl Message { + #[must_use] + pub fn request(method: Vec<u8>, scheme: Vec<u8>, authority: Vec<u8>, path: Vec<u8>) -> Self { + Self { + informational: Vec::new(), + control: ControlData::Request { + method, + scheme, + authority, + path, + }, + header: FieldSection::default(), + content: Vec::new(), + trailer: FieldSection::default(), + } + } + + #[must_use] + pub fn response(status: StatusCode) -> Self { + Self { + informational: Vec::new(), + control: ControlData::Response(status), + header: FieldSection::default(), + content: Vec::new(), + trailer: FieldSection::default(), + } + } + + pub fn put_header(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) { + self.header.put(name, value); + } + + pub fn put_trailer(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) { + self.trailer.put(name, value); + } + + pub fn write_content(&mut self, d: impl AsRef<[u8]>) { + self.content.extend_from_slice(d.as_ref()); + } + + #[must_use] + pub fn informational(&self) -> &[InformationalResponse] { + &self.informational + } + + #[must_use] + pub fn control(&self) -> &ControlData { + &self.control + } + + #[must_use] + pub fn header(&self) -> &FieldSection { + &self.header + } + + #[must_use] + pub fn content(&self) -> &[u8] { + &self.content + } + + #[must_use] + pub fn trailer(&self) -> &FieldSection { + &self.trailer + } + + #[cfg(feature = "read-http")] + fn read_chunked<T, R>(r: &mut T) -> Res<Vec<u8>> + where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, + { + let mut content = Vec::new(); + loop { + let mut line = read_line(r)?; + if let Some(i) = index_of(SEMICOLON, &line) { + std::mem::drop(line.split_off(i)); + } + let count_str = String::from_utf8(line)?; + let count = usize::from_str_radix(&count_str, 16)?; + if count == 0 { + return Ok(content); + } + let mut buf = vec![0; count]; + r.borrow_mut().read_exact(&mut buf)?; + assert!(read_line(r)?.is_empty()); + content.append(&mut buf); + } + } + + #[cfg(feature = "read-http")] + #[allow(clippy::read_zero_byte_vec)] // https://github.com/rust-lang/rust-clippy/issues/9274 + pub fn read_http<T, R>(r: &mut T) -> Res<Self> + where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, + { + let line = read_line(r)?; + let mut control = ControlData::read_http(line)?; + let mut informational = Vec::new(); + while let Some(status) = control.informational() { + let fields = FieldSection::read_http(r)?; + informational.push(InformationalResponse::new(status, fields)); + let line = read_line(r)?; + control = ControlData::read_http(line)?; + } + + let mut header = FieldSection::read_http(r)?; + + let (content, trailer) = if matches!(control.status(), Some(204) | Some(304)) { + // 204 and 304 have no body, no matter what Content-Length says. + // Unfortunately, we can't do the same for responses to HEAD. + (Vec::new(), FieldSection::default()) + } else if header.is_chunked() { + let content = Self::read_chunked(r)?; + let trailer = FieldSection::read_http(r)?; + (content, trailer) + } else { + let mut content = Vec::new(); + if let Some(cl) = header.get(CONTENT_LENGTH) { + let cl_str = String::from_utf8(Vec::from(cl))?; + let cl_int = cl_str.parse::<usize>()?; + if cl_int > 0 { + content.resize(cl_int, 0); + r.borrow_mut().read_exact(&mut content)?; + } + } else { + // Note that for a request, the spec states that the content is + // empty, but this just reads all input like for a response. + r.borrow_mut().read_to_end(&mut content)?; + } + (content, FieldSection::default()) + }; + + header.strip_connection_headers(); + Ok(Self { + informational, + control, + header, + content, + trailer, + }) + } + + #[cfg(feature = "write-http")] + pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { + for info in &self.informational { + ControlData::Response(info.status()).write_http(w)?; + info.fields().write_http(w)?; + } + self.control.write_http(w)?; + if !self.content.is_empty() { + if self.trailer.is_empty() { + write!(w, "Content-Length: {}\r\n", self.content.len())?; + } else { + w.write_all(b"Transfer-Encoding: chunked\r\n")?; + } + } + self.header.write_http(w)?; + + if self.header.is_chunked() { + write!(w, "{:x}\r\n", self.content.len())?; + w.write_all(&self.content)?; + w.write_all(b"\r\n0\r\n")?; + self.trailer.write_http(w)?; + } else { + w.write_all(&self.content)?; + } + + Ok(()) + } + + /// Read a BHTTP message. + #[cfg(feature = "read-bhttp")] + pub fn read_bhttp<T, R>(r: &mut T) -> Res<Self> + where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, + { + let t = read_varint(r)?.ok_or(Error::Truncated)?; + let request = t == 0 || t == 2; + let mode = match t { + 0 | 1 => Mode::KnownLength, + 2 | 3 => Mode::IndefiniteLength, + _ => return Err(Error::InvalidMode), + }; + + let mut control = ControlData::read_bhttp(request, r)?; + let mut informational = Vec::new(); + while let Some(status) = control.informational() { + let fields = FieldSection::read_bhttp(mode, r)?; + informational.push(InformationalResponse::new(status, fields)); + control = ControlData::read_bhttp(request, r)?; + } + let header = FieldSection::read_bhttp(mode, r)?; + + let mut content = read_vec(r)?.unwrap_or_default(); + if mode == Mode::IndefiniteLength && !content.is_empty() { + loop { + let mut extra = read_vec(r)?.unwrap_or_default(); + if extra.is_empty() { + break; + } + content.append(&mut extra); + } + } + + let trailer = FieldSection::read_bhttp(mode, r)?; + + Ok(Self { + informational, + control, + header, + content, + trailer, + }) + } + + #[cfg(feature = "write-bhttp")] + pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { + write_varint(self.control.code(mode), w)?; + for info in &self.informational { + info.write_bhttp(mode, w)?; + } + self.control.write_bhttp(w)?; + self.header.write_bhttp(mode, w)?; + + write_vec(&self.content, w)?; + if mode == Mode::IndefiniteLength && !self.content.is_empty() { + write_len(0, w)?; + } + self.trailer.write_bhttp(mode, w)?; + Ok(()) + } +} + +#[cfg(feature = "write-http")] +impl std::fmt::Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let mut buf = Vec::new(); + self.write_http(&mut buf).map_err(|_| std::fmt::Error)?; + write!(f, "{:?}", String::from_utf8_lossy(&buf)) + } +} diff --git a/third_party/rust/bhttp/src/parse.rs b/third_party/rust/bhttp/src/parse.rs new file mode 100644 index 0000000000..42ba2b8b01 --- /dev/null +++ b/third_party/rust/bhttp/src/parse.rs @@ -0,0 +1,81 @@ +#[cfg(feature = "read-http")] +use crate::{Error, ReadSeek, Res}; +#[cfg(feature = "read-http")] +use std::borrow::BorrowMut; + +pub const HTAB: u8 = 0x09; +#[cfg(feature = "read-http")] +pub const NL: u8 = 0x0a; +#[cfg(feature = "read-http")] +pub const CR: u8 = 0x0d; +pub const SP: u8 = 0x20; +pub const COMMA: u8 = 0x2c; +#[cfg(feature = "read-http")] +pub const SLASH: u8 = 0x2f; +#[cfg(feature = "read-http")] +pub const COLON: u8 = 0x3a; +#[cfg(feature = "read-http")] +pub const SEMICOLON: u8 = 0x3b; + +pub fn is_ows(x: u8) -> bool { + x == SP || x == HTAB +} + +pub fn trim_ows(v: &[u8]) -> &[u8] { + for s in 0..v.len() { + if !is_ows(v[s]) { + for e in (s..v.len()).rev() { + if !is_ows(v[e]) { + return &v[s..=e]; + } + } + } + } + &v[..0] +} + +#[cfg(feature = "read-http")] +pub fn downcase(n: &mut [u8]) { + for i in n { + if *i >= 0x41 && *i <= 0x5a { + *i += 0x20; + } + } +} + +pub fn index_of(v: u8, line: &[u8]) -> Option<usize> { + for (i, x) in line.iter().enumerate() { + if *x == v { + return Some(i); + } + } + None +} + +#[cfg(feature = "read-http")] +pub fn split_at(v: u8, mut line: Vec<u8>) -> Option<(Vec<u8>, Vec<u8>)> { + index_of(v, &line).map(|i| { + let tail = line.split_off(i + 1); + let _ = line.pop(); + (line, tail) + }) +} + +#[cfg(feature = "read-http")] +pub fn read_line<T, R>(r: &mut T) -> Res<Vec<u8>> +where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, +{ + let mut buf = Vec::new(); + r.borrow_mut().read_until(NL, &mut buf)?; + let tail = buf.pop(); + if tail != Some(NL) { + return Err(Error::Truncated); + } + if buf.pop().ok_or(Error::Missing(CR))? == CR { + Ok(buf) + } else { + Err(Error::Missing(CR)) + } +} diff --git a/third_party/rust/bhttp/src/rw.rs b/third_party/rust/bhttp/src/rw.rs new file mode 100644 index 0000000000..8fee16daa9 --- /dev/null +++ b/third_party/rust/bhttp/src/rw.rs @@ -0,0 +1,106 @@ +use crate::err::Res; +#[cfg(feature = "read-bhttp")] +use crate::{err::Error, ReadSeek}; +#[cfg(feature = "read-bhttp")] +use std::borrow::BorrowMut; +use std::{convert::TryFrom, io}; + +#[cfg(feature = "write-bhttp")] +#[allow(clippy::cast_possible_truncation)] +fn write_uint(n: u8, v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> { + let v = v.into(); + assert!(n > 0 && usize::from(n) < std::mem::size_of::<u64>()); + for i in 0..n { + w.write_all(&[((v >> (8 * (n - i - 1))) & 0xff) as u8])?; + } + Ok(()) +} + +#[cfg(feature = "write-bhttp")] +pub fn write_varint(v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> { + let v = v.into(); + match () { + _ if v < (1 << 6) => write_uint(1, v, w), + _ if v < (1 << 14) => write_uint(2, v | (1 << 14), w), + _ if v < (1 << 30) => write_uint(4, v | (2 << 30), w), + _ if v < (1 << 62) => write_uint(8, v | (3 << 62), w), + _ => panic!("Varint value too large"), + } +} + +#[cfg(feature = "write-bhttp")] +pub fn write_len(len: usize, w: &mut impl io::Write) -> Res<()> { + write_varint(u64::try_from(len).unwrap(), w) +} + +#[cfg(feature = "write-bhttp")] +pub fn write_vec(v: &[u8], w: &mut impl io::Write) -> Res<()> { + write_len(v.len(), w)?; + w.write_all(v)?; + Ok(()) +} + +#[cfg(feature = "read-bhttp")] +fn read_uint<T, R>(n: usize, r: &mut T) -> Res<Option<u64>> +where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, +{ + let mut buf = [0; 7]; + let count = r.borrow_mut().read(&mut buf[..n])?; + if count == 0 { + return Ok(None); + } else if count < n { + return Err(Error::Truncated); + } + let mut v = 0; + for i in &buf[..n] { + v = (v << 8) | u64::from(*i); + } + Ok(Some(v)) +} + +#[cfg(feature = "read-bhttp")] +pub fn read_varint<T, R>(r: &mut T) -> Res<Option<u64>> +where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, +{ + if let Some(b1) = read_uint(1, r)? { + Ok(Some(match b1 >> 6 { + 0 => b1 & 0x3f, + 1 => ((b1 & 0x3f) << 8) | read_uint(1, r)?.ok_or(Error::Truncated)?, + 2 => ((b1 & 0x3f) << 24) | read_uint(3, r)?.ok_or(Error::Truncated)?, + 3 => ((b1 & 0x3f) << 56) | read_uint(7, r)?.ok_or(Error::Truncated)?, + _ => unreachable!(), + })) + } else { + Ok(None) + } +} + +#[cfg(feature = "read-bhttp")] +pub fn read_vec<T, R>(r: &mut T) -> Res<Option<Vec<u8>>> +where + T: BorrowMut<R> + ?Sized, + R: ReadSeek + ?Sized, +{ + use std::io::SeekFrom; + + if let Some(len) = read_varint(r)? { + // Check that the input contains enough data. Before allocating. + let r = r.borrow_mut(); + let pos = r.stream_position()?; + let end = r.seek(SeekFrom::End(0))?; + if end - pos < len { + return Err(Error::Truncated); + } + let _ = r.seek(SeekFrom::Start(pos))?; + + let mut v = vec![0; usize::try_from(len)?]; + r.read_exact(&mut v)?; + Ok(Some(v)) + } else { + Ok(None) + } +} diff --git a/third_party/rust/bhttp/tests/test.rs b/third_party/rust/bhttp/tests/test.rs new file mode 100644 index 0000000000..024df8d6d9 --- /dev/null +++ b/third_party/rust/bhttp/tests/test.rs @@ -0,0 +1,225 @@ +// Rather than grapple with #[cfg(...)] for every variable and import. +#![cfg(all(feature = "http", feature = "bhttp"))] + +use bhttp::{Error, Message, Mode}; +use std::{io::Cursor, mem::drop}; + +const CHUNKED_HTTP: &[u8] = b"HTTP/1.1 200 OK\r\n\ + Transfer-Encoding: camel, chunked\r\n\ + \r\n\ + 4\r\n\ + This\r\n\ + 6\r\n \ + conte\r\n\ + 13;chunk-extension=foo\r\n\ + nt contains CRLF.\r\n\ + \r\n\ + 0\r\n\ + Trailer: text\r\n\ + \r\n"; +const TRANSFER_ENCODING: &[u8] = b"transfer-encoding"; +const CHUNKED_KNOWN: &[u8] = &[ + 0x01, 0x40, 0xc8, 0x00, 0x1d, 0x54, 0x68, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x20, 0x43, 0x52, 0x4c, 0x46, 0x2e, + 0x0d, 0x0a, 0x0d, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x04, 0x74, 0x65, 0x78, 0x74, +]; +const CHUNKED_INDEFINITE: &[u8] = &[ + 0x03, 0x40, 0xc8, 0x00, 0x1d, 0x54, 0x68, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x20, 0x43, 0x52, 0x4c, 0x46, 0x2e, + 0x0d, 0x0a, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x04, 0x74, 0x65, 0x78, 0x74, + 0x00, +]; + +const REQUEST: &[u8] = b"GET /hello.txt HTTP/1.1\r\n\ + user-agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3\r\n\ + host: www.example.com\r\n\ + accept-language: en, mi\r\n\ + \r\n"; +const REQUEST_KNOWN: &[u8] = &[ + 0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x00, 0x0a, 0x2f, 0x68, 0x65, + 0x6c, 0x6c, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x40, 0x6c, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x34, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, 0x36, 0x2e, 0x33, + 0x20, 0x6c, 0x69, 0x62, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, 0x36, 0x2e, 0x33, 0x20, + 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x53, 0x4c, 0x2f, 0x30, 0x2e, 0x39, 0x2e, 0x37, 0x6c, 0x20, 0x7a, + 0x6c, 0x69, 0x62, 0x2f, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x0f, 0x77, + 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x0f, 0x61, + 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x06, 0x65, + 0x6e, 0x2c, 0x20, 0x6d, 0x69, 0x00, 0x00, +]; + +#[test] +fn chunked_read() { + drop(Message::read_http(&mut Cursor::new(CHUNKED_HTTP)).unwrap()); +} + +#[test] +fn chunked_read_known() { + drop(Message::read_bhttp(&mut Cursor::new(CHUNKED_KNOWN)).unwrap()); +} + +#[test] +fn chunked_read_indefinite() { + drop(Message::read_bhttp(&mut Cursor::new(CHUNKED_INDEFINITE)).unwrap()); +} + +#[test] +fn chunked_to_known() { + let m = Message::read_http(&mut Cursor::new(CHUNKED_HTTP)).unwrap(); + assert!(m.header().get(TRANSFER_ENCODING).is_none()); + + let mut buf = Vec::new(); + m.write_bhttp(Mode::KnownLength, &mut buf).unwrap(); + println!("result: {}", hex::encode(&buf)); + assert_eq!(&buf[..], CHUNKED_KNOWN); +} + +#[test] +fn chunked_to_indefinite() { + let m = Message::read_http(&mut Cursor::new(CHUNKED_HTTP)).unwrap(); + assert!(m.header().get(TRANSFER_ENCODING).is_none()); + + let mut buf = Vec::new(); + m.write_bhttp(Mode::IndefiniteLength, &mut buf).unwrap(); + println!("result: {}", hex::encode(&buf)); + assert_eq!(&buf[..], CHUNKED_INDEFINITE); +} + +#[test] +fn convert_request() { + let m = Message::read_http(&mut Cursor::new(REQUEST)).unwrap(); + let mut buf = Vec::new(); + m.write_bhttp(Mode::KnownLength, &mut buf).unwrap(); + println!("result: {}", hex::encode(&buf)); + assert_eq!(&buf[..], REQUEST_KNOWN); +} + +#[test] +fn padded_to_http() { + let mut padded = Vec::from(REQUEST_KNOWN); + padded.resize(padded.len() + 100, 0); + let m = Message::read_bhttp(&mut Cursor::new(&padded[..])).unwrap(); + let mut buf = Vec::new(); + m.write_http(&mut buf).unwrap(); + assert_eq!(&buf[..], REQUEST); +} + +#[test] +fn truncated_to_http() { + let mut padded = Vec::from(REQUEST_KNOWN); + assert_eq!(2, padded.iter().rev().take_while(|&x| *x == 0).count()); + padded.truncate(padded.len() - 2); + + let m = Message::read_bhttp(&mut Cursor::new(&padded[..])).unwrap(); + let mut buf = Vec::new(); + m.write_http(&mut buf).unwrap(); + assert_eq!(&buf[..], REQUEST); +} + +#[test] +fn tiny_request() { + const REQUEST: &[u8] = &[ + 0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x0b, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x01, 0x2f, + ]; + let m = Message::read_bhttp(&mut Cursor::new(REQUEST)).unwrap(); + assert_eq!(m.control().method().unwrap(), b"GET"); + assert_eq!(m.control().scheme().unwrap(), b"https"); + assert_eq!(m.control().authority().unwrap(), b"example.com"); + assert_eq!(m.control().path().unwrap(), b"/"); + assert!(m.control().status().is_none()); + assert!(m.header().is_empty()); + assert!(m.content().is_empty()); + assert!(m.trailer().is_empty()); +} + +#[test] +fn tiny_response() { + const RESPONSE: &[u8] = &[0x01, 0x40, 0xc8]; + let m = Message::read_bhttp(&mut Cursor::new(RESPONSE)).unwrap(); + assert!(m.informational().is_empty()); + assert_eq!(m.control().status().unwrap(), 200); + assert!(m.control().method().is_none()); + assert!(m.control().scheme().is_none()); + assert!(m.control().authority().is_none()); + assert!(m.control().path().is_none()); + assert!(m.header().is_empty()); + assert!(m.content().is_empty()); + assert!(m.trailer().is_empty()); +} + +#[test] +fn connect_request() { + const REQUEST: &[u8] = b"CONNECT test.example HTTP/1.1\r\n\ + Host: example.com\r\n\ + \r\n"; + let err = Message::read_http(&mut Cursor::new(REQUEST)).unwrap_err(); + assert!(matches!(err, Error::ConnectUnsupported)); +} + +/// Verify that Connection and Proxy-Connection are stripped out properly. +#[test] +fn connection_header() { + const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\ + Host: example.com\r\n\ + other: test\r\n\ + Connection: sample\r\n\ + Connection: other, garbage\r\n\ + sample: test2\r\n\ + px: test3\r\n\ + proXy-connection: px\r\n\ + \r\n"; + + let m = Message::read_http(&mut Cursor::new(REQUEST)).unwrap(); + assert!(m.header().get(b"other").is_none()); + assert!(m.header().get(b"sample").is_none()); + assert!(m.header().get(b"garbage").is_none()); + assert!(m.header().get(b"connection").is_none()); + assert!(m.header().get(b"proxy-connection").is_none()); + assert!(m.header().get(b"px").is_none()); +} + +/// Verify that hop-by-hop headers (other than transfer-encoding) are stripped out properly. +#[test] +fn hop_by_hop() { + const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\ + Host: example.com\r\n\ + keep-alive: 1\r\n\ + te: trailers\r\n\ + trailer: te\r\n\ + upgrade: h2c\r\n\ + \r\n"; + + let m = Message::read_http(&mut Cursor::new(REQUEST)).unwrap(); + assert!(m.header().get(b"keep-alive").is_none()); + assert!(m.header().get(b"te").is_none()); + assert!(m.header().get(b"trailer").is_none()); + assert!(m.header().get(b"transfer-encoding").is_none()); + assert!(m.header().get(b"upgrade").is_none()); +} + +/// Verify that very bad chunked encoding produces a result. +#[test] +fn bad_chunked() { + const REQUEST: &[u8] = b"POST test.example HTTP/1.1\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n"; + + let e = Message::read_http(&mut Cursor::new(REQUEST)).unwrap_err(); + assert!(matches!(e, Error::Truncated)); +} + +/// If a length field overruns the buffer, stop. +#[test] +fn oversized() { + const REQUEST: &[u8] = &[0x00, 0x01]; + let e = Message::read_bhttp(&mut Cursor::new(REQUEST)).unwrap_err(); + assert!(matches!(e, Error::Truncated)); +} + +/// If a length field overruns the buffer, stop before over-allocating. +#[test] +fn oversized_max() { + const REQUEST: &[u8] = &[0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; + let e = Message::read_bhttp(&mut Cursor::new(REQUEST)).unwrap_err(); + assert!(matches!(e, Error::Truncated)); +} |