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 = [
"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]]

@ -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 `<id>/img`
### POST usage

@ -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<DatabaseError> 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<RequestError> 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,

@ -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::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,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> {
// let mut hashmap = HashMap::new();
// dbg!(&self.body);
@ -75,12 +165,6 @@ impl Status {
// }
// Ok(hashmap)
// }
//}
impl Request {
pub fn add_body(&mut self, body: Vec<u8>) {
self.body = Some(body);
}
}
impl TryFrom<Vec<String>> for Request {
@ -128,14 +212,14 @@ impl Response {
pub fn respond(&self) -> Vec<u8> {
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
}
}

@ -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<Vec<Post>, DatabaseError> {
/// generates next id
fn next_id() -> Result<u32, DatabaseError> {
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<Response, HandlingErr
if let Some(s) = request
.headers
.iter()
.find(|(a, b)| a.to_lowercase() == "content-length")
.find(|(a, _)| a.to_lowercase() == "content-length")
{
if request.method == Method::Post {
let body: Vec<u8> = reader
@ -197,7 +199,7 @@ fn handle(mut reader: BufReader<&mut TcpStream>) -> Result<Response, HandlingErr
request.add_body(body);
}
}
dbg!(&request);
//dbg!(&request);
match request.method {
Method::Get => get(&request.uri),
@ -224,12 +226,19 @@ fn get(path: &str) -> Result<Response, HandlingError> {
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<Response, HandlingError> {
.unwrap()
.parse::<u32>()
.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)?
.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<Response, HandlingError> {
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", "<br>")
.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,

@ -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<Image>,
pub img: Option<Vec<u8>>,
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,
"<article>\
<img src=\"{}\" alt=\"img\">\
<img src=\"{}/img\" alt=\"img\">\
<div>\
<div class=\"meta\">\
<div>\

@ -19,8 +19,9 @@
<main>
<div class="wrap slim">
<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>
<input type="file" name="file"/>
<input type="submit" value="Create">
</form>
</div>

Loading…
Cancel
Save