Skip to content

Commit

Permalink
Use redis, allow only one CTF to be created per day.
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Zhang committed Sep 7, 2016
1 parent dc085b0 commit 10c81c9
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 7 deletions.
2 changes: 1 addition & 1 deletion cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

app = Flask(__name__, static_url_path='')
self_path = os.path.dirname(os.path.abspath(__file__))
app.config.from_object(config.CalendarConfig(app_root=self_path))
app.config.from_object(config.CalendarConfig(app_root=self_path, testing=os.getenv('STAGING')=='True'))

This comment has been minimized.

Copy link
@chaosagent

chaosagent Sep 7, 2016

Member

Don't use testing or enable DEBUG for staging; staging should be as close to prod as possible. DEBUG and testing also turn off csrf protection and allow access to stack traces.

This comment has been minimized.

Copy link
@mzhang28

mzhang28 Sep 8, 2016

Member

OK fixed

db.init_app(app)

login_manager.init_app(app)
Expand Down
1 change: 1 addition & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def __init__(self, app_root=None, testing=False):
self.MAILGUN_API_KEY = os.getenv('MAILGUN_API_KEY', '')

This comment has been minimized.

Copy link
@chaosagent

chaosagent Sep 7, 2016

Member

if testing is None and something in is.environ testing = true

if testing:
self.DEBUG = True

This comment has been minimized.

Copy link
@chaosagent

chaosagent Sep 7, 2016

Member

Testing is specifically for unit tests, so debugging is not very needed here

self.TESTING = True
self.WTF_CSRF_ENABLED = False

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ psycopg2
pymysql
pytest
python-dotenv
redis
requests
sqlalchemy
wtforms
63 changes: 62 additions & 1 deletion util.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,72 @@
from __future__ import print_function # In python 2.7

import datetime
import os
import random
import re
from functools import wraps
import sys
import time
from functools import wraps, update_wrapper

from flask import abort
from flask import g
from flask_login import current_user, login_required
from passlib.hash import bcrypt
from redis import from_url

redis = from_url(os.getenv("REDIS_URL"))

This comment has been minimized.

Copy link
@chaosagent

chaosagent Sep 7, 2016

Member

use import redis and redis.from_url to keep things explicit



class RateLimitedException(Exception): pass

This comment has been minimized.

Copy link
@chaosagent

chaosagent Sep 7, 2016

Member

Newline before pass



class RateLimit(object):
expiration_window = 10

def __init__(self, key_prefix, limit, interval, send_x_headers):
self.reset = (int(time.time()) // interval) * interval + interval
self.key = key_prefix + str(self.reset)
self.limit = limit
self.interval = interval
self.send_x_headers = send_x_headers
with redis.pipeline() as p:
p.incr(self.key)
p.expireat(self.key, self.reset + self.expiration_window)
print(str(p.get(self.key)), file=sys.stderr)
self.current = p.execute()[0] # min(p.execute()[0], limit)

def increment(self):
with redis.pipeline() as p:
p.incr(self.key)
print(p.get(self.key), file=sys.stderr)

def decrement(self):
with redis.pipeline() as p:
p.decr(self.key)

remaining = property(lambda x: x.limit - x.current)
over_limit = property(lambda x: x.current > x.limit)


def rate_limit(limit=1, interval=120, send_x_headers=True, scope_func='global'):
def decorator(f):
def rate_limited(*args, **kwargs):

This comment has been minimized.

Copy link
@chaosagent

chaosagent Sep 7, 2016

Member

@wraps not update wrapper >.<

key = 'ratelimit/%s/%s/' % (f.__name__, scope_func())
rlimit = RateLimit(key, limit, interval, send_x_headers)
g._view_rate_limit = rlimit
print("%s\t%s" % (rlimit.current, rlimit.limit), file=sys.stderr)
if rlimit.over_limit:
raise RateLimitedException("You done fucked.")
try:
result = f(*args, **kwargs)
except Exception, e:
print("An error occurred: %s" % e, file=sys.stderr)
rlimit.decrement()
return result

return update_wrapper(rate_limited, f)

return decorator


def isoformat(seconds):
Expand Down
18 changes: 13 additions & 5 deletions views/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import config
from forms import EventForm
from models import db, Event
from util import admin_required, isoformat
from util import admin_required, isoformat, RateLimitedException, rate_limit

blueprint = Blueprint('events', __name__, template_folder='templates')

Expand All @@ -17,14 +17,22 @@
def events_create():
event_create_form = EventForm()
if event_create_form.validate_on_submit():
new_event = Event(owner=current_user)
event_create_form.populate_obj(new_event)
db.session.add(new_event)
db.session.commit()
try:
create_event(event_create_form)
except RateLimitedException, e:
return str(e), 429
return redirect(url_for('.events_owned'))
return render_template('events/create.html', event_create_form=event_create_form)


@rate_limit(limit=1, interval=24 * 3600, scope_func=lambda: 'user:%s' % current_user.username)
def create_event(event_create_form):
new_event = Event(owner=current_user)
event_create_form.populate_obj(new_event)
db.session.add(new_event)
db.session.commit()


@blueprint.route('/list/json')
@blueprint.route('/list/json/page/<int:page_number>')
def events_list_json(page_number=1):
Expand Down

0 comments on commit 10c81c9

Please sign in to comment.