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}