From 4ad15f084919a8b0f904a044c9f237e294a1893f Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 18 Nov 2024 18:52:39 +0100 Subject: [PATCH 01/10] fix malformed json on auth ref. oscar.krause/fastapi-dls#1 --- test/main.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/main.py b/test/main.py index 67856c9..1574aba 100644 --- a/test/main.py +++ b/test/main.py @@ -106,6 +106,40 @@ def test_auth_v1_origin(): assert response.json().get('origin_ref') == ORIGIN_REF +def test_auth_v1_origin_malformed_json(): + import re + + # see oscar.krause/fastapi-dls#1 + json = """{ + "registration_pending": "false", + "environment": { + "guest_driver_version": "guest_driver_version", + "hostname": "myhost", + "ip_address_list": ["192.168.1.123"], + "os_version": "os_version", + "os_platform": "os_platform", + "fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}, + "host_driver_version": "host_driver_version" + }, + "update_pending": "false", + "candidate_origin_ref": {ORIGIN_REF}, + }""" + + # test regex (temporary, until this section is merged into main.py + + json_test = '{"environment": {"fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}' + regex = r'(\"mac_address_list\"\:\s?\[)([\w\d])' + replaced = re.sub(regex, r'\1"\2', json_test) + assert replaced == '{"environment": {"fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]}}' + + json = re.sub(regex, r'\1"\2', json) + # + + response = client.post('/auth/v1/origin', json=payload) + assert response.status_code == 200 + assert response.json().get('origin_ref') == ORIGIN_REF + + def auth_v1_origin_update(): payload = { "registration_pending": False, From e33024db868246d33aa241ebe3b453c1b965d41c Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 18 Nov 2024 19:58:15 +0100 Subject: [PATCH 02/10] fixed variable names ref. oscar.krause/fastapi-dls#1 --- test/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/main.py b/test/main.py index 1574aba..fa96c30 100644 --- a/test/main.py +++ b/test/main.py @@ -110,7 +110,7 @@ def test_auth_v1_origin_malformed_json(): import re # see oscar.krause/fastapi-dls#1 - json = """{ + payload = """{ "registration_pending": "false", "environment": { "guest_driver_version": "guest_driver_version", @@ -132,7 +132,7 @@ def test_auth_v1_origin_malformed_json(): replaced = re.sub(regex, r'\1"\2', json_test) assert replaced == '{"environment": {"fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]}}' - json = re.sub(regex, r'\1"\2', json) + payload = re.sub(regex, r'\1"\2', payload) # response = client.post('/auth/v1/origin', json=payload) From a6b2f2a942cded76ea94b2bf75c175d534e773da Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 18 Nov 2024 20:30:37 +0100 Subject: [PATCH 03/10] fixed json payload --- test/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/main.py b/test/main.py index fa96c30..2384570 100644 --- a/test/main.py +++ b/test/main.py @@ -108,6 +108,7 @@ def test_auth_v1_origin(): def test_auth_v1_origin_malformed_json(): import re + import json # see oscar.krause/fastapi-dls#1 payload = """{ @@ -133,6 +134,7 @@ def test_auth_v1_origin_malformed_json(): assert replaced == '{"environment": {"fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]}}' payload = re.sub(regex, r'\1"\2', payload) + payload = json.loads(payload) # response = client.post('/auth/v1/origin', json=payload) From 1aee423120bf3b9e7a70febbd5f7e6b516b37415 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 18 Nov 2024 21:00:14 +0100 Subject: [PATCH 04/10] fixes --- test/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/main.py b/test/main.py index 2384570..c564208 100644 --- a/test/main.py +++ b/test/main.py @@ -111,7 +111,7 @@ def test_auth_v1_origin_malformed_json(): import json # see oscar.krause/fastapi-dls#1 - payload = """{ + payload = f"""{ "registration_pending": "false", "environment": { "guest_driver_version": "guest_driver_version", @@ -123,7 +123,7 @@ def test_auth_v1_origin_malformed_json(): "host_driver_version": "host_driver_version" }, "update_pending": "false", - "candidate_origin_ref": {ORIGIN_REF}, + "candidate_origin_ref": "{ORIGIN_REF}" }""" # test regex (temporary, until this section is merged into main.py From 018d7c34fcdc95898ea4ed97847a93579015f76a Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Mon, 18 Nov 2024 21:24:00 +0100 Subject: [PATCH 05/10] fixes --- test/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/main.py b/test/main.py index c564208..4265aeb 100644 --- a/test/main.py +++ b/test/main.py @@ -111,20 +111,20 @@ def test_auth_v1_origin_malformed_json(): import json # see oscar.krause/fastapi-dls#1 - payload = f"""{ + payload = f'''{{ "registration_pending": "false", - "environment": { + "environment": {{ "guest_driver_version": "guest_driver_version", "hostname": "myhost", "ip_address_list": ["192.168.1.123"], "os_version": "os_version", "os_platform": "os_platform", - "fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}, + "fingerprint": {{"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}, "host_driver_version": "host_driver_version" - }, + }}, "update_pending": "false", "candidate_origin_ref": "{ORIGIN_REF}" - }""" + }}''' # test regex (temporary, until this section is merged into main.py From 15f14cac1176174f64a603c1a3fdc43fd37eb0f3 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 19 Nov 2024 09:18:12 +0100 Subject: [PATCH 06/10] implemented "SUPPORT_MALFORMED_JSON" variable --- .PKGBUILD/PKGBUILD | 1 + .gitlab-ci.yml | 1 + README.md | 31 ++++++++++++++++--------------- app/main.py | 5 +++++ app/middleware.py | 40 ++++++++++++++++++++++++++++++++++++++++ test/main.py | 39 ++++++++------------------------------- 6 files changed, 71 insertions(+), 46 deletions(-) create mode 100644 app/middleware.py diff --git a/.PKGBUILD/PKGBUILD b/.PKGBUILD/PKGBUILD index 09f606b..820d8cc 100644 --- a/.PKGBUILD/PKGBUILD +++ b/.PKGBUILD/PKGBUILD @@ -48,6 +48,7 @@ package() { install -Dm755 "$srcdir/$pkgname/app/main.py" "$pkgdir/opt/$pkgname/main.py" install -Dm755 "$srcdir/$pkgname/app/orm.py" "$pkgdir/opt/$pkgname/orm.py" install -Dm755 "$srcdir/$pkgname/app/util.py" "$pkgdir/opt/$pkgname/util.py" + install -Dm755 "$srcdir/$pkgname/app/middleware.py" "$pkgdir/opt/$pkgname/middleware.py" install -Dm644 "$srcdir/$pkgname.default" "$pkgdir/etc/default/$pkgname" install -Dm644 "$srcdir/$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service" install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a8aa3bd..898d06b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,6 +140,7 @@ test: - test/**/* variables: DATABASE: sqlite:///../app/db.sqlite + SUPPORT_MALFORMED_JSON: true parallel: matrix: - IMAGE: [ 'python:3.12-slim-bookworm' ] diff --git a/README.md b/README.md index 8d14c53..b0e9e01 100644 --- a/README.md +++ b/README.md @@ -410,21 +410,22 @@ After first success you have to replace `--issue` with `--renew`. # Configuration -| Variable | Default | Usage | -|------------------------|----------------------------------------|------------------------------------------------------------------------------------------------------| -| `DEBUG` | `false` | Toggles `fastapi` debug mode | -| `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable | -| `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable | -| `TOKEN_EXPIRE_DAYS` | `1` | Client auth-token validity (used for authenticate client against api, **not `.tok` file!**) | -| `LEASE_EXPIRE_DAYS` | `90` | Lease time in days | -| `LEASE_RENEWAL_PERIOD` | `0.15` | The percentage of the lease period that must elapse before a licensed client can renew a license \*1 | -| `DATABASE` | `sqlite:///db.sqlite` | See [official SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html) | -| `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \*2 | -| `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid | -| `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid | -| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid | -| `INSTANCE_KEY_RSA` | `/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs \*3 | -| `INSTANCE_KEY_PUB` | `/cert/instance.public.pem` | Site-wide public key \*3 | +| Variable | Default | Usage | +|--------------------------|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| `DEBUG` | `false` | Toggles `fastapi` debug mode | +| `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable | +| `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable | +| `TOKEN_EXPIRE_DAYS` | `1` | Client auth-token validity (used for authenticate client against api, **not `.tok` file!**) | +| `LEASE_EXPIRE_DAYS` | `90` | Lease time in days | +| `LEASE_RENEWAL_PERIOD` | `0.15` | The percentage of the lease period that must elapse before a licensed client can renew a license \*1 | +| `DATABASE` | `sqlite:///db.sqlite` | See [official SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html) | +| `CORS_ORIGINS` | `https://{DLS_URL}` | Sets `Access-Control-Allow-Origin` header (comma separated string) \*2 | +| `SITE_KEY_XID` | `00000000-0000-0000-0000-000000000000` | Site identification uuid | +| `INSTANCE_REF` | `10000000-0000-0000-0000-000000000001` | Instance identification uuid | +| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid | +| `INSTANCE_KEY_RSA` | `/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs \*3 | +| `INSTANCE_KEY_PUB` | `/cert/instance.public.pem` | Site-wide public key \*3 | +| `SUPPORT_MALFORMED_JSON` | `false` | Support parsing for mal formatted "mac_address_list" ([Issue](https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/issues/1)) | \*1 For example, if the lease period is one day and the renewal period is 20%, the client attempts to renew its license every 4.8 hours. If network connectivity is lost, the loss of connectivity is detected during license renewal and the diff --git a/app/main.py b/app/main.py index 41fb238..3ad688f 100644 --- a/app/main.py +++ b/app/main.py @@ -96,6 +96,11 @@ app.add_middleware( allow_methods=['*'], allow_headers=['*'], ) +if bool(env('SUPPORT_MALFORMED_JSON', False)): + from middleware import PatchMalformedJsonMiddleware + + logger.info(f'Enabled "PatchMalformedJsonMiddleware"!') + app.add_middleware(PatchMalformedJsonMiddleware, enabled=True) # Helper diff --git a/app/middleware.py b/app/middleware.py new file mode 100644 index 0000000..c96e520 --- /dev/null +++ b/app/middleware.py @@ -0,0 +1,40 @@ +import json +import logging +import re + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request + +logger = logging.getLogger(__name__) + + +class PatchMalformedJsonMiddleware(BaseHTTPMiddleware): + # see oscar.krause/fastapi-dls#1 + + def __init__(self, app, enabled: bool): + super().__init__(app) + self.enabled = enabled + + async def dispatch(self, request: Request, call_next): + body = await request.body() + content_type = request.headers.get('Content-Type') + + if self.enabled and content_type == 'application/json': + body = body.decode() + try: + json.loads(body) + except json.decoder.JSONDecodeError: + logger.warning(f'Malformed json received! Try to fix it, "PatchMalformedJsonMiddleware" is enabled.') + body = body.replace('\t', '') + body = body.replace('\n', '') + + regex = '(\"mac_address_list\"\:\s?\[)([\w\d])' + s = re.sub(regex, r'\1"\2', body) + logger.debug(f'Fixed JSON: "{s}"') + s = json.loads(s) # ensure json is now valid + + # set new body + request._body = json.dumps(s).encode('utf-8') + + response = await call_next(request) + return response diff --git a/test/main.py b/test/main.py index 4265aeb..3aa229b 100644 --- a/test/main.py +++ b/test/main.py @@ -1,7 +1,8 @@ +import sys from base64 import b64encode as b64enc -from hashlib import sha256 from calendar import timegm from datetime import datetime +from hashlib import sha256 from os.path import dirname, join from uuid import uuid4, UUID @@ -9,7 +10,8 @@ from dateutil.relativedelta import relativedelta from jose import jwt, jwk from jose.constants import ALGORITHMS from starlette.testclient import TestClient -import sys + +from middleware import PatchMalformedJsonMiddleware # add relative path to use packages as they were in the app/ dir sys.path.append('../') @@ -18,6 +20,7 @@ sys.path.append('../app') from app import main from app.util import load_key +main.app.add_middleware(PatchMalformedJsonMiddleware, enabled=True) client = TestClient(main.app) ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld' @@ -106,40 +109,14 @@ def test_auth_v1_origin(): assert response.json().get('origin_ref') == ORIGIN_REF -def test_auth_v1_origin_malformed_json(): +def test_auth_v1_origin_malformed_json(): # see oscar.krause/fastapi-dls#1 import re - import json - - # see oscar.krause/fastapi-dls#1 - payload = f'''{{ - "registration_pending": "false", - "environment": {{ - "guest_driver_version": "guest_driver_version", - "hostname": "myhost", - "ip_address_list": ["192.168.1.123"], - "os_version": "os_version", - "os_platform": "os_platform", - "fingerprint": {{"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}, - "host_driver_version": "host_driver_version" - }}, - "update_pending": "false", - "candidate_origin_ref": "{ORIGIN_REF}" - }}''' - + # test regex (temporary, until this section is merged into main.py - json_test = '{"environment": {"fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}' - regex = r'(\"mac_address_list\"\:\s?\[)([\w\d])' + regex = '(\"mac_address_list\"\:\s?\[)([\w\d])' replaced = re.sub(regex, r'\1"\2', json_test) assert replaced == '{"environment": {"fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]}}' - - payload = re.sub(regex, r'\1"\2', payload) - payload = json.loads(payload) - # - - response = client.post('/auth/v1/origin', json=payload) - assert response.status_code == 200 - assert response.json().get('origin_ref') == ORIGIN_REF def auth_v1_origin_update(): From fb3ac4291fbc7fe810d820d7269e382bb7e98d41 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 19 Nov 2024 09:40:59 +0100 Subject: [PATCH 07/10] code styling --- app/middleware.py | 5 +++-- test/main.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/middleware.py b/app/middleware.py index c96e520..7e3d20d 100644 --- a/app/middleware.py +++ b/app/middleware.py @@ -11,6 +11,8 @@ logger = logging.getLogger(__name__) class PatchMalformedJsonMiddleware(BaseHTTPMiddleware): # see oscar.krause/fastapi-dls#1 + REGEX = '(\"mac_address_list\"\:\s?\[)([\w\d])' + def __init__(self, app, enabled: bool): super().__init__(app) self.enabled = enabled @@ -28,8 +30,7 @@ class PatchMalformedJsonMiddleware(BaseHTTPMiddleware): body = body.replace('\t', '') body = body.replace('\n', '') - regex = '(\"mac_address_list\"\:\s?\[)([\w\d])' - s = re.sub(regex, r'\1"\2', body) + s = re.sub(PatchMalformedJsonMiddleware.REGEX, r'\1"\2', body) logger.debug(f'Fixed JSON: "{s}"') s = json.loads(s) # ensure json is now valid diff --git a/test/main.py b/test/main.py index 3aa229b..2e70558 100644 --- a/test/main.py +++ b/test/main.py @@ -111,11 +111,11 @@ def test_auth_v1_origin(): def test_auth_v1_origin_malformed_json(): # see oscar.krause/fastapi-dls#1 import re + from middleware import PatchMalformedJsonMiddleware # test regex (temporary, until this section is merged into main.py - json_test = '{"environment": {"fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}' - regex = '(\"mac_address_list\"\:\s?\[)([\w\d])' - replaced = re.sub(regex, r'\1"\2', json_test) + body = '{"environment": {"fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}' + replaced = re.sub(PatchMalformedJsonMiddleware.REGEX, r'\1"\2', body) assert replaced == '{"environment": {"fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]}}' From 88c78efcd987d3d5c275358ce55148e3ee3dce0c Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 19 Nov 2024 09:53:31 +0100 Subject: [PATCH 08/10] fixes --- test/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/main.py b/test/main.py index 2e70558..2cfacf4 100644 --- a/test/main.py +++ b/test/main.py @@ -11,14 +11,13 @@ from jose import jwt, jwk from jose.constants import ALGORITHMS from starlette.testclient import TestClient -from middleware import PatchMalformedJsonMiddleware - # add relative path to use packages as they were in the app/ dir sys.path.append('../') sys.path.append('../app') from app import main from app.util import load_key +from middleware import PatchMalformedJsonMiddleware main.app.add_middleware(PatchMalformedJsonMiddleware, enabled=True) client = TestClient(main.app) From 55446f7d9c37113960293db10fa824001f0c002e Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 19 Nov 2024 14:14:06 +0100 Subject: [PATCH 09/10] fixes --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 898d06b..a8aa3bd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ test: - test/**/* variables: DATABASE: sqlite:///../app/db.sqlite - SUPPORT_MALFORMED_JSON: true parallel: matrix: - IMAGE: [ 'python:3.12-slim-bookworm' ] From 317699ff582736c33275b6d9c975d49a149f6d5d Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Wed, 20 Nov 2024 09:10:43 +0100 Subject: [PATCH 10/10] code styling --- app/middleware.py | 12 +++++++----- test/main.py | 8 +++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/middleware.py b/app/middleware.py index 7e3d20d..998474b 100644 --- a/app/middleware.py +++ b/app/middleware.py @@ -27,15 +27,17 @@ class PatchMalformedJsonMiddleware(BaseHTTPMiddleware): json.loads(body) except json.decoder.JSONDecodeError: logger.warning(f'Malformed json received! Try to fix it, "PatchMalformedJsonMiddleware" is enabled.') - body = body.replace('\t', '') - body = body.replace('\n', '') - - s = re.sub(PatchMalformedJsonMiddleware.REGEX, r'\1"\2', body) + s = PatchMalformedJsonMiddleware.fix_json(body) logger.debug(f'Fixed JSON: "{s}"') s = json.loads(s) # ensure json is now valid - # set new body request._body = json.dumps(s).encode('utf-8') response = await call_next(request) return response + + @staticmethod + def fix_json(s: str) -> str: + s = s.replace('\t', '') + s = s.replace('\n', '') + return re.sub(PatchMalformedJsonMiddleware.REGEX, r'\1"\2', s) diff --git a/test/main.py b/test/main.py index 2cfacf4..8f2e2c7 100644 --- a/test/main.py +++ b/test/main.py @@ -17,9 +17,8 @@ sys.path.append('../app') from app import main from app.util import load_key -from middleware import PatchMalformedJsonMiddleware -main.app.add_middleware(PatchMalformedJsonMiddleware, enabled=True) +# main.app.add_middleware(PatchMalformedJsonMiddleware, enabled=True) client = TestClient(main.app) ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld' @@ -109,12 +108,11 @@ def test_auth_v1_origin(): def test_auth_v1_origin_malformed_json(): # see oscar.krause/fastapi-dls#1 - import re from middleware import PatchMalformedJsonMiddleware # test regex (temporary, until this section is merged into main.py - body = '{"environment": {"fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}' - replaced = re.sub(PatchMalformedJsonMiddleware.REGEX, r'\1"\2', body) + s = '{"environment": {"fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}' + replaced = PatchMalformedJsonMiddleware.fix_json(s) assert replaced == '{"environment": {"fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]}}'