use crate::byte::{split, splitn};
use crate::http::server::HttpSettings;
use crate::http::ReadWrite;
use crate::{Fail, Result};
use std::{collections::HashMap, net::SocketAddr};
#[derive(Debug, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Head,
Connect,
Options,
Trace,
}
#[derive(Debug)]
pub struct HttpRequest<'a> {
method: HttpMethod,
url: &'a str,
headers: HashMap<String, &'a str>,
get: HashMap<String, &'a str>,
post: HashMap<String, Vec<u8>>,
ip: String,
body: Vec<u8>,
}
impl<'a> HttpRequest<'a> {
pub fn method(&self) -> &HttpMethod {
&self.method
}
pub fn url(&self) -> &str {
self.url
}
pub fn headers(&self) -> &HashMap<String, &str> {
&self.headers
}
pub fn get(&self) -> &HashMap<String, &str> {
&self.get
}
pub fn post(&self) -> &HashMap<String, Vec<u8>> {
&self.post
}
pub fn post_utf8(&self) -> HashMap<String, String> {
let mut post_utf8 = HashMap::new();
for (k, v) in &self.post {
post_utf8.insert(k.to_string(), String::from_utf8_lossy(v).to_string());
}
post_utf8
}
pub fn body(&self) -> &[u8] {
&self.body
}
pub fn ip(&self) -> &str {
&self.ip
}
pub fn from(
raw_header: &'a str,
mut partial_body: Vec<u8>,
stream: &mut impl ReadWrite,
address: SocketAddr,
settings: &HttpSettings,
) -> Result<Self> {
let mut header = raw_header.lines();
let mut reqln = header
.next()
.ok_or_else(|| Fail::new("Empty header"))?
.split(' ');
let method = match reqln
.next()
.ok_or_else(|| Fail::new("No method in header"))?
{
"GET" => Ok(HttpMethod::Get),
"POST" => Ok(HttpMethod::Post),
"PUT" => Ok(HttpMethod::Put),
"DELETE" => Ok(HttpMethod::Delete),
"HEAD" => Ok(HttpMethod::Head),
"CONNECT" => Ok(HttpMethod::Connect),
"OPTIONS" => Ok(HttpMethod::Options),
"TRACE" => Ok(HttpMethod::Trace),
_ => Fail::from("Invalid method in header"),
}?;
let mut get_raw = "";
let url = if let Some(full_url) = reqln.next() {
let mut split_url = full_url.splitn(2, '?');
let url = split_url
.next()
.ok_or_else(|| Fail::new("No URL in header"))?;
if let Some(params) = split_url.next() {
get_raw = params;
}
url
} else {
"/"
};
let mut headers = HashMap::new();
header.for_each(|hl| {
let mut hls = hl.splitn(2, ':');
if let (Some(key), Some(value)) = (hls.next(), hls.next()) {
headers.insert(key.trim().to_lowercase(), value.trim());
}
});
let buf_len = if let Some(buf_len) = headers.get("Content-Length") {
Some(buf_len)
} else {
headers.get("content-length")
};
if let Some(buf_len) = buf_len {
let con_len = buf_len
.parse::<usize>()
.ok()
.ok_or_else(|| Fail::new("Content-Length is not of type usize"))?;
if con_len > settings.max_body_size {
return Fail::from("Max body size exceeded");
}
let mut read_fails = 0;
while partial_body.len() < con_len {
let mut rest_body = vec![0u8; settings.body_buffer];
let length = stream
.read(&mut rest_body)
.ok()
.ok_or_else(|| Fail::new("Stream broken"))?;
rest_body.truncate(length);
partial_body.append(&mut rest_body);
if length < settings.body_buffer {
read_fails += 1;
if read_fails > settings.body_read_attempts {
return Fail::from("Read body failed too often");
}
}
}
}
let get = parse_parameters(get_raw, |v| v)?;
let post = parse_post(&headers, &partial_body).unwrap_or_default();
let ip = match headers.get("x-real-ip") {
Some(x_real_ip) if address.ip().is_loopback() => x_real_ip.to_string(),
_ => address.ip().to_string(),
};
Ok(Self {
method,
url,
headers,
get,
post,
ip,
body: partial_body,
})
}
}
fn parse_post(headers: &HashMap<String, &str>, body: &[u8]) -> Result<HashMap<String, Vec<u8>>> {
match headers.get("content-type") {
Some(&content_type_header) => {
let mut content_type_header = content_type_header.split(';').map(|s| s.trim());
let mut content_type = None;
let boundary = content_type_header.find_map(|s| {
if s.starts_with("boundary=") {
return s.split('=').nth(1);
} else if content_type.is_none() {
content_type = Some(s);
}
None
});
match content_type {
Some(content_type) => {
if content_type == "multipart/form-data" {
parse_post_upload(
body,
boundary.ok_or_else(|| Fail::new("post upload, but no boundary"))?,
)
} else {
parse_parameters(&String::from_utf8(body.to_vec())?, |v| {
v.as_bytes().to_vec()
})
}
}
None => parse_parameters(&String::from_utf8(body.to_vec())?, |v| {
v.as_bytes().to_vec()
}),
}
}
None => parse_parameters(&String::from_utf8(body.to_vec())?, |v| {
v.as_bytes().to_vec()
}),
}
}
fn parse_post_upload(body: &[u8], boundary: &str) -> Result<HashMap<String, Vec<u8>>> {
let mut params = HashMap::new();
let mut sections = split(&body, &format!("--{boundary}\r\n"));
sections.remove(0);
for mut section in sections {
let last_sep = format!("--{boundary}--\r\n");
if section.ends_with(last_sep.as_bytes()) {
section = §ion[..(section.len() - last_sep.len() - 2)];
}
let lines = splitn(3, §ion, b"\r\n");
let name = String::from_utf8_lossy(lines[0])
.split(';')
.map(|s| s.trim())
.find_map(|s| {
if s.starts_with("name=") {
let name = s.split('=').nth(1)?;
Some(name[1..(name.len() - 1)].to_lowercase())
} else {
None
}
})
.ok_or_else(|| Fail::new("missing name in post body section"))?;
let data_section = lines
.get(2)
.ok_or_else(|| Fail::new("broken section in post body"))?;
let data_lines = splitn(2, data_section, b"\r\n");
let next_data_line = data_lines
.first()
.ok_or_else(|| Fail::new("broken section in post body"))?;
let value = if let Some(file_data_line) = data_lines.get(1) {
if next_data_line.is_empty() {
file_data_line.to_vec()
} else if file_data_line.is_empty() {
next_data_line.to_vec()
} else {
[&next_data_line[..], &b"\r\n"[..], &file_data_line[..]]
.concat()
.to_vec()
}
} else {
next_data_line.to_vec()
};
params.insert(name, value);
}
Ok(params)
}
fn parse_parameters<'a, V>(
raw: &'a str,
process_value: fn(&'a str) -> V,
) -> Result<HashMap<String, V>> {
let mut params = HashMap::new();
for p in raw.split('&') {
let mut ps = p.splitn(2, '=');
params.insert(
ps.next()
.ok_or_else(|| Fail::new("broken x-www-form-urlencoded parameters"))?
.trim()
.to_lowercase(), process_value(if let Some(value) = ps.next() {
value.trim() } else {
"" }),
);
}
Ok(params)
}