From 77a522a16f3a7ccf714e511e30e5063a4582a283 Mon Sep 17 00:00:00 2001 From: type-two Date: Mon, 14 Sep 2026 15:02:56 +1000 Subject: [PATCH] Production hardening + container for public hosting - Request body cap (16KB), search query cap, input range validation (year 1800-2399, date/time fields, lat/lon bounds) - Dockerfile (gunicorn, 2 workers) + compose binding 127.0.0.1:7799 for cloudflared-fronted deployment Co-Authored-By: Claude Fable 5 --- .dockerignore | 5 +++++ Dockerfile | 8 ++++++++ app.py | 9 ++++++++- compose.yml | 7 +++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3ca0841 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.venv/ +.git/ +__pycache__/ +*.pyc +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4403d6e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.12-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt gunicorn==23.0.0 +COPY . . +EXPOSE 7799 +# 2 workers is plenty: chart computation is a few ms; timezonefinder/geonames load per worker +CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:7799", "--access-logfile", "-", "app:app"] diff --git a/app.py b/app.py index 3792a37..f53491c 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,7 @@ import geo import interp app = Flask(__name__, static_folder="static", static_url_path="") +app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 # chart inputs are tiny; cap request bodies @app.get("/") @@ -15,7 +16,7 @@ def index(): @app.get("/api/search") def api_search(): - q = request.args.get("q", "") + q = request.args.get("q", "")[:64] return jsonify(geo.search_cities(q)) @@ -41,6 +42,12 @@ def cast(d): house_system = "P" except (KeyError, TypeError) as e: raise ValueError(f"bad input: {e}") + if not (1800 <= year <= 2399): + raise ValueError("year must be 1800-2399 (ephemeris range)") + if not (1 <= month <= 12 and 1 <= day <= 31 and 0 <= hour <= 23 and 0 <= minute <= 59): + raise ValueError("date/time out of range") + if not (-90 <= lat <= 90 and -180 <= lon <= 180): + raise ValueError("coordinates out of range") if d.get("utc_offset") not in (None, ""): offset, tzname = float(d["utc_offset"]), "manual" diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f2de67e --- /dev/null +++ b/compose.yml @@ -0,0 +1,7 @@ +services: + nategodd: + build: . + container_name: nategodd + restart: unless-stopped + ports: + - "127.0.0.1:7799:7799" # cloudflared reaches it via localhost; never exposed directly