1use 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
53pub const WEBSOCKET_REVIEW_AUDIT_SCHEMA: &str = "security.websocket-review-audit/v1";
55#[cfg(feature = "supplied-session-review")]
57pub const WEBSOCKET_SUPPLIED_SESSION_AUDIT_SCHEMA: &str = "security.websocket-review-audit/v2";
58pub const WEBSOCKET_REVIEW_CAPABILITY_ID: &str = "termivar.websocket-review/v1";
60pub const WEBSOCKET_REVIEW_ACTION_ID: &str = "websocket.review";
62pub const WEBSOCKET_REVIEW_TRANSPORT: &str = "rfc6455_http1_upgrade";
64pub const WEBSOCKET_REVIEW_AUTHENTICATION: &str = "anonymous";
66#[cfg(feature = "supplied-session-review")]
68pub const WEBSOCKET_SUPPLIED_SESSION_CONTEXT: &str = "supplied_session";
69pub const WEBSOCKET_REVIEW_HTTP2_EXTENDED_CONNECT: &str = "unsupported";
71pub const WEBSOCKET_REVIEW_TRANSPORT_BYTE_SCOPE: &str = "socket_wire_bytes_tls_ciphertext_when_wss";
73pub const MAX_WEBSOCKET_REVIEW_CONNECTIONS: u8 = 1;
75pub const MAX_WEBSOCKET_REVIEW_APPLICATION_BYTES_PER_DIRECTION: u64 = 64 * 1024;
77
78#[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 #[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#[cfg(feature = "supplied-session-review")]
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub(crate) enum WebSocketSuppliedSessionQualification {
124 ContextQualified,
126 ContextUnqualified,
128 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#[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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
257pub enum WebSocketReviewCompleteness {
258 ConfiguredExchangeComplete,
259 Incomplete,
260}
261
262impl WebSocketReviewCompleteness {
263 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
275pub enum WebSocketReviewTerminal {
276 Completed,
278 Incomplete,
280 Cancelled,
282 DeadlineReached,
284 LimitReached,
286 AuthorityRefused,
288}
289
290impl WebSocketReviewTerminal {
291 #[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#[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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
379pub enum WebSocketReviewMessageStatus {
380 NotSent,
381 SentNoResponse,
382 ResponseMatched,
383 ResponseMismatched,
384}
385
386impl WebSocketReviewMessageStatus {
387 #[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#[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#[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#[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 #[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 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 pub const fn transport_read_bytes(&self) -> u64 {
653 self.transport_read_bytes
654 }
655
656 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 pub const fn accounted_request_body_bytes(&self) -> u64 {
668 self.accounted_request_body_bytes
669 }
670
671 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 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 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
1043pub(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#[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
1946pub(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#[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
2008pub(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 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 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 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 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}