Files
Aether/apps/aether-gateway/src/handlers/proxy/websocket/responses/settlement.rs
T
elky 579f2c7cc1 feat(security): harden gateway boundaries and usage policies
Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
2026-09-04 03:45:52 +08:00

311 lines
12 KiB
Rust

//! Responses WebSocket 的结算信号 → 记账事实映射。
//!
//! 结算判定本身是 transport 中立的,住在
//! [`crate::execution_runtime::attempt_lifecycle`]。这里只做 WS 专属的一件事:
//! 把 relay loop 的结算触发信号 [`ResponsesWebSocketTurnOutcome`] 翻译成那两个
//! 正交事实。
use super::turn::ResponsesWebSocketTurnOutcome;
use crate::execution_runtime::attempt_lifecycle::{
AttemptClientDelivery, AttemptProviderOutcome, AttemptTerminalFacts,
CLIENT_CANCELLED_STATUS_CODE, STREAM_TIMEOUT_STATUS_CODE,
};
/// 把「结算触发信号」+「已观察到的 provider 终态」+「已记录的投递结果」映射成
/// 两个正交事实。
///
/// `ResponsesWebSocketTurnOutcome` 描述的是 relay loop 为什么现在结算这一
/// attempt,它对 provider 的信息量并不总是完整的:
///
/// - `ProviderTerminal` / `Failure` 本身就在描述供应商这一轮的结果,是权威的。
/// - `Cancelled` 只说明「我们为客户端或连接层面的原因停下了」,不携带任何
/// provider 信息。已经观察到的 provider 终态是独立事实,不能被它覆盖——
/// 这正是评审第 5 条要求分开记录的那一处。
///
/// `recorded_delivery` 是 relay loop 明确记下的投递失败(写客户端 socket 失败)。
/// 它与结算信号推出的投递结果取「只要有一侧失败就是失败」,并优先保留明确记录
/// 的原因。
pub(super) fn attempt_facts_for_outcome(
observed_provider_terminal: Option<AttemptProviderOutcome>,
recorded_delivery: AttemptClientDelivery,
settling: ResponsesWebSocketTurnOutcome,
) -> AttemptTerminalFacts {
let facts = match settling {
ResponsesWebSocketTurnOutcome::ProviderTerminal {
status_code,
cancelled,
} => AttemptTerminalFacts {
provider: AttemptProviderOutcome::Terminal {
status_code,
cancelled_by_provider: cancelled,
},
delivery: AttemptClientDelivery::Complete,
},
ResponsesWebSocketTurnOutcome::Failure {
status_code,
reason,
} => AttemptTerminalFacts {
provider: AttemptProviderOutcome::Aborted {
status_code,
reason,
// 现状只有 504 一族(首事件/终态超时)会投射 pool stream timeout。
stream_timeout: status_code == STREAM_TIMEOUT_STATUS_CODE,
},
delivery: AttemptClientDelivery::Complete,
},
ResponsesWebSocketTurnOutcome::Cancelled { reason } => AttemptTerminalFacts {
provider: observed_provider_terminal.unwrap_or(AttemptProviderOutcome::Aborted {
status_code: CLIENT_CANCELLED_STATUS_CODE,
reason,
stream_timeout: false,
}),
delivery: AttemptClientDelivery::Aborted { reason },
},
};
AttemptTerminalFacts {
delivery: match recorded_delivery {
AttemptClientDelivery::Aborted { .. } => recorded_delivery,
AttemptClientDelivery::Complete => facts.delivery,
},
..facts
}
}
/// 客户端投递失败时应该用哪个结算信号。
///
/// provider 终态已经到达就用那条终态:它是权威的 provider 事实,绝不能被
/// `client_disconnected()` 覆盖掉——那正是把已完成响应记成 void billing 的原因。
/// 供应商还没给出终态时,客户端断开才是这一 attempt 的全部结论。
pub(super) fn settle_signal_for_client_delivery_failure(
terminal_outcome: Option<ResponsesWebSocketTurnOutcome>,
) -> ResponsesWebSocketTurnOutcome {
terminal_outcome.unwrap_or_else(ResponsesWebSocketTurnOutcome::client_disconnected)
}
#[cfg(test)]
mod tests {
use super::super::turn::ResponsesWebSocketTurnOutcome;
use super::{attempt_facts_for_outcome, settle_signal_for_client_delivery_failure};
use crate::execution_runtime::attempt_lifecycle::{
AttemptClientDelivery, AttemptProviderOutcome, AttemptTerminalFacts,
};
const fn terminal(status_code: u16) -> AttemptProviderOutcome {
AttemptProviderOutcome::Terminal {
status_code,
cancelled_by_provider: false,
}
}
const fn provider_cancelled() -> AttemptProviderOutcome {
AttemptProviderOutcome::Terminal {
status_code: 499,
cancelled_by_provider: true,
}
}
const fn aborted(status_code: u16, reason: &'static str) -> AttemptProviderOutcome {
AttemptProviderOutcome::Aborted {
status_code,
reason,
stream_timeout: status_code == 504,
}
}
/// §1.6 现状 outcome → 双事实映射表,逐行。
#[test]
fn every_settle_signal_maps_to_a_provider_outcome_and_a_client_delivery() {
assert_eq!(
attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::ProviderTerminal {
status_code: 200,
cancelled: false,
},
),
AttemptTerminalFacts {
provider: terminal(200),
delivery: AttemptClientDelivery::Complete,
}
);
assert_eq!(
attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::ProviderTerminal {
status_code: 499,
cancelled: true,
},
),
AttemptTerminalFacts {
provider: provider_cancelled(),
delivery: AttemptClientDelivery::Complete,
}
);
assert_eq!(
attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::upstream_closed()
),
AttemptTerminalFacts {
provider: aborted(
502,
"upstream WebSocket closed before provider terminal event"
),
delivery: AttemptClientDelivery::Complete,
}
);
assert_eq!(
attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::client_disconnected()
),
AttemptTerminalFacts {
provider: aborted(499, "client disconnected before provider terminal event"),
delivery: AttemptClientDelivery::Aborted {
reason: "client disconnected before provider terminal event",
},
}
);
// 超时一族必须保留 stream_timeout 标记,否则 pool stream timeout 效果丢失。
let first_event_timeout = attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::first_event_timeout(),
);
assert!(first_event_timeout.provider.stream_timeout());
let terminal_timeout = attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::terminal_timeout(),
);
assert!(terminal_timeout.provider.stream_timeout());
// 非 504 的失败不得被当成流式超时。
assert!(!attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::upstream_closed()
)
.provider
.stream_timeout());
// provider 终态即使状态码是 504 也不投射 stream timeout:现状
// `stream_timeout()` 只匹配 Failure 分支。
assert!(!attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::ProviderTerminal {
status_code: 504,
cancelled: false,
},
)
.provider
.stream_timeout());
}
/// `Cancelled` 不携带 provider 信息,已观察到的终态不能被它覆盖;
/// `ProviderTerminal` / `Failure` 本身就是权威的 provider 事实。
#[test]
fn an_observed_provider_terminal_survives_a_client_side_cancellation() {
let observed = terminal(200);
let facts = attempt_facts_for_outcome(
Some(observed),
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::client_disconnected(),
);
assert_eq!(facts.provider, observed);
assert_eq!(
facts.delivery,
AttemptClientDelivery::Aborted {
reason: "client disconnected before provider terminal event",
}
);
// 权威信号不被已记录事实改写。
let facts = attempt_facts_for_outcome(
Some(observed),
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::upstream_closed(),
);
assert_eq!(
facts.provider,
aborted(
502,
"upstream WebSocket closed before provider terminal event"
)
);
assert_eq!(facts.delivery, AttemptClientDelivery::Complete);
}
/// 结算信号的选择:provider 终态已到达就用它,否则才是 client 断开。
/// 这是修正的核心——旧实现无条件用 client_disconnected() 覆盖,
/// 于是已完成的响应被记成 void billing。
#[test]
fn a_reached_terminal_is_the_settle_signal_for_a_delivery_failure() {
let terminal_outcome = ResponsesWebSocketTurnOutcome::ProviderTerminal {
status_code: 200,
cancelled: false,
};
assert_eq!(
settle_signal_for_client_delivery_failure(Some(terminal_outcome)),
terminal_outcome
);
assert_eq!(
settle_signal_for_client_delivery_failure(None),
ResponsesWebSocketTurnOutcome::client_disconnected()
);
}
/// 明确记录的投递失败不会被结算信号推出的「投递成功」覆盖。
#[test]
fn a_recorded_delivery_failure_survives_a_provider_terminal_settle_signal() {
let facts = attempt_facts_for_outcome(
Some(terminal(200)),
AttemptClientDelivery::Aborted {
reason: "write failed",
},
ResponsesWebSocketTurnOutcome::ProviderTerminal {
status_code: 200,
cancelled: false,
},
);
assert_eq!(facts.provider, terminal(200));
assert_eq!(
facts.delivery,
AttemptClientDelivery::Aborted {
reason: "write failed"
}
);
// 投递失败不是供应商的错误,摘要不该因此补 parser_error。
assert_eq!(facts.forced_error(), None);
}
#[test]
fn plan_permit_loss_is_gateway_cancellation_not_provider_failure() {
let facts = attempt_facts_for_outcome(
None,
AttemptClientDelivery::Complete,
ResponsesWebSocketTurnOutcome::connection_admission_lost(),
);
assert_eq!(
facts.provider,
aborted(
499,
"gateway WebSocket connection admission became unhealthy"
)
);
assert_eq!(
facts.delivery,
AttemptClientDelivery::Aborted {
reason: "gateway WebSocket connection admission became unhealthy"
}
);
assert_eq!(facts.forced_error(), None);
}
}