"""WSGI entry point for the Team Tryouts application. This module provides the production WSGI server using Waitress (Windows). Use this file to start the application in production instead of Flask's built-in development server. Usage: python wsgi.py Configuration via environment variables: HOST: interface to bind (default: 0.0.0.0 — see the note below) PORT: Port to listen on (default: 10000) WAITRESS_THREADS: Number of worker threads (default: CPU*2+1) TRUSTED_PROXY: whose X-Forwarded-For to believe (default: * — see below) A note on the three defaults above (OPS-002) -------------------------------------------- They are what this file has always done, kept deliberately. Two of them are known to be wrong, and they are *still* not changed here, because the right value depends on something the repository cannot tell us: is nginx on the same machine as the app, or is the app in a Pterodactyl container with nginx elsewhere? Guessing fails in one of two directions, and neither is recoverable by reading a log: - bind to loopback when nginx is on another host, and the site is simply gone; - stop trusting X-Forwarded-For when it was the only source of client addresses, and every request looks like it comes from the proxy — one shared rate-limit bucket, so the first person to mistype a password five times locks the limiter for everybody. So: the values became settings, the safe values are what `.env.example` carries for a new deployment, and the existing one keeps working untouched until someone who knows the topology sets them. docs/deployment.md walks through both cases. """ import multiprocessing import os from app.app import create_app app = create_app() def proxy_settings(): """How much of X-Forwarded-For to believe, and from whom (OPS-002). This decides which address the rate limiter counts against and which one the audit log records as `ip=`. The default is `*` — believe anyone. That is the setting the audit called out for inverting the risk rather than fixing it: before it, rate limiting was miscalibrated but closed; after it, forgeable on every request by anyone who can reach the app directly. It stays the default only because narrowing it blind can take the rate limiter down for every legitimate user, and it is now one environment variable away instead of being welded into the file. Values: '' trust nobody; remote_addr is the peer. Correct when nothing proxies the app. '127.0.0.1' trust a reverse proxy on this same machine. The usual case, and what .env.example ships. '*' trust everyone. Only correct when the app cannot be reached except through the proxy, at the network level. Returns: dict: keyword arguments for waitress.serve. """ trusted = os.getenv('TRUSTED_PROXY', '*').strip() if not trusted: return {'clear_untrusted_proxy_headers': True} return { 'trusted_proxy': trusted, 'trusted_proxy_count': int(os.getenv('TRUSTED_PROXY_COUNT', 1)), 'trusted_proxy_headers': {'x-forwarded-for', 'x-forwarded-proto'}, 'clear_untrusted_proxy_headers': True, } if __name__ == '__main__': from waitress import serve port = int(os.getenv('PORT', 10000)) threads = int(os.getenv('WAITRESS_THREADS', multiprocessing.cpu_count() * 2 + 1)) # The comment that used to sit here said "Bind to localhost by default # (Nginx reverse proxy)" next to a default of 0.0.0.0 — it described the # intention and the code did the opposite, which is worse than either: # the app was reachable directly and nobody reading the file would have # known. The default is unchanged; the comment now says what it does. host = os.getenv('HOST', '0.0.0.0') # noqa: S104 — see the module docstring print(f'Starting Waitress server on {host}:{port} with {threads} threads') if host == '0.0.0.0' and os.getenv('TRUSTED_PROXY', '*') == '*': # noqa: S104 print( 'WARNING: listening on every interface and trusting X-Forwarded-For from ' 'anyone. If this port is reachable without going through nginx, the rate ' 'limiter and the audit log can be fed any address a caller likes. ' 'Set HOST and TRUSTED_PROXY — see docs/deployment.md (OPS-002).' ) serve( app, host=host, port=port, threads=threads, # Graceful shutdown settings channel_timeout=30, # Seconds to wait for in-flight requests cleanup_interval=30, **proxy_settings(), )