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