// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The address a server listens on when none is given: loopback only, as uvicorn and Starlette
/// default to, so a development server is not reachable from the network by accident.
pub let host : String = "127.0.0.1"
///|
/// The port every server in the family listens on by default.
pub let port : Int32 = 12000
///|
/// How long a TLS handshake may take, in milliseconds: asyncio's and hypercorn's 60 seconds. A
/// peer that connects and never finishes holds a connection slot until then.
pub let handshake : Int32 = 60000
///|
/// A bound, listening socket. Connections queue from the moment `listen` returns, so a caller
/// can connect to it, or hand its port out, without racing the server.
struct Listener {
server : @socket.TcpServer
tls : @tls.Server?
handshake : Int32?
cleartext : Bool
}
///|
/// Bind `host:port` and listen, for TLS connections when `tls` is given. Port 0 asks the system
/// for a free one; `addr` says which. `reuse_addr` lets a restarted server bind while the old
/// socket lingers in TIME_WAIT, as uvicorn does. `handshake` bounds each TLS handshake; `None`
/// waits for ever.
///
/// `cleartext` lets a TLS listener take connections in the clear too, on the same port: one that
/// opens with a TLS handshake record is served TLS, any other is left in the clear. Off, a TLS
/// listener takes TLS only.
pub async fn listen(
host~ : String = host,
port~ : Int32 = port,
tls? : @tls.Server,
reuse_addr~ : Bool = true,
handshake~ : Int32? = Some(handshake),
cleartext~ : Bool = false,
) -> Listener {
let server = @socket.TcpServer(
@socket.Addr::parse(join(host, port)),
reuse_addr~,
)
{ server, tls, handshake, cleartext, }
}
///|
/// The address listened on; with port 0 asked for, the port the system chose.
pub fn Listener::addr(self : Listener) -> @socket.Addr {
self.server.addr
}
///|
/// A connection as the handler gets it: TLS done within the handshake bound, when there is TLS.
async fn Listener::open(self : Listener, sock : @socket.Tcp) -> Conn {
guard self.tls is Some(config) else { return Conn::plain(sock) }
let open = async fn() {
if !self.cleartext {
return Conn::server(sock, config)
}
let conn = Conn::plain(sock)
if conn.opens_tls() {
conn.secure(config)
} else {
conn
}
}
match self.handshake {
Some(ms) => @async.with_timeout(ms, open)
None => open()
}
}
///|
/// The next connection and its peer's address, after the TLS handshake when there is TLS. A
/// handshake that fails or runs out of time raises, with the socket closed.
pub async fn Listener::accept(self : Listener) -> (Conn, @socket.Addr) {
let (sock, peer) = self.server.accept()
errdefer sock.close()
(self.open(sock), peer)
}
///|
/// Run `handler` on each connection with the peer's address. When the handler returns, a TLS
/// connection is shut down with a close_notify. A connection whose handshake fails, or whose
/// handler raises, is closed without stopping the server. Runs until cancelled, then closes the
/// listener.
///
/// `limit` caps the connections served at once; the rest wait in the listen queue.
pub async fn Listener::serve(
self : Listener,
handler : async (Conn, @socket.Addr) -> Unit,
limit? : Int32,
) -> Unit {
self.server.run_forever(
(sock, peer) => {
let conn = self.open(sock)
handler(conn, peer)
conn.shutdown() catch {
_ => ()
}
},
max_connections?=limit,
)
}
///|
/// Stop listening.
pub fn Listener::close(self : Listener) -> Unit {
self.server.close()
}
///|
/// `listen`, then `serve`: accept connections on `host:port` and run `handler` on each until
/// cancelled.
pub async fn serve(
handler : async (Conn, @socket.Addr) -> Unit,
host~ : String = host,
port~ : Int32 = port,
tls? : @tls.Server,
limit? : Int32,
reuse_addr~ : Bool = true,
handshake~ : Int32? = Some(handshake),
) -> Unit {
listen(host~, port~, tls?, reuse_addr~, handshake~).serve(handler, limit?)
}
///|
/// Connect to `host:port`, under TLS when `tls` is given, and return once the handshake is done.
/// The host is resolved, then names the server for SNI and for checking its certificate.
pub async fn dial(
host : String,
port~ : Int32,
tls? : @tls.Client,
now? : Int64,
) -> Conn {
let sock = @socket.Tcp::connect_to_host(host, port~)
errdefer sock.close()
match tls {
Some(config) => Conn::client(sock, config, host, now?)
None => Conn::plain(sock)
}
}
///|
/// `host:port`, with an IPv6 literal in brackets (RFC 3986 ยง3.2.2).
fn join(host : String, port : Int32) -> String {
if host.contains(":") && !host.has_prefix("[") {
"[\{host}]:\{port}"
} else {
"\{host}:\{port}"
}
}