CherryPy Project Download

RateLimitTool

import cherrypy
from datetime import datetime, timedelta

'''
Rate Limiting Tool

Author: Joshua Roesslein <jroesslein at gmail.com>

Provides a tool that allows the developer to apply rate limits 
for their endpoints. If the user exceeds their limit the tool will block
further requests on the handler until the reset time passes.

'''

'''
Rate Limit Datastore 

Provides persistent storage for the rate limit tool.
This default implementation uses the sqlite3 module.
To use your own data backend implement this class
and pass it into the tool during use.

YOU MUST first execute this script (python ratelimit.py) to install the sqlite3
database tables before running the server. To flush the datastore, delete
the DB file (default: .ratelimitdata.sqlite3) and rerun the script.

'''
import sqlite3
class RateLimitDatastore(object):
  def _conn(self):
    return sqlite3.connect('.ratelimitdata.sqlite3',
                           detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)

  def install_tables(self):
    conn = self._conn()
    conn.execute('''CREATE TABLE IF NOT EXISTS trackers(
                  token TEXT, endpoint TEXT, hit_count INTEGER, reset_time TIMESTAMP)''')
    conn.close()

  def get(self, token, endpoint):
    conn = self._conn()
    tracker = conn.execute('SELECT * FROM trackers WHERE token=? AND endpoint=?',
                           (token, endpoint)).fetchone()
    conn.close()
    if tracker is not None:
      return tracker[2:4]
    else:
      return None

  def put(self, token, endpoint, tracker):
    conn = self._conn()
    conn.execute('INSERT INTO trackers VALUES(?,?,?,?)',
                 (token, endpoint, tracker[0], tracker[1]))
    conn.commit()
    conn.close()
    
  def update(self, token, endpoint, tracker):
    conn = self._conn()
    values = (tracker[0],)
    set_query = 'hit_count=?'
    if tracker[1] is not None:
      values += (tracker[1],)
      set_query += ', reset_time=?'
    values += (token, endpoint,)
    query = 'UPDATE trackers SET %s WHERE token=? AND endpoint=?' % set_query
    conn.execute(query, values)
    conn.commit()
    conn.close() 

if __name__ == '__main__':
  '''Install database tables'''
  print 'Installing tables...'
  RateLimitDatastore().install_tables()
  print 'Done.'

class RateLimitTool(cherrypy.Tool):
  def __init__(self, priority=60):
    self._name = None
    self._priority = priority
    self.callable = None
    self._setargs()

  def _check_limit(self, quotas, counter, datastore, rate_exceed_code=400):
    token = cherrypy.request.login
    if token is None:
      token = cherrypy.request.remote.ip

    # Find quota for the requesting user
    quota = None
    for q in quotas:
      allowed = q.get('allowed', None)
      if allowed is None or allowed.count(token):
        quota = q
        break
    if quota is None:
      # User not allowed to make this request!
      raise cherrypy.HTTPError(403)

    # Get limit
    limit = quota.get('limit', None)
    if limit is None:
      return

    # Get tracker
    if counter.get('global', False):
      endpoint = 'global'
    else:
      endpoint = cherrypy.request.path_info
    tracker = datastore.get(token, endpoint)

    # Get reset delay from counter (if not specified will be set to zero)
    delay = counter.get('reset_delay', 0)
    if isinstance(delay, dict):
      delay = delay.get(qid, delay.get('default', 0))
      
    # Create new tracker if reset time has been reached or tracker not found
    if tracker is None or tracker[1] <= datetime.utcnow():
      delay = counter.get('reset_delay', 0)
      if isinstance(delay, dict):
        delay = delay.get(qid, delay.get('default', 0))
      new_tracker = (0, datetime.utcnow() + timedelta(seconds=delay))
      if tracker is None:
        datastore.put(token, endpoint, new_tracker)
      else:
        datastore.update(token, endpoint, new_tracker)
      tracker = new_tracker

    # Verify user has not exceeded rate limit
    limit = quota.get('limit', None)
    if limit is not None and tracker[0] >= limit:
      raise cherrypy.HTTPError(400)

    # Store data needed for charge hook in request
    cherrypy.request.ratelimit_tracker = tracker
    cherrypy.request.ratelimit_token = token
    cherrypy.request.ratelimit_endpoint = endpoint

  def _charge_hit(self, datastore):
    # Get tracker. If not set, this request is not limited
    try:
      tracker = cherrypy.request.ratelimit_tracker
    except:
      return

    # Only charge a hit if a non 500 status is returned
    if int(cherrypy.response.status.split()[0]) < 500:
      datastore.update(cherrypy.request.ratelimit_token,
                    cherrypy.request.ratelimit_endpoint, (tracker[0] + 1, None))

  def _setup(self):
    '''Hook into request'''
    conf = self._merged_args()
    p = conf.pop('priority', self._priority)
    if 'datastore' not in conf:
      conf['datastore'] = RateLimitDatastore()

    # Check hit count hook
    cherrypy.request.hooks.attach('before_handler', self._check_limit,
                                  priority=p, **conf)

    # Charge hit hook
    conf.pop('quotas', None)
    conf.pop('counter', None)
    cherrypy.request.hooks.attach('on_end_request', self._charge_hit,
                                  priority=p, **conf)

Hosted by WebFaction

Log in as guest/cherrypy to create/edit wiki pages