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/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..998474b --- /dev/null +++ b/app/middleware.py @@ -0,0 +1,43 @@ +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 + + REGEX = '(\"mac_address_list\"\:\s?\[)([\w\d])' + + 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.') + 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 67856c9..8f2e2c7 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,6 @@ from dateutil.relativedelta import relativedelta from jose import jwt, jwk from jose.constants import ALGORITHMS from starlette.testclient import TestClient -import sys # add relative path to use packages as they were in the app/ dir sys.path.append('../') @@ -18,6 +18,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,6 +107,15 @@ def test_auth_v1_origin(): assert response.json().get('origin_ref') == ORIGIN_REF +def test_auth_v1_origin_malformed_json(): # see oscar.krause/fastapi-dls#1 + from middleware import PatchMalformedJsonMiddleware + + # test regex (temporary, until this section is merged into main.py + 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"]}}' + + def auth_v1_origin_update(): payload = { "registration_pending": False,