webcomment/src/server.rs

392 lines
11 KiB
Rust
Raw Normal View History

2022-10-15 13:32:57 +00:00
use crate::{config::*, db::*, helpers, queries::*, templates::*};
2022-10-15 15:32:35 +00:00
use argon2::{Argon2, PasswordHash, PasswordVerifier};
2022-10-15 21:48:06 +00:00
use crossbeam_channel::Sender;
2022-10-21 17:33:20 +00:00
use log::{error, warn};
2022-10-15 13:32:57 +00:00
use tera::Context;
2022-10-29 09:03:37 +00:00
pub async fn run_server(config: &'static Config, dbs: Dbs, templates: &'static Templates) {
2022-10-15 13:32:57 +00:00
tide::log::start();
2022-10-15 21:48:06 +00:00
let (notify_send, notify_recv) = crossbeam_channel::bounded(10);
2022-10-29 09:03:37 +00:00
tokio::spawn(crate::notify::run_notifier(config, notify_recv));
2022-10-15 21:48:06 +00:00
2022-10-15 13:32:57 +00:00
let mut app = tide::new();
app.at(&format!("{}t/:topic", config.root_url)).get({
let dbs = dbs.clone();
move |req: tide::Request<()>| {
2022-10-29 09:03:37 +00:00
serve_comments(req, config, templates, dbs.clone(), Context::new(), 200)
2022-10-15 13:32:57 +00:00
}
});
app.at(&format!("{}t/:topic", config.root_url)).post({
let dbs = dbs.clone();
move |req: tide::Request<()>| {
2022-10-29 09:03:37 +00:00
handle_post_comments(req, config, templates, dbs.clone(), notify_send.clone())
2022-10-15 13:32:57 +00:00
}
});
2022-10-29 09:03:37 +00:00
app.at(&format!("{}admin", config.root_url))
.get(move |req: tide::Request<()>| serve_admin_login(req, config, templates));
2022-10-15 13:32:57 +00:00
app.at(&format!("{}admin", config.root_url)).post({
let dbs = dbs.clone();
2022-10-29 09:03:37 +00:00
move |req: tide::Request<()>| handle_post_admin(req, config, templates, dbs.clone())
2022-10-15 13:32:57 +00:00
});
app.listen(config.listen).await.unwrap();
}
async fn serve_comments<'a>(
req: tide::Request<()>,
2022-10-29 09:03:37 +00:00
config: &Config,
templates: &Templates,
2022-10-15 13:32:57 +00:00
dbs: Dbs,
2022-10-22 13:13:38 +00:00
mut context: Context,
2022-10-22 15:56:24 +00:00
status_code: u16,
2022-10-15 13:32:57 +00:00
) -> tide::Result<tide::Response> {
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| {
2022-10-29 09:03:37 +00:00
check_admin_password_hash(config, &String::from(psw.value()))
2022-10-15 13:32:57 +00:00
});
let topic_hash = TopicHash::from_topic(topic);
context.insert("config", &config);
context.insert("admin", &admin);
if admin {
if let Ok(query) = req.query::<ApproveQuery>() {
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::<RemoveQuery>() {
if let Ok(comment_id) = CommentId::from_base64(&query.remove) {
2022-10-22 15:56:24 +00:00
helpers::remove_comment(comment_id, &dbs)
2022-10-15 13:32:57 +00:00
.map_err(|e| error!("Removing comment: {:?}", e))
.ok();
}
}
2022-10-22 15:56:24 +00:00
if let Ok(query) = req.query::<EditQuery>() {
if let Ok(comment_id) = CommentId::from_base64(&query.edit) {
if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
context.insert("edit_comment", &comment_id.to_base64());
context.insert("edit_comment_author", &comment.author);
context.insert("edit_comment_email", &comment.email);
context.insert("edit_comment_text", &comment.text);
}
}
}
2022-10-15 13:32:57 +00:00
context.insert(
"comments_pending",
&helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs)
.map(|(comment_id, comment)| CommentWithId {
author: comment.author,
2022-10-22 15:56:24 +00:00
editable: admin,
2022-10-15 13:32:57 +00:00
id: comment_id.to_base64(),
needs_approval: true,
post_time: comment.post_time,
text: comment.text,
})
.collect::<Vec<CommentWithId>>(),
);
}
context.insert(
"comments",
&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
.map(|(comment_id, comment)| CommentWithId {
author: comment.author,
2022-10-22 15:56:24 +00:00
editable: admin,
2022-10-15 13:32:57 +00:00
id: comment_id.to_base64(),
needs_approval: false,
post_time: comment.post_time,
text: comment.text,
})
.collect::<Vec<CommentWithId>>(),
);
2022-10-22 15:56:24 +00:00
Ok(tide::Response::builder(status_code)
.content_type(tide::http::mime::HTML)
.body(templates.tera.render("comments.html", &context)?)
.build())
2022-10-15 13:32:57 +00:00
}
async fn serve_admin<'a>(
_req: tide::Request<()>,
2022-10-29 09:03:37 +00:00
config: &Config,
templates: &Templates,
2022-10-15 13:32:57 +00:00
dbs: Dbs,
) -> tide::Result<tide::Response> {
let mut context = Context::new();
context.insert("config", &config);
context.insert("admin", &true);
context.insert(
"comments",
&dbs.comment_pending
.iter()
.filter_map(|entry| {
2022-10-15 15:32:35 +00:00
let ((_topic_hash, _time, comment_id), ()) = entry
2022-10-15 13:32:57 +00:00
.map_err(|e| error!("Reading comment_pending: {:?}", e))
2022-10-15 15:32:35 +00:00
.ok()?;
2022-10-15 13:32:57 +00:00
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,
2022-10-22 15:56:24 +00:00
editable: true,
2022-10-15 13:32:57 +00:00
id: comment_id.to_base64(),
needs_approval: true,
post_time: comment.post_time,
text: comment.text,
})
})
.collect::<Vec<CommentWithId>>(),
);
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<()>,
2022-10-29 09:03:37 +00:00
config: &Config,
templates: &Templates,
2022-10-15 13:32:57 +00:00
) -> tide::Result<tide::Response> {
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<()>,
2022-10-29 09:03:37 +00:00
config: &Config,
templates: &Templates,
2022-10-15 13:32:57 +00:00
dbs: Dbs,
2022-10-15 21:48:06 +00:00
notify_send: Sender<()>,
2022-10-15 13:32:57 +00:00
) -> tide::Result<tide::Response> {
2022-10-22 15:56:24 +00:00
let admin = req.cookie("admin").map_or(false, |psw| {
2022-10-29 09:03:37 +00:00
check_admin_password_hash(config, &String::from(psw.value()))
2022-10-22 15:56:24 +00:00
});
let client_addr = if !admin && config.antispam_enable {
2022-10-29 09:03:37 +00:00
match helpers::get_client_addr(config, &req) {
2022-10-21 17:33:20 +00:00
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
};
2022-10-20 18:17:49 +00:00
let mut errors = Vec::new();
2022-10-22 13:13:38 +00:00
let mut context = Context::new();
2022-10-20 18:17:49 +00:00
2022-10-15 13:32:57 +00:00
match req.body_form::<CommentQuery>().await? {
CommentQuery::NewComment(query) => {
let Ok(topic) = req.param("topic") else {
return Err(tide::Error::from_str(404, "No topic"))
};
2022-10-29 09:03:37 +00:00
helpers::check_comment(config, &query.comment, &mut errors);
2022-10-22 15:56:24 +00:00
2022-10-21 17:33:20 +00:00
if let Some(client_addr) = &client_addr {
if let Some(antispam_timeout) =
2022-10-29 09:03:37 +00:00
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
2022-10-21 17:33:20 +00:00
{
errors.push(format!(
"The edition quota from your IP is reached. You will be unblocked in {}s.",
antispam_timeout
));
}
}
2022-10-15 13:32:57 +00:00
2022-10-20 18:17:49 +00:00
if errors.is_empty() {
2022-10-21 17:33:20 +00:00
if let Some(client_addr) = &client_addr {
helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
}
2022-10-20 18:17:49 +00:00
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,
2022-10-22 15:56:24 +00:00
author: query.comment.author,
email: if query.comment.email.is_empty() {
2022-10-20 18:17:49 +00:00
None
} else {
2022-10-22 15:56:24 +00:00
Some(query.comment.email)
2022-10-20 18:17:49 +00:00
},
last_edit_time: None,
post_time: time,
2022-10-22 15:56:24 +00:00
text: query.comment.text,
2022-10-20 18:17:49 +00:00
};
helpers::new_pending_comment(&comment, &dbs)
.map_err(|e| error!("Adding pending comment: {:?}", e))
.ok();
notify_send.send(()).ok();
2022-10-22 13:13:38 +00:00
} else {
2022-10-22 15:56:24 +00:00
context.insert("new_comment_author", &query.comment.author);
context.insert("new_comment_email", &query.comment.email);
context.insert("new_comment_text", &query.comment.text);
2022-10-20 18:17:49 +00:00
}
2022-10-22 15:56:24 +00:00
context.insert("new_comment_errors", &errors);
}
CommentQuery::EditComment(query) => {
if !admin {
return Err(tide::Error::from_str(403, "Forbidden"));
}
2022-10-29 09:03:37 +00:00
helpers::check_comment(config, &query.comment, &mut errors);
2022-10-22 15:56:24 +00:00
let comment_id = if let Ok(comment_id) = CommentId::from_base64(&query.id) {
comment_id
} else {
return Err(tide::Error::from_str(400, "Invalid comment id"));
};
let mut comment = if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
comment
} else {
return Err(tide::Error::from_str(404, "Not found"));
};
if let Some(client_addr) = &client_addr {
if let Some(antispam_timeout) =
2022-10-29 09:03:37 +00:00
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
2022-10-22 15:56:24 +00:00
{
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 time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
comment.author = query.comment.author;
comment.email = if query.comment.email.is_empty() {
None
} else {
Some(query.comment.email)
};
comment.text = query.comment.text;
comment.last_edit_time = Some(time);
dbs.comment.insert(&comment_id, &comment).unwrap();
} else {
context.insert("edit_comment", &comment_id.to_base64());
context.insert("edit_comment_author", &query.comment.author);
context.insert("edit_comment_email", &query.comment.email);
context.insert("edit_comment_text", &query.comment.text);
}
context.insert("edit_comment_errors", &errors);
2022-10-15 13:32:57 +00:00
}
}
2022-10-22 15:56:24 +00:00
serve_comments(
req,
config,
templates,
dbs,
context,
if errors.is_empty() { 200 } else { 400 },
)
.await
2022-10-15 13:32:57 +00:00
}
async fn handle_post_admin(
mut req: tide::Request<()>,
2022-10-29 09:03:37 +00:00
config: &Config,
templates: &Templates,
2022-10-15 13:32:57 +00:00
dbs: Dbs,
) -> tide::Result<tide::Response> {
if let Some(psw) = req.cookie("admin") {
2022-10-29 09:03:37 +00:00
if check_admin_password(config, &String::from(psw.value())).is_some() {
2022-10-22 15:56:24 +00:00
#[allow(clippy::match_single_binding)]
2022-10-15 13:32:57 +00:00
match req.body_form::<AdminQuery>().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::<AdminQuery>().await? {
2022-10-29 09:03:37 +00:00
if let Some(password_hash) = check_admin_password(config, &query.psw) {
serve_admin(req, config, 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
})
2022-10-15 13:32:57 +00:00
} else {
serve_admin_login(req, config, templates).await
}
} else {
serve_admin_login(req, config, templates).await
}
}
2022-10-15 15:32:35 +00:00
fn check_admin_password(config: &Config, password: &str) -> Option<String> {
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)
}