feat: log IP address for pending comments
This commit is contained in:
parent
17e61b8a67
commit
e710e6678f
6 changed files with 71 additions and 49 deletions
13
README.md
13
README.md
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Rust webserver for comments, that you can easily embed in a website.
|
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
|
## Features
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ Rust webserver for comments, that you can easily embed in a website.
|
||||||
* Comment frequency limit per IP
|
* Comment frequency limit per IP
|
||||||
* i18n
|
* i18n
|
||||||
* Petnames! (anonymous comment authors get a funny random name)
|
* Petnames! (anonymous comment authors get a funny random name)
|
||||||
|
* Designed for privacy and moderation
|
||||||
|
|
||||||
## Use
|
## 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.
|
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
|
## License
|
||||||
|
|
||||||
CopyLeft 2022 Pascal Engélibert [(why copyleft?)](https://txmn.tk/blog/why-copyleft/)
|
CopyLeft 2022 Pascal Engélibert [(why copyleft?)](https://txmn.tk/blog/why-copyleft/)
|
||||||
|
|
|
@ -11,7 +11,7 @@ pub type Time = u64;
|
||||||
pub struct Dbs {
|
pub struct Dbs {
|
||||||
pub comment: Tree<CommentId, Comment>,
|
pub comment: Tree<CommentId, Comment>,
|
||||||
pub comment_approved: Tree<(TopicHash, Time, CommentId), ()>,
|
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)
|
/// client_addr -> (last_mutation, mutation_count)
|
||||||
pub client_mutation: Tree<IpAddr, (Time, u32)>,
|
pub client_mutation: Tree<IpAddr, (Time, u32)>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,11 @@ use crate::{config::Config, db::*, queries::*};
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::{net::IpAddr, str::FromStr};
|
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();
|
let comment_id = CommentId::new();
|
||||||
dbs.comment.insert(&comment_id, comment)?;
|
dbs.comment.insert(&comment_id, comment)?;
|
||||||
dbs.comment_pending.insert(
|
dbs.comment_pending.insert(
|
||||||
|
@ -12,7 +16,7 @@ pub fn new_pending_comment(comment: &Comment, dbs: &Dbs) -> Result<CommentId, sl
|
||||||
comment.post_time,
|
comment.post_time,
|
||||||
comment_id.clone(),
|
comment_id.clone(),
|
||||||
),
|
),
|
||||||
&(),
|
&addr,
|
||||||
)?;
|
)?;
|
||||||
Ok(comment_id)
|
Ok(comment_id)
|
||||||
}
|
}
|
||||||
|
@ -47,16 +51,16 @@ pub fn remove_comment(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_comments_by_topic<'a>(
|
pub fn iter_comments_by_topic<'a, V: typed_sled::KV>(
|
||||||
topic_hash: TopicHash,
|
topic_hash: TopicHash,
|
||||||
tree: &'a Tree<(TopicHash, Time, CommentId), ()>,
|
tree: &'a Tree<(TopicHash, Time, CommentId), V>,
|
||||||
dbs: &'a Dbs,
|
dbs: &'a Dbs,
|
||||||
) -> impl Iterator<Item = (CommentId, Comment)> + 'a {
|
) -> impl Iterator<Item = (CommentId, Comment, V)> + 'a {
|
||||||
tree.range(
|
tree.range(
|
||||||
(topic_hash.clone(), 0, CommentId::zero())..=(topic_hash, Time::MAX, CommentId::max()),
|
(topic_hash.clone(), 0, CommentId::zero())..=(topic_hash, Time::MAX, CommentId::max()),
|
||||||
)
|
)
|
||||||
.filter_map(|entry| {
|
.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))
|
.map_err(|e| error!("Reading comment_by_topic_and_time: {:?}", e))
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let comment = dbs
|
let comment = dbs
|
||||||
|
@ -68,7 +72,7 @@ pub fn iter_comments_by_topic<'a>(
|
||||||
error!("Comment not found");
|
error!("Comment not found");
|
||||||
None
|
None
|
||||||
})?;
|
})?;
|
||||||
Some((comment_id, comment))
|
Some((comment_id, comment, val))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,12 +81,13 @@ pub fn iter_approved_comments_by_topic(
|
||||||
dbs: &Dbs,
|
dbs: &Dbs,
|
||||||
) -> impl Iterator<Item = (CommentId, Comment)> + '_ {
|
) -> impl Iterator<Item = (CommentId, Comment)> + '_ {
|
||||||
iter_comments_by_topic(topic_hash, &dbs.comment_approved, dbs)
|
iter_comments_by_topic(topic_hash, &dbs.comment_approved, dbs)
|
||||||
|
.map(|(comment_id, comment, ())| (comment_id, comment))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_pending_comments_by_topic(
|
pub fn iter_pending_comments_by_topic(
|
||||||
topic_hash: TopicHash,
|
topic_hash: TopicHash,
|
||||||
dbs: &Dbs,
|
dbs: &Dbs,
|
||||||
) -> impl Iterator<Item = (CommentId, Comment)> + '_ {
|
) -> impl Iterator<Item = (CommentId, Comment, Option<IpAddr>)> + '_ {
|
||||||
iter_comments_by_topic(topic_hash, &dbs.comment_pending, dbs)
|
iter_comments_by_topic(topic_hash, &dbs.comment_pending, dbs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,8 @@ async fn serve_comments<'a>(
|
||||||
context.insert(
|
context.insert(
|
||||||
"comments_pending",
|
"comments_pending",
|
||||||
&helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs)
|
&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,
|
author: comment.author,
|
||||||
editable: admin,
|
editable: admin,
|
||||||
id: comment_id.to_base64(),
|
id: comment_id.to_base64(),
|
||||||
|
@ -142,6 +143,7 @@ async fn serve_comments<'a>(
|
||||||
"comments",
|
"comments",
|
||||||
&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
|
&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
|
||||||
.map(|(comment_id, comment)| CommentWithId {
|
.map(|(comment_id, comment)| CommentWithId {
|
||||||
|
addr: None,
|
||||||
author: comment.author,
|
author: comment.author,
|
||||||
editable: admin,
|
editable: admin,
|
||||||
id: comment_id.to_base64(),
|
id: comment_id.to_base64(),
|
||||||
|
@ -186,7 +188,7 @@ async fn serve_admin<'a>(
|
||||||
&dbs.comment_pending
|
&dbs.comment_pending
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| {
|
.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))
|
.map_err(|e| error!("Reading comment_pending: {:?}", e))
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let comment = dbs
|
let comment = dbs
|
||||||
|
@ -199,6 +201,7 @@ async fn serve_admin<'a>(
|
||||||
None
|
None
|
||||||
})?;
|
})?;
|
||||||
Some(CommentWithId {
|
Some(CommentWithId {
|
||||||
|
addr: addr.map(|addr| addr.to_string()),
|
||||||
author: comment.author,
|
author: comment.author,
|
||||||
editable: true,
|
editable: true,
|
||||||
id: comment_id.to_base64(),
|
id: comment_id.to_base64(),
|
||||||
|
@ -257,27 +260,22 @@ async fn handle_post_comments(
|
||||||
|
|
||||||
let client_langs = get_client_langs(&req);
|
let client_langs = get_client_langs(&req);
|
||||||
|
|
||||||
let client_addr = if !admin && config.antispam_enable {
|
let client_addr = match helpers::get_client_addr(config, &req) {
|
||||||
match helpers::get_client_addr(config, &req) {
|
Some(Ok(addr)) => Some(addr),
|
||||||
Some(Ok(addr)) => {
|
Some(Err(e)) => {
|
||||||
if config.antispam_whitelist.contains(&addr) {
|
warn!("Unable to parse client addr: {}", e);
|
||||||
None
|
None
|
||||||
} else {
|
}
|
||||||
Some(addr)
|
None => {
|
||||||
}
|
warn!("No client addr");
|
||||||
}
|
None
|
||||||
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 errors = Vec::new();
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
|
@ -291,28 +289,32 @@ async fn handle_post_comments(
|
||||||
helpers::check_comment(config, &query.comment, &mut errors);
|
helpers::check_comment(config, &query.comment, &mut errors);
|
||||||
|
|
||||||
if let Some(client_addr) = &client_addr {
|
if let Some(client_addr) = &client_addr {
|
||||||
if let Some(antispam_timeout) =
|
if antispam_enabled {
|
||||||
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
|
if let Some(antispam_timeout) =
|
||||||
{
|
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
|
||||||
errors.push(
|
{
|
||||||
locales
|
errors.push(
|
||||||
.tr(
|
locales
|
||||||
&client_langs,
|
.tr(
|
||||||
"error-antispam",
|
&client_langs,
|
||||||
Some(&FluentArgs::from_iter([(
|
"error-antispam",
|
||||||
"antispam_timeout",
|
Some(&FluentArgs::from_iter([(
|
||||||
antispam_timeout,
|
"antispam_timeout",
|
||||||
)])),
|
antispam_timeout,
|
||||||
)
|
)])),
|
||||||
.unwrap()
|
)
|
||||||
.into_owned(),
|
.unwrap()
|
||||||
);
|
.into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.is_empty() {
|
if errors.is_empty() {
|
||||||
if let Some(client_addr) = &client_addr {
|
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);
|
let topic_hash = TopicHash::from_topic(topic);
|
||||||
|
@ -338,7 +340,7 @@ async fn handle_post_comments(
|
||||||
post_time: time,
|
post_time: time,
|
||||||
text: query.comment.text,
|
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))
|
.map_err(|e| error!("Adding pending comment: {:?}", e))
|
||||||
.ok();
|
.ok();
|
||||||
notify_send
|
notify_send
|
||||||
|
|
|
@ -37,6 +37,7 @@ impl Templates {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct CommentWithId {
|
pub struct CommentWithId {
|
||||||
|
pub addr: Option<String>,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub editable: bool,
|
pub editable: bool,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
{% for comment in comments_pending %}
|
{% for comment in comments_pending %}
|
||||||
<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id | safe }}">
|
<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id | safe }}">
|
||||||
<span class="comment-author">{{ comment.author }}</span>
|
<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>
|
<span class="comment-date">{{ comment.post_time | date(format="%F %R", locale=time_lang) }}</span>
|
||||||
{% if comment.editable %}
|
{% if comment.editable %}
|
||||||
<a href="?edit={{ comment.id | safe }}#edit_comment-form">{{ tr(l=l,k="admin-comment-edit")|safe }}</a>
|
<a href="?edit={{ comment.id | safe }}#edit_comment-form">{{ tr(l=l,k="admin-comment-edit")|safe }}</a>
|
||||||
|
|
Loading…
Reference in a new issue