diff --git a/.DEBIAN/requirements-ubuntu-23.04.txt b/.DEBIAN/requirements-ubuntu-23.04.txt deleted file mode 100644 index f2d4b05..0000000 --- a/.DEBIAN/requirements-ubuntu-23.04.txt +++ /dev/null @@ -1,10 +0,0 @@ -# https://packages.ubuntu.com -fastapi==0.91.0 -uvicorn[standard]==0.15.0 -python-jose[pycryptodome]==3.3.0 -pycryptodome==3.11.0 -python-dateutil==2.8.2 -sqlalchemy==1.4.46 -markdown==3.4.3 -python-dotenv==0.21.0 -jinja2==3.1.2 diff --git a/.DEBIAN/requirements-ubuntu-23.10.txt b/.DEBIAN/requirements-ubuntu-23.10.txt deleted file mode 100644 index 4cab03f..0000000 --- a/.DEBIAN/requirements-ubuntu-23.10.txt +++ /dev/null @@ -1,10 +0,0 @@ -# https://packages.ubuntu.com -fastapi==0.101.0 -uvicorn[standard]==0.23.2 -python-jose[pycryptodome]==3.3.0 -pycryptodome==3.11.0 -python-dateutil==2.8.2 -sqlalchemy==1.4.47 -markdown==3.4.4 -python-dotenv==1.0.0 -jinja2==3.1.2 diff --git a/.PKGBUILD/PKGBUILD b/.PKGBUILD/PKGBUILD index b2eaf5a..7255bf0 100644 --- a/.PKGBUILD/PKGBUILD +++ b/.PKGBUILD/PKGBUILD @@ -52,6 +52,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" # copy static asset files install -Dm755 "$srcdir/$pkgname/app/static/assets/css/bootstrap.min.css" "$pkgdir/opt/$pkgname/static/assets/css/bootstrap.min.css" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2e3b9b..a8aa3bd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -144,11 +144,9 @@ test: matrix: - IMAGE: [ 'python:3.12-slim-bookworm' ] REQUIREMENTS: [ 'requirements.txt' ] - - IMAGE: [ 'debian:bookworm' ] + - IMAGE: [ 'debian:bookworm' ] # EOL: June 06, 2026 REQUIREMENTS: [ '.DEBIAN/requirements-bookworm-12.txt' ] - - IMAGE: [ 'ubuntu:23.10' ] - REQUIREMENTS: [ '.DEBIAN/requirements-ubuntu-23.10.txt' ] - - IMAGE: [ 'ubuntu:24.04' ] + - IMAGE: [ 'ubuntu:24.04' ] # EOL: April 2036 REQUIREMENTS: [ '.DEBIAN/requirements-ubuntu-24.04.txt' ] - IMAGE: [ 'ubuntu:24.10' ] REQUIREMENTS: [ '.DEBIAN/requirements-ubuntu-24.10.txt' ] diff --git a/README.md b/README.md index 9cde329..ed8c646 100644 --- a/README.md +++ b/README.md @@ -330,11 +330,12 @@ Packages are available here: Successful tested with: -- Debian 12 (Bookworm) (EOL: tba.) -- Ubuntu 22.10 (Kinetic Kudu) (EOL: July 20, 2023) -- Ubuntu 23.04 (Lunar Lobster) (EOL: January 2024) -- Ubuntu 23.10 (Mantic Minotaur) (EOL: July 2024) -- Ubuntu 24.04 (Noble Numbat) (EOL: April 2036) +- **Debian 12 (Bookworm)** (EOL: June 06, 2026) +- *Ubuntu 22.10 (Kinetic Kudu)* (EOL: July 20, 2023) +- *Ubuntu 23.04 (Lunar Lobster)* (EOL: January 2024) +- *Ubuntu 23.10 (Mantic Minotaur)* (EOL: July 2024) +- **Ubuntu 24.04 (Noble Numbat)** (EOL: April 2036) +- *Ubuntu 24.10 (Oracular Oriole)* (EOL: tba.) Not working with: @@ -410,21 +411,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 b9cafc3..0c414d4 100644 --- a/app/main.py +++ b/app/main.py @@ -100,6 +100,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 bc91f96..b1f7f25 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,