"""Flask application factory.""" import logging import os import sys import uuid from flask import Flask, Response, g, request from app.config import VERSION, config def setup_logging(app: Flask) -> None: """Configure structured logging.""" log_level = logging.DEBUG if app.debug else logging.INFO if app.debug: log_format = "%(asctime)s %(levelname)s [%(name)s] %(message)s" else: log_format = ( '{"time":"%(asctime)s","level":"%(levelname)s",' '"logger":"%(name)s","message":"%(message)s"}' ) logging.basicConfig( level=log_level, format=log_format, stream=sys.stdout, ) # Reduce noise from werkzeug in production if not app.debug: logging.getLogger("werkzeug").setLevel(logging.WARNING) app.logger.info("FlaskPaste starting", extra={"config": type(app.config).__name__}) def setup_security_headers(app: Flask) -> None: """Add security headers to all responses.""" @app.after_request def add_security_headers(response: Response) -> Response: # Prevent MIME type sniffing response.headers["X-Content-Type-Options"] = "nosniff" # Prevent clickjacking response.headers["X-Frame-Options"] = "DENY" # XSS protection (legacy but still useful) response.headers["X-XSS-Protection"] = "1; mode=block" # Referrer policy response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" # Content Security Policy (restrictive for API) response.headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'" # Permissions policy response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()" # HSTS - enforce HTTPS (1 year, include subdomains) response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" # Prevent caching of sensitive paste data response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private" response.headers["Pragma"] = "no-cache" return response def setup_request_id(app: Flask) -> None: """Add request ID tracking for log correlation and tracing.""" @app.before_request def assign_request_id(): # Use incoming X-Request-ID from proxy, or generate a new one request_id = request.headers.get("X-Request-ID", "").strip() if not request_id: request_id = str(uuid.uuid4()) g.request_id = request_id @app.after_request def add_request_id_header(response: Response) -> Response: # Echo request ID back to client for tracing request_id = getattr(g, "request_id", None) if request_id: response.headers["X-Request-ID"] = request_id # Access logging with request ID app.logger.info( "%s %s %s [rid=%s]", request.method, request.path, response.status_code, request_id or "-", ) return response def setup_error_handlers(app: Flask) -> None: """Register global error handlers.""" import json @app.errorhandler(400) def bad_request(error): app.logger.warning("Bad request: %s [rid=%s]", request.path, getattr(g, "request_id", "-")) return Response( json.dumps({"error": "Bad request"}), status=400, mimetype="application/json", ) @app.errorhandler(404) def not_found(error): return Response( json.dumps({"error": "Not found"}), status=404, mimetype="application/json", ) @app.errorhandler(429) def rate_limit_exceeded(error): app.logger.warning( "Rate limit exceeded: %s from %s [rid=%s]", request.path, request.remote_addr, getattr(g, "request_id", "-"), ) return Response( json.dumps({"error": "Rate limit exceeded", "retry_after": error.description}), status=429, mimetype="application/json", ) @app.errorhandler(500) def internal_error(error): app.logger.error( "Internal error: %s - %s [rid=%s]", request.path, str(error), getattr(g, "request_id", "-"), ) return Response( json.dumps({"error": "Internal server error"}), status=500, mimetype="application/json", ) @app.errorhandler(Exception) def handle_exception(error): app.logger.exception( "Unhandled exception: %s [rid=%s]", str(error), getattr(g, "request_id", "-") ) return Response( json.dumps({"error": "Internal server error"}), status=500, mimetype="application/json", ) def setup_rate_limiting(app: Flask) -> None: """Configure rate limiting.""" from flask_limiter import Limiter from flask_limiter.util import get_remote_address limiter = Limiter( key_func=get_remote_address, app=app, default_limits=["200 per day", "60 per hour"], storage_uri="memory://", strategy="fixed-window", ) # Store limiter on app for use in routes app.extensions["limiter"] = limiter return limiter def setup_metrics(app: Flask) -> None: """Configure Prometheus metrics.""" # Only enable metrics in production if app.config.get("TESTING"): return try: from prometheus_flask_exporter import PrometheusMetrics metrics = PrometheusMetrics(app) # Add app info metrics.info("flaskpaste_info", "FlaskPaste application info", version=VERSION) app.extensions["metrics"] = metrics except ImportError: app.logger.warning("prometheus_flask_exporter not available, metrics disabled") def create_app(config_name: str | None = None) -> Flask: """Create and configure the Flask application.""" if config_name is None: config_name = os.environ.get("FLASK_ENV", "default") app = Flask(__name__) app.config.from_object(config[config_name]) # Setup logging first setup_logging(app) # Setup request ID tracking setup_request_id(app) # Setup security headers setup_security_headers(app) # Setup error handlers setup_error_handlers(app) # Setup rate limiting (skip in testing) if not app.config.get("TESTING"): setup_rate_limiting(app) # Setup metrics (skip in testing) setup_metrics(app) # Initialize database from app import database database.init_app(app) # Register blueprints from app.api import bp as api_bp app.register_blueprint(api_bp) app.logger.info("FlaskPaste initialized successfully") return app