Skip to main content

liminal_server/server/connection/websocket/
bound.rs

1//! SRV-044 R1: the WebSocket address binds with its config alone, before
2//! the services and the supervisor exist, and
3//! [`WebSocketListener::accept_on`](super::WebSocketListener::accept_on)
4//! starts the accept worker on it once they do.
5//!
6//! Between the two, the kernel completes a client's connect and holds it, with
7//! its upgrade request, in the listen backlog; the upgrade is read once
8//! accepting starts. Nothing is refused for starting and nothing waits on a
9//! timer.
10use std::net::{SocketAddr, TcpListener};
11use std::time::Duration;
12
13use super::AcceptorSettings;
14use crate::ServerError;
15use crate::config::types::WebSocketConfig;
16
17#[cfg(test)]
18#[path = "bound_tests.rs"]
19pub(in crate::server) mod tests;
20
21/// The configured WebSocket address, bound and not yet accepting.
22#[derive(Debug)]
23pub struct BoundWebSocket {
24    pub(super) listener: TcpListener,
25    pub(super) local_addr: SocketAddr,
26    pub(super) settings: AcceptorSettings,
27}
28
29impl BoundWebSocket {
30    /// Binds the configured WebSocket listen address. No thread starts and no
31    /// supervisor is needed.
32    ///
33    /// # Errors
34    /// Returns [`ServerError::ListenerBind`] when the configured address cannot
35    /// bind, and [`ServerError::ListenerAccept`] when the bound address cannot
36    /// be read or the build target cannot represent the pinned liminal frame
37    /// bound.
38    pub fn bind(config: &WebSocketConfig) -> Result<Self, ServerError> {
39        let settings = AcceptorSettings {
40            path: config.path.clone(),
41            allowed_origins: config.allowed_origins.clone(),
42            allowed_origin_ports: config.allowed_origin_ports.clone(),
43            ping_interval: config.ping_interval_ms.map(Duration::from_millis),
44            message_bound: super::liminal_ws_message_bound()?,
45        };
46        let listener = TcpListener::bind(config.listen_address).map_err(|source| {
47            ServerError::ListenerBind {
48                address: config.listen_address,
49                source,
50            }
51        })?;
52        // The listener stays BLOCKING (W4 leg 1): the accept worker kernel-parks
53        // in `accept` with zero idle wakes; shutdown wakes it via the self-connect
54        // interrupt.
55        let local_addr = listener
56            .local_addr()
57            .map_err(|error| ServerError::ListenerAccept {
58                message: format!("failed to inspect websocket listener address: {error}"),
59            })?;
60        tracing::info!(
61            websocket_listen_address = %local_addr,
62            upgrade_path = %settings.path,
63            allowed_origins = settings.allowed_origins.len(),
64            allowed_origin_ports = settings.allowed_origin_ports.len(),
65            keepalive = ?settings.ping_interval,
66            "liminal websocket listener bound"
67        );
68        Ok(Self {
69            listener,
70            local_addr,
71            settings,
72        })
73    }
74
75    /// The bound address, an ephemeral port resolved.
76    #[must_use]
77    pub const fn local_addr(&self) -> SocketAddr {
78        self.local_addr
79    }
80}