From fe8dba588850c16b28b955a51c871504094e6fdc Mon Sep 17 00:00:00 2001 From: Darius Auding <Darius.auding@gmx.de> Date: Sun, 4 Jun 2023 20:58:42 +0200 Subject: [PATCH] Add multipart post text form support, check for `Content-Length` Header --- core/http/src/handlers/handlers.rs | 45 ++++----- core/http/src/handling/request.rs | 140 ++++++++++++++++++++++++-- core/http/src/handling/response.rs | 4 +- core/http/src/handling/routes.rs | 32 +++++- core/http/src/setup.rs | 1 + core/http/src/utils/mime/mime_enum.rs | 5 + site/404.html | 5 +- site/src/main.rs | 8 +- 8 files changed, 196 insertions(+), 44 deletions(-) diff --git a/core/http/src/handlers/handlers.rs b/core/http/src/handlers/handlers.rs index f81441c..63172c5 100755 --- a/core/http/src/handlers/handlers.rs +++ b/core/http/src/handlers/handlers.rs @@ -12,7 +12,7 @@ use crate::setup::MountPoint; pub async fn handle_connection(mut stream: TcpStream, mountpoints: Vec<MountPoint<'_>>) { let mut buf_reader = BufReader::new(&mut stream); - let mut http_request: Vec<String> = Vec::with_capacity(100); + let mut http_request: Vec<String> = Vec::with_capacity(30); loop { let mut buffer = String::new(); if let Err(_) = buf_reader.read_line(&mut buffer).await { @@ -74,26 +74,28 @@ pub async fn handle_connection(mut stream: TcpStream, mountpoints: Vec<MountPoin if let Ok(size) = len { size } else { - 0 + eprintln!("\x1b[31m`{}` must have a `Content-Length` header\x1b[0m", request.method); + len_not_defined(stream, Status::LengthRequired).await; + return; } } else { + if request.mandatory_body() { + eprintln!("\x1b[31m`{}` must have a `Content-Length` header\x1b[0m", request.method); + len_not_defined(stream, Status::LengthRequired).await; + return; + } 0 }; - - let mut buffer: Vec<u8> = vec![]; - buf_reader.read_buf(&mut buffer).await.unwrap(); - if buffer.len() != length { - let respone = len_not_defined(Status::LengthRequired); - let mut response = respone.0.as_bytes().to_vec(); - response.extend_from_slice(&respone.1); - if let Err(e) = stream.write_all(&response).await { - eprintln!("\x1b[31mError {e} occured when trying to write answer to TCP-Stream for Client, aborting\x1b[0m"); + if length != 0 { + let mut buffer: Vec<u8> = vec![]; + buf_reader.read_buf(&mut buffer).await.unwrap(); + if buffer.len() != length { + len_not_defined(stream, Status::LengthRequired).await; return; } - return; + data.is_complete = true; + data.buffer = buffer; } - data.is_complete = true; - data.buffer = buffer; } let mut handled_response: Option<Outcome<Response, Status, Data>> = None; @@ -165,14 +167,11 @@ fn failure_handler(status: Status) -> (String, Vec<u8>) { ) } -fn len_not_defined(status: Status) -> (String, Vec<u8>) { +async fn len_not_defined(mut stream: TcpStream, status: Status) { let page_411 = NamedFile::open(PathBuf::from("411.html")).unwrap(); - ( - format!( - "HTTP/1.1 {status}\r\nContent-Length: {}\r\nContent-Type: {}\r\n\r\n", - page_411.get_len(), - page_411.get_mime() - ), - page_411.get_data(), - ) + let mut response = format!("HTTP/1.1 {}\r\nContent-Length: {}\r\nContent-Type: {}\r\n\r\n", status, page_411.get_len(), page_411.get_mime()).as_bytes().to_vec(); + response.extend_from_slice(&page_411.get_data()); + if let Err(e) = stream.write_all(&response).await { + eprintln!("\x1b[31mError {e} occured when trying to write answer to TCP-Stream for Client, aborting\x1b[0m"); + } } diff --git a/core/http/src/handling/request.rs b/core/http/src/handling/request.rs index f90bacb..5f02d3d 100644 --- a/core/http/src/handling/request.rs +++ b/core/http/src/handling/request.rs @@ -2,10 +2,7 @@ use std::{collections::HashMap, error::Error, fmt::Display}; -trait FromPost {} - -impl FromPost for &str {} -impl FromPost for Vec<u8> {} +use crate::utils::mime::mime_enum::Mime; use super::{ methods::Method, @@ -68,6 +65,12 @@ impl Request<'_> { _ => false, } } + pub fn mandatory_body(&self) -> bool { + match self.method { + Method::Post | Method::Put | Method::Patch => true, + _ => false, + } + } // pub fn get_post_form_key<T: FromRequest>(&self, data: Data) -> T {} pub fn get_get_form_keys( &self, @@ -108,7 +111,132 @@ impl Request<'_> { } Ok(response) } - pub fn get_post_form_key(&self, key: &str, data: Data) { - todo!() + pub fn get_post_text_form_key(&self, key: &str, data: &Data) -> Result<String, ()> { + let mut post_type = self + .headers + .iter() + .find(|header| header.contains("Content-Type: ")) + .unwrap() + .to_string(); + + post_type = post_type + .strip_prefix("Content-Type: ") + .unwrap() + .to_string(); + + let post_type: Vec<&str> = post_type.trim().split(';').collect(); + let mime_type = post_type[0].parse::<Mime>().unwrap(); + + match mime_type { + Mime::ApplicationXWwwFormUrlencoded => { + let data = String::from_utf8(data.buffer.clone()).unwrap(); + let kvps = data + .split("&") + .map(|kvp| kvp.split_once("=").unwrap()) + .collect::<HashMap<&str, &str>>(); + if let Some(val) = kvps.get(key) { + Ok(val.to_string()) + } else { + Err(()) + } + } + Mime::MultipartFormData => { + let from_req = post_type[1..] + .iter() + .find(|val| val.contains("boundary=")) + .unwrap() + .strip_prefix("boundary=") + .unwrap(); + let mut boundary = "--".as_bytes().to_vec(); + boundary.extend_from_slice(from_req.trim_matches('"').as_bytes()); + let mut end_boundary = boundary.clone(); + end_boundary.extend_from_slice(b"--"); + boundary.extend_from_slice(&[b'\r']); + let parts = data + .buffer + .split(|byte| byte == &b'\n') + .collect::<Vec<&[u8]>>(); + + let mut boundary_found = false; + let mut key_found = false; + let mut value = vec![]; + for part in parts { + if part == [] { + continue; + } + if (key_found && part == boundary) || part == end_boundary { + break; + } + if !boundary_found && part == boundary { + boundary_found = true; + continue; + } + if part.starts_with(b"Content-Disposition: form-data; name=") { + let headers = part + .split(|byte| byte == &b';') + .filter(|header| !header.is_empty()) + .collect::<Vec<_>>(); + if headers.len() < 2 { + continue; + } + let name = headers[1].split(|byte| byte == &b'=').collect::<Vec<_>>(); + if name.len() != 2 { + continue; + } + let mkey = String::from_utf8_lossy(name[1]) + .to_string() + .trim_end() + .trim_matches('"') + .to_owned(); + key_found = key == mkey; + } else if key_found == true { + value.extend_from_slice(part); + value.extend_from_slice(&[b'\n']); + } + } + Ok(String::from_utf8_lossy(&value) + .to_owned() + .trim() + .to_string()) + } + _ => Err(()), + } + } +} + +#[cfg(test)] +mod test { + use crate::handling::routes::Data; + + use super::Request; + + #[test] + fn try_post_text() { + let req = Request { + uri: "", + headers: vec!["Content-Type: multipart/form-data;boundary=\"boundary\"".to_string()], + method: crate::handling::methods::Method::Post, + }; + let data = Data { + buffer: b"--boundary\r +Content-Disposition: form-data; name=\"field1\"\r +\r +value1\r +--boundary\r +Content-Disposition: form-data; name=\"field2\"; filename=\"example.txt\"\n\r +\r +value2\r +--boundary--" + .to_vec(), + is_complete: true, + }; + assert_eq!( + "value1", + req.get_post_text_form_key("field1", &data).unwrap() + ); + assert_eq!( + "value2", + req.get_post_text_form_key("field2", &data).unwrap() + ); } } diff --git a/core/http/src/handling/response.rs b/core/http/src/handling/response.rs index 2f7fdcf..81a54af 100644 --- a/core/http/src/handling/response.rs +++ b/core/http/src/handling/response.rs @@ -21,10 +21,10 @@ pub trait ResponseBody: Send { impl ResponseBody for Body { fn get_data(&self) -> Vec<u8> { - self.body.clone() + self.body() } fn get_mime(&self) -> Mime { - Mime::TextPlain + self.mime_type() } fn get_len(&self) -> usize { diff --git a/core/http/src/handling/routes.rs b/core/http/src/handling/routes.rs index 9ded1ce..9fd3f77 100644 --- a/core/http/src/handling/routes.rs +++ b/core/http/src/handling/routes.rs @@ -1,7 +1,10 @@ -use super::{ - methods::Method, - request::{MediaType, Request}, - response::{Outcome, Response, Status}, +use crate::{ + handling::{ + methods::Method, + request::{MediaType, Request}, + response::{Outcome, Response, Status}, + }, + utils::mime::mime_enum::Mime, }; pub struct RoutInfo { @@ -63,7 +66,26 @@ pub type Uri<'a> = &'a str; #[derive(Debug, Clone)] pub struct Body { - pub body: Vec<u8>, + body: Vec<u8>, + mime_type: Mime, +} + +impl Body { + pub fn new(body: Vec<u8>, mime_type: Mime) -> Self { + Self { body, mime_type } + } + pub fn set_mime_type(&mut self, mime_type: Mime) { + self.mime_type = mime_type; + } + pub fn set_body(&mut self, body: Vec<u8>) { + self.body = body; + } + pub fn mime_type(&self) -> Mime { + self.mime_type + } + pub fn body(&self) -> Vec<u8> { + self.body.clone() + } } #[derive(Debug, Clone)] diff --git a/core/http/src/setup.rs b/core/http/src/setup.rs index 9f57e76..50f5144 100644 --- a/core/http/src/setup.rs +++ b/core/http/src/setup.rs @@ -66,6 +66,7 @@ impl<'a> Config { self } pub async fn launch(self) { + println!("Server launched from http://{}", self.address.local_addr().unwrap()); let mut sigint = signal(SignalKind::interrupt()).unwrap(); loop { select! { diff --git a/core/http/src/utils/mime/mime_enum.rs b/core/http/src/utils/mime/mime_enum.rs index fbf4415..0a6a068 100644 --- a/core/http/src/utils/mime/mime_enum.rs +++ b/core/http/src/utils/mime/mime_enum.rs @@ -4061,6 +4061,11 @@ impl std::str::FromStr for Mime { type Err = ParseMimeError; fn from_str(s: &str) -> Result<Self, Self::Err> { + let s = if let Some(str) = s.split_once(';') { + str.0 + } else { + s + }; match MIME_MAP.get(s).copied() { Some(mimetype) => Ok(mimetype), None => Err(ParseMimeError(1)), diff --git a/site/404.html b/site/404.html index 0a1a4ed..7934c61 100644 --- a/site/404.html +++ b/site/404.html @@ -7,13 +7,10 @@ <body> <h1>404</h1> <p>Hi from Rust</p> - <form action="/" method="POST"> + <form action="/post/post" method="post"> <label for="message">Enter your message:</label> <input type="text" id="message" name="message" /> <button type="submit">Send</button> - <label for="message">Enter your message:</label> - <input type="text" id="asdf" name="message" /> - <button type="submit">Send</button> </form> <form action="/static/hi" method="get"> <label for="jump">Enter asdf</label> diff --git a/site/src/main.rs b/site/src/main.rs index deb31d6..1005312 100644 --- a/site/src/main.rs +++ b/site/src/main.rs @@ -58,12 +58,12 @@ fn fileserver(path: &str) -> Result<NamedFile, Status> { NamedFile::open(PathBuf::from("static/".to_string() + path)) } -fn post_hi_handler(_request: Request, data: Data) -> Outcome<Response, Status, Data> { +fn post_hi_handler(request: Request, data: Data) -> Outcome<Response, Status, Data> { if data.is_empty() { return Outcome::Forward(data); } - let data = if let Ok(str) = String::from_utf8(data.buffer) { - str + let data = if let Ok(val) = request.get_post_text_form_key("message", &data) { + val } else { return Outcome::Failure(Status::BadRequest); }; @@ -111,7 +111,7 @@ async fn main() { rank: 0, }; - http::build("192.168.178.32:8000") + http::build("127.0.0.1:8000") .await .mount("/", vec![fileserver, post_test, static_hi]) .mount("/post/", vec![post_test]) -- GitLab