diff --git a/Cargo.toml b/Cargo.toml index 64eb495..516052a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "ohttp-client", "ohttp-client-cli", "ohttp-server", + "sync-async", ] [workspace.package] @@ -17,7 +18,7 @@ description = "Oblivious HTTP" keywords = ["ohttp", "http", "bhttp", "ietf"] categories = ["network-programming", "web-programming", "security"] readme = "README.md" -version = "0.6.1" +version = "0.7.0" edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.82.0" diff --git a/README.md b/README.md index 4be3eef..6f15330 100644 --- a/README.md +++ b/README.md @@ -17,19 +17,11 @@ descriptive. The `bhttp` crate has the following features: -- `read-bhttp` enables parsing of binary HTTP messages. This is enabled by - default. +- `http` enables parsing and generation of binary HTTP messages. + This is disabled 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. +- `stream` enables stream processing (presently just reading) + of binary HTTP messages. This is disabled by default until it stabilizes. The `ohttp` crate has the following features: @@ -47,6 +39,10 @@ The `ohttp` crate has the following features: [NSS](https://firefox-source-docs.mozilla.org/security/nss/index.html). This is disabled by default and cannot be enabled at the same time as `rust-hpke`. +- `stream` enables stream processing (presently just reading) + of [chunked Oblivious HTTP messages](https://datatracker.ietf.org/doc/html/draft-ietf-ohai-chunked-ohttp). + This is disabled by default until it stabilizes. + ## Utilities @@ -128,16 +124,15 @@ export NSS_DIR=$workspace/nss export LD_LIBRARY_PATH=$workspace/dist/Debug/lib ``` -You might need to tweak this. On a Mac, use `DYLD_LIBRARY_PATH` instead of -`LD_LIBRARY_PATH`. And if you are building with `--release`, the path includes -"Release" rather than "Debug". +On a Mac, use `DYLD_LIBRARY_PATH` instead of `LD_LIBRARY_PATH`. +If you are building with `--release`, the path includes "Release" rather than "Debug". Then you should be able to build and run tests: ```sh cd $workspace -cargo build -cargo test +cargo build -F nss,client,server,http --no-default-features +cargo test -F nss,client,server,http --no-default-features ``` diff --git a/bhttp-convert/Cargo.toml b/bhttp-convert/Cargo.toml index 58724d8..96e4c57 100644 --- a/bhttp-convert/Cargo.toml +++ b/bhttp-convert/Cargo.toml @@ -9,4 +9,4 @@ structopt = "0.3" [dependencies.bhttp] path= "../bhttp" -features = ["bhttp", "http"] +features = ["http"] diff --git a/bhttp-convert/src/main.rs b/bhttp-convert/src/main.rs index 763fadf..054c64f 100644 --- a/bhttp-convert/src/main.rs +++ b/bhttp-convert/src/main.rs @@ -1,11 +1,12 @@ #![deny(warnings, clippy::pedantic)] -use bhttp::{Message, Mode}; use std::{ fs::File, io::{self, Read}, path::PathBuf, }; + +use bhttp::{Message, Mode}; use structopt::StructOpt; #[derive(Debug, StructOpt)] diff --git a/bhttp/Cargo.toml b/bhttp/Cargo.toml index f2d71b4..eb67e07 100644 --- a/bhttp/Cargo.toml +++ b/bhttp/Cargo.toml @@ -13,17 +13,18 @@ categories.workspace = true readme.workspace = true [features] -default = ["bhttp"] -bhttp = ["read-bhttp", "write-bhttp"] -http = ["read-http", "write-http"] -read-bhttp = [] -write-bhttp = [] -read-http = ["url"] -write-http = [] +default = [] +http = ["dep:url"] +stream = ["dep:futures", "dep:pin-project"] [dependencies] +futures = {version = "0.3", optional = true} +pin-project = {version = "1.1", optional = true} thiserror = "1" url = {version = "2", optional = true} [dev-dependencies] hex = "0.4" + +[dev-dependencies.sync-async] +path= "../sync-async" diff --git a/bhttp/src/err.rs b/bhttp/src/err.rs index d9d9b6f..45d6fab 100644 --- a/bhttp/src/err.rs +++ b/bhttp/src/err.rs @@ -1,6 +1,4 @@ -use thiserror::Error; - -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum Error { #[error("a request used the CONNECT method")] ConnectUnsupported, @@ -16,8 +14,14 @@ pub enum Error { InvalidMode, #[error("the status code of a response needs to be in 100..=599")] InvalidStatus, + #[cfg(feature = "stream")] + #[error("a method was called when the message was in the wrong state")] + InvalidState, #[error("IO error {0}")] Io(#[from] std::io::Error), + #[cfg(feature = "stream")] + #[error("the size of a vector exceeded the limit that was set")] + LimitExceeded, #[error("a field or line was missing a necessary character 0x{0:x}")] Missing(u8), #[error("a URL was missing a key component")] @@ -31,14 +35,8 @@ pub enum Error { #[error("a message included the Upgrade field")] UpgradeUnsupported, #[error("a URL could not be parsed into components: {0}")] - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] UrlParse(#[from] url::ParseError), } -#[cfg(any( - feature = "read-http", - feature = "write-http", - feature = "read-bhttp", - feature = "write-bhttp" -))] pub type Res = Result; diff --git a/bhttp/src/lib.rs b/bhttp/src/lib.rs index 3c8fbde..caa6b37 100644 --- a/bhttp/src/lib.rs +++ b/bhttp/src/lib.rs @@ -1,51 +1,36 @@ #![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 std::{ + borrow::BorrowMut, + io, + ops::{Deref, DerefMut}, +}; + +#[cfg(feature = "http")] use url::Url; mod err; mod parse; -#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))] mod rw; - -#[cfg(any(feature = "read-http", feature = "read-bhttp",))] -use std::borrow::BorrowMut; +#[cfg(feature = "stream")] +pub mod stream; 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")] +#[cfg(feature = "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}; +use rw::{read_varint, read_vec, write_len, write_varint, write_vec}; -#[cfg(feature = "read-http")] +#[cfg(feature = "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"; -#[derive(Clone, Copy, PartialEq, Eq)] +/// An HTTP status code. +#[derive(Clone, Copy, Debug)] pub struct StatusCode(u16); impl StatusCode { @@ -88,17 +73,49 @@ impl From for u16 { } } +#[cfg(test)] +impl PartialEq for StatusCode +where + Self: TryFrom, + T: Copy, +{ + fn eq(&self, other: &T) -> bool { + StatusCode::try_from(*other).is_ok_and(|o| o.0 == self.0) + } +} + +#[cfg(not(test))] +impl PartialEq for StatusCode { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for StatusCode {} + pub trait ReadSeek: io::BufRead + io::Seek {} impl ReadSeek for io::Cursor where T: AsRef<[u8]> {} impl ReadSeek for io::BufReader where T: io::Read + io::Seek {} +/// The encoding mode of a binary HTTP message. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))] pub enum Mode { KnownLength, IndeterminateLength, } +impl TryFrom for Mode { + type Error = Error; + fn try_from(t: u64) -> Result { + match t { + 0 | 1 => Ok(Self::KnownLength), + 2 | 3 => Ok(Self::IndeterminateLength), + _ => Err(Error::InvalidMode), + } + } +} + +/// A single HTTP field. pub struct Field { name: Vec, value: Vec, @@ -120,7 +137,7 @@ impl Field { &self.value } - #[cfg(feature = "write-http")] + #[cfg(feature = "http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { w.write_all(&self.name)?; w.write_all(b": ")?; @@ -129,37 +146,64 @@ impl Field { 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")] + #[cfg(feature = "http")] pub fn obs_fold(&mut self, extra: &[u8]) { self.value.push(SP); self.value.extend(trim_ows(extra)); } } +#[cfg(test)] +impl std::fmt::Debug for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{n}: {v}", + n = String::from_utf8_lossy(&self.name), + v = String::from_utf8_lossy(&self.value), + ) + } +} + +/// A field section (headers or trailers). #[derive(Default)] pub struct FieldSection(Vec); + impl FieldSection { #[must_use] pub fn is_empty(&self) -> bool { self.0.is_empty() } + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + /// 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 { + self.get_all(n).next() + } + + /// Gets all of the values of the named field. + pub fn get_all<'a, 'b>(&'a self, n: &'b [u8]) -> impl Iterator + 'b + where + 'a: 'b, + { + self.0.iter().filter_map(move |f| { if &f.name[..] == n { - return Some(&f.value); + Some(&f.value[..]) + } else { + None } - } - None + }) } pub fn put(&mut self, name: impl Into>, value: impl Into>) { @@ -192,7 +236,7 @@ impl FieldSection { /// As required by the HTTP specification, remove the Connection header /// field, everything it refers to, and a few extra fields. - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] fn strip_connection_headers(&mut self) { const CONNECTION: &[u8] = b"connection"; const PROXY_CONNECTION: &[u8] = b"proxy-connection"; @@ -232,7 +276,7 @@ impl FieldSection { }); } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] fn parse_line(fields: &mut Vec, line: Vec) -> Res<()> { // obs-fold is helpful in specs, so support it here too let f = if is_ows(line[0]) { @@ -251,7 +295,7 @@ impl FieldSection { Ok(()) } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] pub fn read_http(r: &mut T) -> Res where T: BorrowMut + ?Sized, @@ -267,7 +311,6 @@ impl FieldSection { } } - #[cfg(feature = "read-bhttp")] fn read_bhttp_fields(terminator: bool, r: &mut T) -> Res> where T: BorrowMut + ?Sized, @@ -302,7 +345,6 @@ impl FieldSection { } } - #[cfg(feature = "read-bhttp")] pub fn read_bhttp(mode: Mode, r: &mut T) -> Res where T: BorrowMut + ?Sized, @@ -320,7 +362,6 @@ impl FieldSection { 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)?; @@ -328,7 +369,6 @@ impl FieldSection { 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(); @@ -341,7 +381,7 @@ impl FieldSection { Ok(()) } - #[cfg(feature = "write-http")] + #[cfg(feature = "http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { for f in &self.0 { f.write_http(w)?; @@ -351,6 +391,17 @@ impl FieldSection { } } +#[cfg(test)] +impl std::fmt::Debug for FieldSection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + for fv in self.fields() { + fv.fmt(f)?; + } + Ok(()) + } +} + +/// Control data for an HTTP message, either request or response. pub enum ControlData { Request { method: Vec, @@ -420,7 +471,7 @@ impl ControlData { } } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] pub fn read_http(line: Vec) -> Res { // request-line = method SP request-target SP HTTP-version // status-line = HTTP-version SP status-code SP [reason-phrase] @@ -467,7 +518,6 @@ impl ControlData { } } - #[cfg(feature = "read-bhttp")] pub fn read_bhttp(request: bool, r: &mut T) -> Res where T: BorrowMut + ?Sized, @@ -493,7 +543,6 @@ impl ControlData { } /// If this is an informational response. - #[cfg(any(feature = "read-bhttp", feature = "read-http"))] #[must_use] fn informational(&self) -> Option { match self { @@ -502,7 +551,6 @@ impl ControlData { } } - #[cfg(feature = "write-bhttp")] #[must_use] fn code(&self, mode: Mode) -> u64 { match (self, mode) { @@ -513,7 +561,6 @@ impl ControlData { } } - #[cfg(feature = "write-bhttp")] pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> { match self { Self::Request { @@ -532,7 +579,7 @@ impl ControlData { Ok(()) } - #[cfg(feature = "write-http")] + #[cfg(feature = "http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { match self { Self::Request { @@ -560,6 +607,69 @@ impl ControlData { } } +#[cfg(test)] +impl PartialEq<(M, S, A, P)> for ControlData +where + M: AsRef<[u8]>, + S: AsRef<[u8]>, + A: AsRef<[u8]>, + P: AsRef<[u8]>, +{ + fn eq(&self, other: &(M, S, A, P)) -> bool { + match self { + Self::Request { + method, + scheme, + authority, + path, + } => { + method == other.0.as_ref() + && scheme == other.1.as_ref() + && authority == other.2.as_ref() + && path == other.3.as_ref() + } + Self::Response(_) => false, + } + } +} + +#[cfg(test)] +impl PartialEq for ControlData +where + StatusCode: TryFrom, + T: Copy, +{ + fn eq(&self, other: &T) -> bool { + match self { + Self::Request { .. } => false, + Self::Response(code) => code == other, + } + } +} + +#[cfg(test)] +impl std::fmt::Debug for ControlData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Self::Request { + method, + scheme, + authority, + path, + } => write!( + f, + "{m} {s}://{a}{p}", + m = String::from_utf8_lossy(method), + s = String::from_utf8_lossy(scheme), + a = String::from_utf8_lossy(authority), + p = String::from_utf8_lossy(path), + ), + Self::Response(code) => write!(f, "{code:?}"), + } + } +} + +/// An informational status code and the associated header fields. pub struct InformationalResponse { status: StatusCode, fields: FieldSection, @@ -581,7 +691,6 @@ impl InformationalResponse { &self.fields } - #[cfg(feature = "write-bhttp")] fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { write_varint(self.status.code(), w)?; self.fields.write_bhttp(mode, w)?; @@ -589,80 +698,152 @@ impl InformationalResponse { } } +impl Deref for InformationalResponse { + type Target = FieldSection; + + fn deref(&self) -> &Self::Target { + &self.fields + } +} + +impl DerefMut for InformationalResponse { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.fields + } +} + +/// A header block, including control data and headers. +pub struct Header { + control: ControlData, + fields: FieldSection, +} + +impl Header { + #[must_use] + pub fn control(&self) -> &ControlData { + &self.control + } +} + +impl From for Header { + fn from(control: ControlData) -> Self { + Self { + control, + fields: FieldSection::default(), + } + } +} + +impl From<(ControlData, FieldSection)> for Header { + fn from((control, fields): (ControlData, FieldSection)) -> Self { + Self { control, fields } + } +} + +impl std::ops::Deref for Header { + type Target = FieldSection; + fn deref(&self) -> &Self::Target { + &self.fields + } +} + +impl std::ops::DerefMut for Header { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.fields + } +} + +#[cfg(test)] +impl std::fmt::Debug for Header { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + self.control.fmt(f)?; + self.fields.fmt(f) + } +} + +/// An HTTP message, either request or response, +/// including any optional informational responses on a response. pub struct Message { informational: Vec, - control: ControlData, - header: FieldSection, + header: Header, content: Vec, trailer: FieldSection, } impl Message { + /// Construct a minimal request message. #[must_use] pub fn request(method: Vec, scheme: Vec, authority: Vec, path: Vec) -> Self { Self { informational: Vec::new(), - control: ControlData::Request { + header: Header::from(ControlData::Request { method, scheme, authority, path, - }, - header: FieldSection::default(), + }), content: Vec::new(), trailer: FieldSection::default(), } } + /// Construct a minimal response message. #[must_use] pub fn response(status: StatusCode) -> Self { Self { informational: Vec::new(), - control: ControlData::Response(status), - header: FieldSection::default(), + header: Header::from(ControlData::Response(status)), content: Vec::new(), trailer: FieldSection::default(), } } + /// Set a header field value. pub fn put_header(&mut self, name: impl Into>, value: impl Into>) { self.header.put(name, value); } + /// Set a trailer field value. pub fn put_trailer(&mut self, name: impl Into>, value: impl Into>) { self.trailer.put(name, value); } + /// Extend the content of the message with the given bytes. pub fn write_content(&mut self, d: impl AsRef<[u8]>) { self.content.extend_from_slice(d.as_ref()); } + /// Access informational status responses. #[must_use] pub fn informational(&self) -> &[InformationalResponse] { &self.informational } + /// Access control data. #[must_use] pub fn control(&self) -> &ControlData { - &self.control + self.header.control() } + /// Get the header. #[must_use] - pub fn header(&self) -> &FieldSection { + pub fn header(&self) -> &Header { &self.header } + /// Get the content of the message. #[must_use] pub fn content(&self) -> &[u8] { &self.content } + /// Get the trailer fields. #[must_use] pub fn trailer(&self) -> &FieldSection { &self.trailer } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] fn read_chunked(r: &mut T) -> Res> where T: BorrowMut + ?Sized, @@ -686,7 +867,8 @@ impl Message { } } - #[cfg(feature = "read-http")] + /// Read an HTTP/1.1 message. + #[cfg(feature = "http")] #[allow(clippy::read_zero_byte_vec)] // https://github.com/rust-lang/rust-clippy/issues/9274 pub fn read_http(r: &mut T) -> Res where @@ -703,20 +885,20 @@ impl Message { control = ControlData::read_http(line)?; } - let mut header = FieldSection::read_http(r)?; + let mut hfields = FieldSection::read_http(r)?; let (content, trailer) = if matches!(control.status().map(StatusCode::code), Some(204 | 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() { + } else if hfields.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) { + if let Some(cl) = hfields.get(CONTENT_LENGTH) { let cl_str = String::from_utf8(Vec::from(cl))?; let cl_int = cl_str.parse::()?; if cl_int > 0 { @@ -731,23 +913,23 @@ impl Message { (content, FieldSection::default()) }; - header.strip_connection_headers(); + hfields.strip_connection_headers(); Ok(Self { informational, - control, - header, + header: Header::from((control, hfields)), content, trailer, }) } - #[cfg(feature = "write-http")] + /// Write out an HTTP/1.1 message. + #[cfg(feature = "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)?; + self.header.control.write_http(w)?; if !self.content.is_empty() { if self.trailer.is_empty() { write!(w, "Content-Length: {}\r\n", self.content.len())?; @@ -770,7 +952,6 @@ impl Message { } /// Read a BHTTP message. - #[cfg(feature = "read-bhttp")] pub fn read_bhttp(r: &mut T) -> Res where T: BorrowMut + ?Sized, @@ -778,11 +959,7 @@ impl Message { { 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::IndeterminateLength, - _ => return Err(Error::InvalidMode), - }; + let mode = Mode::try_from(t)?; let mut control = ControlData::read_bhttp(request, r)?; let mut informational = Vec::new(); @@ -791,7 +968,7 @@ impl Message { informational.push(InformationalResponse::new(status, fields)); control = ControlData::read_bhttp(request, r)?; } - let header = FieldSection::read_bhttp(mode, r)?; + let hfields = FieldSection::read_bhttp(mode, r)?; let mut content = read_vec(r)?.unwrap_or_default(); if mode == Mode::IndeterminateLength && !content.is_empty() { @@ -808,20 +985,19 @@ impl Message { Ok(Self { informational, - control, - header, + header: Header::from((control, hfields)), content, trailer, }) } - #[cfg(feature = "write-bhttp")] + /// Write a BHTTP message. pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { - write_varint(self.control.code(mode), w)?; + write_varint(self.header.control.code(mode), w)?; for info in &self.informational { info.write_bhttp(mode, w)?; } - self.control.write_bhttp(w)?; + self.header.control.write_bhttp(w)?; self.header.write_bhttp(mode, w)?; write_vec(&self.content, w)?; @@ -833,7 +1009,7 @@ impl Message { } } -#[cfg(feature = "write-http")] +#[cfg(feature = "http")] impl std::fmt::Debug for Message { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { let mut buf = Vec::new(); diff --git a/bhttp/src/parse.rs b/bhttp/src/parse.rs index ee52493..06165fc 100644 --- a/bhttp/src/parse.rs +++ b/bhttp/src/parse.rs @@ -1,20 +1,21 @@ -#[cfg(feature = "read-http")] -use crate::{Error, ReadSeek, Res}; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] use std::borrow::BorrowMut; +#[cfg(feature = "http")] +use crate::{Error, ReadSeek, Res}; + pub const HTAB: u8 = 0x09; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const NL: u8 = 0x0a; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const CR: u8 = 0x0d; pub const SP: u8 = 0x20; pub const COMMA: u8 = 0x2c; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const SLASH: u8 = 0x2f; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const COLON: u8 = 0x3a; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const SEMICOLON: u8 = 0x3b; pub fn is_ows(x: u8) -> bool { @@ -34,7 +35,7 @@ pub fn trim_ows(v: &[u8]) -> &[u8] { &v[..0] } -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub fn downcase(n: &mut [u8]) { for i in n { if *i >= 0x41 && *i <= 0x5a { @@ -52,7 +53,7 @@ pub fn index_of(v: u8, line: &[u8]) -> Option { None } -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub fn split_at(v: u8, mut line: Vec) -> Option<(Vec, Vec)> { index_of(v, &line).map(|i| { let tail = line.split_off(i + 1); @@ -61,7 +62,7 @@ pub fn split_at(v: u8, mut line: Vec) -> Option<(Vec, Vec)> { }) } -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub fn read_line(r: &mut T) -> Res> where T: BorrowMut + ?Sized, diff --git a/bhttp/src/rw.rs b/bhttp/src/rw.rs index 861bce6..0520356 100644 --- a/bhttp/src/rw.rs +++ b/bhttp/src/rw.rs @@ -1,12 +1,10 @@ -#[cfg(feature = "read-bhttp")] -use std::borrow::BorrowMut; -use std::{convert::TryFrom, io}; +use std::{borrow::BorrowMut, convert::TryFrom, io}; -use crate::err::Res; -#[cfg(feature = "read-bhttp")] -use crate::{err::Error, ReadSeek}; +use crate::{ + err::{Error, Res}, + ReadSeek, +}; -#[cfg(feature = "write-bhttp")] #[allow(clippy::cast_possible_truncation)] pub(crate) fn write_uint(v: impl Into, w: &mut impl io::Write) -> Res<()> { let v = v.into().to_be_bytes(); @@ -15,7 +13,6 @@ pub(crate) fn write_uint(v: impl Into, w: &mut impl io::Wri Ok(()) } -#[cfg(feature = "write-bhttp")] pub fn write_varint(v: impl Into, w: &mut impl io::Write) -> Res<()> { let v = v.into(); match () { @@ -27,51 +24,43 @@ pub fn write_varint(v: impl Into, w: &mut impl io::Write) -> Res<()> { } } -#[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(n: usize, r: &mut T) -> Res> +fn read_uint(r: &mut T) -> Res> where T: BorrowMut + ?Sized, R: ReadSeek + ?Sized, { - let mut buf = [0; 7]; - let count = r.borrow_mut().read(&mut buf[..n])?; + let mut buf = [0; 8]; + let count = r.borrow_mut().read(&mut buf[(8 - N)..])?; if count == 0 { Ok(None) - } else if count < n { + } else if count < N { Err(Error::Truncated) } else { - let mut v = 0; - for i in &buf[..n] { - v = (v << 8) | u64::from(*i); - } - Ok(Some(v)) + Ok(Some(u64::from_be_bytes(buf))) } } -#[cfg(feature = "read-bhttp")] pub fn read_varint(r: &mut T) -> Res> where T: BorrowMut + ?Sized, R: ReadSeek + ?Sized, { - if let Some(b1) = read_uint(1, r)? { + 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)?, + 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 { @@ -79,7 +68,6 @@ where } } -#[cfg(feature = "read-bhttp")] pub fn read_vec(r: &mut T) -> Res>> where T: BorrowMut + ?Sized, diff --git a/bhttp/src/stream/int.rs b/bhttp/src/stream/int.rs new file mode 100644 index 0000000..3a000b6 --- /dev/null +++ b/bhttp/src/stream/int.rs @@ -0,0 +1,258 @@ +use std::{ + future::Future, + pin::{pin, Pin}, + task::{Context, Poll}, +}; + +use futures::io::AsyncRead; + +use crate::{Error, Res}; + +/// A reader for a network-byte-order integer of predetermined size. +#[pin_project::pin_project] +pub struct ReadUint { + /// The source of data. + src: S, + /// A buffer that holds the bytes that have been read so far. + v: [u8; 8], + /// A counter of the number of bytes that are already in place. + /// This starts out at `8-N`. + read: usize, +} + +impl ReadUint { + pub fn stream(self) -> S { + self.src + } +} + +impl Future for ReadUint { + type Output = Res; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + match pin!(this.src).poll_read(cx, &mut this.v[*this.read..]) { + Poll::Pending => Poll::Pending, + Poll::Ready(Ok(count)) => { + if count == 0 { + return Poll::Ready(Err(Error::Truncated)); + } + *this.read += count; + if *this.read == 8 { + Poll::Ready(Ok(u64::from_be_bytes(*this.v))) + } else { + Poll::Pending + } + } + Poll::Ready(Err(e)) => Poll::Ready(Err(Error::from(e))), + } + } +} + +#[cfg(test)] +fn read_uint(src: S) -> ReadUint { + ReadUint { + src, + v: [0; 8], + read: 8 - N, + } +} + +/// A reader for a [QUIC variable-length integer](https://datatracker.ietf.org/doc/html/rfc9000#section-16). +#[pin_project::pin_project(project = ReadVarintProj)] +pub enum ReadVarint { + // Invariant: this Option always contains Some. + First(Option), + Extra1(#[pin] ReadUint), + Extra3(#[pin] ReadUint), + Extra7(#[pin] ReadUint), +} + +impl ReadVarint { + pub fn stream(self) -> S { + match self { + Self::Extra1(s) | Self::Extra3(s) | Self::Extra7(s) => s.stream(), + Self::First(mut s) => s.take().unwrap(), + } + } +} + +impl Future for ReadVarint { + type Output = Res>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.as_mut(); + if let Self::First(ref mut src) = this.get_mut() { + let mut buf = [0; 1]; + let src_ref = src.as_mut().unwrap(); + if let Poll::Ready(res) = pin!(src_ref).poll_read(cx, &mut buf[..]) { + match res { + Ok(0) => return Poll::Ready(Ok(None)), + Ok(_) => (), + Err(e) => return Poll::Ready(Err(Error::from(e))), + } + + let b1 = buf[0]; + let mut v = [0; 8]; + let next = match b1 >> 6 { + 0 => return Poll::Ready(Ok(Some(u64::from(b1)))), + 1 => { + let src = src.take().unwrap(); + v[6] = b1 & 0x3f; + Self::Extra1(ReadUint { src, v, read: 7 }) + } + 2 => { + let src = src.take().unwrap(); + v[4] = b1 & 0x3f; + Self::Extra3(ReadUint { src, v, read: 5 }) + } + 3 => { + let src = src.take().unwrap(); + v[0] = b1 & 0x3f; + Self::Extra7(ReadUint { src, v, read: 1 }) + } + _ => unreachable!(), + }; + + self.set(next); + } + } + let extra = match self.project() { + ReadVarintProj::Extra1(s) | ReadVarintProj::Extra3(s) | ReadVarintProj::Extra7(s) => { + s.poll(cx) + } + ReadVarintProj::First(_) => return Poll::Pending, + }; + if let Poll::Ready(v) = extra { + Poll::Ready(v.map(Some)) + } else { + Poll::Pending + } + } +} + +/// Read a [QUIC variable-length integer](https://datatracker.ietf.org/doc/html/rfc9000#section-16). +pub fn read_varint(src: S) -> ReadVarint { + ReadVarint::First(Some(src)) +} + +#[cfg(test)] +mod test { + use sync_async::SyncResolve; + + use crate::{ + err::Error, + rw::{write_uint as sync_write_uint, write_varint as sync_write_varint}, + stream::int::{read_uint, read_varint}, + }; + + const VARINTS: &[u64] = &[ + 0, + 1, + 63, + 64, + (1 << 14) - 1, + 1 << 14, + (1 << 30) - 1, + 1 << 30, + (1 << 62) - 1, + ]; + + #[test] + fn read_uint_values() { + macro_rules! validate_uint_range { + (@ $n:expr) => { + let m = u64::MAX >> (64 - 8 * $n); + for v in [0, 1, m] { + println!("{n} byte encoding of 0x{v:x}", n = $n); + let mut buf = Vec::with_capacity($n); + sync_write_uint::<$n>(v, &mut buf).unwrap(); + let mut buf_ref = &buf[..]; + let mut fut = read_uint::<_, $n>(&mut buf_ref); + assert_eq!(v, fut.sync_resolve().unwrap()); + let s = fut.stream(); + assert!(s.is_empty()); + } + }; + ($($n:expr),+ $(,)?) => { + $( + validate_uint_range!(@ $n); + )+ + } + } + validate_uint_range!(1, 2, 3, 4, 5, 6, 7, 8); + } + + #[test] + fn read_uint_truncated() { + macro_rules! validate_uint_truncated { + (@ $n:expr) => { + let m = u64::MAX >> (64 - 8 * $n); + for v in [0, 1, m] { + println!("{n} byte encoding of 0x{v:x}", n = $n); + let mut buf = Vec::with_capacity($n); + sync_write_uint::<$n>(v, &mut buf).unwrap(); + for i in 1..buf.len() { + let err = read_uint::<_, $n>(&mut &buf[..i]).sync_resolve().unwrap_err(); + assert!(matches!(err, Error::Truncated)); + } + } + }; + ($($n:expr),+ $(,)?) => { + $( + validate_uint_truncated!(@ $n); + )+ + } + } + validate_uint_truncated!(1, 2, 3, 4, 5, 6, 7, 8); + } + + #[test] + fn read_varint_values() { + for &v in VARINTS { + let mut buf = Vec::new(); + sync_write_varint(v, &mut buf).unwrap(); + let mut buf_ref = &buf[..]; + let mut fut = read_varint(&mut buf_ref); + assert_eq!(Some(v), fut.sync_resolve().unwrap()); + let s = fut.stream(); + assert!(s.is_empty()); + } + } + + #[test] + fn read_varint_none() { + assert!(read_varint(&mut &[][..]).sync_resolve().unwrap().is_none()); + } + + #[test] + fn read_varint_truncated() { + for &v in VARINTS { + let mut buf = Vec::new(); + sync_write_varint(v, &mut buf).unwrap(); + for i in 1..buf.len() { + let err = { + let mut buf: &[u8] = &buf[..i]; + read_varint(&mut buf).sync_resolve() + } + .unwrap_err(); + assert!(matches!(err, Error::Truncated)); + } + } + } + + #[test] + fn read_varint_extra() { + const EXTRA: &[u8] = &[161, 2, 49]; + for &v in VARINTS { + let mut buf = Vec::new(); + sync_write_varint(v, &mut buf).unwrap(); + buf.extend_from_slice(EXTRA); + let mut buf_ref = &buf[..]; + let mut fut = read_varint(&mut buf_ref); + assert_eq!(Some(v), fut.sync_resolve().unwrap()); + let s = fut.stream(); + assert_eq!(&s[..], EXTRA); + } + } +} diff --git a/bhttp/src/stream/mod.rs b/bhttp/src/stream/mod.rs new file mode 100644 index 0000000..bfba2c1 --- /dev/null +++ b/bhttp/src/stream/mod.rs @@ -0,0 +1,589 @@ +#![allow(dead_code)] +#![allow(clippy::incompatible_msrv)] // This module uses features from rust 1.82 + +use std::{ + cmp::min, + io::{Cursor, Error as IoError, Result as IoResult}, + mem, + pin::{pin, Pin}, + task::{Context, Poll}, +}; + +use futures::{stream::unfold, AsyncRead, Stream, TryStreamExt}; + +use crate::{ + err::Res, + stream::{int::read_varint, vec::read_vec}, + ControlData, Error, Field, FieldSection, Header, InformationalResponse, Message, Mode, COOKIE, +}; +mod int; +mod vec; + +trait AsyncReadControlData: Sized { + async fn async_read(request: bool, src: S) -> Res; +} + +impl AsyncReadControlData for ControlData { + async fn async_read(request: bool, mut src: S) -> Res { + let v = if request { + let method = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + let scheme = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + let authority = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + let path = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + Self::Request { + method, + scheme, + authority, + path, + } + } else { + let code = read_varint(&mut src).await?.ok_or(Error::Truncated)?; + Self::Response(crate::StatusCode::try_from(code)?) + }; + Ok(v) + } +} + +trait AsyncReadFieldSection: Sized { + async fn async_read(mode: Mode, src: S) -> Res; +} + +impl AsyncReadFieldSection for FieldSection { + async fn async_read(mode: Mode, mut src: S) -> Res { + let fields = if mode == Mode::KnownLength { + // Known-length fields can just be read into a buffer. + if let Some(buf) = read_vec(&mut src).await? { + Self::read_bhttp_fields(false, &mut Cursor::new(&buf[..]))? + } else { + Vec::new() + } + } else { + // The async version needs to be implemented directly. + let mut fields: Vec = Vec::new(); + let mut cookie_index: Option = None; + loop { + if let Some(n) = read_vec(&mut src).await? { + if n.is_empty() { + break fields; + } + let mut v = read_vec(&mut src).await?.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 fields.is_empty() { + break fields; + } else { + return Err(Error::Truncated); + } + } + }; + Ok(Self(fields)) + } +} + +#[derive(Default)] +enum BodyState { + // The starting state. + #[default] + Init, + // When reading the length, use this. + ReadLength { + buf: [u8; 8], + read: usize, + }, + // When reading the data, track how much is left. + ReadData { + remaining: usize, + }, +} + +impl BodyState { + fn read_len() -> Self { + Self::ReadLength { + buf: [0; 8], + read: 0, + } + } +} + +pub struct Body<'b, S> { + msg: &'b mut AsyncMessage, +} + +impl Body<'_, S> {} + +impl AsyncRead for Body<'_, S> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + self.msg.read_body(cx, buf).map_err(IoError::other) + } +} + +/// A helper function for the more complex body-reading code. +fn poll_error(e: Error) -> Poll> { + Poll::Ready(Err(IoError::other(e))) +} + +enum AsyncMessageState { + Init, + // Processing Informational responses (or before that). + Informational(bool), + // Having obtained the control data for the header, this is it. + Header(ControlData), + // Processing the Body. + Body(BodyState), + // Processing the trailer. + Trailer, + // All done. + Done, +} + +pub struct AsyncMessage { + // Whether this is a request and which mode. + mode: Option, + state: AsyncMessageState, + src: S, +} + +unsafe impl Send for AsyncMessage {} + +impl AsyncMessage { + async fn next_info(&mut self) -> Res> { + let request = if matches!(self.state, AsyncMessageState::Init) { + // Read control data ... + let t = read_varint(&mut self.src).await?.ok_or(Error::Truncated)?; + let request = t == 0 || t == 2; + self.mode = Some(Mode::try_from(t)?); + self.state = AsyncMessageState::Informational(request); + request + } else { + // ... or recover it. + let AsyncMessageState::Informational(request) = self.state else { + return Err(Error::InvalidState); + }; + request + }; + + let control = ControlData::async_read(request, &mut self.src).await?; + if let Some(status) = control.informational() { + let mode = self.mode.unwrap(); + let fields = FieldSection::async_read(mode, &mut self.src).await?; + Ok(Some(InformationalResponse::new(status, fields))) + } else { + self.state = AsyncMessageState::Header(control); + Ok(None) + } + } + + /// Produces a stream of informational responses from a fresh message. + /// Returns an empty stream if passed a request (or if there are no informational responses). + /// Error values on the stream indicate failures. + /// + /// There is no need to call this method to read a request, though + /// doing so is harmless. + /// + /// You can discard the stream that this function returns + /// without affecting the message. You can then either call this + /// method again to get any additional informational responses or + /// call `header()` to get the message header. + pub fn informational(&mut self) -> impl Stream> + '_ { + unfold(self, |this| async move { + this.next_info().await.transpose().map(|info| (info, this)) + }) + } + + /// This reads the header. If you have not called `informational` + /// and drained the resulting stream, this will do that for you. + /// # Panics + /// Never. + pub async fn header(&mut self) -> Res
{ + if matches!( + self.state, + AsyncMessageState::Init | AsyncMessageState::Informational(_) + ) { + // Need to scrub for errors, + // so that this can abort properly if there is one. + // The `try_any` usage is there to ensure that the stream is fully drained. + _ = self.informational().try_any(|_| async { false }).await?; + } + + if matches!(self.state, AsyncMessageState::Header(_)) { + let mode = self.mode.unwrap(); + let hfields = FieldSection::async_read(mode, &mut self.src).await?; + + let AsyncMessageState::Header(control) = mem::replace( + &mut self.state, + AsyncMessageState::Body(BodyState::default()), + ) else { + unreachable!(); + }; + Ok(Header::from((control, hfields))) + } else { + Err(Error::InvalidState) + } + } + + fn body_state(&mut self, s: BodyState) { + self.state = AsyncMessageState::Body(s); + } + + fn body_done(&mut self) { + self.state = AsyncMessageState::Trailer; + } + + /// Read the length of a body chunk. + /// This updates the values of `read` and `buf` to track the portion of the length + /// that was successfully read. + /// Returns `Some` with the error code that should be used if the reading + /// resulted in a conclusive outcome. + fn read_body_len( + cx: &mut Context<'_>, + src: &mut S, + first: bool, + read: &mut usize, + buf: &mut [u8; 8], + ) -> Option>> { + let mut src = pin!(src); + if *read == 0 { + let mut b = [0; 1]; + match src.as_mut().poll_read(cx, &mut b[..]) { + Poll::Pending => return Some(Poll::Pending), + Poll::Ready(Ok(0)) => { + return if first { + // It's OK for the first length to be absent. + // Just skip to the end. + *read = 8; + None + } else { + // ...it's not OK to drop length when continuing. + Some(poll_error(Error::Truncated)) + }; + } + Poll::Ready(Ok(1)) => match b[0] >> 6 { + 0 => { + buf[7] = b[0] & 0x3f; + *read = 8; + } + 1 => { + buf[6] = b[0] & 0x3f; + *read = 7; + } + 2 => { + buf[4] = b[0] & 0x3f; + *read = 5; + } + 3 => { + buf[0] = b[0] & 0x3f; + *read = 1; + } + _ => unreachable!(), + }, + Poll::Ready(Ok(_)) => unreachable!(), + Poll::Ready(Err(e)) => return Some(Poll::Ready(Err(e))), + } + } + if *read < 8 { + match src.as_mut().poll_read(cx, &mut buf[*read..]) { + Poll::Pending => return Some(Poll::Pending), + Poll::Ready(Ok(0)) => return Some(poll_error(Error::Truncated)), + Poll::Ready(Ok(len)) => { + *read += len; + } + Poll::Ready(Err(e)) => return Some(Poll::Ready(Err(e))), + } + } + None + } + + fn read_body(&mut self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { + // The length that precedes the first chunk can be absent. + // Only allow that for the first chunk (if indeterminate length). + let first = if let AsyncMessageState::Body(BodyState::Init) = &self.state { + self.body_state(BodyState::read_len()); + true + } else { + false + }; + + // Read the length. This uses `read_body_len` to track the state of this reading. + // This doesn't use `ReadVarint` or any convenience functions because we + // need to track the state and we don't want the borrow checker to flip out. + if let AsyncMessageState::Body(BodyState::ReadLength { buf, read }) = &mut self.state { + if let Some(res) = Self::read_body_len(cx, &mut self.src, first, read, buf) { + return res; + } + if *read == 8 { + match usize::try_from(u64::from_be_bytes(*buf)) { + Ok(0) => { + self.body_done(); + return Poll::Ready(Ok(0)); + } + Ok(remaining) => { + self.body_state(BodyState::ReadData { remaining }); + } + Err(e) => return poll_error(Error::IntRange(e)), + } + } + } + + match &mut self.state { + AsyncMessageState::Body(BodyState::ReadData { remaining }) => { + let amount = min(*remaining, buf.len()); + let res = pin!(&mut self.src).poll_read(cx, &mut buf[..amount]); + match res { + Poll::Pending => Poll::Pending, + Poll::Ready(Ok(0)) => poll_error(Error::Truncated), + Poll::Ready(Ok(len)) => { + *remaining -= len; + if *remaining == 0 { + let mode = self.mode.unwrap(); + if mode == Mode::IndeterminateLength { + self.body_state(BodyState::read_len()); + } else { + self.body_done(); + } + } + Poll::Ready(Ok(len)) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + } + } + AsyncMessageState::Trailer => Poll::Ready(Ok(0)), + _ => Poll::Pending, + } + } + + /// Read the body. + /// This produces an implementation of `AsyncRead` that filters out + /// the framing from the message body. + /// # Errors + /// This errors when the header has not been read. + /// Any IO errors are generated by the returned `Body` instance. + pub fn body(&mut self) -> Res> { + match self.state { + AsyncMessageState::Body(_) => Ok(Body { msg: self }), + _ => Err(Error::InvalidState), + } + } + + /// Read any trailer. + /// This might be empty. + /// # Errors + /// This errors when the body has not been read. + /// # Panics + /// Never. + pub async fn trailer(&mut self) -> Res { + if matches!(self.state, AsyncMessageState::Trailer) { + let trailer = FieldSection::async_read(self.mode.unwrap(), &mut self.src).await?; + self.state = AsyncMessageState::Done; + Ok(trailer) + } else { + Err(Error::InvalidState) + } + } +} + +/// Asynchronous reading for a [`Message`]. +pub trait AsyncReadMessage: Sized { + fn async_read(src: S) -> AsyncMessage; +} + +impl AsyncReadMessage for Message { + fn async_read(src: S) -> AsyncMessage { + AsyncMessage { + mode: None, + state: AsyncMessageState::Init, + src, + } + } +} + +#[cfg(test)] +mod test { + use std::pin::pin; + + use futures::TryStreamExt; + use sync_async::{Dribble, SyncRead, SyncResolve, SyncTryCollect}; + + use crate::{stream::AsyncReadMessage, Error, Message}; + + // Example from Section 5.1 of RFC 9292. + const REQUEST1: &[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, + ]; + const REQUEST2: &[u8] = &[ + 0x02, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x00, 0x0a, 0x2f, 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x74, 0x78, 0x74, 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, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + #[test] + fn informational() { + const INFO: &[u8] = &[1, 64, 100, 0, 64, 200, 0]; + let mut buf_alias = INFO; + let mut msg = Message::async_read(&mut buf_alias); + let info = msg.informational().sync_collect::>().unwrap(); + assert_eq!(info.len(), 1); + let err = msg.informational().sync_collect::>(); + assert!(matches!(err, Err(Error::InvalidState))); + let hdr = pin!(msg.header()).sync_resolve().unwrap(); + assert_eq!(hdr.control().status().unwrap().code(), 200); + assert!(hdr.is_empty()); + } + + #[test] + fn sample_requests() { + fn validate_sample_request(mut buf: &[u8]) { + let mut msg = Message::async_read(&mut buf); + let info = msg.informational().sync_collect::>().unwrap(); + assert!(info.is_empty()); + + let hdr = pin!(msg.header()).sync_resolve().unwrap(); + assert_eq!(hdr.control(), &(b"GET", b"https", b"", b"/hello.txt")); + assert_eq!( + hdr.get(b"user-agent"), + Some(&b"curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"[..]), + ); + assert_eq!(hdr.get(b"host"), Some(&b"www.example.com"[..])); + assert_eq!(hdr.get(b"accept-language"), Some(&b"en, mi"[..])); + assert_eq!(hdr.len(), 3); + + let body = pin!(msg.body().unwrap()).sync_read_to_end(); + assert!(body.is_empty()); + + let trailer = pin!(msg.trailer()).sync_resolve().unwrap(); + assert!(trailer.is_empty()); + } + + validate_sample_request(REQUEST1); + validate_sample_request(REQUEST2); + validate_sample_request(&REQUEST2[..REQUEST2.len() - 12]); + } + + #[test] + fn truncated_header() { + // The indefinite-length request example includes 10 bytes of padding. + // The three additional zero values at the end represent: + // 1. The terminating zero for the header field section. + // 2. The terminating zero for the (empty) body. + // 3. The terminating zero for the (absent) trailer field section. + // The latter two (body and trailer) can be cut and the message will still work. + // The first is not optional; dropping it means that the message is truncated. + let mut buf = &mut &REQUEST2[..REQUEST2.len() - 13]; + let mut msg = Message::async_read(&mut buf); + // Use this test to test skipping a few things. + let err = pin!(msg.header()).sync_resolve().unwrap_err(); + assert!(matches!(err, Error::Truncated)); + } + + /// This test is crazy. It reads a byte at a time and checks the state constantly. + #[test] + fn sample_response() { + const RESPONSE: &[u8] = &[ + 0x03, 0x40, 0x66, 0x07, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x0a, 0x22, 0x73, + 0x6c, 0x65, 0x65, 0x70, 0x20, 0x31, 0x35, 0x22, 0x00, 0x40, 0x67, 0x04, 0x6c, 0x69, + 0x6e, 0x6b, 0x23, 0x3c, 0x2f, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x2e, 0x63, 0x73, 0x73, + 0x3e, 0x3b, 0x20, 0x72, 0x65, 0x6c, 0x3d, 0x70, 0x72, 0x65, 0x6c, 0x6f, 0x61, 0x64, + 0x3b, 0x20, 0x61, 0x73, 0x3d, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x04, 0x6c, 0x69, 0x6e, + 0x6b, 0x24, 0x3c, 0x2f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x2e, 0x6a, 0x73, 0x3e, + 0x3b, 0x20, 0x72, 0x65, 0x6c, 0x3d, 0x70, 0x72, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x3b, + 0x20, 0x61, 0x73, 0x3d, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x00, 0x40, 0xc8, 0x04, + 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x37, 0x20, 0x4a, + 0x75, 0x6c, 0x20, 0x32, 0x30, 0x30, 0x39, 0x20, 0x31, 0x32, 0x3a, 0x32, 0x38, 0x3a, + 0x35, 0x33, 0x20, 0x47, 0x4d, 0x54, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x06, + 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x2d, 0x6d, 0x6f, + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x1d, 0x57, 0x65, 0x64, 0x2c, 0x20, 0x32, 0x32, + 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x30, 0x39, 0x20, 0x31, 0x39, 0x3a, 0x31, + 0x35, 0x3a, 0x35, 0x36, 0x20, 0x47, 0x4d, 0x54, 0x04, 0x65, 0x74, 0x61, 0x67, 0x14, + 0x22, 0x33, 0x34, 0x61, 0x61, 0x33, 0x38, 0x37, 0x2d, 0x64, 0x2d, 0x31, 0x35, 0x36, + 0x38, 0x65, 0x62, 0x30, 0x30, 0x22, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x0e, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x02, + 0x35, 0x31, 0x04, 0x76, 0x61, 0x72, 0x79, 0x0f, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x2d, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x0c, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x0a, 0x74, 0x65, 0x78, 0x74, 0x2f, + 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x00, 0x33, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, + 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x20, 0x4d, 0x79, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0x61, 0x20, + 0x74, 0x72, 0x61, 0x69, 0x6c, 0x69, 0x6e, 0x67, 0x20, 0x43, 0x52, 0x4c, 0x46, 0x2e, + 0x0d, 0x0a, 0x00, 0x00, + ]; + + let mut buf = RESPONSE; + let mut msg = Message::async_read(Dribble::new(&mut buf)); + + { + // Need to scope access to `info` or it will hold the reference to `msg`. + let mut info = pin!(msg.informational()); + + let info1 = info.try_next().sync_resolve().unwrap().unwrap(); + assert_eq!(info1.status(), 102_u16); + assert_eq!(info1.len(), 1); + assert_eq!(info1.get(b"running"), Some(&b"\"sleep 15\""[..])); + + let info2 = info.try_next().sync_resolve().unwrap().unwrap(); + assert_eq!(info2.status(), 103_u16); + assert_eq!(info2.len(), 2); + let links = info2.get_all(b"link").collect::>(); + assert_eq!( + &links, + &[ + &b"; rel=preload; as=style"[..], + &b"; rel=preload; as=script"[..], + ] + ); + + assert!(info.try_next().sync_resolve().unwrap().is_none()); + } + + let hdr = pin!(msg.header()).sync_resolve().unwrap(); + assert_eq!(hdr.control(), &200_u16); + assert_eq!(hdr.len(), 8); + assert_eq!(hdr.get(b"vary"), Some(&b"Accept-Encoding"[..])); + assert_eq!(hdr.get(b"etag"), Some(&b"\"34aa387-d-1568eb00\""[..])); + + { + let mut body = pin!(msg.body().unwrap()); + assert_eq!(body.sync_read_exact(12), b"Hello World!"); + } + // Attempting to read the trailer before finishing the body should fail. + assert!(matches!( + pin!(msg.trailer()).sync_resolve(), + Err(Error::InvalidState) + )); + { + // Picking up the body again should work fine. + let mut body = pin!(msg.body().unwrap()); + assert_eq!( + body.sync_read_to_end(), + b" My content includes a trailing CRLF.\r\n" + ); + } + let trailer = pin!(msg.trailer()).sync_resolve().unwrap(); + assert!(trailer.is_empty()); + } +} diff --git a/bhttp/src/stream/vec.rs b/bhttp/src/stream/vec.rs new file mode 100644 index 0000000..7289be9 --- /dev/null +++ b/bhttp/src/stream/vec.rs @@ -0,0 +1,221 @@ +use std::{ + future::Future, + mem, + pin::{pin, Pin}, + task::{Context, Poll}, +}; + +use futures::{io::AsyncRead, FutureExt}; + +use super::int::{read_varint, ReadVarint}; +use crate::{Error, Res}; + +/// A reader for a varint-length-prefixed buffer. +#[pin_project::pin_project(project = ReadVecProj)] +#[allow(clippy::module_name_repetitions)] +pub enum ReadVec { + // Invariant: This Option is always Some. + ReadLen { + src: Option>, + cap: u64, + }, + ReadBody { + src: S, + buf: Vec, + remaining: usize, + }, +} + +impl ReadVec { + /// # Panics + /// If `limit` is more than `usize::MAX` or + /// if this is called after the length is read. + pub fn limit(&mut self, limit: u64) { + usize::try_from(limit).expect("cannot set a limit larger than usize::MAX"); + if let Self::ReadLen { ref mut cap, .. } = self { + *cap = limit; + } else { + panic!("cannot set a limit once the size has been read"); + } + } + + pub fn stream(self) -> S { + match self { + Self::ReadLen { mut src, .. } => src.take().unwrap().stream(), + Self::ReadBody { src, .. } => src, + } + } +} + +impl Future for ReadVec { + type Output = Res>>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.as_mut(); + if let Self::ReadLen { src, cap } = this.get_mut() { + match src.as_mut().unwrap().poll_unpin(cx) { + Poll::Ready(Ok(None)) => return Poll::Ready(Ok(None)), + Poll::Ready(Ok(Some(0))) => return Poll::Ready(Ok(Some(Vec::new()))), + Poll::Ready(Ok(Some(sz))) => { + if sz > *cap { + return Poll::Ready(Err(Error::LimitExceeded)); + } + // `cap` cannot exceed min(usize::MAX, u64::MAX). + let sz = usize::try_from(sz).unwrap(); + let body = Self::ReadBody { + src: src.take().unwrap().stream(), + buf: vec![0; sz], + remaining: sz, + }; + self.set(body); + } + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Pending => return Poll::Pending, + } + } + + let ReadVecProj::ReadBody { + src, + buf, + remaining, + } = self.project() + else { + return Poll::Pending; + }; + + let offset = buf.len() - *remaining; + match pin!(src).poll_read(cx, &mut buf[offset..]) { + Poll::Pending => Poll::Pending, + Poll::Ready(Err(e)) => Poll::Ready(Err(Error::from(e))), + Poll::Ready(Ok(0)) => Poll::Ready(Err(Error::Truncated)), + Poll::Ready(Ok(c)) => { + *remaining -= c; + if *remaining > 0 { + Poll::Pending + } else { + Poll::Ready(Ok(Some(mem::take(buf)))) + } + } + } + } +} + +#[allow(clippy::module_name_repetitions)] +pub fn read_vec(src: S) -> ReadVec { + ReadVec::ReadLen { + src: Some(read_varint(src)), + cap: u64::try_from(usize::MAX).unwrap_or(u64::MAX), + } +} + +#[cfg(test)] +mod test { + + use std::{ + cmp, + fmt::Debug, + io::Result, + pin::Pin, + task::{Context, Poll}, + }; + + use futures::AsyncRead; + use sync_async::SyncResolve; + + use crate::{rw::write_varint as sync_write_varint, stream::vec::read_vec, Error}; + + const FILL_VALUE: u8 = 90; + + fn fill(len: T) -> Vec + where + u64: TryFrom, + >::Error: Debug, + usize: TryFrom, + >::Error: Debug, + T: Debug + Copy, + { + let mut buf = Vec::new(); + sync_write_varint(u64::try_from(len).unwrap(), &mut buf).unwrap(); + buf.resize(buf.len() + usize::try_from(len).unwrap(), FILL_VALUE); + buf + } + + #[test] + fn read_vecs() { + for len in [0, 1, 2, 3, 64] { + let buf = fill(len); + let mut buf_ref = &buf[..]; + let mut fut = read_vec(&mut buf_ref); + if let Ok(Some(out)) = fut.sync_resolve() { + assert_eq!(len, out.len()); + assert!(out.iter().all(|&v| v == FILL_VALUE)); + + assert!(fut.stream().is_empty()); + } + } + } + + #[test] + fn exceed_cap() { + const LEN: u64 = 20; + let buf = fill(LEN); + let mut buf_ref = &buf[..]; + let mut fut = read_vec(&mut buf_ref); + fut.limit(LEN - 1); + assert!(matches!(fut.sync_resolve(), Err(Error::LimitExceeded))); + } + + /// This class implements `AsyncRead`, but + /// always blocks after returning a fixed value. + #[derive(Default)] + struct IncompleteRead<'a> { + data: &'a [u8], + consumed: usize, + } + + impl<'a> IncompleteRead<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, consumed: 0 } + } + } + + impl AsyncRead for IncompleteRead<'_> { + fn poll_read( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let remaining = &self.data[self.consumed..]; + if remaining.is_empty() { + Poll::Pending + } else { + let copied = cmp::min(buf.len(), remaining.len()); + buf[..copied].copy_from_slice(&remaining[..copied]); + self.as_mut().consumed += copied; + Poll::Ready(std::io::Result::Ok(copied)) + } + } + } + + #[test] + #[should_panic(expected = "cannot set a limit once the size has been read")] + fn late_cap() { + let mut buf = IncompleteRead::new(&[2, 1]); + _ = read_vec(&mut buf).sync_resolve_with(|mut f| { + println!("pending"); + f.as_mut().limit(100); + }); + } + + #[test] + #[cfg(any(target_pointer_width = "32", target_pointer_width = "16"))] + #[should_panic(expected = "cannot set a limit larger than usize::MAX")] + fn too_large_cap() { + const LEN: u64 = 20; + let buf = fill(LEN); + + let mut buf_ref = &buf[..]; + let mut fut = read_vec(&mut buf_ref); + fut.limit(u64::try_from(usize::MAX).unwrap() + 1); + } +} diff --git a/bhttp/tests/test.rs b/bhttp/tests/test.rs index c6729c6..9ed9731 100644 --- a/bhttp/tests/test.rs +++ b/bhttp/tests/test.rs @@ -1,5 +1,5 @@ // Rather than grapple with #[cfg(...)] for every variable and import. -#![cfg(all(feature = "http", feature = "bhttp"))] +#![cfg(feature = "http")] use std::{io::Cursor, mem::drop}; diff --git a/ohttp-client-cli/Cargo.toml b/ohttp-client-cli/Cargo.toml index 0e9a190..68d77e8 100644 --- a/ohttp-client-cli/Cargo.toml +++ b/ohttp-client-cli/Cargo.toml @@ -23,7 +23,7 @@ hex = "0.4" [dependencies.bhttp] path= "../bhttp" -features = ["bhttp", "http"] +features = ["http"] [dependencies.ohttp] path= "../ohttp" diff --git a/ohttp-client-cli/src/main.rs b/ohttp-client-cli/src/main.rs index 7acc0f1..37a948d 100644 --- a/ohttp-client-cli/src/main.rs +++ b/ohttp-client-cli/src/main.rs @@ -1,8 +1,9 @@ #![deny(warnings, clippy::pedantic)] +use std::io::{self, BufRead, Write}; + use bhttp::{Message, Mode}; use ohttp::{init, ClientRequest}; -use std::io::{self, BufRead, Write}; fn main() { init(); diff --git a/ohttp-client/Cargo.toml b/ohttp-client/Cargo.toml index f049f5f..8d35f3d 100644 --- a/ohttp-client/Cargo.toml +++ b/ohttp-client/Cargo.toml @@ -27,7 +27,7 @@ tokio = { version = "1", features = ["full"] } [dependencies.bhttp] path= "../bhttp" -features = ["bhttp", "http"] +features = ["http"] [dependencies.ohttp] path= "../ohttp" diff --git a/ohttp-client/src/main.rs b/ohttp-client/src/main.rs index 1d71dc2..4e8a431 100644 --- a/ohttp-client/src/main.rs +++ b/ohttp-client/src/main.rs @@ -1,7 +1,8 @@ #![deny(warnings, clippy::pedantic)] -use bhttp::{Message, Mode}; use std::{fs::File, io, io::Read, ops::Deref, path::PathBuf, str::FromStr}; + +use bhttp::{Message, Mode}; use structopt::StructOpt; type Res = Result>; diff --git a/ohttp-server/Cargo.toml b/ohttp-server/Cargo.toml index 9b9be88..1e92762 100644 --- a/ohttp-server/Cargo.toml +++ b/ohttp-server/Cargo.toml @@ -26,7 +26,7 @@ warp = { version = "0.3", features = ["tls"] } [dependencies.bhttp] path= "../bhttp" -features = ["bhttp", "write-http"] +features = ["http"] [dependencies.ohttp] path= "../ohttp" diff --git a/ohttp/Cargo.toml b/ohttp/Cargo.toml index f7bb4f5..7f2ddba 100644 --- a/ohttp/Cargo.toml +++ b/ohttp/Cargo.toml @@ -13,7 +13,7 @@ categories.workspace = true readme.workspace = true [features] -default = ["client", "server", "rust-hpke"] +default = ["client", "server", "rust-hpke", "stream"] app-svc = ["nss"] client = [] external-sqlite = [] @@ -21,6 +21,7 @@ gecko = ["nss", "mozbuild"] nss = ["bindgen"] rust-hpke = ["rand", "aead", "aes-gcm", "chacha20poly1305", "hkdf", "sha2", "hpke"] server = [] +stream = ["dep:futures", "dep:pin-project"] unsafe-print-secrets = [] [dependencies] @@ -28,10 +29,12 @@ aead = {version = "0.5", optional = true, features = ["std"]} aes-gcm = {version = "0.10", optional = true} byteorder = "1.4" chacha20poly1305 = {version = "0.10", optional = true} +futures = {version = "0.3", optional = true} hex = "0.4" hkdf = {version = "0.12", optional = true} hpke = {version = "0.13", optional = true, default-features = false, features = ["std", "x25519"]} log = {version = "0.4", default-features = false} +pin-project = {version = "1.1", optional = true} rand = {version = "0.9", optional = true} regex = {version = "~1.11", optional = true} sha2 = {version = "0.10", optional = true} @@ -51,3 +54,4 @@ features = ["runtime"] [dev-dependencies] env_logger = {version = "0.10", default-features = false} +sync-async = {path = "../sync-async"} diff --git a/ohttp/build.rs b/ohttp/build.rs index 6596ba3..801af5a 100644 --- a/ohttp/build.rs +++ b/ohttp/build.rs @@ -344,7 +344,7 @@ mod nss { assert_eq!( v.next(), Some("3"), - "NSS version 3.62 or higher is needed (or set $NSS_DIR)" + " version 3.62 or higher is needed (or set $NSS_DIR)" ); if let Some(minor) = v.next() { let minor = minor diff --git a/ohttp/src/config.rs b/ohttp/src/config.rs index 24acea3..df1ea3d 100644 --- a/ohttp/src/config.rs +++ b/ohttp/src/config.rs @@ -1,24 +1,24 @@ -use crate::{ - err::{Error, Res}, - hpke::{Aead as AeadId, Kdf, Kem}, - KeyId, -}; -use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; use std::{ convert::TryFrom, io::{BufRead, BufReader, Cursor, Read}, }; +use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; + #[cfg(feature = "nss")] use crate::nss::{ hpke::{generate_key_pair, Config as HpkeConfig, HpkeR}, PrivateKey, PublicKey, }; - #[cfg(feature = "rust-hpke")] use crate::rh::hpke::{ derive_key_pair, generate_key_pair, Config as HpkeConfig, HpkeR, PrivateKey, PublicKey, }; +use crate::{ + err::{Error, Res}, + hpke::{Aead as AeadId, Kdf, Kem}, + KeyId, +}; /// A tuple of KDF and AEAD identifiers. #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -95,18 +95,15 @@ impl KeyConfig { Self::strip_unsupported(&mut symmetric, kem); assert!(!symmetric.is_empty()); let (sk, pk) = derive_key_pair(kem, ikm)?; - Ok(Self { + return Ok(Self { key_id, kem, symmetric, sk: Some(sk), pk, - }) - } - #[cfg(not(feature = "rust-hpke"))] - { - Err(Error::Unsupported) + }); } + Err(Error::Unsupported) } /// Encode a list of key configurations. @@ -260,6 +257,22 @@ impl KeyConfig { Err(Error::Unsupported) } } + + #[allow(clippy::similar_names)] // for kem_id and key_id + pub(crate) fn decode_hpke_config(&self, r: &mut Cursor<&[u8]>) -> Res { + let key_id = r.read_u8()?; + if key_id != self.key_id { + return Err(Error::KeyId); + } + let kem_id = Kem::try_from(r.read_u16::()?)?; + if kem_id != self.kem { + return Err(Error::InvalidKem); + } + let kdf_id = Kdf::try_from(r.read_u16::()?)?; + let aead_id = AeadId::try_from(r.read_u16::()?)?; + let hpke_config = HpkeConfig::new(self.kem, kdf_id, aead_id); + Ok(hpke_config) + } } impl AsRef for KeyConfig { @@ -270,11 +283,12 @@ impl AsRef for KeyConfig { #[cfg(test)] mod test { + use std::iter::zip; + use crate::{ hpke::{Aead, Kdf, Kem}, init, Error, KeyConfig, KeyId, SymmetricSuite, }; - use std::iter::zip; const KEY_ID: KeyId = 1; const KEM: Kem = Kem::X25519Sha256; diff --git a/ohttp/src/crypto.rs b/ohttp/src/crypto.rs new file mode 100644 index 0000000..a22b74e --- /dev/null +++ b/ohttp/src/crypto.rs @@ -0,0 +1,13 @@ +use crate::{err::Res, AeadId}; + +pub trait Decrypt { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res>; + #[allow(dead_code)] // Used by stream feature. + fn alg(&self) -> AeadId; +} + +pub trait Encrypt { + fn seal(&mut self, aad: &[u8], ct: &[u8]) -> Res>; + #[allow(dead_code)] // Used by stream feature. + fn alg(&self) -> AeadId; +} diff --git a/ohttp/src/err.rs b/ohttp/src/err.rs index 17c3fb3..ec7741e 100644 --- a/ohttp/src/err.rs +++ b/ohttp/src/err.rs @@ -5,9 +5,15 @@ pub enum Error { #[cfg(feature = "rust-hpke")] #[error("a problem occurred with the AEAD")] Aead(#[from] aead::Error), + #[cfg(feature = "stream")] + #[error("a stream chunk was larger than the maximum allowed size")] + ChunkTooLarge, #[cfg(feature = "nss")] #[error("a problem occurred during cryptographic processing: {0}")] Crypto(#[from] crate::nss::Error), + #[cfg(feature = "stream")] + #[error("a stream contained data after the last chunk")] + ExtraData, #[error("an error was found in the format")] Format, #[cfg(feature = "rust-hpke")] @@ -23,12 +29,18 @@ pub enum Error { Io(#[from] std::io::Error), #[error("the key ID was invalid")] KeyId, + #[cfg(feature = "stream")] + #[error("the object was not ready")] + NotReady, + #[error("the configuration contained too many symmetric suites")] + TooManySymmetricSuites, #[error("a field was truncated")] Truncated, #[error("the configuration was not supported")] Unsupported, - #[error("the configuration contained too many symmetric suites")] - TooManySymmetricSuites, + #[cfg(feature = "stream")] + #[error("writes are not supported after closing")] + WriteAfterClose, } impl From for Error { diff --git a/ohttp/src/lib.rs b/ohttp/src/lib.rs index 38e3666..133d9b8 100644 --- a/ohttp/src/lib.rs +++ b/ohttp/src/lib.rs @@ -4,8 +4,11 @@ not(all(feature = "client", feature = "server")), allow(dead_code, unused_imports) )] +#[cfg(all(feature = "nss", feature = "rust-hpke"))] +compile_error!("features \"nss\" and \"rust-hpke\" are mutually incompatible"); mod config; +mod crypto; mod err; pub mod hpke; #[cfg(feature = "nss")] @@ -14,48 +17,48 @@ mod nss; mod rand; #[cfg(feature = "rust-hpke")] mod rh; +#[cfg(feature = "stream")] +mod stream; -pub use crate::{ - config::{KeyConfig, SymmetricSuite}, - err::Error, -}; - -use crate::{ - err::Res, - hpke::{Aead as AeadId, Kdf, Kem}, -}; -use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; -use log::trace; use std::{ cmp::max, convert::TryFrom, - io::{BufReader, Read}, + io::{Cursor, Read}, mem::size_of, }; -#[cfg(feature = "nss")] -use crate::nss::random; +use byteorder::{NetworkEndian, WriteBytesExt}; +use crypto::{Decrypt, Encrypt}; +use log::trace; + #[cfg(feature = "nss")] use crate::nss::{ aead::{Aead, Mode, NONCE_LEN}, hkdf::{Hkdf, KeyMechanism}, hpke::{Config as HpkeConfig, Exporter, HpkeR, HpkeS}, + random, PublicKey, SymKey, }; - -#[cfg(feature = "rust-hpke")] -use crate::rand::random; +#[cfg(feature = "stream")] +use crate::stream::{ClientRequest as StreamClient, ServerRequest as ServerRequestStream}; +pub use crate::{ + config::{KeyConfig, SymmetricSuite}, + err::Error, +}; +use crate::{err::Res, hpke::Aead as AeadId}; #[cfg(feature = "rust-hpke")] -use crate::rh::{ - aead::{Aead, Mode, NONCE_LEN}, - hkdf::{Hkdf, KeyMechanism}, - hpke::{Config as HpkeConfig, Exporter, HpkeR, HpkeS}, +use crate::{ + rand::random, + rh::{ + aead::{Aead, Mode, NONCE_LEN}, + hkdf::{Hkdf, KeyMechanism}, + hpke::{Config as HpkeConfig, Exporter, HpkeR, HpkeS, PublicKey}, + SymKey, + }, }; /// The request header is a `KeyId` and 2 each for KEM, KDF, and AEAD identifiers const REQUEST_HEADER_LEN: usize = size_of::() + 6; const INFO_REQUEST: &[u8] = b"message/bhttp request"; -/// The info used for HPKE export is `INFO_REQUEST`, a zero byte, and the header. -const INFO_LEN: usize = INFO_REQUEST.len() + 1 + REQUEST_HEADER_LEN; const LABEL_RESPONSE: &[u8] = b"message/bhttp response"; const INFO_KEY: &[u8] = b"key"; const INFO_NONCE: &[u8] = b"nonce"; @@ -69,9 +72,9 @@ pub fn init() { } /// Construct the info parameter we use to initialize an `HpkeS` instance. -fn build_info(key_id: KeyId, config: HpkeConfig) -> Res> { - let mut info = Vec::with_capacity(INFO_LEN); - info.extend_from_slice(INFO_REQUEST); +fn build_info(label: &[u8], key_id: KeyId, config: HpkeConfig) -> Res> { + let mut info = Vec::with_capacity(label.len() + 1 + REQUEST_HEADER_LEN); + info.extend_from_slice(label); info.push(0); info.write_u8(key_id)?; info.write_u16::(u16::from(config.kem()))?; @@ -85,8 +88,9 @@ fn build_info(key_id: KeyId, config: HpkeConfig) -> Res> { /// This might not be necessary if we agree on a format. #[cfg(feature = "client")] pub struct ClientRequest { - hpke: HpkeS, - header: Vec, + key_id: KeyId, + config: HpkeConfig, + pk: PublicKey, } #[cfg(feature = "client")] @@ -95,14 +99,11 @@ impl ClientRequest { pub fn from_config(config: &mut KeyConfig) -> Res { // TODO(mt) choose the best config, not just the first. let selected = config.select(config.symmetric[0])?; - - // Build the info, which contains the message header. - let info = build_info(config.key_id, selected)?; - let hpke = HpkeS::new(selected, &mut config.pk, &info)?; - - let header = Vec::from(&info[INFO_REQUEST.len() + 1..]); - debug_assert_eq!(header.len(), REQUEST_HEADER_LEN); - Ok(Self { hpke, header }) + Ok(Self { + key_id: config.key_id, + config: selected, + pk: config.pk.clone(), + }) } /// Reads an encoded configuration and constructs a single use client sender. @@ -127,21 +128,32 @@ impl ClientRequest { /// Encapsulate a request. This consumes this object. /// This produces a response handler and the bytes of an encapsulated request. pub fn encapsulate(mut self, request: &[u8]) -> Res<(Vec, ClientResponse)> { - let extra = - self.hpke.config().kem().n_enc() + self.hpke.config().aead().n_t() + request.len(); - let expected_len = self.header.len() + extra; + // Build the info, which contains the message header. + let info = build_info(INFO_REQUEST, self.key_id, self.config)?; + let mut hpke = HpkeS::new(self.config, &mut self.pk, &info)?; - let mut enc_request = self.header; + let header = Vec::from(&info[INFO_REQUEST.len() + 1..]); + debug_assert_eq!(header.len(), REQUEST_HEADER_LEN); + + let extra = hpke.config().kem().n_enc() + hpke.config().aead().n_t() + request.len(); + let expected_len = header.len() + extra; + + let mut enc_request = header; enc_request.reserve_exact(extra); - let enc = self.hpke.enc()?; + let enc = hpke.enc()?; enc_request.extend_from_slice(&enc); - let mut ct = self.hpke.seal(&[], request)?; + let mut ct = hpke.seal(&[], request)?; enc_request.append(&mut ct); debug_assert_eq!(expected_len, enc_request.len()); - Ok((enc_request, ClientResponse::new(self.hpke, enc))) + Ok((enc_request, ClientResponse::new(hpke, enc))) + } + + #[cfg(feature = "stream")] + pub fn encapsulate_stream(self, dst: S) -> Res> { + StreamClient::start(dst, self.config, self.key_id, self.pk) } } @@ -170,48 +182,45 @@ impl Server { &self.config } - /// Remove encapsulation on a message. + fn decode_request_header(&self, r: &mut Cursor<&[u8]>, label: &[u8]) -> Res<(HpkeR, Vec)> { + let hpke_config = self.config.decode_hpke_config(r)?; + let sym = SymmetricSuite::new(hpke_config.kdf(), hpke_config.aead()); + let config = self.config.select(sym)?; + let info = build_info(label, self.config.key_id, hpke_config)?; + + let mut enc = vec![0; config.kem().n_enc()]; + r.read_exact(&mut enc)?; + + Ok(( + HpkeR::new( + config, + &self.config.pk, + self.config.sk.as_ref().unwrap(), + &enc, + &info, + )?, + enc, + )) + } + + /// Remove encapsulation on a request. /// # Panics /// Not as a consequence of this code, but Rust won't know that for sure. - #[allow(clippy::similar_names)] // for kem_id and key_id pub fn decapsulate(&self, enc_request: &[u8]) -> Res<(Vec, ServerResponse)> { - if enc_request.len() < REQUEST_HEADER_LEN { + if enc_request.len() <= REQUEST_HEADER_LEN { return Err(Error::Truncated); } - let mut r = BufReader::new(enc_request); - let key_id = r.read_u8()?; - if key_id != self.config.key_id { - return Err(Error::KeyId); - } - let kem_id = Kem::try_from(r.read_u16::()?)?; - if kem_id != self.config.kem { - return Err(Error::InvalidKem); - } - let kdf_id = Kdf::try_from(r.read_u16::()?)?; - let aead_id = AeadId::try_from(r.read_u16::()?)?; - let sym = SymmetricSuite::new(kdf_id, aead_id); - - let info = build_info( - key_id, - HpkeConfig::new(self.config.kem, sym.kdf(), sym.aead()), - )?; - - let cfg = self.config.select(sym)?; - let mut enc = vec![0; cfg.kem().n_enc()]; - r.read_exact(&mut enc)?; - let mut hpke = HpkeR::new( - cfg, - &self.config.pk, - self.config.sk.as_ref().unwrap(), - &enc, - &info, - )?; + let mut r = Cursor::new(enc_request); + let (mut hpke, enc) = self.decode_request_header(&mut r, INFO_REQUEST)?; - let mut ct = Vec::new(); - r.read_to_end(&mut ct)?; + let request = hpke.open(&[], &enc_request[usize::try_from(r.position())?..])?; + Ok((request, ServerResponse::new(&hpke, &enc)?)) + } - let request = hpke.open(&[], &ct)?; - Ok((request, ServerResponse::new(&hpke, enc)?)) + /// Remove encapsulation on a streamed request. + #[cfg(feature = "stream")] + pub fn decapsulate_stream(self, src: S) -> ServerRequestStream { + ServerRequestStream::new(self.config, src) } } @@ -219,19 +228,16 @@ fn entropy(config: HpkeConfig) -> usize { max(config.aead().n_n(), config.aead().n_k()) } -fn make_aead( - mode: Mode, - cfg: HpkeConfig, - exp: &impl Exporter, - enc: Vec, - response_nonce: &[u8], -) -> Res { - let secret = exp.export(LABEL_RESPONSE, entropy(cfg))?; - let mut salt = enc; - salt.extend_from_slice(response_nonce); +fn export_secret(exp: &E, label: &[u8], cfg: HpkeConfig) -> Res { + exp.export(label, entropy(cfg)) +} + +fn make_aead(mode: Mode, cfg: HpkeConfig, secret: &SymKey, enc: &[u8], nonce: &[u8]) -> Res { + let mut salt = enc.to_vec(); + salt.extend_from_slice(nonce); let hkdf = Hkdf::new(cfg.kdf()); - let prk = hkdf.extract(&salt, &secret)?; + let prk = hkdf.extract(&salt, secret)?; let key = hkdf.expand_key(&prk, INFO_KEY, KeyMechanism::Aead(cfg.aead()))?; let iv = hkdf.expand_data(&prk, INFO_NONCE, cfg.aead().n_n())?; @@ -250,9 +256,15 @@ pub struct ServerResponse { #[cfg(feature = "server")] impl ServerResponse { - fn new(hpke: &HpkeR, enc: Vec) -> Res { + fn new(hpke: &HpkeR, enc: &[u8]) -> Res { let response_nonce = random(entropy(hpke.config())); - let aead = make_aead(Mode::Encrypt, hpke.config(), hpke, enc, &response_nonce)?; + let aead = make_aead( + Mode::Encrypt, + hpke.config(), + &export_secret(hpke, LABEL_RESPONSE, hpke.config())?, + enc, + &response_nonce, + )?; Ok(Self { response_nonce, aead, @@ -302,48 +314,54 @@ impl ClientResponse { let mut aead = make_aead( Mode::Decrypt, self.hpke.config(), - &self.hpke, - self.enc, + &export_secret(&self.hpke, LABEL_RESPONSE, self.hpke.config())?, + &self.enc, response_nonce, )?; - aead.open(&[], 0, ct) // 0 is the sequence number + aead.open(&[], ct) // 0 is the sequence number } } #[cfg(all(test, feature = "client", feature = "server"))] mod test { + use std::{fmt::Debug, io::ErrorKind}; + + use log::trace; + use crate::{ config::SymmetricSuite, err::Res, hpke::{Aead, Kdf, Kem}, ClientRequest, Error, KeyConfig, KeyId, Server, }; - use log::trace; - use std::{fmt::Debug, io::ErrorKind}; - const KEY_ID: KeyId = 1; - const KEM: Kem = Kem::X25519Sha256; - const SYMMETRIC: &[SymmetricSuite] = &[ + pub const KEY_ID: KeyId = 1; + pub const KEM: Kem = Kem::X25519Sha256; + pub const SYMMETRIC: &[SymmetricSuite] = &[ SymmetricSuite::new(Kdf::HkdfSha256, Aead::Aes128Gcm), SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305), ]; - const REQUEST: &[u8] = &[ + pub 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, ]; - const RESPONSE: &[u8] = &[0x01, 0x40, 0xc8]; + pub const RESPONSE: &[u8] = &[0x01, 0x40, 0xc8]; - fn init() { + pub fn init() { crate::init(); _ = env_logger::try_init(); // ignore errors here } + pub fn make_config() -> KeyConfig { + KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap() + } + #[test] fn request_response() { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); trace!("Config: {}", hex::encode(&encoded_config)); @@ -368,7 +386,7 @@ mod test { fn two_requests() { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); @@ -408,7 +426,7 @@ mod test { fn request_truncated(cut: usize) { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); @@ -439,7 +457,7 @@ mod test { fn response_truncated(cut: usize) { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); @@ -498,7 +516,7 @@ mod test { fn request_from_config_list() { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); diff --git a/ohttp/src/nss/aead.rs b/ohttp/src/nss/aead.rs index 7841861..966f461 100644 --- a/ohttp/src/nss/aead.rs +++ b/ohttp/src/nss/aead.rs @@ -18,6 +18,7 @@ use super::{ }, }; use crate::{ + crypto::{Decrypt, Encrypt}, err::{Error, Res}, hpke::Aead as AeadId, }; @@ -69,8 +70,11 @@ impl Mode { /// This is an AEAD instance that uses the pub struct Aead { mode: Mode, + #[allow(dead_code)] + algorithm: AeadId, ctx: Context, nonce_base: [u8; NONCE_LEN], + decrypt_counter: SequenceNumber, } impl Aead { @@ -120,15 +124,72 @@ impl Aead { }; Ok(Self { mode, + algorithm, ctx: Context::from_ptr(ptr)?, nonce_base, + decrypt_counter: 0, }) } - pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { + fn do_open(ctx: &Context, aad: &[u8], ct: &[u8], nonce: &mut [u8], mech: u32) -> Res> { + let mut pt = vec![0; ct.len()]; // NSS needs more space than it uses for plaintext. + let mut pt_len: c_int = 0; + let pt_expected = ct.len().checked_sub(TAG_LEN).ok_or(Error::Truncated)?; + secstatus_to_res(unsafe { + PK11_AEADOp( + **ctx, + CK_GENERATOR_FUNCTION::from(mech), + c_int_len(NONCE_LEN - COUNTER_LEN), // Fixed portion of the nonce. + nonce.as_mut_ptr(), + c_int_len(nonce.len()), + aad.as_ptr(), + c_int_len(aad.len()), + pt.as_mut_ptr(), + &raw mut pt_len, + c_int_len(pt.len()), // signed :( + ct.as_ptr().add(pt_expected).cast_mut(), // const cast :( + c_int_len(TAG_LEN), + ct.as_ptr(), + c_int_len(pt_expected), + ) + })?; + let len = usize::try_from(pt_len).unwrap(); + debug_assert_eq!(len, pt_expected); + pt.truncate(len); + Ok(pt) + } + + pub fn open_seq(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res> { + assert_eq!(self.mode, Mode::Decrypt); + let mut nonce = self.nonce_base; + for (i, n) in nonce.iter_mut().rev().take(COUNTER_LEN).enumerate() { + *n ^= u8::try_from((seq >> (8 * i)) & 0xff).unwrap(); + } + + Self::do_open(&self.ctx, aad, ct, &mut nonce, CKG_NO_GENERATE) + } +} + +impl Decrypt for Aead { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res> { + assert_eq!(self.mode, Mode::Decrypt); + // Note: NSS doesn't do any nonce generation for decryption, which is CRAZY. + let counter = self.decrypt_counter; + self.decrypt_counter += 1; + self.open_seq(aad, counter, ct) + } + + fn alg(&self) -> AeadId { + self.algorithm + } +} + +impl Encrypt for Aead { + fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { assert_eq!(self.mode, Mode::Encrypt); // A copy for the nonce generator to write into. But we don't use the value. let mut nonce = self.nonce_base; + // Ciphertext with enough space for the tag. // Even though we give the operation a separate buffer for the tag, // reserve the capacity on allocation. @@ -159,37 +220,8 @@ impl Aead { Ok(ct) } - pub fn open(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res> { - assert_eq!(self.mode, Mode::Decrypt); - let mut nonce = self.nonce_base; - for (i, n) in nonce.iter_mut().rev().take(COUNTER_LEN).enumerate() { - *n ^= u8::try_from((seq >> (8 * i)) & 0xff).unwrap(); - } - let mut pt = vec![0; ct.len()]; // NSS needs more space than it uses for plaintext. - let mut pt_len: c_int = 0; - let pt_expected = ct.len().checked_sub(TAG_LEN).ok_or(Error::Truncated)?; - secstatus_to_res(unsafe { - PK11_AEADOp( - *self.ctx, - CK_GENERATOR_FUNCTION::from(CKG_NO_GENERATE), - c_int_len(NONCE_LEN - COUNTER_LEN), // Fixed portion of the nonce. - nonce.as_mut_ptr(), - c_int_len(nonce.len()), - aad.as_ptr(), - c_int_len(aad.len()), - pt.as_mut_ptr(), - &raw mut pt_len, - c_int_len(pt.len()), // signed :( - ct.as_ptr().add(pt_expected).cast_mut(), // const cast :( - c_int_len(TAG_LEN), - ct.as_ptr(), - c_int_len(pt_expected), - ) - })?; - let len = usize::try_from(pt_len).unwrap(); - debug_assert_eq!(len, pt_expected); - pt.truncate(len); - Ok(pt) + fn alg(&self) -> AeadId { + self.algorithm } } @@ -199,6 +231,7 @@ mod test { super::{super::hpke::Aead as AeadId, init}, Aead, Mode, SequenceNumber, NONCE_LEN, }; + use crate::crypto::{Decrypt, Encrypt}; /// Check that the first invocation of encryption matches expected values. /// Also check decryption of the same. @@ -218,7 +251,7 @@ mod test { assert_eq!(&ciphertext[..], ct); let mut dec = Aead::new(Mode::Decrypt, algorithm, &k, *nonce).unwrap(); - let plaintext = dec.open(aad, 0, ct).unwrap(); + let plaintext = dec.open(aad, ct).unwrap(); assert_eq!(&plaintext[..], pt); } @@ -233,7 +266,11 @@ mod test { ) { let k = Aead::import_key(algorithm, key).unwrap(); let mut dec = Aead::new(Mode::Decrypt, algorithm, &k, *nonce).unwrap(); - let plaintext = dec.open(aad, seq, ct).unwrap(); + let plaintext = if seq == 0 { + dec.open(aad, ct).unwrap() + } else { + dec.open_seq(aad, seq, ct).unwrap() + }; assert_eq!(&plaintext[..], pt); } @@ -330,4 +367,26 @@ mod test { // Now use the real nonce and sequence number from the example. decrypt(ALG, KEY, NONCE_BASE, 654_360_564, AAD, PT, CT); } + + #[test] + fn seal_open_many() { + const PT: &[u8] = b"abc"; + const ALG: AeadId = AeadId::Aes128Gcm; + const KEY: &[u8] = &[0; 16]; + const NONCE: [u8; NONCE_LEN] = [0; NONCE_LEN]; + + init(); + let k = Aead::import_key(ALG, KEY).unwrap(); + + let mut e = Aead::new(Mode::Encrypt, ALG, &k, NONCE).unwrap(); + let mut d = Aead::new(Mode::Decrypt, ALG, &k, NONCE).unwrap(); + + for i in 1..5 { + let ct = e.seal(&[], PT).unwrap(); + println!("ct{i}: {}", hex::encode(&ct)); + + let pt = d.open(&[], &ct).unwrap(); + assert_eq!(pt, PT); + } + } } diff --git a/ohttp/src/nss/err.rs b/ohttp/src/nss/err.rs index af85066..bc7c86d 100644 --- a/ohttp/src/nss/err.rs +++ b/ohttp/src/nss/err.rs @@ -10,9 +10,10 @@ clippy::module_name_repetitions )] +use std::os::raw::c_char; + use super::{SECStatus, SECSuccess}; use crate::err::Res; -use std::os::raw::c_char; include!(concat!(env!("OUT_DIR"), "/nspr_error.rs")); mod codes { diff --git a/ohttp/src/nss/hkdf.rs b/ohttp/src/nss/hkdf.rs index a142f39..835a034 100644 --- a/ohttp/src/nss/hkdf.rs +++ b/ohttp/src/nss/hkdf.rs @@ -1,3 +1,7 @@ +use std::{convert::TryFrom, os::raw::c_int, ptr::null_mut}; + +use log::trace; + use super::{ super::hpke::{Aead, Kdf}, p11::{ @@ -10,8 +14,6 @@ use super::{ }, }; use crate::err::Res; -use log::trace; -use std::{convert::TryFrom, os::raw::c_int, ptr::null_mut}; #[derive(Clone, Copy)] pub enum KeyMechanism { diff --git a/ohttp/src/nss/hpke.rs b/ohttp/src/nss/hpke.rs index 0d8deaf..09e6f88 100644 --- a/ohttp/src/nss/hpke.rs +++ b/ohttp/src/nss/hpke.rs @@ -13,7 +13,10 @@ use super::{ err::{sec::SEC_ERROR_INVALID_ARGS, secstatus_to_res, Error}, p11::{sys, Item, PrivateKey, PublicKey, Slot, SymKey}, }; -use crate::err::Res; +use crate::{ + crypto::{Decrypt, Encrypt}, + err::Res, +}; /// Configuration for `Hpke`. #[derive(Clone, Copy)] @@ -135,8 +138,10 @@ impl HpkeS { let slc = unsafe { std::slice::from_raw_parts(r.data, len) }; Ok(Vec::from(slc)) } +} - pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { +impl Encrypt for HpkeS { + fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { let mut out: *mut sys::SECItem = null_mut(); secstatus_to_res(unsafe { sys::PK11_HPKE_Seal( @@ -149,6 +154,10 @@ impl HpkeS { let v = Item::from_ptr(out)?; Ok(unsafe { v.into_vec() }) } + + fn alg(&self) -> Aead { + self.config.aead() + } } impl Exporter for HpkeS { @@ -214,8 +223,10 @@ impl HpkeR { })?; PublicKey::from_ptr(ptr) } +} - pub fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res> { +impl Decrypt for HpkeR { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res> { let mut out: *mut sys::SECItem = null_mut(); secstatus_to_res(unsafe { sys::PK11_HPKE_Open( @@ -228,6 +239,10 @@ impl HpkeR { let v = Item::from_ptr(out)?; Ok(unsafe { v.into_vec() }) } + + fn alg(&self) -> Aead { + self.config.aead() + } } impl Exporter for HpkeR { @@ -304,7 +319,11 @@ pub fn generate_key_pair(kem: Kem) -> Res<(PrivateKey, PublicKey)> { #[cfg(test)] mod test { use super::{generate_key_pair, Config, HpkeContext, HpkeR, HpkeS}; - use crate::{hpke::Aead, init}; + use crate::{ + crypto::{Decrypt, Encrypt}, + hpke::Aead, + init, + }; const INFO: &[u8] = b"info"; const AAD: &[u8] = b"aad"; diff --git a/ohttp/src/nss/mod.rs b/ohttp/src/nss/mod.rs index 0e70184..91dc44e 100644 --- a/ohttp/src/nss/mod.rs +++ b/ohttp/src/nss/mod.rs @@ -1,8 +1,4 @@ -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. +#![allow(clippy::incompatible_msrv)] // This feature needs 1.70 mod err; #[macro_use] @@ -11,10 +7,12 @@ pub mod aead; pub mod hkdf; pub mod hpke; -pub use self::p11::{random, PrivateKey, PublicKey}; +use std::{ptr::null, sync::OnceLock}; + use err::secstatus_to_res; pub use err::Error; -use std::{ptr::null, sync::OnceLock}; + +pub use self::p11::{random, PrivateKey, PublicKey, SymKey}; #[allow(clippy::pedantic, non_upper_case_globals, clippy::upper_case_acronyms)] mod nss_init { diff --git a/ohttp/src/nss/p11.rs b/ohttp/src/nss/p11.rs index 6f1978a..471e5d7 100644 --- a/ohttp/src/nss/p11.rs +++ b/ohttp/src/nss/p11.rs @@ -244,7 +244,7 @@ impl<'a, T: Sized + 'a> ParamItem<'a, T> { } pub fn ptr(&mut self) -> *mut SECItem { - std::ptr::addr_of_mut!(self.item) + ptr::addr_of_mut!(self.item) } } diff --git a/ohttp/src/rh/aead.rs b/ohttp/src/rh/aead.rs index 733e4cf..62b6bbb 100644 --- a/ohttp/src/rh/aead.rs +++ b/ohttp/src/rh/aead.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] // TODO: remove - use std::convert::TryFrom; use aead::{AeadMut, Key, KeyInit, Nonce, Payload}; @@ -7,12 +5,15 @@ use aes_gcm::{Aes128Gcm, Aes256Gcm}; use chacha20poly1305::ChaCha20Poly1305; use super::SymKey; -use crate::{err::Res, hpke::Aead as AeadId}; +use crate::{ + crypto::{Decrypt, Encrypt}, + err::Res, + hpke::Aead as AeadId, +}; /// All the nonces are the same length. Exploit that. pub const NONCE_LEN: usize = 12; const COUNTER_LEN: usize = 8; -const TAG_LEN: usize = 16; type SequenceNumber = u64; @@ -56,6 +57,8 @@ impl AeadEngine { /// A switch-hitting AEAD that uses a selected primitive. pub struct Aead { mode: Mode, + #[allow(dead_code, reason = "Used by stream feature")] + algorithm: AeadId, engine: AeadEngine, nonce_base: [u8; NONCE_LEN], seq: SequenceNumber, @@ -82,6 +85,7 @@ impl Aead { }; Ok(Self { mode, + algorithm, engine: aead, nonce_base, seq: 0, @@ -102,7 +106,28 @@ impl Aead { nonce } - pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { + pub fn open_seq(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res> { + assert_eq!(self.mode, Mode::Decrypt); + let nonce = self.nonce(seq); + let pt = self.engine.decrypt(&nonce, Payload { msg: ct, aad })?; + Ok(pt) + } +} + +impl Decrypt for Aead { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res> { + let res = self.open_seq(aad, self.seq, ct); + self.seq += 1; + res + } + + fn alg(&self) -> AeadId { + self.algorithm + } +} + +impl Encrypt for Aead { + fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { assert_eq!(self.mode, Mode::Encrypt); // A copy for the nonce generator to write into. But we don't use the value. let nonce = self.nonce(self.seq); @@ -111,19 +136,18 @@ impl Aead { Ok(ct) } - pub fn open(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res> { - assert_eq!(self.mode, Mode::Decrypt); - let nonce = self.nonce(seq); - let pt = self.engine.decrypt(&nonce, Payload { msg: ct, aad })?; - Ok(pt) + fn alg(&self) -> AeadId { + self.algorithm } } #[cfg(test)] mod test { - use super::{ - super::super::{hpke::Aead as AeadId, init}, - Aead, Mode, SequenceNumber, NONCE_LEN, + use super::SequenceNumber; + use crate::{ + crypto::{Decrypt, Encrypt}, + hpke::Aead as AeadId, + init, Aead, Mode, NONCE_LEN, }; /// Check that the first invocation of encryption matches expected values. @@ -144,7 +168,7 @@ mod test { assert_eq!(&ciphertext[..], ct); let mut dec = Aead::new(Mode::Decrypt, algorithm, &k, *nonce).unwrap(); - let plaintext = dec.open(aad, 0, ct).unwrap(); + let plaintext = dec.open(aad, ct).unwrap(); assert_eq!(&plaintext[..], pt); } @@ -159,7 +183,7 @@ mod test { ) { let k = Aead::import_key(algorithm, key).unwrap(); let mut dec = Aead::new(Mode::Decrypt, algorithm, &k, *nonce).unwrap(); - let plaintext = dec.open(aad, seq, ct).unwrap(); + let plaintext = dec.open_seq(aad, seq, ct).unwrap(); assert_eq!(&plaintext[..], pt); } @@ -256,4 +280,26 @@ mod test { // Now use the real nonce and sequence number from the example. decrypt(ALG, KEY, NONCE_BASE, 654_360_564, AAD, PT, CT); } + + #[test] + fn seal_open_many() { + const PT: &[u8] = b"abc"; + const ALG: AeadId = AeadId::Aes128Gcm; + const KEY: &[u8] = &[0; 16]; + const NONCE: [u8; NONCE_LEN] = [0; NONCE_LEN]; + + init(); + let k = Aead::import_key(ALG, KEY).unwrap(); + + let mut e = Aead::new(Mode::Encrypt, ALG, &k, NONCE).unwrap(); + let mut d = Aead::new(Mode::Decrypt, ALG, &k, NONCE).unwrap(); + + for i in 1..5 { + let ct = e.seal(&[], PT).unwrap(); + println!("ct{i}: {}", hex::encode(&ct)); + + let pt = d.open(&[], &ct).unwrap(); + assert_eq!(pt, PT); + } + } } diff --git a/ohttp/src/rh/hkdf.rs b/ohttp/src/rh/hkdf.rs index aeb3a8d..a88f60e 100644 --- a/ohttp/src/rh/hkdf.rs +++ b/ohttp/src/rh/hkdf.rs @@ -1,13 +1,14 @@ #![allow(dead_code)] // TODO: remove +use hkdf::Hkdf as HkdfImpl; +use log::trace; +use sha2::{Sha256, Sha384, Sha512}; + use super::SymKey; use crate::{ err::{Error, Res}, hpke::{Aead, Kdf}, }; -use hkdf::Hkdf as HkdfImpl; -use log::trace; -use sha2::{Sha256, Sha384, Sha512}; #[derive(Clone, Copy)] pub enum KeyMechanism { diff --git a/ohttp/src/rh/hpke.rs b/ohttp/src/rh/hpke.rs index 6ba7d9a..e0d3b3e 100644 --- a/ohttp/src/rh/hpke.rs +++ b/ohttp/src/rh/hpke.rs @@ -12,6 +12,7 @@ use rust_hpke::{ use super::SymKey; use crate::{ + crypto::{Decrypt, Encrypt}, hpke::{Aead, Kdf, Kem}, Error, Res, }; @@ -248,13 +249,19 @@ impl HpkeS { pub fn enc(&self) -> Res> { Ok(self.enc.clone()) } +} - pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { +impl Encrypt for HpkeS { + fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { let mut buf = pt.to_owned(); let mut tag = self.context.seal(&mut buf, aad)?; buf.append(&mut tag); Ok(buf) } + + fn alg(&self) -> Aead { + self.config.aead() + } } impl Exporter for HpkeS { @@ -417,13 +424,19 @@ impl HpkeR { } }) } +} - pub fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res> { +impl Decrypt for HpkeR { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res> { let mut buf = ct.to_owned(); let pt_len = self.context.open(&mut buf, aad)?.len(); buf.truncate(pt_len); Ok(buf) } + + fn alg(&self) -> Aead { + self.config.aead() + } } impl Exporter for HpkeR { @@ -471,6 +484,7 @@ pub fn derive_key_pair(kem: Kem, ikm: &[u8]) -> Res<(PrivateKey, PublicKey)> { mod test { use super::{generate_key_pair, Config, HpkeR, HpkeS}; use crate::{ + crypto::{Decrypt, Encrypt}, hpke::{Aead, Kem}, init, }; diff --git a/ohttp/src/stream.rs b/ohttp/src/stream.rs new file mode 100644 index 0000000..825bb01 --- /dev/null +++ b/ohttp/src/stream.rs @@ -0,0 +1,1109 @@ +#![allow(clippy::incompatible_msrv)] // Until I can make MSRV conditional on feature choice. + +use std::{ + cmp::min, + io::{Cursor, Error as IoError, Result as IoResult}, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use futures::{AsyncRead, AsyncWrite}; +use pin_project::pin_project; + +use crate::{ + build_info, + crypto::{Decrypt, Encrypt}, + entropy, + err::Res, + export_secret, make_aead, random, Aead, Error, HpkeConfig, HpkeR, HpkeS, KeyConfig, KeyId, + Mode, PublicKey, SymKey, REQUEST_HEADER_LEN, +}; + +/// The info string for a chunked request. +pub(crate) const INFO_REQUEST: &[u8] = b"message/bhttp chunked request"; +/// The exporter label for a chunked response. +pub(crate) const LABEL_RESPONSE: &[u8] = b"message/bhttp chunked response"; +/// The length of the plaintext of the largest chunk that is permitted. +const MAX_CHUNK_PLAINTEXT: usize = 1 << 14; +const CHUNK_AAD: &[u8] = b""; +const FINAL_CHUNK_AAD: &[u8] = b"final"; + +#[allow(clippy::unnecessary_wraps)] +fn ioerror(e: E) -> Poll> +where + Error: From, +{ + Poll::Ready(Err(IoError::other(Error::from(e)))) +} + +#[pin_project(project = ChunkWriterProjection)] +struct ChunkWriter { + #[pin] + dst: D, + cipher: E, + buf: Vec, + closed: bool, +} + +impl ChunkWriter { + fn write_len(w: &mut [u8], len: usize) -> &[u8] { + let v: u64 = len.try_into().unwrap(); + let (v, len) = match () { + () if v < (1 << 6) => (v, 1), + () if v < (1 << 14) => (v | (1 << 14), 2), + () if v < (1 << 30) => (v | (2 << 30), 4), + () if v < (1 << 62) => (v | (3 << 62), 8), + () => panic!("varint value too large"), + }; + w[..len].copy_from_slice(&v.to_be_bytes()[(8 - len)..]); + &w[..len] + } +} + +impl ChunkWriter { + /// Flush our buffer. + /// Returns `Poll::Pending` when blocked, + /// `Poll::Ready(Ok(()))` when flushed, + /// and `Poll::Ready(Err(..))` when it encounters an error. + fn flush( + this: &mut ChunkWriterProjection<'_, D, C>, + cx: &mut Context<'_>, + ) -> Poll> { + if this.buf.is_empty() { + return Poll::Ready(Ok(())); + } + loop { + match this.dst.as_mut().poll_write(cx, &this.buf[..]) { + Poll::Pending => return Poll::Pending, + Poll::Ready(Ok(len)) => { + if len < this.buf.len() { + // We've written something to the underlying writer, + // which is probably blocked. + // We could return `Poll::Pending`, + // but that would mean taking responsibility + // for calling `cx.waker().wake()` + // when more space comes available. + // + // So, rather than do that, loop. + // If the underlying writer is truly blocked, + // it assumes responsibility for waking the task. + *this.buf = this.buf.split_off(len); + } else { + this.buf.clear(); + return Poll::Ready(Ok(())); + } + } + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + } + } + } + + fn write_chunk( + this: &mut ChunkWriterProjection<'_, D, C>, + cx: &mut Context<'_>, + input: &[u8], + last: bool, + ) -> IoResult { + let aad = if last { FINAL_CHUNK_AAD } else { CHUNK_AAD }; + let mut ct = this.cipher.seal(aad, input).map_err(IoError::other)?; + let (len, written) = if last { + (0, 0) + } else { + (ct.len(), input.len()) + }; + + let mut len_buf = [0; 8]; + let len = Self::write_len(&mut len_buf[..], len); + let w = match this.dst.as_mut().poll_write(cx, len) { + Poll::Pending => 0, + Poll::Ready(Ok(w)) => w, + Poll::Ready(e @ Err(_)) => return e, + }; + + if w < len.len() { + this.buf.extend_from_slice(&len[w..]); + this.buf.append(&mut ct); + } else { + match this.dst.as_mut().poll_write(cx, &ct[..]) { + Poll::Pending => { + *this.buf = ct; + } + Poll::Ready(Ok(w)) => { + *this.buf = ct.split_off(w); + } + Poll::Ready(e @ Err(_)) => return e, + } + } + Ok(written) + } +} + +impl AsyncWrite for ChunkWriter { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + input: &[u8], + ) -> Poll> { + let mut this = self.project(); + if *this.closed { + return ioerror(Error::WriteAfterClose); + } + + // We have buffered data, so dump it into the output directly. + let flushed = Self::flush(&mut this, cx); + if matches!(flushed, Poll::Pending | Poll::Ready(Err(_))) { + return flushed.map(|_| unreachable!()); + } + + // Now encipher a chunk. + let len = min(input.len(), MAX_CHUNK_PLAINTEXT); + Poll::Ready(Self::write_chunk(&mut this, cx, &input[..len], false)) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + let flushed = Self::flush(&mut this, cx); + if matches!(flushed, Poll::Pending | Poll::Ready(Err(_))) { + flushed.map(|_| unreachable!()) + } else { + this.dst.as_mut().poll_flush(cx) + } + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + let flushed = Self::flush(&mut this, cx); + if matches!(flushed, Poll::Pending | Poll::Ready(Err(_))) { + return flushed; + } + + if !*this.closed { + *this.closed = true; + if let Err(e) = Self::write_chunk(&mut this, cx, &[], true) { + return Poll::Ready(Err(e)); + } + // `write_chunk` might have buffered some data after being blocked. + // We have to try to write that out here (see `flush()` for details). + let flushed = Self::flush(&mut this, cx); + if matches!(flushed, Poll::Pending | Poll::Ready(Err(_))) { + return flushed; + } + } + this.dst.as_mut().poll_close(cx) + } +} + +#[pin_project(project = ClientProjection)] +pub struct ClientRequest { + #[pin] + writer: ChunkWriter, +} + +impl ClientRequest { + /// Start the processing of a stream. + pub fn start(dst: D, config: HpkeConfig, key_id: KeyId, mut pk: PublicKey) -> Res { + let info = build_info(INFO_REQUEST, key_id, config)?; + let hpke = HpkeS::new(config, &mut pk, &info)?; + + let mut header = Vec::from(&info[INFO_REQUEST.len() + 1..]); + debug_assert_eq!(header.len(), REQUEST_HEADER_LEN); + + let mut e = hpke.enc()?; + header.append(&mut e); + + Ok(Self { + writer: ChunkWriter { + dst, + cipher: hpke, + buf: header, + closed: false, + }, + }) + } + + /// Get an object that can be used to process the response. + /// + /// While this can be used while sending the request, + /// doing so creates a risk of revealing unwanted information to the gateway. + /// That includes the round trip time between client and gateway, + /// which might reveal information about the location of the client. + pub fn response(&self, src: R) -> Res> { + let enc = self.writer.cipher.enc()?; + let secret = export_secret( + &self.writer.cipher, + LABEL_RESPONSE, + self.writer.cipher.config(), + )?; + Ok(ClientResponse { + src, + config: self.writer.cipher.config(), + state: ClientResponseState::Header { + enc, + secret, + nonce: [0; 16], + read: 0, + }, + }) + } +} + +impl AsyncWrite for ClientRequest { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + input: &[u8], + ) -> Poll> { + self.project().writer.as_mut().poll_write(cx, input) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().writer.as_mut().poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().writer.as_mut().poll_close(cx) + } +} + +enum ChunkReader { + Length { + len: [u8; 8], + offset: usize, + }, + EncryptedData { + buf: Vec, + offset: usize, + length: usize, + }, + CleartextData { + buf: Vec, + offset: usize, + last: bool, + }, + Done, +} + +impl ChunkReader { + fn length() -> Self { + Self::Length { + len: [0; 8], + offset: 0, + } + } + + fn data(length: usize) -> Self { + // Avoid use `with_capacity` here. Only allocate when necessary. + // We might be able to into the buffer we're given instead, to save an allocation. + Self::EncryptedData { + buf: Vec::new(), + // Note that because we're allocating the full chunk, + // we need to track what has been used. + offset: 0, + length, + } + } + + fn read_fixed( + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + buf: &mut [u8], + offset: &mut usize, + ) -> Option>> { + while *offset < buf.len() { + // Read any remaining bytes of the length. + match src.as_mut().poll_read(cx, &mut buf[*offset..]) { + Poll::Pending => return Some(Poll::Pending), + Poll::Ready(Ok(0)) => { + return Some(ioerror(Error::Truncated)); + } + Poll::Ready(Ok(r)) => { + *offset += r; + } + e @ Poll::Ready(Err(_)) => return Some(e), + } + } + None + } + + fn read_length0( + &mut self, + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + ) -> Option>> { + let Self::Length { len, offset } = self else { + return None; + }; + + let res = Self::read_fixed(src.as_mut(), cx, &mut len[..1], offset); + if res.is_some() { + return res; + } + + let form = len[0] >> 6; + if form == 0 { + *self = Self::data(usize::from(len[0])); + } else { + let v = mem::replace(&mut len[0], 0) & 0x3f; + let i = match form { + 1 => 6, + 2 => 4, + 3 => 0, + _ => unreachable!(), + }; + len[i] = v; + *offset = i + 1; + } + None + } + + fn read_length( + &mut self, + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + aead: &mut C, + ) -> Option>> { + // Read the first byte. + let res = self.read_length0(src.as_mut(), cx); + if res.is_some() { + return res; + } + + let Self::Length { len, offset } = self else { + return None; + }; + + let res = Self::read_fixed(src.as_mut(), cx, &mut len[..], offset); + if res.is_some() { + return res; + } + + let remaining = match usize::try_from(u64::from_be_bytes(*len)) { + Ok(remaining) => remaining, + Err(e) => return Some(ioerror(e)), + }; + if remaining > MAX_CHUNK_PLAINTEXT + aead.alg().n_t() { + return Some(ioerror(Error::ChunkTooLarge)); + } + + *self = Self::data(remaining); + None + } + + /// Optional optimization that reads a single chunk into the output buffer. + fn read_into_output( + &mut self, + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + aead: &mut C, + output: &mut [u8], + ) -> Option>> { + let Self::EncryptedData { + buf, + offset, + length, + } = self + else { + return None; + }; + if *length == 0 || *offset > 0 || output.len() < *length { + // We need to pull in a complete chunk in one go for this to be worthwhile. + return None; + } + + match src.as_mut().poll_read(cx, &mut output[..*length]) { + Poll::Pending => Some(Poll::Pending), + Poll::Ready(Ok(0)) => Some(ioerror(Error::Truncated)), + Poll::Ready(Ok(r)) => { + if r == *length { + let pt = match aead.open(CHUNK_AAD, &output[..r]) { + Ok(pt) => pt, + Err(e) => return Some(ioerror(e)), + }; + output[..pt.len()].copy_from_slice(&pt); + *self = Self::length(); + Some(Poll::Ready(Ok(pt.len()))) + } else { + buf.reserve_exact(*length); + buf.extend_from_slice(&output[..r]); + buf.resize(*length, 0); + *offset += r; + None + } + } + e @ Poll::Ready(Err(_)) => Some(e), + } + } + + /// Provide any decrypted cleartext that we were unable to deliver + /// on previous calls to `read()`. + fn deliver_cleartext(&mut self, output: &mut [u8]) -> Option { + let Self::CleartextData { buf, offset, last } = self else { + return None; + }; + + if *offset + output.len() < buf.len() { + // `output` is too small for the chunk, fill it and update the offset. + output.copy_from_slice(&buf[*offset..*offset + output.len()]); + *offset += output.len(); + Some(output.len()) + } else { + // Deliver what we have remaining. + // + // Note that this could, by using a different return status, + // allow `read()` function to continue reading. + // However, that complicates the code more than is really worth it. + let len = buf.len() - *offset; + output[..len].copy_from_slice(&buf[*offset..]); + if *last { + *self = Self::Done; + } else { + *self = Self::length(); + } + Some(len) + } + } + + fn read( + &mut self, + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + cipher: &mut C, + output: &mut [u8], + ) -> Poll> { + if let Some(delivered) = self.deliver_cleartext(output) { + return Poll::Ready(Ok(delivered)); + } + + while !matches!(self, Self::Done) { + if let Some(res) = self.read_length(src.as_mut(), cx, cipher) { + return res; + } + + // Read data. + if let Some(res) = self.read_into_output(src.as_mut(), cx, cipher, output) { + return res; + } + + let Self::EncryptedData { + buf, + offset, + length, + } = self + else { + unreachable!(); + }; + + // Allocate now as needed. + let last = *length == 0; + if buf.is_empty() { + let sz = if last { + MAX_CHUNK_PLAINTEXT + cipher.alg().n_t() + } else { + *length + }; + buf.resize(sz, 0); + } + + match src.as_mut().poll_read(cx, &mut buf[*offset..]) { + Poll::Pending => return Poll::Pending, + Poll::Ready(Ok(0)) => { + if last { + buf.truncate(*offset); + } else { + return ioerror(Error::Truncated); + } + } + Poll::Ready(Ok(r)) => { + *offset += r; + if last || *offset < *length { + continue; // Keep reading + } + } + e @ Poll::Ready(Err(_)) => return e, + } + + let aad = if last { FINAL_CHUNK_AAD } else { CHUNK_AAD }; + let pt = cipher.open(aad, buf).map_err(IoError::other)?; + + let delivered = if pt.len() > output.len() { + output.copy_from_slice(&pt[..output.len()]); + // Buffer any undelivered cleartext data. + *self = Self::CleartextData { + buf: pt[output.len()..].to_vec(), + offset: 0, + last, + }; + output.len() + } else { + output[..pt.len()].copy_from_slice(&pt); + if last { + *self = Self::Done; + } else { + *self = Self::length(); + if pt.is_empty() { + // We can't return zero length, as that means "end of stream". + // So read the next chunk if this one was empty. + continue; + } + } + pt.len() + }; + return Poll::Ready(Ok(delivered)); + } + + Poll::Ready(Ok(0)) + } +} + +enum ServerRequestState { + HpkeConfig { + buf: [u8; 7], + read: usize, + }, + Enc { + config: HpkeConfig, + info: Vec, + read: usize, + }, + Body { + hpke: HpkeR, + state: ChunkReader, + }, +} + +#[pin_project(project = ServerRequestProjection)] +pub struct ServerRequest { + #[pin] + src: S, + key_config: KeyConfig, + enc: Vec, + state: ServerRequestState, +} + +impl ServerRequest { + pub fn new(key_config: KeyConfig, src: S) -> Self { + Self { + src, + key_config, + enc: Vec::new(), + state: ServerRequestState::HpkeConfig { + buf: [0; 7], + read: 0, + }, + } + } + + /// Get a response that wraps the given async write instance. + /// This fails with an error if the request header hasn't been processed. + /// This condition is not exposed through a future anywhere, + /// but you can wait for the first byte of data. + pub fn response(&self, dst: D) -> Res> { + let ServerRequestState::Body { hpke, state: _ } = &self.state else { + return Err(Error::NotReady); + }; + + let response_nonce = random(entropy(hpke.config())); + let aead = make_aead( + Mode::Encrypt, + hpke.config(), + &export_secret(hpke, LABEL_RESPONSE, hpke.config())?, + &self.enc, + &response_nonce, + )?; + Ok(ServerResponse { + writer: ChunkWriter { + dst, + cipher: aead, + buf: response_nonce, + closed: false, + }, + }) + } +} + +impl ServerRequest { + fn read_config( + this: &mut ServerRequestProjection<'_, S>, + cx: &mut Context<'_>, + ) -> Option>> { + let ServerRequestState::HpkeConfig { buf, read } = this.state else { + return None; + }; + + let res = ChunkReader::read_fixed(this.src.as_mut(), cx, &mut buf[..], read); + if res.is_some() { + return res; + } + + let config = match this + .key_config + .decode_hpke_config(&mut Cursor::new(&buf[..])) + { + Ok(cfg) => cfg, + Err(e) => return Some(ioerror(e)), + }; + let info = match build_info(INFO_REQUEST, this.key_config.key_id, config) { + Ok(info) => info, + Err(e) => return Some(ioerror(e)), + }; + this.enc.resize(config.kem().n_enc(), 0); + + *this.state = ServerRequestState::Enc { + config, + info, + read: 0, + }; + None + } + + fn read_enc( + this: &mut ServerRequestProjection<'_, S>, + cx: &mut Context<'_>, + ) -> Option>> { + let ServerRequestState::Enc { config, info, read } = this.state else { + return None; + }; + + let res = ChunkReader::read_fixed(this.src.as_mut(), cx, &mut this.enc[..], read); + if res.is_some() { + return res; + } + + let hpke = match HpkeR::new( + *config, + &this.key_config.pk, + this.key_config.sk.as_ref().unwrap(), + this.enc, + info, + ) { + Ok(hpke) => hpke, + Err(e) => return Some(ioerror(e)), + }; + + *this.state = ServerRequestState::Body { + hpke, + state: ChunkReader::length(), + }; + None + } +} + +impl AsyncRead for ServerRequest { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + output: &mut [u8], + ) -> Poll> { + let mut this = self.project(); + if let Some(res) = Self::read_config(&mut this, cx) { + return res; + } + + if let Some(res) = Self::read_enc(&mut this, cx) { + return res; + } + + if let ServerRequestState::Body { hpke, state } = this.state { + state.read(this.src, cx, hpke, output) + } else { + Poll::Ready(Ok(0)) + } + } +} + +#[pin_project(project = ServerResponseProjection)] +pub struct ServerResponse { + #[pin] + writer: ChunkWriter, +} + +impl AsyncWrite for ServerResponse { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + input: &[u8], + ) -> Poll> { + self.project().writer.as_mut().poll_write(cx, input) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().writer.as_mut().poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().writer.as_mut().poll_close(cx) + } +} + +enum ClientResponseState { + Header { + enc: Vec, + secret: SymKey, + nonce: [u8; 16], + read: usize, + }, + Body { + aead: Aead, + state: ChunkReader, + }, +} + +#[pin_project(project = ClientResponseProjection)] +pub struct ClientResponse { + #[pin] + src: S, + config: HpkeConfig, + state: ClientResponseState, +} + +impl ClientResponse { + fn read_nonce( + this: &mut ClientResponseProjection<'_, S>, + cx: &mut Context<'_>, + ) -> Option>> { + let ClientResponseState::Header { + enc, + secret, + nonce, + read, + } = this.state + else { + return None; + }; + + let nonce = &mut nonce[..entropy(*this.config)]; + let res = ChunkReader::read_fixed(this.src.as_mut(), cx, nonce, read); + if res.is_some() { + return res; + } + + let aead = match make_aead(Mode::Decrypt, *this.config, secret, enc, nonce) { + Ok(aead) => aead, + Err(e) => return Some(ioerror(e)), + }; + + *this.state = ClientResponseState::Body { + aead, + state: ChunkReader::length(), + }; + None + } +} + +impl AsyncRead for ClientResponse { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + output: &mut [u8], + ) -> Poll> { + let mut this = self.project(); + if let Some(res) = Self::read_nonce(&mut this, cx) { + return res; + } + + if let ClientResponseState::Body { aead, state } = this.state { + state.read(this.src, cx, aead, output) + } else { + Poll::Ready(Ok(0)) + } + } +} + +#[cfg(test)] +mod test { + use std::{ + io::Result as IoResult, + pin::Pin, + task::{Context, Poll}, + }; + + use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + use log::trace; + use pin_project::pin_project; + use sync_async::{Dribble, Pipe, SplitAt, Stutter, SyncRead, SyncResolve, Unadapt}; + + use crate::{ + test::{init, make_config, REQUEST, RESPONSE}, + ClientRequest, Server, + }; + + #[test] + fn request_response() { + init(); + + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let mut client_request = client.encapsulate_stream(request_write).unwrap(); + client_request.write_all(REQUEST).sync_resolve().unwrap(); + client_request.close().sync_resolve().unwrap(); + + trace!("Request: {}", hex::encode(REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + // The server receives a request. + let mut server_request = server.decapsulate_stream(&enc_request[..]); + assert_eq!(server_request.sync_read_to_end(), REQUEST); + + // The server sends a response. + let (mut response_read, response_write) = Pipe::new(); + let mut server_response = server_request.response(response_write).unwrap(); + server_response.write_all(RESPONSE).sync_resolve().unwrap(); + server_response.close().sync_resolve().unwrap(); + + let enc_response = response_read.sync_read_to_end(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + // The client receives a response. + let mut client_response = client_request.response(&enc_response[..]).unwrap(); + let response_buf = client_response.sync_read_to_end(); + assert_eq!(response_buf, RESPONSE); + trace!("Response: {}", hex::encode(response_buf)); + } + + /// Run the `request_response` test, but do it with streams that are one byte apiece + /// on the output side. + #[test] + fn dribble_out() { + init(); + + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let request_write = Stutter::new(Dribble::new(request_write)); + let mut client_request = client.encapsulate_stream(request_write).unwrap(); + client_request.write_all(REQUEST).sync_resolve().unwrap(); + client_request.close().sync_resolve().unwrap(); + + trace!("Request: {}", hex::encode(REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + // The server receives a request. + let enc_req_stream = Stutter::new(Dribble::new(&enc_request[..])); + let mut server_request = server.decapsulate_stream(enc_req_stream); + assert_eq!(server_request.sync_read_to_end(), REQUEST); + + // The server sends a response. + let (mut response_read, response_write) = Pipe::new(); + let response_write = Stutter::new(Dribble::new(response_write)); + let mut server_response = server_request.response(response_write).unwrap(); + server_response.write_all(RESPONSE).sync_resolve().unwrap(); + server_response.close().sync_resolve().unwrap(); + + let enc_response = response_read.sync_read_to_end(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + // The client receives a response. + let enc_resp_stream = Stutter::new(Dribble::new(&enc_response[..])); + let mut client_response = client_request.response(enc_resp_stream).unwrap(); + let response_buf = client_response.sync_read_to_end(); + assert_eq!(response_buf, RESPONSE); + trace!("Response: {}", hex::encode(response_buf)); + } + + fn write_wrapped(s: S, w: W, data: &[u8]) -> S + where + S: AsyncWrite + AsyncWriteExt + Unpin, + W: FnOnce(S) -> T, + T: AsyncWrite + AsyncWriteExt + Unpin + Unadapt, + { + let mut s = w(s); + s.write_all(data).sync_resolve().unwrap(); + s.close().sync_resolve().unwrap(); + s.unadapt() + } + + fn read_wrapped(s: S, w: W) -> (Vec, S) + where + S: AsyncRead + AsyncReadExt + Unpin, + W: FnOnce(S) -> T, + T: AsyncRead + AsyncReadExt + Unpin + Unadapt, + { + let mut s = w(s); + (s.sync_read_to_end(), s.unadapt()) + } + + /// With each on its own, Stutter and Dribble don't cause the code to do anything differently. + /// You need both in order to effect a change in the way that the streaming code operates. + #[pin_project] + struct StutterDribble { + #[pin] + s: Stutter>, + } + + impl StutterDribble { + fn new(s: S) -> Self { + Self { + s: Stutter::new(Dribble::new(s)), + } + } + } + + impl Unadapt for StutterDribble { + type S = S; + fn unadapt(self) -> Self::S { + self.s.unadapt().unadapt() + } + } + + impl AsyncRead for StutterDribble { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let this = self.project(); + this.s.poll_read(cx, buf) + } + } + + impl AsyncWrite for StutterDribble { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.project().s.poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().s.poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().s.poll_close(cx) + } + } + + /// Run the `request_response` test, but do it with streams that are one byte apiece + /// on the input side. This is the one that produces the most output. + #[test] + fn dribble_in() { + init(); + + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let client_request = client.encapsulate_stream(request_write).unwrap(); + let client_request = write_wrapped(client_request, StutterDribble::new, REQUEST); + + trace!("Request: {}", hex::encode(REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + // The server receives a request. + let enc_req_stream = &enc_request[..]; + let server_request = server.decapsulate_stream(enc_req_stream); + let (request_data, server_request) = read_wrapped(server_request, StutterDribble::new); + assert_eq!(request_data, REQUEST); + + // The server sends a response. + let (mut response_read, response_write) = Pipe::new(); + let server_response = server_request.response(response_write).unwrap(); + _ = write_wrapped(server_response, StutterDribble::new, RESPONSE); + + let enc_response = response_read.sync_read_to_end(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + // The client receives a response. + let client_response = client_request.response(&enc_response[..]).unwrap(); + let (response_data, _) = read_wrapped(client_response, StutterDribble::new); + assert_eq!(response_data, RESPONSE); + trace!("Response: {}", hex::encode(response_data)); + } + + /// Run the `request_response` test, but do it with streams that are one byte apiece + /// on the input side. This is the one that produces the most output. + #[test] + fn split_in() { + init(); + + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let client_request = client.encapsulate_stream(request_write).unwrap(); + let client_request = write_wrapped( + client_request, + |s| SplitAt::new(s, REQUEST.len() / 2), + REQUEST, + ); + + trace!("Request: {}", hex::encode(REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + // The server receives a request. + let enc_req_stream = &enc_request[..]; + let server_request = server.decapsulate_stream(enc_req_stream); + let (request_data, server_request) = read_wrapped(server_request, Stutter::new); + assert_eq!(request_data, REQUEST); + + // The server sends a response. + let (mut response_read, response_write) = Pipe::new(); + let server_response = server_request.response(response_write).unwrap(); + _ = write_wrapped( + server_response, + |s| SplitAt::new(s, RESPONSE.len() / 2), + RESPONSE, + ); + + let enc_response = response_read.sync_read_to_end(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + // The client receives a response. + let client_response = client_request.response(&enc_response[..]).unwrap(); + let (response_data, _) = read_wrapped(client_response, Stutter::new); + assert_eq!(response_data, RESPONSE); + trace!("Response: {}", hex::encode(response_data)); + } + + /// Check that a longer request can be read properly. + /// This checks that any cleartext that doesn't fit in the output buffer + /// is correctly buffered. + #[test] + fn long_request() { + /// A longer request. + const LONG_REQUEST: &[u8] = &[0u8; 1024]; + init(); + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let mut client_request = client.encapsulate_stream(request_write).unwrap(); + client_request + .write_all(LONG_REQUEST) + .sync_resolve() + .unwrap(); + client_request.close().sync_resolve().unwrap(); + trace!("Request: {}", hex::encode(LONG_REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); // The server receives a request. + let mut server_request = server.decapsulate_stream(&enc_request[..]); + assert_eq!(server_request.sync_read_to_end(), LONG_REQUEST); + } +} diff --git a/pre-commit b/pre-commit index 758b923..a0bdddc 100755 --- a/pre-commit +++ b/pre-commit @@ -6,10 +6,12 @@ # $ ln -s ../../hooks/pre-commit .git/hooks/pre-commit root="$(git rev-parse --show-toplevel 2>/dev/null)" +RUST_FMT_CFG="imports_granularity=Crate,group_imports=StdExternalCrate" # Some sanity checking. -hash cargo || exit 1 -[[ -n "$root" ]] || exit 1 +set -e +hash cargo +[[ -n "$root" ]] # Installation. if [[ "$1" == "install" ]]; then @@ -23,31 +25,59 @@ if [[ "$1" == "install" ]]; then exit fi -# Check formatting. +# Stash unstaged changes if [[ "$1" != "all" ]]; then - msg="pre-commit stash @$(git rev-parse --short @) $RANDOM" - trap 'git stash list -1 --format="format:%s" | grep -q "'"$msg"'" && git stash pop -q' EXIT - git stash push -k -u -q -m "$msg" + stashdir="$(mktemp -d "$root"/.pre-commit.stashXXXXXX)" + msg="pre-commit stash @$(git rev-parse --short @) ${stashdir##*.stash}" + gitdir="$(git rev-parse --git-dir 2>/dev/null)" + + stash() { + # Move MERGE_[HEAD|MODE|MSG] files to the root directory, and let `git stash push` save them. + find "$gitdir" -maxdepth 1 -name 'MERGE_*' -exec mv \{\} "$stashdir" \; + git stash push -k -u -q -m "$msg" + } + + unstash() { + git stash list -1 --format="format:%s" | grep -q "$msg" && git stash pop -q + # Moves MERGE files restored by `git stash pop` back into .git/ directory. + if [[ -d "$stashdir" ]]; then + find "$stashdir" -exec mv -n \{\} "$gitdir" \; + rmdir "$stashdir" + fi + } + + trap unstash EXIT + stash fi -if ! errors=($(cargo fmt -- --check --config imports_granularity=crate -l)); then - echo "Formatting errors found." - echo "Run \`cargo fmt\` to fix the following files:" + +# Check formatting +if ! errors=($(cargo fmt -- --check --config "$RUST_FMT_CFG" -l)); then + echo "Formatting errors found in:" for err in "${errors[@]}"; do echo " $err" done + echo "To fix, run \`cargo fmt -- --config $RUST_FMT_CFG\`" exit 1 fi -if ! cargo clippy --tests; then - exit 1 -fi -if ! cargo test; then - exit 1 -fi -if [[ -n "$NSS_DIR" ]]; then - if ! cargo clippy --tests --no-default-features --features nss; then - exit 1 - fi - if ! cargo test --no-default-features --features nss; then + +check() { + msg="$1" + shift + if ! "$@"; then + echo "${msg}: Failed command:" + echo " ${@@Q}" exit 1 fi +} +versions=(stable) +if [[ "$1" == "all" ]]; then + versions+=(1.82.0) fi +for v in "${versions[@]}"; do + check "clippy" cargo "+$v" clippy --tests -F stream + check "test" cargo "+$v" test -F stream + if [[ -n "$NSS_DIR" ]]; then + check "clippy(NSS)" cargo "+$v" clippy --tests --no-default-features --features nss,http,client,server,stream + check "test(NSS)" cargo "+$v" test --no-default-features --features nss,http,client,server,stream + fi +done diff --git a/sync-async/Cargo.toml b/sync-async/Cargo.toml new file mode 100644 index 0000000..7c35dd2 --- /dev/null +++ b/sync-async/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "sync-async" +description = "Synchronous Helpers for Async Code" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +readme.workspace = true + +[dependencies] +futures = "0.3" +pin-project = "1.1" \ No newline at end of file diff --git a/sync-async/src/lib.rs b/sync-async/src/lib.rs new file mode 100644 index 0000000..f4a9826 --- /dev/null +++ b/sync-async/src/lib.rs @@ -0,0 +1,386 @@ +use std::{ + cmp::min, + future::Future, + io::Result as IoResult, + pin::{pin, Pin}, + task::{Context, Poll}, +}; + +use futures::{ + io::{ReadHalf, WriteHalf}, + AsyncRead, AsyncReadExt, AsyncWrite, TryStream, TryStreamExt, +}; +use pin_project::pin_project; + +fn noop_context() -> Context<'static> { + use std::{ + ptr::null, + task::{RawWaker, RawWakerVTable, Waker}, + }; + + const fn noop_raw_waker() -> RawWaker { + unsafe fn noop_clone(_data: *const ()) -> RawWaker { + noop_raw_waker() + } + + unsafe fn noop(_data: *const ()) {} + + const NOOP_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop); + RawWaker::new(null(), &NOOP_WAKER_VTABLE) + } + + pub fn noop_waker_ref() -> &'static Waker { + #[repr(transparent)] + struct SyncRawWaker(RawWaker); + unsafe impl Sync for SyncRawWaker {} + + static NOOP_WAKER_INSTANCE: SyncRawWaker = SyncRawWaker(noop_raw_waker()); + + // SAFETY: `Waker` is #[repr(transparent)] over its `RawWaker`. + unsafe { &*(std::ptr::addr_of!(NOOP_WAKER_INSTANCE.0).cast()) } + } + + Context::from_waker(noop_waker_ref()) +} + +/// Drives the given future (`f`) until it resolves. +/// Executes the indicated function (`p`) each time the +/// poll returned `Poll::Pending`. +pub trait SyncResolve { + type Output; + + fn sync_resolve(&mut self) -> Self::Output { + self.sync_resolve_with(|_| {}) + } + + fn sync_resolve_with)>(&mut self, p: P) -> Self::Output; +} + +impl SyncResolve for F { + type Output = F::Output; + + fn sync_resolve_with)>(&mut self, p: P) -> Self::Output { + let mut cx = noop_context(); + let mut fut = Pin::new(self); + let mut v = fut.as_mut().poll(&mut cx); + while v.is_pending() { + p(fut.as_mut()); + v = fut.as_mut().poll(&mut cx); + } + if let Poll::Ready(v) = v { + v + } else { + unreachable!(); + } + } +} + +/// A synchronous collect method for [`TryStream`]. +pub trait SyncTryCollect { + type Item; + type Error; + + /// Synchronously gather all items from a stream. + /// # Errors + /// When the underlying source produces an error. + fn sync_collect>(self) -> Result; +} + +impl SyncTryCollect for S { + type Item = S::Ok; + type Error = S::Error; + + fn sync_collect>(self) -> Result { + pin!(self.try_collect::()).sync_resolve() + } +} + +/// Synchronous reading for [`AsyncRead`], using [`SyncResolve`]. +pub trait SyncRead { + fn sync_read_exact(&mut self, amount: usize) -> Vec; + fn sync_read_to_end(&mut self) -> Vec; +} + +impl SyncRead for S { + fn sync_read_exact(&mut self, amount: usize) -> Vec { + let mut buf = vec![0; amount]; + let res = self.read_exact(&mut buf[..]); + pin!(res).sync_resolve().unwrap(); + buf + } + + fn sync_read_to_end(&mut self) -> Vec { + let mut buf = Vec::new(); + let res = self.read_to_end(&mut buf); + pin!(res).sync_resolve().unwrap(); + buf + } +} + +pub trait Unadapt { + type S; + fn unadapt(self) -> Self::S; +} + +/// An adapter for [`AsyncRead`] and [`AsyncWrite`] that reads or writes a single byte at a time. +#[pin_project(project = DribbleProjection)] +pub struct Dribble { + #[pin] + s: S, +} + +impl Dribble { + pub fn new(s: S) -> Self { + Self { s } + } + + pub fn unwrap(self) -> S { + self.s + } +} + +impl Unadapt for Dribble { + type S = S; + fn unadapt(self) -> Self::S { + self.s + } +} + +impl AsyncRead for Dribble { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let mut this = self.project(); + this.s.as_mut().poll_read(cx, &mut buf[..1]) + } +} + +impl AsyncWrite for Dribble { + fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + let mut this = self.project(); + this.s.as_mut().poll_write(cx, &buf[..1]) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + this.s.as_mut().poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + this.s.as_mut().poll_close(cx) + } +} + +/// An adapter for [`AsyncRead`] and [`AsyncWrite`] that blocks at a chosen offset. +#[pin_project(project = SplitAtProjection)] +pub struct SplitAt { + #[pin] + s: S, + remaining: Option, +} + +impl SplitAt { + /// Split the stream at the selected `offset`. + /// Read or write calls will stop at the indicated offset, + /// with a single `Poll::Pending` return at that point, + /// after which all operations proceed normally. + pub fn new(s: S, offset: usize) -> Self { + Self { + s, + remaining: Some(offset), + } + } +} + +impl Unadapt for SplitAt { + type S = S; + fn unadapt(self) -> Self::S { + self.s + } +} + +impl AsyncRead for SplitAt { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let mut this = self.project(); + if let Some(r) = this.remaining { + let remaining = *r; + if remaining == 0 { + *this.remaining = None; + cx.waker().wake_by_ref(); + return Poll::Pending; + } + + let cut = min(remaining, buf.len()); + let res = this.s.as_mut().poll_read(cx, &mut buf[..cut]); + if let Poll::Ready(Ok(count)) = res { + *this.remaining = Some(remaining - count); + } + res + } else { + this.s.as_mut().poll_read(cx, buf) + } + } +} + +impl AsyncWrite for SplitAt { + fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + let mut this = self.project(); + if let Some(r) = this.remaining { + let remaining = *r; + if remaining == 0 { + *this.remaining = None; + cx.waker().wake_by_ref(); + return Poll::Pending; + } + + let cut = min(remaining, buf.len()); + let res = this.s.as_mut().poll_write(cx, &buf[..cut]); + if let Poll::Ready(Ok(count)) = res { + *this.remaining = Some(remaining - count); + } + res + } else { + this.s.as_mut().poll_write(cx, buf) + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + this.s.as_mut().poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + this.s.as_mut().poll_close(cx) + } +} + +/// An adapter for [`AsyncRead`] and [`AsyncWrite`] that blocks after every single byte read or written. +#[pin_project(project = StutterProjection)] +pub struct Stutter { + stall: bool, + #[pin] + s: S, +} + +impl Stutter { + pub fn new(s: S) -> Self { + Self { stall: false, s } + } + + fn stutter(self: Pin<&mut Self>, cx: &mut Context<'_>, f: F) -> Poll + where + F: FnOnce(Pin<&mut S>, &mut Context<'_>) -> Poll, + { + let mut this = self.project(); + *this.stall = !*this.stall; + if *this.stall { + // When returning `Poll::Pending`, you have to wake the task. + // We aren't running code anywhere except here, + // so call it here and ensure that the task is picked up immediately. + cx.waker().wake_by_ref(); + Poll::Pending + } else { + f(this.s.as_mut(), cx) + } + } +} + +impl Unadapt for Stutter { + type S = S; + fn unadapt(self) -> Self::S { + self.s + } +} + +impl AsyncRead for Stutter { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + Self::stutter(self, cx, |s, cx| s.poll_read(cx, buf)) + } +} + +impl AsyncWrite for Stutter { + fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + Self::stutter(self, cx, |s, cx| s.poll_write(cx, buf)) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Self::stutter(self, cx, AsyncWrite::poll_flush) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Self::stutter(self, cx, AsyncWrite::poll_close) + } +} + +/// A paired [`AsyncRead`]/[`AsyncWrite`] implementation pair with separate read and write cursors. +/// +/// This allows tests to create paired read and write objects, +/// where writes to one can be read by the other. +/// +/// This relies on the implementation of `AyncReadExt::split` to provide +/// any locking and concurrency, rather than implementing it. +#[derive(Default)] +#[pin_project] +pub struct Pipe { + buf: Vec, + r: usize, + w: usize, +} + +impl Pipe { + #[must_use] + pub fn new() -> (ReadHalf, WriteHalf) { + AsyncReadExt::split(Self::default()) + } +} + +impl AsyncRead for Pipe { + fn poll_read( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let amnt = min(buf.len(), self.buf.len() - self.r); + buf[..amnt].copy_from_slice(&self.buf[self.r..self.r + amnt]); + self.r += amnt; + Poll::Ready(Ok(amnt)) + } +} + +impl AsyncWrite for Pipe { + fn poll_write( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + mut buf: &[u8], + ) -> Poll> { + if self.w < self.buf.len() { + let overlap = min(buf.len() - self.w, self.buf.len()); + let range = self.w..(self.w + overlap); + self.buf[range].copy_from_slice(&buf[..overlap]); + buf = &buf[overlap..]; + } + self.buf.extend_from_slice(buf); + self.w += buf.len(); + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +}