feat: implement server-level CORS support and cleanup CORS handling in API endpoints

This commit is contained in:
Lloyd
2025-11-24 00:11:32 +00:00
parent 7f00575aa9
commit 34a775925b
3 changed files with 26 additions and 50 deletions
+1
View File
@@ -35,6 +35,7 @@ dependencies = [
"pyyaml>=6.0.0",
"cherrypy>=18.0.0",
"paho-mqtt>=1.6.0",
"cherrypy-cors==1.7.0",
]
+3 -50
View File
@@ -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 = []
+22
View File
@@ -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,
},
}