Also add an option `direct-reply` to control the number of comments
on a thread without referencing a child (to avoid a simple while loop
that `curl -XPOST ...` the url).

Defaults to 3, that means a /24 (or /48 for IPv6) address can only post
3 direct responses on a thread at all.
This commit is contained in:
Martin Zimmermann 2013-11-13 16:10:17 +01:00
parent f0ee0a18b1
commit 9f2062a900
6 changed files with 53 additions and 39 deletions

View File

@ -186,14 +186,19 @@ for IPv4, ``/48`` for IPv6).
[guard] [guard]
enabled = true enabled = true
ratelimit = 2 ratelimit = 2
direct-reply = 3
enabled enabled
enable guard, recommended in production. Not useful for debugging enable guard, recommended in production. Not useful for debugging
purposes. purposes.
ratelimit: N ratelimit
limit to N new comments per minute. limit to N new comments per minute.
direct-reply
how many comments directly to the thread (prevent a simple
`while true; do curl ...; done`.
Appendum Appendum
--------- ---------

View File

@ -119,8 +119,8 @@ class Config:
"to = ", "from = ", "to = ", "from = ",
"[guard]", "[guard]",
"enabled = true", "enabled = true",
"ratelimit = 2" "ratelimit = 2",
"" "direct-reply = 3"
] ]
@classmethod @classmethod

View File

@ -2,11 +2,9 @@
import sqlite3 import sqlite3
class IssoDBException(Exception):
pass
from isso.db.comments import Comments from isso.db.comments import Comments
from isso.db.threads import Threads from isso.db.threads import Threads
from isso.db.spam import Guard
class SQLite3: class SQLite3:
@ -19,6 +17,7 @@ class SQLite3:
self.threads = Threads(self) self.threads = Threads(self)
self.comments = Comments(self) self.comments = Comments(self)
self.guard = Guard(self)
self.execute([ self.execute([
'CREATE TRIGGER IF NOT EXISTS remove_stale_threads', 'CREATE TRIGGER IF NOT EXISTS remove_stale_threads',

View File

@ -33,7 +33,6 @@ class Comments:
' text VARCHAR, author VARCHAR, email VARCHAR, website VARCHAR,', ' text VARCHAR, author VARCHAR, email VARCHAR, website VARCHAR,',
' likes INTEGER DEFAULT 0, dislikes INTEGER DEFAULT 0, voters BLOB NOT NULL);']) ' likes INTEGER DEFAULT 0, dislikes INTEGER DEFAULT 0, voters BLOB NOT NULL);'])
@spam.check
def add(self, uri, c): def add(self, uri, c):
""" """
Add a new comment to the database and return public fields as dict. Add a new comment to the database and return public fields as dict.

View File

@ -1,47 +1,55 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
import time import time
import functools
from isso.db import IssoDBException
class TooManyComments(IssoDBException): class Guard:
pass
def __init__(self, db):
def check(func): self.db = db
self.conf = db.conf.section("guard")
def dec(self, uri, c): def validate(self, uri, comment):
if not self.db.conf.getboolean("guard", "enabled"): if not self.conf.getboolean("enabled"):
return func(self, uri, c) return True, ""
# block more than two comments per minute for func in (self._limit, self._spam):
valid, reason = func(uri, comment)
if not valid:
return False, reason
return True, ""
@classmethod
def ids(cls, rv):
return [str(col[0]) for col in rv]
def _limit(self, uri, comment):
# block more than :param:`ratelimit` comments per minute
rv = self.db.execute([ rv = self.db.execute([
'SELECT id FROM comments WHERE remote_addr = ? AND created > ?;' 'SELECT id FROM comments WHERE remote_addr = ? AND ? - created < 60;'
], (c["remote_addr"], time.time() + 60)).fetchall() ], (comment["remote_addr"], time.time())).fetchall()
if len(rv) >= self.db.conf.getint("guard", "ratelimit"): if len(rv) >= self.conf.getint("ratelimit"):
raise TooManyComments return False, "{0}: ratelimit exceeded ({1})".format(
comment["remote_addr"], ', '.join(Guard.ids(rv)))
# block more than three comments as direct response to the post # block more than three comments as direct response to the post
rv = self.db.execute([ if comment["parent"] is None:
'SELECT id FROM comments WHERE remote_addr = ? AND parent IS NULL;'
], (c["remote_addr"], )).fetchall()
if len(rv) >= 3:
raise TooManyComments
# block reply to own comment if the cookie is still available (max age)
if "parent" in c:
rv = self.db.execute([ rv = self.db.execute([
'SELECT id FROM comments WHERE remote_addr = ? AND id = ? AND ? - created < ?;' 'SELECT id FROM comments WHERE',
], (c["remote_addr"], c["parent"], time.time(), ' tid = (SELECT id FROM threads WHERE uri = ?)',
self.db.conf.getint("general", "max-age"))).fetchall() 'AND remote_addr = ?',
'AND parent IS NULL;'
], (uri, comment["remote_addr"])).fetchall()
if len(rv) > 0: if len(rv) >= self.conf.getint("direct-reply"):
raise TooManyComments return False, "%i direct responses to %s" % (len(rv), uri)
return func(self, uri, c) return True, ""
return dec def _spam(self, uri, comment):
return True, ""

View File

@ -15,7 +15,7 @@ from werkzeug.exceptions import BadRequest, Forbidden, NotFound
from isso.compat import text_type as str from isso.compat import text_type as str
from isso import utils, local from isso import utils, local, db
from isso.utils import http, parse, markdown from isso.utils import http, parse, markdown
from isso.utils.crypto import pbkdf2 from isso.utils.crypto import pbkdf2
from isso.views import requires from isso.views import requires
@ -61,6 +61,7 @@ class API(object):
self.conf = isso.conf.section("general") self.conf = isso.conf.section("general")
self.moderated = isso.conf.getboolean("moderation", "enabled") self.moderated = isso.conf.getboolean("moderation", "enabled")
self.guard = isso.db.guard
self.threads = isso.db.threads self.threads = isso.db.threads
self.comments = isso.db.comments self.comments = isso.db.comments
@ -127,8 +128,10 @@ class API(object):
# notify extensions that the new comment is about to save # notify extensions that the new comment is about to save
self.signal("comments.new:before-save", thread, data) self.signal("comments.new:before-save", thread, data)
if data is None: valid, reason = self.guard.validate(uri, data)
raise Forbidden if not valid:
self.signal("comments.new:guard", reason)
raise Forbidden(reason)
with self.isso.lock: with self.isso.lock:
rv = self.comments.add(uri, data) rv = self.comments.add(uri, data)