diff --git a/src/ddns_service/app.py b/src/ddns_service/app.py index b4ef356..d0fd583 100644 --- a/src/ddns_service/app.py +++ b/src/ddns_service/app.py @@ -1,12 +1,13 @@ """Application class - central dependency holder.""" +import argon2 import logging import threading -import argon2 - +from .config import load_config from .dns import DNSService from .email import EmailService +from .logging import setup_logging from .models import create_tables, init_database from .ratelimit import BadLimiter, GoodLimiter @@ -18,14 +19,16 @@ class Application: Holds configuration and all service instances. """ - def __init__(self, config): + def __init__(self, config, config_path=None): """ Initialize application with configuration. Args: config: Configuration dictionary from TOML file. + config_path: Path to configuration file (for reload). """ self.config = config + self.config_path = config_path self.password_hasher = argon2.PasswordHasher() self.shutdown_event = threading.Event() @@ -57,6 +60,39 @@ class Application: self.bad_limiter = BadLimiter(self.config) logging.info("Rate limiters initialized") + def reload_config(self): + """ + Reload configuration from file. + + Does not reload: database settings, host, port. + """ + new_config = load_config(self.config_path) + + # Preserve DB and bind settings + new_config["database"] = self.config["database"] + new_config["daemon"]["host"] = self.config["daemon"]["host"] + new_config["daemon"]["port"] = self.config["daemon"]["port"] + + self.config = new_config + + # Reconfigure logging + setup_logging( + level=self.config["daemon"]["log_level"], + target=self.config["daemon"]["log_target"], + syslog_socket=self.config["daemon"]["syslog_socket"], + syslog_facility=self.config["daemon"]["syslog_facility"], + log_file=self.config["daemon"]["log_file"], + log_file_size=self.config["daemon"]["log_file_size"], + log_versions=self.config["daemon"]["log_versions"], + ) + + # Re-init services + self.init_dns() + self.init_email() + self.init_rate_limiters() + + logging.info("Configuration reloaded") + def signal_shutdown(self): """Signal the application to shut down.""" logging.info("Shutdown signaled") diff --git a/src/ddns_service/main.py b/src/ddns_service/main.py index 3c2b6ec..69a4ad3 100644 --- a/src/ddns_service/main.py +++ b/src/ddns_service/main.py @@ -153,7 +153,7 @@ def main(): ) # Create application instance - app = Application(config) + app = Application(config, config_path) # Initialize database try: diff --git a/src/ddns_service/server.py b/src/ddns_service/server.py index 50b012b..5161c52 100644 --- a/src/ddns_service/server.py +++ b/src/ddns_service/server.py @@ -461,14 +461,39 @@ def run_daemon(app): expired_cleanup_thread = ExpiredRecordsCleanupThread(app) expired_cleanup_thread.start() - # Setup signal handlers def signal_handler(signum, frame): logging.info(f"Signal received: {signum}, shutting down") app.signal_shutdown() + def sighup_handler(signum, frame): + logging.info("SIGHUP received, reloading configuration") + try: + app.reload_config() + + # Update server attributes + server.proxy_header = app.config["daemon"].get("proxy_header", "") + server.trusted_networks = _parse_trusted_proxies( + app.config["daemon"].get("trusted_proxies", []) + ) + + # Reload SSL if enabled + if app.config["daemon"]["ssl"]: + new_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + new_context.load_cert_chain( + app.config["daemon"]["ssl_cert_file"], + app.config["daemon"]["ssl_key_file"] + ) + # Note: existing connections use old cert, new connections use new + server.socket = new_context.wrap_socket( + server.socket.detach(), server_side=True + ) + except Exception as e: + logging.error(f"Config reload failed: {e}") + signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGHUP, sighup_handler) paths = ", ".join(ep["path"] for ep in config["endpoints"]) logging.info(f"Daemon started: {proto}://{host}:{port} endpoints=[{paths}]")