use super::{cli, config, db, static_files, templates, utils::*}; use handlebars::to_json; use serde::{Deserialize, Serialize}; use serde_json::value::Value as Json; use sha2::{Digest, Sha512Trunc256}; use std::{ convert::{TryFrom, TryInto}, sync::{Arc, RwLock}, }; use warp::Filter; use warp::Reply; type PasswordHash = [u8; 32]; #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize)] struct AdId([u8; 16]); impl AdId { fn random() -> Self { Self(rand::random()) } } impl AsRef<[u8]> for AdId { fn as_ref(&self) -> &[u8] { &self.0 } } impl TryFrom<&[u8]> for AdId { type Error = std::array::TryFromSliceError; fn try_from(v: &[u8]) -> Result { Ok(Self(v.try_into()?)) } } pub async fn start_server( config: config::Config, dbs: db::Dbs, templates: templates::Templates<'static>, opt: cli::MainOpt, ) { let templates = Arc::new(templates); let cache_ads = Arc::new(RwLock::new(to_json( dbs.ads .iter() .filter_map(|x| { let (ad_id, ad) = x.ok()?; Some(AdWithId { id: hex::encode(ad_id.as_ref()), ad: bincode::deserialize::(&ad).ok()?, }) }) .collect::>(), ))); let config = Arc::new(config); let dbs = Arc::new(dbs); let handle_index = { let cache_ads = cache_ads.clone(); let templates = templates.clone(); move |errors: &[ErrorTemplate]| { warp::reply::html( templates .hb .render( "index.html", &IndexTemplate { lang: "fr", errors, ads: &*cache_ads.read().unwrap(), }, ) .unwrap_or_else(|e| e.to_string()), ) .into_response() } }; let handle_admin_login = { let templates = templates.clone(); move |errors: &[ErrorTemplate]| { warp::reply::html( templates .hb .render( "admin_login.html", &AdminLoginTemplate { lang: "fr", errors }, ) .unwrap_or_else(|e| e.to_string()), ) .into_response() } }; let handle_admin = { let cache_ads = cache_ads.clone(); move |errors: &[ErrorTemplate]| { warp::reply::html( templates .hb .render( "admin.html", &AdminTemplate { lang: "fr", errors, ads: &*cache_ads.read().unwrap(), }, ) .unwrap_or_else(|e| e.to_string()), ) .into_response() } }; let route_static = warp::path("static") .and(warp::get()) .and(warp::fs::dir(opt.dir.0.join(static_files::STATIC_DIR))); let handle_new_ad = { let handle_index = handle_index.clone(); let dbs = dbs.clone(); let cache_ads = cache_ads.clone(); move |query: NewAdQuery| { let mut hasher = Sha512Trunc256::new(); hasher.update(query.psw); dbs.ads .insert( AdId::random(), bincode::serialize(&Ad { author: query.author, password: hasher.finalize()[..].try_into().unwrap(), price: query.price, pubkey: if query.pubkey.is_empty() { None } else { Some(match format_pubkey(&query.pubkey) { Ok(pubkey) => pubkey, Err(e) => return handle_index(&[ErrorTemplate{text: match e { PubkeyDecodeError::BadChecksum => "La somme de contrôle de la clé publique est incorrecte.", PubkeyDecodeError::BadFormat => "Le format de la clé publique est incorrect.", }}]) }) }, quantity: query.quantity, time: 0, title: query.title, }) .unwrap(), ) .unwrap(); dbs.ads.flush().unwrap(); let mut cache_ads = cache_ads.write().unwrap(); *cache_ads = to_json( dbs.ads .iter() .filter_map(|x| { let (ad_id, ad) = x.ok()?; Some(AdWithId { id: hex::encode(ad_id.as_ref()), ad: bincode::deserialize::(&ad).ok()?, }) }) .collect::>(), ); drop(cache_ads); redirect_302("/").into_response() } }; let handle_rm_ad = { let handle_index = handle_index.clone(); let dbs = dbs.clone(); let cache_ads = cache_ads.clone(); move |query: RmAdQuery| { let mut hasher = Sha512Trunc256::new(); hasher.update(query.psw); let password: PasswordHash = hasher.finalize()[..].try_into().unwrap(); /*query .ads .into_iter() .filter_map(|x| Some(AdId::try_from(hex::decode(x).ok()?.as_ref()).ok()?)) .for_each(|ad_id| { if let Some(raw) = dbs.ads.get(&ad_id).unwrap() { if let Ok(ad) = bincode::deserialize::(&raw) { if ad.password == password { dbs.ads.remove(&ad_id).unwrap(); } } } });*/ if let Ok(ad_id) = hex::decode(query.ad) { if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) { if let Some(raw) = dbs.ads.get(&ad_id).unwrap() { if let Ok(ad) = bincode::deserialize::(&raw) { if ad.password == password { dbs.ads.remove(&ad_id).unwrap(); dbs.ads.flush().unwrap(); let mut cache_ads = cache_ads.write().unwrap(); *cache_ads = to_json( dbs.ads .iter() .filter_map(|x| { let (ad_id, ad) = x.ok()?; Some(AdWithId { id: hex::encode(ad_id.as_ref()), ad: bincode::deserialize::(&ad).ok()?, }) }) .collect::>(), ); } else { return handle_index(&[ErrorTemplate { text: "Le mot de passe de l'annonce est incorrect.", }]); } } } } } redirect_302("/").into_response() } }; let handle_admin_rm_ad = { move |query: AdminRmAdQuery| { if let Ok(ad_id) = hex::decode(query.ad) { if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) { if dbs.ads.remove(&ad_id).unwrap().is_some() { dbs.ads.flush().unwrap(); let mut cache_ads = cache_ads.write().unwrap(); *cache_ads = to_json( dbs.ads .iter() .filter_map(|x| { let (ad_id, ad) = x.ok()?; Some(AdWithId { id: hex::encode(ad_id.as_ref()), ad: bincode::deserialize::(&ad).ok()?, }) }) .collect::>(), ); } } } redirect_302("/admin").into_response() } }; let route_index = warp::path::end().and(warp::get().map(move || handle_index(&[])).or( warp::post().and( warp::body::form::().map(move |query: Query| match query { Query::NewAdQuery(query) => handle_new_ad(query), Query::RmAdQuery(query) => handle_rm_ad(query), }), ), )); let route_admin = warp::path("admin").and( warp::path::end() .and( warp::cookie("admin") .and(warp::body::form::()) .map({ let handle_admin_login = handle_admin_login.clone(); let config = config.clone(); move |psw, query| { if config.admin_passwords.contains(&psw) { let AdminQuery::RmAdQuery(query) = query; handle_admin_rm_ad(query) } else { handle_admin_login(&[ErrorTemplate { text: "Mot de passe administrateur invalide", }]) } } }) .or(warp::cookie("admin").map({ let handle_admin = handle_admin.clone(); let handle_admin_login = handle_admin_login.clone(); let config = config.clone(); move |psw| { if config.admin_passwords.contains(&psw) { handle_admin(&[]) } else { handle_admin_login(&[ErrorTemplate { text: "Mot de passe administrateur invalide", }]) } } })) .or(warp::path::end().map({ let handle_admin_login = handle_admin_login.clone(); move || handle_admin_login(&[]) })), ) .or(warp::path("login").and(warp::path::end()).and( warp::post() .and(warp::get().map(move || handle_admin(&[]))) .or(warp::body::form::().map({ let config = config.clone(); move |query: AdminLoginQuery| { if config.admin_passwords.contains(&query.psw) { warp::reply::with_header( redirect_302("/admin").into_response(), "Set-Cookie", format!( "admin={}; path=/; HttpOnly", urlencoding::encode(&query.psw) ), ) .into_response() } else { handle_admin_login(&[ErrorTemplate { text: "Mot de passe administrateur invalide", }]) .into_response() } } })), )) .or(warp::path("logout").and(warp::path::end()).map(|| { warp::reply::with_header( redirect_302("/").into_response(), "Set-Cookie", "admin=; HttpOnly; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT", ) .into_response() })), ); warp::serve(route_static.or(route_index).or(route_admin)) .run(config.listen) .await; } #[derive(Serialize)] struct IndexTemplate<'a> { lang: &'a str, errors: &'a [ErrorTemplate<'a>], ads: &'a Json, } #[derive(Serialize)] struct AdminLoginTemplate<'a> { lang: &'a str, errors: &'a [ErrorTemplate<'a>], } #[derive(Serialize)] struct AdminTemplate<'a> { lang: &'a str, errors: &'a [ErrorTemplate<'a>], ads: &'a Json, } #[derive(Serialize)] struct ErrorTemplate<'a> { text: &'a str, } #[derive(Clone, Debug, Deserialize)] struct NewAdQuery { author: String, price: String, psw: String, pubkey: String, quantity: String, title: String, } #[derive(Clone, Debug, Deserialize)] struct RmAdQuery { ad: String, psw: String, } #[derive(Clone, Debug, Deserialize)] struct AdminRmAdQuery { ad: String, } #[derive(Clone, Debug, Deserialize)] struct AdminLoginQuery { psw: String, } #[derive(Clone, Debug, Deserialize)] #[serde(tag = "a")] enum Query { #[serde(rename = "new_ad")] NewAdQuery(NewAdQuery), #[serde(rename = "rm_ad")] RmAdQuery(RmAdQuery), } #[derive(Clone, Debug, Deserialize)] #[serde(tag = "a")] enum AdminQuery { #[serde(rename = "rm_ad")] RmAdQuery(AdminRmAdQuery), } #[derive(Clone, Debug, Deserialize, Serialize)] struct Ad { author: String, password: PasswordHash, price: String, pubkey: Option, quantity: String, time: u64, title: String, } #[derive(Clone, Debug, Serialize)] struct AdWithId { id: String, ad: Ad, }