Skip to main content

crw_server/
setup.rs

1//! Interactive setup command that downloads LightPanda and creates a local config.
2
3use sha2::{Digest, Sha256};
4use std::env::consts::{ARCH, OS};
5use std::path::PathBuf;
6
7const LIGHTPANDA_BASE_URL: &str =
8    "https://github.com/lightpanda-io/browser/releases/download/nightly";
9
10/// Run the interactive setup: download LightPanda binary and create config.
11pub async fn run_setup() {
12    println!();
13    let (os_label, arch_label, binary_name) = match (OS, ARCH) {
14        ("linux", "x86_64") => ("Linux", "x86_64", "lightpanda-x86_64-linux"),
15        ("macos", "aarch64") => ("macOS", "aarch64", "lightpanda-aarch64-macos"),
16        _ => {
17            eprintln!("  ✗ Unsupported platform: {OS} {ARCH}");
18            eprintln!("    LightPanda provides binaries for Linux x86_64 and macOS aarch64.");
19            std::process::exit(1);
20        }
21    };
22
23    println!("  → Detected: {os_label} {arch_label}");
24
25    // Download LightPanda binary.
26    let install_dir = home_local_bin();
27    let install_path = install_dir.join("lightpanda");
28
29    println!("  → Downloading LightPanda...");
30
31    let url = format!("{LIGHTPANDA_BASE_URL}/{binary_name}");
32    let bytes = match download_binary(&url).await {
33        Ok(b) => b,
34        Err(e) => {
35            eprintln!("  ✗ Download failed: {e}");
36            std::process::exit(1);
37        }
38    };
39
40    // Verify binary integrity via SHA256 checksum.
41    let actual_hash = sha256_hex(&bytes);
42    let checksum_url = format!("{LIGHTPANDA_BASE_URL}/{binary_name}.sha256");
43    match download_checksum(&checksum_url).await {
44        Ok(expected_hash) => {
45            if actual_hash != expected_hash {
46                eprintln!("  ✗ SHA256 checksum mismatch!");
47                eprintln!("    Expected: {expected_hash}");
48                eprintln!("    Actual:   {actual_hash}");
49                eprintln!("    The downloaded binary may be corrupted or tampered with.");
50                std::process::exit(1);
51            }
52            println!("  ✓ SHA256 checksum verified: {actual_hash}");
53        }
54        Err(_) => {
55            // Checksum file not available — log the hash for manual verification.
56            println!("  ⚠ No checksum file available, SHA256: {actual_hash}");
57        }
58    }
59
60    if let Err(e) = std::fs::create_dir_all(&install_dir) {
61        eprintln!("  ✗ Failed to create {}: {e}", install_dir.display());
62        std::process::exit(1);
63    }
64
65    if let Err(e) = std::fs::write(&install_path, &bytes) {
66        eprintln!("  ✗ Failed to write {}: {e}", install_path.display());
67        std::process::exit(1);
68    }
69
70    #[cfg(unix)]
71    {
72        use std::os::unix::fs::PermissionsExt;
73        let perms = std::fs::Permissions::from_mode(0o755);
74        if let Err(e) = std::fs::set_permissions(&install_path, perms) {
75            eprintln!("  ✗ Failed to chmod +x: {e}");
76            std::process::exit(1);
77        }
78    }
79
80    println!("  ✓ Installed to {}", install_path.display());
81
82    // Write config.local.toml if it doesn't exist.
83    let config_path = PathBuf::from("config.local.toml");
84    if config_path.exists() {
85        println!("  ✓ config.local.toml already exists (skipped)");
86    } else {
87        let config_content = r#"[renderer]
88mode = "lightpanda"
89
90[renderer.lightpanda]
91ws_url = "ws://127.0.0.1:9222/"
92"#;
93        if let Err(e) = std::fs::write(&config_path, config_content) {
94            eprintln!("  ✗ Failed to write config.local.toml: {e}");
95            std::process::exit(1);
96        }
97        println!("  ✓ Created config.local.toml");
98    }
99
100    println!();
101    println!("  Start the server with JS rendering:");
102    println!("    lightpanda serve --host 127.0.0.1 --port 9222 &");
103    println!("    crw-server");
104    println!();
105}
106
107async fn download_binary(url: &str) -> Result<Vec<u8>, reqwest::Error> {
108    let client = reqwest::Client::builder()
109        .redirect(crw_core::url_safety::safe_redirect_policy())
110        .build()?;
111
112    let resp = client.get(url).send().await?.error_for_status()?;
113    let bytes = resp.bytes().await?;
114    Ok(bytes.to_vec())
115}
116
117/// Download and parse a .sha256 checksum file (format: "<hash>  <filename>" or just "<hash>").
118async fn download_checksum(url: &str) -> Result<String, String> {
119    let client = reqwest::Client::builder()
120        .redirect(crw_core::url_safety::safe_redirect_policy())
121        .build()
122        .map_err(|e| format!("client build error: {e}"))?;
123
124    let resp = client
125        .get(url)
126        .send()
127        .await
128        .map_err(|e| format!("fetch error: {e}"))?;
129
130    if !resp.status().is_success() {
131        return Err(format!("HTTP {}", resp.status()));
132    }
133
134    let text = resp.text().await.map_err(|e| format!("read error: {e}"))?;
135
136    // Parse checksum file: first token is the hex hash.
137    let hash = text
138        .split_whitespace()
139        .next()
140        .ok_or_else(|| "empty checksum file".to_string())?
141        .to_lowercase();
142
143    // Sanity check: should be 64 hex chars (SHA256).
144    if hash.len() != 64 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
145        return Err(format!("invalid checksum format: {hash}"));
146    }
147
148    Ok(hash)
149}
150
151fn sha256_hex(data: &[u8]) -> String {
152    let mut hasher = Sha256::new();
153    hasher.update(data);
154    hex::encode(hasher.finalize())
155}
156
157fn home_local_bin() -> PathBuf {
158    let home = std::env::var("HOME")
159        .or_else(|_| std::env::var("USERPROFILE"))
160        .unwrap_or_else(|_| ".".to_string());
161    PathBuf::from(home).join(".local").join("bin")
162}