]> git.ipfire.org Git - ipfire.org.git/blame - src/web/base.py
Deploy rate-limiting
[ipfire.org.git] / src / web / base.py
CommitLineData
940227cb
MT
1#!/usr/bin/python
2
cc3b928d 3import datetime
66862195 4import dateutil.parser
cfe7d74c 5import functools
11347e46 6import http.client
a69e87a1 7import ipaddress
47ed77ed 8import logging
940227cb
MT
9import time
10import tornado.locale
11import tornado.web
12
f110a9ff 13from ..decorators import *
a95c2f97 14from .. import util
60024cc8 15
cfe7d74c
MT
16def blacklisted(method):
17 @tornado.gen.coroutine
18 @functools.wraps(method)
19 def wrapper(self, *args, **kwargs):
20 # Check if remote is blacklisted
21 is_blacklisted = yield self.remote.is_blacklisted()
22
23 # If so, redirect to the blocked page
24 if is_blacklisted:
25 logging.warning("%s is blacklisted" % self.remote)
26
27 return self.redirect("https://www.ipfire.org/blocked")
28
29 return method(self, *args, **kwargs)
30
31 return wrapper
32
372ef119
MT
33class ratelimit(object):
34 def __init__(self, minutes=15, requests=180):
35 self.minutes = minutes
36 self.requests = requests
37
38 def __call__(self, method):
39 @functools.wraps(method)
40 def wrapper(handler, *args, **kwargs):
41 # Pass the request to the rate limiter and get a request object
42 req = handler.backend.ratelimiter.handle_request(handler.request,
43 handler, minutes=self.minutes, limit=self.requests)
44
45 # If the rate limit has been reached, we won't allow
46 # processing the request and therefore send HTTP error code 429.
47 if req.is_ratelimited():
48 raise tornado.web.HTTPError(429, "Rate limit exceeded")
49
50 return method(handler, *args, **kwargs)
51
52 return wrapper
53
cfe7d74c 54
940227cb 55class BaseHandler(tornado.web.RequestHandler):
f6ed3d4d
MT
56 def set_expires(self, seconds):
57 # For HTTP/1.1
b592d7c8 58 self.add_header("Cache-Control", "max-age=%s, must-revalidate" % seconds)
f6ed3d4d
MT
59
60 # For HTTP/1.0
61 expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
b592d7c8 62 self.add_header("Expires", expires)
f6ed3d4d 63
37ed7c3c
MT
64 def write_error(self, status_code, **kwargs):
65 # Translate code into message
66 try:
11347e46 67 message = http.client.responses[status_code]
37ed7c3c
MT
68 except KeyError:
69 message = None
70
71 self.render("error.html", status_code=status_code, message=message, **kwargs)
72
d3d02231
MT
73 def xsrf_form_html(self, *args, **kwargs):
74 # Set Vary: Cookie header
75 self.add_header("Vary", "Cookie")
76
77 return super().xsrf_form_html(*args, **kwargs)
78
4ca1a601
MT
79 @property
80 def hostname(self):
81 # Remove the development prefix
82 return self.request.host.replace(".dev.", ".")
83
bff08cb0
MT
84 def get_template_namespace(self):
85 ns = tornado.web.RequestHandler.get_template_namespace(self)
86
911064cf 87 now = datetime.date.today()
cc3b928d 88
bff08cb0 89 ns.update({
6c13ca2d 90 "backend" : self.backend,
5613b94b 91 "debug" : self.application.settings.get("debug", False),
a95c2f97
MT
92 "format_size" : util.format_size,
93 "format_time" : util.format_time,
4ca1a601 94 "hostname" : self.hostname,
911064cf
MT
95 "now" : now,
96 "year" : now.year,
bff08cb0 97 })
940227cb 98
bff08cb0 99 return ns
940227cb 100
9068dba1
MT
101 def get_remote_ip(self):
102 # Fix for clients behind a proxy that sends "X-Forwarded-For".
66862195 103 remote_ips = self.request.remote_ip.split(", ")
494d80e6 104
66862195
MT
105 for remote_ip in remote_ips:
106 try:
a69e87a1 107 addr = ipaddress.ip_address(remote_ip)
66862195
MT
108 except ValueError:
109 # Skip invalid IP addresses.
110 continue
9068dba1 111
66862195
MT
112 # Check if the given IP address is from a
113 # private network.
114 if addr.is_private:
115 continue
9068dba1 116
66862195 117 return remote_ip
9068dba1 118
494d80e6
MT
119 # Return the last IP if nothing else worked
120 return remote_ips.pop()
121
cfe7d74c
MT
122 @lazy_property
123 def remote(self):
124 address = self.get_remote_ip()
125
126 if address:
127 return self.backend.geoip.lookup(address)
128
f110a9ff
MT
129 @lazy_property
130 def current_country_code(self):
131 remote_ip = self.get_remote_ip()
132
133 if remote_ip:
134 return self.backend.geoip.get_country(remote_ip)
135
9068dba1
MT
136 def get_remote_location(self):
137 if not hasattr(self, "__remote_location"):
138 remote_ip = self.get_remote_ip()
139
140 self.__remote_location = self.geoip.get_location(remote_ip)
141
142 return self.__remote_location
143
bd2723d4
MT
144 def get_argument_int(self, *args, **kwargs):
145 arg = self.get_argument(*args, **kwargs)
146
147 if arg is None or arg == "":
148 return
149
150 try:
151 return int(arg)
152 except ValueError:
153 raise tornado.web.HTTPError(400)
154
66862195
MT
155 def get_argument_date(self, arg, *args, **kwargs):
156 value = self.get_argument(arg, *args, **kwargs)
157 if value is None:
158 return
159
160 try:
161 return dateutil.parser.parse(value)
162 except ValueError:
163 raise tornado.web.HTTPError(400)
164
5cc10421
MT
165 def get_file(self, name):
166 try:
167 file = self.request.files[name][0]
168
169 return file["filename"], file["body"], file["content_type"]
170 except KeyError:
171 return None
172
66862195
MT
173 # Login stuff
174
175 def get_current_user(self):
176 session_id = self.get_cookie("session_id")
177 if not session_id:
178 return
179
180 # Get account from the session object
181 account = self.backend.accounts.get_by_session(session_id, self.request.host)
182
183 # If the account was not found or the session was not valid
184 # any more, we will remove the cookie.
185 if not account:
186 self.clear_cookie("session_id")
187
188 return account
189
a6dc0bad
MT
190 @property
191 def backend(self):
192 return self.application.backend
193
9068dba1
MT
194 @property
195 def db(self):
196 return self.backend.db
197
940227cb
MT
198 @property
199 def accounts(self):
a6dc0bad 200 return self.backend.accounts
940227cb
MT
201
202 @property
9068dba1
MT
203 def downloads(self):
204 return self.backend.downloads
205
66862195
MT
206 @property
207 def fireinfo(self):
208 return self.backend.fireinfo
209
9068dba1
MT
210 @property
211 def iuse(self):
212 return self.backend.iuse
940227cb 213
e28b082e
MT
214 @property
215 def memcached(self):
216 return self.backend.memcache
217
940227cb
MT
218 @property
219 def mirrors(self):
9068dba1
MT
220 return self.backend.mirrors
221
222 @property
223 def netboot(self):
224 return self.backend.netboot
940227cb 225
940227cb
MT
226 @property
227 def releases(self):
9068dba1 228 return self.backend.releases
940227cb
MT
229
230 @property
9068dba1
MT
231 def geoip(self):
232 return self.backend.geoip
940227cb
MT
233
234 @property
66862195
MT
235 def talk(self):
236 return self.backend.talk
940227cb 237
66862195 238
3403dc5e
MT
239class NotFoundHandler(BaseHandler):
240 def prepare(self):
241 # Raises 404 as soon as it is called
242 raise tornado.web.HTTPError(404)
243
3403dc5e 244
37ed7c3c
MT
245class ErrorHandler(BaseHandler):
246 """
247 Raises any error we want
248 """
249 def get(self, code):
250 try:
251 code = int(code)
252 except:
253 raise tornado.web.HTTPError(400)
254
255 raise tornado.web.HTTPError(code)
baa693a3
MT
256
257
258class BlockedHandler(BaseHandler):
259 def get(self):
260 # 403 - Forbidden
261 self.set_status(403)
262
263 self.render("static/blocked.html", address=self.get_remote_ip())