diff --git a/core/archipelago/src/api/rpc/tor/handlers.rs b/core/archipelago/src/api/rpc/tor/handlers.rs index 3001f00e..d45caa5f 100644 --- a/core/archipelago/src/api/rpc/tor/handlers.rs +++ b/core/archipelago/src/api/rpc/tor/handlers.rs @@ -4,9 +4,21 @@ use std::time::{SystemTime, UNIX_EPOCH}; impl RpcHandler { /// List all configured hidden services with their .onion addresses. + /// Services for known-but-uninstalled apps are hidden (issue #79). pub(in crate::api::rpc) async fn handle_tor_list_services(&self) -> Result { let config_dir = self.config.data_dir.join("tor-config"); - let services = list_services(&config_dir).await?; + let (data, _) = self.state_manager.get_snapshot().await; + let mut apps = AppInstallState { + known: Default::default(), + installed: Default::default(), + }; + for (id, pkg) in &data.package_data { + apps.known.insert(id.clone()); + if pkg.installed.is_some() { + apps.installed.insert(id.clone()); + } + } + let services = list_services(&config_dir, Some(&apps)).await?; let tor_running = check_tor_running().await; Ok(serde_json::json!({ "services": services, "tor_running": tor_running })) } diff --git a/core/archipelago/src/api/rpc/tor/mod.rs b/core/archipelago/src/api/rpc/tor/mod.rs index 7b596ae2..0e3d60d4 100644 --- a/core/archipelago/src/api/rpc/tor/mod.rs +++ b/core/archipelago/src/api/rpc/tor/mod.rs @@ -228,15 +228,67 @@ pub(super) async fn sync_all_hostname_copies(config: &ServicesConfig) { // ─── Service Listing ───────────────────────────────────────────── -pub(super) async fn list_services(config_dir: &std::path::Path) -> Result> { +/// Which packages the node knows about and which are installed — used to +/// hide hidden services for apps that aren't installed. ISO first-boot used +/// to pre-bake onions for a fixed app list (bitcoin/electrumx/lnd/btcpay/ +/// mempool/fedimint), so fresh nodes showed Tor sites for apps that were +/// never installed (issue #79). +pub(super) struct AppInstallState { + pub known: std::collections::HashSet, + pub installed: std::collections::HashSet, +} + +/// Package ids a Tor service name may correspond to. Service names predate +/// the catalog app ids (the ISO baked "bitcoin"/"btcpay"), so one service +/// can map to several package ids. +fn service_alias_candidates(name: &str) -> Vec<&str> { + match name { + "bitcoin" | "bitcoin-knots" | "bitcoin-core" => { + vec!["bitcoin", "bitcoin-knots", "bitcoin-core"] + } + "electrumx" | "electrs" | "mempool-electrs" => { + vec!["electrumx", "electrs", "mempool-electrs"] + } + "btcpay" | "btcpay-server" | "btcpayserver" => { + vec!["btcpay", "btcpay-server", "btcpayserver"] + } + "mempool" | "mempool-web" => vec!["mempool", "mempool-web"], + other => vec![other], + } +} + +impl AppInstallState { + /// A service is listed unless it names a known-but-uninstalled app. + /// The node's own service, the content relay, and custom user-created + /// services (names matching no catalog package) always show. + fn service_visible(&self, name: &str) -> bool { + if name == "archipelago" || name == "relay" { + return true; + } + let candidates = service_alias_candidates(name); + if !candidates.iter().any(|c| self.known.contains(*c)) { + return true; // not an app — custom hidden service + } + candidates.iter().any(|c| self.installed.contains(*c)) + } +} + +pub(super) async fn list_services( + config_dir: &std::path::Path, + apps: Option<&AppInstallState>, +) -> Result> { let base = detect_hidden_service_base(); let config = load_services_config(config_dir).await; let mut services = Vec::new(); let mut seen = std::collections::HashSet::new(); + let visible = |name: &str| apps.map(|a| a.service_visible(name)).unwrap_or(true); for entry in &config.services { - let onion = read_onion_address(&entry.name).await; seen.insert(entry.name.clone()); + if !visible(&entry.name) { + continue; + } + let onion = read_onion_address(&entry.name).await; services.push(TorService { name: entry.name.clone(), local_port: entry.local_port, @@ -260,9 +312,12 @@ pub(super) async fn list_services(config_dir: &std::path::Path) -> Result