typeduck-codex-web 0.1.0

A browser interface for Codex CLI sessions
Documentation
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::net::SocketAddr;
use std::time::Duration;

use tokio::net::TcpListener;
use tokio::process::Command;
use tokio::time::timeout;

pub(crate) async fn bind_listeners(
    requested_port: u16,
) -> std::io::Result<(Vec<TcpListener>, u16)> {
    let primary = TcpListener::bind((Ipv4Addr::LOCALHOST, requested_port)).await?;
    let port = primary.local_addr()?.port();
    let mut listeners = vec![primary];

    for address in detect_tailnet_ips()
        .await
        .into_iter()
        .chain([IpAddr::V6(Ipv6Addr::LOCALHOST)])
    {
        if let Ok(listener) = TcpListener::bind(SocketAddr::new(address, port)).await {
            listeners.push(listener);
        }
    }
    Ok((listeners, port))
}

async fn detect_tailnet_ips() -> Vec<IpAddr> {
    let mut addresses = Vec::new();
    for family in ["-4", "-6"] {
        let mut command = Command::new("tailscale");
        command.args(["ip", family]).kill_on_drop(true);
        let Ok(Ok(output)) = timeout(Duration::from_millis(750), command.output()).await else {
            continue;
        };
        if output.status.success() {
            addresses.extend(
                String::from_utf8_lossy(&output.stdout)
                    .lines()
                    .filter_map(|line| line.trim().parse::<IpAddr>().ok()),
            );
        }
    }
    addresses.sort_unstable();
    addresses.dedup();
    addresses
}

pub(crate) fn authority(address: SocketAddr) -> String {
    match address.ip() {
        IpAddr::V4(ip) => format!("{ip}:{}", address.port()),
        IpAddr::V6(ip) => format!("[{ip}]:{}", address.port()),
    }
}

#[cfg(test)]
#[path = "network_tests.rs"]
mod tests;