Skip to content

Commit 1070804

Browse files
authored
feat(dashboard): add SSL configuration resolution for dashboard (#7102)
fixes: #7058 program will not exit but fallback to non-ssl mode when ssl config is wrong
1 parent 7db7f4a commit 1070804

File tree

2 files changed

+109
-35
lines changed

2 files changed

+109
-35
lines changed

astrbot/dashboard/server.py

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,61 @@ def _init_jwt_secret(self) -> None:
300300
logger.info("Initialized random JWT secret for dashboard.")
301301
self._jwt_secret = self.config["dashboard"]["jwt_secret"]
302302

303+
@staticmethod
304+
def _resolve_dashboard_ssl_config(
305+
ssl_config: dict,
306+
) -> tuple[bool, dict[str, str]]:
307+
cert_file = (
308+
os.environ.get("DASHBOARD_SSL_CERT")
309+
or os.environ.get("ASTRBOT_DASHBOARD_SSL_CERT")
310+
or ssl_config.get("cert_file", "")
311+
)
312+
key_file = (
313+
os.environ.get("DASHBOARD_SSL_KEY")
314+
or os.environ.get("ASTRBOT_DASHBOARD_SSL_KEY")
315+
or ssl_config.get("key_file", "")
316+
)
317+
ca_certs = (
318+
os.environ.get("DASHBOARD_SSL_CA_CERTS")
319+
or os.environ.get("ASTRBOT_DASHBOARD_SSL_CA_CERTS")
320+
or ssl_config.get("ca_certs", "")
321+
)
322+
323+
if not cert_file or not key_file:
324+
logger.warning(
325+
"dashboard.ssl.enable 已启用,但未同时配置 cert_file 和 key_file,SSL 配置将不会生效。",
326+
)
327+
return False, {}
328+
329+
cert_path = Path(cert_file).expanduser()
330+
key_path = Path(key_file).expanduser()
331+
if not cert_path.is_file():
332+
logger.warning(
333+
f"dashboard.ssl.enable 已启用,但 SSL 证书文件不存在: {cert_path},SSL 配置将不会生效。",
334+
)
335+
return False, {}
336+
if not key_path.is_file():
337+
logger.warning(
338+
f"dashboard.ssl.enable 已启用,但 SSL 私钥文件不存在: {key_path},SSL 配置将不会生效。",
339+
)
340+
return False, {}
341+
342+
resolved_ssl_config = {
343+
"certfile": str(cert_path.resolve()),
344+
"keyfile": str(key_path.resolve()),
345+
}
346+
347+
if ca_certs:
348+
ca_path = Path(ca_certs).expanduser()
349+
if not ca_path.is_file():
350+
logger.warning(
351+
f"dashboard.ssl.enable 已启用,但 SSL CA 证书文件不存在: {ca_path},SSL 配置将不会生效。",
352+
)
353+
return False, {}
354+
resolved_ssl_config["ca_certs"] = str(ca_path.resolve())
355+
356+
return True, resolved_ssl_config
357+
303358
def run(self):
304359
ip_addr = []
305360
dashboard_config = self.core_lifecycle.astrbot_config.get("dashboard", {})
@@ -322,6 +377,11 @@ def run(self):
322377
or os.environ.get("ASTRBOT_DASHBOARD_SSL_ENABLE"),
323378
bool(ssl_config.get("enable", False)),
324379
)
380+
resolved_ssl_config: dict[str, str] = {}
381+
if ssl_enable:
382+
ssl_enable, resolved_ssl_config = self._resolve_dashboard_ssl_config(
383+
ssl_config,
384+
)
325385
scheme = "https" if ssl_enable else "http"
326386

327387
if not enable:
@@ -373,41 +433,10 @@ def run(self):
373433
config = HyperConfig()
374434
config.bind = [f"{host}:{port}"]
375435
if ssl_enable:
376-
cert_file = (
377-
os.environ.get("DASHBOARD_SSL_CERT")
378-
or os.environ.get("ASTRBOT_DASHBOARD_SSL_CERT")
379-
or ssl_config.get("cert_file", "")
380-
)
381-
key_file = (
382-
os.environ.get("DASHBOARD_SSL_KEY")
383-
or os.environ.get("ASTRBOT_DASHBOARD_SSL_KEY")
384-
or ssl_config.get("key_file", "")
385-
)
386-
ca_certs = (
387-
os.environ.get("DASHBOARD_SSL_CA_CERTS")
388-
or os.environ.get("ASTRBOT_DASHBOARD_SSL_CA_CERTS")
389-
or ssl_config.get("ca_certs", "")
390-
)
391-
392-
cert_path = Path(cert_file).expanduser()
393-
key_path = Path(key_file).expanduser()
394-
if not cert_file or not key_file:
395-
raise ValueError(
396-
"dashboard.ssl.enable 为 true 时,必须配置 cert_file 和 key_file。",
397-
)
398-
if not cert_path.is_file():
399-
raise ValueError(f"SSL 证书文件不存在: {cert_path}")
400-
if not key_path.is_file():
401-
raise ValueError(f"SSL 私钥文件不存在: {key_path}")
402-
403-
config.certfile = str(cert_path.resolve())
404-
config.keyfile = str(key_path.resolve())
405-
406-
if ca_certs:
407-
ca_path = Path(ca_certs).expanduser()
408-
if not ca_path.is_file():
409-
raise ValueError(f"SSL CA 证书文件不存在: {ca_path}")
410-
config.ca_certs = str(ca_path.resolve())
436+
config.certfile = resolved_ssl_config["certfile"]
437+
config.keyfile = resolved_ssl_config["keyfile"]
438+
if "ca_certs" in resolved_ssl_config:
439+
config.ca_certs = resolved_ssl_config["ca_certs"]
411440

412441
# 根据配置决定是否禁用访问日志
413442
disable_access_log = dashboard_config.get("disable_access_log", True)

tests/test_dashboard.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,51 @@ async def test_get_stat(app: Quart, authenticated_header: dict):
108108
assert data["status"] == "ok" and "platform" in data["data"]
109109

110110

111+
@pytest.mark.asyncio
112+
async def test_dashboard_ssl_missing_cert_and_key_falls_back_to_http(
113+
core_lifecycle_td: AstrBotCoreLifecycle,
114+
monkeypatch,
115+
):
116+
shutdown_event = asyncio.Event()
117+
server = AstrBotDashboard(core_lifecycle_td, core_lifecycle_td.db, shutdown_event)
118+
original_dashboard_config = copy.deepcopy(
119+
core_lifecycle_td.astrbot_config.get("dashboard", {}),
120+
)
121+
warning_messages = []
122+
info_messages = []
123+
124+
async def fake_serve(app, config, shutdown_trigger):
125+
return config
126+
127+
try:
128+
core_lifecycle_td.astrbot_config["dashboard"]["ssl"] = {
129+
"enable": True,
130+
"cert_file": "",
131+
"key_file": "",
132+
}
133+
monkeypatch.setattr(server, "check_port_in_use", lambda port: False)
134+
monkeypatch.setattr("astrbot.dashboard.server.serve", fake_serve)
135+
monkeypatch.setattr(
136+
"astrbot.dashboard.server.logger.warning",
137+
lambda message: warning_messages.append(message),
138+
)
139+
monkeypatch.setattr(
140+
"astrbot.dashboard.server.logger.info",
141+
lambda message: info_messages.append(message),
142+
)
143+
144+
config = await server.run()
145+
146+
assert getattr(config, "certfile", None) is None
147+
assert getattr(config, "keyfile", None) is None
148+
assert any("cert_file 和 key_file" in message for message in warning_messages)
149+
assert any(
150+
"正在启动 WebUI, 监听地址: http://" in message for message in info_messages
151+
)
152+
finally:
153+
core_lifecycle_td.astrbot_config["dashboard"] = original_dashboard_config
154+
155+
111156
@pytest.mark.asyncio
112157
async def test_subagent_config_accepts_default_persona(
113158
app: Quart,

0 commit comments

Comments
 (0)