use crate::{config::*, db::*, helpers, queries::*, templates::*}; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use crossbeam_channel::Sender; use log::{error, warn}; use std::sync::Arc; use tera::Context; pub async fn run_server(config: Arc, dbs: Dbs, templates: Arc) { tide::log::start(); let (notify_send, notify_recv) = crossbeam_channel::bounded(10); tokio::spawn(crate::notify::run_notifier(config.clone(), notify_recv)); let mut app = tide::new(); app.at(&format!("{}t/:topic", config.root_url)).get({ let config = config.clone(); let templates = templates.clone(); let dbs = dbs.clone(); move |req: tide::Request<()>| { serve_comments( req, config.clone(), templates.clone(), dbs.clone(), &[], Context::new(), ) } }); app.at(&format!("{}t/:topic", config.root_url)).post({ let config = config.clone(); let templates = templates.clone(); let dbs = dbs.clone(); move |req: tide::Request<()>| { handle_post_comments( req, config.clone(), templates.clone(), dbs.clone(), notify_send.clone(), ) } }); app.at(&format!("{}admin", config.root_url)).get({ let config = config.clone(); let templates = templates.clone(); move |req: tide::Request<()>| serve_admin_login(req, config.clone(), templates.clone()) }); app.at(&format!("{}admin", config.root_url)).post({ let config = config.clone(); let templates = templates.clone(); let dbs = dbs.clone(); move |req: tide::Request<()>| { handle_post_admin(req, config.clone(), templates.clone(), dbs.clone()) } }); app.listen(config.listen).await.unwrap(); } async fn serve_comments<'a>( req: tide::Request<()>, config: Arc, templates: Arc, dbs: Dbs, errors: &[String], mut context: Context, ) -> tide::Result { let Ok(topic) = req.param("topic") else { return Err(tide::Error::from_str(404, "No topic")) }; let admin = req.cookie("admin").map_or(false, |psw| { check_admin_password_hash(&config, &String::from(psw.value())) }); let topic_hash = TopicHash::from_topic(topic); //let mut context = Context::new(); context.insert("config", &config); context.insert("admin", &admin); context.insert("new_comment_errors", errors); if admin { if let Ok(query) = req.query::() { if let Ok(comment_id) = CommentId::from_base64(&query.approve) { helpers::approve_comment(comment_id, &dbs) .map_err(|e| error!("Approving comment: {:?}", e)) .ok(); } } if let Ok(query) = req.query::() { if let Ok(comment_id) = CommentId::from_base64(&query.remove) { helpers::remove_pending_comment(comment_id, &dbs) .map_err(|e| error!("Removing comment: {:?}", e)) .ok(); } } context.insert( "comments_pending", &helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs) .map(|(comment_id, comment)| CommentWithId { author: comment.author, id: comment_id.to_base64(), needs_approval: true, post_time: comment.post_time, text: comment.text, }) .collect::>(), ); } context.insert( "comments", &helpers::iter_approved_comments_by_topic(topic_hash, &dbs) .map(|(comment_id, comment)| CommentWithId { author: comment.author, id: comment_id.to_base64(), needs_approval: false, post_time: comment.post_time, text: comment.text, }) .collect::>(), ); Ok( tide::Response::builder(if errors.is_empty() { 200 } else { 400 }) .content_type(tide::http::mime::HTML) .body(templates.tera.render("comments.html", &context)?) .build(), ) } async fn serve_admin<'a>( _req: tide::Request<()>, config: Arc, templates: Arc, dbs: Dbs, ) -> tide::Result { let mut context = Context::new(); context.insert("config", &config); context.insert("admin", &true); context.insert( "comments", &dbs.comment_pending .iter() .filter_map(|entry| { let ((_topic_hash, _time, comment_id), ()) = entry .map_err(|e| error!("Reading comment_pending: {:?}", e)) .ok()?; let comment = dbs .comment .get(&comment_id) .map_err(|e| error!("Reading comment: {:?}", e)) .ok()? .or_else(|| { error!("Comment not found"); None })?; Some(CommentWithId { author: comment.author, id: comment_id.to_base64(), needs_approval: true, post_time: comment.post_time, text: comment.text, }) }) .collect::>(), ); Ok(tide::Response::builder(200) .content_type(tide::http::mime::HTML) .body(templates.tera.render("comments.html", &context)?) .build()) } async fn serve_admin_login( _req: tide::Request<()>, config: Arc, templates: Arc, ) -> tide::Result { let mut context = Context::new(); context.insert("config", &config); Ok(tide::Response::builder(200) .content_type(tide::http::mime::HTML) .body(templates.tera.render("admin_login.html", &context)?) .build()) } async fn handle_post_comments( mut req: tide::Request<()>, config: Arc, templates: Arc, dbs: Dbs, notify_send: Sender<()>, ) -> tide::Result { let client_addr = if config.antispam_enable { match helpers::get_client_addr(&config, &req) { Some(Ok(addr)) => { if config.antispam_whitelist.contains(&addr) { None } else { Some(addr) } } Some(Err(e)) => { warn!("Unable to parse client addr: {}", e); None } None => { warn!("No client addr"); None } } } else { None }; let mut errors = Vec::new(); let mut context = Context::new(); match req.body_form::().await? { CommentQuery::NewComment(query) => { let Ok(topic) = req.param("topic") else { return Err(tide::Error::from_str(404, "No topic")) }; if query.author.len() > config.comment_author_max_len { errors.push(format!( "Author name length is {} but maximum is {}.", query.author.len(), config.comment_author_max_len )); } if query.email.len() > config.comment_email_max_len { errors.push(format!( "E-mail length is {} but maximum is {}.", query.email.len(), config.comment_email_max_len )); } if query.text.len() > config.comment_text_max_len { errors.push(format!( "Comment length is {} but maximum is {}.", query.text.len(), config.comment_text_max_len )); } if let Some(client_addr) = &client_addr { if let Some(antispam_timeout) = helpers::antispam_check_client_mutation(client_addr, &dbs, &config).unwrap() { errors.push(format!( "The edition quota from your IP is reached. You will be unblocked in {}s.", antispam_timeout )); } } if errors.is_empty() { if let Some(client_addr) = &client_addr { helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap(); } let topic_hash = TopicHash::from_topic(topic); let time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); let comment = Comment { topic_hash, author: query.author, email: if query.email.is_empty() { None } else { Some(query.email) }, last_edit_time: None, post_time: time, text: query.text, }; helpers::new_pending_comment(&comment, &dbs) .map_err(|e| error!("Adding pending comment: {:?}", e)) .ok(); notify_send.send(()).ok(); } else { context.insert("new_comment_author", &query.author); context.insert("new_comment_email", &query.email); context.insert("new_comment_text", &query.text); } } _ => {} } serve_comments(req, config, templates, dbs, &errors, context).await } async fn handle_post_admin( mut req: tide::Request<()>, config: Arc, templates: Arc, dbs: Dbs, ) -> tide::Result { if let Some(psw) = req.cookie("admin") { if check_admin_password(&config, &String::from(psw.value())).is_some() { match req.body_form::().await? { _ => serve_admin(req, config, templates, dbs).await, } } else { serve_admin_login(req, config, templates).await } } else if let AdminQuery::Login(query) = req.body_form::().await? { if let Some(password_hash) = check_admin_password(&config, &query.psw) { serve_admin(req, config.clone(), templates, dbs) .await .map(|mut r| { let mut cookie = tide::http::Cookie::new("admin", password_hash); cookie.set_http_only(Some(true)); cookie.set_path(config.root_url.clone()); if let Some(domain) = &config.cookies_domain { cookie.set_domain(domain.clone()); } if config.cookies_https_only { cookie.set_secure(Some(true)); } r.insert_cookie(cookie); r }) } else { serve_admin_login(req, config, templates).await } } else { serve_admin_login(req, config, templates).await } } fn check_admin_password(config: &Config, password: &str) -> Option { let argon2 = Argon2::default(); config .admin_passwords .iter() .filter_map(|admin_password| PasswordHash::new(admin_password).ok()) .find(|admin_password| { argon2 .verify_password(password.as_bytes(), admin_password) .is_ok() }) .map(|password_hash| password_hash.to_string()) } fn check_admin_password_hash(config: &Config, password_hash: &str) -> bool { config.admin_passwords.iter().any(|h| h == password_hash) }