]> git.gir.st - subscriptionfeed.git/blob - app/__init__.py
fix exception in pagination code on &foo=<emptystring>
[subscriptionfeed.git] / app / __init__.py
1 import base64
2 import secrets
3 import importlib
4 from flask import Flask
5
6 from .common.common import *
7 from .common.user import init_login
8
9 app = Flask(__name__)
10 app.secret_key = base64.b64decode(cf['frontend'].get('secret_key','')) or \
11 secrets.token_bytes(16) # development fallback; CSRF/cookies won't persist.
12 init_login(app)
13
14 for name in cf['frontend']['modules'].split(','):
15 blueprint = importlib.import_module('.'+name, __name__)
16 app.register_blueprint(blueprint.frontend)
17
18 # TODO: move this somewhere else
19 @app.template_global()
20 def querystring_page(fields):
21 tmp = dict(request.args)
22 for field,what in fields.items():
23 if type(what) is tuple:
24 (plusminus, default) = what
25 tmp[field] = int(tmp.get(field) or str(default)) + plusminus
26 elif type(what) is type(None):
27 if field in tmp: del tmp[field]
28 else:
29 tmp[field] = what
30 from werkzeug.urls import url_encode
31 return url_encode(tmp)
32
33 # TODO: should this go somewhere else?
34 # This error handler logs requests to external apis, and POST data. this makes debugging of api responses easier, as the request can be reconstructed and replayed.
35 from flask import g, request
36 from werkzeug.exceptions import InternalServerError
37 @app.errorhandler(InternalServerError)
38 def log_errors(e):
39 if request.method == "POST":
40 app.logger.error(request.data)
41 if 'api_requests' in g:
42 app.logger.error(g.api_requests)
43 return e
44
45 # TODO: build a proper flask extension
46 # Magic CSRF protection: This modifies outgoing HTML responses and injects a csrf token into all forms.
47 # All post requests are then checked if they contain the valid token.
48 # TODO:
49 # - knobs: mimetypes, http methods, form field name, token generator
50 # - inject a http header into all responses (that could be used by apis)
51 # - allow csrf token to be passed in http header, json, ...
52 # - a decorator on routes to opt out of verification or output munging
53 # https://stackoverflow.com/questions/19574694/flask-hit-decorator-before-before-request-signal-fires
54 # - allow specifying hmac message contents (currently request.remote_addr)
55 import hmac
56 import hashlib
57 from flask import request
58 from werkzeug.exceptions import BadRequest
59 from html.parser import HTMLParser
60 @app.template_global()
61 def csrf_token():
62 # TODO: will fail behind reverse proxy (remote_addr always localhost)
63 return hmac.new(app.secret_key, request.remote_addr.encode('ascii'), hashlib.sha256).hexdigest()
64 @app.after_request
65 def add_csrf_protection(response):
66 if response.mimetype == "text/html":
67 csrf_elem = f'<input type="hidden" name="csrf" value="{csrf_token()}"/>'
68 new_response = add_csrf(response.get_data().decode('utf-8'), csrf_elem)
69 response.set_data(new_response.encode('utf-8'))
70 return response
71 @app.before_request
72 def verify_csrf_protection():
73 if request.method == "POST" and request.form.get('csrf') != csrf_token():
74 raise BadRequest("CSRF validation failed")
75 request.form = request.form.copy() # make it mutable
76 request.form.poplist('csrf') # remove our csrf again
77 def add_csrf(html_in, csrf_elem):
78 class FindForms(HTMLParser):
79 def __init__(self, html):
80 super().__init__()
81 self.forms = [] # tuples of (line_number, tag_offset, tag_length)
82 super().feed(html)
83 def handle_starttag(self, tag, attrs):
84 line, offset = self.getpos()
85 if tag == "form" and dict(attrs).get('method','').upper() == "POST":
86 self.forms.append((line, offset, len(self.get_starttag_text())))
87 lines = html_in.splitlines(keepends=True)
88 # Note: going in reverse, to not invalidate offsets:
89 for line, offset, length in reversed(FindForms(html_in).forms):
90 l = lines[line-1]
91 lines[line-1] = l[:offset+length] + csrf_elem + l[offset+length:]
92 return "".join(lines)
Imprint / Impressum