use bytes::Bytes;
use super::poison::HttpPoison;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct HttpRequest {
pub method: Bytes,
pub path: Bytes,
pub version: HttpVersion,
pub headers: Vec<(Bytes, Bytes)>,
pub body: Bytes,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct HttpResponse {
pub status: u16,
pub reason: Bytes,
pub version: HttpVersion,
pub headers: Vec<(Bytes, Bytes)>,
pub body: Bytes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum HttpVersion {
Http1_0,
Http1_1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum BodyFraming {
None,
ContentLength(u64),
Chunked,
UntilClose,
}
impl HttpRequest {
pub fn new(
method: Bytes,
path: Bytes,
version: HttpVersion,
headers: Vec<(Bytes, Bytes)>,
body: Bytes,
) -> Self {
Self {
method,
path,
version,
headers,
body,
}
}
pub fn method_str(&self) -> Option<&str> {
std::str::from_utf8(&self.method).ok()
}
pub fn path_str(&self) -> Option<&str> {
std::str::from_utf8(&self.path).ok()
}
pub fn host(&self) -> Option<&str> {
self.header_str("host")
}
pub fn user_agent(&self) -> Option<&str> {
self.header_str("user-agent")
}
pub fn cookie(&self) -> Option<&str> {
self.header_str("cookie")
}
pub fn referer(&self) -> Option<&str> {
self.header_str("referer")
}
pub fn accept(&self) -> Option<&str> {
self.header_str("accept")
}
pub fn content_type(&self) -> Option<&str> {
self.header_str("content-type")
}
pub fn content_length(&self) -> Option<u64> {
self.header_str("content-length")
.and_then(|v| v.trim().parse().ok())
}
pub fn header(&self, name: &str) -> Option<&[u8]> {
header_lookup(&self.headers, name).next()
}
pub fn headers_all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a [u8]> + 'a {
header_lookup(&self.headers, name)
}
fn header_str(&self, name: &str) -> Option<&str> {
self.header(name).and_then(|v| std::str::from_utf8(v).ok())
}
}
impl HttpResponse {
pub fn new(
status: u16,
reason: Bytes,
version: HttpVersion,
headers: Vec<(Bytes, Bytes)>,
body: Bytes,
) -> Self {
Self {
status,
reason,
version,
headers,
body,
}
}
pub fn reason_str(&self) -> Option<&str> {
std::str::from_utf8(&self.reason).ok()
}
pub fn content_type(&self) -> Option<&str> {
self.header_str("content-type")
}
pub fn content_length(&self) -> Option<u64> {
self.header_str("content-length")
.and_then(|v| v.trim().parse().ok())
}
pub fn set_cookie(&self) -> impl Iterator<Item = &str> + '_ {
self.headers_all("set-cookie")
.filter_map(|v| std::str::from_utf8(v).ok())
}
pub fn status_class(&self) -> Option<u8> {
let cls = self.status / 100;
if (1..=5).contains(&cls) {
Some(cls as u8)
} else {
None
}
}
pub fn is_success(&self) -> bool {
self.status_class() == Some(2)
}
pub fn is_redirect(&self) -> bool {
self.status_class() == Some(3)
}
pub fn is_client_error(&self) -> bool {
self.status_class() == Some(4)
}
pub fn is_server_error(&self) -> bool {
self.status_class() == Some(5)
}
pub fn header(&self, name: &str) -> Option<&[u8]> {
header_lookup(&self.headers, name).next()
}
pub fn headers_all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a [u8]> + 'a {
header_lookup(&self.headers, name)
}
fn header_str(&self, name: &str) -> Option<&str> {
self.header(name).and_then(|v| std::str::from_utf8(v).ok())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum SmugglingPolicy {
Strict,
Normalize,
Observe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum Normalization {
StrippedContentLength,
CollapsedContentLength,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct Authority {
pub raw: Bytes,
pub host: String,
pub port: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SwitchKind {
ConnectTunnel,
Upgrade { protocol: Bytes },
Http2PriorKnowledge,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct RequestHead {
pub method: Bytes,
pub path: Bytes,
pub version: HttpVersion,
pub headers: Vec<(Bytes, Bytes)>,
pub framing: BodyFraming,
pub applied: Vec<Normalization>,
pub raw: Bytes,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct ResponseHead {
pub status: u16,
pub reason: Bytes,
pub version: HttpVersion,
pub headers: Vec<(Bytes, Bytes)>,
pub framing: BodyFraming,
pub interim: bool,
pub applied: Vec<Normalization>,
pub raw: Bytes,
}
impl RequestHead {
pub fn method_str(&self) -> Option<&str> {
std::str::from_utf8(&self.method).ok()
}
pub fn path_str(&self) -> Option<&str> {
std::str::from_utf8(&self.path).ok()
}
pub fn host(&self) -> Option<&str> {
self.header_str("host")
}
pub fn content_length(&self) -> Option<u64> {
self.header_str("content-length")
.and_then(|v| v.trim().parse().ok())
}
pub fn header(&self, name: &str) -> Option<&[u8]> {
header_lookup(&self.headers, name).next()
}
pub fn headers_all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a [u8]> + 'a {
header_lookup(&self.headers, name)
}
pub fn authority(&self) -> Result<Authority, HttpPoison> {
let mut hosts = header_lookup(&self.headers, "host");
let host_header = hosts.next();
if hosts.next().is_some() {
return Err(HttpPoison::DuplicateHost);
}
let raw: Bytes = match absolute_form_authority(&self.path) {
Some(range) => self.path.slice(range),
None => match host_header {
Some(_) => {
let idx = self
.headers
.iter()
.position(|(k, _)| k.as_ref().eq_ignore_ascii_case(b"host"))
.expect("host header was just found");
self.headers[idx].1.clone()
}
None => Bytes::new(),
},
};
parse_authority(raw)
}
pub fn upgrade_protocols(&self) -> impl Iterator<Item = &str> + '_ {
let named_in_connection = self
.headers_all("connection")
.flat_map(split_list_tokens)
.any(|t| t.eq_ignore_ascii_case("upgrade"));
self.headers_all("upgrade")
.flat_map(split_list_tokens)
.filter(move |_| named_in_connection)
}
pub fn is_websocket_upgrade(&self) -> bool {
self.method.as_ref() == b"GET"
&& self
.upgrade_protocols()
.any(|t| t.eq_ignore_ascii_case("websocket"))
&& self.header("sec-websocket-key").is_some()
&& self.header("sec-websocket-version").is_some()
}
fn header_str(&self, name: &str) -> Option<&str> {
self.header(name).and_then(|v| std::str::from_utf8(v).ok())
}
}
fn split_list_tokens(value: &[u8]) -> impl Iterator<Item = &str> + '_ {
value
.split(|&b| b == b',')
.filter_map(|seg| std::str::from_utf8(seg).ok())
.map(|seg| seg.trim_matches(|c| c == ' ' || c == '\t'))
.filter(|seg| !seg.is_empty())
}
fn absolute_form_authority(target: &[u8]) -> Option<std::ops::Range<usize>> {
let sep = target.windows(3).position(|w| w == b"://")?;
if sep == 0 || target[..sep].iter().any(|b| !b.is_ascii_alphanumeric()) {
return None;
}
let start = sep + 3;
let end = target[start..]
.iter()
.position(|b| matches!(b, b'/' | b'?' | b'#'))
.map_or(target.len(), |p| p + start);
Some(start..end)
}
fn parse_authority(raw: Bytes) -> Result<Authority, HttpPoison> {
if !raw.is_ascii() {
return Err(HttpPoison::NonAsciiAuthority);
}
let s = std::str::from_utf8(&raw).map_err(|_| HttpPoison::NonAsciiAuthority)?;
let s = s.rsplit_once('@').map_or(s, |(_, after)| after);
let (host, port) = if let Some(rest) = s.strip_prefix('[') {
let (inside, after) = rest.split_once(']').ok_or(HttpPoison::NonAsciiAuthority)?;
let port = match after.strip_prefix(':') {
Some(p) if !p.is_empty() => Some(p.parse().map_err(|_| HttpPoison::NonAsciiAuthority)?),
Some(_) => return Err(HttpPoison::NonAsciiAuthority),
None => None,
};
(inside.to_ascii_lowercase(), port)
} else {
match s.rsplit_once(':') {
Some((h, p)) if !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()) => (
h.to_ascii_lowercase(),
Some(p.parse().map_err(|_| HttpPoison::NonAsciiAuthority)?),
),
Some((_, p)) if !p.is_empty() => return Err(HttpPoison::NonAsciiAuthority),
_ => (s.to_ascii_lowercase(), None),
}
};
Ok(Authority { raw, host, port })
}
impl ResponseHead {
pub fn reason_str(&self) -> Option<&str> {
std::str::from_utf8(&self.reason).ok()
}
pub fn status_class(&self) -> Option<u8> {
let cls = self.status / 100;
if (1..=5).contains(&cls) {
Some(cls as u8)
} else {
None
}
}
pub fn is_success(&self) -> bool {
self.status_class() == Some(2)
}
pub fn content_length(&self) -> Option<u64> {
self.header_str("content-length")
.and_then(|v| v.trim().parse().ok())
}
pub fn header(&self, name: &str) -> Option<&[u8]> {
header_lookup(&self.headers, name).next()
}
pub fn headers_all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a [u8]> + 'a {
header_lookup(&self.headers, name)
}
fn header_str(&self, name: &str) -> Option<&str> {
self.header(name).and_then(|v| std::str::from_utf8(v).ok())
}
}
fn header_lookup<'h, 'n>(
headers: &'h [(Bytes, Bytes)],
name: &'n str,
) -> impl Iterator<Item = &'h [u8]> + use<'h, 'n> {
headers
.iter()
.filter(move |(k, _)| k.as_ref().eq_ignore_ascii_case(name.as_bytes()))
.map(|(_, v)| v.as_ref())
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct HttpConfig {
pub max_buffer: usize,
pub max_headers: usize,
}
impl HttpConfig {
#[must_use]
pub fn with_max_buffer(mut self, n: usize) -> Self {
self.max_buffer = n;
self
}
#[must_use]
pub fn with_max_headers(mut self, n: usize) -> Self {
self.max_headers = n;
self
}
}
impl Default for HttpConfig {
fn default() -> Self {
Self {
max_buffer: 1024 * 1024, max_headers: 64,
}
}
}