-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #64 from geoadmin/develop
New Release v2.5.0 - #minor
- Loading branch information
Showing
11 changed files
with
618 additions
and
658 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,144 +1,2 @@ | ||
import logging | ||
import re | ||
import time | ||
|
||
from werkzeug.exceptions import HTTPException | ||
|
||
from flask import Flask | ||
from flask import abort | ||
from flask import g | ||
from flask import request | ||
from flask.helpers import url_for | ||
|
||
from app import settings | ||
from app.helpers.utils import make_error_msg | ||
from app.settings import ALLOWED_DOMAINS_PATTERN | ||
from app.settings import CACHE_CONTROL | ||
from app.settings import CACHE_CONTROL_4XX | ||
|
||
logger = logging.getLogger(__name__) | ||
route_logger = logging.getLogger('app.routes') | ||
|
||
# Standard Flask application initialisation | ||
|
||
app = Flask(__name__) | ||
app.config.from_object(settings) | ||
|
||
|
||
def is_domain_allowed(domain): | ||
return re.match(ALLOWED_DOMAINS_PATTERN, domain) is not None | ||
|
||
|
||
# NOTE it is better to have this method registered first (before validate_origin) otherwise | ||
# the route might not be logged if another method reject the request. | ||
@app.before_request | ||
def log_route(): | ||
g.setdefault('request_started', time.time()) | ||
route_logger.info('%s %s', request.method, request.path) | ||
|
||
|
||
# Reject request from non allowed origins | ||
@app.before_request | ||
def validate_origin(): | ||
# The Origin headers is automatically set by the browser and cannot be changed by the javascript | ||
# application. Unfortunately this header is only set if the request comes from another origin. | ||
# Sec-Fetch-Site header is set to `same-origin` by most of the browser except by Safari ! | ||
# The best protection would be to use the Sec-Fetch-Site and Origin header, however this is | ||
# not supported by Safari. Therefore we added a fallback to the Referer header for Safari. | ||
sec_fetch_site = request.headers.get('Sec-Fetch-Site', None) | ||
origin = request.headers.get('Origin', None) | ||
referrer = request.headers.get('Referer', None) | ||
|
||
logger.debug( | ||
"Validate origin: sec_fetch_site=%s, origin=%s, referrer=%s", | ||
sec_fetch_site, | ||
origin, | ||
referrer | ||
) | ||
|
||
if origin is not None: | ||
if is_domain_allowed(origin): | ||
return | ||
logger.error('Origin=%s does not match %s', origin, ALLOWED_DOMAINS_PATTERN) | ||
abort(403, 'Permission denied') | ||
|
||
# BGDIINF_SB-3115: Apparently IOS 16 has a bug and set Sec-Fetch-Site=cross-site even if the | ||
# request is originated (same origin and/or referrer) from the same site ! Therefore to avoid | ||
# issue on IOS we first checks the referrer before checking Sec-Fetch-Site even if this not | ||
# correct. | ||
if referrer is not None: | ||
if is_domain_allowed(referrer): | ||
return | ||
logger.error('Referer=%s does not match %s', referrer, ALLOWED_DOMAINS_PATTERN) | ||
abort(403, 'Permission denied') | ||
|
||
if sec_fetch_site is not None: | ||
if sec_fetch_site in ['same-origin', 'same-site']: | ||
return | ||
logger.error('Sec-Fetch-Site=%s is not allowed', sec_fetch_site) | ||
abort(403, 'Permission denied') | ||
|
||
logger.error('Referer and/or Origin and/or Sec-Fetch-Site headers not set') | ||
abort(403, 'Permission denied') | ||
|
||
|
||
# Add CORS Headers to all request | ||
@app.after_request | ||
def add_cors_header(response): | ||
# Do not add CORS header to internal /checker endpoint. | ||
if request.endpoint == 'checker': | ||
return response | ||
|
||
response.headers['Access-Control-Allow-Origin'] = request.host_url | ||
if 'Origin' in request.headers and is_domain_allowed(request.headers['Origin']): | ||
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] | ||
response.headers['Vary'] = 'Origin' | ||
response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, OPTIONS' | ||
response.headers['Access-Control-Allow-Headers'] = '*' | ||
return response | ||
|
||
|
||
@app.after_request | ||
def add_cache_control_header(response): | ||
# For /checker route we let the frontend proxy decide how to cache it. | ||
if request.method == 'GET' and request.endpoint != 'checker': | ||
if response.status_code >= 400: | ||
response.headers.set('Cache-Control', CACHE_CONTROL_4XX) | ||
else: | ||
response.headers.set('Cache-Control', CACHE_CONTROL) | ||
return response | ||
|
||
|
||
# NOTE it is better to have this method registered last (after add_cors_header) otherwise | ||
# the response might not be correct (e.g. headers added in another after_request hook). | ||
@app.after_request | ||
def log_response(response): | ||
route_logger.info( | ||
"%s %s - %s", | ||
request.method, | ||
request.path, | ||
response.status, | ||
extra={ | ||
'response': | ||
{ | ||
"status_code": response.status_code, "headers": dict(response.headers.items()) | ||
}, | ||
"duration": time.time() - g.get('request_started', time.time()) | ||
} | ||
) | ||
return response | ||
|
||
|
||
# Register error handler to make sure that every error returns a json answer | ||
@app.errorhandler(Exception) | ||
def handle_exception(err): | ||
"""Return JSON instead of HTML for HTTP errors.""" | ||
if isinstance(err, HTTPException): | ||
logger.error(err) | ||
return make_error_msg(err.code, err.description) | ||
|
||
logger.exception('Unexpected exception: %s', err) | ||
return make_error_msg(500, "Internal server error, please consult logs") | ||
|
||
|
||
from app import routes # isort:skip pylint: disable=wrong-import-position | ||
from app import routes | ||
from app.app import app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import logging | ||
import re | ||
import time | ||
|
||
from werkzeug.exceptions import HTTPException | ||
|
||
from flask import Flask | ||
from flask import g | ||
from flask import request | ||
|
||
from app import settings | ||
from app.helpers.utils import make_error_msg | ||
from app.settings import ALLOWED_DOMAINS_PATTERN | ||
from app.settings import CACHE_CONTROL | ||
from app.settings import CACHE_CONTROL_4XX | ||
|
||
logger = logging.getLogger(__name__) | ||
route_logger = logging.getLogger('app.routes') | ||
|
||
# Standard Flask application initialisation | ||
|
||
app = Flask(__name__) | ||
app.config.from_object(settings) | ||
|
||
|
||
def is_domain_allowed(domain): | ||
return re.match(ALLOWED_DOMAINS_PATTERN, domain) is not None | ||
|
||
|
||
# NOTE it is better to have this method registered first (before validate_origin) otherwise | ||
# the route might not be logged if another method reject the request. | ||
@app.before_request | ||
def log_route(): | ||
g.setdefault('request_started', time.time()) | ||
route_logger.info('%s %s', request.method, request.path) | ||
|
||
|
||
# Add CORS Headers to all request | ||
@app.after_request | ||
def add_cors_header(response): | ||
# Only add CORS header if an origin or referer header is present in request | ||
# is a host part of our whitelist | ||
cors_origin = None | ||
if 'Origin' in request.headers and is_domain_allowed(request.headers['Origin']): | ||
cors_origin = request.headers['Origin'] | ||
|
||
if 'Referer' in request.headers and is_domain_allowed(request.headers['Referer']): | ||
cors_origin = request.headers['Referer'] | ||
|
||
if cors_origin: | ||
response.headers['Access-Control-Allow-Origin'] = cors_origin | ||
response.headers['Vary'] = 'Origin' | ||
response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD, OPTIONS' | ||
response.headers['Access-Control-Allow-Headers'] = '*' | ||
|
||
return response | ||
|
||
|
||
@app.after_request | ||
def add_cache_control_header(response): | ||
# For /checker route we let the frontend proxy decide how to cache it. | ||
if request.method == 'GET' and request.endpoint != 'checker': | ||
if response.status_code >= 400: | ||
response.headers.set('Cache-Control', CACHE_CONTROL_4XX) | ||
else: | ||
response.headers.set('Cache-Control', CACHE_CONTROL) | ||
return response | ||
|
||
|
||
# NOTE it is better to have this method registered last (after add_cors_header) otherwise | ||
# the response might not be correct (e.g. headers added in another after_request hook). | ||
@app.after_request | ||
def log_response(response): | ||
route_logger.info( | ||
"%s %s - %s", | ||
request.method, | ||
request.path, | ||
response.status, | ||
extra={ | ||
'response': | ||
{ | ||
"status_code": response.status_code, "headers": dict(response.headers.items()) | ||
}, | ||
"duration": time.time() - g.get('request_started', time.time()) | ||
} | ||
) | ||
return response | ||
|
||
|
||
# Register error handler to make sure that every error returns a json answer | ||
@app.errorhandler(Exception) | ||
def handle_exception(err): | ||
"""Return JSON instead of HTML for HTTP errors.""" | ||
if isinstance(err, HTTPException): | ||
logger.error(err) | ||
return make_error_msg(err.code, err.description) | ||
|
||
logger.exception('Unexpected exception: %s', err) | ||
return make_error_msg(500, "Internal server error, please consult logs") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.