From adbeab8b68c78db55ed4eef5894144a6c76cbfe0 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 28 Mar 2026 23:21:20 +0800 Subject: [PATCH] feat(dashboard): add SSL configuration resolution for dashboard fixes: #7058 --- astrbot/dashboard/server.py | 99 ++++++++++++++++++++++++------------- tests/test_dashboard.py | 45 +++++++++++++++++ 2 files changed, 109 insertions(+), 35 deletions(-) diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index a4742aa672..cbb7296bd0 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -300,6 +300,61 @@ def _init_jwt_secret(self) -> None: logger.info("Initialized random JWT secret for dashboard.") self._jwt_secret = self.config["dashboard"]["jwt_secret"] + @staticmethod + def _resolve_dashboard_ssl_config( + ssl_config: dict, + ) -> tuple[bool, dict[str, str]]: + cert_file = ( + os.environ.get("DASHBOARD_SSL_CERT") + or os.environ.get("ASTRBOT_DASHBOARD_SSL_CERT") + or ssl_config.get("cert_file", "") + ) + key_file = ( + os.environ.get("DASHBOARD_SSL_KEY") + or os.environ.get("ASTRBOT_DASHBOARD_SSL_KEY") + or ssl_config.get("key_file", "") + ) + ca_certs = ( + os.environ.get("DASHBOARD_SSL_CA_CERTS") + or os.environ.get("ASTRBOT_DASHBOARD_SSL_CA_CERTS") + or ssl_config.get("ca_certs", "") + ) + + if not cert_file or not key_file: + logger.warning( + "dashboard.ssl.enable 已启用,但未同时配置 cert_file 和 key_file,SSL 配置将不会生效。", + ) + return False, {} + + cert_path = Path(cert_file).expanduser() + key_path = Path(key_file).expanduser() + if not cert_path.is_file(): + logger.warning( + f"dashboard.ssl.enable 已启用,但 SSL 证书文件不存在: {cert_path},SSL 配置将不会生效。", + ) + return False, {} + if not key_path.is_file(): + logger.warning( + f"dashboard.ssl.enable 已启用,但 SSL 私钥文件不存在: {key_path},SSL 配置将不会生效。", + ) + return False, {} + + resolved_ssl_config = { + "certfile": str(cert_path.resolve()), + "keyfile": str(key_path.resolve()), + } + + if ca_certs: + ca_path = Path(ca_certs).expanduser() + if not ca_path.is_file(): + logger.warning( + f"dashboard.ssl.enable 已启用,但 SSL CA 证书文件不存在: {ca_path},SSL 配置将不会生效。", + ) + return False, {} + resolved_ssl_config["ca_certs"] = str(ca_path.resolve()) + + return True, resolved_ssl_config + def run(self): ip_addr = [] dashboard_config = self.core_lifecycle.astrbot_config.get("dashboard", {}) @@ -322,6 +377,11 @@ def run(self): or os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE"), bool(ssl_config.get("enable", False)), ) + resolved_ssl_config: dict[str, str] = {} + if ssl_enable: + ssl_enable, resolved_ssl_config = self._resolve_dashboard_ssl_config( + ssl_config, + ) scheme = "https" if ssl_enable else "http" if not enable: @@ -373,41 +433,10 @@ def run(self): config = HyperConfig() config.bind = [f"{host}:{port}"] if ssl_enable: - cert_file = ( - os.environ.get("DASHBOARD_SSL_CERT") - or os.environ.get("ASTRBOT_DASHBOARD_SSL_CERT") - or ssl_config.get("cert_file", "") - ) - key_file = ( - os.environ.get("DASHBOARD_SSL_KEY") - or os.environ.get("ASTRBOT_DASHBOARD_SSL_KEY") - or ssl_config.get("key_file", "") - ) - ca_certs = ( - os.environ.get("DASHBOARD_SSL_CA_CERTS") - or os.environ.get("ASTRBOT_DASHBOARD_SSL_CA_CERTS") - or ssl_config.get("ca_certs", "") - ) - - cert_path = Path(cert_file).expanduser() - key_path = Path(key_file).expanduser() - if not cert_file or not key_file: - raise ValueError( - "dashboard.ssl.enable 为 true 时,必须配置 cert_file 和 key_file。", - ) - if not cert_path.is_file(): - raise ValueError(f"SSL 证书文件不存在: {cert_path}") - if not key_path.is_file(): - raise ValueError(f"SSL 私钥文件不存在: {key_path}") - - config.certfile = str(cert_path.resolve()) - config.keyfile = str(key_path.resolve()) - - if ca_certs: - ca_path = Path(ca_certs).expanduser() - if not ca_path.is_file(): - raise ValueError(f"SSL CA 证书文件不存在: {ca_path}") - config.ca_certs = str(ca_path.resolve()) + config.certfile = resolved_ssl_config["certfile"] + config.keyfile = resolved_ssl_config["keyfile"] + if "ca_certs" in resolved_ssl_config: + config.ca_certs = resolved_ssl_config["ca_certs"] # 根据配置决定是否禁用访问日志 disable_access_log = dashboard_config.get("disable_access_log", True) diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 7fba6893e5..6dc352057c 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -108,6 +108,51 @@ async def test_get_stat(app: Quart, authenticated_header: dict): assert data["status"] == "ok" and "platform" in data["data"] +@pytest.mark.asyncio +async def test_dashboard_ssl_missing_cert_and_key_falls_back_to_http( + core_lifecycle_td: AstrBotCoreLifecycle, + monkeypatch, +): + shutdown_event = asyncio.Event() + server = AstrBotDashboard(core_lifecycle_td, core_lifecycle_td.db, shutdown_event) + original_dashboard_config = copy.deepcopy( + core_lifecycle_td.astrbot_config.get("dashboard", {}), + ) + warning_messages = [] + info_messages = [] + + async def fake_serve(app, config, shutdown_trigger): + return config + + try: + core_lifecycle_td.astrbot_config["dashboard"]["ssl"] = { + "enable": True, + "cert_file": "", + "key_file": "", + } + monkeypatch.setattr(server, "check_port_in_use", lambda port: False) + monkeypatch.setattr("astrbot.dashboard.server.serve", fake_serve) + monkeypatch.setattr( + "astrbot.dashboard.server.logger.warning", + lambda message: warning_messages.append(message), + ) + monkeypatch.setattr( + "astrbot.dashboard.server.logger.info", + lambda message: info_messages.append(message), + ) + + config = await server.run() + + assert getattr(config, "certfile", None) is None + assert getattr(config, "keyfile", None) is None + assert any("cert_file 和 key_file" in message for message in warning_messages) + assert any( + "正在启动 WebUI, 监听地址: http://" in message for message in info_messages + ) + finally: + core_lifecycle_td.astrbot_config["dashboard"] = original_dashboard_config + + @pytest.mark.asyncio async def test_subagent_config_accepts_default_persona( app: Quart,