1use super::tcp::stream::validate_stream_record;
9use super::{
10 ConnectionState, DiscoveredPeer, PacketBuffer, PacketTx, ReceivedPacket, Transport,
11 TransportAddr, TransportError, TransportId, TransportState, TransportType,
12};
13use crate::Identity;
14use crate::config::WebSocketConfig;
15use crate::dataplane::validate_direct_fsp_transport_fragment;
16use crate::discovery::local_udp::LocalKeyHint;
17use futures::{SinkExt, StreamExt};
18use rand::RngExt;
19use secp256k1::XOnlyPublicKey;
20use serde::Serialize;
21use std::collections::{HashMap, VecDeque};
22use std::net::SocketAddr;
23use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
24use std::sync::{Arc, Mutex as StdMutex};
25use std::time::{Duration, SystemTime, UNIX_EPOCH};
26use tokio::net::{TcpListener, TcpStream};
27use tokio::sync::{Mutex, Semaphore, mpsc};
28use tokio::task::JoinHandle;
29use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
30use tokio_tungstenite::tungstenite::protocol::WebSocketConfig as TungsteniteConfig;
31use tokio_tungstenite::tungstenite::{Bytes, Message};
32use tokio_tungstenite::{WebSocketStream, accept_hdr_async_with_config, connect_async_with_config};
33use tracing::{debug, info};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36enum Direction {
37 Inbound,
38 Outbound,
39}
40
41struct Connection {
42 generation: u64,
43 tx: mpsc::Sender<Vec<u8>>,
44}
45
46type ConnectionPool = Arc<Mutex<HashMap<TransportAddr, Connection>>>;
47type ConnectionStates = Arc<StdMutex<HashMap<TransportAddr, ConnectionState>>>;
48type DiscoveryQueue = Arc<StdMutex<VecDeque<DiscoveredPeer>>>;
49
50#[derive(Debug, Default)]
51struct WebSocketStats {
52 connections_opened: AtomicU64,
53 connections_closed: AtomicU64,
54 connections_rejected: AtomicU64,
55 reconnect_attempts: AtomicU64,
56 frames_sent: AtomicU64,
57 frames_received: AtomicU64,
58 bytes_sent: AtomicU64,
59 bytes_received: AtomicU64,
60 invalid_frames: AtomicU64,
61 send_queue_full: AtomicU64,
62}
63
64#[derive(Debug, Serialize)]
65pub struct WebSocketStatsSnapshot {
66 pub connections_opened: u64,
67 pub connections_closed: u64,
68 pub connections_rejected: u64,
69 pub reconnect_attempts: u64,
70 pub frames_sent: u64,
71 pub frames_received: u64,
72 pub bytes_sent: u64,
73 pub bytes_received: u64,
74 pub invalid_frames: u64,
75 pub send_queue_full: u64,
76}
77
78impl WebSocketStats {
79 fn snapshot(&self) -> WebSocketStatsSnapshot {
80 let load = |value: &AtomicU64| value.load(Ordering::Relaxed);
81 WebSocketStatsSnapshot {
82 connections_opened: load(&self.connections_opened),
83 connections_closed: load(&self.connections_closed),
84 connections_rejected: load(&self.connections_rejected),
85 reconnect_attempts: load(&self.reconnect_attempts),
86 frames_sent: load(&self.frames_sent),
87 frames_received: load(&self.frames_received),
88 bytes_sent: load(&self.bytes_sent),
89 bytes_received: load(&self.bytes_received),
90 invalid_frames: load(&self.invalid_frames),
91 send_queue_full: load(&self.send_queue_full),
92 }
93 }
94}
95
96#[derive(Clone)]
97struct Runtime {
98 transport_id: TransportId,
99 config: WebSocketConfig,
100 local_pubkey: [u8; 32],
101 packet_tx: PacketTx,
102 pool: ConnectionPool,
103 states: ConnectionStates,
104 discoveries: DiscoveryQueue,
105 running: Arc<AtomicBool>,
106 total_slots: Arc<Semaphore>,
107 inbound_slots: Arc<Semaphore>,
108 generation: Arc<AtomicU64>,
109 stats: Arc<WebSocketStats>,
110}
111
112impl Runtime {
113 fn websocket_config(&self) -> TungsteniteConfig {
114 let mut config = TungsteniteConfig::default();
115 config.max_message_size = Some(self.config.max_frame_bytes());
116 config.max_frame_size = Some(self.config.max_frame_bytes());
117 config.max_write_buffer_size = self.config.max_frame_bytes().saturating_mul(2);
118 config.write_buffer_size = 0;
119 config
120 }
121
122 fn next_generation(&self) -> u64 {
123 self.generation.fetch_add(1, Ordering::Relaxed)
124 }
125
126 fn set_state(&self, addr: &TransportAddr, state: ConnectionState) {
127 self.states
128 .lock()
129 .unwrap_or_else(|error| error.into_inner())
130 .insert(addr.clone(), state);
131 }
132
133 fn clear_state_if(&self, addr: &TransportAddr, generation: u64) {
134 let connected_generation = self
135 .pool
136 .try_lock()
137 .ok()
138 .and_then(|pool| pool.get(addr).map(|connection| connection.generation));
139 if connected_generation != Some(generation) {
140 self.states
141 .lock()
142 .unwrap_or_else(|error| error.into_inner())
143 .remove(addr);
144 }
145 }
146}
147
148pub struct WebSocketTransport {
150 transport_id: TransportId,
151 name: Option<String>,
152 config: WebSocketConfig,
153 state: TransportState,
154 local_addr: Option<SocketAddr>,
155 runtime: Runtime,
156 tasks: Arc<StdMutex<Vec<JoinHandle<()>>>>,
157}
158
159impl WebSocketTransport {
160 pub fn new(
161 transport_id: TransportId,
162 name: Option<String>,
163 config: WebSocketConfig,
164 packet_tx: PacketTx,
165 identity: &Identity,
166 ) -> Self {
167 let max_connections = config.max_connections();
168 let max_inbound = config.max_inbound_connections();
169 let runtime = Runtime {
170 transport_id,
171 config: config.clone(),
172 local_pubkey: identity.pubkey().serialize(),
173 packet_tx,
174 pool: Arc::new(Mutex::new(HashMap::new())),
175 states: Arc::new(StdMutex::new(HashMap::new())),
176 discoveries: Arc::new(StdMutex::new(VecDeque::new())),
177 running: Arc::new(AtomicBool::new(false)),
178 total_slots: Arc::new(Semaphore::new(max_connections)),
179 inbound_slots: Arc::new(Semaphore::new(max_inbound)),
180 generation: Arc::new(AtomicU64::new(1)),
181 stats: Arc::new(WebSocketStats::default()),
182 };
183 Self {
184 transport_id,
185 name,
186 config,
187 state: TransportState::Configured,
188 local_addr: None,
189 runtime,
190 tasks: Arc::new(StdMutex::new(Vec::new())),
191 }
192 }
193
194 pub fn name(&self) -> Option<&str> {
195 self.name.as_deref()
196 }
197
198 pub fn local_addr(&self) -> Option<SocketAddr> {
199 self.local_addr
200 }
201
202 pub fn public_url(&self) -> Option<&str> {
203 self.config.public_url.as_deref()
204 }
205
206 pub fn stats(&self) -> WebSocketStatsSnapshot {
207 self.runtime.stats.snapshot()
208 }
209
210 fn push_task(&self, task: JoinHandle<()>) {
211 self.tasks
212 .lock()
213 .unwrap_or_else(|error| error.into_inner())
214 .push(task);
215 }
216
217 pub async fn start_async(&mut self) -> Result<(), TransportError> {
218 if !self.state.can_start() {
219 return Err(TransportError::AlreadyStarted);
220 }
221 self.config
222 .validate()
223 .map_err(TransportError::StartFailed)?;
224 self.state = TransportState::Starting;
225 self.runtime.running.store(true, Ordering::Release);
226
227 if let Some(bind_addr) = self.config.bind_addr.as_deref() {
228 let bind_addr = bind_addr
229 .parse::<SocketAddr>()
230 .map_err(|error| TransportError::StartFailed(error.to_string()))?;
231 let listener = TcpListener::bind(bind_addr)
232 .await
233 .map_err(|error| TransportError::bind_failed(bind_addr, error))?;
234 self.local_addr = Some(
235 listener
236 .local_addr()
237 .map_err(|error| TransportError::StartFailed(error.to_string()))?,
238 );
239 self.push_task(tokio::spawn(run_accept_loop(
240 self.runtime.clone(),
241 listener,
242 )));
243 }
244
245 for seed_url in self.config.seed_urls.clone() {
246 let addr = TransportAddr::from_string(&seed_url);
247 self.runtime.set_state(&addr, ConnectionState::Connecting);
248 self.push_task(tokio::spawn(run_seed_dialer(self.runtime.clone(), addr)));
249 }
250
251 self.state = TransportState::Up;
252 info!(
253 transport_id = %self.transport_id,
254 local_addr = ?self.local_addr,
255 seeds = self.config.seed_urls.len(),
256 "WebSocket transport started"
257 );
258 Ok(())
259 }
260
261 pub async fn stop_async(&mut self) -> Result<(), TransportError> {
262 if !self.state.is_operational() {
263 return Err(TransportError::NotStarted);
264 }
265 self.runtime.running.store(false, Ordering::Release);
266 let tasks = {
267 let mut tasks = self.tasks.lock().unwrap_or_else(|error| error.into_inner());
268 std::mem::take(&mut *tasks)
269 };
270 for task in &tasks {
271 task.abort();
272 }
273 for task in tasks {
274 let _ = task.await;
275 }
276 self.runtime.pool.lock().await.clear();
277 self.runtime
278 .states
279 .lock()
280 .unwrap_or_else(|error| error.into_inner())
281 .clear();
282 self.local_addr = None;
283 self.state = TransportState::Down;
284 Ok(())
285 }
286
287 pub async fn send_async(
288 &self,
289 addr: &TransportAddr,
290 data: &[u8],
291 ) -> Result<usize, TransportError> {
292 if !self.state.is_operational() {
293 return Err(TransportError::NotStarted);
294 }
295 if data.len() > self.config.max_frame_bytes() {
296 return Err(TransportError::MtuExceeded {
297 packet_size: data.len(),
298 mtu: self.config.max_frame_bytes().min(u16::MAX as usize) as u16,
299 });
300 }
301 validate_websocket_record(data).map_err(TransportError::SendFailed)?;
302 let tx = self
303 .runtime
304 .pool
305 .lock()
306 .await
307 .get(addr)
308 .map(|connection| connection.tx.clone())
309 .ok_or(TransportError::NotStarted)?;
310 tx.try_send(data.to_vec()).map_err(|error| match error {
311 mpsc::error::TrySendError::Full(_) => {
312 self.runtime
313 .stats
314 .send_queue_full
315 .fetch_add(1, Ordering::Relaxed);
316 TransportError::SendFailed("WebSocket send queue full".into())
317 }
318 mpsc::error::TrySendError::Closed(_) => {
319 TransportError::SendFailed("WebSocket connection closed".into())
320 }
321 })?;
322 Ok(data.len())
323 }
324
325 pub async fn connect_async(&self, addr: &TransportAddr) -> Result<(), TransportError> {
326 if !self.state.is_operational() {
327 return Err(TransportError::NotStarted);
328 }
329 let raw = addr
330 .as_str()
331 .ok_or_else(|| TransportError::InvalidAddress(addr.to_string()))?;
332 let candidate = WebSocketConfig {
333 seed_urls: vec![raw.to_owned()],
334 ..self.config.clone()
335 };
336 candidate
337 .validate()
338 .map_err(TransportError::InvalidAddress)?;
339 if self.connection_state_sync(addr) == ConnectionState::Connected {
340 return Ok(());
341 }
342 {
343 let mut states = self
344 .runtime
345 .states
346 .lock()
347 .unwrap_or_else(|error| error.into_inner());
348 if matches!(states.get(addr), Some(ConnectionState::Connecting)) {
349 return Ok(());
350 }
351 states.insert(addr.clone(), ConnectionState::Connecting);
352 }
353 self.push_task(tokio::spawn(run_one_shot_dial(
354 self.runtime.clone(),
355 addr.clone(),
356 )));
357 Ok(())
358 }
359
360 pub fn connection_state_sync(&self, addr: &TransportAddr) -> ConnectionState {
361 if let Ok(pool) = self.runtime.pool.try_lock()
362 && pool.contains_key(addr)
363 {
364 return ConnectionState::Connected;
365 }
366 self.runtime
367 .states
368 .lock()
369 .unwrap_or_else(|error| error.into_inner())
370 .get(addr)
371 .cloned()
372 .unwrap_or(ConnectionState::None)
373 }
374
375 pub async fn close_connection_async(&self, addr: &TransportAddr) {
376 self.runtime.pool.lock().await.remove(addr);
377 self.runtime
378 .states
379 .lock()
380 .unwrap_or_else(|error| error.into_inner())
381 .remove(addr);
382 }
383}
384
385impl Transport for WebSocketTransport {
386 fn transport_id(&self) -> TransportId {
387 self.transport_id
388 }
389
390 fn transport_type(&self) -> &TransportType {
391 &TransportType::WEBSOCKET
392 }
393
394 fn state(&self) -> TransportState {
395 self.state
396 }
397
398 fn mtu(&self) -> u16 {
399 self.config.mtu()
400 }
401
402 fn start(&mut self) -> Result<(), TransportError> {
403 Err(TransportError::NotSupported(
404 "use start_async() for WebSocket transport".into(),
405 ))
406 }
407
408 fn stop(&mut self) -> Result<(), TransportError> {
409 Err(TransportError::NotSupported(
410 "use stop_async() for WebSocket transport".into(),
411 ))
412 }
413
414 fn send(&self, _addr: &TransportAddr, _data: &[u8]) -> Result<(), TransportError> {
415 Err(TransportError::NotSupported(
416 "use send_async() for WebSocket transport".into(),
417 ))
418 }
419
420 fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
421 Ok(self
422 .runtime
423 .discoveries
424 .lock()
425 .unwrap_or_else(|error| error.into_inner())
426 .drain(..)
427 .collect())
428 }
429
430 fn auto_connect(&self) -> bool {
431 true
432 }
433
434 fn accept_connections(&self) -> bool {
435 self.config.accept_connections()
436 }
437}
438
439async fn run_accept_loop(runtime: Runtime, listener: TcpListener) {
440 while runtime.running.load(Ordering::Acquire) {
441 let Ok((stream, peer_addr)) = listener.accept().await else {
442 continue;
443 };
444 let Ok(inbound_permit) = runtime.inbound_slots.clone().try_acquire_owned() else {
445 runtime
446 .stats
447 .connections_rejected
448 .fetch_add(1, Ordering::Relaxed);
449 continue;
450 };
451 let Ok(total_permit) = runtime.total_slots.clone().try_acquire_owned() else {
452 runtime
453 .stats
454 .connections_rejected
455 .fetch_add(1, Ordering::Relaxed);
456 continue;
457 };
458 let accepted_runtime = runtime.clone();
459 tokio::spawn(async move {
460 let _inbound_permit = inbound_permit;
461 let _total_permit = total_permit;
462 if let Err(error) = accept_connection(accepted_runtime, stream, peer_addr).await {
463 debug!(%peer_addr, %error, "WebSocket inbound connection ended");
464 }
465 });
466 }
467}
468
469#[allow(clippy::result_large_err)]
470async fn accept_connection(
471 runtime: Runtime,
472 stream: TcpStream,
473 peer_addr: SocketAddr,
474) -> Result<(), TransportError> {
475 let path = runtime.config.path().to_owned();
476 let callback = move |request: &Request, response: Response| {
477 if request.uri().path() == path {
478 Ok(response)
479 } else {
480 let mut error = ErrorResponse::new(Some("not found".into()));
481 *error.status_mut() = tokio_tungstenite::tungstenite::http::StatusCode::NOT_FOUND;
482 Err(error)
483 }
484 };
485 let websocket =
486 accept_hdr_async_with_config(stream, callback, Some(runtime.websocket_config()))
487 .await
488 .map_err(|_| TransportError::ConnectionRefused)?;
489 let generation = runtime.next_generation();
490 let addr = TransportAddr::from_string(&format!("ws-peer://{peer_addr}/{generation}"));
491 run_connection(
492 runtime,
493 addr,
494 websocket,
495 generation,
496 Direction::Inbound,
497 false,
498 )
499 .await
500}
501
502async fn run_seed_dialer(runtime: Runtime, addr: TransportAddr) {
503 let mut delay_ms = runtime.config.reconnect_initial_ms();
504 while runtime.running.load(Ordering::Acquire) {
505 runtime.set_state(&addr, ConnectionState::Connecting);
506 runtime
507 .stats
508 .reconnect_attempts
509 .fetch_add(1, Ordering::Relaxed);
510 let result = dial_and_run(runtime.clone(), addr.clone()).await;
511 if !runtime.running.load(Ordering::Acquire) {
512 break;
513 }
514 match result {
515 Ok(()) => {
516 delay_ms = runtime.config.reconnect_initial_ms();
517 }
518 Err(error) => {
519 runtime.set_state(&addr, ConnectionState::Failed(error.to_string()));
520 debug!(remote_addr = %addr, %error, "WebSocket seed connection failed");
521 delay_ms = delay_ms
522 .saturating_mul(2)
523 .min(runtime.config.reconnect_max_ms());
524 }
525 }
526 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
527 }
528}
529
530async fn run_one_shot_dial(runtime: Runtime, addr: TransportAddr) {
531 let result = dial_and_run(runtime.clone(), addr.clone()).await;
532 if let Err(error) = result {
533 runtime.set_state(&addr, ConnectionState::Failed(error.to_string()));
534 }
535}
536
537async fn dial_and_run(runtime: Runtime, addr: TransportAddr) -> Result<(), TransportError> {
538 let _slot = runtime
539 .total_slots
540 .clone()
541 .try_acquire_owned()
542 .map_err(|_| TransportError::ConnectionRefused)?;
543 let url = addr
544 .as_str()
545 .ok_or_else(|| TransportError::InvalidAddress(addr.to_string()))?;
546 let connect = connect_async_with_config(url, Some(runtime.websocket_config()), false);
547 let (websocket, _) = tokio::time::timeout(
548 Duration::from_millis(runtime.config.connect_timeout_ms()),
549 connect,
550 )
551 .await
552 .map_err(|_| TransportError::Timeout)?
553 .map_err(|error| TransportError::StartFailed(error.to_string()))?;
554 let generation = runtime.next_generation();
555 run_connection(
556 runtime,
557 addr,
558 websocket,
559 generation,
560 Direction::Outbound,
561 true,
562 )
563 .await
564}
565
566async fn run_connection<S>(
567 runtime: Runtime,
568 addr: TransportAddr,
569 websocket: WebSocketStream<S>,
570 generation: u64,
571 direction: Direction,
572 request_key_hint: bool,
573) -> Result<(), TransportError>
574where
575 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
576{
577 let (mut sink, mut stream) = websocket.split();
578 let (tx, mut rx) = mpsc::channel::<Vec<u8>>(runtime.config.max_send_queue());
579 {
580 let mut pool = runtime.pool.lock().await;
581 if pool.contains_key(&addr) {
582 return Err(TransportError::AlreadyStarted);
583 }
584 pool.insert(addr.clone(), Connection { generation, tx });
585 }
586 runtime.set_state(&addr, ConnectionState::Connected);
587 runtime
588 .stats
589 .connections_opened
590 .fetch_add(1, Ordering::Relaxed);
591
592 let mut pending_nonce = request_key_hint.then(|| rand::rng().random::<u64>());
593 if let Some(nonce) = pending_nonce {
594 sink.send(Message::Binary(
595 LocalKeyHint::Request { nonce }.encode().into(),
596 ))
597 .await
598 .map_err(|error| TransportError::SendFailed(error.to_string()))?;
599 }
600
601 let started = tokio::time::Instant::now();
602 let mut last_received = started;
603 let ping_secs = runtime.config.ping_interval_secs();
604 let idle_secs = runtime.config.idle_timeout_secs();
605 let mut ping = tokio::time::interval(if ping_secs == 0 {
606 Duration::from_secs(24 * 60 * 60)
607 } else {
608 Duration::from_secs(ping_secs)
609 });
610 ping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
611 let mut check = tokio::time::interval(Duration::from_secs(1));
612 check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
613
614 enum Event {
615 Outbound(Option<Vec<u8>>),
616 Inbound(Option<Result<Message, tokio_tungstenite::tungstenite::Error>>),
617 Ping,
618 Check,
619 }
620
621 let outcome = loop {
622 let event = tokio::select! {
623 outbound = rx.recv() => Event::Outbound(outbound),
624 inbound = stream.next() => Event::Inbound(inbound),
625 _ = ping.tick(), if ping_secs > 0 => Event::Ping,
626 _ = check.tick() => Event::Check,
627 };
628 match event {
629 Event::Outbound(Some(data)) => {
630 let len = data.len();
631 if let Err(error) = sink.send(Message::Binary(data.into())).await {
632 break Err(TransportError::SendFailed(error.to_string()));
633 }
634 runtime.stats.frames_sent.fetch_add(1, Ordering::Relaxed);
635 runtime
636 .stats
637 .bytes_sent
638 .fetch_add(len as u64, Ordering::Relaxed);
639 }
640 Event::Outbound(None) => break Ok(()),
641 Event::Inbound(Some(Ok(Message::Binary(data)))) => {
642 last_received = tokio::time::Instant::now();
643 if let Some(hint) = LocalKeyHint::decode(&data) {
644 match hint {
645 LocalKeyHint::Request { nonce } => {
646 let reply = LocalKeyHint::Response {
647 nonce,
648 pubkey: runtime.local_pubkey,
649 };
650 if let Err(error) =
651 sink.send(Message::Binary(reply.encode().into())).await
652 {
653 break Err(TransportError::SendFailed(error.to_string()));
654 }
655 }
656 LocalKeyHint::Response { nonce, pubkey }
657 if pending_nonce == Some(nonce) =>
658 {
659 pending_nonce = None;
660 if pubkey != runtime.local_pubkey
661 && let Ok(pubkey) = XOnlyPublicKey::from_slice(&pubkey)
662 {
663 runtime
664 .discoveries
665 .lock()
666 .unwrap_or_else(|error| error.into_inner())
667 .push_back(DiscoveredPeer::with_hint(
668 runtime.transport_id,
669 addr.clone(),
670 pubkey,
671 ));
672 }
673 }
674 LocalKeyHint::Response { .. } => {}
675 }
676 continue;
677 }
678 if data.len() > runtime.config.max_frame_bytes()
679 || validate_websocket_record(&data).is_err()
680 {
681 runtime.stats.invalid_frames.fetch_add(1, Ordering::Relaxed);
682 break Err(TransportError::RecvFailed(
683 "invalid WebSocket FIPS physical record".into(),
684 ));
685 }
686 let len = data.len();
687 let packet = ReceivedPacket::with_timestamp(
688 runtime.transport_id,
689 addr.clone(),
690 PacketBuffer::new(data.to_vec()),
691 now_ms(),
692 );
693 if runtime.packet_tx.send(packet).is_err() {
694 break Err(TransportError::RecvFailed(
695 "node packet channel closed".into(),
696 ));
697 }
698 runtime
699 .stats
700 .frames_received
701 .fetch_add(1, Ordering::Relaxed);
702 runtime
703 .stats
704 .bytes_received
705 .fetch_add(len as u64, Ordering::Relaxed);
706 }
707 Event::Inbound(Some(Ok(Message::Ping(payload)))) => {
708 last_received = tokio::time::Instant::now();
709 if let Err(error) = sink.send(Message::Pong(payload)).await {
710 break Err(TransportError::SendFailed(error.to_string()));
711 }
712 }
713 Event::Inbound(Some(Ok(Message::Pong(_)))) => {
714 last_received = tokio::time::Instant::now();
715 }
716 Event::Inbound(Some(Ok(Message::Close(_))) | None) => break Ok(()),
717 Event::Inbound(Some(Ok(Message::Text(_) | Message::Frame(_)))) => {
718 runtime.stats.invalid_frames.fetch_add(1, Ordering::Relaxed);
719 break Err(TransportError::RecvFailed(
720 "WebSocket transport accepts binary messages only".into(),
721 ));
722 }
723 Event::Inbound(Some(Err(error))) => {
724 break Err(TransportError::RecvFailed(error.to_string()));
725 }
726 Event::Ping => {
727 if let Err(error) = sink.send(Message::Ping(Bytes::new())).await {
728 break Err(TransportError::SendFailed(error.to_string()));
729 }
730 }
731 Event::Check => {
732 if pending_nonce.is_some()
733 && started.elapsed()
734 >= Duration::from_millis(runtime.config.key_hint_timeout_ms())
735 {
736 break Err(TransportError::Timeout);
737 }
738 if idle_secs > 0 && last_received.elapsed() >= Duration::from_secs(idle_secs) {
739 break Err(TransportError::Timeout);
740 }
741 }
742 }
743 };
744
745 {
746 let mut pool = runtime.pool.lock().await;
747 if pool
748 .get(&addr)
749 .is_some_and(|connection| connection.generation == generation)
750 {
751 pool.remove(&addr);
752 }
753 }
754 runtime.clear_state_if(&addr, generation);
755 runtime
756 .stats
757 .connections_closed
758 .fetch_add(1, Ordering::Relaxed);
759 debug!(
760 transport_id = %runtime.transport_id,
761 remote_addr = %addr,
762 ?direction,
763 "WebSocket physical connection closed"
764 );
765 outcome
766}
767
768fn now_ms() -> u64 {
769 SystemTime::now()
770 .duration_since(UNIX_EPOCH)
771 .unwrap_or_default()
772 .as_millis() as u64
773}
774
775fn validate_websocket_record(data: &[u8]) -> Result<(), String> {
776 if validate_direct_fsp_transport_fragment(data) {
777 return Ok(());
778 }
779 validate_stream_record(data)
780 .map_err(|error| format!("invalid FIPS physical record: {error}"))
781}
782
783#[cfg(test)]
784mod tests;