]> git.ipfire.org Git - ipfire.org.git/blob - src/web/base.py
11406547c62d9ae0dfdd92a90d63ef758dfff16a
[ipfire.org.git] / src / web / base.py
1 #!/usr/bin/python
2
3 import asyncio
4 import datetime
5 import dateutil.parser
6 import functools
7 import http.client
8 import ipaddress
9 import logging
10 import time
11 import tornado.locale
12 import tornado.web
13
14 from ..decorators import *
15 from .. import util
16
17 class ratelimit(object):
18 """
19 A decorator class which limits how often a function can be called
20 """
21 def __init__(self, *, minutes, requests):
22 self.minutes = minutes
23 self.requests = requests
24
25 def __call__(self, method):
26 @functools.wraps(method)
27 async def wrapper(handler, *args, **kwargs):
28 # Pass the request to the rate limiter and get a request object
29 req = handler.backend.ratelimiter.handle_request(handler.request,
30 handler, minutes=self.minutes, limit=self.requests)
31
32 # If the rate limit has been reached, we won't allow
33 # processing the request and therefore send HTTP error code 429.
34 if await req.is_ratelimited():
35 raise tornado.web.HTTPError(429, "Rate limit exceeded")
36
37 # Call the wrapped method
38 result = method(handler, *args, **kwargs)
39
40 # Await it if it is a coroutine
41 if asyncio.iscoroutine(result):
42 return await result
43
44 # Return the result
45 return result
46
47 return wrapper
48
49
50 class BaseHandler(tornado.web.RequestHandler):
51 def prepare(self):
52 # Mark this as private when someone is logged in
53 if self.current_user:
54 self.set_header("Cache-Control", "private")
55
56 # Always send Vary: Cookie
57 self.set_header("Vary", "Cookie")
58
59 def set_expires(self, seconds):
60 # For HTTP/1.1
61 self.add_header("Cache-Control", "max-age=%s, must-revalidate" % seconds)
62
63 # For HTTP/1.0
64 expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds)
65 self.set_header("Expires", expires)
66
67 def write_error(self, status_code, **kwargs):
68 # Translate code into message
69 try:
70 message = http.client.responses[status_code]
71 except KeyError:
72 message = None
73
74 self.render("error.html", status_code=status_code, message=message, **kwargs)
75
76 @property
77 def hostname(self):
78 # Return hostname in production
79 if self.request.host.endswith("ipfire.org"):
80 return self.request.host
81
82 # Remove the development prefix
83 subdomain, delimier, domain = self.request.host.partition(".")
84 if subdomain:
85 return "%s.ipfire.org" % subdomain
86
87 # Return whatever it is
88 return self.request.host
89
90 def get_template_namespace(self):
91 ns = tornado.web.RequestHandler.get_template_namespace(self)
92
93 now = datetime.date.today()
94
95 ns.update({
96 "backend" : self.backend,
97 "debug" : self.application.settings.get("debug", False),
98 "format_size" : util.format_size,
99 "format_time" : util.format_time,
100 "hostname" : self.hostname,
101 "now" : now,
102 "q" : None,
103 "year" : now.year,
104 })
105
106 return ns
107
108 def get_remote_ip(self):
109 # Fix for clients behind a proxy that sends "X-Forwarded-For".
110 remote_ips = self.request.remote_ip.split(", ")
111
112 for remote_ip in remote_ips:
113 try:
114 addr = ipaddress.ip_address(remote_ip)
115 except ValueError:
116 # Skip invalid IP addresses.
117 continue
118
119 # Check if the given IP address is from a
120 # private network.
121 if addr.is_private:
122 continue
123
124 return remote_ip
125
126 # Return the last IP if nothing else worked
127 return remote_ips.pop()
128
129 @lazy_property
130 def current_address(self):
131 address = self.get_remote_ip()
132
133 if address:
134 return util.Address(self.backend, address)
135
136 @lazy_property
137 def current_country_code(self):
138 if self.current_address:
139 return self.current_address.country_code
140
141 def get_argument_int(self, *args, **kwargs):
142 arg = self.get_argument(*args, **kwargs)
143
144 if arg is None or arg == "":
145 return
146
147 try:
148 return int(arg)
149 except ValueError:
150 raise tornado.web.HTTPError(400, "Could not convert integer: %s" % arg)
151
152 def get_argument_float(self, *args, **kwargs):
153 arg = self.get_argument(*args, **kwargs)
154
155 if arg is None or arg == "":
156 return
157
158 try:
159 return float(arg)
160 except ValueError:
161 raise tornado.web.HTTPError(400, "Could not convert float: %s" % arg)
162
163 def get_argument_date(self, arg, *args, **kwargs):
164 value = self.get_argument(arg, *args, **kwargs)
165 if value is None:
166 return
167
168 try:
169 return dateutil.parser.parse(value)
170 except ValueError:
171 raise tornado.web.HTTPError(400)
172
173 def get_file(self, name):
174 try:
175 file = self.request.files[name][0]
176
177 return file["filename"], file["body"], file["content_type"]
178 except KeyError:
179 return None
180
181 # Login stuff
182
183 def get_current_user(self):
184 session_id = self.get_cookie("session_id")
185 if not session_id:
186 return
187
188 # Get account from the session object
189 account = self.backend.accounts.get_by_session(session_id, self.request.host)
190
191 # If the account was not found or the session was not valid
192 # any more, we will remove the cookie.
193 if not account:
194 self.clear_cookie("session_id")
195
196 return account
197
198 @property
199 def backend(self):
200 return self.application.backend
201
202 @property
203 def db(self):
204 return self.backend.db
205
206 @property
207 def accounts(self):
208 return self.backend.accounts
209
210 @property
211 def downloads(self):
212 return self.backend.downloads
213
214 @property
215 def fireinfo(self):
216 return self.backend.fireinfo
217
218 @property
219 def iuse(self):
220 return self.backend.iuse
221
222 @property
223 def mirrors(self):
224 return self.backend.mirrors
225
226 @property
227 def netboot(self):
228 return self.backend.netboot
229
230 @property
231 def releases(self):
232 return self.backend.releases
233
234
235 class APIHandler(BaseHandler):
236 def check_xsrf_cookie(self):
237 """
238 Do nothing here, because we cannot verify the XSRF token
239 """
240 pass
241
242 def prepare(self):
243 # Do not cache any API communication
244 self.set_header("Cache-Control", "no-cache")
245
246
247 class NotFoundHandler(BaseHandler):
248 def prepare(self):
249 # Raises 404 as soon as it is called
250 raise tornado.web.HTTPError(404)
251
252
253 class ErrorHandler(BaseHandler):
254 """
255 Raises any error we want
256 """
257 def get(self, code):
258 try:
259 code = int(code)
260 except:
261 raise tornado.web.HTTPError(400)
262
263 raise tornado.web.HTTPError(code)