]>
git.ipfire.org Git - ipfire.org.git/blob - src/web/base.py
efc47d7fe751b5dfce6806b0357b7af64ed9e96f
17 from ..decorators
import *
21 log
= logging
.getLogger(__name__
)
23 class ratelimit(object):
25 A decorator class which limits how often a function can be called
27 def __init__(self
, *, minutes
, requests
):
28 self
.minutes
= minutes
29 self
.requests
= requests
31 def __call__(self
, method
):
32 @functools.wraps(method
)
33 async def wrapper(handler
, *args
, **kwargs
):
34 # Pass the request to the rate limiter and get a request object
35 req
= handler
.backend
.ratelimiter
.handle_request(handler
.request
,
36 handler
, minutes
=self
.minutes
, limit
=self
.requests
)
38 # If the rate limit has been reached, we won't allow
39 # processing the request and therefore send HTTP error code 429.
40 if await req
.is_ratelimited():
41 raise tornado
.web
.HTTPError(429, "Rate limit exceeded")
43 # Call the wrapped method
44 result
= method(handler
, *args
, **kwargs
)
46 # Await it if it is a coroutine
47 if asyncio
.iscoroutine(result
):
56 class BaseHandler(tornado
.web
.RequestHandler
):
58 # Mark this as private when someone is logged in
60 self
.set_header("Cache-Control", "private")
62 # Always send Vary: Cookie
63 self
.set_header("Vary", "Cookie")
65 def set_expires(self
, seconds
):
67 self
.add_header("Cache-Control", "max-age=%s, must-revalidate" % seconds
)
70 expires
= datetime
.datetime
.utcnow() + datetime
.timedelta(seconds
=seconds
)
71 self
.set_header("Expires", expires
)
73 def write_error(self
, status_code
, **kwargs
):
74 # Translate code into message
76 message
= http
.client
.responses
[status_code
]
80 self
.render("error.html", status_code
=status_code
, message
=message
, **kwargs
)
82 def browser_accepts(self
, t
):
84 Checks if type is in Accept: header
88 for elem
in self
.request
.headers
.get("Accept", "").split(","):
90 type, delim
, q
= elem
.partition(";")
94 # Check if the filetype is in the list of accepted ones
99 # Return hostname in production
100 if self
.request
.host
.endswith("ipfire.org"):
101 return self
.request
.host
103 # Remove the development prefix
104 subdomain
, delimier
, domain
= self
.request
.host
.partition(".")
106 return "%s.ipfire.org" % subdomain
108 # Return whatever it is
109 return self
.request
.host
111 def get_template_namespace(self
):
112 ns
= tornado
.web
.RequestHandler
.get_template_namespace(self
)
114 now
= datetime
.date
.today()
117 "backend" : self
.backend
,
118 "debug" : self
.application
.settings
.get("debug", False),
119 "format_size" : util
.format_size
,
120 "format_time" : util
.format_time
,
121 "hostname" : self
.hostname
,
129 def get_remote_ip(self
):
130 # Fix for clients behind a proxy that sends "X-Forwarded-For".
131 remote_ips
= self
.request
.remote_ip
.split(", ")
133 for remote_ip
in remote_ips
:
135 addr
= ipaddress
.ip_address(remote_ip
)
137 # Skip invalid IP addresses.
140 # Check if the given IP address is from a
147 # Return the last IP if nothing else worked
148 return remote_ips
.pop()
151 def current_address(self
):
152 address
= self
.get_remote_ip()
155 return util
.Address(self
.backend
, address
)
158 def current_country_code(self
):
159 if self
.current_address
:
160 return self
.current_address
.country_code
163 def user_agent(self
):
165 Returns the HTTP user agent
167 return self
.request
.headers
.get("User-Agent", None)
171 return self
.request
.headers
.get("Referer", None)
173 def _request_basic_authentication(self
):
175 Called to ask the client to perform HTTP Basic authentication
177 # Ask for authentication
180 # Say that we support Basic
181 self
.set_header("WWW-Authenticate", "Basic realm=Restricted")
185 def perform_basic_authentication(self
):
187 This handles HTTP Basic authentication.
190 cred
= self
.request
.headers
.get("Authorization", None)
192 return self
._request
_basic
_authentication
()
194 # No basic auth? We cannot handle that
195 if not cred
.startswith("Basic "):
196 return self
._request
_basic
_authentication
()
198 # Decode the credentials
200 # Convert into bytes()
201 cred
= cred
[6:].encode()
204 cred
= base64
.b64decode(cred
).decode()
206 username
, password
= cred
.split(":", 1)
208 # Fail if any of those steps failed
211 raise tornado
.web
.HTTPError(400, "Authorization data was malformed")
213 # Find the user in the database
214 return self
.backend
.accounts
.auth(username
, password
)
218 log
.info("%s authenticated successfully using HTTP Basic authentication" % account
.uid
)
220 log
.warning("Could not authenticate %s" % username
)
224 def get_argument_int(self
, *args
, **kwargs
):
225 arg
= self
.get_argument(*args
, **kwargs
)
227 if arg
is None or arg
== "":
233 raise tornado
.web
.HTTPError(400, "Could not convert integer: %s" % arg
)
235 def get_argument_float(self
, *args
, **kwargs
):
236 arg
= self
.get_argument(*args
, **kwargs
)
238 if arg
is None or arg
== "":
244 raise tornado
.web
.HTTPError(400, "Could not convert float: %s" % arg
)
246 def get_argument_date(self
, arg
, *args
, **kwargs
):
247 value
= self
.get_argument(arg
, *args
, **kwargs
)
252 return dateutil
.parser
.parse(value
)
254 raise tornado
.web
.HTTPError(400)
256 def get_file(self
, name
):
258 file = self
.request
.files
[name
][0]
260 return file["filename"], file["body"], file["content_type"]
264 # Initialize libmagic
265 magic
= magic
.Magic(mime
=True, uncompress
=True)
269 def _deliver_file(self
, data
, filename
=None, prefix
=None):
271 mimetype
= self
.magic
.from_buffer(data
)
274 self
.set_header("Content-Type", mimetype
or "application/octet-stream")
276 # Fetch the file extension
277 if not filename
and prefix
:
278 ext
= mimetypes
.guess_extension(mimetype
)
280 # Compose a new filename
281 filename
= "%s%s" % (prefix
, ext
)
285 self
.set_header("Content-Disposition", "inline; filename=\"%s\"" % filename
)
289 self
.set_header("Content-Length", len(data
))
296 def get_current_user(self
):
297 session_id
= self
.get_cookie("session_id")
301 # Get account from the session object
302 account
= self
.backend
.accounts
.get_by_session(session_id
, self
.request
.host
)
304 # If the account was not found or the session was not valid
305 # any more, we will remove the cookie.
307 self
.clear_cookie("session_id")
313 return self
.application
.backend
317 return self
.backend
.db
321 return self
.backend
.accounts
325 return self
.backend
.downloads
329 return self
.backend
.fireinfo
333 return self
.backend
.iuse
337 return self
.backend
.mirrors
341 return self
.backend
.netboot
345 return self
.backend
.releases
348 class AnalyticsMixin(object):
351 Collect some data about this request
354 log
.debug("Analytics for %s:" % self
)
355 log
.debug(" User-Agent: %s" % self
.user_agent
)
356 log
.debug(" Referrer : %s" % self
.referrer
)
358 # Do nothing if this requst should be ignored
359 if self
._ignore
_analytics
():
362 with self
.db
.transaction():
364 self
.backend
.analytics
.log_unique_visit(
365 address
=self
.current_address
,
366 referrer
=self
.referrer
,
367 country_code
=self
.current_country_code
,
368 user_agent
=self
.user_agent
,
369 host
=self
.request
.host
,
370 uri
=self
.request
.uri
,
373 source
=self
.get_argument("utm_source", None),
374 medium
=self
.get_argument("utm_medium", None),
375 campaign
=self
.get_argument("utm_campaign", None),
376 content
=self
.get_argument("utm_content", None),
377 term
=self
.get_argument("utm_term", None),
380 q
=self
.get_argument("q", None),
383 def _ignore_analytics(self
):
385 Checks if this request should be ignored
387 ignored_user_agents
= (
392 # Only log GET requests
393 if not self
.request
.method
== "GET":
396 # Ignore everything from matching user agents
398 for ignored_user_agent
in ignored_user_agents
:
399 if self
.user_agent
.startswith(ignored_user_agent
):
403 class APIHandler(BaseHandler
):
404 def check_xsrf_cookie(self
):
406 Do nothing here, because we cannot verify the XSRF token
411 # Do not cache any API communication
412 self
.set_header("Cache-Control", "no-cache")
415 class NotFoundHandler(BaseHandler
):
417 # Raises 404 as soon as it is called
418 raise tornado
.web
.HTTPError(404)
421 class ErrorHandler(BaseHandler
):
423 Raises any error we want
429 raise tornado
.web
.HTTPError(400)
431 raise tornado
.web
.HTTPError(code
)