@ -4,22 +4,24 @@ mod http;
mod post ;
use crate ::errors ::{ HandlingError , InternalError , RequestError } ;
use crate ::html ::{ FAQ , INDEX, STYLE , FAVICON } ;
use crate ::html ::{ FAQ , FAVICON, INDEX, STYLE } ;
use crate ::http ::{ Request , Response , Status } ;
use crate ::post ::{ Post , Thread } ;
use std ::error ::Error ;
use std ::io ::{ Read , Write } ;
use std ::io ;
use std ::io ::{ BufRead , BufReader , Read , Write } ;
use std ::net ::{ SocketAddr , TcpListener , TcpStream } ;
use std ::path ::{ Path , PathBuf } ;
use std ::str ;
use std ::str ::from_utf8 ;
use std ::sync ::{ Arc , Mutex, MutexGuard } ;
use std ::sync ::{ Arc , RwLock } ;
use std ::thread ::JoinHandle ;
use bincode ::{ deserialize , serialize } ;
use byteorder::{ BigEndian , LittleEndian , ReadBytesExt , WriteBytesExt } ;
use errors::DatabaseError ;
use html_escape ::encode_text ;
use http ::Method ;
use lazy_static ::lazy_static ;
use log ::* ;
use simplelog ::* ;
@ -27,6 +29,9 @@ use structopt::StructOpt;
use threadpool ::ThreadPool ;
use urlencoding ::decode ;
// use this instead of hard-coding
type ID_TYPE = u32 ;
// represents command line arguments
#[ derive(StructOpt, Debug) ]
struct Opt {
@ -45,129 +50,217 @@ struct Opt {
lazy_static ! {
// parse command line arguments
static ref OPT : Opt = Opt ::from_args ( ) ;
static ref DB : sled ::Db = sled ::open ( & OPT . database ) . expect ( "failed to open db" ) ;
static ref THREADS : sled ::Tree = DB . open_tree ( b" threads " ) . expect ( "failed to initialize threads" ) ;
static ref POSTS : sled ::Tree = DB . open_tree ( b" posts " ) . expect ( "failed to intialize posts" ) ;
}
#[ derive(Debug) ]
struct State {
db : sled ::Db ,
id_counter : u32 ,
// cache here?
/// increments the id stored inside the db
fn increment ( old : Option < & [ u8 ] > ) -> Option < Vec < u8 > > {
let number = match old {
Some ( s ) = > {
let buf : [ u8 ; 4 ] = s . try_into ( ) . unwrap ( ) ;
u32 ::from_be_bytes ( buf ) + 1
// FIXME
}
None = > 0 ,
} ;
Some ( number . to_be_bytes ( ) . to_vec ( ) )
}
unsafe impl Send for State { }
unsafe impl Sync for State { }
// DATABASE ACCESS FUNCTIONS
/// returns post from id
fn get_post ( id : u32 ) -> Result < Post , DatabaseError > {
POSTS
. get ( id . to_be_bytes ( ) ) ?
. ok_or ( DatabaseError ::NotInDb )
. and_then ( | x | deserialize ::< Post > ( & x ) . map_err ( | _ | DatabaseError ::SerError ) )
}
impl State {
fn new ( db_name : impl AsRef < Path > ) -> Self {
let mut s = Self {
db : sled ::open ( db_name ) . unwrap ( ) ,
id_counter : 0 ,
} ;
s . init_id ( ) ;
s
}
/// returns thread from id
fn get_thread ( id : u32 ) -> Result < Vec < Post > , DatabaseError > {
THREADS
. get ( id . to_be_bytes ( ) ) ?
. ok_or ( DatabaseError ::NotInDb )
. and_then ( | x | deserialize ::< Thread > ( & x ) . map_err ( | _ | DatabaseError ::SerError ) ) ?
. into_iter ( )
. map ( | x | get_post ( x ) )
. collect ( )
// let thread = THREADS.get(id.to_be_bytes()).and_then()
}
fn next_id ( & mut self ) -> String {
// TODO improve this
// means the highest possible id is 999_999_999, then it loops back to zero
// NOTE this could technically be used to limit the storage required,
// might possibly be a good feature
self . id_counter = ( self . id_counter + 1 ) % 1_000_000_000 ;
let width = u32 ::MAX . ilog10 ( ) as usize ; // happens to be 9 in this case
//let mut v = vec![];
//v.write_u32::<BigEndian>(self.id_counter).unwrap()
//self.db.insert(b"id", v);
format! ( "{:0width$}" , self . id_counter )
}
fn init_id ( & mut self ) {
self . id_counter = match self . db . get ( b" id " ) . unwrap ( ) {
Some ( s ) = > ( & ( * s ) ) . read_u32 ::< BigEndian > ( ) . unwrap ( ) ,
None = > 0 ,
}
}
/// generates next id
fn next_id ( ) -> Result < u32 , DatabaseError > {
DB . fetch_and_update ( b" id " , increment )
. map ( | x | match x {
Some ( s ) = > {
let buf : [ u8 ; 4 ] = ( * s ) . try_into ( ) . unwrap ( ) ;
u32 ::from_be_bytes ( buf )
}
None = > 0 u32 ,
} )
. map_err ( | e | DatabaseError ::from ( e ) )
}
/// top level handling of requests
fn handle ( request : & str , state : Arc < Mutex < State > > ) -> Response {
let request = match request . parse ::< Request > ( ) {
Ok ( s ) = > s ,
Err ( e ) = > return Response ::new ( Status ::BadRequest , vec! [ ] , e . to_string ( ) ) ,
} ;
let state = state . lock ( ) . unwrap ( ) ;
/// returns current id
/// not used
fn get_id ( ) -> Result < u32 , DatabaseError > {
let s = DB . get ( b" id " ) ? . unwrap ( ) ;
let buf : [ u8 ; 4 ] = ( * s ) . try_into ( ) . unwrap ( ) ;
Ok ( u32 ::from_be_bytes ( buf ) )
}
match request . method . as_str ( ) {
"GET" = > match get ( & request . uri , state ) {
Ok ( s ) = > s ,
Err ( e ) = > Response ::new ( Status ::NotFound , vec! [ ] , dbg! ( e ) . to_string ( ) ) ,
} ,
"POST" = > match post ( request , state ) {
Ok ( s ) = > s ,
Err ( e ) = > Response ::new ( Status ::Ok , vec! [ ] , "" . to_string ( ) ) ,
} ,
// check admin hash
"DELETE" = > Response ::new ( Status ::Ok , vec! [ ] , "" . to_string ( ) ) ,
_ = > Response ::new ( Status ::Ok , vec! [ ] , RequestError ::BadRequest . to_string ( ) ) ,
/// lists all threads
fn list_threads ( ) -> Result < Vec < Post > , DatabaseError > {
THREADS
. iter ( )
. take_while ( | x | x . is_ok ( ) )
. map ( | x | x . unwrap ( ) . 0 )
. map ( | x | {
POSTS . get ( x ) . map ( | x | match x {
Some ( s ) = > s ,
None = > {
error ! ( "couldn't find thread op among posts" ) ;
panic! ( "unrecoverable error" ) ;
}
} )
} )
. map ( | x | {
x . map_err ( | e | DatabaseError ::from ( e ) )
. and_then ( | i | deserialize ::< Post > ( & i ) . map_err ( | _ | DatabaseError ::SerError ) )
} )
. collect ( )
}
// NOTE worst out of everything, but I guess it's not so bad
fn add_post ( mut post : Post , thread_id : u32 ) -> Result < ( ) , DatabaseError > {
let id = next_id ( ) ? ;
post . id = id ;
let mut thread = deserialize ::< Thread > (
& THREADS
. get ( thread_id . to_be_bytes ( ) ) ?
. ok_or ( DatabaseError ::NotInDb ) ? ,
)
. map_err ( | _ | DatabaseError ::SerError ) ? ;
thread . push ( post . id ) ;
let thread = serialize ( & thread ) . map_err ( | _ | DatabaseError ::SerError ) ? ;
THREADS . insert ( thread_id . to_be_bytes ( ) , thread ) ? ;
POSTS . insert (
id . to_be_bytes ( ) ,
serialize ( & post ) . map_err ( | _ | DatabaseError ::SerError ) ? ,
) ? ;
Ok ( ( ) )
}
fn add_thread ( mut post : Post ) -> Result < ( ) , DatabaseError > {
let id = next_id ( ) ? ;
let thread = Thread ::new ( id ) ;
post . id = id ;
THREADS . insert (
id . to_be_bytes ( ) ,
serialize ( & thread ) . map_err ( | _ | DatabaseError ::SerError ) ? ,
) ? ;
POSTS . insert (
id . to_be_bytes ( ) ,
serialize ( & post ) . map_err ( | _ | DatabaseError ::SerError ) ? ,
) ? ;
Ok ( ( ) )
}
// END DATABASE ACCESS FUNCTIONS
/// top level handling of requests
fn handle ( mut reader : BufReader < & mut TcpStream > ) -> Result < Response , HandlingError > {
let mut request : Request = reader
. by_ref ( )
. lines ( )
. take_while ( | l | l . is_ok ( ) )
. map ( | l | l . unwrap ( ) )
. take_while ( | l | ! l . is_empty ( ) )
. collect ::< Vec < _ > > ( )
. try_into ( ) ? ;
if let Some ( s ) = request
. headers
. iter ( )
. find ( | ( a , b ) | a . to_lowercase ( ) = = "content-length" )
{
let body : Vec < u8 > = reader
. bytes ( )
. take ( s . 1. parse ::< usize > ( ) . unwrap ( ) )
. collect ::< Result < Vec < u8 > , io ::Error > > ( )
. unwrap ( ) ;
// TODO test if it works
// FIXME deunwrap
// TODO handle body too large
request . add_body ( body ) ;
}
dbg! ( & request ) ;
match request . method {
Method ::Get = > get ( & request . uri ) ,
Method ::Post = > post ( request ) ,
// TODO check admin hash
_ = > Ok ( Response ::from ( HandlingError ::from (
RequestError ::BadRequest ,
) ) ) ,
}
}
/// get "/" returns head of every thread
/// or returns posts from thread with id in uri
fn get ( path : & str , state : MutexGuard < State > ) -> Result < Response , HandlingError > {
fn get ( path : & str ) -> Result < Response , HandlingError > {
match path {
// list threads in this case
"/" = > {
let content = content (
"index" ,
& state
. db
. iter ( )
. map ( | x | x . unwrap ( ) ) //FIXME unwraps
. map ( | x | deserialize ::< Thread > ( & x . 1 ) . unwrap ( ) . head ( ) ) // FIXME here too
. fold ( String ::from ( "" ) , | a , b | format! ( "{}{}" , a , b ) ) ,
) ;
Ok ( Response ::new ( Status ::Ok , vec! [ ] , content ) )
// FIXME absolute unwrap hell
let ops = list_threads ( ) ?
. iter ( )
. fold ( String ::from ( "" ) , | a , b | format! ( "{a}\n{b}" ) ) ;
let c = content ( "index" , & ops ) ;
Ok ( Response ::new ( Status ::Ok , vec! [ ] , c ) )
}
// TODO favicon.ico
"/css" = > Ok ( Response ::new ( Status ::Ok , vec! [ ] , String ::from ( STYLE ) ) ) ,
"/faq" = > Ok ( Response ::new ( Status ::Ok , vec! [ ] , String ::from ( FAQ ) ) ) ,
//"/favicon.ico" => Ok(Response::new(Status::Ok, vec![("content-type", "image/x-icon")], FAVICON)),
// TODO favicon
// list specific thread here
// FIXME unwrap hell
s = > {
let content = content (
s ,
& deserialize ::< Thread > (
& state
. db
. get ( & s [ 1 .. ] )
. map_err ( | _ | HandlingError ::ServerError ( InternalError ::DatabaseReadError ) ) ?
. ok_or ( HandlingError ::ClientError ( RequestError ::NotFound ) ) ? ,
)
let id = s
. trim_start_matches ( "/" )
. split ( "/" )
. next ( )
. unwrap ( )
. to_string ( ) ,
) ;
Ok ( Response ::new ( Status ::Ok , vec! [ ] , content ) )
. parse ::< u32 > ( )
. 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 ) )
}
}
}
fn post ( request : Request , mut state : MutexGuard < State > ) -> Result < Response , HandlingError > {
let content = encode_text ( & request . body ) . into_owned ( ) ;
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 ( ) ;
let post = Post ::new ( 0 , c ) ;
match request . uri . as_str ( ) {
// means we wish to create a new thread
"/" = > {
// TODO fix unwrap
let id = state . next_id ( ) ;
let thread = Thread ( vec! [ Post ::new ( & id , content ) ] ) ;
state . db . insert ( & id , serialize ( & thread ) . unwrap ( ) ) . unwrap ( ) ;
info ! ( "Created new thread, id: {}" , id ) ;
add_thread ( post ) ? ;
info ! ( "Created new thread" ) ;
// TODO add redirect
Ok ( Response ::new (
@ -179,41 +272,33 @@ fn post(request: Request, mut state: MutexGuard<State>) -> Result<Response, Hand
// means we wish to post in specific thread
s = > {
// first we increment id
let id = state . next_id ( ) ;
let post = Post ::new ( & id , content ) ;
let mut thread = deserialize ::< Thread > (
& state
. db
. remove ( & s [ 1 .. ] )
. map_err ( | _ | HandlingError ::ServerError ( InternalError ::DatabaseWriteError ) ) ?
. ok_or ( HandlingError ::ClientError ( RequestError ::NotFound ) ) ? ,
)
. unwrap ( ) ;
thread . 0. push ( post ) ;
let id = s
. trim_start_matches ( "/" )
. split ( "/" )
. next ( )
. unwrap ( )
. parse ::< u32 > ( )
. map_err ( | _ | RequestError ::NotFound ) ? ;
add_post ( post , id ) ? ;
state
. db
. insert ( & s [ 1 .. ] , serialize ( & thread ) . unwrap ( ) )
. unwrap ( ) ;
// make this less weird
info ! ( "Added new post to thread {}" , s ) ;
info ! ( "Added new post to thread {id}" ) ;
// TODO add redirect
Ok ( Response ::new ( Status ::SeeOther , vec! [ ( "location" , s ) ] , String ::from ( "" ) ) )
Ok ( Response ::new (
Status ::SeeOther ,
vec! [ ( "location" , s ) ] ,
String ::from ( "" ) ,
) )
}
}
}
fn delete ( path : & str , database : Arc < Mutex < sled ::Db > > ) -> Response {
fn delete ( path : & str ) -> Response {
todo! ( ) ;
}
#[ inline ]
fn content ( name : & str , main : & str ) -> String {
INDEX . replace ( "{name}" , & name ) . replace ( "{}" , & main )
INDEX . replace ( "{name}" , name ) . replace ( "{}" , main )
}
fn main ( ) {
@ -229,10 +314,7 @@ fn main() {
TerminalMode ::Mixed ,
ColorChoice ::Auto ,
)
. unwrap ( ) ;
// open database
let state = Arc ::new ( Mutex ::new ( State ::new ( & OPT . database ) ) ) ;
// TODO setup cache
. expect ( "failed to setup logger" ) ;
// wait for requests
for stream in listener . incoming ( ) {
@ -244,23 +326,16 @@ fn main() {
}
} ;
let state = Arc ::clone ( & state ) ;
pool . execute ( move | | {
let ip = stream . peer_addr ( ) . unwrap ( ) ;
// TODO check if allowed ip before reading request
let mut buffer = [ 0 ; 1 < < 10 ] ; // 2 to the power of 10
// how do I implement images with this?
stream . read ( & mut buffer ) . unwrap ( ) ;
let reader = BufReader ::new ( & mut stream ) ;
// NOTE possibly problematic when we cosider images
let text = str ::from_utf8 ( & buffer ) . unwrap ( ) . trim_matches ( 0 as char ) ;
// ^-- gets rid of null at the end of buffer
let response = match handle ( reader ) {
Ok ( s ) = > s ,
Err ( e ) = > Response ::from ( dbg! ( e ) ) ,
} ;
// handle request
stream
. write ( handle ( text , state ) . to_string ( ) . as_bytes ( ) )
. unwrap ( ) ;
stream . write ( response . to_string ( ) . as_bytes ( ) ) . unwrap ( ) ;
} ) ;
}
}