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.
pull/38/head
Martin Zimmermann 11 years ago
parent f0ee0a18b1
commit 9f2062a900

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

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

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

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

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

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

Loading…
Cancel
Save