· 6 years ago · Oct 18, 2019, 08:28 AM
1import hashlib
2import memcache
3import traceback
4from flask import request
5from functools import wraps
6from wakatime_website import app
7from werkzeug.contrib.cache import MemcachedCache
8
9mc = memcache.Client()
10cache = MemcachedCache(mc)
11
12def cached(fn=None, unique_per_user=True, minutes=30):
13 """Caches a Flask route/view in memcached.
14
15 The request url, args, and current user are used to build the cache key.
16 Only GET requests are cached.
17 By default, cached requests expire after 30 minutes.
18 """
19
20 if not isinstance(minutes, int):
21 raise Exception('Minutes must be an integer number.')
22
23 def wrapper(func):
24 @wraps(func)
25 def inner(*args, **kwargs):
26 if request.method != 'GET':
27 return func(*args, **kwargs)
28
29 prefix = 'flask-request'
30 path = request.full_path
31 user_id = app.current_user.id if app.current_user.is_authenticated else None
32 key = u('{user}-{method}-{path}').format(
33 user=user_id,
34 method=request.method,
35 path=path,
36 )
37 hashed = hashlib.md5(key.encode('utf8')).hexdigest()
38 hashed = '{prefix}-{hashed}'.format(prefix=prefix, hashed=hashed)
39
40 try:
41 resp = cache.get(hashed)
42 if resp:
43 return resp
44 except:
45 app.logger.error(traceback.format_exc())
46 resp = None
47
48 resp = func(*args, **kwargs)
49 try:
50 cache.set(hashed, resp, timeout=minutes * 60)
51 except:
52 app.logger.error(traceback.format_exc())
53 return resp
54
55 return inner
56 return wrapper(fn) if fn else wrapper
57Rate limit API requests by IP or Current User
58import redis
59import traceback
60from flask import abort, request
61from functools import wraps
62from wakatime_website import app
63
64r = redis.Redis(decode_responses=True)
65
66def rate_limited(fn=None, limit=20, methods=[], ip=True, user=True, minutes=1):
67 """Limits requests to this endpoint to `limit` per `minutes`."""
68
69 if not isinstance(limit, int):
70 raise Exception('Limit must be an integer number.')
71 if limit < 1:
72 raise Exception('Limit must be greater than zero.')
73
74 def wrapper(func):
75 @wraps(func)
76 def inner(*args, **kwargs):
77 if not methods or request.method in methods:
78
79 if ip:
80 increment_counter(type='ip', for_methods=methods,
81 minutes=minutes)
82 count = get_count(type='ip', for_methods=methods)
83 if count > limit:
84 abort(429)
85
86 if user and app.current_user.is_authenticated:
87 increment_counter(type='user', for_methods=methods,
88 minutes=minutes)
89 count = get_count(type='user', for_methods=methods)
90 if count > limit:
91 abort(429)
92
93 return func(*args, **kwargs)
94
95 return inner
96 return wrapper(fn) if fn else wrapper
97
98def get_counter_key(type=None, for_only_this_route=True, for_methods=None): if not isinstance(for_methods, list):
99 for_methods = []
100 if type == 'ip':
101 key = request.remote_addr
102 elif type == 'user':
103 key = app.current_user.id if app.current_user.is_authenticated else None
104 else:
105 raise Exception('Unknown rate limit type: {0}'.format(type))
106 route = ''
107 if for_only_this_route:
108 route = '{endpoint}'.format(
109 endpoint=request.endpoint,
110 )
111 return u('{type}-{methods}-{key}{route}').format(
112 type=type,
113 key=key,
114 methods=','.join(for_methods),
115 route=route,
116 )
117
118def increment_counter(type=None, for_only_this_route=True, for_methods=None,
119 minutes=1):
120 if type not in ['ip', 'user']:
121 raise Exception('Type must be ip or user.')
122
123 key = get_counter_key(type=type, for_only_this_route=for_only_this_route,
124 for_methods=for_methods)
125 try:
126 r.incr(key)
127 r.expire(key, time=60 * minutes)
128 except:
129 app.logger.error(traceback.format_exc())
130 pass
131
132def get_count(type=None, for_only_this_route=True, for_methods=None):
133 key = get_counter_key(type=type, for_only_this_route=for_only_this_route,
134 for_methods=for_methods)
135 try:
136 return int(r.get(key) or 0)
137 except:
138 app.logger.error(traceback.format_exc())
139 return 0
140
141# Prevent brute forcing secrets or tokens
142import redis
143import traceback
144from flask import request
145from functools import wraps
146from wakatime_website import app
147from werkzeug.exceptions import NotFound
148
149r = redis.Redis(decode_responses=True)
150
151def protected(fn=None, limit=10, minutes=60):
152 """Bans IP after requesting a protected resource too many times.
153
154 Prevents IP from making more than `limit` requests per `minutes` to
155 the decorated route. Prevents enumerating secrets or tokens from urls or
156 query arguments by blocking requests after too many 404 not found errors.
157 """
158
159 if not isinstance(limit, int):
160 raise Exception('Limit must be an integer number.')
161 if not isinstance(minutes, int):
162 raise Exception('Minutes must be an integer number.')
163
164 def wrapper(func):
165 @wraps(func)
166 def inner(*args, **kwargs):
167 key = u('bruteforce-{}-{}').format(request.endpoint, request.remote_addr)
168 try:
169 count = int(r.get(key) or 0)
170 if count > limit:
171 r.incr(key)
172 seconds = 60 * minutes
173 r.expire(key, time=seconds)
174 app.logger.info('Request blocked by protected decorator.')
175 return '404', 404
176 except:
177 app.logger.error(traceback.format_exc())
178
179 try:
180 result = func(*args, **kwargs)
181 except NotFound:
182 try:
183 r.incr(key)
184 seconds = 60 * minutes
185 r.expire(key, time=seconds)
186 except:
187 pass
188 raise
189
190 if isinstance(result, tuple) and len(result) > 1 and result[1] == 404:
191 try:
192 r.incr(key)
193 seconds = 60 * minutes
194 r.expire(key, time=seconds)
195 except:
196 pass
197
198 return result
199
200 return inner
201 return wrapper(fn) if fn else wrapper