diff --git a/.gitignore b/.gitignore index a97b897..6332952 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .env -instance/ +.instance/ *.db documents/ @@ -12,6 +12,10 @@ __pycache__/ .pytest_cache/ .coverage + htmlcov/ .DS_Store -*.log \ No newline at end of file +*.log + +.certs/ +*.pem \ No newline at end of file diff --git a/instance/team_tryouts.db b/instance/team_tryouts.db index b8d9f0f..b1569f0 100644 Binary files a/instance/team_tryouts.db and b/instance/team_tryouts.db differ diff --git a/run_https.py b/run_https.py new file mode 100644 index 0000000..65af865 --- /dev/null +++ b/run_https.py @@ -0,0 +1,129 @@ +"""Development HTTPS server for testing production settings locally. + +Generates a self-signed certificate (if not present) and runs the +application via Waitress wrapped in a TLS socket. This simulates the +production environment where Nginx handles TLS termination. + +Usage: + python run_https.py + +The server will listen on https://localhost:8443 +Accept the self-signed certificate warning in your browser to proceed. +""" + +import os +import socket +import subprocess +import ssl +import sys +from waitress.server import create_server +from app import create_app + +CERT_FILE = 'certs/localhost.pem' +KEY_FILE = 'certs/localhost-key.pem' + + +def generate_self_signed_cert(): + """Generate a self-signed certificate for local HTTPS testing. + + Uses OpenSSL to create a key and certificate valid for 365 days. + Skips generation if certificate files already exist. + """ + if os.path.exists(CERT_FILE) and os.path.exists(KEY_FILE): + print('[OK] Self-signed certificate already exists.') + return + + os.makedirs('certs', exist_ok=True) + + print('[INFO] Generating self-signed certificate for localhost...') + try: + subprocess.run([ + 'openssl', 'req', '-x509', '-newkey', 'rsa:2048', + '-keyout', KEY_FILE, + '-out', CERT_FILE, + '-days', '365', + '-nodes', + '-subj', '/CN=localhost' + ], check=True, capture_output=True) + print('[OK] Certificate generated: certs/localhost.pem') + except FileNotFoundError: + print('[ERROR] OpenSSL not found. Install OpenSSL or use:') + print(' winget install OpenSSL.OpenSSL') + print(' OR download from https://slproweb.com/products/Win32OpenSSL.html') + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f'[ERROR] Certificate generation failed: {e}') + sys.exit(1) + + +def main(): + """Start the HTTPS development server.""" + port = int(os.getenv('HTTPS_PORT', 8443)) + host = os.getenv('HOST', '127.0.0.1') + + # Ensure certificate exists + generate_self_signed_cert() + + # Create the Flask application + app = create_app() + + # Create SSL context + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(CERT_FILE, KEY_FILE) + + # Create a TCP socket, wrap it with TLS, then pass to Waitress + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((host, port)) + sock.listen(5) + + # Wrap the socket with TLS + ssl_sock = context.wrap_socket(sock, server_side=True) + + # ===================================================================== + # WSGI Middleware: Tell Flask the connection is HTTPS + # + # Waitress doesn't know the underlying socket is TLS, so Flask sees + # wsgi.url_scheme = "http". Without this middleware, force_https() + # would cause an infinite redirect loop (ERR_TOO_MANY_REDIRECTS). + # ===================================================================== + class ForceHTTPSMiddleware: + """WSGI middleware that sets url_scheme to 'https'. + + Since we're wrapping the TCP socket with SSL before passing it + to Waitress, Flask's request.is_secure returns False because + Waitress reports wsgi.url_scheme='http'. This middleware fixes + that so Flask correctly identifies the connection as HTTPS. + """ + + def __init__(self, wsgi_app): + self.wsgi_app = wsgi_app + + def __call__(self, environ, start_response): + environ['wsgi.url_scheme'] = 'https' + environ['HTTPS'] = 'on' + return self.wsgi_app(environ, start_response) + + # Wrap the Flask app with the HTTPS middleware + app.wsgi_app = ForceHTTPSMiddleware(app.wsgi_app) + + print(f'\n╔══════════════════════════════════════════════════════╗') + print(f'║ TEAM TRYOUTS - Development HTTPS Server ║') + print(f'╠══════════════════════════════════════════════════════╣') + print(f'║ URL: https://{host}:{port} ║') + print(f'║ Cert: self-signed (accept browser warning) ║') + print(f'║ Press Ctrl+C to stop ║') + print(f'╚══════════════════════════════════════════════════════╝\n') + + # Create Waitress server with the SSL-wrapped socket + server = create_server( + app, + sockets=[ssl_sock], + threads=4, + channel_timeout=30, + ) + server.run() + + +if __name__ == '__main__': + main()