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