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]
enabled = true
ratelimit = 2
direct-reply = 3
enabled
enable guard, recommended in production. Not useful for debugging
purposes.
ratelimit: N
ratelimit
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
---------

View File

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

View File

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

View File

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

View File

@ -1,47 +1,55 @@
# -*- encoding: utf-8 -*-
import time
from isso.db import IssoDBException
import functools
class TooManyComments(IssoDBException):
pass
class Guard:
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"):
return func(self, uri, c)
if not self.conf.getboolean("enabled"):
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([
'SELECT id FROM comments WHERE remote_addr = ? AND created > ?;'
], (c["remote_addr"], time.time() + 60)).fetchall()
'SELECT id FROM comments WHERE remote_addr = ? AND ? - created < 60;'
], (comment["remote_addr"], time.time())).fetchall()
if len(rv) >= self.db.conf.getint("guard", "ratelimit"):
raise TooManyComments
if len(rv) >= self.conf.getint("ratelimit"):
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
if comment["parent"] is None:
rv = self.db.execute([
'SELECT id FROM comments WHERE remote_addr = ? AND parent IS NULL;'
], (c["remote_addr"], )).fetchall()
'SELECT id FROM comments WHERE',
' tid = (SELECT id FROM threads WHERE uri = ?)',
'AND remote_addr = ?',
'AND parent IS NULL;'
], (uri, comment["remote_addr"])).fetchall()
if len(rv) >= 3:
raise TooManyComments
if len(rv) >= self.conf.getint("direct-reply"):
return False, "%i direct responses to %s" % (len(rv), uri)
# block reply to own comment if the cookie is still available (max age)
if "parent" in c:
rv = self.db.execute([
'SELECT id FROM comments WHERE remote_addr = ? AND id = ? AND ? - created < ?;'
], (c["remote_addr"], c["parent"], time.time(),
self.db.conf.getint("general", "max-age"))).fetchall()
return True, ""
if len(rv) > 0:
raise TooManyComments
return func(self, uri, c)
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 import utils, local
from isso import utils, local, db
from isso.utils import http, parse, markdown
from isso.utils.crypto import pbkdf2
from isso.views import requires
@ -61,6 +61,7 @@ class API(object):
self.conf = isso.conf.section("general")
self.moderated = isso.conf.getboolean("moderation", "enabled")
self.guard = isso.db.guard
self.threads = isso.db.threads
self.comments = isso.db.comments
@ -127,8 +128,10 @@ class API(object):
# notify extensions that the new comment is about to save
self.signal("comments.new:before-save", thread, data)
if data is None:
raise Forbidden
valid, reason = self.guard.validate(uri, data)
if not valid:
self.signal("comments.new:guard", reason)
raise Forbidden(reason)
with self.isso.lock:
rv = self.comments.add(uri, data)