fix(notify): /ready JMAP 探针对齐真实客户端(Basic 认证 + 路径归一)
架构师复核指出两点会致全站不可用的缺陷(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)。
This commit is contained in:
parent
c0aab5e966
commit
c5bc7b8d82
|
|
@ -384,7 +384,7 @@ fn role_name(role: mailbox::Role) -> Option<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_session_url(value: &str) -> Result<String, JmapError> {
|
pub(crate) fn normalize_session_url(value: &str) -> Result<String, JmapError> {
|
||||||
let parsed =
|
let parsed =
|
||||||
url::Url::parse(value).map_err(|_| JmapError::InvalidRequest("JMAP_SESSION_URL"))?;
|
url::Url::parse(value).map_err(|_| JmapError::InvalidRequest("JMAP_SESSION_URL"))?;
|
||||||
if parsed.scheme() != "https" {
|
if parsed.scheme() != "https" {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// 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.
|
/// Callers refresh the cache first so this never reports a stale view.
|
||||||
fn cached_jmap_session_url(app: &AppState) -> Option<String> {
|
fn cached_jmap_session(app: &AppState) -> Option<(String, String, SecretString)> {
|
||||||
app.business_runtime
|
app.business_runtime.read().ok()?.as_ref().map(|config| {
|
||||||
.read()
|
(
|
||||||
.ok()?
|
config.jmap_session_url.clone(),
|
||||||
.as_ref()
|
config.jmap_username.clone(),
|
||||||
.map(|config| config.jmap_session_url.clone())
|
config.jmap_password.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cached_bot_token(app: &AppState) -> Option<secrecy::SecretString> {
|
fn cached_bot_token(app: &AppState) -> Option<secrecy::SecretString> {
|
||||||
|
|
@ -86,13 +88,21 @@ fn cached_bot_token(app: &AppState) -> Option<secrecy::SecretString> {
|
||||||
.map(|config| config.bot_token.clone())
|
.map(|config| config.bot_token.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// End-to-end JMAP session probe: the configured session URL must answer with a 2xx.
|
/// End-to-end JMAP session probe: fetch `/.well-known/jmap` with the configured credentials and
|
||||||
/// Reusable by the remote debug surface so readiness and debugging share one implementation.
|
/// 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> {
|
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");
|
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 {
|
match tokio::time::timeout(PROBE_TIMEOUT, request).await {
|
||||||
Ok(Ok(response)) => response
|
Ok(Ok(response)) => response
|
||||||
.error_for_status()
|
.error_for_status()
|
||||||
|
|
@ -129,8 +139,10 @@ async fn ready(State(app): State<AppState>) -> Response {
|
||||||
let configured = app.setup_missing.is_empty();
|
let configured = app.setup_missing.is_empty();
|
||||||
let redis = app.state.is_enabled().await.is_ok();
|
let redis = app.state.is_enabled().await.is_ok();
|
||||||
refresh_business_config(&app).await;
|
refresh_business_config(&app).await;
|
||||||
let jmap = probe_jmap_session(&app).await.is_ok();
|
// Probes run in parallel: worst case one PROBE_TIMEOUT instead of two.
|
||||||
let telegram = probe_telegram_get_me(&app).await.is_ok();
|
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 {
|
if configured && redis && jmap && telegram {
|
||||||
axum::Json(serde_json::json!({
|
axum::Json(serde_json::json!({
|
||||||
"status": "ready",
|
"status": "ready",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue