From 06d2d7a9572fad15dd9fe72567e13c42956bdedc Mon Sep 17 00:00:00 2001 From: "Dawid J. Kubis" Date: Sat, 27 May 2023 16:49:07 +0200 Subject: [PATCH] added image support, multipart/form-data parsing (not too great but will do), added image serving, removed unnecessary module `html.rs` --- Cargo.lock | 8 +-- README.md | 3 +- src/errors.rs | 6 +-- src/html.rs | 9 ---- src/http.rs | 123 +++++++++++++++++++++++++++++++++++++-------- src/main.rs | 64 +++++++++++++++-------- src/post.rs | 6 +-- src/www/index.html | 3 +- 8 files changed, 159 insertions(+), 63 deletions(-) delete mode 100644 src/html.rs diff --git a/Cargo.lock b/Cargo.lock index c9c49bd..5bfe406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,7 +347,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.17", + "syn 2.0.18", ] [[package]] @@ -426,9 +426,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45b6ddbb36c5b969c182aec3c4a0bce7df3fbad4b77114706a49aacc80567388" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -470,7 +470,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.17", + "syn 2.0.18", ] [[package]] diff --git a/README.md b/README.md index 2f33a8c..74c868b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ + single page + threads might be deleted if they reach maximum storage capacity - the least active thread will then be removed. -+ id's will be attached to every thread and every response ++ ids will be attached to every thread and every response based on the order of creation. ++ images are served on `/img` ### POST usage diff --git a/src/errors.rs b/src/errors.rs index ce99b57..232adc6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -33,8 +33,6 @@ pub enum DatabaseError { SerError, } -pub enum InternalError {} - #[derive(Debug, Error)] pub enum HandlingError { #[error("client error: {0}")] @@ -48,7 +46,7 @@ pub enum HandlingError { impl From for Status { fn from(err: DatabaseError) -> Self { match err { - DatabaseError::SledError(e) => Status::InternalServerError, + DatabaseError::SledError(_) => Status::InternalServerError, DatabaseError::NotInDb => Status::BadRequest, DatabaseError::SerError => Status::InternalServerError, } @@ -59,7 +57,7 @@ impl From for Status { fn from(err: RequestError) -> Self { match err { RequestError::NotAForm => Status::BadRequest, - RequestError::NotFound => Status::BadRequest, + RequestError::NotFound => Status::NotFound, RequestError::UnusedMethod => Status::BadRequest, RequestError::NotAuthorized(_) => Status::Unauthorized, RequestError::UrlDecodeErr(_) => Status::BadRequest, diff --git a/src/html.rs b/src/html.rs deleted file mode 100644 index ebaa56c..0000000 --- a/src/html.rs +++ /dev/null @@ -1,9 +0,0 @@ -// statically linked index and favicon -pub const INDEX: &'static str = include_str!("www/index.html"); -pub const FAVICON: &'static [u8] = include_bytes!("www/favicon.ico"); -pub const STYLE: &'static str = include_str!("www/style.css"); -pub const FAQ: &'static str = include_str!("www/faq.html"); - -pub trait Htmlizable { - fn to_html(&self) -> String; -} diff --git a/src/http.rs b/src/http.rs index 4579642..191f94c 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,12 +1,13 @@ -use std::collections::HashMap; use std::fmt; use std::str::{FromStr, Lines}; use crate::errors::{HandlingError, RequestError}; +use html_escape::encode_text; + const HTTP_VERSION: &'static str = "HTTP/1.1"; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Request { pub method: Method, pub uri: String, @@ -31,7 +32,7 @@ pub enum Status { InternalServerError = 500, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Method { Get, Post, @@ -63,24 +64,107 @@ impl Status { } } -//impl Request { -// pub fn form(&self) -> Result, RequestError> { -// let mut hashmap = HashMap::new(); -// dbg!(&self.body); -// for mut x in self.body.split("&").map(|x| x.split("=")) { -// hashmap.insert( -// x.next().ok_or(RequestError::NotAForm)?, -// decode(x.next().ok_or(RequestError::NotAForm)?)?.into_owned().replace("+", " "), -// ); -// } -// Ok(hashmap) -// } -//} +#[derive(Debug)] +pub struct Form { + pub content: String, + pub image: Option>, +} + +// TODO do this more imperatively, fuck this complicated splitting shit +impl TryFrom<&Request> for Form { + type Error = RequestError; + + fn try_from(value: &Request) -> Result { + // check if body + let body = value.body.as_ref().ok_or(RequestError::MissingBody)?; + + // get boundary + + let mut boundary: Vec = vec![]; + boundary.extend_from_slice(b"--"); + boundary.extend( + value + .headers + .iter() + .find(|(a, _)| a.to_lowercase() == "content-type") + .map(|(_, b)| b) + .ok_or(RequestError::NotAForm)? + .chars() + .skip_while(|&c| c != '=') // NOTE hope this means boundary + .skip(1) + .map(|c| c as u8), + ); + //boundary.extend_from_slice(b"\r\n"); + //boundary.extend_from_slice(b"\n"); + + // setup basic stuff + let sep = b"\r\n\r\n"; + let mut pos = 0; + // get content + pos += body + .windows(boundary.len()) + .position(|x| x == boundary) + .ok_or(RequestError::NotAForm)? + + boundary.len(); + // skip useless shit and get to the point + pos += body[pos..] + .windows(sep.len()) + .position(|x| x == sep) + .ok_or(RequestError::NotAForm)? + + sep.len(); + let temp = body[pos..] + .windows(boundary.len()) + .position(|x| x == boundary) + .ok_or(RequestError::NotAForm)? + + pos; + + let content = String::from_utf8_lossy(&body[pos..temp]).to_string(); + //let content = encode_text(&content.trim().to_string()).to_string(); + // do that later + dbg!(&content); + + // good so far + + pos = temp + boundary.len(); + pos += body[pos..] + .windows(sep.len()) + .position(|x| x == sep) + .ok_or(RequestError::NotAForm)? + + sep.len(); + // now at image data + + let temp = body[pos..] + .windows(boundary.len()) + .position(|x| x == boundary) + .ok_or(RequestError::NotAForm)? + + pos; + + let image = if &body[pos..temp] == b"\r\n" { + dbg!("no image"); + None + } else { + Some(body[pos..temp].to_owned()) + }; + + Ok(Form { content, image }) + } +} impl Request { pub fn add_body(&mut self, body: Vec) { self.body = Some(body); } + // pub fn form(&self) -> Result, RequestError> { + // let mut hashmap = HashMap::new(); + // dbg!(&self.body); + // for mut x in self.body.split("&").map(|x| x.split("=")) { + // hashmap.insert( + // x.next().ok_or(RequestError::NotAForm)?, + // decode(x.next().ok_or(RequestError::NotAForm)?)?.into_owned().replace("+", " "), + // ); + // } + // Ok(hashmap) + // } } impl TryFrom> for Request { @@ -128,14 +212,14 @@ impl Response { pub fn respond(&self) -> Vec { let mut res = vec![]; res.extend_from_slice(HTTP_VERSION.as_bytes()); - res.extend_from_slice(b" "); + res.push(b' '); res.extend_from_slice(&(self.status as i32).to_string().as_bytes()); // network endianness - res.extend_from_slice(b" "); + res.push(b' '); res.extend_from_slice(self.status.message().as_bytes()); res.extend_from_slice(b"\r\n"); for (i, j) in self.headers.iter() { res.extend_from_slice(i.as_bytes()); - res.extend_from_slice(b":"); + res.push(b':'); res.extend_from_slice(j.as_bytes()); res.extend_from_slice(b"\r\n"); } @@ -144,4 +228,3 @@ impl Response { res } } - diff --git a/src/main.rs b/src/main.rs index 0852bb6..52ab05d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,9 @@ mod errors; -mod html; mod http; mod post; -use crate::errors::{HandlingError, InternalError, RequestError}; -use crate::html::{FAQ, FAVICON, INDEX, STYLE}; -use crate::http::{Request, Response, Status}; +use crate::errors::{HandlingError, RequestError}; +use crate::http::{Form, Request, Response, Status}; use crate::post::{Post, Thread}; use std::error::Error; @@ -31,6 +29,10 @@ use urlencoding::decode; // use this instead of hard-coding type ID_TYPE = u32; +pub const INDEX: &'static str = include_str!("www/index.html"); +pub const FAVICON: &'static [u8] = include_bytes!("www/favicon.ico"); +pub const STYLE: &'static str = include_str!("www/style.css"); +pub const FAQ: &'static str = include_str!("www/faq.html"); // represents command line arguments #[derive(StructOpt, Debug)] @@ -91,7 +93,7 @@ fn get_thread(id: u32) -> Result, DatabaseError> { /// generates next id fn next_id() -> Result { - DB.fetch_and_update(b"id", increment) + DB.update_and_fetch(b"id", increment) .map(|x| match x { Some(s) => { let buf: [u8; 4] = (*s).try_into().unwrap(); @@ -184,7 +186,7 @@ fn handle(mut reader: BufReader<&mut TcpStream>) -> Result = reader @@ -197,7 +199,7 @@ fn handle(mut reader: BufReader<&mut TcpStream>) -> Result get(&request.uri), @@ -224,12 +226,19 @@ fn get(path: &str) -> Result { Ok(Response::new(Status::Ok, vec![], c.into())) } // TODO favicon.ico - "/css" => Ok(Response::new(Status::Ok, vec![], String::from(STYLE).into())), + "/css" => Ok(Response::new( + Status::Ok, + vec![], + String::from(STYLE).into(), + )), "/faq" => Ok(Response::new(Status::Ok, vec![], String::from(FAQ).into())), - "/favicon.ico" => Ok(Response::new(Status::Ok, vec![("content-type", "image/x-icon")], FAVICON.to_vec())), + "/favicon.ico" => Ok(Response::new( + Status::Ok, + vec![("content-type", "image/x-icon")], + FAVICON.to_vec(), + )), // list specific thread here - // FIXME unwrap hell s => { let id = s .trim_start_matches("/") @@ -238,22 +247,35 @@ fn get(path: &str) -> Result { .unwrap() .parse::() .map_err(|_| RequestError::NotFound)?; - let c = get_thread(id)? - .iter() - .fold(String::from(""), |a, b| format!("{a}\n{b}")); - let c = content(&id.to_string(), &c); - - Ok(Response::new(Status::Ok, vec![], c.into())) + if s.ends_with("img") { + Ok(Response::new( + Status::Ok, + vec![], + get_post(id)?.img.ok_or(RequestError::NotFound)?, + )) + } else { + let c = get_thread(id)? + .iter() + .fold(String::from(""), |a, b| format!("{a}\n{b}")); + let c = content(&id.to_string(), &c); + Ok(Response::new(Status::Ok, vec![], c.into())) + } } } } fn post(request: Request) -> Result { - let binding = &request.body.ok_or(RequestError::MissingBody)?; - let c = String::from_utf8_lossy(&binding); - let c = encode_text(&c).into_owned(); + // TODO check content-type + + let form = Form::try_from(&request)?; - let post = Post::new(0, c); + let mut post = Post::new( + 0, + dbg!(encode_text(form.content.trim())) + .replace("\r\n", "
") + .to_string(), + ); + post.img = form.image; match request.uri.as_str() { // means we wish to create a new thread @@ -326,7 +348,7 @@ fn main() { }; pool.execute(move || { - let reader = BufReader::new(&mut stream); + let reader = BufReader::with_capacity(2 << 20, &mut stream); // 2^20, should be enough for images let response = match handle(reader) { Ok(s) => s, diff --git a/src/post.rs b/src/post.rs index bb7f30a..941e3c0 100644 --- a/src/post.rs +++ b/src/post.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Post { pub id: u32, // technically reduntant but whatever - //pub img: Option, + pub img: Option>, pub body: String, } @@ -40,7 +40,7 @@ impl Post { pub fn new(id: u32, body: String) -> Self { Self { id, - //img, + img: None, body, } } @@ -52,7 +52,7 @@ impl fmt::Display for Post { write!( f, "
\ - \"img\"\ + \"img\"\
\
\
\ diff --git a/src/www/index.html b/src/www/index.html index 973600a..5e5e327 100644 --- a/src/www/index.html +++ b/src/www/index.html @@ -19,8 +19,9 @@

Create new thread

-
+ +