From 2d421a9b9fa776cc452de27e9d1f9f72869435b6 Mon Sep 17 00:00:00 2001 From: 81reap Date: Thu, 24 Sep 2026 01:21:30 -0400 Subject: [PATCH] fix(server) :: report the bounded port Before the startup banner was built from the configuration not the one SQLPage was bound to. Setting `port` to `0`, which asks the operating system for a free port, announced `http://127.0.0.1:0`. Now it reports `HttpServer::addrs()`. --- CHANGELOG.md | 1 + src/webserver/http.rs | 64 ++++++++++++++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b68788..df2786e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - OIDC now checks both normalized request paths and their resolved SQL files against protected prefixes, closing authentication bypasses through path and clean-URL aliases. Nonce verification also rejects provider-returned Argon2 parameters outside SQLPage's fixed low-cost profile before hashing. - `cargo install sqlpage`, and any build from the crates.io tarball, no longer needs internet access. The browser libraries now come from npm and ship inside the published crate. Building from a git checkout needs `npm ci` first. Pre-built binaries and the Docker image are unaffected. - The browser libraries are now part of the browser scripts. SQLPage no longer defines the `window.tabler` and `window.bootstrap` globals; custom scripts that reached for them should load their own copy of Bootstrap. +- The startup message now reports the address the server actually bound instead of the one it was configured with. ## v0.46.3 diff --git a/src/webserver/http.rs b/src/webserver/http.rs index 76e48af2..8448b9dc 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -40,6 +40,7 @@ use chrono::{DateTime, Utc}; use futures_util::StreamExt; use futures_util::stream::Stream; use std::borrow::Cow; +use std::net::SocketAddr; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; @@ -680,7 +681,7 @@ pub async fn run_server(config: &AppConfig, state: AppState) -> anyhow::Result<( } } - log_welcome_message(config); + log_welcome_message(config, &server.addrs()); server .run() .await @@ -691,25 +692,33 @@ pub async fn run_server(config: &AppConfig, state: AppState) -> anyhow::Result<( Ok(()) } -fn log_welcome_message(config: &AppConfig) { +fn website_url(bound_to: SocketAddr) -> String { + let port = bound_to.port(); + let ip = bound_to.ip(); + if ip.is_unspecified() { + format!( + "http://localhost:{port}\n\ + (also accessible from other devices using your IP address)" + ) + } else if ip.is_ipv6() { + format!("http://[{ip}]:{port}") + } else { + format!("http://{ip}:{port}") + } +} + +fn log_welcome_message(config: &AppConfig, bound_to: &[SocketAddr]) { let address_message = if let Some(unix_socket) = &config.unix_socket { format!("unix socket \"{}\"", unix_socket.display()) } else if let Some(domain) = &config.https_domain { format!("https://{domain}") } else { - let listen_on = config.listen_on(); - let port = listen_on.port(); - let ip = listen_on.ip(); - if ip.is_unspecified() { - format!( - "http://localhost:{port}\n\ - (also accessible from other devices using your IP address)" - ) - } else if ip.is_ipv6() { - format!("http://[{ip}]:{port}") - } else { - format!("http://{ip}:{port}") - } + bound_to + .iter() + .copied() + .map(website_url) + .collect::>() + .join("\n") }; let (sparkle, link, computer, rocket) = if cfg!(target_os = "windows") { @@ -747,10 +756,33 @@ fn bind_unix_socket_err(e: std::io::Error, unix_socket: &std::path::Path) -> any #[cfg(test)] mod tests { - use super::{request_span_name, sql_execution_span_name}; + use super::{request_span_name, sql_execution_span_name, website_url}; use actix_web::test::TestRequest; use std::path::Path; + #[test] + fn website_url_reports_the_address_the_server_bound() { + assert_eq!( + website_url("127.0.0.1:34567".parse().unwrap()), + "http://127.0.0.1:34567" + ); + } + + #[test] + fn website_url_sends_an_unspecified_address_to_localhost() { + assert!( + website_url("0.0.0.0:8080".parse().unwrap()).starts_with("http://localhost:8080\n") + ); + } + + #[test] + fn website_url_brackets_an_ipv6_address() { + assert_eq!( + website_url("[::1]:8080".parse().unwrap()), + "http://[::1]:8080" + ); + } + #[test] fn request_span_name_uses_request_path_when_no_matched_route_exists() { let request = TestRequest::with_uri("/todos/42?filter=open").to_srv_request();