rdsh 0.7.0

Rust port/shim for dsh: fast launcher, token saver, zero behavior breakage via passthrough
//! `rdsh setup --web`: floating first-run connect UI on localhost.
//! One-shot local server (127.0.0.1 only): Apple-style glass page showing
//! OAuth/API-key status. Writes only on explicit key submit (allowlisted
//! ref names); browser auto-opens on interactive terminals.

use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;

const HTML: &str = include_str!("setup.html");
const ICON: &str = include_str!("../assets/icon.svg");

pub fn cmd_setup_web(port: u16, locale: crate::locale::Locale) -> anyhow::Result<()> {
    let listener = std::net::TcpListener::bind(format!("127.0.0.1:{port}")).map_err(|e| {
        anyhow::anyhow!(
            "{} 127.0.0.1:{port}: {e}",
            match locale {
                crate::locale::Locale::English => "cannot listen on",
                crate::locale::Locale::Chinese => "无法监听",
                crate::locale::Locale::Japanese => "待受できません",
            }
        )
    })?;
    listener.set_nonblocking(true)?;
    crate::shutdown::install(locale)?;
    let port = listener.local_addr()?.port();
    let token = Arc::new(crate::local_http::random_token()?);
    let handoff = crate::token_handoff::TokenHandoff::create(&token, locale)?;
    let base = format!("http://127.0.0.1:{port}/");
    eprintln!(
        "[rdsh setup] {}: {base} ({})",
        match locale {
            crate::locale::Locale::English => "floating UI",
            crate::locale::Locale::Chinese => "浮动界面",
            crate::locale::Locale::Japanese => "フローティングUI",
        },
        match locale {
            crate::locale::Locale::English => "localhost only, Ctrl-C to stop",
            crate::locale::Locale::Chinese => "仅限 localhost,按 Ctrl-C 停止",
            crate::locale::Locale::Japanese => "localhost のみ、Ctrl-C で停止",
        }
    );
    eprintln!(
        "[rdsh setup] {}: {}",
        match locale {
            crate::locale::Locale::English => "token handoff file",
            crate::locale::Locale::Chinese => "令牌交接文件",
            crate::locale::Locale::Japanese => "トークン引き渡しファイル",
        },
        handoff.path().display()
    );
    eprintln!(
        "[rdsh setup] {}",
        match locale {
            crate::locale::Locale::English =>
                "enter the file contents in the setup page; the token is never printed",
            crate::locale::Locale::Chinese => "请在设置页面中输入文件内容;令牌绝不会打印",
            crate::locale::Locale::Japanese =>
                "セットアップページでファイル内容を入力してください。トークンは表示されません",
        }
    );
    use std::io::IsTerminal as _;
    if std::io::stdin().is_terminal() {
        // Keep the bearer token out of the browser-launch argv.
        if let Err(e) = crate::auth::open_browser(&base, locale) {
            eprintln!(
                "[rdsh setup] {} ({e:#}); {}",
                match locale {
                    crate::locale::Locale::English => "could not open browser",
                    crate::locale::Locale::Chinese => "无法打开浏览器",
                    crate::locale::Locale::Japanese => "ブラウザを開けませんでした",
                },
                match locale {
                    crate::locale::Locale::English => "open the URL manually",
                    crate::locale::Locale::Chinese => "请手动打开 URL",
                    crate::locale::Locale::Japanese => "URL を手動で開いてください",
                }
            );
        }
    }
    let done = Arc::new(AtomicBool::new(false));
    let connections = Arc::new(AtomicUsize::new(0));
    loop {
        if done.load(Ordering::Relaxed) || crate::shutdown::requested() {
            break;
        }
        match listener.accept() {
            Ok((mut s, _)) => {
                let Some(slot) = crate::local_http::ConnectionSlot::acquire(&connections) else {
                    let _ = crate::local_http::respond(
                        &mut s,
                        503,
                        "application/json",
                        "{\"error\":\"busy\"}",
                    );
                    continue;
                };
                let d = done.clone();
                let token = token.clone();
                std::thread::spawn(move || {
                    let _slot = slot;
                    if let Err(e) = handle(s, &d, &token, port, locale) {
                        eprintln!("[rdsh setup] {e:#}");
                    }
                });
            }
            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
                std::thread::sleep(std::time::Duration::from_millis(100));
            }
            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
            Err(e) => {
                eprintln!("[rdsh setup] accept: {e:#}");
                break;
            }
        }
    }
    Ok(())
}

fn handle(
    mut s: std::net::TcpStream,
    done: &AtomicBool,
    token: &str,
    port: u16,
    locale: crate::locale::Locale,
) -> anyhow::Result<()> {
    use std::borrow::Cow;
    s.set_read_timeout(Some(std::time::Duration::from_secs(10)))?;
    let req = match crate::local_http::read_request(&mut s) {
        Ok(req) => req,
        Err(crate::local_http::ReadRequestError::Idle) => return Ok(()),
        Err(crate::local_http::ReadRequestError::Status(status)) => {
            crate::local_http::respond(
                &mut s,
                status,
                "application/json",
                "{\"error\":\"invalid request\"}",
            )?;
            return Ok(());
        }
    };
    if !req.trusted(port) {
        crate::local_http::respond(
            &mut s,
            403,
            "application/json",
            "{\"error\":\"untrusted host or origin\"}",
        )?;
        return Ok(());
    }
    let finish_after_response = req.method.as_str() == "POST"
        && req.target.as_str() == "/api/done"
        && req.authorized(token);
    if req.method.as_str() == "GET" {
        let asset = match req.target.as_str() {
            "/icon.png" => Some((
                "image/png",
                include_bytes!("../assets/icon-256.png").as_slice(),
            )),
            "/favicon.ico" => Some((
                "image/x-icon",
                include_bytes!("../assets/icon.ico").as_slice(),
            )),
            _ => None,
        };
        if let Some((ctype, bytes)) = asset {
            crate::local_http::respond(&mut s, 200, ctype, bytes)?;
            return Ok(());
        }
    }
    let (status, ctype, payload): (u16, &str, Cow<str>) =
        match (req.method.as_str(), req.target.as_str()) {
            ("GET", "/") => (
                200,
                "text/html; charset=utf-8",
                Cow::Owned(HTML.replace(
                    "__RDSH_LOCALE__",
                    match locale {
                        crate::locale::Locale::English => "en",
                        crate::locale::Locale::Chinese => "zh-CN",
                        crate::locale::Locale::Japanese => "ja",
                    },
                )),
            ),
            ("GET", "/icon.svg") => (200, "image/svg+xml", Cow::Borrowed(ICON)),
            ("GET", "/api/status") if !req.authorized(token) => (
                401,
                "application/json",
                Cow::Borrowed("{\"error\":\"unauthorized\"}"),
            ),
            ("GET", "/api/status") => (
                200,
                "application/json",
                Cow::Owned(crate::auth::setup_status_json()),
            ),
            ("POST", "/api/key") if !req.authorized(token) => (
                401,
                "application/json",
                Cow::Borrowed("{\"error\":\"unauthorized\"}"),
            ),
            ("POST", "/api/key") if req.header("content-type") != Some("application/json") => (
                400,
                "application/json",
                Cow::Borrowed("{\"error\":\"JSON required\"}"),
            ),
            ("POST", "/api/key") => match store_key_body(&req.body) {
                Ok(stored) => (
                    200,
                    "application/json",
                    Cow::Owned(serde_json::json!({"stored": stored}).to_string()),
                ),
                Err(e) => (
                    400,
                    "application/json",
                    Cow::Owned(serde_json::json!({"error": e.to_string()}).to_string()),
                ),
            },
            ("GET", "/api/extras") if !req.authorized(token) => (
                401,
                "application/json",
                Cow::Borrowed("{\"error\":\"unauthorized\"}"),
            ),
            ("GET", "/api/extras") => {
                // Report the effective state: an explicit features.<id> wins
                // over the legacy extras list (issue #384).
                let cfg = crate::rdsh_config::load();
                (
                    200,
                    "application/json",
                    Cow::Owned(
                        serde_json::json!({"enable": crate::rdsh_config::enabled_extras(&cfg)})
                            .to_string(),
                    ),
                )
            }
            ("POST", "/api/extras") if !req.authorized(token) => (
                401,
                "application/json",
                Cow::Borrowed("{\"error\":\"unauthorized\"}"),
            ),
            ("POST", "/api/extras") => match store_extras_body(&req.body) {
                Ok(enable) => (
                    200,
                    "application/json",
                    Cow::Owned(serde_json::json!({"enable": enable}).to_string()),
                ),
                Err(e) => (
                    400,
                    "application/json",
                    Cow::Owned(serde_json::json!({"error": e.to_string()}).to_string()),
                ),
            },
            ("POST", "/api/done") if !req.authorized(token) => (
                401,
                "application/json",
                Cow::Borrowed("{\"error\":\"unauthorized\"}"),
            ),
            ("POST", "/api/done") => (200, "application/json", Cow::Borrowed("{\"ok\":true}")),
            _ => (
                404,
                "application/json",
                Cow::Borrowed("{\"error\":\"not found\"}"),
            ),
        };
    crate::local_http::respond(&mut s, status, ctype, payload.as_bytes())?;
    // The main thread can exit as soon as it observes done. Send the complete
    // acknowledgement first, and keep serving if writing the response fails.
    if finish_after_response {
        done.store(true, Ordering::Relaxed);
    }
    Ok(())
}

fn store_extras_body(body: &str) -> anyhow::Result<Vec<String>> {
    let v: serde_json::Value = serde_json::from_str(body)?;
    let arr = v
        .get("enable")
        .and_then(|x| x.as_array())
        .ok_or_else(|| anyhow::anyhow!("enable must be an array of extra names"))?;
    if arr.iter().any(|x| !x.is_string()) {
        anyhow::bail!("enable must be an array of extra names");
    }
    let requested: Vec<String> = arr
        .iter()
        .filter_map(|x| x.as_str())
        .map(|s| s.trim().to_string())
        .collect();
    let mut enable = Vec::new();
    crate::rdsh_config::update(crate::locale::Locale::English, |cfg| {
        // The setup switches are authoritative: write the canonical feature key
        // for every known extra and keep the legacy list in sync (issue #384).
        for id in crate::rdsh_config::KNOWN_EXTRAS {
            let on = requested.iter().any(|s| s == id);
            if on {
                enable.push((*id).to_string());
            }
            cfg.set_dotted(&format!("features.{id}"), if on { "true" } else { "false" })?;
        }
        cfg.extras.enable = enable.clone();
        Ok(())
    })?;
    Ok(enable)
}

fn store_key_body(body: &str) -> anyhow::Result<bool> {
    let v: serde_json::Value = serde_json::from_str(body)?;
    let name = v.get("name").and_then(|x| x.as_str()).unwrap_or("");
    let value = v.get("value").and_then(|x| x.as_str()).unwrap_or("");
    crate::auth::setup_store_key(name, value)
}