use super::collection::{Collection, HashMapCollection};
use std::borrow::Cow;
use super::json::JsonNode;
#[derive(Debug, Clone, Default)]
pub struct RequestData {
pub method: String,
pub uri: String,
pub uri_raw: String,
pub path: String,
pub query_string: String,
pub protocol: String,
pub headers: HashMapCollection,
pub args_get: HashMapCollection,
pub args_post: HashMapCollection,
pub cookies: HashMapCollection,
pub multipart_part_headers: HashMapCollection,
pub files: HashMapCollection,
pub body_processor: String,
pub body_error: Option<String>,
pub body: Vec<u8>,
pub client_ip: String,
pub client_port: u16,
pub server_name: String,
pub server_addr: String,
pub server_port: u16,
}
impl RequestData {
pub fn new() -> Self {
Self::default()
}
pub fn set_uri(&mut self, uri: &str) {
self.uri = uri.to_string();
self.uri_raw = uri.to_string();
if let Some(pos) = uri.find('?') {
self.path = uri[..pos].to_string();
self.query_string = uri[pos + 1..].to_string();
self.parse_query_string(&self.query_string.clone());
} else {
self.path = uri.to_string();
self.query_string.clear();
}
}
pub fn set_method(&mut self, method: &str) {
self.method = method.to_string();
}
pub fn set_protocol(&mut self, protocol: &str) {
self.protocol = protocol.to_string();
}
pub fn add_header(&mut self, name: &str, value: &str) {
let lower = name.to_lowercase();
if lower == "cookie" {
self.parse_cookie_header(value);
}
self.headers.add(lower, value.to_string());
}
fn parse_cookie_header(&mut self, value: &str) {
for pair in value.split(';') {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
match pair.split_once('=') {
Some((k, v)) => self.cookies.add(k.trim().to_string(), v.trim().to_string()),
None => self.cookies.add(pair.to_string(), String::new()),
}
}
}
pub fn append_body(&mut self, data: &[u8]) {
self.body.extend_from_slice(data);
}
pub fn body_str(&self) -> String {
String::from_utf8_lossy(&self.body).to_string()
}
pub fn body_length(&self) -> usize {
self.body.len()
}
fn parse_query_string(&mut self, qs: &str) {
for pair in qs.split('&') {
if let Some(pos) = pair.find('=') {
let key = &pair[..pos];
let value = &pair[pos + 1..];
let key = form_decode(key);
let value = form_decode(value);
self.args_get.add(key, value);
} else if !pair.is_empty() {
let key = form_decode(pair);
self.args_get.add(key, String::new());
}
}
}
pub fn parse_multipart_body(&mut self, content_type: &str) -> bool {
let Some(boundary) = extract_multipart_boundary(content_type) else {
return false;
};
let body = self.body_str();
let delimiter = format!("--{boundary}");
let mut sections = body.split(delimiter.as_str());
let _preamble = sections.next();
for section in sections {
if section.starts_with("--") {
break;
}
let section = section
.strip_prefix("\r\n")
.or_else(|| section.strip_prefix("\n"))
.unwrap_or(section);
let (head, content) = match section.split_once("\r\n\r\n") {
Some(split) => split,
None => match section.split_once("\n\n") {
Some(split) => split,
None => (section, ""),
},
};
let content = content
.strip_suffix("\r\n")
.or_else(|| content.strip_suffix("\n"))
.unwrap_or(content);
let mut part_name: Option<String> = None;
let mut filename: Option<String> = None;
let mut header_lines: Vec<String> = Vec::new();
for line in head.lines() {
let line = line.trim_end_matches('\r');
if line.is_empty() {
continue;
}
header_lines.push(line.to_string());
if let Some((hname, hval)) = line.split_once(':') {
if hname.trim().eq_ignore_ascii_case("content-disposition") {
part_name = extract_disposition_param(hval, "name");
filename = extract_disposition_param(hval, "filename");
}
}
}
let Some(name) = part_name else {
continue;
};
for line in header_lines {
self.multipart_part_headers.add(name.clone(), line);
}
if let Some(fname) = filename {
self.files.add(name, fname);
} else {
self.args_post.add(name, content.to_string());
}
}
self.body_processor = "MULTIPART".to_string();
true
}
pub fn parse_form_body(&mut self) {
let body_str = self.body_str();
for pair in body_str.split('&') {
if let Some(pos) = pair.find('=') {
let key = &pair[..pos];
let value = &pair[pos + 1..];
let key = form_decode(key);
let value = form_decode(value);
self.args_post.add(key, value);
} else if !pair.is_empty() {
self.args_post.add(form_decode(pair), String::new());
}
}
}
pub fn parse_json_body(&mut self) -> Result<(), String> {
self.body_processor = "JSON".to_string();
if self.body.iter().all(|b| b.is_ascii_whitespace()) {
return Ok(());
}
let value: JsonNode = match serde_json::from_slice(&self.body) {
Ok(v) => v,
Err(e) => {
let msg = format!("JSON parsing error: {e}");
self.body_error = Some(msg.clone());
return Err(msg);
}
};
let mut count = 0usize;
let mut path = String::from("json");
let truncated = flatten_json(&value, &mut path, &mut self.args_post, &mut count, 0);
if truncated {
let msg = format!(
"JSON body exceeded processing limits ({MAX_JSON_ARGS} arguments or \
{MAX_JSON_DEPTH} levels of nesting); only part of the body was inspected"
);
self.body_error = Some(msg.clone());
return Err(msg);
}
Ok(())
}
pub fn parse_xml_body(&mut self) -> Result<(), String> {
use quick_xml::events::Event;
self.body_processor = "XML".to_string();
if self.body.iter().all(|b| b.is_ascii_whitespace()) {
return Ok(());
}
let mut reader = quick_xml::Reader::from_reader(self.body.as_slice());
reader.config_mut().check_end_names = true;
let mut path: Vec<String> = Vec::new();
let mut text: Vec<String> = Vec::new();
let mut count = 0usize;
let mut unresolved_entity: Option<String> = None;
let mut buf = Vec::new();
macro_rules! cap {
($name:expr, $value:expr) => {
if push_xml_arg(&$name, $value, &mut self.args_post, &mut count) {
let msg = xml_limit_message();
self.body_error = Some(msg.clone());
return Err(msg);
}
};
}
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
if path.len() >= MAX_XML_DEPTH {
let msg = format!(
"XML body exceeded {MAX_XML_DEPTH} levels of nesting; \
only part of the body was inspected"
);
self.body_error = Some(msg.clone());
return Err(msg);
}
path.push(decode_name(e.name().as_ref()));
text.push(String::new());
for attr in e.attributes().flatten() {
let attr_name = decode_name(attr.key.as_ref());
let value = attr
.decoded_and_normalized_value(
quick_xml::XmlVersion::Implicit1_0,
reader.decoder(),
)
.map(|v| v.into_owned())
.unwrap_or_default();
let name = format!("xml.{}.@{}", path.join("."), attr_name);
cap!(name, value);
}
}
Ok(Event::End(_)) => {
if let Some(collected) = text.pop() {
let trimmed = collected.trim();
if !trimmed.is_empty() && !path.is_empty() {
let name = format!("xml.{}", path.join("."));
cap!(name, trimmed.to_string());
}
}
path.pop();
}
Ok(Event::Text(e)) => {
if let (Some(current), Ok(t)) = (text.last_mut(), e.decode()) {
current.push_str(t.as_ref());
}
}
Ok(Event::CData(e)) => {
if let Some(current) = text.last_mut() {
current.push_str(&String::from_utf8_lossy(e.as_ref()));
}
}
Ok(Event::GeneralRef(e)) => {
let name = e.decode().map(|n| n.into_owned()).unwrap_or_default();
match resolve_entity(&name) {
Some(resolved) => {
if let Some(current) = text.last_mut() {
current.push_str(&resolved);
}
}
None => {
unresolved_entity.get_or_insert(name);
}
}
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(e) => {
let msg = format!("XML parsing error: {e}");
self.body_error = Some(msg.clone());
return Err(msg);
}
}
buf.clear();
}
if !path.is_empty() {
let msg = format!(
"XML body ended with {} element(s) still open; the document is truncated \
and was only partly inspected",
path.len()
);
self.body_error = Some(msg.clone());
return Err(msg);
}
if let Some(name) = unresolved_entity {
let msg = format!(
"XML body references the undeclared or custom entity '&{name};', which is \
not expanded (expanding it is how XXE and entity-expansion attacks work). \
Any content it carries was not inspected."
);
self.body_error = Some(msg.clone());
return Err(msg);
}
Ok(())
}
pub fn all_args(&self) -> Vec<(&str, &str)> {
let mut all = self.args_get.all();
all.extend(self.args_post.all());
all
}
}
const MAX_XML_ARGS: usize = 4096;
const MAX_XML_DEPTH: usize = 64;
fn xml_limit_message() -> String {
format!(
"XML body exceeded {MAX_XML_ARGS} extracted values; only part of the \
body was inspected"
)
}
fn resolve_entity(name: &str) -> Option<String> {
if let Some(predefined) = quick_xml::escape::resolve_predefined_entity(name) {
return Some(predefined.to_string());
}
let digits = name.strip_prefix('#')?;
let code = match digits.strip_prefix(['x', 'X']) {
Some(hex) => u32::from_str_radix(hex, 16).ok()?,
None => digits.parse::<u32>().ok()?,
};
char::from_u32(code).map(|c| c.to_string())
}
fn decode_name(raw: &[u8]) -> String {
String::from_utf8_lossy(raw).to_string()
}
fn form_decode(component: &str) -> String {
let plus_decoded: Cow<'_, str> = if component.as_bytes().contains(&b'+') {
Cow::Owned(component.replace('+', " "))
} else {
Cow::Borrowed(component)
};
percent_encoding::percent_decode_str(&plus_decoded)
.decode_utf8_lossy()
.into_owned()
}
fn push_xml_arg(
name: &str,
value: String,
args: &mut HashMapCollection,
count: &mut usize,
) -> bool {
if *count >= MAX_XML_ARGS {
return true;
}
*count += 1;
args.add(name.to_string(), value);
false
}
const MAX_JSON_ARGS: usize = 4096;
const MAX_JSON_DEPTH: usize = 64;
fn flatten_json(
value: &JsonNode,
path: &mut String,
args: &mut HashMapCollection,
count: &mut usize,
depth: usize,
) -> bool {
if depth > MAX_JSON_DEPTH {
return true;
}
match value {
JsonNode::Object(entries) => {
for (key, child) in entries {
let restore = path.len();
path.push('.');
path.push_str(key);
let truncated = flatten_json(child, path, args, count, depth + 1);
path.truncate(restore);
if truncated {
return true;
}
}
false
}
JsonNode::Array(items) => {
for (index, child) in items.iter().enumerate() {
let restore = path.len();
path.push('.');
path.push_str(&index.to_string());
let truncated = flatten_json(child, path, args, count, depth + 1);
path.truncate(restore);
if truncated {
return true;
}
}
false
}
JsonNode::String(s) => push_json_arg(path, s.clone(), args, count),
JsonNode::Number(n) => push_json_arg(path, n.clone(), args, count),
JsonNode::Bool(b) => push_json_arg(path, b.to_string(), args, count),
JsonNode::Null => push_json_arg(path, String::new(), args, count),
}
}
fn push_json_arg(
path: &str,
value: String,
args: &mut HashMapCollection,
count: &mut usize,
) -> bool {
if *count >= MAX_JSON_ARGS {
return true;
}
*count += 1;
args.add(path.to_string(), value);
false
}
fn extract_multipart_boundary(content_type: &str) -> Option<String> {
for param in content_type.split(';').skip(1) {
let param = param.trim();
if let Some((key, value)) = param.split_once('=') {
if key.trim().eq_ignore_ascii_case("boundary") {
let value = value.trim().trim_matches('"');
if !value.is_empty() {
return Some(value.to_string());
}
}
}
}
None
}
fn extract_disposition_param(header_value: &str, param: &str) -> Option<String> {
for part in header_value.split(';') {
let part = part.trim();
if let Some((key, value)) = part.split_once('=') {
if key.trim().eq_ignore_ascii_case(param) {
return Some(value.trim().trim_matches('"').to_string());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_cookie_header() {
let mut req = RequestData::new();
req.add_header("Cookie", "session=abc123; theme=dark; bare");
assert_eq!(req.cookies.get("session"), Some(vec!["abc123"]));
assert_eq!(req.cookies.get("theme"), Some(vec!["dark"]));
assert_eq!(req.cookies.get("bare"), Some(vec![""]));
}
#[test]
fn test_parse_multipart_body_fields_and_headers() {
let mut req = RequestData::new();
let body = "--XyZ\r\n\
Content-Disposition: form-data; name=\"field1\"\r\n\
\r\n\
value1\r\n\
--XyZ\r\n\
Content-Disposition: form-data; name=\"upload\"; filename=\"a.txt\"\r\n\
Content-Type: text/plain\r\n\
\r\n\
file contents\r\n\
--XyZ--\r\n";
req.append_body(body.as_bytes());
assert!(req.parse_multipart_body("multipart/form-data; boundary=XyZ"));
assert_eq!(req.args_post.get("field1"), Some(vec!["value1"]));
assert_eq!(req.args_post.get("upload"), None);
assert_eq!(req.files.get("upload"), Some(vec!["a.txt"]));
let field1_headers = req.multipart_part_headers.get("field1").unwrap();
assert_eq!(
field1_headers,
vec!["Content-Disposition: form-data; name=\"field1\""]
);
let upload_headers = req.multipart_part_headers.get("upload").unwrap();
assert!(upload_headers.contains(&"Content-Type: text/plain"));
assert_eq!(req.body_processor, "MULTIPART");
}
#[test]
fn test_parse_multipart_body_quoted_boundary_and_lf_only() {
let mut req = RequestData::new();
let body = "--b1\nContent-Disposition: form-data; name=\"k\"\n\nv\n--b1--\n";
req.append_body(body.as_bytes());
assert!(req.parse_multipart_body("multipart/form-data; boundary=\"b1\""));
assert_eq!(req.args_post.get("k"), Some(vec!["v"]));
}
#[test]
fn test_parse_multipart_body_missing_boundary() {
let mut req = RequestData::new();
req.append_body(b"irrelevant");
assert!(!req.parse_multipart_body("multipart/form-data"));
assert!(req.body_processor.is_empty());
}
}