changeset 701:094492e66eb9

Implement rate limiting on user photo uploads.
author Brian Neal <bgneal@gmail.com>
date Sat, 14 Sep 2013 17:23:13 -0500
parents e888d627928f
children 1fd0f88480bc
files sg101/settings/base.py user_photos/forms.py
diffstat 2 files changed, 30 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/sg101/settings/base.py	Wed Sep 11 20:31:23 2013 -0500
+++ b/sg101/settings/base.py	Sat Sep 14 17:23:13 2013 -0500
@@ -299,6 +299,7 @@
 USER_PHOTOS_BASE_URL = 'https://s3-us-west-1.amazonaws.com'
 USER_PHOTOS_MAX_SIZE = (660, 720)
 USER_PHOTOS_THUMB_SIZE = (120, 120)
+USER_PHOTOS_RATE_LIMIT = (50, 86400)   # number / seconds
 
 #######################################################################
 # Asynchronous settings (queues, queued_search, redis, celery, etc)
--- a/user_photos/forms.py	Wed Sep 11 20:31:23 2013 -0500
+++ b/user_photos/forms.py	Sat Sep 14 17:23:13 2013 -0500
@@ -6,9 +6,26 @@
 
 from core.s3 import S3Bucket
 from core.image_uploader import upload
+from core.services import get_redis_connection
 from user_photos.models import Photo
 
 
+def rate_limit(key, limit, seconds):
+    """Use Redis to do a rate limit check. Returns True if the limit is violated
+    and False otherwise.
+
+    key - the key to check in Redis
+    limit - the rate limit maximum value
+    seconds - the rate limit period in seconds
+
+    """
+    conn = get_redis_connection()
+    val = conn.incr(key)
+    if val == 1:
+        conn.expire(key, seconds)
+    return val > limit
+
+
 class UploadForm(forms.Form):
     image_file = forms.ImageField()
 
@@ -16,6 +33,18 @@
         self.user = kwargs.pop('user')
         super(UploadForm, self).__init__(*args, **kwargs)
 
+    def clean(self):
+        cleaned_data = super(UploadForm, self).clean()
+
+        # rate limit uploads
+        key = 'user_photos:counts:' + self.user.username
+        limit = settings.USER_PHOTOS_RATE_LIMIT
+        if rate_limit(key, *limit):
+            raise forms.ValidationError("You've exceeded your upload quota. "
+                    "Please try again later.")
+
+        return cleaned_data
+
     def save(self):
         """Processes the image and creates a new Photo object, which is saved to
         the database. The new Photo instance is returned.