From 23f84bbcebcb674ed41dcff6f4b9b6cf95d3225c Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Sun, 29 Sep 2019 18:06:59 +0100 Subject: [PATCH] people: Check StopForumSpam when registering accounts Signed-off-by: Michael Tremer --- Makefile.am | 1 + src/backend/accounts.py | 90 +++++++++++++++++++++++++++ src/backend/base.py | 8 +++ src/backend/zeiterfassung.py | 9 +-- src/templates/auth/register-spam.html | 20 ++++++ src/web/auth.py | 10 ++- 6 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 src/templates/auth/register-spam.html diff --git a/Makefile.am b/Makefile.am index 24cf8d7c..eef9578e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -114,6 +114,7 @@ templates_auth_DATA = \ src/templates/auth/activated.html \ src/templates/auth/login.html \ src/templates/auth/register.html \ + src/templates/auth/register-spam.html \ src/templates/auth/register-success.html templates_authdir = $(templatesdir)/auth diff --git a/src/backend/accounts.py b/src/backend/accounts.py index 71253a44..1c348a2a 100644 --- a/src/backend/accounts.py +++ b/src/backend/accounts.py @@ -2,6 +2,7 @@ # encoding: utf-8 import datetime +import json import ldap import ldap.modlist import logging @@ -9,6 +10,7 @@ import os import phonenumbers import sshpubkeys import time +import tornado.httpclient import urllib.parse import urllib.request import zxcvbn @@ -163,6 +165,15 @@ class Accounts(Object): "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \ % (number, number, number, number)) + @tornado.gen.coroutine + def check_spam(self, uid, email, address): + sfs = StopForumSpam(self.backend, uid, email, address) + + # Get spam score + score = yield sfs.check() + + return score >= 50 + # Registration def register(self, uid, email, first_name, last_name): @@ -920,6 +931,85 @@ class Account(Object): ]) +class StopForumSpam(Object): + def init(self, uid, email, address): + self.uid, self.email, self.address = uid, email, address + + @tornado.gen.coroutine + def send_request(self, **kwargs): + arguments = { + "json" : "1", + } + arguments.update(kwargs) + + # Create request + request = tornado.httpclient.HTTPRequest( + "https://api.stopforumspam.org/api", method="POST") + request.body = urllib.parse.urlencode(arguments) + + # Send the request + response = yield self.backend.http_client.fetch(request) + + # Decode the JSON response + return json.loads(response.body.decode()) + + @tornado.gen.coroutine + def check_address(self): + response = yield self.send_request(ip=self.address) + + try: + confidence = response["ip"]["confidence"] + except KeyError: + confidence = 100 + + logging.debug("Confidence for %s: %s" % (self.address, confidence)) + + return confidence + + @tornado.gen.coroutine + def check_username(self): + response = yield self.send_request(username=self.uid) + + try: + confidence = response["username"]["confidence"] + except KeyError: + confidence = 100 + + logging.debug("Confidence for %s: %s" % (self.uid, confidence)) + + return confidence + + @tornado.gen.coroutine + def check_email(self): + response = yield self.send_request(email=self.email) + + try: + confidence = response["email"]["confidence"] + except KeyError: + confidence = 100 + + logging.debug("Confidence for %s: %s" % (self.email, confidence)) + + return confidence + + @tornado.gen.coroutine + def check(self, threshold=95): + """ + This function tries to detect if we have a spammer. + + To honour the privacy of our users, we only send the IP + address and username and if those are on the database, we + will send the email address as well. + """ + confidences = yield [self.check_address(), self.check_username()] + + if any((c < threshold for c in confidences)): + confidences += yield [self.check_email()] + + # Build a score based on the lowest confidence + return 100 - min(confidences) + + if __name__ == "__main__": a = Accounts() diff --git a/src/backend/base.py b/src/backend/base.py index 075546e0..17027665 100644 --- a/src/backend/base.py +++ b/src/backend/base.py @@ -3,6 +3,7 @@ import configparser import io import tornado.gen +import tornado.httpclient from . import accounts from . import blog @@ -44,6 +45,12 @@ class Backend(object): # Setup database. self.setup_database() + # Create HTTPClient + self.http_client = tornado.httpclient.AsyncHTTPClient( + defaults = { + "User-Agent" : "IPFireWebApp", + } + ) # Initialize settings first. self.settings = settings.Settings(self) self.memcache = memcached.Memcached(self) @@ -91,6 +98,7 @@ class Backend(object): def run_task(self, task, *args, **kwargs): tasks = { "check-mirrors" : self.mirrors.check_all, + "check-spam-score" : self.accounts.check_spam_score, "cleanup" : self.cleanup, "scan-files" : self.releases.scan_files, "send-all-messages" : self.messages.queue.send_all, diff --git a/src/backend/zeiterfassung.py b/src/backend/zeiterfassung.py index aa0173e2..158d6113 100644 --- a/src/backend/zeiterfassung.py +++ b/src/backend/zeiterfassung.py @@ -23,13 +23,6 @@ class ZeiterfassungClient(Object): if not all((self.url, self.api_key, self.api_secret)): raise RuntimeError("%s is not configured" % self.__class__.__name__) - # Create HTTPClient - self.http_client = tornado.httpclient.AsyncHTTPClient( - defaults = { - "User-Agent" : "IPFireWebApp", - } - ) - def _make_signature(self, method, path, body): # Empty since we only support POST canonical_query = "" @@ -63,7 +56,7 @@ class ZeiterfassungClient(Object): ) # Send the request - response = yield self.http_client.fetch(request) + response = yield self.backend.http_client.fetch(request) # Decode the JSON response d = json.loads(response.body.decode()) diff --git a/src/templates/auth/register-spam.html b/src/templates/auth/register-spam.html new file mode 100644 index 00000000..6b44690d --- /dev/null +++ b/src/templates/auth/register-spam.html @@ -0,0 +1,20 @@ +{% extends "../base.html" %} + +{% block title %}{{ _("Oops!") }}{% end block %} + +{% block content %} +
+
+
+
+ + +

+ {{ _("Unfortunately we could not create your account because you have shown up on our spam radar.") }} + {{ _("Please get in touch if you think that this is an error.") }} +

+
+
+
+
+{% end block %} diff --git a/src/web/auth.py b/src/web/auth.py index ce4a9c85..d2872695 100644 --- a/src/web/auth.py +++ b/src/web/auth.py @@ -90,7 +90,7 @@ class RegisterHandler(base.BaseHandler): self.render("auth/register.html") - @base.blacklisted + @tornado.gen.coroutine @base.ratelimit(minutes=24*60, requests=5) def post(self): uid = self.get_argument("uid") @@ -99,6 +99,14 @@ class RegisterHandler(base.BaseHandler): first_name = self.get_argument("first_name") last_name = self.get_argument("last_name") + # Check if this is a spam account + is_spam = yield self.backend.accounts.check_spam(uid, email, + address=self.get_remote_ip()) + + if is_spam: + self.render("auth/register-spam.html") + return + # Register account try: with self.db.transaction(): -- 2.39.2