//! SoulStream Setup — the file a tester runs. Our own window in the app's //! stack (a Tauri webview drawing the first-run tour's design). The alpha //! key is the gate: it is checked against GitHub first, and only a key that //! can read the private repository gets any further. The installer is then //! DOWNLOADED from the latest release with that key (never carried inside //! this exe, where it could be pulled out), its signature verified against //! the same public key the app's updater trusts, and run silently. The key //! goes to the Credential Manager where the app reads it, the choices into //! the app's config.json, and the app is launched. //! //! Without WebView2 this window cannot draw. main() checks the runtime //! first and, if it is missing, says so and leaves — the app itself needs //! WebView2 too, so there is nothing to install on such a machine yet. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod pubkey { include!(concat!(env!("OUT_DIR"), "/pubkey.rs")); } use std::path::PathBuf; use std::process::Command; use std::time::Duration; use base64::Engine; use serde::Serialize; use tauri::{AppHandle, Emitter}; const REPO: &str = "KeelrLeek/SoulStream"; /// The rolling prerelease `.github/workflows/nightly.yml` replaces daily — /// never the one Setup installs. const NIGHTLY_TAG: &str = "nightly"; const APP_DATA_DIRNAME: &str = "com.soulstream.app"; const KEYRING_SERVICE: &str = "com.soulstream.app"; const KEYRING_TOKEN_USER: &str = "updater-token"; const USER_AGENT: &str = concat!("SoulStream-Setup/", env!("CARGO_PKG_VERSION")); fn app_data_dir() -> Option { dirs::data_dir().map(|d| d.join(APP_DATA_DIRNAME)) } fn config_path() -> Option { app_data_dir().map(|d| d.join("config.json")) } fn load_config() -> serde_json::Map { config_path() .and_then(|p| std::fs::read_to_string(p).ok()) .and_then(|s| serde_json::from_str::(&s).ok()) .and_then(|v| v.as_object().cloned()) .unwrap_or_default() } fn save_config(cfg: &serde_json::Map) -> Result<(), String> { let p = config_path().ok_or("no app data folder")?; if let Some(dir) = p.parent() { std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; } let json = serde_json::to_string_pretty(&serde_json::Value::Object(cfg.clone())) .map_err(|e| e.to_string())?; std::fs::write(&p, json).map_err(|e| e.to_string()) } fn music_dir() -> PathBuf { dirs::audio_dir() .or_else(dirs::home_dir) .unwrap_or_else(|| PathBuf::from(".")) } fn keyring_entry() -> Option { keyring::Entry::new(KEYRING_SERVICE, KEYRING_TOKEN_USER).ok() } fn saved_key() -> Option { keyring_entry() .and_then(|e| e.get_password().ok()) .map(|t| t.trim().to_string()) .filter(|t| !t.is_empty()) } /// Where the installer put the app, from the uninstall entry it registers; /// the per-user default when there is none yet. fn install_location() -> Option { use winreg::enums::HKEY_CURRENT_USER; use winreg::RegKey; let hkcu = RegKey::predef(HKEY_CURRENT_USER); if let Ok(uninstall) = hkcu.open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Uninstall") { for name in uninstall.enum_keys().flatten() { if let Ok(k) = uninstall.open_subkey(&name) { let display: String = k.get_value("DisplayName").unwrap_or_default(); if display == "SoulStream" { if let Ok(loc) = k.get_value::("InstallLocation") { let loc = loc.trim_matches('"'); if !loc.is_empty() { return Some(PathBuf::from(loc)); } } } } } } dirs::data_local_dir().map(|d| d.join("SoulStream")) } fn installed_version() -> Option { use winreg::enums::HKEY_CURRENT_USER; use winreg::RegKey; let uninstall = RegKey::predef(HKEY_CURRENT_USER) .open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Uninstall") .ok()?; for name in uninstall.enum_keys().flatten() { if let Ok(k) = uninstall.open_subkey(&name) { let display: String = k.get_value("DisplayName").unwrap_or_default(); if display == "SoulStream" { return k.get_value::("DisplayVersion").ok(); } } } None } fn app_exe() -> Option { let dir = install_location()?; for name in ["SoulStream.exe", "app.exe"] { let p = dir.join(name); if p.is_file() { return Some(p); } } None } fn webview2_present() -> bool { use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE}; use winreg::RegKey; const CLIENT: &str = r"Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"; const CLIENT_WOW: &str = r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"; for (hive, path) in [ (HKEY_LOCAL_MACHINE, CLIENT_WOW), (HKEY_LOCAL_MACHINE, CLIENT), (HKEY_CURRENT_USER, CLIENT), ] { if let Ok(k) = RegKey::predef(hive).open_subkey(path) { if let Ok(pv) = k.get_value::("pv") { if !pv.trim().is_empty() && pv.trim() != "0.0.0.0" { return true; } } } } false } // ---------------------------------------------------------------- GitHub /// The latest release as the key can see it: the installer and its /// signature, by their API asset URLs (a private repository's assets are /// reachable only that way). #[derive(Serialize, Clone)] struct Release { version: String, name: String, published: Option, installer_name: String, installer_size: u64, installer_url: String, sig_url: Option, } /// Why a release lookup failed, and whether it was the KEY that GitHub /// refused. Only a refusal is a reason to throw the saved key away — a /// laptop between wifi networks is not, and the entry thrown away is the /// one the app reads for its own updates. struct Failure { message: String, key_refused: bool, } impl Failure { fn plain(message: impl Into) -> Self { Self { message: message.into(), key_refused: false } } fn refused(message: impl Into) -> Self { Self { message: message.into(), key_refused: true } } } impl From for String { fn from(f: Failure) -> Self { f.message } } /// A bare message is never a refusal — a release missing its installer says /// nothing about the key that read it. impl From<&str> for Failure { fn from(m: &str) -> Self { Failure::plain(m) } } impl From for Failure { fn from(m: String) -> Self { Failure::plain(m) } } fn client() -> Result { reqwest::Client::builder() .timeout(Duration::from_secs(60)) .user_agent(USER_AGENT) .build() .map_err(|e| e.to_string()) } async fn fetch_release(token: &str) -> Result { // Not `/releases/latest`: that endpoint answers with the newest FULL // release and every SoulStream release so far is a prerelease, so it // 404s and a perfectly good key reads back as a bad one. The list comes // newest first — take the first that is neither a draft nor the rolling // nightly, which is not what a tester is here to install. let resp = client()? .get(format!("https://api.github.com/repos/{REPO}/releases?per_page=30")) .bearer_auth(token) .header(reqwest::header::ACCEPT, "application/vnd.github+json") .header("X-GitHub-Api-Version", "2022-11-28") .send() .await .map_err(|e| Failure::plain(format!("Could not reach GitHub — is this computer online? ({e})")))?; match resp.status().as_u16() { 200 => {} 401 => return Err(Failure::refused("That key was refused.")), 403 => return Err(Failure::refused("That key cannot read the SoulStream repository.")), // A private repository answers 404 to a key that cannot see it. 404 => return Err(Failure::refused("No release found — or that key cannot see the repository.")), s => return Err(Failure::plain(format!("GitHub answered {s}."))), } let list: Vec = resp.json().await.map_err(|e| Failure::plain(e.to_string()))?; // The key worked; there is simply nothing published yet. Not its fault. let v = list .into_iter() .find(|r| { r["draft"].as_bool() != Some(true) && r["tag_name"].as_str() != Some(NIGHTLY_TAG) }) .ok_or_else(|| Failure::plain("That key works, but the repository has no release to install yet."))?; let tag = v["tag_name"].as_str().unwrap_or(""); let assets = v["assets"].as_array().cloned().unwrap_or_default(); let find = |suffix: &str| { assets .iter() .find(|a| a["name"].as_str().map_or(false, |n| n.ends_with(suffix))) .cloned() }; let installer = find("_x64-setup.exe").ok_or("The release carries no installer.")?; let sig = find("_x64-setup.exe.sig"); Ok(Release { version: tag.trim_start_matches('v').to_string(), name: v["name"].as_str().unwrap_or(tag).to_string(), published: v["published_at"].as_str().map(String::from), installer_name: installer["name"].as_str().unwrap_or("SoulStream-setup.exe").to_string(), installer_size: installer["size"].as_u64().unwrap_or(0), installer_url: installer["url"].as_str().unwrap_or("").to_string(), sig_url: sig.and_then(|a| a["url"].as_str().map(String::from)), }) } #[derive(Serialize, Clone)] struct Phase { phase: String, detail: String, received: u64, total: u64, } fn phase(app: &AppHandle, name: &str, detail: impl Into, received: u64, total: u64) { let _ = app.emit( "setup:phase", Phase { phase: name.into(), detail: detail.into(), received, total }, ); } /// An asset, streamed down with the key; progress goes to the window. async fn download(app: &AppHandle, token: &str, url: &str, total: u64, label: &str) -> Result, String> { let mut resp = client()? .get(url) .bearer_auth(token) .header(reqwest::header::ACCEPT, "application/octet-stream") .send() .await .map_err(|e| format!("The download failed to start: {e}"))?; if !resp.status().is_success() { return Err(format!("The download was refused ({}).", resp.status().as_u16())); } let total = resp.content_length().unwrap_or(total); let mut bytes = Vec::with_capacity(total as usize); let mut last = std::time::Instant::now(); while let Some(chunk) = resp.chunk().await.map_err(|e| format!("The download broke off: {e}"))? { bytes.extend_from_slice(&chunk); if last.elapsed() >= Duration::from_millis(120) { last = std::time::Instant::now(); phase(app, "downloading", label, bytes.len() as u64, total); } } phase(app, "downloading", label, bytes.len() as u64, total); Ok(bytes) } /// The updater plugin's check, verbatim: the pubkey and the signature are /// base64 wrappings of minisign's text formats. fn verify_signature(data: &[u8], signature_b64: &str) -> Result<(), String> { let decode = |s: &str| -> Result { let raw = base64::engine::general_purpose::STANDARD .decode(s.trim()) .map_err(|e| format!("signature is not base64: {e}"))?; String::from_utf8(raw).map_err(|_| "signature is not text".to_string()) }; let public_key = minisign_verify::PublicKey::decode(&decode(pubkey::PUBKEY)?) .map_err(|e| format!("public key: {e}"))?; let signature = minisign_verify::Signature::decode(&decode(signature_b64)?) .map_err(|e| format!("signature: {e}"))?; public_key .verify(data, &signature, true) .map_err(|e| format!("the installer does not match its signature: {e}")) } // ---------------------------------------------------------------- commands #[derive(Serialize)] struct Defaults { version: String, music_dir: String, library_dir: String, library_layout: String, glass_mode: bool, loudness: bool, has_token: bool, installed_version: Option, install_location: Option, } #[tauri::command] fn defaults() -> Defaults { let cfg = load_config(); let music = music_dir(); let library_dir = cfg .get("download_dir") .and_then(|v| v.as_str()) .filter(|s| !s.trim().is_empty()) .map(String::from) .unwrap_or_else(|| { // The app's own default, legacy name honoured. let legacy = music.join("SoulSeek+"); if legacy.is_dir() { legacy } else { music.join("SoulStream") } .to_string_lossy() .into_owned() }); let library_layout = cfg .get("library_layout") .and_then(|v| v.as_str()) .filter(|s| !s.trim().is_empty()) .map(String::from) .unwrap_or_else(|| "{artist}\\{album}".into()); Defaults { version: env!("CARGO_PKG_VERSION").into(), music_dir: music.to_string_lossy().into_owned(), library_dir, library_layout, glass_mode: cfg.get("glass_mode").and_then(|v| v.as_bool()).unwrap_or(false), loudness: cfg.get("volume_normalization").and_then(|v| v.as_bool()).unwrap_or(true), has_token: saved_key().is_some(), installed_version: installed_version(), install_location: install_location().map(|p| p.to_string_lossy().into_owned()), } } /// The gate. A key that can read the release is saved to the Credential /// Manager and answered with what it would install; any other is refused /// and nothing is kept. #[tauri::command] async fn verify_key(token: String) -> Result { let t = token.trim().to_string(); if t.is_empty() { return Err("Paste your alpha key first.".into()); } let release = fetch_release(&t).await?; let e = keyring_entry().ok_or("the Credential Manager is not available")?; e.set_password(&t).map_err(|e| format!("the Credential Manager refused the key: {e}"))?; Ok(release) } /// A key saved by an earlier run, re-checked: still good, or gone. #[tauri::command] async fn check_saved_key() -> Result, String> { let Some(t) = saved_key() else { return Ok(None) }; match fetch_release(&t).await { Ok(r) => Ok(Some(r)), Err(e) if e.key_refused => { // The same Credential Manager entry the app reads for its own // updates, so it goes only when GitHub actually turned it down. if let Some(entry) = keyring_entry() { let _ = entry.delete_credential(); } Err(format!("The key saved on this computer no longer works — {}", e.message)) } Err(e) => Err(e.message), } } #[tauri::command] async fn pick_folder() -> Option { tauri::async_runtime::spawn_blocking(|| { rfd::FileDialog::new() .set_title("Where your music lives") .pick_folder() .map(|p| p.to_string_lossy().into_owned()) }) .await .ok() .flatten() } /// The choices, merged into the app's config.json (everything else in it is /// kept), and the first-run flag set: the tour asked its questions here. #[tauri::command] fn write_config( library_dir: String, library_layout: String, glass_mode: bool, loudness: bool, ) -> Result<(), String> { let mut cfg = load_config(); let lib = library_dir.trim(); if !lib.is_empty() { cfg.insert("download_dir".into(), lib.into()); } let lay = library_layout.trim(); if !lay.is_empty() { cfg.insert("library_layout".into(), lay.into()); } cfg.insert("glass_mode".into(), glass_mode.into()); cfg.insert("volume_normalization".into(), loudness.into()); cfg.insert("setup_done".into(), true.into()); if !cfg.contains_key("update_channel") { cfg.insert("update_channel".into(), "stable".into()); } save_config(&cfg) } /// Download the latest release with the saved key, verify it, run it /// silently. Phases go to the window; the result is the install folder. #[tauri::command] async fn install(app: AppHandle) -> Result { let run = async { let token = saved_key().ok_or("No alpha key is saved — go back and verify one.")?; phase(&app, "resolving", "Asking GitHub for the latest release…", 0, 0); let release = fetch_release(&token).await?; let sig_url = release .sig_url .clone() .ok_or("The release carries no signature; refusing to install it.")?; let label = format!("SoulStream {} · {:.1} MB", release.version, release.installer_size as f64 / 1e6); let bytes = download(&app, &token, &release.installer_url, release.installer_size, &label).await?; phase(&app, "verifying", "Checking the installer's signature…", bytes.len() as u64, bytes.len() as u64); let sig = download(&app, &token, &sig_url, 0, "signature").await?; let sig = String::from_utf8(sig).map_err(|_| "the signature is not text".to_string())?; verify_signature(&bytes, &sig)?; let dir = std::env::temp_dir().join("SoulStream-Setup"); std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; let exe = dir.join(&release.installer_name); std::fs::write(&exe, &bytes).map_err(|e| format!("could not write the installer: {e}"))?; phase(&app, "installing", "Copying SoulStream into place…", 0, 0); let status = tauri::async_runtime::spawn_blocking(move || Command::new(&exe).arg("/S").status()) .await .map_err(|e| e.to_string())? .map_err(|e| format!("could not start the installer: {e}"))?; if !status.success() { return Err(format!("the installer ended with code {}", status.code().unwrap_or(-1))); } let loc = install_location().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(); Ok::(loc) }; match run.await { Ok(loc) => { phase(&app, "done", loc.clone(), 0, 0); Ok(loc) } Err(e) => { phase(&app, "failed", e.clone(), 0, 0); Err(e) } } } /// Open the installed app and leave. #[tauri::command] fn launch(app: AppHandle) -> Result<(), String> { let exe = app_exe().ok_or("SoulStream is not installed here")?; Command::new(&exe) .current_dir(exe.parent().unwrap_or(&exe)) .spawn() .map_err(|e| format!("could not start SoulStream: {e}"))?; app.exit(0); Ok(()) } #[tauri::command] fn quit(app: AppHandle) { app.exit(0); } fn main() { if !webview2_present() { // Nothing here can draw without it, and neither can the app. rfd::MessageDialog::new() .set_title("SoulStream Setup") .set_description("SoulStream needs Microsoft Edge WebView2, which this computer does not have. Install it from Microsoft, then run Setup again.") .show(); return; } tauri::Builder::default() .invoke_handler(tauri::generate_handler![ defaults, verify_key, check_saved_key, pick_folder, write_config, install, launch, quit ]) .run({ #[cfg_attr(not(debug_assertions), allow(unused_mut))] let mut context = tauri::generate_context!(); // Driving the window from a probe, debug builds only. #[cfg(debug_assertions)] if let Ok(port) = std::env::var("SOULSTREAM_SETUP_CDP") { for w in &mut context.config_mut().app.windows { w.additional_browser_args = Some(format!("--remote-debugging-port={port}")); } } context }) .expect("SoulStream Setup could not start"); }