use std::any::Any;
use std::fmt;
use std::io::{BufRead, Read};
use std::net::IpAddr;
use std::sync::Arc;
use rustclamp_core::Reference;
use super::{Response, Session, error};
const MAX_LINE: u64 = 8 * 1024;
const MAX_HEADERS: usize = 100;
pub const MAX_BODY: usize = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Request {
pub method: String,
pub path: String,
pub query: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
pub peer: Option<IpAddr>,
pub(super) session: Option<Session>,
pub(super) params: Vec<(String, String)>,
pub(super) state: State,
pub(super) extensions: State,
pub(super) too_large: bool,
pub(super) reference: Minted,
pub(super) keep_alive: bool,
}
#[derive(Clone, Default)]
pub(super) struct State(pub(super) Arc<Vec<Arc<dyn Any + Send + Sync>>>);
impl fmt::Debug for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("State").finish_non_exhaustive()
}
}
impl PartialEq for State {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for State {}
#[derive(Clone, Copy, Debug)]
pub(super) struct Minted(pub(super) Reference);
impl PartialEq for Minted {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for Minted {}
impl Request {
pub fn new(method: &str, target: &str) -> Self {
let target = target.split('#').next().unwrap_or_default();
let (path, query) = target.split_once('?').unwrap_or((target, ""));
Self {
method: method.into(),
path: path.into(),
query: query.into(),
headers: Vec::new(),
body: Vec::new(),
peer: None,
session: None,
params: Vec::new(),
state: State::default(),
extensions: State::default(),
too_large: false,
keep_alive: false,
reference: Minted(Reference::new()),
}
}
pub fn reference(&self) -> Reference {
self.reference.0
}
pub fn get(target: &str) -> Self {
Self::new("GET", target)
}
pub fn post(target: &str) -> Self {
Self::new("POST", target)
}
#[must_use]
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.headers.push((name.to_ascii_lowercase(), value.into()));
self
}
#[must_use]
pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = body.into();
self
}
#[must_use]
pub fn with_peer(mut self, peer: IpAddr) -> Self {
self.peer = Some(peer);
self
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
pub fn query(&self, key: &str) -> Option<String> {
field(&self.query, key)
}
pub fn form(&self, key: &str) -> Option<String> {
if let Some(value) = super::upload::form_field(self, key) {
return value;
}
field(&String::from_utf8_lossy(&self.body), key)
}
pub fn param(&self, name: &str) -> Option<&str> {
self.params
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.as_str())
}
pub fn param_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
self.param(name)?.parse().ok()
}
pub fn session(&self) -> Option<&Session> {
self.session.as_ref()
}
pub fn state<T: Any>(&self) -> Option<&T> {
self.state.0.iter().find_map(|value| value.downcast_ref())
}
#[must_use]
pub fn with_extension<T: Any + Send + Sync>(mut self, value: T) -> Self {
let kept = Arc::make_mut(&mut self.extensions.0);
kept.retain(|kept| (**kept).type_id() != std::any::TypeId::of::<T>());
kept.push(Arc::new(value));
self
}
pub fn extension<T: Any>(&self) -> Option<&T> {
self.extensions
.0
.iter()
.find_map(|value| value.downcast_ref())
}
pub fn body_too_large(&self) -> bool {
self.too_large
}
pub fn cookie(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.filter(|(key, _)| key == "cookie")
.flat_map(|(_, value)| value.split(';'))
.find_map(|pair| {
let (key, value) = pair.trim().split_once('=')?;
(key == name).then_some(value)
})
}
}
pub(super) fn default_limit(request: &Request) -> usize {
if request
.header("content-type")
.is_some_and(super::upload::is_multipart)
{
super::MAX_UPLOAD
} else {
MAX_BODY
}
}
pub(super) fn parse(
reader: &mut impl BufRead,
peer: Option<IpAddr>,
limit: &dyn Fn(&Request) -> usize,
) -> Result<Request, Response> {
let line = read_line(reader)?;
let mut parts = line.split_whitespace();
let (Some(method), Some(target), Some(version)) = (parts.next(), parts.next(), parts.next())
else {
return Err(error(400));
};
let mut request = Request::new(method, target);
request.peer = peer;
loop {
let line = read_line(reader)?;
if line.is_empty() {
break;
}
if request.headers.len() == MAX_HEADERS {
return Err(error(431));
}
let (name, value) = line.split_once(':').ok_or_else(|| error(400))?;
request = request.with_header(name.trim(), value.trim());
}
request.keep_alive = version == "HTTP/1.1"
&& !request
.header("connection")
.is_some_and(|value| value.eq_ignore_ascii_case("close"));
if request.header("transfer-encoding").is_some() {
return Err(error(411));
}
if let Some(length) = request.header("content-length") {
let length: usize = length.parse().map_err(|_| error(400))?;
if length > limit(&request) {
request.too_large = true;
request.keep_alive = false;
return Ok(request);
}
let mut body = vec![0; length];
reader.read_exact(&mut body).map_err(|_| error(400))?;
request.body = body;
}
Ok(request)
}
fn read_line(reader: &mut impl BufRead) -> Result<String, Response> {
let mut line = Vec::new();
reader
.take(MAX_LINE + 1)
.read_until(b'\n', &mut line)
.map_err(|_| error(400))?;
if line.len() as u64 > MAX_LINE {
return Err(error(431));
}
let line = String::from_utf8(line).map_err(|_| error(400))?;
Ok(line.trim_end_matches(['\r', '\n']).to_owned())
}
pub(super) fn field(pairs: &str, key: &str) -> Option<String> {
pairs.split('&').find_map(|pair| {
let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
(decode(name) == key).then(|| decode(value))
})
}
pub(super) fn encode(text: &str) -> String {
use std::fmt::Write;
let mut out = String::with_capacity(text.len());
for byte in text.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char);
}
_ => {
let _ = write!(out, "%{byte:02X}");
}
}
}
out
}
pub(super) fn decode(text: &str) -> String {
let bytes = text.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'+' => out.push(b' '),
b'%' if i + 2 < bytes.len() => {
match std::str::from_utf8(&bytes[i + 1..i + 3])
.ok()
.and_then(|hex| u8::from_str_radix(hex, 16).ok())
{
Some(byte) => {
out.push(byte);
i += 2;
}
None => out.push(b'%'),
}
}
byte => out.push(byte),
}
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_raw(raw: &str) -> Result<Request, u16> {
parse(&mut raw.as_bytes(), None, &default_limit).map_err(|response| response.status)
}
#[test]
fn parses_line_headers_query_and_body() {
let request = parse_raw(
"POST /contact?ref=home%20page HTTP/1.1\r\nHost: x\r\nContent-Type: application/x-www-form-urlencoded\r\nCookie: a=1; session=abc\r\nContent-Length: 20\r\n\r\nname=Neo+C&msg=hi%21extra",
)
.unwrap();
assert_eq!(
(request.method.as_str(), request.path.as_str()),
("POST", "/contact")
);
assert_eq!(request.query("ref").as_deref(), Some("home page"));
assert_eq!(request.header("HOST"), Some("x"));
assert_eq!(request.cookie("session"), Some("abc"));
assert_eq!(request.body, b"name=Neo+C&msg=hi%21");
assert_eq!(request.form("name").as_deref(), Some("Neo C"));
assert_eq!(request.form("msg").as_deref(), Some("hi!"));
}
#[test]
fn refuses_malformed_and_oversized_requests() {
assert_eq!(parse_raw("").unwrap_err(), 400);
assert_eq!(parse_raw("GET /\r\n\r\n").unwrap_err(), 400);
assert_eq!(
parse_raw("GET / HTTP/1.1\r\nno colon\r\n\r\n").unwrap_err(),
400
);
assert_eq!(
parse_raw("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n").unwrap_err(),
411
);
let big = format!(
"POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n",
MAX_BODY + 1
);
let big = parse_raw(&big).unwrap();
assert!(big.body_too_large() && big.body.is_empty() && !big.keep_alive);
let small = parse(
&mut "POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nabcd".as_bytes(),
None,
&|_| 3,
)
.unwrap();
assert!(small.body_too_large());
let upload = |length| {
format!(
"POST / HTTP/1.1\r\nContent-Type: multipart/form-data; boundary=b\r\nContent-Length: {length}\r\n\r\n"
)
};
assert_eq!(parse_raw(&upload(MAX_BODY + 1)).unwrap_err(), 400);
assert!(
parse_raw(&upload(super::super::MAX_UPLOAD + 1))
.unwrap()
.body_too_large()
);
let long = format!("GET /{} HTTP/1.1\r\n\r\n", "a".repeat(MAX_LINE as usize));
assert_eq!(parse_raw(&long).unwrap_err(), 431);
let many = format!(
"GET / HTTP/1.1\r\n{}\r\n",
"X: 1\r\n".repeat(MAX_HEADERS + 1)
);
assert_eq!(parse_raw(&many).unwrap_err(), 431);
assert_eq!(
parse_raw("POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\nab").unwrap_err(),
400
);
}
#[test]
fn decode_handles_bad_escapes() {
assert_eq!(decode("100%"), "100%");
assert_eq!(decode("%zz%41"), "%zzA");
assert_eq!(decode("a+b%2Bc"), "a b+c");
}
}