From c5bc7b8d828b5200a498e79927a55879e4eb0a0a Mon Sep 17 00:00:00 2001 From: anonsaber Date: Mon, 28 Sep 2026 10:37:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(notify):=20/ready=20JMAP=20=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E5=AF=B9=E9=BD=90=E7=9C=9F=E5=AE=9E=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=EF=BC=88Basic=20=E8=AE=A4=E8=AF=81=20+=20=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=BD=92=E4=B8=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 架构师复核指出两点会致全站不可用的缺陷(notify.rs): - 探针未带认证:真实 jmap_client 以 .credentials() 取 session,Stalwart session 端点需认证;无认证必 401 → /ready 永久 503 → ingress 停止路由。 改为从 business_runtime 快照取 jmap_username/jmap_password, basic_auth 随请求发送(零新增依赖,凭据仅入请求头,绝不日志)。 - session URL 未归一:配置若是裸 origin,GET 根路径必 404 → 误报。 改为复用 normalize_session_url(client.rs 改为 pub(crate))并请求 标准 /.well-known/jmap,与 JmapClientBackend::connect_with_runtime 同路径。 顺带:两探针 tokio::join! 并行,/ready 最坏耗时 3s→3s(原 6s)。 门禁 4/4 绿(45 passed, 0 failed, 1 ignored)。 --- src/domain/jmap/client.rs | 2 +- src/notify.rs | 36 ++++++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/domain/jmap/client.rs b/src/domain/jmap/client.rs index 5250d54..07dfe11 100644 --- a/src/domain/jmap/client.rs +++ b/src/domain/jmap/client.rs @@ -384,7 +384,7 @@ fn role_name(role: mailbox::Role) -> Option { } } -fn normalize_session_url(value: &str) -> Result { +pub(crate) fn normalize_session_url(value: &str) -> Result { let parsed = url::Url::parse(value).map_err(|_| JmapError::InvalidRequest("JMAP_SESSION_URL"))?; if parsed.scheme() != "https" { diff --git a/src/notify.rs b/src/notify.rs index 0d6a3ea..68d0210 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -70,12 +70,14 @@ const PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(3_00 /// Read the cached runtime configuration without holding the lock guard across an await. /// Callers refresh the cache first so this never reports a stale view. -fn cached_jmap_session_url(app: &AppState) -> Option { - app.business_runtime - .read() - .ok()? - .as_ref() - .map(|config| config.jmap_session_url.clone()) +fn cached_jmap_session(app: &AppState) -> Option<(String, String, SecretString)> { + app.business_runtime.read().ok()?.as_ref().map(|config| { + ( + config.jmap_session_url.clone(), + config.jmap_username.clone(), + config.jmap_password.clone(), + ) + }) } fn cached_bot_token(app: &AppState) -> Option { @@ -86,13 +88,21 @@ fn cached_bot_token(app: &AppState) -> Option { .map(|config| config.bot_token.clone()) } -/// End-to-end JMAP session probe: the configured session URL must answer with a 2xx. -/// Reusable by the remote debug surface so readiness and debugging share one implementation. +/// End-to-end JMAP session probe: fetch `/.well-known/jmap` with the configured credentials and +/// require a 2xx. Mirrors `JmapClientBackend::connect_with_runtime` (normalized origin + Basic +/// auth): probing the raw URL unauthenticated would report not-ready forever and make ingress +/// stop routing. Reusable by the remote debug surface so readiness and debugging share one +/// implementation. async fn probe_jmap_session(app: &AppState) -> Result<(), &'static str> { - let Some(session_url) = cached_jmap_session_url(app) else { + let Some((session_url, username, password)) = cached_jmap_session(app) else { return Err("jmap session url not configured"); }; - let request = reqwest::Client::new().get(session_url).send(); + let origin = crate::domain::jmap::client::normalize_session_url(&session_url) + .map_err(|_| "jmap session url invalid")?; + let request = reqwest::Client::new() + .get(format!("{origin}/.well-known/jmap")) + .basic_auth(&username, Some(password.expose_secret())) + .send(); match tokio::time::timeout(PROBE_TIMEOUT, request).await { Ok(Ok(response)) => response .error_for_status() @@ -129,8 +139,10 @@ async fn ready(State(app): State) -> Response { let configured = app.setup_missing.is_empty(); let redis = app.state.is_enabled().await.is_ok(); refresh_business_config(&app).await; - let jmap = probe_jmap_session(&app).await.is_ok(); - let telegram = probe_telegram_get_me(&app).await.is_ok(); + // Probes run in parallel: worst case one PROBE_TIMEOUT instead of two. + let (jmap, telegram) = tokio::join!(probe_jmap_session(&app), probe_telegram_get_me(&app)); + let jmap = jmap.is_ok(); + let telegram = telegram.is_ok(); if configured && redis && jmap && telegram { axum::Json(serde_json::json!({ "status": "ready",