Skip to main content

confium_net_ws/
listener.rs

1//! Listening endpoint for `ws://` URLs.
2//!
3//! [`WsListener`] wraps [`std::net::TcpListener`] and implements
4//! [`confium_net::Listener`]. Each [`accept`](confium_net::Listener::accept)
5//! blocks for an inbound TCP connection, then performs the WebSocket
6//! server-side handshake ([`tungstenite::accept`]) to upgrade it,
7//! returning a [`crate::WsTransport`] wrapping the upgraded
8//! [`tungstenite::WebSocket`].
9//!
10//! Server-side TLS for `wss://` is not implemented here; a
11//! TLS-terminating reverse proxy (nginx, Caddy, etc.) in front of a
12//! plain `ws://` listener is the recommended deployment.
13
14use std::net::Ipv4Addr;
15use std::net::SocketAddr;
16use std::net::TcpListener as StdTcpListener;
17
18use confium_net::Listener;
19use confium_net::Result;
20use confium_net::Transport;
21use confium_net::error::ClosedSnafu;
22
23use crate::WsTransport;
24
25/// Listening endpoint for inbound WebSocket connections.
26///
27/// Holding this alive keeps the bound TCP socket open; dropping it
28/// closes the socket (the OS releases the port).
29pub struct WsListener {
30    inner: Option<StdTcpListener>,
31}
32
33impl WsListener {
34    /// Bind a new listener at `host:port`. The address family is
35    /// always IPv4 (matching the loopback test pattern used by
36    /// `confium-net-tcp`); pass `0.0.0.0` for any interface or
37    /// `127.0.0.1` for loopback-only. Passing port `0` requests an
38    /// ephemeral port from the OS; the caller can read the assigned
39    /// port back via [`local_addr`](Self::local_addr).
40    pub fn bind(scheme: &str, host: &str, port: u16) -> std::io::Result<Self> {
41        // Both `ws` and `wss` URLs may be passed here. For `wss://`
42        // we expect a reverse proxy to terminate TLS upstream and
43        // forward plain WebSocket frames; the listener itself always
44        // binds a plain TCP socket.
45        let _ = scheme;
46
47        // Resolve `host` as an IPv4 literal. The loopback test path
48        // uses `127.0.0.1`; `0.0.0.0` is the all-interfaces
49        // wildcard. DNS names are not supported at the listener —
50        // production deployments front the listener with a proxy
51        // that handles name-based routing.
52        let ip: Ipv4Addr = host.parse().map_err(|_| {
53            std::io::Error::new(
54                std::io::ErrorKind::InvalidInput,
55                format!("invalid IPv4 bind address '{host}'"),
56            )
57        })?;
58        let listener = StdTcpListener::bind(SocketAddr::new(std::net::IpAddr::V4(ip), port))?;
59        listener.set_nonblocking(false).ok();
60        Ok(Self {
61            inner: Some(listener),
62        })
63    }
64
65    /// The locally-bound socket address. Useful for reading back the
66    /// OS-assigned ephemeral port after binding with port `0`.
67    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
68        self.inner
69            .as_ref()
70            .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotConnected))?
71            .local_addr()
72    }
73}
74
75impl Listener for WsListener {
76    fn accept(&mut self) -> Result<Box<dyn Transport>> {
77        let listener = match &self.inner {
78            Some(l) => l,
79            None => return ClosedSnafu.fail(),
80        };
81        let (stream, _peer) = listener.accept().map_err(crate::transport::io_to_closed)?;
82        // Disable Nagle on accepted streams for the same latency
83        // reasons as dial-out peers (TC round messages are small and
84        // bursty — see TODO.roadmap/05 §Performance).
85        stream.set_nodelay(true).ok();
86        // Upgrade the raw TCP stream to a WebSocket by performing the
87        // server-side handshake (RFC 6455 §4.2).
88        let ws = tungstenite::accept(stream).map_err(crate::transport::handshake_to_closed)?;
89        Ok(Box::new(WsTransport::from_server(ws)))
90    }
91}
92
93impl Drop for WsListener {
94    fn drop(&mut self) {
95        // Dropping the inner listener closes the socket; take() is
96        // for explicitness so a future `close` method can report
97        // errors.
98        self.inner.take();
99    }
100}