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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-09-14 15:02:56 +10:00
parent c5f848733a
commit 77a522a16f
4 changed files with 28 additions and 1 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
.venv/
.git/
__pycache__/
*.pyc
.DS_Store

8
Dockerfile Normal file
View File

@ -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"]

9
app.py
View File

@ -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"

7
compose.yml Normal file
View File

@ -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