static-web-server 3.0.0-beta.2

A cross-platform, high-performance and asynchronous web server for static files-serving.
// SPDX-License-Identifier: MIT OR Apache-2.0
// This file is part of Static Web Server.
// See https://static-web-server.net/ for more information
// Copyright (C) 2019-present Jose Quintana <joseluisq.net>

//! Default browser launcher for the TCP server implementations.

use std::net::SocketAddr;

use super::ServerRunConfig;

pub(super) fn listener_url(
    scheme: &str,
    listener: &tokio::net::TcpListener,
    run_cfg: &ServerRunConfig<'_>,
) -> Option<String> {
    if !run_cfg.open {
        return None;
    }

    match listener.local_addr() {
        Ok(addr) => Some(browser_url(scheme, addr, run_cfg.open_path)),
        Err(err) => {
            tracing::warn!(%err, "failed to determine listener address for browser URL");
            None
        }
    }
}

pub(super) fn open(url: String) {
    let spawn_result = std::thread::Builder::new()
        .name("sws-browser".to_owned())
        .spawn(move || match open::that_detached(&url) {
            Ok(()) => {
                tracing::info!(%url, "opened server URL in web browser");
            }
            Err(err) => {
                tracing::warn!(%url, %err, "failed to open server URL in web browser");
            }
        });

    if let Err(err) = spawn_result {
        tracing::warn!(%err, "failed to spawn browser launcher thread");
    }
}

#[must_use]
fn browser_url(scheme: &str, addr: SocketAddr, path: Option<&str>) -> String {
    let addr = if addr.ip().is_unspecified() {
        format!("localhost:{}", addr.port())
    } else {
        addr.to_string()
    };

    match path {
        Some(path) if path.starts_with('/') => format!("{scheme}://{addr}{path}"),
        Some(path) => format!("{scheme}://{addr}/{path}"),
        None => format!("{scheme}://{addr}/"),
    }
}

#[cfg(test)]
mod tests {
    use super::{ServerRunConfig, browser_url, listener_url};

    #[tokio::test]
    async fn disabled_browser_launch_has_no_url() {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let run_cfg = ServerRunConfig {
            addr_str: "127.0.0.1:0",
            threads: 1,
            open: false,
            open_path: Some("/docs"),
            conn: Default::default(),
        };

        assert_eq!(listener_url("http", &listener, &run_cfg), None);
    }

    #[tokio::test]
    async fn enabled_browser_launch_uses_configured_path() {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let port = listener.local_addr().unwrap().port();
        let run_cfg = ServerRunConfig {
            addr_str: "127.0.0.1:0",
            threads: 1,
            open: true,
            open_path: Some("/docs"),
            conn: Default::default(),
        };

        assert_eq!(
            listener_url("http", &listener, &run_cfg),
            Some(format!("http://127.0.0.1:{port}/docs"))
        );
    }

    #[test]
    fn ipv4_unspecified_address_uses_localhost() {
        let addr = "0.0.0.0:8080".parse().unwrap();
        assert_eq!(browser_url("http", addr, None), "http://localhost:8080/");
    }

    #[test]
    fn ipv6_unspecified_address_uses_localhost() {
        let addr = "[::]:8080".parse().unwrap();
        assert_eq!(browser_url("http", addr, None), "http://localhost:8080/");
    }

    #[test]
    fn https_unspecified_address_uses_localhost() {
        let addr = "[::]:8443".parse().unwrap();
        assert_eq!(browser_url("https", addr, None), "https://localhost:8443/");
    }

    #[test]
    fn https_url_preserves_bound_port() {
        let addr = "127.0.0.1:8443".parse().unwrap();
        assert_eq!(browser_url("https", addr, None), "https://127.0.0.1:8443/");
    }

    #[test]
    fn configured_path_is_appended_to_url() {
        let addr = "127.0.0.1:8080".parse().unwrap();
        assert_eq!(
            browser_url("http", addr, Some("/docs")),
            "http://127.0.0.1:8080/docs"
        );
    }

    #[test]
    fn configured_path_without_leading_slash_is_normalized() {
        let addr = "127.0.0.1:8080".parse().unwrap();
        assert_eq!(
            browser_url("http", addr, Some("docs")),
            "http://127.0.0.1:8080/docs"
        );
    }
}