Skip to content

Commit

Permalink
Merge pull request #64 from geoadmin/develop
Browse files Browse the repository at this point in the history
New Release v2.5.0 - #minor
  • Loading branch information
ltshb authored Aug 29, 2024
2 parents bd68802 + ec0ad73 commit 5391ba4
Show file tree
Hide file tree
Showing 11 changed files with 618 additions and 658 deletions.
10 changes: 6 additions & 4 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ signature-mutators=
expected-line-ending-format=LF

# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# This regex match URL and allow them to be longer than the line limit. The regex only match lines
# with only one URL (plus some characters that are required by python syntax, like '#', or quotes and trailing comma)
ignore-long-lines=^\s*(#\s*)?f?[<"']?https?://\S+[>"']?,?$

# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
Expand Down Expand Up @@ -510,6 +512,6 @@ min-public-methods=2

# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception,
StandardError
overgeneral-exceptions=builtins.BaseException,
builtins.Exception,
builtins.StandardError
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Buster slim python 3.7 base image.
FROM python:3.9-slim-buster
FROM python:3.12-slim-bullseye
ENV HTTP_PORT 8080
RUN groupadd -r geoadmin && useradd -r -s /bin/false -g geoadmin geoadmin

Expand Down
18 changes: 9 additions & 9 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ verify_ssl = true
name = "pypi"

[packages]
PyYAML = "~=5.4"
gevent = "~=20.9.0"
gunicorn = "~=19.9.0"
Flask = "~=2.0"
Pillow = "~=9.1"
qrcode = "~=7.3"
logging-utilities = "~=3.0"
python-dotenv = "~=0.19"
PyYAML = "~=6.0.2"
gevent = "~=24.2.1"
gunicorn = "~=23.0.0"
Flask = "~=3.0.3"
Pillow = "~=10.4.0"
qrcode = "~=7.4"
logging-utilities = "~=4.4.1"
python-dotenv = "~=1.0.1"

[dev-packages]
yapf = "*"
Expand All @@ -23,4 +23,4 @@ click = "*"
pipenv-shebang = "*"

[requires]
python_version = "3.9"
python_version = "3.12"
941 changes: 479 additions & 462 deletions Pipfile.lock

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

| Branch | Status |
|--------|-----------|
| develop | ![Build Status](https://codebuild.eu-central-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoibVJMdm9sbXA4cU1YcWtFMml1UVQwQ2tmdzQ3Z0ZsOFBjVDlsb1NNNTFwZURlWE9qdENiVytId0VzVkJpblBvTmxqVEllSEt0cnlVcXNNR2pqRTNESjRNPSIsIml2UGFyYW1ldGVyU3BlYyI6IkdsNE1FbkZka0hqTFFscjAiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=develop) |
| master | ![Build Status](https://codebuild.eu-central-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoibVJMdm9sbXA4cU1YcWtFMml1UVQwQ2tmdzQ3Z0ZsOFBjVDlsb1NNNTFwZURlWE9qdENiVytId0VzVkJpblBvTmxqVEllSEt0cnlVcXNNR2pqRTNESjRNPSIsIml2UGFyYW1ldGVyU3BlYyI6IkdsNE1FbkZka0hqTFFscjAiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master) |
| develop | ![Build Status](https://codebuild.eu-central-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiRS9LWUtEMWtjbThGbmt5OC9DWlJkZ2l0M096NWpuc3grZkRtek85QUpUSUMvZ1NHWUN3QXJWdzdpb0c2OGtXMTFkYVpyUzJvcEtxN2xoUmxwUlZIbU9RPSIsIml2UGFyYW1ldGVyU3BlYyI6InhaRHVOdjhYRW84T2FIRUsiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=develop) |
| master | ![Build Status](https://codebuild.eu-central-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiRS9LWUtEMWtjbThGbmt5OC9DWlJkZ2l0M096NWpuc3grZkRtek85QUpUSUMvZ1NHWUN3QXJWdzdpb0c2OGtXMTFkYVpyUzJvcEtxN2xoUmxwUlZIbU9RPSIsIml2UGFyYW1ldGVyU3BlYyI6InhaRHVOdjhYRW84T2FIRUsiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master) |

## Table of content

Expand Down Expand Up @@ -139,7 +139,6 @@ Each image contains the following metadata:
These metadata can be read with the following command

```bash
# NOTE: Currently we don't have permission to do docker pull on AWS ECR
make dockerlogin
docker pull 974517877189.dkr.ecr.eu-central-1.amazonaws.com/service-qrcode:develop.latest

Expand Down
146 changes: 2 additions & 144 deletions app/__init__.py
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
99 changes: 99 additions & 0 deletions app/app.py
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")
2 changes: 1 addition & 1 deletion app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from flask import make_response
from flask import request

from app import app
from app.app import app
from app.helpers.url import validate_url
from app.version import APP_VERSION

Expand Down
2 changes: 1 addition & 1 deletion service_qrcode.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'''Service launcher to use Flask without wsgi.py
'''
from app import app # pylint: disable=unused-import
from app.app import app # pylint: disable=unused-import
from app.helpers.utils import init_logging

# Initialize Logging using JSON format for all loggers and using the Stream Handler.
Expand Down
Loading

0 comments on commit 5391ba4

Please sign in to comment.