From 15f14cac1176174f64a603c1a3fdc43fd37eb0f3 Mon Sep 17 00:00:00 2001 From: Oscar Krause Date: Tue, 19 Nov 2024 09:18:12 +0100 Subject: [PATCH] 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():