From e710e6678f254071fa5483d6071ed7528bfd75da Mon Sep 17 00:00:00 2001 From: tuxmain Date: Sun, 4 Dec 2022 15:45:52 +0100 Subject: [PATCH] feat: log IP address for pending comments --- README.md | 13 ++++++- src/db.rs | 2 +- src/helpers.rs | 21 ++++++----- src/server.rs | 80 +++++++++++++++++++++-------------------- src/templates.rs | 1 + templates/comments.html | 3 ++ 6 files changed, 71 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 9c6ad6a..3e5092b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Rust webserver for comments, that you can easily embed in a website. -**Early development, not safe for production yet** +**Early development, not safe for production yet** ([see milestones](https://git.txmn.tk/tuxmain/webcomment/milestone/1)) ## Features @@ -14,6 +14,7 @@ Rust webserver for comments, that you can easily embed in a website. * Comment frequency limit per IP * i18n * Petnames! (anonymous comment authors get a funny random name) +* Designed for privacy and moderation ## Use @@ -36,6 +37,16 @@ If enabled, a message can be sent to a Matrix room (private or public) on every The account must have joined the room for Webcomment to be able to send messages to it. +## Moderation + +New comments are not public before being approved by the administrator (by default). + +## Privacy + +Uses no cookie, no unique user identifier. At each mutation (i.e. new comment or edition), the client IP address is stored for a limited duration (configurable) only for antispam to work. (antispam can be disabled) The client IP address is also stored for each pending comment, but it is removed as soon as the comment gets approved. + +However, keep in mind that if a reverse proxy (or any other intermediate tool) is used, IP addresses and other metadata may be logged somewhere. + ## License CopyLeft 2022 Pascal Engélibert [(why copyleft?)](https://txmn.tk/blog/why-copyleft/) diff --git a/src/db.rs b/src/db.rs index e68f2d3..b76a626 100644 --- a/src/db.rs +++ b/src/db.rs @@ -11,7 +11,7 @@ pub type Time = u64; pub struct Dbs { pub comment: Tree, pub comment_approved: Tree<(TopicHash, Time, CommentId), ()>, - pub comment_pending: Tree<(TopicHash, Time, CommentId), ()>, + pub comment_pending: Tree<(TopicHash, Time, CommentId), Option>, /// client_addr -> (last_mutation, mutation_count) pub client_mutation: Tree, } diff --git a/src/helpers.rs b/src/helpers.rs index 81e9c8f..099fff9 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -3,7 +3,11 @@ use crate::{config::Config, db::*, queries::*}; use log::error; use std::{net::IpAddr, str::FromStr}; -pub fn new_pending_comment(comment: &Comment, dbs: &Dbs) -> Result { +pub fn new_pending_comment( + comment: &Comment, + addr: Option, + dbs: &Dbs, +) -> Result { let comment_id = CommentId::new(); dbs.comment.insert(&comment_id, comment)?; dbs.comment_pending.insert( @@ -12,7 +16,7 @@ pub fn new_pending_comment(comment: &Comment, dbs: &Dbs) -> Result Result( +pub fn iter_comments_by_topic<'a, V: typed_sled::KV>( topic_hash: TopicHash, - tree: &'a Tree<(TopicHash, Time, CommentId), ()>, + tree: &'a Tree<(TopicHash, Time, CommentId), V>, dbs: &'a Dbs, -) -> impl Iterator + 'a { +) -> impl Iterator + 'a { tree.range( (topic_hash.clone(), 0, CommentId::zero())..=(topic_hash, Time::MAX, CommentId::max()), ) .filter_map(|entry| { - let ((_topic_hash, _time, comment_id), ()) = entry + let ((_topic_hash, _time, comment_id), val) = entry .map_err(|e| error!("Reading comment_by_topic_and_time: {:?}", e)) .ok()?; let comment = dbs @@ -68,7 +72,7 @@ pub fn iter_comments_by_topic<'a>( error!("Comment not found"); None })?; - Some((comment_id, comment)) + Some((comment_id, comment, val)) }) } @@ -77,12 +81,13 @@ pub fn iter_approved_comments_by_topic( dbs: &Dbs, ) -> impl Iterator + '_ { iter_comments_by_topic(topic_hash, &dbs.comment_approved, dbs) + .map(|(comment_id, comment, ())| (comment_id, comment)) } pub fn iter_pending_comments_by_topic( topic_hash: TopicHash, dbs: &Dbs, -) -> impl Iterator + '_ { +) -> impl Iterator)> + '_ { iter_comments_by_topic(topic_hash, &dbs.comment_pending, dbs) } diff --git a/src/server.rs b/src/server.rs index f887394..6148e21 100644 --- a/src/server.rs +++ b/src/server.rs @@ -126,7 +126,8 @@ async fn serve_comments<'a>( context.insert( "comments_pending", &helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs) - .map(|(comment_id, comment)| CommentWithId { + .map(|(comment_id, comment, addr)| CommentWithId { + addr: addr.map(|addr| addr.to_string()), author: comment.author, editable: admin, id: comment_id.to_base64(), @@ -142,6 +143,7 @@ async fn serve_comments<'a>( "comments", &helpers::iter_approved_comments_by_topic(topic_hash, &dbs) .map(|(comment_id, comment)| CommentWithId { + addr: None, author: comment.author, editable: admin, id: comment_id.to_base64(), @@ -186,7 +188,7 @@ async fn serve_admin<'a>( &dbs.comment_pending .iter() .filter_map(|entry| { - let ((_topic_hash, _time, comment_id), ()) = entry + let ((_topic_hash, _time, comment_id), addr) = entry .map_err(|e| error!("Reading comment_pending: {:?}", e)) .ok()?; let comment = dbs @@ -199,6 +201,7 @@ async fn serve_admin<'a>( None })?; Some(CommentWithId { + addr: addr.map(|addr| addr.to_string()), author: comment.author, editable: true, id: comment_id.to_base64(), @@ -257,27 +260,22 @@ async fn handle_post_comments( let client_langs = get_client_langs(&req); - let client_addr = if !admin && 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 - } + let client_addr = match helpers::get_client_addr(config, &req) { + Some(Ok(addr)) => Some(addr), + Some(Err(e)) => { + warn!("Unable to parse client addr: {}", e); + None + } + None => { + warn!("No client addr"); + None } - } else { - None }; + let antispam_enabled = !admin + && config.antispam_enable + && client_addr + .as_ref() + .map_or(false, |addr| !config.antispam_whitelist.contains(addr)); let mut errors = Vec::new(); let mut context = Context::new(); @@ -291,28 +289,32 @@ async fn handle_post_comments( helpers::check_comment(config, &query.comment, &mut errors); if let Some(client_addr) = &client_addr { - if let Some(antispam_timeout) = - helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap() - { - errors.push( - locales - .tr( - &client_langs, - "error-antispam", - Some(&FluentArgs::from_iter([( - "antispam_timeout", - antispam_timeout, - )])), - ) - .unwrap() - .into_owned(), - ); + if antispam_enabled { + if let Some(antispam_timeout) = + helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap() + { + errors.push( + locales + .tr( + &client_langs, + "error-antispam", + Some(&FluentArgs::from_iter([( + "antispam_timeout", + antispam_timeout, + )])), + ) + .unwrap() + .into_owned(), + ); + } } } if errors.is_empty() { if let Some(client_addr) = &client_addr { - helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap(); + if antispam_enabled { + helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap(); + } } let topic_hash = TopicHash::from_topic(topic); @@ -338,7 +340,7 @@ async fn handle_post_comments( post_time: time, text: query.comment.text, }; - helpers::new_pending_comment(&comment, &dbs) + helpers::new_pending_comment(&comment, client_addr, &dbs) .map_err(|e| error!("Adding pending comment: {:?}", e)) .ok(); notify_send diff --git a/src/templates.rs b/src/templates.rs index dc9fb5b..987fe7d 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -37,6 +37,7 @@ impl Templates { #[derive(Clone, Debug, Serialize)] pub struct CommentWithId { + pub addr: Option, pub author: String, pub editable: bool, pub id: String, diff --git a/templates/comments.html b/templates/comments.html index a1363af..7cf683f 100644 --- a/templates/comments.html +++ b/templates/comments.html @@ -10,6 +10,9 @@ {% for comment in comments_pending %}
{{ comment.author }} + {% if comment.addr %} + {{ comment.addr }} + {% endif %} {{ comment.post_time | date(format="%F %R", locale=time_lang) }} {% if comment.editable %} {{ tr(l=l,k="admin-comment-edit")|safe }}