#![deny(warnings, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] // Too lazy to document these. #[cfg(feature = "read-bhttp")] use std::convert::TryFrom; #[cfg(any( feature = "read-http", feature = "write-http", feature = "read-bhttp", feature = "write-bhttp" ))] use std::io; #[cfg(feature = "read-http")] use url::Url; mod err; mod parse; #[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))] mod rw; pub use err::Error; #[cfg(any( feature = "read-http", feature = "write-http", feature = "read-bhttp", feature = "write-bhttp" ))] use err::Res; #[cfg(feature = "read-http")] use parse::{downcase, is_ows, read_line, split_at, COLON, SEMICOLON, SLASH, SP}; use parse::{index_of, trim_ows, COMMA}; #[cfg(feature = "read-bhttp")] use rw::{read_varint, read_vec}; #[cfg(feature = "write-bhttp")] use rw::{write_len, write_varint, write_vec}; #[cfg(feature = "read-http")] const CONTENT_LENGTH: &[u8] = b"content-length"; #[cfg(feature = "read-bhttp")] const COOKIE: &[u8] = b"cookie"; const TRANSFER_ENCODING: &[u8] = b"transfer-encoding"; const CHUNKED: &[u8] = b"chunked"; pub type StatusCode = u16; #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))] pub enum Mode { KnownLength, IndefiniteLength, } pub struct Field { name: Vec, value: Vec, } impl Field { #[must_use] pub fn new(name: Vec, value: Vec) -> Self { Self { name, value } } #[must_use] pub fn name(&self) -> &[u8] { &self.name } #[must_use] pub fn value(&self) -> &[u8] { &self.value } #[cfg(feature = "write-http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { w.write_all(&self.name)?; w.write_all(b": ")?; w.write_all(&self.value)?; w.write_all(b"\r\n")?; Ok(()) } #[cfg(feature = "write-bhttp")] pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> { write_vec(&self.name, w)?; write_vec(&self.value, w)?; Ok(()) } #[cfg(feature = "read-http")] pub fn obs_fold(&mut self, extra: &[u8]) { self.value.push(SP); self.value.extend(trim_ows(extra)); } } #[derive(Default)] pub struct FieldSection(Vec); impl FieldSection { #[must_use] pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Gets the value from the first instance of the field. #[must_use] pub fn get(&self, n: &[u8]) -> Option<&[u8]> { for f in &self.0 { if &f.name[..] == n { return Some(&f.value); } } None } pub fn put(&mut self, name: impl Into>, value: impl Into>) { self.0.push(Field::new(name.into(), value.into())); } pub fn iter(&self) -> impl Iterator { self.0.iter() } #[must_use] pub fn fields(&self) -> &[Field] { &self.0 } #[must_use] pub fn is_chunked(&self) -> bool { // Look at the last symbol in Transfer-Encoding. // This is very primitive decoding; structured field this is not. if let Some(te) = self.get(TRANSFER_ENCODING) { let mut slc = te; while let Some(i) = index_of(COMMA, slc) { slc = trim_ows(&slc[i + 1..]); } slc == CHUNKED } else { false } } /// As required by the HTTP specification, remove the Connection header /// field, everything it refers to, and a few extra fields. #[cfg(feature = "read-http")] fn strip_connection_headers(&mut self) { const CONNECTION: &[u8] = b"connection"; const PROXY_CONNECTION: &[u8] = b"proxy-connection"; const SHOULD_REMOVE: &[&[u8]] = &[ CONNECTION, PROXY_CONNECTION, b"keep-alive", b"te", b"trailer", b"transfer-encoding", b"upgrade", ]; let mut listed = Vec::new(); let mut track = |n| { let mut name = Vec::from(trim_ows(n)); downcase(&mut name); if !listed.contains(&name) { listed.push(name); } }; for f in self .0 .iter() .filter(|f| f.name() == CONNECTION || f.name == PROXY_CONNECTION) { let mut v = f.value(); while let Some(i) = index_of(COMMA, v) { track(&v[..i]); v = &v[i + 1..]; } track(v); } self.0.retain(|f| { !SHOULD_REMOVE.contains(&f.name()) && listed.iter().all(|x| &x[..] != f.name()) }); } #[cfg(feature = "read-http")] fn parse_line(fields: &mut Vec, line: Vec) -> Res<()> { // obs-fold is helpful in specs, so support it here too let f = if is_ows(line[0]) { let mut e = fields.pop().ok_or(Error::ObsFold)?; e.obs_fold(&line); e } else if let Some((n, v)) = split_at(COLON, line) { let mut name = Vec::from(trim_ows(&n)); downcase(&mut name); let value = Vec::from(trim_ows(&v)); Field::new(name, value) } else { return Err(Error::Missing(COLON)); }; fields.push(f); Ok(()) } #[cfg(feature = "read-http")] pub fn read_http(r: &mut impl io::BufRead) -> Res { let mut fields = Vec::new(); loop { let line = read_line(r)?; if trim_ows(&line).is_empty() { return Ok(Self(fields)); } Self::parse_line(&mut fields, line)?; } } #[cfg(feature = "read-bhttp")] fn read_bhttp_fields(terminator: bool, r: &mut impl io::BufRead) -> Res> { let mut fields = Vec::new(); let mut cookie_index: Option = None; loop { if let Some(n) = read_vec(r)? { if n.is_empty() { if terminator { return Ok(fields); } return Err(Error::Truncated); } let mut v = read_vec(r)?.ok_or(Error::Truncated)?; if n == COOKIE { if let Some(i) = &cookie_index { fields[*i].value.extend_from_slice(b"; "); fields[*i].value.append(&mut v); continue; } cookie_index = Some(fields.len()); } fields.push(Field::new(n, v)); } else if terminator { return Err(Error::Truncated); } else { return Ok(fields); } } } #[cfg(feature = "read-bhttp")] pub fn read_bhttp(mode: Mode, r: &mut impl io::BufRead) -> Res { let fields = if mode == Mode::KnownLength { if let Some(buf) = read_vec(r)? { Self::read_bhttp_fields(false, &mut io::BufReader::new(&buf[..]))? } else { Vec::new() } } else { Self::read_bhttp_fields(true, r)? }; Ok(Self(fields)) } #[cfg(feature = "write-bhttp")] fn write_bhttp_headers(&self, w: &mut impl io::Write) -> Res<()> { for f in &self.0 { f.write_bhttp(w)?; } Ok(()) } #[cfg(feature = "write-bhttp")] pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { if mode == Mode::KnownLength { let mut buf = Vec::new(); self.write_bhttp_headers(&mut buf)?; write_vec(&buf, w)?; } else { self.write_bhttp_headers(w)?; write_len(0, w)?; } Ok(()) } #[cfg(feature = "write-http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { for f in &self.0 { f.write_http(w)?; } w.write_all(b"\r\n")?; Ok(()) } } pub enum ControlData { Request { method: Vec, scheme: Vec, authority: Vec, path: Vec, }, Response(StatusCode), } impl ControlData { #[must_use] pub fn is_request(&self) -> bool { matches!(self, Self::Request { .. }) } #[must_use] pub fn method(&self) -> Option<&[u8]> { if let Self::Request { method, .. } = self { Some(method) } else { None } } #[must_use] pub fn scheme(&self) -> Option<&[u8]> { if let Self::Request { scheme, .. } = self { Some(scheme) } else { None } } #[must_use] pub fn authority(&self) -> Option<&[u8]> { if let Self::Request { authority, .. } = self { if authority.is_empty() { None } else { Some(authority) } } else { None } } #[must_use] pub fn path(&self) -> Option<&[u8]> { if let Self::Request { path, .. } = self { if path.is_empty() { None } else { Some(path) } } else { None } } #[must_use] pub fn status(&self) -> Option { if let Self::Response(code) = self { Some(*code) } else { None } } #[cfg(feature = "read-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] let (a, r) = split_at(SP, line).ok_or(Error::Missing(SP))?; let (b, _) = split_at(SP, r).ok_or(Error::Missing(SP))?; if index_of(SLASH, &a).is_some() { // Probably a response, so treat it as such. let status_str = String::from_utf8(b)?; let code = status_str.parse::()?; Ok(Self::Response(code)) } else if index_of(COLON, &b).is_some() { // Now try to parse the URL. let url_str = String::from_utf8(b)?; let parsed = Url::parse(&url_str)?; let authority = parsed.host_str().map_or_else(String::new, |host| { let mut authority = String::from(host); if let Some(port) = parsed.port() { authority.push(':'); authority.push_str(&port.to_string()); } authority }); let mut path = String::from(parsed.path()); if let Some(q) = parsed.query() { path.push('?'); path.push_str(q); } Ok(Self::Request { method: a, scheme: Vec::from(parsed.scheme().as_bytes()), authority: Vec::from(authority.as_bytes()), path: Vec::from(path.as_bytes()), }) } else { if a == b"CONNECT" { return Err(Error::ConnectUnsupported); } Ok(Self::Request { method: a, scheme: Vec::from(&b"https"[..]), authority: Vec::new(), path: b, }) } } #[cfg(feature = "read-bhttp")] pub fn read_bhttp(request: bool, r: &mut impl io::BufRead) -> Res { let v = if request { let method = read_vec(r)?.ok_or(Error::Truncated)?; let scheme = read_vec(r)?.ok_or(Error::Truncated)?; let authority = read_vec(r)?.ok_or(Error::Truncated)?; let path = read_vec(r)?.ok_or(Error::Truncated)?; Self::Request { method, scheme, authority, path, } } else { Self::Response(u16::try_from(read_varint(r)?.ok_or(Error::Truncated)?)?) }; Ok(v) } /// If this is an informational response. #[cfg(any(feature = "read-bhttp", feature = "read-http"))] #[must_use] fn informational(&self) -> Option { match self { Self::Response(v) if *v >= 100 && *v < 200 => Some(*v), _ => None, } } #[cfg(feature = "write-bhttp")] #[must_use] fn code(&self, mode: Mode) -> u64 { match (self, mode) { (Self::Request { .. }, Mode::KnownLength) => 0, (Self::Response(_), Mode::KnownLength) => 1, (Self::Request { .. }, Mode::IndefiniteLength) => 2, (Self::Response(_), Mode::IndefiniteLength) => 3, } } #[cfg(feature = "write-bhttp")] pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> { match self { Self::Request { method, scheme, authority, path, } => { write_vec(method, w)?; write_vec(scheme, w)?; write_vec(authority, w)?; write_vec(path, w)?; } Self::Response(status) => write_varint(*status, w)?, } Ok(()) } #[cfg(feature = "write-http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { match self { Self::Request { method, scheme, authority, path, } => { w.write_all(method)?; w.write_all(b" ")?; if !authority.is_empty() { w.write_all(scheme)?; w.write_all(b"://")?; w.write_all(authority)?; } w.write_all(path)?; w.write_all(b" HTTP/1.1\r\n")?; } Self::Response(status) => { let buf = format!("HTTP/1.1 {} Reason\r\n", *status); w.write_all(buf.as_bytes())?; } } Ok(()) } } pub struct InformationalResponse { status: StatusCode, fields: FieldSection, } impl InformationalResponse { #[must_use] pub fn new(status: StatusCode, fields: FieldSection) -> Self { Self { status, fields } } #[must_use] pub fn status(&self) -> StatusCode { self.status } #[must_use] pub fn fields(&self) -> &FieldSection { &self.fields } #[cfg(feature = "write-bhttp")] fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { write_varint(self.status, w)?; self.fields.write_bhttp(mode, w)?; Ok(()) } } pub struct Message { informational: Vec, control: ControlData, header: FieldSection, content: Vec, trailer: FieldSection, } impl Message { #[must_use] pub fn request(method: Vec, scheme: Vec, authority: Vec, path: Vec) -> Self { Self { informational: Vec::new(), control: ControlData::Request { method, scheme, authority, path, }, header: FieldSection::default(), content: Vec::new(), trailer: FieldSection::default(), } } #[must_use] pub fn response(status: StatusCode) -> Self { Self { informational: Vec::new(), control: ControlData::Response(status), header: FieldSection::default(), content: Vec::new(), trailer: FieldSection::default(), } } pub fn put_header(&mut self, name: impl Into>, value: impl Into>) { self.header.put(name, value); } pub fn put_trailer(&mut self, name: impl Into>, value: impl Into>) { self.trailer.put(name, value); } pub fn write_content(&mut self, d: impl AsRef<[u8]>) { self.content.extend_from_slice(d.as_ref()); } #[must_use] pub fn informational(&self) -> &[InformationalResponse] { &self.informational } #[must_use] pub fn control(&self) -> &ControlData { &self.control } #[must_use] pub fn header(&self) -> &FieldSection { &self.header } #[must_use] pub fn content(&self) -> &[u8] { &self.content } #[must_use] pub fn trailer(&self) -> &FieldSection { &self.trailer } #[cfg(feature = "read-http")] fn read_chunked(r: &mut impl io::BufRead) -> Res> { let mut content = Vec::new(); loop { let mut line = read_line(r)?; if let Some(i) = index_of(SEMICOLON, &line) { std::mem::drop(line.split_off(i)); } let count_str = String::from_utf8(line)?; let count = usize::from_str_radix(&count_str, 16)?; if count == 0 { return Ok(content); } let mut buf = vec![0; count]; r.read_exact(&mut buf)?; assert!(read_line(r)?.is_empty()); content.append(&mut buf); } } #[cfg(feature = "read-http")] #[allow(clippy::read_zero_byte_vec)] // https://github.com/rust-lang/rust-clippy/issues/9274 pub fn read_http(r: &mut impl io::BufRead) -> Res { let line = read_line(r)?; let mut control = ControlData::read_http(line)?; let mut informational = Vec::new(); while let Some(status) = control.informational() { let fields = FieldSection::read_http(r)?; informational.push(InformationalResponse::new(status, fields)); let line = read_line(r)?; control = ControlData::read_http(line)?; } let mut header = FieldSection::read_http(r)?; let (content, trailer) = if matches!(control.status(), Some(204) | Some(304)) { // 204 and 304 have no body, no matter what Content-Length says. // Unfortunately, we can't do the same for responses to HEAD. (Vec::new(), FieldSection::default()) } else if header.is_chunked() { let content = Self::read_chunked(r)?; let trailer = FieldSection::read_http(r)?; (content, trailer) } else { let mut content = Vec::new(); if let Some(cl) = header.get(CONTENT_LENGTH) { let cl_str = String::from_utf8(Vec::from(cl))?; let cl_int = cl_str.parse::()?; if cl_int > 0 { content.resize(cl_int, 0); r.read_exact(&mut content)?; } } else { // Note that for a request, the spec states that the content is // empty, but this just reads all input like for a response. r.read_to_end(&mut content)?; } (content, FieldSection::default()) }; header.strip_connection_headers(); Ok(Self { informational, control, header, content, trailer, }) } #[cfg(feature = "write-http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { for info in &self.informational { ControlData::Response(info.status()).write_http(w)?; info.fields().write_http(w)?; } self.control.write_http(w)?; if !self.content.is_empty() { if self.trailer.is_empty() { write!(w, "Content-Length: {}\r\n", self.content.len())?; } else { w.write_all(b"Transfer-Encoding: chunked\r\n")?; } } self.header.write_http(w)?; if self.header.is_chunked() { write!(w, "{:x}\r\n", self.content.len())?; w.write_all(&self.content)?; w.write_all(b"\r\n0\r\n")?; self.trailer.write_http(w)?; } else { w.write_all(&self.content)?; } Ok(()) } /// Read a BHTTP message. #[cfg(feature = "read-bhttp")] pub fn read_bhttp(r: &mut impl io::BufRead) -> Res { let t = read_varint(r)?.ok_or(Error::Truncated)?; let request = t == 0 || t == 2; let mode = match t { 0 | 1 => Mode::KnownLength, 2 | 3 => Mode::IndefiniteLength, _ => return Err(Error::InvalidMode), }; let mut control = ControlData::read_bhttp(request, r)?; let mut informational = Vec::new(); while let Some(status) = control.informational() { let fields = FieldSection::read_bhttp(mode, r)?; informational.push(InformationalResponse::new(status, fields)); control = ControlData::read_bhttp(request, r)?; } let header = FieldSection::read_bhttp(mode, r)?; let mut content = read_vec(r)?.unwrap_or_default(); if mode == Mode::IndefiniteLength && !content.is_empty() { loop { let mut extra = read_vec(r)?.unwrap_or_default(); if extra.is_empty() { break; } content.append(&mut extra); } } let trailer = FieldSection::read_bhttp(mode, r)?; Ok(Self { informational, control, header, content, trailer, }) } #[cfg(feature = "write-bhttp")] pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { write_varint(self.control.code(mode), w)?; for info in &self.informational { info.write_bhttp(mode, w)?; } self.control.write_bhttp(w)?; self.header.write_bhttp(mode, w)?; write_vec(&self.content, w)?; if mode == Mode::IndefiniteLength && !self.content.is_empty() { write_len(0, w)?; } self.trailer.write_bhttp(mode, w)?; Ok(()) } } #[cfg(feature = "write-http")] impl std::fmt::Debug for Message { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { let mut buf = Vec::new(); self.write_http(&mut buf).map_err(|_| std::fmt::Error)?; write!(f, "{:?}", String::from_utf8_lossy(&buf)) } }