diff --git a/pyproject.toml b/pyproject.toml index 543e73c..9160b98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "pyyaml>=6.0.0", "cherrypy>=18.0.0", "paho-mqtt>=1.6.0", + "cherrypy-cors==1.7.0", ] diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index aca4111..1d27345 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -12,36 +12,6 @@ from .cad_calibration_engine import CADCalibrationEngine logger = logging.getLogger("HTTPServer") -def is_cors_enabled(config: dict) -> bool: - """Check if CORS is enabled in the configuration""" - return config.get("web", {}).get("cors_enabled", False) - - -def add_cors_headers(): - """Add CORS headers to allow cross-origin requests""" - cherrypy.response.headers['Access-Control-Allow-Origin'] = '*' - cherrypy.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' - cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' - - -def cors_tool(): - """CherryPy tool to add CORS headers""" - if cherrypy.request.method == 'OPTIONS': - # Handle preflight requests - cherrypy.response.headers['Access-Control-Allow-Origin'] = '*' - cherrypy.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' - cherrypy.response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' - cherrypy.response.headers['Access-Control-Max-Age'] = '86400' - return '' - else: - # Add CORS headers to actual requests - add_cors_headers() - - -# Register the CORS tool -cherrypy.tools.cors = cherrypy.Tool('before_handler', cors_tool) - - # system systems # GET /api/stats # GET /api/logs @@ -94,23 +64,14 @@ class APIEndpoints: self.daemon_instance = daemon_instance self._config_path = config_path or '/etc/pymc_repeater/config.yaml' self.cad_calibration = CADCalibrationEngine(daemon_instance, event_loop) - - - self._cors_enabled = is_cors_enabled(self.config) - - logger.info(f"CORS {'enabled' if self._cors_enabled else 'disabled'} (config: web.cors_enabled={self._cors_enabled})") - - # Configure CORS tool for this class if enabled - if self._cors_enabled: - self._cp_config = {'tools.cors.on': True} - else: - self._cp_config = {'tools.cors.on': False} @cherrypy.expose def default(self, *args, **kwargs): - """Handle OPTIONS requests for CORS preflight""" + """Handle default requests""" if cherrypy.request.method == "OPTIONS": + # OPTIONS handled by server-level CORS middleware return "" + # For non-OPTIONS requests, return 404 raise cherrypy.HTTPError(404) @@ -278,7 +239,6 @@ class APIEndpoints: @cherrypy.expose @cherrypy.tools.json_out() def packet_stats(self, hours=24): - try: hours = int(hours) stats = self._get_storage().get_packet_stats(hours=hours) @@ -290,7 +250,6 @@ class APIEndpoints: @cherrypy.expose @cherrypy.tools.json_out() def packet_type_stats(self, hours=24): - try: hours = int(hours) stats = self._get_storage().get_packet_type_stats(hours=hours) @@ -302,7 +261,6 @@ class APIEndpoints: @cherrypy.expose @cherrypy.tools.json_out() def route_stats(self, hours=24): - try: hours = int(hours) stats = self._get_storage().get_route_stats(hours=hours) @@ -314,7 +272,6 @@ class APIEndpoints: @cherrypy.expose @cherrypy.tools.json_out() def recent_packets(self, limit=100): - try: limit = int(limit) packets = self._get_storage().get_recent_packets(limit=limit) @@ -640,10 +597,6 @@ class APIEndpoints: cherrypy.response.headers['Cache-Control'] = 'no-cache' cherrypy.response.headers['Connection'] = 'keep-alive' - # Add CORS headers conditionally for SSE endpoint - if self._cors_enabled: - cherrypy.response.headers['Access-Control-Allow-Origin'] = '*' - if not hasattr(self.cad_calibration, 'message_queue'): self.cad_calibration.message_queue = [] diff --git a/repeater/web/http_server.py b/repeater/web/http_server.py index 9a6b4ba..f91e1dd 100644 --- a/repeater/web/http_server.py +++ b/repeater/web/http_server.py @@ -7,6 +7,7 @@ from datetime import datetime from typing import Callable, Optional import cherrypy +import cherrypy_cors from pymc_core.protocol.utils import PAYLOAD_TYPES, ROUTE_TYPES from repeater import __version__ @@ -83,6 +84,10 @@ class StatsApp: @cherrypy.expose def default(self, *args, **kwargs): """Handle client-side routing - serve index.html for all non-API routes.""" + # Handle OPTIONS requests for any path + if cherrypy.request.method == "OPTIONS": + return "" + # Let API routes pass through if args and args[0] == 'api': raise cherrypy.NotFound() @@ -109,13 +114,27 @@ class HTTPStatsServer: self.host = host self.port = port + self.config = config or {} self.app = StatsApp( stats_getter, node_name, pub_key, send_advert_func, config, event_loop, daemon_instance, config_path ) + + # Set up CORS at the server level if enabled + self._cors_enabled = self.config.get("web", {}).get("cors_enabled", False) + logger.info(f"CORS enabled: {self._cors_enabled}") + + def _setup_server_cors(self): + """Set up CORS using cherrypy_cors.install()""" + cherrypy_cors.install() + logger.info("CORS support enabled") def start(self): try: + + if self._cors_enabled: + self._setup_server_cors() + # Serve static files from the html directory (compiled Vue.js app) html_dir = os.path.join(os.path.dirname(__file__), "html") assets_dir = os.path.join(html_dir, "assets") @@ -123,6 +142,7 @@ class HTTPStatsServer: config = { "/": { "tools.sessions.on": False, + "cors.expose.on": self._cors_enabled, # Ensure proper content types for Vue.js files "tools.staticfile.content_types": { 'js': 'application/javascript', @@ -133,6 +153,7 @@ class HTTPStatsServer: "/assets": { "tools.staticdir.on": True, "tools.staticdir.dir": assets_dir, + "cors.expose.on": self._cors_enabled, # Set proper content types for assets "tools.staticdir.content_types": { 'js': 'application/javascript', @@ -143,6 +164,7 @@ class HTTPStatsServer: "/favicon.ico": { "tools.staticfile.on": True, "tools.staticfile.filename": os.path.join(html_dir, "favicon.ico"), + "cors.expose.on": self._cors_enabled, }, }