feat: log IP address for pending comments

This commit is contained in:
Pascal Engélibert 2022-12-04 15:45:52 +01:00
parent 17e61b8a67
commit e710e6678f
Signed by: tuxmain
GPG key ID: 3504BC6D362F7DCA
6 changed files with 71 additions and 49 deletions

View file

@ -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/)

View file

@ -11,7 +11,7 @@ pub type Time = u64;
pub struct Dbs {
pub comment: Tree<CommentId, Comment>,
pub comment_approved: Tree<(TopicHash, Time, CommentId), ()>,
pub comment_pending: Tree<(TopicHash, Time, CommentId), ()>,
pub comment_pending: Tree<(TopicHash, Time, CommentId), Option<IpAddr>>,
/// client_addr -> (last_mutation, mutation_count)
pub client_mutation: Tree<IpAddr, (Time, u32)>,
}

View file

@ -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<CommentId, sled::Error> {
pub fn new_pending_comment(
comment: &Comment,
addr: Option<IpAddr>,
dbs: &Dbs,
) -> Result<CommentId, sled::Error> {
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<CommentId, sl
comment.post_time,
comment_id.clone(),
),
&(),
&addr,
)?;
Ok(comment_id)
}
@ -47,16 +51,16 @@ pub fn remove_comment(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment
Ok(None)
}
pub fn iter_comments_by_topic<'a>(
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<Item = (CommentId, Comment)> + 'a {
) -> impl Iterator<Item = (CommentId, Comment, V)> + '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<Item = (CommentId, Comment)> + '_ {
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<Item = (CommentId, Comment)> + '_ {
) -> impl Iterator<Item = (CommentId, Comment, Option<IpAddr>)> + '_ {
iter_comments_by_topic(topic_hash, &dbs.comment_pending, dbs)
}

View file

@ -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

View file

@ -37,6 +37,7 @@ impl Templates {
#[derive(Clone, Debug, Serialize)]
pub struct CommentWithId {
pub addr: Option<String>,
pub author: String,
pub editable: bool,
pub id: String,

View file

@ -10,6 +10,9 @@
{% for comment in comments_pending %}
<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id | safe }}">
<span class="comment-author">{{ comment.author }}</span>
{% if comment.addr %}
<span class="comment-addr">{{ comment.addr }}</span>
{% endif %}
<span class="comment-date">{{ comment.post_time | date(format="%F %R", locale=time_lang) }}</span>
{% if comment.editable %}
<a href="?edit={{ comment.id | safe }}#edit_comment-form">{{ tr(l=l,k="admin-comment-edit")|safe }}</a>