This repository has been archived by the owner on Oct 12, 2017. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
/
RateLimitTool
170 lines (141 loc) · 5.36 KB
/
RateLimitTool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
= 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)
}}}