From 6394cfb1c2ad1e69f5904829898197e89b0d1cf4 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Wed, 19 Jun 2019 21:14:19 +0200 Subject: [PATCH] Re-think rate limiting for resending The way it was done ended up defeaeting the service sending things right away for people who did *not* violate the rate limit. So instead, keep track of exactly when the last email was sent for each user, and rate-limit based on that. --- .../migrations/0004_resend_rate_limit.py | 30 +++++++++++++++++++ django/archives/mailarchives/models.py | 8 +++++ django/archives/mailarchives/views.py | 9 ++++-- resender/archives_resender.py | 2 +- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 django/archives/mailarchives/migrations/0004_resend_rate_limit.py diff --git a/django/archives/mailarchives/migrations/0004_resend_rate_limit.py b/django/archives/mailarchives/migrations/0004_resend_rate_limit.py new file mode 100644 index 0000000..bdd522f --- /dev/null +++ b/django/archives/mailarchives/migrations/0004_resend_rate_limit.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-06-19 19:02 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('mailarchives', '0003_message_resend'), + ] + + operations = [ + migrations.CreateModel( + name='LastResentMessage', + fields=[ + ('sentto', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('sentat', models.DateTimeField()), + ], + ), + migrations.AlterUniqueTogether( + name='resendmessage', + unique_together=set([('message', 'sendto')]), + ), + ] diff --git a/django/archives/mailarchives/models.py b/django/archives/mailarchives/models.py index 5c0c1e0..0affa3b 100644 --- a/django/archives/mailarchives/models.py +++ b/django/archives/mailarchives/models.py @@ -126,6 +126,14 @@ class ResendMessage(models.Model): sendto = models.ForeignKey(User, null=False, blank=False) registeredat = models.DateTimeField(null=False, blank=False) + class Meta: + unique_together = (('message', 'sendto'), ) + + +class LastResentMessage(models.Model): + sentto = models.ForeignKey(User, null=False, blank=False, primary_key=True) + sentat = models.DateTimeField(null=False, blank=False) + class ApiClient(models.Model): apikey = models.CharField(max_length=100, null=False, blank=False) diff --git a/django/archives/mailarchives/views.py b/django/archives/mailarchives/views.py index 03fdf7b..4835e7e 100644 --- a/django/archives/mailarchives/views.py +++ b/django/archives/mailarchives/views.py @@ -647,10 +647,15 @@ def resend(request, messageid): if request.method == 'POST': if request.POST.get('resend', None) == '1': # Figure out if this user has sent an email recently, and if so refuse it - if ResendMessage.objects.filter(sendto=request.user, registeredat__gt=datetime.now()).exists(): + if LastResentMessage.objects.filter(sentto=request.user, sentat__gt=datetime.now() - timedelta(seconds=settings.RESEND_RATE_LIMIT_SECONDS)).exists(): return HttpResponse("You have already requested an email to be sent in the past {0} seconds. Please try again later.".format(settings.RESEND_RATE_LIMIT_SECONDS)) - ResendMessage.objects.get_or_create(message=m, sendto=request.user, registeredat=datetime.now() + timedelta(seconds=settings.RESEND_RATE_LIMIT_SECONDS)) + ResendMessage.objects.get_or_create(message=m, sendto=request.user, defaults={ + 'registeredat': datetime.now(), + }) + connection.cursor().execute("INSERT INTO mailarchives_lastresentmessage (sentto_id, sentat) VALUES (%(id)s, CURRENT_TIMESTAMP) ON CONFLICT (sentto_id) DO UPDATE SET sentat=EXCLUDED.sentat", { + 'id': request.user.id, + }) connection.cursor().execute("NOTIFY archives_resend") return HttpResponseRedirect('/message-id/resend/{0}/complete'.format(m.messageid)) diff --git a/resender/archives_resender.py b/resender/archives_resender.py index c097ed7..061ea16 100755 --- a/resender/archives_resender.py +++ b/resender/archives_resender.py @@ -16,7 +16,7 @@ import psycopg2 def process_queue(conn, sender, smtpserver, heloname): with conn.cursor() as curs: - curs.execute("SELECT r.id, u.email, m.rawtxt FROM mailarchives_resendmessage r INNER JOIN auth_user u ON u.id=r.sendto_id INNER JOIN messages m ON m.id=r.message_id WHERE registeredat < CURRENT_TIMESTAMP ORDER BY r.id FOR UPDATE OF r LIMIT 1") + curs.execute("SELECT r.id, u.email, m.rawtxt FROM mailarchives_resendmessage r INNER JOIN auth_user u ON u.id=r.sendto_id INNER JOIN messages m ON m.id=r.message_id ORDER BY r.id FOR UPDATE OF r LIMIT 1") ll = curs.fetchall() if len(ll) == 0: conn.rollback() -- 2.39.5