Skip to main content

termivar_scanner/web_runtime/
websocket_runtime.rs

1//! One-shot, bounded RFC 6455 execution under the shared web authority.
2//!
3//! The policy layer validates the operator declaration. This module repeats the
4//! security-sensitive checks at the transport boundary, consumes one parent
5//! request-accounting lease, and emits only value-free references and counts.
6
7use std::{
8    future::Future,
9    io,
10    pin::Pin,
11    sync::{
12        atomic::{AtomicU64, Ordering},
13        Arc,
14    },
15    task::{Context, Poll},
16    time::Duration,
17};
18
19use futures::{SinkExt, StreamExt};
20use sha2::{Digest, Sha256};
21use tokio::{
22    io::{AsyncRead, AsyncWrite, ReadBuf},
23    net::{lookup_host, TcpStream},
24};
25use tokio_rustls::rustls::{self, ClientConfig, RootCertStore};
26use tokio_tungstenite::{
27    client_async_tls_with_config,
28    tungstenite::{
29        client::IntoClientRequest,
30        http::{HeaderValue, Response},
31        protocol::WebSocketConfig,
32        Error as TungsteniteError, Message,
33    },
34    Connector, MaybeTlsStream, WebSocketStream,
35};
36use tokio_util::sync::CancellationToken;
37
38#[cfg(feature = "supplied-session-review")]
39use crate::supplied_session_review::SuppliedSessionAuthorization;
40use crate::{
41    runtime_budget::{RequestAccountingBroker, RequestAccountingLease},
42    websocket_review::{
43        WebSocketOriginMode, WebSocketReviewPolicy, MAX_WEBSOCKET_REVIEW_CONTROL_FRAMES,
44        MAX_WEBSOCKET_REVIEW_MESSAGES, MAX_WEBSOCKET_REVIEW_WALL_TIME_MS,
45    },
46    DecisionExecutionStage, TransportDispatchOutcome,
47};
48
49use super::authority::SharedWebRuntimeAuthority;
50#[cfg(feature = "supplied-session-review")]
51use super::supplied_session_runtime::SuppliedSessionWebSocketPermit;
52
53/// Schema of the immutable WebSocket review audit.
54pub const WEBSOCKET_REVIEW_AUDIT_SCHEMA: &str = "security.websocket-review-audit/v1";
55/// Schema of a WebSocket review bound to one supplied-session health window.
56#[cfg(feature = "supplied-session-review")]
57pub const WEBSOCKET_SUPPLIED_SESSION_AUDIT_SCHEMA: &str = "security.websocket-review-audit/v2";
58/// Stable identity of this opt-in capability.
59pub const WEBSOCKET_REVIEW_CAPABILITY_ID: &str = "termivar.websocket-review/v1";
60/// Semantic action charged to the parent request-accounting authority.
61pub const WEBSOCKET_REVIEW_ACTION_ID: &str = "websocket.review";
62/// Transport shape implemented by V1.
63pub const WEBSOCKET_REVIEW_TRANSPORT: &str = "rfc6455_http1_upgrade";
64/// Authentication shape implemented by V1.
65pub const WEBSOCKET_REVIEW_AUTHENTICATION: &str = "anonymous";
66/// Context token for an explicitly selected supplied Authorization session.
67#[cfg(feature = "supplied-session-review")]
68pub const WEBSOCKET_SUPPLIED_SESSION_CONTEXT: &str = "supplied_session";
69/// HTTP/2 extended CONNECT is outside the V1 implementation.
70pub const WEBSOCKET_REVIEW_HTTP2_EXTENDED_CONNECT: &str = "unsupported";
71/// Byte-counting layer used for partial failures and timeouts.
72pub const WEBSOCKET_REVIEW_TRANSPORT_BYTE_SCOPE: &str = "socket_wire_bytes_tls_ciphertext_when_wss";
73/// Exactly one connection attempt can be made by one selected review.
74pub const MAX_WEBSOCKET_REVIEW_CONNECTIONS: u8 = 1;
75/// Aggregate application payload admitted in either direction.
76pub const MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION: u64 = 64 * 1024;
77
78/// Conservative limits on conclusions supported by the bounded exchange.
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum WebSocketReviewClaimLimit {
81    BrowserOriginSecurityNotEstablished,
82    AuthenticationNotEstablished,
83    AuthorizationNotEstablished,
84    AvailabilityNotEstablished,
85    VulnerabilityNotEstablished,
86    ExploitabilityNotEstablished,
87    ImpactNotEstablished,
88    SourceAuthenticityNotEstablished,
89}
90
91impl WebSocketReviewClaimLimit {
92    /// Stable reporting token.
93    #[must_use]
94    pub const fn as_str(self) -> &'static str {
95        match self {
96            Self::BrowserOriginSecurityNotEstablished => "browser_origin_security_not_established",
97            Self::AuthenticationNotEstablished => "authentication_not_established",
98            Self::AuthorizationNotEstablished => "authorization_not_established",
99            Self::AvailabilityNotEstablished => "availability_not_established",
100            Self::VulnerabilityNotEstablished => "vulnerability_not_established",
101            Self::ExploitabilityNotEstablished => "exploitability_not_established",
102            Self::ImpactNotEstablished => "impact_not_established",
103            Self::SourceAuthenticityNotEstablished => "source_authenticity_not_established",
104        }
105    }
106}
107
108const WEBSOCKET_REVIEW_CLAIM_LIMITS: [WebSocketReviewClaimLimit; 8] = [
109    WebSocketReviewClaimLimit::BrowserOriginSecurityNotEstablished,
110    WebSocketReviewClaimLimit::AuthenticationNotEstablished,
111    WebSocketReviewClaimLimit::AuthorizationNotEstablished,
112    WebSocketReviewClaimLimit::AvailabilityNotEstablished,
113    WebSocketReviewClaimLimit::VulnerabilityNotEstablished,
114    WebSocketReviewClaimLimit::ExploitabilityNotEstablished,
115    WebSocketReviewClaimLimit::ImpactNotEstablished,
116    WebSocketReviewClaimLimit::SourceAuthenticityNotEstablished,
117];
118
119/// Whether the bounded WebSocket exchange was enclosed by committed healthy
120/// supplied-session checkpoints.
121#[cfg(feature = "supplied-session-review")]
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub(crate) enum WebSocketSuppliedSessionQualification {
124    /// One admitted exchange was enclosed by committed healthy checkpoints.
125    ContextQualified,
126    /// One admitted exchange lacks a committed healthy terminal checkpoint.
127    ContextUnqualified,
128    /// No WebSocket request was admitted under the shared authority.
129    NotEstablished,
130}
131
132#[cfg(feature = "supplied-session-review")]
133impl WebSocketSuppliedSessionQualification {
134    pub(crate) const fn as_str(self) -> &'static str {
135        match self {
136            Self::ContextQualified => "context_qualified",
137            Self::ContextUnqualified => "context_unqualified",
138            Self::NotEstablished => "not_established",
139        }
140    }
141}
142
143/// Value-safe summary of one supplied-session health checkpoint used by the
144/// WebSocket context boundary.
145#[derive(Clone, Debug, Eq, PartialEq)]
146#[cfg(feature = "supplied-session-review")]
147pub(crate) struct WebSocketSuppliedSessionHealthAudit {
148    outcome: &'static str,
149    evidence_reference: Option<String>,
150}
151
152#[cfg(feature = "supplied-session-review")]
153impl WebSocketSuppliedSessionHealthAudit {
154    pub(super) fn new(outcome: &'static str, evidence_reference: Option<&str>) -> Self {
155        Self {
156            outcome,
157            evidence_reference: evidence_reference.map(str::to_owned),
158        }
159    }
160
161    pub(crate) const fn outcome(&self) -> &'static str {
162        self.outcome
163    }
164
165    pub(crate) fn evidence_reference(&self) -> Option<&str> {
166        self.evidence_reference.as_deref()
167    }
168}
169
170/// Redaction-safe supplied-session binding for the WebSocket V2 audit.
171///
172/// The principal alias is an operator assertion. Checkpoint references prove
173/// only that the scanner committed its bounded health observations; neither
174/// this record nor a completed exchange authenticates identity or authority.
175#[derive(Clone, Debug, Eq, PartialEq)]
176#[cfg(feature = "supplied-session-review")]
177pub(crate) struct WebSocketSuppliedSessionContextAudit {
178    policy_reference: String,
179    application_reference: String,
180    principal_reference: String,
181    principal_alias: String,
182    startup_health: Option<WebSocketSuppliedSessionHealthAudit>,
183    terminal_health: Option<WebSocketSuppliedSessionHealthAudit>,
184    qualification: WebSocketSuppliedSessionQualification,
185}
186
187#[cfg(feature = "supplied-session-review")]
188impl WebSocketSuppliedSessionContextAudit {
189    #[allow(clippy::too_many_arguments)]
190    pub(super) fn new(
191        policy_reference: &str,
192        application_reference: &str,
193        principal_reference: &str,
194        principal_alias: &str,
195        startup_health: Option<WebSocketSuppliedSessionHealthAudit>,
196        terminal_health: Option<WebSocketSuppliedSessionHealthAudit>,
197        qualification: WebSocketSuppliedSessionQualification,
198    ) -> Self {
199        Self {
200            policy_reference: policy_reference.to_owned(),
201            application_reference: application_reference.to_owned(),
202            principal_reference: principal_reference.to_owned(),
203            principal_alias: principal_alias.to_owned(),
204            startup_health,
205            terminal_health,
206            qualification,
207        }
208    }
209
210    pub(crate) fn policy_reference(&self) -> &str {
211        &self.policy_reference
212    }
213
214    pub(crate) fn application_reference(&self) -> &str {
215        &self.application_reference
216    }
217
218    pub(crate) fn principal_reference(&self) -> &str {
219        &self.principal_reference
220    }
221
222    pub(crate) fn principal_alias(&self) -> &str {
223        &self.principal_alias
224    }
225
226    pub(crate) const fn principal_assurance(&self) -> &'static str {
227        "operator_declared"
228    }
229
230    pub(crate) const fn credential_mechanism(&self) -> &'static str {
231        "authorization_header"
232    }
233
234    pub(crate) const fn epoch(&self) -> u8 {
235        1
236    }
237
238    pub(crate) const fn startup_health(&self) -> Option<&WebSocketSuppliedSessionHealthAudit> {
239        self.startup_health.as_ref()
240    }
241
242    pub(crate) const fn terminal_health(&self) -> Option<&WebSocketSuppliedSessionHealthAudit> {
243        self.terminal_health.as_ref()
244    }
245
246    pub(crate) const fn qualification(&self) -> WebSocketSuppliedSessionQualification {
247        self.qualification
248    }
249
250    pub(crate) const fn continuous_authentication_established(&self) -> bool {
251        false
252    }
253}
254
255/// Completeness of the exact operator-configured message exchange.
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
257pub enum WebSocketReviewCompleteness {
258    ConfiguredExchangeComplete,
259    Incomplete,
260}
261
262impl WebSocketReviewCompleteness {
263    /// Stable reporting token.
264    #[must_use]
265    pub const fn as_str(self) -> &'static str {
266        match self {
267            Self::ConfiguredExchangeComplete => "configured_exchange_complete",
268            Self::Incomplete => "incomplete",
269        }
270    }
271}
272
273/// Coarse terminal state of one selected WebSocket review.
274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
275pub enum WebSocketReviewTerminal {
276    /// Every configured message was sent and paired with one application response.
277    Completed,
278    /// A safe operational failure stopped the exchange.
279    Incomplete,
280    /// The host cancellation domain stopped the exchange.
281    Cancelled,
282    /// The lesser of the policy and parent deadlines elapsed.
283    DeadlineReached,
284    /// A configured, compiled, or parent byte limit stopped the exchange.
285    LimitReached,
286    /// The one-shot or exact-origin authority refused execution before transport.
287    AuthorityRefused,
288}
289
290impl WebSocketReviewTerminal {
291    /// Stable reporting token.
292    #[must_use]
293    pub const fn as_str(self) -> &'static str {
294        match self {
295            Self::Completed => "completed",
296            Self::Incomplete => "incomplete",
297            Self::Cancelled => "cancelled",
298            Self::DeadlineReached => "deadline_reached",
299            Self::LimitReached => "limit_reached",
300            Self::AuthorityRefused => "authority_refused",
301        }
302    }
303}
304
305/// Value-free classification of the first execution failure.
306#[derive(Clone, Copy, Debug, Eq, PartialEq)]
307pub enum WebSocketReviewFailureKind {
308    AuthorityAlreadyMinted,
309    EndpointOutsideAuthority,
310    ParentAuthorityUnavailable,
311    RequestConstruction,
312    ConnectionFailed,
313    HandshakeRejected,
314    ExtensionNegotiationRefused,
315    SubprotocolNegotiationRefused,
316    SendFailed,
317    ReadFailed,
318    BinaryMessageUnsupported,
319    CloseFailed,
320    PeerClosed,
321    MessageTooLarge,
322    MessageLimitReached,
323    ControlFrameLimitReached,
324    ApplicationByteLimitReached,
325    ParentResponseBudgetReached,
326    Cancelled,
327    DeadlineReached,
328    InternalInvariant,
329}
330
331impl WebSocketReviewFailureKind {
332    /// Stable reporting token. It never includes a URL, payload, or backend error.
333    #[must_use]
334    pub const fn as_str(self) -> &'static str {
335        match self {
336            Self::AuthorityAlreadyMinted => "authority_already_minted",
337            Self::EndpointOutsideAuthority => "endpoint_outside_authority",
338            Self::ParentAuthorityUnavailable => "parent_authority_unavailable",
339            Self::RequestConstruction => "request_construction",
340            Self::ConnectionFailed => "connection_failed",
341            Self::HandshakeRejected => "handshake_rejected",
342            Self::ExtensionNegotiationRefused => "extension_negotiation_refused",
343            Self::SubprotocolNegotiationRefused => "subprotocol_negotiation_refused",
344            Self::SendFailed => "send_failed",
345            Self::ReadFailed => "read_failed",
346            Self::BinaryMessageUnsupported => "binary_message_unsupported",
347            Self::CloseFailed => "close_failed",
348            Self::PeerClosed => "peer_closed",
349            Self::MessageTooLarge => "message_too_large",
350            Self::MessageLimitReached => "message_limit_reached",
351            Self::ControlFrameLimitReached => "control_frame_limit_reached",
352            Self::ApplicationByteLimitReached => "application_byte_limit_reached",
353            Self::ParentResponseBudgetReached => "parent_response_budget_reached",
354            Self::Cancelled => "cancelled",
355            Self::DeadlineReached => "deadline_reached",
356            Self::InternalInvariant => "internal_invariant",
357        }
358    }
359
360    const fn terminal(self) -> WebSocketReviewTerminal {
361        match self {
362            Self::AuthorityAlreadyMinted | Self::EndpointOutsideAuthority => {
363                WebSocketReviewTerminal::AuthorityRefused
364            },
365            Self::MessageTooLarge
366            | Self::MessageLimitReached
367            | Self::ControlFrameLimitReached
368            | Self::ApplicationByteLimitReached
369            | Self::ParentResponseBudgetReached => WebSocketReviewTerminal::LimitReached,
370            Self::Cancelled => WebSocketReviewTerminal::Cancelled,
371            Self::DeadlineReached => WebSocketReviewTerminal::DeadlineReached,
372            _ => WebSocketReviewTerminal::Incomplete,
373        }
374    }
375}
376
377/// Result of one configured outbound message and its next application response.
378#[derive(Clone, Copy, Debug, Eq, PartialEq)]
379pub enum WebSocketReviewMessageStatus {
380    NotSent,
381    SentNoResponse,
382    ResponseMatched,
383    ResponseMismatched,
384}
385
386impl WebSocketReviewMessageStatus {
387    /// Stable reporting token.
388    #[must_use]
389    pub const fn as_str(self) -> &'static str {
390        match self {
391            Self::NotSent => "not_sent",
392            Self::SentNoResponse => "sent_no_response",
393            Self::ResponseMatched => "response_matched",
394            Self::ResponseMismatched => "response_mismatched",
395        }
396    }
397}
398
399/// Payload-free audit of one configured exchange.
400#[derive(Clone, Debug, Eq, PartialEq)]
401pub struct WebSocketReviewMessageAudit {
402    message_reference: String,
403    expected_response_reference: String,
404    observed_response_reference: Option<String>,
405    outbound_length: u64,
406    expected_response_length: u64,
407    inbound_length: Option<u64>,
408    status: WebSocketReviewMessageStatus,
409}
410
411impl WebSocketReviewMessageAudit {
412    pub fn message_reference(&self) -> &str {
413        &self.message_reference
414    }
415
416    pub fn expected_response_reference(&self) -> &str {
417        &self.expected_response_reference
418    }
419
420    pub fn observed_response_reference(&self) -> Option<&str> {
421        self.observed_response_reference.as_deref()
422    }
423
424    pub const fn outbound_length(&self) -> u64 {
425        self.outbound_length
426    }
427
428    pub const fn expected_response_length(&self) -> u64 {
429        self.expected_response_length
430    }
431
432    pub const fn inbound_length(&self) -> Option<u64> {
433        self.inbound_length
434    }
435
436    pub const fn status(&self) -> WebSocketReviewMessageStatus {
437        self.status
438    }
439}
440
441/// Effective numeric ceilings attached to the audit.
442#[derive(Clone, Copy, Debug, Eq, PartialEq)]
443pub struct WebSocketReviewExecutionLimits {
444    max_connections: u8,
445    max_outbound_application_bytes: u64,
446    max_inbound_application_bytes: u64,
447    max_outbound_message_bytes: u64,
448    max_inbound_message_bytes: u64,
449    max_messages: u64,
450    max_control_frames: u64,
451    max_wall_time_ms: u64,
452}
453
454impl WebSocketReviewExecutionLimits {
455    pub const fn max_connections(self) -> u8 {
456        self.max_connections
457    }
458
459    pub const fn max_outbound_application_bytes(self) -> u64 {
460        self.max_outbound_application_bytes
461    }
462
463    pub const fn max_inbound_application_bytes(self) -> u64 {
464        self.max_inbound_application_bytes
465    }
466
467    pub const fn max_outbound_message_bytes(self) -> u64 {
468        self.max_outbound_message_bytes
469    }
470
471    pub const fn max_inbound_message_bytes(self) -> u64 {
472        self.max_inbound_message_bytes
473    }
474
475    pub const fn max_messages(self) -> u64 {
476        self.max_messages
477    }
478
479    pub const fn max_control_frames(self) -> u64 {
480        self.max_control_frames
481    }
482
483    pub const fn max_wall_time_ms(self) -> u64 {
484        self.max_wall_time_ms
485    }
486}
487
488/// Redaction-safe result of one selected WebSocket protocol review.
489#[derive(Clone, Debug, Eq, PartialEq)]
490pub struct WebAssessmentWebSocketReviewAudit {
491    policy_reference: String,
492    endpoint_reference: String,
493    origin_mode: WebSocketOriginMode,
494    subprotocol_requested: bool,
495    subprotocol_negotiated: bool,
496    request_attempt_count: u8,
497    request_admitted_count: u8,
498    connection_attempt_count: u8,
499    handshake_completed_count: u8,
500    configured_message_count: u64,
501    outbound_message_count: u64,
502    inbound_message_count: u64,
503    outbound_control_frame_count: u64,
504    inbound_control_frame_count: u64,
505    outbound_application_bytes: u64,
506    inbound_application_bytes: u64,
507    transport_read_bytes: u64,
508    transport_write_bytes: u64,
509    accounted_request_body_bytes: u64,
510    accounted_response_bytes: u64,
511    terminal: WebSocketReviewTerminal,
512    failure: Option<WebSocketReviewFailureKind>,
513    messages: Vec<WebSocketReviewMessageAudit>,
514    limits: WebSocketReviewExecutionLimits,
515    #[cfg(feature = "supplied-session-review")]
516    supplied_session_context: Option<WebSocketSuppliedSessionContextAudit>,
517}
518
519impl WebAssessmentWebSocketReviewAudit {
520    pub const fn schema(&self) -> &'static str {
521        #[cfg(feature = "supplied-session-review")]
522        if self.supplied_session_context.is_some() {
523            return WEBSOCKET_SUPPLIED_SESSION_AUDIT_SCHEMA;
524        }
525        WEBSOCKET_REVIEW_AUDIT_SCHEMA
526    }
527
528    pub const fn capability_id(&self) -> &'static str {
529        WEBSOCKET_REVIEW_CAPABILITY_ID
530    }
531
532    pub const fn selected(&self) -> bool {
533        true
534    }
535
536    pub fn policy_reference(&self) -> &str {
537        &self.policy_reference
538    }
539
540    pub fn endpoint_reference(&self) -> &str {
541        &self.endpoint_reference
542    }
543
544    pub const fn origin_mode(&self) -> WebSocketOriginMode {
545        self.origin_mode
546    }
547
548    pub const fn subprotocol_requested(&self) -> bool {
549        self.subprotocol_requested
550    }
551
552    pub const fn subprotocol_negotiated(&self) -> bool {
553        self.subprotocol_negotiated
554    }
555
556    pub const fn compression_enabled(&self) -> bool {
557        false
558    }
559
560    pub const fn reconnect_enabled(&self) -> bool {
561        false
562    }
563
564    pub const fn authentication(&self) -> &'static str {
565        #[cfg(feature = "supplied-session-review")]
566        if self.supplied_session_context.is_some() {
567            return "supplied_authorization_header";
568        }
569        WEBSOCKET_REVIEW_AUTHENTICATION
570    }
571
572    pub const fn context(&self) -> &'static str {
573        #[cfg(feature = "supplied-session-review")]
574        if self.supplied_session_context.is_some() {
575            return WEBSOCKET_SUPPLIED_SESSION_CONTEXT;
576        }
577        WEBSOCKET_REVIEW_AUTHENTICATION
578    }
579
580    #[cfg(feature = "supplied-session-review")]
581    /// Returns the redaction-safe supplied-session context attached to a V2 audit.
582    #[must_use]
583    pub(crate) const fn supplied_session_context(
584        &self,
585    ) -> Option<&WebSocketSuppliedSessionContextAudit> {
586        self.supplied_session_context.as_ref()
587    }
588
589    pub const fn transport(&self) -> &'static str {
590        WEBSOCKET_REVIEW_TRANSPORT
591    }
592
593    pub const fn http2_extended_connect(&self) -> &'static str {
594        WEBSOCKET_REVIEW_HTTP2_EXTENDED_CONNECT
595    }
596
597    pub const fn request_count(&self) -> u8 {
598        self.request_admitted_count
599    }
600
601    pub const fn request_attempt_count(&self) -> u8 {
602        self.request_attempt_count
603    }
604
605    pub const fn request_admitted_count(&self) -> u8 {
606        self.request_admitted_count
607    }
608
609    /// Direct connection attempts. V1 never retries or reconnects.
610    pub const fn connection_count(&self) -> u8 {
611        self.connection_attempt_count
612    }
613
614    pub const fn connection_attempt_count(&self) -> u8 {
615        self.connection_attempt_count
616    }
617
618    pub const fn handshake_completed_count(&self) -> u8 {
619        self.handshake_completed_count
620    }
621
622    pub const fn configured_message_count(&self) -> u64 {
623        self.configured_message_count
624    }
625
626    pub const fn outbound_message_count(&self) -> u64 {
627        self.outbound_message_count
628    }
629
630    pub const fn inbound_message_count(&self) -> u64 {
631        self.inbound_message_count
632    }
633
634    pub const fn outbound_control_frame_count(&self) -> u64 {
635        self.outbound_control_frame_count
636    }
637
638    pub const fn inbound_control_frame_count(&self) -> u64 {
639        self.inbound_control_frame_count
640    }
641
642    pub const fn outbound_application_bytes(&self) -> u64 {
643        self.outbound_application_bytes
644    }
645
646    pub const fn inbound_application_bytes(&self) -> u64 {
647        self.inbound_application_bytes
648    }
649
650    /// Bytes read from the direct socket, including the HTTP Upgrade and
651    /// framing; WSS counts encrypted TLS records at this layer.
652    pub const fn transport_read_bytes(&self) -> u64 {
653        self.transport_read_bytes
654    }
655
656    /// Bytes written to the direct socket, with the same scope as reads.
657    pub const fn transport_write_bytes(&self) -> u64 {
658        self.transport_write_bytes
659    }
660
661    pub const fn transport_byte_scope(&self) -> &'static str {
662        WEBSOCKET_REVIEW_TRANSPORT_BYTE_SCOPE
663    }
664
665    /// HTTP request-body bytes charged to the parent lease. WebSocket payloads
666    /// are child-local and this is always zero in V1.
667    pub const fn accounted_request_body_bytes(&self) -> u64 {
668        self.accounted_request_body_bytes
669    }
670
671    /// Raw socket bytes read and charged exactly once to the sole parent lease.
672    pub const fn accounted_response_bytes(&self) -> u64 {
673        self.accounted_response_bytes
674    }
675
676    pub const fn accounted_transport_response_bytes(&self) -> u64 {
677        self.accounted_response_bytes
678    }
679
680    pub const fn terminal(&self) -> WebSocketReviewTerminal {
681        self.terminal
682    }
683
684    pub const fn failure(&self) -> Option<WebSocketReviewFailureKind> {
685        self.failure
686    }
687
688    pub const fn first_failure(&self) -> Option<WebSocketReviewFailureKind> {
689        self.failure
690    }
691
692    pub const fn completeness(&self) -> WebSocketReviewCompleteness {
693        if matches!(self.terminal, WebSocketReviewTerminal::Completed) {
694            WebSocketReviewCompleteness::ConfiguredExchangeComplete
695        } else {
696            WebSocketReviewCompleteness::Incomplete
697        }
698    }
699
700    pub const fn expected_response_count(&self) -> u64 {
701        self.configured_message_count
702    }
703
704    pub fn matched_response_count(&self) -> u64 {
705        u64::try_from(
706            self.messages
707                .iter()
708                .filter(|message| message.status == WebSocketReviewMessageStatus::ResponseMatched)
709                .count(),
710        )
711        .unwrap_or(u64::MAX)
712    }
713
714    pub fn mismatched_response_count(&self) -> u64 {
715        u64::try_from(
716            self.messages
717                .iter()
718                .filter(|message| {
719                    message.status == WebSocketReviewMessageStatus::ResponseMismatched
720                })
721                .count(),
722        )
723        .unwrap_or(u64::MAX)
724    }
725
726    pub fn messages(&self) -> &[WebSocketReviewMessageAudit] {
727        &self.messages
728    }
729
730    pub const fn limits(&self) -> WebSocketReviewExecutionLimits {
731        self.limits
732    }
733
734    pub const fn claim_limits(&self) -> &'static [WebSocketReviewClaimLimit] {
735        &WEBSOCKET_REVIEW_CLAIM_LIMITS
736    }
737
738    #[cfg(feature = "supplied-session-review")]
739    pub(super) fn bind_supplied_session_context(
740        &mut self,
741        context: WebSocketSuppliedSessionContextAudit,
742    ) {
743        self.supplied_session_context = Some(context);
744    }
745
746    /// Revalidates bounded counts and local request/response conservation.
747    pub fn is_consistent(&self) -> bool {
748        let configured = u64::try_from(self.messages.len()).unwrap_or(u64::MAX);
749        let outbound_messages = self
750            .messages
751            .iter()
752            .filter(|message| message.status != WebSocketReviewMessageStatus::NotSent)
753            .count();
754        let inbound_messages = self
755            .messages
756            .iter()
757            .filter(|message| message.inbound_length.is_some())
758            .count();
759        let outbound_bytes = self
760            .messages
761            .iter()
762            .filter(|message| message.status != WebSocketReviewMessageStatus::NotSent)
763            .fold(0_u64, |total, message| {
764                total.saturating_add(message.outbound_length)
765            });
766        let inbound_bytes = self.messages.iter().fold(0_u64, |total, message| {
767            total.saturating_add(message.inbound_length.unwrap_or(0))
768        });
769        let message_rows_are_valid = self.messages.iter().all(|message| {
770            message.outbound_length <= self.limits.max_outbound_message_bytes
771                && message.expected_response_length <= self.limits.max_inbound_message_bytes
772                && match message.status {
773                    WebSocketReviewMessageStatus::NotSent => {
774                        message.inbound_length.is_none()
775                            && message.observed_response_reference.is_none()
776                    },
777                    WebSocketReviewMessageStatus::SentNoResponse => {
778                        message.inbound_length.is_none()
779                            && message.observed_response_reference.is_none()
780                    },
781                    WebSocketReviewMessageStatus::ResponseMatched => {
782                        message.inbound_length == Some(message.expected_response_length)
783                            && message.observed_response_reference.as_deref()
784                                == Some(message.expected_response_reference.as_str())
785                    },
786                    WebSocketReviewMessageStatus::ResponseMismatched => {
787                        message.inbound_length.is_some()
788                            && message.observed_response_reference.is_none()
789                    },
790                }
791        });
792        let complete = self.terminal == WebSocketReviewTerminal::Completed;
793        self.request_attempt_count <= 1
794            && self.request_admitted_count <= self.request_attempt_count
795            && self.connection_attempt_count <= self.request_admitted_count
796            && self.connection_attempt_count <= self.limits.max_connections
797            && self.handshake_completed_count <= self.connection_attempt_count
798            && (!self.subprotocol_negotiated
799                || (self.subprotocol_requested && self.handshake_completed_count == 1))
800            && self.configured_message_count == configured
801            && self.configured_message_count > 0
802            && self.configured_message_count <= self.limits.max_messages
803            && self.outbound_message_count == u64::try_from(outbound_messages).unwrap_or(u64::MAX)
804            && self.inbound_message_count == u64::try_from(inbound_messages).unwrap_or(u64::MAX)
805            && self.inbound_message_count <= self.outbound_message_count
806            && self.outbound_application_bytes == outbound_bytes
807            && self.inbound_application_bytes == inbound_bytes
808            && self.accounted_request_body_bytes == 0
809            && self.accounted_response_bytes == self.transport_read_bytes
810            && self.transport_read_bytes >= self.inbound_application_bytes
811            && self.transport_write_bytes >= self.outbound_application_bytes
812            && (self.transport_read_bytes == 0 || self.connection_attempt_count == 1)
813            && (self.transport_write_bytes == 0 || self.connection_attempt_count == 1)
814            && (self.outbound_message_count == 0 || self.handshake_completed_count == 1)
815            && self.outbound_message_count <= self.limits.max_messages
816            && self.inbound_message_count <= self.limits.max_messages
817            && self
818                .outbound_control_frame_count
819                .saturating_add(self.inbound_control_frame_count)
820                <= self.limits.max_control_frames
821            && self.outbound_application_bytes <= self.limits.max_outbound_application_bytes
822            && self.inbound_application_bytes <= self.limits.max_inbound_application_bytes
823            && self.limits.max_connections == MAX_WEBSOCKET_REVIEW_CONNECTIONS
824            && self.limits.max_outbound_application_bytes
825                <= MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION
826            && self.limits.max_inbound_application_bytes
827                <= MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION
828            && self.limits.max_messages
829                <= u64::try_from(MAX_WEBSOCKET_REVIEW_MESSAGES).unwrap_or(u64::MAX)
830            && self.limits.max_control_frames <= u64::from(MAX_WEBSOCKET_REVIEW_CONTROL_FRAMES)
831            && self.limits.max_wall_time_ms <= MAX_WEBSOCKET_REVIEW_WALL_TIME_MS
832            && message_rows_are_valid
833            && {
834                #[cfg(feature = "supplied-session-review")]
835                {
836                    self.supplied_session_context
837                        .as_ref()
838                        .is_none_or(|context| {
839                            let startup = context.startup_health();
840                            let terminal = context.terminal_health();
841                            match context.qualification() {
842                                WebSocketSuppliedSessionQualification::ContextQualified => {
843                                    self.request_admitted_count == 1
844                                        && startup.is_some_and(|health| {
845                                            health.outcome() == "healthy"
846                                                && health.evidence_reference().is_some()
847                                        })
848                                        && terminal.is_some_and(|health| {
849                                            health.outcome() == "healthy"
850                                                && health.evidence_reference().is_some()
851                                        })
852                                },
853                                WebSocketSuppliedSessionQualification::ContextUnqualified => {
854                                    self.request_admitted_count == 1
855                                        && startup.is_some_and(|health| {
856                                            health.outcome() == "healthy"
857                                                && health.evidence_reference().is_some()
858                                        })
859                                        && terminal.is_none_or(|health| {
860                                            health.outcome() != "healthy"
861                                                || health.evidence_reference().is_none()
862                                        })
863                                },
864                                WebSocketSuppliedSessionQualification::NotEstablished => {
865                                    self.request_admitted_count == 0
866                                },
867                            }
868                        })
869                }
870                #[cfg(not(feature = "supplied-session-review"))]
871                {
872                    true
873                }
874            }
875            && if complete {
876                self.failure.is_none()
877            } else {
878                self.failure
879                    .is_some_and(|failure| failure.terminal() == self.terminal)
880            }
881            && (!complete
882                || (self.outbound_message_count == self.configured_message_count
883                    && self.inbound_message_count == self.configured_message_count))
884    }
885
886    fn fail(&mut self, failure: WebSocketReviewFailureKind) {
887        self.terminal = failure.terminal();
888        self.failure = Some(failure);
889    }
890
891    fn cancel(&mut self) {
892        self.terminal = WebSocketReviewTerminal::Cancelled;
893        self.failure = Some(WebSocketReviewFailureKind::Cancelled);
894    }
895
896    fn deadline(&mut self) {
897        self.terminal = WebSocketReviewTerminal::DeadlineReached;
898        self.failure = Some(WebSocketReviewFailureKind::DeadlineReached);
899    }
900}
901
902#[derive(Clone, Default)]
903struct TransportByteCounters {
904    read: Arc<AtomicU64>,
905    written: Arc<AtomicU64>,
906}
907
908impl TransportByteCounters {
909    fn snapshot(&self) -> TransportByteSnapshot {
910        TransportByteSnapshot {
911            read: self.read.load(Ordering::Relaxed),
912            written: self.written.load(Ordering::Relaxed),
913        }
914    }
915
916    fn add_read(&self, bytes: usize) {
917        saturating_atomic_add(&self.read, u64::try_from(bytes).unwrap_or(u64::MAX));
918    }
919
920    fn add_written(&self, bytes: usize) {
921        saturating_atomic_add(&self.written, u64::try_from(bytes).unwrap_or(u64::MAX));
922    }
923}
924
925#[derive(Clone, Copy)]
926struct TransportByteSnapshot {
927    read: u64,
928    written: u64,
929}
930
931impl TransportByteSnapshot {
932    fn delta_from(self, earlier: Self) -> Self {
933        Self {
934            read: self.read.saturating_sub(earlier.read),
935            written: self.written.saturating_sub(earlier.written),
936        }
937    }
938}
939
940fn saturating_atomic_add(counter: &AtomicU64, increment: u64) {
941    // A zero increment or saturation still performs the original successful RMW.
942    let mut current = counter.load(Ordering::Relaxed);
943    loop {
944        match counter.compare_exchange_weak(
945            current,
946            current.saturating_add(increment),
947            Ordering::Relaxed,
948            Ordering::Relaxed,
949        ) {
950            Ok(_) => return,
951            Err(observed) => current = observed,
952        }
953    }
954}
955
956struct CountingStream<T> {
957    inner: T,
958    counters: TransportByteCounters,
959}
960
961impl<T> CountingStream<T> {
962    fn new(inner: T) -> (Self, TransportByteCounters) {
963        let counters = TransportByteCounters::default();
964        (
965            Self {
966                inner,
967                counters: counters.clone(),
968            },
969            counters,
970        )
971    }
972}
973
974impl<T: AsyncRead + Unpin> AsyncRead for CountingStream<T> {
975    fn poll_read(
976        self: Pin<&mut Self>,
977        context: &mut Context<'_>,
978        buffer: &mut ReadBuf<'_>,
979    ) -> Poll<io::Result<()>> {
980        let this = self.get_mut();
981        let before = buffer.filled().len();
982        let result = Pin::new(&mut this.inner).poll_read(context, buffer);
983        let read = buffer.filled().len().saturating_sub(before);
984        if read != 0 {
985            this.counters.add_read(read);
986        }
987        result
988    }
989}
990
991impl<T: AsyncWrite + Unpin> AsyncWrite for CountingStream<T> {
992    fn poll_write(
993        self: Pin<&mut Self>,
994        context: &mut Context<'_>,
995        buffer: &[u8],
996    ) -> Poll<io::Result<usize>> {
997        let this = self.get_mut();
998        match Pin::new(&mut this.inner).poll_write(context, buffer) {
999            Poll::Ready(Ok(written)) => {
1000                this.counters.add_written(written);
1001                Poll::Ready(Ok(written))
1002            },
1003            other => other,
1004        }
1005    }
1006
1007    fn poll_flush(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
1008        Pin::new(&mut self.get_mut().inner).poll_flush(context)
1009    }
1010
1011    fn poll_shutdown(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
1012        Pin::new(&mut self.get_mut().inner).poll_shutdown(context)
1013    }
1014
1015    fn is_write_vectored(&self) -> bool {
1016        self.inner.is_write_vectored()
1017    }
1018
1019    fn poll_write_vectored(
1020        self: Pin<&mut Self>,
1021        context: &mut Context<'_>,
1022        buffers: &[io::IoSlice<'_>],
1023    ) -> Poll<io::Result<usize>> {
1024        let this = self.get_mut();
1025        match Pin::new(&mut this.inner).poll_write_vectored(context, buffers) {
1026            Poll::Ready(Ok(written)) => {
1027                this.counters.add_written(written);
1028                Poll::Ready(Ok(written))
1029            },
1030            other => other,
1031        }
1032    }
1033}
1034
1035type CountedWebSocket = WebSocketStream<MaybeTlsStream<CountingStream<TcpStream>>>;
1036
1037struct RuntimeMessage {
1038    text: String,
1039    expected_response_sha256: [u8; 32],
1040    expected_response_length: u64,
1041}
1042
1043/// Consuming executor minted only by [`SharedWebRuntimeAuthority`].
1044pub(super) struct WebSocketReviewRuntime {
1045    endpoint: url::Url,
1046    application_origin: String,
1047    origin_mode: WebSocketOriginMode,
1048    subprotocol: Option<String>,
1049    messages: Vec<RuntimeMessage>,
1050    request_accounting: RequestAccountingBroker,
1051    cancellation: CancellationToken,
1052    parent_deadline: Option<tokio::time::Instant>,
1053    audit: WebAssessmentWebSocketReviewAudit,
1054}
1055
1056#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1057pub(super) enum WebSocketReviewRuntimeMintError {
1058    AuthorityAlreadyMinted,
1059    EndpointOutsideAuthority,
1060    InternalInvariant,
1061}
1062
1063impl WebSocketReviewRuntimeMintError {
1064    const fn failure(self) -> WebSocketReviewFailureKind {
1065        match self {
1066            Self::AuthorityAlreadyMinted => WebSocketReviewFailureKind::AuthorityAlreadyMinted,
1067            Self::EndpointOutsideAuthority => WebSocketReviewFailureKind::EndpointOutsideAuthority,
1068            Self::InternalInvariant => WebSocketReviewFailureKind::InternalInvariant,
1069        }
1070    }
1071}
1072
1073impl WebSocketReviewRuntime {
1074    pub(super) fn new(
1075        policy: &WebSocketReviewPolicy,
1076        application_origin: String,
1077        request_accounting: RequestAccountingBroker,
1078        cancellation: CancellationToken,
1079        parent_deadline: Option<tokio::time::Instant>,
1080    ) -> Result<Self, WebSocketReviewRuntimeMintError> {
1081        let mut messages = Vec::with_capacity(policy.message_count());
1082        let mut message_audits = Vec::with_capacity(policy.message_count());
1083        let mut configured_bytes = 0_u64;
1084        let mut configured_expected_bytes = 0_u64;
1085        for message in policy.execution_messages() {
1086            let outbound_length = message.outbound_length();
1087            configured_bytes = configured_bytes.saturating_add(outbound_length);
1088            configured_expected_bytes =
1089                configured_expected_bytes.saturating_add(message.expected_response_length());
1090            messages.push(RuntimeMessage {
1091                text: message.execution_text().to_owned(),
1092                expected_response_sha256: *message.expected_response_sha256(),
1093                expected_response_length: message.expected_response_length(),
1094            });
1095            message_audits.push(WebSocketReviewMessageAudit {
1096                message_reference: message.message_reference().to_owned(),
1097                expected_response_reference: message.expected_response_reference().to_owned(),
1098                observed_response_reference: None,
1099                outbound_length,
1100                expected_response_length: message.expected_response_length(),
1101                inbound_length: None,
1102                status: WebSocketReviewMessageStatus::NotSent,
1103            });
1104        }
1105        if configured_bytes > MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION
1106            || configured_expected_bytes > MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION
1107            || messages.len() > MAX_WEBSOCKET_REVIEW_MESSAGES
1108        {
1109            return Err(WebSocketReviewRuntimeMintError::InternalInvariant);
1110        }
1111        let configured_message_count = u64::try_from(messages.len()).unwrap_or(u64::MAX);
1112        let limits = WebSocketReviewExecutionLimits {
1113            max_connections: MAX_WEBSOCKET_REVIEW_CONNECTIONS,
1114            max_outbound_application_bytes: MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION,
1115            max_inbound_application_bytes: MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION,
1116            max_outbound_message_bytes: policy.max_outbound_message_bytes(),
1117            max_inbound_message_bytes: policy.max_inbound_message_bytes(),
1118            max_messages: u64::from(policy.max_messages()),
1119            max_control_frames: u64::from(policy.max_control_frames()),
1120            max_wall_time_ms: policy.max_wall_time_ms(),
1121        };
1122        if limits.max_messages > u64::try_from(MAX_WEBSOCKET_REVIEW_MESSAGES).unwrap_or(u64::MAX)
1123            || limits.max_control_frames > u64::from(MAX_WEBSOCKET_REVIEW_CONTROL_FRAMES)
1124            || limits.max_outbound_message_bytes
1125                > MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION
1126            || limits.max_inbound_message_bytes
1127                > MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION
1128            || limits.max_wall_time_ms > MAX_WEBSOCKET_REVIEW_WALL_TIME_MS
1129            || configured_message_count > limits.max_messages
1130            || message_audits.iter().any(|message| {
1131                message.outbound_length > limits.max_outbound_message_bytes
1132                    || message.expected_response_length > limits.max_inbound_message_bytes
1133            })
1134        {
1135            return Err(WebSocketReviewRuntimeMintError::InternalInvariant);
1136        }
1137        Ok(Self {
1138            endpoint: policy.execution_endpoint().clone(),
1139            application_origin,
1140            origin_mode: policy.origin_mode(),
1141            subprotocol: policy.execution_subprotocol().map(str::to_owned),
1142            messages,
1143            request_accounting,
1144            cancellation,
1145            parent_deadline,
1146            audit: WebAssessmentWebSocketReviewAudit {
1147                policy_reference: policy.policy_reference().to_owned(),
1148                endpoint_reference: policy.endpoint_reference().to_owned(),
1149                origin_mode: policy.origin_mode(),
1150                subprotocol_requested: policy.execution_subprotocol().is_some(),
1151                subprotocol_negotiated: false,
1152                request_attempt_count: 0,
1153                request_admitted_count: 0,
1154                connection_attempt_count: 0,
1155                handshake_completed_count: 0,
1156                configured_message_count,
1157                outbound_message_count: 0,
1158                inbound_message_count: 0,
1159                outbound_control_frame_count: 0,
1160                inbound_control_frame_count: 0,
1161                outbound_application_bytes: 0,
1162                inbound_application_bytes: 0,
1163                transport_read_bytes: 0,
1164                transport_write_bytes: 0,
1165                accounted_request_body_bytes: 0,
1166                accounted_response_bytes: 0,
1167                terminal: WebSocketReviewTerminal::Incomplete,
1168                failure: Some(WebSocketReviewFailureKind::InternalInvariant),
1169                messages: message_audits,
1170                limits,
1171                #[cfg(feature = "supplied-session-review")]
1172                supplied_session_context: None,
1173            },
1174        })
1175    }
1176
1177    pub(super) async fn execute(self) -> WebAssessmentWebSocketReviewAudit {
1178        boxed_websocket_child(|| self.execute_inner(None)).await
1179    }
1180
1181    #[cfg(feature = "supplied-session-review")]
1182    pub(super) async fn execute_with_supplied_authorization(
1183        self,
1184        authorization: &SuppliedSessionAuthorization,
1185    ) -> WebAssessmentWebSocketReviewAudit {
1186        boxed_websocket_child(|| self.execute_inner(Some(authorization))).await
1187    }
1188
1189    async fn execute_inner(
1190        mut self,
1191        #[cfg(feature = "supplied-session-review")] authorization: Option<
1192            &SuppliedSessionAuthorization,
1193        >,
1194        #[cfg(not(feature = "supplied-session-review"))] _authorization: Option<&()>,
1195    ) -> WebAssessmentWebSocketReviewAudit {
1196        let local_deadline = tokio::time::Instant::now()
1197            .checked_add(Duration::from_millis(self.audit.limits.max_wall_time_ms));
1198        let Some(deadline) = lesser_deadline(local_deadline, self.parent_deadline) else {
1199            self.audit
1200                .fail(WebSocketReviewFailureKind::InternalInvariant);
1201            return self.audit;
1202        };
1203        if self.cancellation.is_cancelled() {
1204            self.audit.cancel();
1205            return self.audit;
1206        }
1207        if tokio::time::Instant::now() >= deadline {
1208            self.audit.deadline();
1209            return self.audit;
1210        }
1211        self.audit.request_attempt_count = 1;
1212        let mut lease = match self.request_accounting.try_begin_with_request_body_bytes(
1213            WEBSOCKET_REVIEW_ACTION_ID,
1214            DecisionExecutionStage::Passive,
1215            None,
1216            0,
1217        ) {
1218            Ok(lease) => lease,
1219            Err(_) => {
1220                self.audit
1221                    .fail(WebSocketReviewFailureKind::ParentAuthorityUnavailable);
1222                return self.audit;
1223            },
1224        };
1225        self.audit.request_admitted_count = 1;
1226        let transport_outcome = self
1227            .execute_with_lease(
1228                &mut lease,
1229                deadline,
1230                #[cfg(feature = "supplied-session-review")]
1231                authorization,
1232                #[cfg(not(feature = "supplied-session-review"))]
1233                None,
1234            )
1235            .await;
1236        lease.finish(transport_outcome);
1237        debug_assert!(self.audit.is_consistent());
1238        self.audit
1239    }
1240
1241    async fn execute_with_lease(
1242        &mut self,
1243        lease: &mut RequestAccountingLease,
1244        deadline: tokio::time::Instant,
1245        #[cfg(feature = "supplied-session-review")] authorization: Option<
1246            &SuppliedSessionAuthorization,
1247        >,
1248        #[cfg(not(feature = "supplied-session-review"))] _authorization: Option<&()>,
1249    ) -> TransportDispatchOutcome {
1250        #[cfg(feature = "supplied-session-review")]
1251        let request = authorization.map_or_else(
1252            || self.client_request(),
1253            |authorization| self.client_request_with_supplied_authorization(authorization),
1254        );
1255        #[cfg(not(feature = "supplied-session-review"))]
1256        let request = self.client_request();
1257        let prepared_transport = request.and_then(|request| {
1258            let tls_connector = websocket_tls_connector(&self.endpoint)?;
1259            Ok((request, tls_connector))
1260        });
1261        let (request, tls_connector) = match prepared_transport {
1262            Ok(prepared_transport) => prepared_transport,
1263            Err(failure) => {
1264                self.audit.fail(failure);
1265                return TransportDispatchOutcome::TransportFailure;
1266            },
1267        };
1268        let max_inbound = usize::try_from(self.audit.limits.max_inbound_message_bytes)
1269            .unwrap_or(usize::MAX)
1270            .min(usize::try_from(MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION).unwrap());
1271        let websocket_config = WebSocketConfig::default()
1272            .read_buffer_size(max_inbound.clamp(1024, 16 * 1024))
1273            .write_buffer_size(0)
1274            .max_write_buffer_size(
1275                usize::try_from(MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION)
1276                    .unwrap()
1277                    .saturating_add(1024),
1278            )
1279            .max_message_size(Some(max_inbound))
1280            .max_frame_size(Some(max_inbound))
1281            .accept_unmasked_frames(false);
1282
1283        let Some(host) = self.endpoint.host_str().map(str::to_owned) else {
1284            self.audit
1285                .fail(WebSocketReviewFailureKind::RequestConstruction);
1286            return TransportDispatchOutcome::TransportFailure;
1287        };
1288        let Some(port) = self.endpoint.port_or_known_default() else {
1289            self.audit
1290                .fail(WebSocketReviewFailureKind::RequestConstruction);
1291            return TransportDispatchOutcome::TransportFailure;
1292        };
1293        let resolved = match bounded(
1294            &self.cancellation,
1295            deadline,
1296            lookup_host((host.as_str(), port)),
1297        )
1298        .await
1299        {
1300            Ok(Ok(addresses)) => addresses.min(),
1301            Ok(Err(_)) => {
1302                self.audit
1303                    .fail(WebSocketReviewFailureKind::ConnectionFailed);
1304                return TransportDispatchOutcome::TransportFailure;
1305            },
1306            Err(StepStop::Cancelled) => {
1307                self.audit.cancel();
1308                return TransportDispatchOutcome::Cancelled;
1309            },
1310            Err(StepStop::Deadline) => {
1311                self.audit.deadline();
1312                return TransportDispatchOutcome::RequestTimeout;
1313            },
1314        };
1315        let Some(address) = resolved else {
1316            self.audit
1317                .fail(WebSocketReviewFailureKind::ConnectionFailed);
1318            return TransportDispatchOutcome::TransportFailure;
1319        };
1320
1321        self.audit.connection_attempt_count = 1;
1322        let stream = match bounded(&self.cancellation, deadline, TcpStream::connect(address)).await
1323        {
1324            Ok(Ok(stream)) => stream,
1325            Ok(Err(_)) => {
1326                self.audit
1327                    .fail(WebSocketReviewFailureKind::ConnectionFailed);
1328                return TransportDispatchOutcome::TransportFailure;
1329            },
1330            Err(StepStop::Cancelled) => {
1331                self.audit.cancel();
1332                return TransportDispatchOutcome::Cancelled;
1333            },
1334            Err(StepStop::Deadline) => {
1335                self.audit.deadline();
1336                return TransportDispatchOutcome::RequestTimeout;
1337            },
1338        };
1339
1340        let (stream, counters) = CountingStream::new(stream);
1341        let read_guard =
1342            match bounded(&self.cancellation, deadline, lease.acquire_response_read()).await {
1343                Ok(guard) => guard,
1344                Err(StepStop::Cancelled) => {
1345                    self.audit.cancel();
1346                    return TransportDispatchOutcome::Cancelled;
1347                },
1348                Err(StepStop::Deadline) => {
1349                    self.audit.deadline();
1350                    return TransportDispatchOutcome::RequestTimeout;
1351                },
1352            };
1353        if lease.remaining_response_bytes() == 0 {
1354            drop(read_guard);
1355            self.audit
1356                .fail(WebSocketReviewFailureKind::ParentResponseBudgetReached);
1357            return TransportDispatchOutcome::ResponseBudgetReached;
1358        }
1359        let before = counters.snapshot();
1360        let connected = bounded(
1361            &self.cancellation,
1362            deadline,
1363            client_async_tls_with_config(request, stream, Some(websocket_config), tls_connector),
1364        )
1365        .await;
1366        let accounting = self.observe_transport_delta(lease, &counters, before);
1367        drop(read_guard);
1368        if let Err(outcome) = accounting {
1369            return outcome;
1370        }
1371        let connected = match connected {
1372            Ok(Ok(connected)) => connected,
1373            Ok(Err(error)) => {
1374                self.audit.fail(classify_connect_error(&error));
1375                return TransportDispatchOutcome::TransportFailure;
1376            },
1377            Err(StepStop::Cancelled) => {
1378                self.audit.cancel();
1379                return TransportDispatchOutcome::Cancelled;
1380            },
1381            Err(StepStop::Deadline) => {
1382                self.audit.deadline();
1383                return TransportDispatchOutcome::RequestTimeout;
1384            },
1385        };
1386        let (mut socket, response) = connected;
1387        self.audit.handshake_completed_count = 1;
1388        if response.headers().contains_key("sec-websocket-extensions") {
1389            self.audit
1390                .fail(WebSocketReviewFailureKind::ExtensionNegotiationRefused);
1391            return self
1392                .close_after_failure(
1393                    &mut socket,
1394                    lease,
1395                    &counters,
1396                    deadline,
1397                    TransportDispatchOutcome::TransportFailure,
1398                )
1399                .await;
1400        }
1401        if !subprotocol_matches(&response, self.subprotocol.as_deref()) {
1402            self.audit
1403                .fail(WebSocketReviewFailureKind::SubprotocolNegotiationRefused);
1404            return self
1405                .close_after_failure(
1406                    &mut socket,
1407                    lease,
1408                    &counters,
1409                    deadline,
1410                    TransportDispatchOutcome::TransportFailure,
1411                )
1412                .await;
1413        }
1414        self.audit.subprotocol_negotiated = self.subprotocol.is_some();
1415
1416        for index in 0..self.messages.len() {
1417            if self.audit.outbound_message_count >= self.audit.limits.max_messages {
1418                self.audit
1419                    .fail(WebSocketReviewFailureKind::MessageLimitReached);
1420                return self
1421                    .close_after_failure(
1422                        &mut socket,
1423                        lease,
1424                        &counters,
1425                        deadline,
1426                        TransportDispatchOutcome::TransportFailure,
1427                    )
1428                    .await;
1429            }
1430            let text = self.messages[index].text.clone();
1431            let read_guard =
1432                match bounded(&self.cancellation, deadline, lease.acquire_response_read()).await {
1433                    Ok(guard) => guard,
1434                    Err(StepStop::Cancelled) => {
1435                        self.audit.cancel();
1436                        return TransportDispatchOutcome::Cancelled;
1437                    },
1438                    Err(StepStop::Deadline) => {
1439                        self.audit.deadline();
1440                        return TransportDispatchOutcome::RequestTimeout;
1441                    },
1442                };
1443            if lease.remaining_response_bytes() == 0 {
1444                drop(read_guard);
1445                self.audit
1446                    .fail(WebSocketReviewFailureKind::ParentResponseBudgetReached);
1447                return TransportDispatchOutcome::ResponseBudgetReached;
1448            }
1449            let before = counters.snapshot();
1450            let sent = bounded(
1451                &self.cancellation,
1452                deadline,
1453                socket.send(Message::text(text)),
1454            )
1455            .await;
1456            let accounting = self.observe_transport_delta(lease, &counters, before);
1457            drop(read_guard);
1458            if let Err(outcome) = accounting {
1459                return outcome;
1460            }
1461            match sent {
1462                Ok(Ok(())) => {
1463                    self.audit.outbound_message_count += 1;
1464                    self.audit.outbound_application_bytes = self
1465                        .audit
1466                        .outbound_application_bytes
1467                        .saturating_add(self.audit.messages[index].outbound_length);
1468                    self.audit.messages[index].status =
1469                        WebSocketReviewMessageStatus::SentNoResponse;
1470                },
1471                Ok(Err(_)) => {
1472                    self.audit.fail(WebSocketReviewFailureKind::SendFailed);
1473                    return TransportDispatchOutcome::TransportFailure;
1474                },
1475                Err(StepStop::Cancelled) => {
1476                    self.audit.cancel();
1477                    return TransportDispatchOutcome::Cancelled;
1478                },
1479                Err(StepStop::Deadline) => {
1480                    self.audit.deadline();
1481                    return TransportDispatchOutcome::RequestTimeout;
1482                },
1483            }
1484            let read_outcome = self
1485                .read_application_response(&mut socket, lease, &counters, deadline, index)
1486                .await;
1487            if let Err(outcome) = read_outcome {
1488                return outcome;
1489            }
1490        }
1491
1492        match self
1493            .send_close(
1494                &mut socket,
1495                lease,
1496                &counters,
1497                deadline,
1498                WebSocketReviewFailureKind::CloseFailed,
1499            )
1500            .await
1501        {
1502            Ok(()) => {
1503                self.audit.terminal = WebSocketReviewTerminal::Completed;
1504                self.audit.failure = None;
1505                TransportDispatchOutcome::Completed
1506            },
1507            Err(outcome) => outcome,
1508        }
1509    }
1510
1511    async fn read_application_response(
1512        &mut self,
1513        socket: &mut CountedWebSocket,
1514        lease: &mut RequestAccountingLease,
1515        counters: &TransportByteCounters,
1516        deadline: tokio::time::Instant,
1517        index: usize,
1518    ) -> Result<(), TransportDispatchOutcome> {
1519        loop {
1520            let read_guard =
1521                match bounded(&self.cancellation, deadline, lease.acquire_response_read()).await {
1522                    Ok(guard) => guard,
1523                    Err(StepStop::Cancelled) => {
1524                        self.audit.cancel();
1525                        return Err(TransportDispatchOutcome::Cancelled);
1526                    },
1527                    Err(StepStop::Deadline) => {
1528                        self.audit.deadline();
1529                        return Err(TransportDispatchOutcome::RequestTimeout);
1530                    },
1531                };
1532            if lease.remaining_response_bytes() == 0 {
1533                drop(read_guard);
1534                self.audit
1535                    .fail(WebSocketReviewFailureKind::ParentResponseBudgetReached);
1536                return Err(TransportDispatchOutcome::ResponseBudgetReached);
1537            }
1538            let before = counters.snapshot();
1539            let next = bounded(&self.cancellation, deadline, socket.next()).await;
1540            let accounting = self.observe_transport_delta(lease, counters, before);
1541            drop(read_guard);
1542            accounting?;
1543            let message = match next {
1544                Ok(Some(Ok(message))) => message,
1545                Ok(Some(Err(TungsteniteError::Capacity(_)))) => {
1546                    self.audit.fail(WebSocketReviewFailureKind::MessageTooLarge);
1547                    return Err(TransportDispatchOutcome::TransportFailure);
1548                },
1549                Ok(Some(Err(_))) => {
1550                    self.audit.fail(WebSocketReviewFailureKind::ReadFailed);
1551                    return Err(TransportDispatchOutcome::TransportFailure);
1552                },
1553                Ok(None) => {
1554                    self.audit.fail(WebSocketReviewFailureKind::PeerClosed);
1555                    return Err(TransportDispatchOutcome::TransportFailure);
1556                },
1557                Err(StepStop::Cancelled) => {
1558                    self.audit.cancel();
1559                    return Err(TransportDispatchOutcome::Cancelled);
1560                },
1561                Err(StepStop::Deadline) => {
1562                    self.audit.deadline();
1563                    return Err(TransportDispatchOutcome::RequestTimeout);
1564                },
1565            };
1566            match message {
1567                Message::Text(text) => {
1568                    return self.observe_application_payload(index, text.as_bytes());
1569                },
1570                Message::Binary(_) => {
1571                    self.audit
1572                        .fail(WebSocketReviewFailureKind::BinaryMessageUnsupported);
1573                    return Err(TransportDispatchOutcome::TransportFailure);
1574                },
1575                Message::Ping(_) => {
1576                    self.observe_inbound_control_and_flush(socket, lease, counters, deadline)
1577                        .await?;
1578                },
1579                Message::Pong(_) => {
1580                    self.observe_inbound_control()?;
1581                },
1582                Message::Close(_) => {
1583                    self.observe_inbound_control()?;
1584                    if self
1585                        .audit
1586                        .outbound_control_frame_count
1587                        .saturating_add(self.audit.inbound_control_frame_count)
1588                        < self.audit.limits.max_control_frames
1589                    {
1590                        let read_guard = match bounded(
1591                            &self.cancellation,
1592                            deadline,
1593                            lease.acquire_response_read(),
1594                        )
1595                        .await
1596                        {
1597                            Ok(guard) => guard,
1598                            Err(StepStop::Cancelled) => {
1599                                self.audit.cancel();
1600                                return Err(TransportDispatchOutcome::Cancelled);
1601                            },
1602                            Err(StepStop::Deadline) => {
1603                                self.audit.deadline();
1604                                return Err(TransportDispatchOutcome::RequestTimeout);
1605                            },
1606                        };
1607                        let before = counters.snapshot();
1608                        let flushed = bounded(&self.cancellation, deadline, socket.flush()).await;
1609                        let accounting = self.observe_transport_delta(lease, counters, before);
1610                        drop(read_guard);
1611                        accounting?;
1612                        match flushed {
1613                            Ok(Ok(())) => {
1614                                self.audit.outbound_control_frame_count += 1;
1615                            },
1616                            Ok(Err(_)) => {},
1617                            Err(StepStop::Cancelled) => {
1618                                self.audit.cancel();
1619                                return Err(TransportDispatchOutcome::Cancelled);
1620                            },
1621                            Err(StepStop::Deadline) => {
1622                                self.audit.deadline();
1623                                return Err(TransportDispatchOutcome::RequestTimeout);
1624                            },
1625                        }
1626                    }
1627                    self.audit.fail(WebSocketReviewFailureKind::PeerClosed);
1628                    return Err(TransportDispatchOutcome::TransportFailure);
1629                },
1630                Message::Frame(_) => {
1631                    self.audit
1632                        .fail(WebSocketReviewFailureKind::InternalInvariant);
1633                    return Err(TransportDispatchOutcome::TransportFailure);
1634                },
1635            }
1636        }
1637    }
1638
1639    fn observe_application_payload(
1640        &mut self,
1641        index: usize,
1642        payload: &[u8],
1643    ) -> Result<(), TransportDispatchOutcome> {
1644        if self.audit.inbound_message_count >= self.audit.limits.max_messages {
1645            self.audit
1646                .fail(WebSocketReviewFailureKind::MessageLimitReached);
1647            return Err(TransportDispatchOutcome::TransportFailure);
1648        }
1649        let payload_length = u64::try_from(payload.len()).unwrap_or(u64::MAX);
1650        let next_total = self
1651            .audit
1652            .inbound_application_bytes
1653            .saturating_add(payload_length);
1654        if payload_length > self.audit.limits.max_inbound_message_bytes
1655            || next_total > self.audit.limits.max_inbound_application_bytes
1656        {
1657            self.audit.fail(
1658                if payload_length > self.audit.limits.max_inbound_message_bytes {
1659                    WebSocketReviewFailureKind::MessageTooLarge
1660                } else {
1661                    WebSocketReviewFailureKind::ApplicationByteLimitReached
1662                },
1663            );
1664            return Err(TransportDispatchOutcome::TransportFailure);
1665        }
1666        self.audit.inbound_message_count += 1;
1667        self.audit.inbound_application_bytes = next_total;
1668        let observed_digest: [u8; 32] = Sha256::digest(payload).into();
1669        let matched = payload_length == self.messages[index].expected_response_length
1670            && observed_digest == self.messages[index].expected_response_sha256;
1671        let message = &mut self.audit.messages[index];
1672        message.inbound_length = Some(payload_length);
1673        message.observed_response_reference =
1674            matched.then(|| message.expected_response_reference.clone());
1675        message.status = if matched {
1676            WebSocketReviewMessageStatus::ResponseMatched
1677        } else {
1678            WebSocketReviewMessageStatus::ResponseMismatched
1679        };
1680        Ok(())
1681    }
1682
1683    fn observe_inbound_control(&mut self) -> Result<(), TransportDispatchOutcome> {
1684        if self
1685            .audit
1686            .inbound_control_frame_count
1687            .saturating_add(self.audit.outbound_control_frame_count)
1688            >= self.audit.limits.max_control_frames
1689        {
1690            self.audit
1691                .fail(WebSocketReviewFailureKind::ControlFrameLimitReached);
1692            return Err(TransportDispatchOutcome::TransportFailure);
1693        }
1694        self.audit.inbound_control_frame_count += 1;
1695        Ok(())
1696    }
1697
1698    async fn observe_inbound_control_and_flush(
1699        &mut self,
1700        socket: &mut CountedWebSocket,
1701        lease: &mut RequestAccountingLease,
1702        counters: &TransportByteCounters,
1703        deadline: tokio::time::Instant,
1704    ) -> Result<(), TransportDispatchOutcome> {
1705        self.observe_inbound_control()?;
1706        if self
1707            .audit
1708            .outbound_control_frame_count
1709            .saturating_add(self.audit.inbound_control_frame_count)
1710            >= self.audit.limits.max_control_frames
1711        {
1712            self.audit
1713                .fail(WebSocketReviewFailureKind::ControlFrameLimitReached);
1714            return Err(TransportDispatchOutcome::TransportFailure);
1715        }
1716        let read_guard =
1717            match bounded(&self.cancellation, deadline, lease.acquire_response_read()).await {
1718                Ok(guard) => guard,
1719                Err(StepStop::Cancelled) => {
1720                    self.audit.cancel();
1721                    return Err(TransportDispatchOutcome::Cancelled);
1722                },
1723                Err(StepStop::Deadline) => {
1724                    self.audit.deadline();
1725                    return Err(TransportDispatchOutcome::RequestTimeout);
1726                },
1727            };
1728        let before = counters.snapshot();
1729        let flushed = bounded(&self.cancellation, deadline, socket.flush()).await;
1730        let accounting = self.observe_transport_delta(lease, counters, before);
1731        drop(read_guard);
1732        accounting?;
1733        match flushed {
1734            Ok(Ok(())) => {
1735                self.audit.outbound_control_frame_count += 1;
1736                Ok(())
1737            },
1738            Ok(Err(_)) => {
1739                self.audit.fail(WebSocketReviewFailureKind::SendFailed);
1740                Err(TransportDispatchOutcome::TransportFailure)
1741            },
1742            Err(StepStop::Cancelled) => {
1743                self.audit.cancel();
1744                Err(TransportDispatchOutcome::Cancelled)
1745            },
1746            Err(StepStop::Deadline) => {
1747                self.audit.deadline();
1748                Err(TransportDispatchOutcome::RequestTimeout)
1749            },
1750        }
1751    }
1752
1753    async fn send_close(
1754        &mut self,
1755        socket: &mut CountedWebSocket,
1756        lease: &mut RequestAccountingLease,
1757        counters: &TransportByteCounters,
1758        deadline: tokio::time::Instant,
1759        failure: WebSocketReviewFailureKind,
1760    ) -> Result<(), TransportDispatchOutcome> {
1761        if self
1762            .audit
1763            .outbound_control_frame_count
1764            .saturating_add(self.audit.inbound_control_frame_count)
1765            >= self.audit.limits.max_control_frames
1766        {
1767            self.audit
1768                .fail(WebSocketReviewFailureKind::ControlFrameLimitReached);
1769            return Err(TransportDispatchOutcome::TransportFailure);
1770        }
1771        let read_guard =
1772            match bounded(&self.cancellation, deadline, lease.acquire_response_read()).await {
1773                Ok(guard) => guard,
1774                Err(StepStop::Cancelled) => {
1775                    self.audit.cancel();
1776                    return Err(TransportDispatchOutcome::Cancelled);
1777                },
1778                Err(StepStop::Deadline) => {
1779                    self.audit.deadline();
1780                    return Err(TransportDispatchOutcome::RequestTimeout);
1781                },
1782            };
1783        let before = counters.snapshot();
1784        let closed = bounded(&self.cancellation, deadline, socket.close(None)).await;
1785        let accounting = self.observe_transport_delta(lease, counters, before);
1786        drop(read_guard);
1787        accounting?;
1788        match closed {
1789            Ok(Ok(())) => {
1790                self.audit.outbound_control_frame_count += 1;
1791                Ok(())
1792            },
1793            Ok(Err(_)) => {
1794                self.audit.fail(failure);
1795                Err(TransportDispatchOutcome::TransportFailure)
1796            },
1797            Err(StepStop::Cancelled) => {
1798                self.audit.cancel();
1799                Err(TransportDispatchOutcome::Cancelled)
1800            },
1801            Err(StepStop::Deadline) => {
1802                self.audit.deadline();
1803                Err(TransportDispatchOutcome::RequestTimeout)
1804            },
1805        }
1806    }
1807
1808    fn observe_transport_delta(
1809        &mut self,
1810        lease: &mut RequestAccountingLease,
1811        counters: &TransportByteCounters,
1812        before: TransportByteSnapshot,
1813    ) -> Result<(), TransportDispatchOutcome> {
1814        let delta = counters.snapshot().delta_from(before);
1815        self.audit.transport_read_bytes =
1816            self.audit.transport_read_bytes.saturating_add(delta.read);
1817        self.audit.transport_write_bytes = self
1818            .audit
1819            .transport_write_bytes
1820            .saturating_add(delta.written);
1821        if delta.read == 0 {
1822            return Ok(());
1823        }
1824        let retained = lease.observe_response_bytes(delta.read);
1825        self.audit.accounted_response_bytes = self
1826            .audit
1827            .accounted_response_bytes
1828            .saturating_add(delta.read);
1829        if retained != delta.read || lease.response_budget_reached() {
1830            self.audit
1831                .fail(WebSocketReviewFailureKind::ParentResponseBudgetReached);
1832            return Err(TransportDispatchOutcome::ResponseBudgetReached);
1833        }
1834        Ok(())
1835    }
1836
1837    async fn close_after_failure(
1838        &mut self,
1839        socket: &mut CountedWebSocket,
1840        lease: &mut RequestAccountingLease,
1841        counters: &TransportByteCounters,
1842        deadline: tokio::time::Instant,
1843        outcome: TransportDispatchOutcome,
1844    ) -> TransportDispatchOutcome {
1845        if self
1846            .audit
1847            .outbound_control_frame_count
1848            .saturating_add(self.audit.inbound_control_frame_count)
1849            >= self.audit.limits.max_control_frames
1850        {
1851            return outcome;
1852        }
1853        let read_guard =
1854            match bounded(&self.cancellation, deadline, lease.acquire_response_read()).await {
1855                Ok(guard) => guard,
1856                Err(StepStop::Cancelled) => {
1857                    self.audit.cancel();
1858                    return TransportDispatchOutcome::Cancelled;
1859                },
1860                Err(StepStop::Deadline) => {
1861                    self.audit.deadline();
1862                    return TransportDispatchOutcome::RequestTimeout;
1863                },
1864            };
1865        let before = counters.snapshot();
1866        let closed = bounded(&self.cancellation, deadline, socket.close(None)).await;
1867        let accounting = self.observe_transport_delta(lease, counters, before);
1868        drop(read_guard);
1869        if let Err(accounting_outcome) = accounting {
1870            return accounting_outcome;
1871        }
1872        match closed {
1873            Ok(Ok(())) => {
1874                self.audit.outbound_control_frame_count += 1;
1875                outcome
1876            },
1877            Ok(Err(_)) => outcome,
1878            Err(StepStop::Cancelled) => {
1879                self.audit.cancel();
1880                TransportDispatchOutcome::Cancelled
1881            },
1882            Err(StepStop::Deadline) => {
1883                self.audit.deadline();
1884                TransportDispatchOutcome::RequestTimeout
1885            },
1886        }
1887    }
1888
1889    fn client_request(
1890        &self,
1891    ) -> Result<tokio_tungstenite::tungstenite::http::Request<()>, WebSocketReviewFailureKind> {
1892        let mut request = self
1893            .endpoint
1894            .as_str()
1895            .into_client_request()
1896            .map_err(|_| WebSocketReviewFailureKind::RequestConstruction)?;
1897        let headers = request.headers_mut();
1898        for name in [
1899            "authorization",
1900            "proxy-authorization",
1901            "cookie",
1902            "sec-websocket-extensions",
1903        ] {
1904            headers.remove(name);
1905        }
1906        if self.origin_mode == WebSocketOriginMode::ApplicationOrigin {
1907            let origin = HeaderValue::from_str(&self.application_origin)
1908                .map_err(|_| WebSocketReviewFailureKind::RequestConstruction)?;
1909            headers.insert("origin", origin);
1910        } else {
1911            headers.remove("origin");
1912        }
1913        if let Some(subprotocol) = self.subprotocol.as_deref() {
1914            let value = HeaderValue::from_str(subprotocol)
1915                .map_err(|_| WebSocketReviewFailureKind::RequestConstruction)?;
1916            headers.insert("sec-websocket-protocol", value);
1917        } else {
1918            headers.remove("sec-websocket-protocol");
1919        }
1920        Ok(request)
1921    }
1922
1923    #[cfg(feature = "supplied-session-review")]
1924    fn client_request_with_supplied_authorization(
1925        &self,
1926        authorization: &SuppliedSessionAuthorization,
1927    ) -> Result<tokio_tungstenite::tungstenite::http::Request<()>, WebSocketReviewFailureKind> {
1928        let mut request = self.client_request()?;
1929        let mut value = HeaderValue::from_str(authorization.as_str())
1930            .map_err(|_| WebSocketReviewFailureKind::RequestConstruction)?;
1931        value.set_sensitive(true);
1932        request.headers_mut().insert("authorization", value);
1933        Ok(request)
1934    }
1935}
1936
1937// Allocate the child before polling it, including its construction copies. The
1938// immediate factory retains no independent task, authority, lease or retry loop.
1939#[inline(never)]
1940fn boxed_websocket_child<F: std::future::Future>(
1941    make: impl FnOnce() -> F,
1942) -> std::pin::Pin<Box<F>> {
1943    Box::pin(make())
1944}
1945
1946/// Executes one selected policy or returns a value-free authority refusal audit.
1947pub(crate) async fn execute_websocket_review(
1948    authority: &SharedWebRuntimeAuthority,
1949    policy: &WebSocketReviewPolicy,
1950) -> WebAssessmentWebSocketReviewAudit {
1951    match authority.mint_websocket_review(policy) {
1952        Ok(runtime) => boxed_websocket_child(|| runtime.execute()).await,
1953        Err(error) => {
1954            let audit = WebSocketReviewRuntime::new(
1955                policy,
1956                String::new(),
1957                authority.request_accounting().clone(),
1958                authority.cancellation_token(),
1959                authority.start().deadline(),
1960            );
1961            match audit {
1962                Ok(runtime) => {
1963                    let mut audit = runtime.audit;
1964                    audit.fail(error.failure());
1965                    audit
1966                },
1967                Err(_) => fallback_audit(policy, error.failure()),
1968            }
1969        },
1970    }
1971}
1972
1973/// Executes one WebSocket review with the exact Authorization value borrowed
1974/// from a sealed, committed supplied-session startup context.
1975#[cfg(feature = "supplied-session-review")]
1976pub(super) async fn execute_supplied_session_websocket_review(
1977    authority: &SharedWebRuntimeAuthority,
1978    policy: &WebSocketReviewPolicy,
1979    permit: &SuppliedSessionWebSocketPermit<'_>,
1980) -> WebAssessmentWebSocketReviewAudit {
1981    match authority.mint_supplied_session_websocket(policy, permit) {
1982        Ok(runtime) => {
1983            boxed_websocket_child(|| {
1984                runtime.execute_with_supplied_authorization(permit.authorization())
1985            })
1986            .await
1987        },
1988        Err(error) => {
1989            let audit = WebSocketReviewRuntime::new(
1990                policy,
1991                String::new(),
1992                authority.request_accounting().clone(),
1993                authority.cancellation_token(),
1994                authority.start().deadline(),
1995            );
1996            match audit {
1997                Ok(runtime) => {
1998                    let mut audit = runtime.audit;
1999                    audit.fail(error.failure());
2000                    audit
2001                },
2002                Err(_) => fallback_audit(policy, error.failure()),
2003            }
2004        },
2005    }
2006}
2007
2008/// Produces a selected, payload-free audit when the parent assessment stops
2009/// before WebSocket dispatch is eligible to begin.
2010pub(crate) fn websocket_review_not_run(
2011    policy: &WebSocketReviewPolicy,
2012    failure: WebSocketReviewFailureKind,
2013) -> WebAssessmentWebSocketReviewAudit {
2014    fallback_audit(policy, failure)
2015}
2016
2017fn fallback_audit(
2018    policy: &WebSocketReviewPolicy,
2019    failure: WebSocketReviewFailureKind,
2020) -> WebAssessmentWebSocketReviewAudit {
2021    let mut messages = Vec::new();
2022    for message in policy
2023        .execution_messages()
2024        .take(MAX_WEBSOCKET_REVIEW_MESSAGES)
2025    {
2026        messages.push(WebSocketReviewMessageAudit {
2027            message_reference: message.message_reference().to_owned(),
2028            expected_response_reference: message.expected_response_reference().to_owned(),
2029            observed_response_reference: None,
2030            outbound_length: message.outbound_length(),
2031            expected_response_length: message.expected_response_length(),
2032            inbound_length: None,
2033            status: WebSocketReviewMessageStatus::NotSent,
2034        });
2035    }
2036    let mut audit = WebAssessmentWebSocketReviewAudit {
2037        policy_reference: policy.policy_reference().to_owned(),
2038        endpoint_reference: policy.endpoint_reference().to_owned(),
2039        origin_mode: policy.origin_mode(),
2040        subprotocol_requested: policy.execution_subprotocol().is_some(),
2041        subprotocol_negotiated: false,
2042        request_attempt_count: 0,
2043        request_admitted_count: 0,
2044        connection_attempt_count: 0,
2045        handshake_completed_count: 0,
2046        configured_message_count: u64::try_from(messages.len()).unwrap_or(u64::MAX),
2047        outbound_message_count: 0,
2048        inbound_message_count: 0,
2049        outbound_control_frame_count: 0,
2050        inbound_control_frame_count: 0,
2051        outbound_application_bytes: 0,
2052        inbound_application_bytes: 0,
2053        transport_read_bytes: 0,
2054        transport_write_bytes: 0,
2055        accounted_request_body_bytes: 0,
2056        accounted_response_bytes: 0,
2057        terminal: failure.terminal(),
2058        failure: Some(failure),
2059        messages,
2060        limits: WebSocketReviewExecutionLimits {
2061            max_connections: 1,
2062            max_outbound_application_bytes: MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION,
2063            max_inbound_application_bytes: MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION,
2064            max_outbound_message_bytes: policy.max_outbound_message_bytes(),
2065            max_inbound_message_bytes: policy.max_inbound_message_bytes(),
2066            max_messages: u64::from(policy.max_messages()),
2067            max_control_frames: u64::from(policy.max_control_frames()),
2068            max_wall_time_ms: policy.max_wall_time_ms(),
2069        },
2070        #[cfg(feature = "supplied-session-review")]
2071        supplied_session_context: None,
2072    };
2073    audit.fail(failure);
2074    audit
2075}
2076
2077fn websocket_root_store() -> RootCertStore {
2078    RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned())
2079}
2080
2081fn websocket_tls_connector(
2082    endpoint: &url::Url,
2083) -> Result<Option<Connector>, WebSocketReviewFailureKind> {
2084    if endpoint.scheme() != "wss" {
2085        return Ok(None);
2086    }
2087    // Preserve the existing Mozilla Web-PKI trust and no-client-auth policy,
2088    // while selecting the reviewed crypto provider and current roots directly.
2089    // This does not install process-global providers or consult native roots.
2090    let config = websocket_tls_config(rustls::crypto::ring::default_provider())?;
2091    Ok(Some(Connector::Rustls(Arc::new(config))))
2092}
2093
2094fn websocket_tls_config(
2095    provider: rustls::crypto::CryptoProvider,
2096) -> Result<ClientConfig, WebSocketReviewFailureKind> {
2097    Ok(ClientConfig::builder_with_provider(Arc::new(provider))
2098        .with_safe_default_protocol_versions()
2099        .map_err(|_| WebSocketReviewFailureKind::InternalInvariant)?
2100        .with_root_certificates(websocket_root_store())
2101        .with_no_client_auth())
2102}
2103
2104fn classify_connect_error(error: &TungsteniteError) -> WebSocketReviewFailureKind {
2105    match error {
2106        TungsteniteError::Http(_) => WebSocketReviewFailureKind::HandshakeRejected,
2107        TungsteniteError::Capacity(_) => WebSocketReviewFailureKind::MessageTooLarge,
2108        TungsteniteError::Protocol(_) | TungsteniteError::HttpFormat(_) => {
2109            WebSocketReviewFailureKind::HandshakeRejected
2110        },
2111        _ => WebSocketReviewFailureKind::ConnectionFailed,
2112    }
2113}
2114
2115fn subprotocol_matches<B>(response: &Response<B>, expected: Option<&str>) -> bool {
2116    let observed = response
2117        .headers()
2118        .get("sec-websocket-protocol")
2119        .and_then(|value| value.to_str().ok());
2120    observed == expected
2121}
2122
2123fn lesser_deadline(
2124    first: Option<tokio::time::Instant>,
2125    second: Option<tokio::time::Instant>,
2126) -> Option<tokio::time::Instant> {
2127    match (first, second) {
2128        (Some(first), Some(second)) => Some(first.min(second)),
2129        (Some(deadline), None) | (None, Some(deadline)) => Some(deadline),
2130        (None, None) => None,
2131    }
2132}
2133
2134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2135enum StepStop {
2136    Cancelled,
2137    Deadline,
2138}
2139
2140async fn bounded<F>(
2141    cancellation: &CancellationToken,
2142    deadline: tokio::time::Instant,
2143    future: F,
2144) -> Result<F::Output, StepStop>
2145where
2146    F: Future,
2147{
2148    tokio::pin!(future);
2149    tokio::select! {
2150        biased;
2151        _ = cancellation.cancelled() => Err(StepStop::Cancelled),
2152        _ = tokio::time::sleep_until(deadline) => Err(StepStop::Deadline),
2153        output = &mut future => Ok(output),
2154    }
2155}
2156
2157#[cfg(test)]
2158mod tests {
2159    use futures::{SinkExt, StreamExt};
2160    use serde_json::json;
2161    use sha2::{Digest, Sha256};
2162    use tokio::{
2163        io::{AsyncReadExt, AsyncWriteExt},
2164        net::TcpListener,
2165    };
2166
2167    use super::*;
2168    use crate::{websocket_review::WebSocketReviewPolicy, RuntimeBudget};
2169
2170    #[test]
2171    fn atomic_saturating_transport_add_preserves_zero_max_and_concurrent_updates() {
2172        let counter = AtomicU64::new(0);
2173        saturating_atomic_add(&counter, 0);
2174        assert_eq!(counter.load(Ordering::Relaxed), 0);
2175        saturating_atomic_add(&counter, 7);
2176        assert_eq!(counter.load(Ordering::Relaxed), 7);
2177        counter.store(u64::MAX - 3, Ordering::Relaxed);
2178        saturating_atomic_add(&counter, 2);
2179        assert_eq!(counter.load(Ordering::Relaxed), u64::MAX - 1);
2180        saturating_atomic_add(&counter, 2);
2181        saturating_atomic_add(&counter, 0);
2182        saturating_atomic_add(&counter, u64::MAX);
2183        assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
2184
2185        counter.store(0, Ordering::Relaxed);
2186        std::thread::scope(|scope| {
2187            for _ in 0..4 {
2188                scope.spawn(|| {
2189                    for _ in 0..1000 {
2190                        saturating_atomic_add(&counter, 3);
2191                    }
2192                });
2193            }
2194        });
2195        assert_eq!(counter.load(Ordering::Relaxed), 12000);
2196    }
2197
2198    fn loopback_policy(
2199        application: &url::Url,
2200        endpoint: &url::Url,
2201        expected_response: &[u8],
2202        max_wall_time_ms: u64,
2203    ) -> WebSocketReviewPolicy {
2204        loopback_policy_with_options(
2205            application,
2206            endpoint,
2207            expected_response,
2208            max_wall_time_ms,
2209            None,
2210            4,
2211            4096,
2212        )
2213    }
2214
2215    fn loopback_policy_with_options(
2216        application: &url::Url,
2217        endpoint: &url::Url,
2218        expected_response: &[u8],
2219        max_wall_time_ms: u64,
2220        subprotocol: Option<&str>,
2221        max_control_frames: u16,
2222        max_inbound_message_bytes: u64,
2223    ) -> WebSocketReviewPolicy {
2224        let expected_sha256 = format!("{:x}", Sha256::digest(expected_response));
2225        let source = serde_json::to_vec(&json!({
2226            "schema": "security.websocket-review-policy/v1",
2227            "target_authorized": true,
2228            "messages_read_only_acknowledged": true,
2229            "message_content_is_non_secret": true,
2230            "endpoint": endpoint.as_str(),
2231            "origin_mode": "application_origin",
2232            "subprotocol": subprotocol,
2233            "compression": false,
2234            "reconnect": false,
2235            "messages": [{
2236                "id": "loopback-message",
2237                "text": "request",
2238                "expected_response": {
2239                    "sha256": expected_sha256,
2240                    "length": expected_response.len()
2241                }
2242            }],
2243            "limits": {
2244                "max_inbound_message_bytes": max_inbound_message_bytes,
2245                "max_outbound_message_bytes": 4096,
2246                "max_messages": 1,
2247                "max_control_frames": max_control_frames,
2248                "max_wall_time_ms": max_wall_time_ms
2249            }
2250        }))
2251        .unwrap();
2252        WebSocketReviewPolicy::parse_json(application, &source).unwrap()
2253    }
2254
2255    fn runtime(
2256        policy: &WebSocketReviewPolicy,
2257        application: &url::Url,
2258        accounting: RequestAccountingBroker,
2259    ) -> WebSocketReviewRuntime {
2260        WebSocketReviewRuntime::new(
2261            policy,
2262            application.origin().ascii_serialization(),
2263            accounting,
2264            CancellationToken::new(),
2265            None,
2266        )
2267        .unwrap()
2268    }
2269
2270    async fn await_server(server: tokio::task::JoinHandle<()>) {
2271        tokio::time::timeout(Duration::from_secs(5), server)
2272            .await
2273            .expect("loopback server did not finish within the bounded test window")
2274            .expect("loopback server task failed");
2275    }
2276
2277    #[test]
2278    fn audit_vocabulary_fallback_and_public_accessors_are_total() {
2279        assert_eq!(
2280            WebSocketReviewCompleteness::ConfiguredExchangeComplete.as_str(),
2281            "configured_exchange_complete"
2282        );
2283        assert_eq!(
2284            WebSocketReviewCompleteness::Incomplete.as_str(),
2285            "incomplete"
2286        );
2287        for (terminal, token) in [
2288            (WebSocketReviewTerminal::Completed, "completed"),
2289            (WebSocketReviewTerminal::Incomplete, "incomplete"),
2290            (WebSocketReviewTerminal::Cancelled, "cancelled"),
2291            (WebSocketReviewTerminal::DeadlineReached, "deadline_reached"),
2292            (WebSocketReviewTerminal::LimitReached, "limit_reached"),
2293            (
2294                WebSocketReviewTerminal::AuthorityRefused,
2295                "authority_refused",
2296            ),
2297        ] {
2298            assert_eq!(terminal.as_str(), token);
2299        }
2300        let failures = [
2301            (
2302                WebSocketReviewFailureKind::AuthorityAlreadyMinted,
2303                "authority_already_minted",
2304            ),
2305            (
2306                WebSocketReviewFailureKind::EndpointOutsideAuthority,
2307                "endpoint_outside_authority",
2308            ),
2309            (
2310                WebSocketReviewFailureKind::ParentAuthorityUnavailable,
2311                "parent_authority_unavailable",
2312            ),
2313            (
2314                WebSocketReviewFailureKind::RequestConstruction,
2315                "request_construction",
2316            ),
2317            (
2318                WebSocketReviewFailureKind::ConnectionFailed,
2319                "connection_failed",
2320            ),
2321            (
2322                WebSocketReviewFailureKind::HandshakeRejected,
2323                "handshake_rejected",
2324            ),
2325            (
2326                WebSocketReviewFailureKind::ExtensionNegotiationRefused,
2327                "extension_negotiation_refused",
2328            ),
2329            (
2330                WebSocketReviewFailureKind::SubprotocolNegotiationRefused,
2331                "subprotocol_negotiation_refused",
2332            ),
2333            (WebSocketReviewFailureKind::SendFailed, "send_failed"),
2334            (WebSocketReviewFailureKind::ReadFailed, "read_failed"),
2335            (
2336                WebSocketReviewFailureKind::BinaryMessageUnsupported,
2337                "binary_message_unsupported",
2338            ),
2339            (WebSocketReviewFailureKind::CloseFailed, "close_failed"),
2340            (WebSocketReviewFailureKind::PeerClosed, "peer_closed"),
2341            (
2342                WebSocketReviewFailureKind::MessageTooLarge,
2343                "message_too_large",
2344            ),
2345            (
2346                WebSocketReviewFailureKind::MessageLimitReached,
2347                "message_limit_reached",
2348            ),
2349            (
2350                WebSocketReviewFailureKind::ControlFrameLimitReached,
2351                "control_frame_limit_reached",
2352            ),
2353            (
2354                WebSocketReviewFailureKind::ApplicationByteLimitReached,
2355                "application_byte_limit_reached",
2356            ),
2357            (
2358                WebSocketReviewFailureKind::ParentResponseBudgetReached,
2359                "parent_response_budget_reached",
2360            ),
2361            (WebSocketReviewFailureKind::Cancelled, "cancelled"),
2362            (
2363                WebSocketReviewFailureKind::DeadlineReached,
2364                "deadline_reached",
2365            ),
2366            (
2367                WebSocketReviewFailureKind::InternalInvariant,
2368                "internal_invariant",
2369            ),
2370        ];
2371        for (failure, token) in failures {
2372            assert_eq!(failure.as_str(), token);
2373            let expected_terminal = match failure {
2374                WebSocketReviewFailureKind::AuthorityAlreadyMinted
2375                | WebSocketReviewFailureKind::EndpointOutsideAuthority => {
2376                    WebSocketReviewTerminal::AuthorityRefused
2377                },
2378                WebSocketReviewFailureKind::MessageTooLarge
2379                | WebSocketReviewFailureKind::MessageLimitReached
2380                | WebSocketReviewFailureKind::ControlFrameLimitReached
2381                | WebSocketReviewFailureKind::ApplicationByteLimitReached
2382                | WebSocketReviewFailureKind::ParentResponseBudgetReached => {
2383                    WebSocketReviewTerminal::LimitReached
2384                },
2385                WebSocketReviewFailureKind::Cancelled => WebSocketReviewTerminal::Cancelled,
2386                WebSocketReviewFailureKind::DeadlineReached => {
2387                    WebSocketReviewTerminal::DeadlineReached
2388                },
2389                _ => WebSocketReviewTerminal::Incomplete,
2390            };
2391            assert_eq!(failure.terminal(), expected_terminal);
2392        }
2393        for (status, token) in [
2394            (WebSocketReviewMessageStatus::NotSent, "not_sent"),
2395            (
2396                WebSocketReviewMessageStatus::SentNoResponse,
2397                "sent_no_response",
2398            ),
2399            (
2400                WebSocketReviewMessageStatus::ResponseMatched,
2401                "response_matched",
2402            ),
2403            (
2404                WebSocketReviewMessageStatus::ResponseMismatched,
2405                "response_mismatched",
2406            ),
2407        ] {
2408            assert_eq!(status.as_str(), token);
2409        }
2410
2411        let application = url::Url::parse("http://127.0.0.1:9/application/").unwrap();
2412        let endpoint = url::Url::parse("ws://127.0.0.1:9/application/socket").unwrap();
2413        let policy = loopback_policy(&application, &endpoint, b"expected", 1_000);
2414        let audit = websocket_review_not_run(
2415            &policy,
2416            WebSocketReviewFailureKind::ParentAuthorityUnavailable,
2417        );
2418        assert_eq!(audit.schema(), WEBSOCKET_REVIEW_AUDIT_SCHEMA);
2419        assert_eq!(audit.capability_id(), WEBSOCKET_REVIEW_CAPABILITY_ID);
2420        assert!(audit.selected());
2421        assert_eq!(audit.policy_reference(), policy.policy_reference());
2422        assert_eq!(audit.endpoint_reference(), policy.endpoint_reference());
2423        assert_eq!(audit.origin_mode(), WebSocketOriginMode::ApplicationOrigin);
2424        assert!(!audit.subprotocol_requested());
2425        assert!(!audit.subprotocol_negotiated());
2426        assert!(!audit.compression_enabled());
2427        assert!(!audit.reconnect_enabled());
2428        assert_eq!(audit.authentication(), "anonymous");
2429        assert_eq!(audit.context(), "anonymous");
2430        assert_eq!(audit.transport(), WEBSOCKET_REVIEW_TRANSPORT);
2431        assert_eq!(audit.http2_extended_connect(), "unsupported");
2432        assert_eq!(audit.request_count(), 0);
2433        assert_eq!(audit.request_attempt_count(), 0);
2434        assert_eq!(audit.request_admitted_count(), 0);
2435        assert_eq!(audit.connection_count(), 0);
2436        assert_eq!(audit.connection_attempt_count(), 0);
2437        assert_eq!(audit.handshake_completed_count(), 0);
2438        assert_eq!(audit.configured_message_count(), 1);
2439        assert_eq!(audit.outbound_message_count(), 0);
2440        assert_eq!(audit.inbound_message_count(), 0);
2441        assert_eq!(audit.outbound_control_frame_count(), 0);
2442        assert_eq!(audit.inbound_control_frame_count(), 0);
2443        assert_eq!(audit.outbound_application_bytes(), 0);
2444        assert_eq!(audit.inbound_application_bytes(), 0);
2445        assert_eq!(audit.transport_read_bytes(), 0);
2446        assert_eq!(audit.transport_write_bytes(), 0);
2447        assert_eq!(
2448            audit.transport_byte_scope(),
2449            WEBSOCKET_REVIEW_TRANSPORT_BYTE_SCOPE
2450        );
2451        assert_eq!(audit.accounted_request_body_bytes(), 0);
2452        assert_eq!(audit.accounted_response_bytes(), 0);
2453        assert_eq!(audit.accounted_transport_response_bytes(), 0);
2454        assert_eq!(audit.terminal(), WebSocketReviewTerminal::Incomplete);
2455        assert_eq!(
2456            audit.failure(),
2457            Some(WebSocketReviewFailureKind::ParentAuthorityUnavailable)
2458        );
2459        assert_eq!(audit.first_failure(), audit.failure());
2460        assert_eq!(
2461            audit.completeness(),
2462            WebSocketReviewCompleteness::Incomplete
2463        );
2464        assert_eq!(audit.expected_response_count(), 1);
2465        assert_eq!(audit.matched_response_count(), 0);
2466        assert_eq!(audit.mismatched_response_count(), 0);
2467        assert_eq!(audit.messages().len(), 1);
2468        let message = &audit.messages()[0];
2469        assert_eq!(
2470            message.message_reference(),
2471            policy.messages().next().unwrap().message_reference()
2472        );
2473        assert_eq!(
2474            message.expected_response_reference(),
2475            policy
2476                .messages()
2477                .next()
2478                .unwrap()
2479                .expected_response_reference()
2480        );
2481        assert_eq!(message.observed_response_reference(), None);
2482        assert_eq!(message.outbound_length(), 7);
2483        assert_eq!(message.expected_response_length(), 8);
2484        assert_eq!(message.inbound_length(), None);
2485        assert_eq!(message.status(), WebSocketReviewMessageStatus::NotSent);
2486        let limits = audit.limits();
2487        assert_eq!(limits.max_connections(), 1);
2488        assert_eq!(limits.max_outbound_application_bytes(), 64 * 1024);
2489        assert_eq!(limits.max_inbound_application_bytes(), 64 * 1024);
2490        assert_eq!(limits.max_outbound_message_bytes(), 4096);
2491        assert_eq!(limits.max_inbound_message_bytes(), 4096);
2492        assert_eq!(limits.max_messages(), 1);
2493        assert_eq!(limits.max_control_frames(), 4);
2494        assert_eq!(limits.max_wall_time_ms(), 1_000);
2495        assert_eq!(audit.claim_limits().len(), 8);
2496        assert!(audit.is_consistent());
2497        assert_eq!(
2498            WebSocketReviewRuntimeMintError::AuthorityAlreadyMinted.failure(),
2499            WebSocketReviewFailureKind::AuthorityAlreadyMinted
2500        );
2501        assert_eq!(
2502            WebSocketReviewRuntimeMintError::EndpointOutsideAuthority.failure(),
2503            WebSocketReviewFailureKind::EndpointOutsideAuthority
2504        );
2505        assert_eq!(
2506            WebSocketReviewRuntimeMintError::InternalInvariant.failure(),
2507            WebSocketReviewFailureKind::InternalInvariant
2508        );
2509        assert!(lesser_deadline(None, None).is_none());
2510    }
2511
2512    #[cfg(feature = "supplied-session-review")]
2513    #[test]
2514    fn supplied_session_audit_requires_exact_health_evidence_for_qualification() {
2515        let application = url::Url::parse("http://127.0.0.1:9/application/").unwrap();
2516        let endpoint = url::Url::parse("ws://127.0.0.1:9/application/socket").unwrap();
2517        let policy = loopback_policy(&application, &endpoint, b"expected", 1_000);
2518        let health = |reference: Option<&str>| {
2519            WebSocketSuppliedSessionHealthAudit::new("healthy", reference)
2520        };
2521        let context = |terminal, qualification| {
2522            WebSocketSuppliedSessionContextAudit::new(
2523                "supplied-session-policy-sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2524                "supplied-session-application-sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
2525                "supplied-session-principal-sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
2526                "fixture-principal",
2527                Some(health(Some("evidence:startup"))),
2528                terminal,
2529                qualification,
2530            )
2531        };
2532
2533        let mut qualified = websocket_review_not_run(
2534            &policy,
2535            WebSocketReviewFailureKind::ParentAuthorityUnavailable,
2536        );
2537        qualified.request_attempt_count = 1;
2538        qualified.request_admitted_count = 1;
2539        qualified.bind_supplied_session_context(context(
2540            Some(health(Some("evidence:terminal"))),
2541            WebSocketSuppliedSessionQualification::ContextQualified,
2542        ));
2543        assert_eq!(qualified.schema(), WEBSOCKET_SUPPLIED_SESSION_AUDIT_SCHEMA);
2544        assert_eq!(qualified.context(), WEBSOCKET_SUPPLIED_SESSION_CONTEXT);
2545        assert_eq!(qualified.authentication(), "supplied_authorization_header");
2546        assert_eq!(
2547            qualified.claim_limits().len(),
2548            8,
2549            "the existing public exhaustive claim enum remains unchanged; the V2 continuous-authentication limitation is added only by the report projection"
2550        );
2551        assert!(qualified.is_consistent());
2552
2553        let mut missing_terminal_receipt = qualified.clone();
2554        missing_terminal_receipt.bind_supplied_session_context(context(
2555            Some(health(None)),
2556            WebSocketSuppliedSessionQualification::ContextQualified,
2557        ));
2558        assert!(
2559            !missing_terminal_receipt.is_consistent(),
2560            "healthy text without its exact committed receipt cannot qualify the context"
2561        );
2562
2563        missing_terminal_receipt.bind_supplied_session_context(context(
2564            Some(health(None)),
2565            WebSocketSuppliedSessionQualification::ContextUnqualified,
2566        ));
2567        assert!(
2568            missing_terminal_receipt.is_consistent(),
2569            "a healthy terminal classification whose receipt was not committed is a valid unqualified result"
2570        );
2571
2572        let mut not_established = websocket_review_not_run(
2573            &policy,
2574            WebSocketReviewFailureKind::ParentAuthorityUnavailable,
2575        );
2576        not_established.request_attempt_count = 1;
2577        not_established.bind_supplied_session_context(context(
2578            Some(health(Some("evidence:terminal"))),
2579            WebSocketSuppliedSessionQualification::NotEstablished,
2580        ));
2581        assert!(
2582            not_established.is_consistent(),
2583            "actual health projections remain valid when no WebSocket request was admitted"
2584        );
2585        not_established.request_admitted_count = 1;
2586        assert!(
2587            !not_established.is_consistent(),
2588            "an admitted request cannot retain a not-established context"
2589        );
2590    }
2591
2592    #[cfg(feature = "supplied-session-review")]
2593    #[test]
2594    fn supplied_authorization_is_added_only_by_the_explicit_handshake_builder() {
2595        let application = url::Url::parse("http://127.0.0.1:9/application/").unwrap();
2596        let endpoint = url::Url::parse("ws://127.0.0.1:9/application/socket").unwrap();
2597        let policy = loopback_policy(&application, &endpoint, b"expected", 1_000);
2598        let runtime = runtime(
2599            &policy,
2600            &application,
2601            RequestAccountingBroker::new(RuntimeBudget::default()),
2602        );
2603        let anonymous = runtime.client_request().unwrap();
2604        assert!(anonymous.headers().get("authorization").is_none());
2605        assert!(anonymous.headers().get("proxy-authorization").is_none());
2606        assert!(anonymous.headers().get("cookie").is_none());
2607
2608        let authorization =
2609            SuppliedSessionAuthorization::new("Bearer HANDSHAKE-SECRET-CANARY").unwrap();
2610        let supplied = runtime
2611            .client_request_with_supplied_authorization(&authorization)
2612            .unwrap();
2613        let value = supplied
2614            .headers()
2615            .get("authorization")
2616            .expect("explicit builder must add the selected Authorization value");
2617        assert_eq!(value, "Bearer HANDSHAKE-SECRET-CANARY");
2618        assert!(value.is_sensitive());
2619        assert!(supplied.headers().get("proxy-authorization").is_none());
2620        assert!(supplied.headers().get("cookie").is_none());
2621    }
2622
2623    #[tokio::test]
2624    async fn parent_request_and_response_limits_refuse_transport_at_the_owned_boundary() {
2625        let application = url::Url::parse("http://127.0.0.1:9/application/").unwrap();
2626        let endpoint = url::Url::parse("ws://127.0.0.1:9/application/socket").unwrap();
2627        let policy = loopback_policy(&application, &endpoint, b"expected", 1_000);
2628        let request_refused = runtime(
2629            &policy,
2630            &application,
2631            RequestAccountingBroker::new(RuntimeBudget::default().with_max_total_requests(0)),
2632        )
2633        .execute()
2634        .await;
2635        assert_eq!(request_refused.request_attempt_count(), 1);
2636        assert_eq!(request_refused.request_admitted_count(), 0);
2637        assert_eq!(request_refused.connection_attempt_count(), 0);
2638        assert_eq!(
2639            request_refused.first_failure(),
2640            Some(WebSocketReviewFailureKind::ParentAuthorityUnavailable)
2641        );
2642        assert!(request_refused.is_consistent());
2643
2644        let response_preflight_refused = runtime(
2645            &policy,
2646            &application,
2647            RequestAccountingBroker::new(RuntimeBudget::default().with_max_response_bytes(0)),
2648        )
2649        .execute()
2650        .await;
2651        assert_eq!(response_preflight_refused.request_attempt_count(), 1);
2652        assert_eq!(response_preflight_refused.request_admitted_count(), 0);
2653        assert_eq!(response_preflight_refused.connection_attempt_count(), 0);
2654        assert_eq!(
2655            response_preflight_refused.first_failure(),
2656            Some(WebSocketReviewFailureKind::ParentAuthorityUnavailable)
2657        );
2658        assert!(response_preflight_refused.is_consistent());
2659
2660        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2661        let address = listener.local_addr().unwrap();
2662        let server = tokio::spawn(async move {
2663            let (mut stream, _) = listener.accept().await.unwrap();
2664            let mut request = [0_u8; 2048];
2665            let read = stream.read(&mut request).await.unwrap();
2666            assert!(request[..read].starts_with(b"GET /application/socket HTTP/1.1\r\n"));
2667            stream
2668                .write_all(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")
2669                .await
2670                .unwrap();
2671            stream.flush().await.unwrap();
2672        });
2673        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
2674        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
2675        let policy = loopback_policy(&application, &endpoint, b"expected", 1_000);
2676        let response_refused = runtime(
2677            &policy,
2678            &application,
2679            RequestAccountingBroker::new(RuntimeBudget::default().with_max_response_bytes(1)),
2680        )
2681        .execute()
2682        .await;
2683        await_server(server).await;
2684        assert_eq!(response_refused.request_attempt_count(), 1);
2685        assert_eq!(response_refused.request_admitted_count(), 1);
2686        assert_eq!(response_refused.connection_attempt_count(), 1);
2687        assert_eq!(response_refused.handshake_completed_count(), 0);
2688        assert_eq!(
2689            response_refused.first_failure(),
2690            Some(WebSocketReviewFailureKind::ParentResponseBudgetReached)
2691        );
2692        assert_eq!(
2693            response_refused.terminal(),
2694            WebSocketReviewTerminal::LimitReached
2695        );
2696        assert!(response_refused.is_consistent());
2697    }
2698
2699    #[tokio::test]
2700    async fn connection_and_http_upgrade_failures_are_distinct_and_value_free() {
2701        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2702        let address = listener.local_addr().unwrap();
2703        let server = tokio::spawn(async move {
2704            let (stream, _) = listener.accept().await.unwrap();
2705            drop(stream);
2706        });
2707        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
2708        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
2709        let policy = loopback_policy(&application, &endpoint, b"expected", 1_000);
2710        let refused = runtime(
2711            &policy,
2712            &application,
2713            RequestAccountingBroker::new(RuntimeBudget::default()),
2714        )
2715        .execute()
2716        .await;
2717        await_server(server).await;
2718        assert_eq!(refused.connection_attempt_count(), 1);
2719        assert_eq!(refused.handshake_completed_count(), 0);
2720        assert_eq!(
2721            refused.first_failure(),
2722            Some(WebSocketReviewFailureKind::ConnectionFailed)
2723        );
2724        assert!(refused.is_consistent());
2725
2726        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2727        let address = listener.local_addr().unwrap();
2728        let server = tokio::spawn(async move {
2729            let (mut stream, _) = listener.accept().await.unwrap();
2730            let mut request = [0_u8; 2048];
2731            let read = stream.read(&mut request).await.unwrap();
2732            assert!(request[..read].starts_with(b"GET /application/socket HTTP/1.1\r\n"));
2733            stream
2734                .write_all(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")
2735                .await
2736                .unwrap();
2737            stream.flush().await.unwrap();
2738        });
2739        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
2740        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
2741        let policy = loopback_policy(&application, &endpoint, b"expected", 1_000);
2742        let rejected = runtime(
2743            &policy,
2744            &application,
2745            RequestAccountingBroker::new(RuntimeBudget::default()),
2746        )
2747        .execute()
2748        .await;
2749        await_server(server).await;
2750        assert_eq!(rejected.handshake_completed_count(), 0);
2751        assert_eq!(
2752            rejected.first_failure(),
2753            Some(WebSocketReviewFailureKind::HandshakeRejected)
2754        );
2755        assert!(rejected.transport_read_bytes() > 0);
2756        assert!(rejected.is_consistent());
2757    }
2758
2759    #[tokio::test]
2760    async fn backend_rejects_a_missing_negotiated_subprotocol_before_messages() {
2761        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2762        let address = listener.local_addr().unwrap();
2763        let server = tokio::spawn(async move {
2764            let (stream, _) = listener.accept().await.unwrap();
2765            let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
2766            let _ = socket.next().await;
2767        });
2768        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
2769        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
2770        let policy = loopback_policy_with_options(
2771            &application,
2772            &endpoint,
2773            b"expected",
2774            2_000,
2775            Some("termivar-review"),
2776            4,
2777            4096,
2778        );
2779        let audit = runtime(
2780            &policy,
2781            &application,
2782            RequestAccountingBroker::new(RuntimeBudget::default()),
2783        )
2784        .execute()
2785        .await;
2786        tokio::time::timeout(Duration::from_secs(2), server)
2787            .await
2788            .unwrap()
2789            .unwrap();
2790        assert_eq!(audit.handshake_completed_count(), 0);
2791        assert!(audit.subprotocol_requested());
2792        assert!(!audit.subprotocol_negotiated());
2793        assert_eq!(
2794            audit.first_failure(),
2795            Some(WebSocketReviewFailureKind::HandshakeRejected)
2796        );
2797        assert!(audit.is_consistent());
2798    }
2799
2800    #[test]
2801    fn subprotocol_comparison_requires_an_exact_selected_value() {
2802        let selected = Response::builder()
2803            .status(101)
2804            .header("sec-websocket-protocol", "termivar-review")
2805            .body(())
2806            .unwrap();
2807        let absent = Response::builder().status(101).body(()).unwrap();
2808        assert!(subprotocol_matches(&selected, Some("termivar-review")));
2809        assert!(!subprotocol_matches(&selected, Some("other")));
2810        assert!(!subprotocol_matches(&selected, None));
2811        assert!(subprotocol_matches(&absent, None));
2812        assert!(!subprotocol_matches(&absent, Some("termivar-review")));
2813    }
2814
2815    #[tokio::test]
2816    async fn ping_pong_is_bounded_and_does_not_replace_the_text_oracle() {
2817        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2818        let address = listener.local_addr().unwrap();
2819        let server = tokio::spawn(async move {
2820            let (stream, _) = listener.accept().await.unwrap();
2821            let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
2822            assert_eq!(
2823                socket.next().await.unwrap().unwrap().into_text().unwrap(),
2824                "request"
2825            );
2826            socket.send(Message::Ping(Vec::new().into())).await.unwrap();
2827            socket.send(Message::text("expected")).await.unwrap();
2828            for _ in 0..2 {
2829                if matches!(
2830                    tokio::time::timeout(Duration::from_secs(1), socket.next()).await,
2831                    Ok(Some(Ok(Message::Close(_)))) | Ok(None)
2832                ) {
2833                    break;
2834                }
2835            }
2836        });
2837        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
2838        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
2839        let policy = loopback_policy(&application, &endpoint, b"expected", 2_000);
2840        let audit = runtime(
2841            &policy,
2842            &application,
2843            RequestAccountingBroker::new(RuntimeBudget::default()),
2844        )
2845        .execute()
2846        .await;
2847        await_server(server).await;
2848        assert_eq!(audit.terminal(), WebSocketReviewTerminal::Completed);
2849        assert_eq!(audit.matched_response_count(), 1);
2850        assert_eq!(audit.inbound_control_frame_count(), 1);
2851        assert!(audit.outbound_control_frame_count() >= 2);
2852        assert!(audit.is_consistent());
2853    }
2854
2855    #[tokio::test]
2856    async fn peer_close_and_control_limit_preserve_distinct_incomplete_evidence() {
2857        for control_limit in [4_u16, 1_u16] {
2858            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2859            let address = listener.local_addr().unwrap();
2860            let server = tokio::spawn(async move {
2861                let (stream, _) = listener.accept().await.unwrap();
2862                let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
2863                let _ = socket.next().await;
2864                if control_limit == 1 {
2865                    socket.send(Message::Ping(Vec::new().into())).await.unwrap();
2866                } else {
2867                    socket.close(None).await.unwrap();
2868                }
2869            });
2870            let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
2871            let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
2872            let policy = loopback_policy_with_options(
2873                &application,
2874                &endpoint,
2875                b"expected",
2876                2_000,
2877                None,
2878                control_limit,
2879                4096,
2880            );
2881            let audit = runtime(
2882                &policy,
2883                &application,
2884                RequestAccountingBroker::new(RuntimeBudget::default()),
2885            )
2886            .execute()
2887            .await;
2888            await_server(server).await;
2889            let expected = if control_limit == 1 {
2890                WebSocketReviewFailureKind::ControlFrameLimitReached
2891            } else {
2892                WebSocketReviewFailureKind::PeerClosed
2893            };
2894            assert_eq!(audit.first_failure(), Some(expected));
2895            assert_eq!(audit.outbound_message_count(), 1);
2896            assert_eq!(audit.inbound_message_count(), 0);
2897            assert!(audit.is_consistent());
2898        }
2899    }
2900
2901    #[tokio::test]
2902    async fn oversized_text_response_is_a_limit_not_a_mismatch() {
2903        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2904        let address = listener.local_addr().unwrap();
2905        let server = tokio::spawn(async move {
2906            let (stream, _) = listener.accept().await.unwrap();
2907            let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
2908            let _ = socket.next().await;
2909            socket.send(Message::text("oversized")).await.unwrap();
2910        });
2911        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
2912        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
2913        let policy =
2914            loopback_policy_with_options(&application, &endpoint, b"tiny", 2_000, None, 4, 4);
2915        let audit = runtime(
2916            &policy,
2917            &application,
2918            RequestAccountingBroker::new(RuntimeBudget::default()),
2919        )
2920        .execute()
2921        .await;
2922        await_server(server).await;
2923        assert_eq!(
2924            audit.first_failure(),
2925            Some(WebSocketReviewFailureKind::MessageTooLarge)
2926        );
2927        assert_eq!(audit.mismatched_response_count(), 0);
2928        assert_eq!(audit.inbound_message_count(), 0);
2929        assert!(audit.is_consistent());
2930    }
2931
2932    #[tokio::test]
2933    async fn loopback_exchange_accounts_raw_transport_and_redacts_mismatch_digest() {
2934        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2935        let address = listener.local_addr().unwrap();
2936        let server = tokio::spawn(async move {
2937            let (stream, _) = listener.accept().await.unwrap();
2938            let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
2939            assert_eq!(
2940                socket.next().await.unwrap().unwrap().into_text().unwrap(),
2941                "request"
2942            );
2943            socket.send(Message::text("unexpected")).await.unwrap();
2944            let _ = socket.next().await;
2945        });
2946
2947        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
2948        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
2949        let policy = loopback_policy(&application, &endpoint, b"expected", 2_000);
2950        let accounting = RequestAccountingBroker::new(
2951            RuntimeBudget::default().with_max_response_bytes(1024 * 1024),
2952        );
2953        let audit = runtime(&policy, &application, accounting.clone())
2954            .execute()
2955            .await;
2956        await_server(server).await;
2957
2958        assert_eq!(audit.terminal(), WebSocketReviewTerminal::Completed);
2959        assert_eq!(
2960            audit.completeness(),
2961            WebSocketReviewCompleteness::ConfiguredExchangeComplete
2962        );
2963        assert_eq!(audit.request_attempt_count(), 1);
2964        assert_eq!(audit.request_admitted_count(), 1);
2965        assert_eq!(audit.connection_attempt_count(), 1);
2966        assert_eq!(audit.handshake_completed_count(), 1);
2967        assert_eq!(audit.matched_response_count(), 0);
2968        assert_eq!(audit.mismatched_response_count(), 1);
2969        assert_eq!(
2970            audit.messages()[0].status(),
2971            WebSocketReviewMessageStatus::ResponseMismatched
2972        );
2973        assert!(audit.messages()[0].observed_response_reference().is_none());
2974        assert!(audit.transport_read_bytes() >= audit.inbound_application_bytes());
2975        assert!(audit.transport_write_bytes() >= audit.outbound_application_bytes());
2976        assert_eq!(
2977            audit.accounted_transport_response_bytes(),
2978            audit.transport_read_bytes()
2979        );
2980        assert_eq!(
2981            accounting.snapshot().response_bytes(),
2982            audit.transport_read_bytes()
2983        );
2984        assert_eq!(accounting.snapshot().request_body_bytes(), 0);
2985        assert!(audit.is_consistent());
2986    }
2987
2988    #[tokio::test]
2989    async fn binary_response_cannot_satisfy_the_text_response_oracle() {
2990        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2991        let address = listener.local_addr().unwrap();
2992        let server = tokio::spawn(async move {
2993            let (stream, _) = listener.accept().await.unwrap();
2994            let mut socket = tokio_tungstenite::accept_async(stream).await.unwrap();
2995            assert_eq!(
2996                socket.next().await.unwrap().unwrap().into_text().unwrap(),
2997                "request"
2998            );
2999            socket
3000                .send(Message::binary(b"expected".to_vec()))
3001                .await
3002                .unwrap();
3003            let _ = socket.next().await;
3004        });
3005
3006        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
3007        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
3008        let policy = loopback_policy(&application, &endpoint, b"expected", 2_000);
3009        let accounting = RequestAccountingBroker::new(
3010            RuntimeBudget::default().with_max_response_bytes(1024 * 1024),
3011        );
3012        let audit = runtime(&policy, &application, accounting.clone())
3013            .execute()
3014            .await;
3015        await_server(server).await;
3016
3017        assert_eq!(audit.terminal(), WebSocketReviewTerminal::Incomplete);
3018        assert_eq!(
3019            audit.first_failure(),
3020            Some(WebSocketReviewFailureKind::BinaryMessageUnsupported)
3021        );
3022        assert_eq!(audit.matched_response_count(), 0);
3023        assert_eq!(audit.mismatched_response_count(), 0);
3024        assert_eq!(audit.inbound_message_count(), 0);
3025        assert_eq!(audit.inbound_application_bytes(), 0);
3026        assert!(audit.transport_read_bytes() > 0);
3027        assert_eq!(
3028            accounting.snapshot().response_bytes(),
3029            audit.transport_read_bytes()
3030        );
3031        assert!(audit.is_consistent());
3032    }
3033
3034    #[tokio::test]
3035    async fn partial_handshake_read_is_charged_when_deadline_stops_the_handshake() {
3036        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
3037        let address = listener.local_addr().unwrap();
3038        let server = tokio::spawn(async move {
3039            let (mut stream, _) = listener.accept().await.unwrap();
3040            let mut request = [0_u8; 1024];
3041            let _ = stream.read(&mut request).await.unwrap();
3042            stream
3043                .write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n")
3044                .await
3045                .unwrap();
3046            stream.flush().await.unwrap();
3047            tokio::time::sleep(Duration::from_secs(2)).await;
3048        });
3049
3050        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
3051        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
3052        let policy = loopback_policy(&application, &endpoint, b"expected", 100);
3053        let accounting = RequestAccountingBroker::new(
3054            RuntimeBudget::default().with_max_response_bytes(1024 * 1024),
3055        );
3056        let audit = runtime(&policy, &application, accounting.clone())
3057            .execute()
3058            .await;
3059        server.abort();
3060
3061        assert_eq!(audit.terminal(), WebSocketReviewTerminal::DeadlineReached);
3062        assert_eq!(
3063            audit.first_failure(),
3064            Some(WebSocketReviewFailureKind::DeadlineReached)
3065        );
3066        assert_eq!(audit.handshake_completed_count(), 0);
3067        assert!(audit.transport_read_bytes() > 0);
3068        assert_eq!(
3069            audit.accounted_transport_response_bytes(),
3070            audit.transport_read_bytes()
3071        );
3072        assert_eq!(
3073            accounting.snapshot().response_bytes(),
3074            audit.transport_read_bytes()
3075        );
3076        assert!(audit.is_consistent());
3077    }
3078
3079    #[tokio::test]
3080    async fn cancellation_and_elapsed_parent_deadline_stop_before_lease_admission() {
3081        let application = url::Url::parse("http://127.0.0.1:9/application/").unwrap();
3082        let endpoint = url::Url::parse("ws://127.0.0.1:9/application/socket").unwrap();
3083        let policy = loopback_policy(&application, &endpoint, b"expected", 1_000);
3084
3085        let cancellation = CancellationToken::new();
3086        cancellation.cancel();
3087        let cancelled = WebSocketReviewRuntime::new(
3088            &policy,
3089            application.origin().ascii_serialization(),
3090            RequestAccountingBroker::new(RuntimeBudget::default()),
3091            cancellation,
3092            None,
3093        )
3094        .unwrap()
3095        .execute()
3096        .await;
3097        assert_eq!(cancelled.request_attempt_count(), 0);
3098        assert_eq!(cancelled.request_admitted_count(), 0);
3099        assert_eq!(cancelled.connection_attempt_count(), 0);
3100        assert_eq!(cancelled.terminal(), WebSocketReviewTerminal::Cancelled);
3101        assert!(cancelled.is_consistent());
3102
3103        let deadline = WebSocketReviewRuntime::new(
3104            &policy,
3105            application.origin().ascii_serialization(),
3106            RequestAccountingBroker::new(RuntimeBudget::default()),
3107            CancellationToken::new(),
3108            Some(tokio::time::Instant::now()),
3109        )
3110        .unwrap()
3111        .execute()
3112        .await;
3113        assert_eq!(deadline.request_attempt_count(), 0);
3114        assert_eq!(deadline.request_admitted_count(), 0);
3115        assert_eq!(deadline.connection_attempt_count(), 0);
3116        assert_eq!(
3117            deadline.terminal(),
3118            WebSocketReviewTerminal::DeadlineReached
3119        );
3120        assert!(deadline.is_consistent());
3121    }
3122
3123    #[test]
3124    fn tls_config_constructor_unit_rejects_a_provider_without_cipher_suites() {
3125        // Pure constructor unit test, not a network/TLS interoperability test.
3126        // Production always supplies the unmodified, fixed ring provider above.
3127        let mut provider = rustls::crypto::ring::default_provider();
3128        provider.cipher_suites.clear();
3129        assert!(matches!(
3130            websocket_tls_config(provider),
3131            Err(WebSocketReviewFailureKind::InternalInvariant)
3132        ));
3133    }
3134
3135    #[tokio::test]
3136    async fn malformed_private_request_preparation_finishes_lease_without_connecting() {
3137        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
3138        let address = listener.local_addr().unwrap();
3139        let application = url::Url::parse(&format!("http://{address}/application/")).unwrap();
3140        let endpoint = url::Url::parse(&format!("ws://{address}/application/socket")).unwrap();
3141        let policy = loopback_policy(&application, &endpoint, b"expected", 2_000);
3142        let accounting =
3143            RequestAccountingBroker::new(RuntimeBudget::default().with_max_total_requests(1));
3144        let mut review = runtime(&policy, &application, accounting.clone());
3145        // Corrupt only private post-mint state; public policy validation is unchanged.
3146        review.application_origin = "invalid\r\norigin".to_owned();
3147        let audit = review.execute().await;
3148
3149        assert_eq!(audit.terminal(), WebSocketReviewTerminal::Incomplete);
3150        assert_eq!(
3151            audit.first_failure(),
3152            Some(WebSocketReviewFailureKind::RequestConstruction)
3153        );
3154        assert_eq!(audit.request_attempt_count(), 1);
3155        assert_eq!(audit.request_admitted_count(), 1);
3156        assert_eq!(audit.connection_attempt_count(), 0);
3157        assert_eq!(audit.handshake_completed_count(), 0);
3158        assert_eq!(audit.outbound_message_count(), 0);
3159        assert_eq!(audit.inbound_message_count(), 0);
3160        assert_eq!(audit.transport_read_bytes(), 0);
3161        assert_eq!(audit.transport_write_bytes(), 0);
3162        assert_eq!(audit.accounted_request_body_bytes(), 0);
3163        assert_eq!(audit.accounted_response_bytes(), 0);
3164        assert!(audit.is_consistent());
3165
3166        let snapshot = accounting.snapshot();
3167        assert_eq!(snapshot.total_requests(), 1);
3168        assert_eq!(snapshot.passive_requests(), 1);
3169        assert_eq!(snapshot.request_body_bytes(), 0);
3170        assert_eq!(snapshot.response_bytes(), 0);
3171        let dispatch = accounting.dispatch_audit();
3172        assert_eq!(dispatch.receipts().len(), 1);
3173        let receipt = &dispatch.receipts()[0];
3174        assert_eq!(receipt.action_id(), WEBSOCKET_REVIEW_ACTION_ID);
3175        assert_eq!(receipt.stage(), DecisionExecutionStage::Passive);
3176        assert_eq!(
3177            receipt.outcome(),
3178            TransportDispatchOutcome::TransportFailure
3179        );
3180        assert_eq!(receipt.request_body_bytes(), 0);
3181        assert_eq!(receipt.response_bytes(), 0);
3182        assert!(
3183            tokio::time::timeout(Duration::from_millis(50), listener.accept())
3184                .await
3185                .is_err()
3186        );
3187    }
3188
3189    #[test]
3190    fn websocket_tls_uses_current_webpki_roots_and_no_client_auth_only_for_wss() {
3191        // Literal subject from the Mozilla root set, independent of the
3192        // connector's construction. An empty/native-only replacement fails.
3193        let digicert_subject = b"1\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x170\x15\x06\x03U\x04\n\x13\x0eDigiCert, Inc.1&0$\x06\x03U\x04\x03\x13\x1dDigiCert TLS ECC P384 Root G5";
3194        let roots = websocket_root_store();
3195        assert!(roots
3196            .roots
3197            .iter()
3198            .any(|root| root.subject.as_ref() == digicert_subject));
3199        let distrusted_subject = b"emSign ECC Root CA - G3";
3200        assert!(roots.roots.iter().all(|root| !root
3201            .subject
3202            .as_ref()
3203            .windows(distrusted_subject.len())
3204            .any(|part| part == distrusted_subject)));
3205
3206        let endpoint = url::Url::parse("wss://example.com/application/socket").unwrap();
3207        let Some(Connector::Rustls(config)) = websocket_tls_connector(&endpoint).unwrap() else {
3208            panic!("wss must use the explicitly reviewed Rustls connector");
3209        };
3210        assert!(!config.client_auth_cert_resolver.has_certs());
3211        assert!(config.alpn_protocols.is_empty());
3212        assert!(!config.enable_early_data);
3213        assert!(config.enable_sni);
3214
3215        let endpoint = url::Url::parse("ws://127.0.0.1/application/socket").unwrap();
3216        assert!(websocket_tls_connector(&endpoint).unwrap().is_none());
3217    }
3218
3219    #[tokio::test]
3220    async fn self_signed_wss_server_is_not_added_to_the_trust_store() {
3221        use rcgen::{CertificateParams, KeyPair};
3222        use tokio_rustls::{
3223            rustls::{
3224                pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer},
3225                ServerConfig,
3226            },
3227            TlsAcceptor,
3228        };
3229
3230        let key = KeyPair::generate().unwrap();
3231        let certificate = CertificateParams::new(vec!["127.0.0.1".to_owned()])
3232            .unwrap()
3233            .self_signed(&key)
3234            .unwrap();
3235        let server_config = ServerConfig::builder()
3236            .with_no_client_auth()
3237            .with_single_cert(
3238                vec![CertificateDer::from(certificate.der().to_vec())],
3239                PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.serialize_der())),
3240            )
3241            .unwrap();
3242        let acceptor = TlsAcceptor::from(Arc::new(server_config));
3243        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
3244        let address = listener.local_addr().unwrap();
3245        let server = tokio::spawn(async move {
3246            let (stream, _) = listener.accept().await.unwrap();
3247            acceptor.accept(stream).await
3248        });
3249
3250        let application = url::Url::parse(&format!("https://{address}/application/")).unwrap();
3251        let endpoint = url::Url::parse(&format!("wss://{address}/application/socket")).unwrap();
3252        let policy = loopback_policy(&application, &endpoint, b"expected", 2_000);
3253        let accounting = RequestAccountingBroker::new(
3254            RuntimeBudget::default().with_max_response_bytes(1024 * 1024),
3255        );
3256        let audit = runtime(&policy, &application, accounting.clone())
3257            .execute()
3258            .await;
3259        let server_result = tokio::time::timeout(Duration::from_secs(2), server)
3260            .await
3261            .unwrap()
3262            .unwrap();
3263
3264        assert!(server_result.is_err());
3265        assert_eq!(audit.request_attempt_count(), 1);
3266        assert_eq!(audit.request_admitted_count(), 1);
3267        assert_eq!(audit.connection_attempt_count(), 1);
3268        assert_eq!(audit.handshake_completed_count(), 0);
3269        assert_eq!(audit.outbound_message_count(), 0);
3270        assert_eq!(audit.inbound_message_count(), 0);
3271        assert_eq!(
3272            audit.first_failure(),
3273            Some(WebSocketReviewFailureKind::ConnectionFailed)
3274        );
3275        assert!(audit.transport_read_bytes() > 0);
3276        assert_eq!(
3277            audit.accounted_transport_response_bytes(),
3278            audit.transport_read_bytes()
3279        );
3280        assert_eq!(
3281            accounting.snapshot().response_bytes(),
3282            audit.transport_read_bytes()
3283        );
3284        assert!(audit.is_consistent());
3285    }
3286}