added image support, multipart/form-data parsing (not too great but will do),

added image serving, removed unnecessary module `html.rs`
doctorpavel
Dawid J. Kubis 2 years ago
parent 1651126606
commit 06d2d7a957

8
Cargo.lock generated

@ -347,7 +347,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.17", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -426,9 +426,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.17" version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45b6ddbb36c5b969c182aec3c4a0bce7df3fbad4b77114706a49aacc80567388" checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -470,7 +470,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.17", "syn 2.0.18",
] ]
[[package]] [[package]]

@ -4,8 +4,9 @@
+ single page + single page
+ threads might be deleted if they reach maximum storage + threads might be deleted if they reach maximum storage
capacity - the least active thread will then be removed. 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. based on the order of creation.
+ images are served on `<id>/img`
### POST usage ### POST usage

@ -33,8 +33,6 @@ pub enum DatabaseError {
SerError, SerError,
} }
pub enum InternalError {}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum HandlingError { pub enum HandlingError {
#[error("client error: {0}")] #[error("client error: {0}")]
@ -48,7 +46,7 @@ pub enum HandlingError {
impl From<DatabaseError> for Status { impl From<DatabaseError> for Status {
fn from(err: DatabaseError) -> Self { fn from(err: DatabaseError) -> Self {
match err { match err {
DatabaseError::SledError(e) => Status::InternalServerError, DatabaseError::SledError(_) => Status::InternalServerError,
DatabaseError::NotInDb => Status::BadRequest, DatabaseError::NotInDb => Status::BadRequest,
DatabaseError::SerError => Status::InternalServerError, DatabaseError::SerError => Status::InternalServerError,
} }
@ -59,7 +57,7 @@ impl From<RequestError> for Status {
fn from(err: RequestError) -> Self { fn from(err: RequestError) -> Self {
match err { match err {
RequestError::NotAForm => Status::BadRequest, RequestError::NotAForm => Status::BadRequest,
RequestError::NotFound => Status::BadRequest, RequestError::NotFound => Status::NotFound,
RequestError::UnusedMethod => Status::BadRequest, RequestError::UnusedMethod => Status::BadRequest,
RequestError::NotAuthorized(_) => Status::Unauthorized, RequestError::NotAuthorized(_) => Status::Unauthorized,
RequestError::UrlDecodeErr(_) => Status::BadRequest, RequestError::UrlDecodeErr(_) => Status::BadRequest,

@ -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;
}

@ -1,12 +1,13 @@
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::str::{FromStr, Lines}; use std::str::{FromStr, Lines};
use crate::errors::{HandlingError, RequestError}; use crate::errors::{HandlingError, RequestError};
use html_escape::encode_text;
const HTTP_VERSION: &'static str = "HTTP/1.1"; const HTTP_VERSION: &'static str = "HTTP/1.1";
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Request { pub struct Request {
pub method: Method, pub method: Method,
pub uri: String, pub uri: String,
@ -31,7 +32,7 @@ pub enum Status {
InternalServerError = 500, InternalServerError = 500,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
pub enum Method { pub enum Method {
Get, Get,
Post, Post,
@ -63,7 +64,96 @@ impl Status {
} }
} }
//impl Request { #[derive(Debug)]
pub struct Form {
pub content: String,
pub image: Option<Vec<u8>>,
}
// TODO do this more imperatively, fuck this complicated splitting shit
impl TryFrom<&Request> for Form {
type Error = RequestError;
fn try_from(value: &Request) -> Result<Self, Self::Error> {
// check if body
let body = value.body.as_ref().ok_or(RequestError::MissingBody)?;
// get boundary
let mut boundary: Vec<u8> = 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<u8>) {
self.body = Some(body);
}
// pub fn form(&self) -> Result<HashMap<&str, String>, RequestError> { // pub fn form(&self) -> Result<HashMap<&str, String>, RequestError> {
// let mut hashmap = HashMap::new(); // let mut hashmap = HashMap::new();
// dbg!(&self.body); // dbg!(&self.body);
@ -75,12 +165,6 @@ impl Status {
// } // }
// Ok(hashmap) // Ok(hashmap)
// } // }
//}
impl Request {
pub fn add_body(&mut self, body: Vec<u8>) {
self.body = Some(body);
}
} }
impl TryFrom<Vec<String>> for Request { impl TryFrom<Vec<String>> for Request {
@ -128,14 +212,14 @@ impl Response {
pub fn respond(&self) -> Vec<u8> { pub fn respond(&self) -> Vec<u8> {
let mut res = vec![]; let mut res = vec![];
res.extend_from_slice(HTTP_VERSION.as_bytes()); 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(&(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(self.status.message().as_bytes());
res.extend_from_slice(b"\r\n"); res.extend_from_slice(b"\r\n");
for (i, j) in self.headers.iter() { for (i, j) in self.headers.iter() {
res.extend_from_slice(i.as_bytes()); 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(j.as_bytes());
res.extend_from_slice(b"\r\n"); res.extend_from_slice(b"\r\n");
} }
@ -144,4 +228,3 @@ impl Response {
res res
} }
} }

@ -1,11 +1,9 @@
mod errors; mod errors;
mod html;
mod http; mod http;
mod post; mod post;
use crate::errors::{HandlingError, InternalError, RequestError}; use crate::errors::{HandlingError, RequestError};
use crate::html::{FAQ, FAVICON, INDEX, STYLE}; use crate::http::{Form, Request, Response, Status};
use crate::http::{Request, Response, Status};
use crate::post::{Post, Thread}; use crate::post::{Post, Thread};
use std::error::Error; use std::error::Error;
@ -31,6 +29,10 @@ use urlencoding::decode;
// use this instead of hard-coding // use this instead of hard-coding
type ID_TYPE = u32; 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 // represents command line arguments
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
@ -91,7 +93,7 @@ fn get_thread(id: u32) -> Result<Vec<Post>, DatabaseError> {
/// generates next id /// generates next id
fn next_id() -> Result<u32, DatabaseError> { fn next_id() -> Result<u32, DatabaseError> {
DB.fetch_and_update(b"id", increment) DB.update_and_fetch(b"id", increment)
.map(|x| match x { .map(|x| match x {
Some(s) => { Some(s) => {
let buf: [u8; 4] = (*s).try_into().unwrap(); let buf: [u8; 4] = (*s).try_into().unwrap();
@ -184,7 +186,7 @@ fn handle(mut reader: BufReader<&mut TcpStream>) -> Result<Response, HandlingErr
if let Some(s) = request if let Some(s) = request
.headers .headers
.iter() .iter()
.find(|(a, b)| a.to_lowercase() == "content-length") .find(|(a, _)| a.to_lowercase() == "content-length")
{ {
if request.method == Method::Post { if request.method == Method::Post {
let body: Vec<u8> = reader let body: Vec<u8> = reader
@ -197,7 +199,7 @@ fn handle(mut reader: BufReader<&mut TcpStream>) -> Result<Response, HandlingErr
request.add_body(body); request.add_body(body);
} }
} }
dbg!(&request); //dbg!(&request);
match request.method { match request.method {
Method::Get => get(&request.uri), Method::Get => get(&request.uri),
@ -224,12 +226,19 @@ fn get(path: &str) -> Result<Response, HandlingError> {
Ok(Response::new(Status::Ok, vec![], c.into())) Ok(Response::new(Status::Ok, vec![], c.into()))
} }
// TODO favicon.ico // 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())), "/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 // list specific thread here
// FIXME unwrap hell
s => { s => {
let id = s let id = s
.trim_start_matches("/") .trim_start_matches("/")
@ -238,22 +247,35 @@ fn get(path: &str) -> Result<Response, HandlingError> {
.unwrap() .unwrap()
.parse::<u32>() .parse::<u32>()
.map_err(|_| RequestError::NotFound)?; .map_err(|_| RequestError::NotFound)?;
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)? let c = get_thread(id)?
.iter() .iter()
.fold(String::from(""), |a, b| format!("{a}\n{b}")); .fold(String::from(""), |a, b| format!("{a}\n{b}"));
let c = content(&id.to_string(), &c); let c = content(&id.to_string(), &c);
Ok(Response::new(Status::Ok, vec![], c.into())) Ok(Response::new(Status::Ok, vec![], c.into()))
} }
} }
} }
}
fn post(request: Request) -> Result<Response, HandlingError> { fn post(request: Request) -> Result<Response, HandlingError> {
let binding = &request.body.ok_or(RequestError::MissingBody)?; // TODO check content-type
let c = String::from_utf8_lossy(&binding);
let c = encode_text(&c).into_owned(); 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", "<br>")
.to_string(),
);
post.img = form.image;
match request.uri.as_str() { match request.uri.as_str() {
// means we wish to create a new thread // means we wish to create a new thread
@ -326,7 +348,7 @@ fn main() {
}; };
pool.execute(move || { 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) { let response = match handle(reader) {
Ok(s) => s, Ok(s) => s,

@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Post { pub struct Post {
pub id: u32, // technically reduntant but whatever pub id: u32, // technically reduntant but whatever
//pub img: Option<Image>, pub img: Option<Vec<u8>>,
pub body: String, pub body: String,
} }
@ -40,7 +40,7 @@ impl Post {
pub fn new(id: u32, body: String) -> Self { pub fn new(id: u32, body: String) -> Self {
Self { Self {
id, id,
//img, img: None,
body, body,
} }
} }
@ -52,7 +52,7 @@ impl fmt::Display for Post {
write!( write!(
f, f,
"<article>\ "<article>\
<img src=\"{}\" alt=\"img\">\ <img src=\"{}/img\" alt=\"img\">\
<div>\ <div>\
<div class=\"meta\">\ <div class=\"meta\">\
<div>\ <div>\

@ -19,8 +19,9 @@
<main> <main>
<div class="wrap slim"> <div class="wrap slim">
<h1>Create new thread</h1> <h1>Create new thread</h1>
<form method="post" enctype="text/plain"> <form method="post" enctype="multipart/form-data" accept-charset="utf-8">
<textarea name="content" rows="10" placeholder="Content" required></textarea> <textarea name="content" rows="10" placeholder="Content" required></textarea>
<input type="file" name="file"/>
<input type="submit" value="Create"> <input type="submit" value="Create">
</form> </form>
</div> </div>

Loading…
Cancel
Save