mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
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
This commit is contained in:
@@ -300,6 +300,61 @@ class AstrBotDashboard:
|
||||
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 @@ class AstrBotDashboard:
|
||||
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 @@ class AstrBotDashboard:
|
||||
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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user