From 16293e3f4e57806cb1b6310f03ee146840ed6796 Mon Sep 17 00:00:00 2001 From: keraliss Date: Tue, 5 May 2026 15:24:45 +0530 Subject: [PATCH 1/6] fixes #73: autometic tor deployment --- Cargo.lock | 1 + Cargo.toml | 2 +- README.md | 1 - docker/docker-compose.yml | 4 +- frontend/app/api.ts | 13 ++ frontend/app/app.css | 15 ++ frontend/app/components/Toast.tsx | 31 ++++ frontend/app/main.tsx | 46 ++++-- frontend/tsconfig.tsbuildinfo | 2 +- src/api/dto.rs | 9 ++ src/api/mod.rs | 2 + src/api/monitoring.rs | 19 ++- src/lib.rs | 1 + src/main.rs | 1 + src/maker_manager/mod.rs | 10 ++ src/server.rs | 26 +++ src/tor_manager.rs | 252 ++++++++++++++++++++++++++++++ 17 files changed, 419 insertions(+), 16 deletions(-) create mode 100644 frontend/app/components/Toast.tsx create mode 100644 src/tor_manager.rs diff --git a/Cargo.lock b/Cargo.lock index 91f81b8..5327186 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2739,6 +2739,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 8fa5f96..77f8070 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ integration-test = ["coinswap/integration-test"] [dependencies] axum = "0.8" -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] } tower-http = { version = "0.6", features = ["fs", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/README.md b/README.md index f12f202..5fff271 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,6 @@ This starts a custom signet bitcoind, a Tor daemon, and the maker dashboard — When creating a maker, use: - RPC: `127.0.0.1:38332`, ZMQ: `tcp://127.0.0.1:28332` - RPC credentials: `user` / `password` -- Tor auth: `coinswap` - SOCKS port: `9050`, Control port: `9051` Other useful commands: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d68ad4b..252ab3a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -31,13 +31,11 @@ services: command: - -c - | - HASH=$$(tor --hash-password "coinswap" | grep "^16:") - cat > /tmp/torrc << EOF SocksPort 0.0.0.0:9050 ControlPort 0.0.0.0:9051 DataDirectory /var/lib/tor - HashedControlPassword $$HASH + CookieAuthentication 0 EOF echo "Starting Tor..." diff --git a/frontend/app/api.ts b/frontend/app/api.ts index 6c44941..64f469d 100644 --- a/frontend/app/api.ts +++ b/frontend/app/api.ts @@ -393,6 +393,19 @@ export const onboarding = { post("/onboarding/startup-check", body), }; +// ─── Tor ────────────────────────────────────────────────────────────────────── + +export type TorSource = "system" | "host" | "docker"; + +export interface TorStatusInfo { + source: TorSource; + managed: boolean; +} + +export async function getTorStatus(): Promise { + return get("/tor/status"); +} + // ─── Helpers ────────────────────────────────────────────────────────────────── /** Convert satoshis to a BTC string (8 decimal places) */ diff --git a/frontend/app/app.css b/frontend/app/app.css index c8105a1..56d554e 100644 --- a/frontend/app/app.css +++ b/frontend/app/app.css @@ -47,6 +47,17 @@ body { } } +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(24px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + @keyframes shimmer { from { background-position: -200% center; @@ -64,6 +75,10 @@ body { animation: slideInUp 0.35s ease-out both; } +@utility animate-slide-in-right { + animation: slideInRight 0.35s ease-out both; +} + @utility animate-shimmer { background: linear-gradient( 90deg, diff --git a/frontend/app/components/Toast.tsx b/frontend/app/components/Toast.tsx new file mode 100644 index 0000000..fb10eba --- /dev/null +++ b/frontend/app/components/Toast.tsx @@ -0,0 +1,31 @@ +import { useEffect, useState } from "react"; + +interface ToastProps { + message: string; + durationMs?: number; + onDismiss: () => void; +} + +export function Toast({ message, durationMs = 5000, onDismiss }: ToastProps) { + const [visible, setVisible] = useState(true); + + useEffect(() => { + const hide = setTimeout(() => setVisible(false), durationMs - 400); + const dismiss = setTimeout(onDismiss, durationMs); + return () => { + clearTimeout(hide); + clearTimeout(dismiss); + }; + }, [durationMs, onDismiss]); + + if (!visible) return null; + + return ( +
+
+ + {message} +
+
+ ); +} diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index b94846f..5fbb58f 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -1,22 +1,50 @@ import { createRoot } from "react-dom/client"; import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { StrictMode, useEffect, useState } from "react"; import "@/app.css"; import Home from "./routes/home"; import MakerDetails from "./routes/makerDetails"; import AddMaker from "./routes/addMaker"; import MakerSetup from "./routes/makersetup"; -import { StrictMode } from "react"; +import { Toast } from "./components/Toast"; +import { getTorStatus } from "./api"; + +function App() { + const [torToast, setTorToast] = useState(null); + + useEffect(() => { + if (sessionStorage.getItem("tor-toast-shown")) return; + getTorStatus() + .then(({ managed, source }) => { + if (managed) { + const label = source === "docker" ? "Docker" : "host binary"; + setTorToast(`Tor started via ${label}`); + sessionStorage.setItem("tor-toast-shown", "1"); + } + }) + .catch(() => {}); + }, []); + + return ( + <> + + + } /> + } /> + } /> + } /> + + + {torToast && ( + setTorToast(null)} /> + )} + + ); +} createRoot(document.getElementById("root")!).render( - - - } /> - } /> - } /> - } /> - - + , ); diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 4b12452..a0067d9 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./vite-env.d.ts","./vite.config.ts","./app/api.ts","./app/main.tsx","./app/components/bitcoindwidget.tsx","./app/components/nav.tsx","./app/routes/addmaker.tsx","./app/routes/home.tsx","./app/routes/makersetup.tsx","./app/routes/onboarding.tsx","./app/routes/makerdetails/components.tsx","./app/routes/makerdetails/dashboard.tsx","./app/routes/makerdetails/history.tsx","./app/routes/makerdetails/index.tsx","./app/routes/makerdetails/log.tsx","./app/routes/makerdetails/settings.tsx","./app/routes/makerdetails/types.ts","./app/routes/makerdetails/wallet.tsx"],"version":"5.9.3"} \ No newline at end of file +{"root":["./vite-env.d.ts","./vite.config.ts","./app/api.ts","./app/main.tsx","./app/components/bitcoindwidget.tsx","./app/components/nav.tsx","./app/components/toast.tsx","./app/routes/addmaker.tsx","./app/routes/home.tsx","./app/routes/makersetup.tsx","./app/routes/onboarding.tsx","./app/routes/makerdetails/components.tsx","./app/routes/makerdetails/dashboard.tsx","./app/routes/makerdetails/history.tsx","./app/routes/makerdetails/index.tsx","./app/routes/makerdetails/log.tsx","./app/routes/makerdetails/settings.tsx","./app/routes/makerdetails/types.ts","./app/routes/makerdetails/wallet.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/src/api/dto.rs b/src/api/dto.rs index ce21c34..b4d7869 100644 --- a/src/api/dto.rs +++ b/src/api/dto.rs @@ -403,3 +403,12 @@ pub struct BitcoindStatusInfo { /// True only when bitcoind was started by the dashboard (and can be stopped via /stop) pub managed: bool, } + +/// Tor connectivity status reported at startup +#[derive(Debug, Serialize, ToSchema)] +pub struct TorStatusInfo { + /// How tor was obtained: "system", "host", or "docker" + pub source: &'static str, + /// Whether the dashboard started/manages the tor process + pub managed: bool, +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 6ca3790..e5b2e99 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -54,6 +54,7 @@ pub type AppState = Arc>; bitcoind::start, bitcoind::stop, onboarding::run_startup_check, + monitoring::get_tor_status, health_check, ), components(schemas( @@ -75,6 +76,7 @@ pub type AppState = Arc>; dto::StartBitcoindRequest, dto::BitcoindStatusInfo, dto::CombinedLogLine, + dto::TorStatusInfo, )), tags( (name = "makers", description = "Maker management"), diff --git a/src/api/monitoring.rs b/src/api/monitoring.rs index 01dc013..eb3f935 100644 --- a/src/api/monitoring.rs +++ b/src/api/monitoring.rs @@ -22,7 +22,7 @@ use crate::utils::log_writer::read_last_n_lines; use super::{ dto::{ ApiResponse, CombinedLogLine, MakerStatus, RpcStatusInfo, SwapHistoryDto, SwapReportDto, - UtxoInfo, + TorStatusInfo, UtxoInfo, }, AppState, }; @@ -39,6 +39,7 @@ pub fn routes() -> Router { .route("/makers/{id}/data-dir", get(get_data_dir)) .route("/makers/{id}/rpc-status", get(get_rpc_status)) .route("/logs/combined", get(get_combined_logs)) + .route("/tor/status", get(get_tor_status)) } /// Get operational status of a maker @@ -531,6 +532,22 @@ struct LogsQuery { lines: Option, } +#[utoipa::path( + get, + path = "/api/tor/status", + tag = "monitoring", + responses( + (status = 200, description = "Tor connectivity status", body = ApiResponse), + ) +)] +pub async fn get_tor_status(State(state): State) -> Json> { + let source = state.lock().await.tor_source(); + Json(ApiResponse::ok(TorStatusInfo { + managed: source != "system", + source, + })) +} + /// Get the Tor address of a maker #[utoipa::path( get, diff --git a/src/lib.rs b/src/lib.rs index 6dca059..3ad8621 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,5 @@ pub mod api; pub mod maker_manager; pub mod middlewares; pub mod server; +pub mod tor_manager; pub mod utils; diff --git a/src/main.rs b/src/main.rs index 953e264..cac1f0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod maker_manager; mod middlewares; mod server; +mod tor_manager; mod utils; use clap::Parser; diff --git a/src/maker_manager/mod.rs b/src/maker_manager/mod.rs index 905547b..8c8a9f3 100644 --- a/src/maker_manager/mod.rs +++ b/src/maker_manager/mod.rs @@ -7,6 +7,7 @@ use std::net::TcpListener; use std::path::PathBuf; use std::sync::Arc; +use crate::tor_manager::TorManager; use crate::utils::log_writer::MakerLogWriter; use anyhow::{anyhow, Result}; use coinswap::bitcoin::Network; @@ -100,6 +101,8 @@ pub struct MakerManager { bitcoind_process: Option, /// Network bitcoind was started on (e.g. "regtest", "signet") bitcoind_network: Option, + #[allow(dead_code)] + tor_manager: TorManager, } impl MakerManager { @@ -109,6 +112,7 @@ impl MakerManager { /// Creates a new MakerManager with persistence at the given config directory. /// Loads any previously saved maker configs and re-initializes them (but does NOT start servers). pub fn new(config_dir: PathBuf) -> Result { + let tor_manager = TorManager::detect_or_start(&config_dir)?; let persistence = PersistenceManager::new(config_dir.clone())?; let saved_configs = persistence.load()?; @@ -118,6 +122,7 @@ impl MakerManager { persistence, bitcoind_process: None, bitcoind_network: None, + tor_manager, }; // Restore previously registered makers (init only, not started) @@ -635,6 +640,11 @@ impl MakerManager { Some(child) } + /// Returns how Tor was obtained: "system", "host", or "docker" + pub fn tor_source(&self) -> &'static str { + self.tor_manager.source_label() + } + /// Returns `(running, network)` for the dashboard-managed bitcoind process. pub fn bitcoind_status(&mut self) -> (bool, Option) { if let Some(ref mut child) = self.bitcoind_process { diff --git a/src/server.rs b/src/server.rs index 8320dea..f912cd7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -106,8 +106,34 @@ impl Server { listener, app.into_make_service_with_connect_info::(), ) + .with_graceful_shutdown(shutdown_signal()) .await?; + tracing::info!("Server stopped"); Ok(()) } } + +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let sigterm = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install SIGTERM handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let sigterm = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => tracing::info!("Received Ctrl+C, shutting down..."), + _ = sigterm => tracing::info!("Received SIGTERM, shutting down..."), + } +} diff --git a/src/tor_manager.rs b/src/tor_manager.rs new file mode 100644 index 0000000..cea2ff3 --- /dev/null +++ b/src/tor_manager.rs @@ -0,0 +1,252 @@ +use std::net::TcpStream; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +const SOCKS_PORT: u16 = 9050; +const CONTROL_PORT: u16 = 9051; +const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); +const STARTUP_TIMEOUT: Duration = Duration::from_secs(30); +const DOCKER_DAEMON_TIMEOUT: Duration = Duration::from_secs(60); +const POLL_INTERVAL: Duration = Duration::from_millis(500); + +enum TorSource { + System, + HostProcess, + Docker, +} + +pub struct TorManager { + source: TorSource, + process: Option, + container_id: Option, +} + +impl TorManager { + pub fn detect_or_start(config_dir: &Path) -> anyhow::Result { + if port_reachable(SOCKS_PORT) { + tracing::info!( + "Tor already running on port {}, using system instance", + SOCKS_PORT + ); + return Ok(TorManager { + source: TorSource::System, + process: None, + container_id: None, + }); + } + + if let Some(binary) = find_binary("tor") { + tracing::info!( + "Found tor binary at {}, spawning host process", + binary.display() + ); + let child = spawn_host_process(&binary, config_dir)?; + wait_for_port(SOCKS_PORT)?; + return Ok(TorManager { + source: TorSource::HostProcess, + process: Some(child), + container_id: None, + }); + } + + if find_binary("docker").is_some() { + tracing::info!("Found docker, ensuring daemon is running..."); + ensure_docker_daemon()?; + tracing::info!("Docker daemon ready, spawning tor container"); + let container_id = spawn_docker() + .map_err(|e| anyhow::anyhow!("Failed to start tor Docker container: {}", e))?; + if let Err(e) = wait_for_port(SOCKS_PORT) { + let _ = std::process::Command::new("docker") + .args(["stop", &container_id]) + .output(); + return Err(e); + } + return Ok(TorManager { + source: TorSource::Docker, + process: None, + container_id: Some(container_id), + }); + } + + Err(anyhow::anyhow!( + "Tor is not running and could not be started automatically. \ + Install tor (`brew install tor` / `apt install tor`) or Docker." + )) + } + + pub fn source_label(&self) -> &'static str { + match self.source { + TorSource::System => "system", + TorSource::HostProcess => "host", + TorSource::Docker => "docker", + } + } +} + +impl Drop for TorManager { + fn drop(&mut self) { + match self.source { + TorSource::System => {} + TorSource::HostProcess => { + if let Some(ref mut child) = self.process { + let _ = child.kill(); + let _ = child.wait(); + tracing::info!("Tor process stopped"); + } + } + TorSource::Docker => { + if let Some(ref id) = self.container_id { + tracing::info!("Stopping tor Docker container {}", id); + let _ = std::process::Command::new("docker") + .args(["stop", id]) + .output(); + } + } + } + } +} + +fn port_reachable(port: u16) -> bool { + let addr: std::net::SocketAddr = ([127, 0, 0, 1], port).into(); + TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT).is_ok() +} + +fn find_binary(name: &str) -> Option { + if let Ok(output) = std::process::Command::new("which").arg(name).output() { + if output.status.success() { + let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !path_str.is_empty() { + return Some(PathBuf::from(path_str)); + } + } + } + + for prefix in ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin"] { + let candidate = PathBuf::from(prefix).join(name); + if candidate.exists() { + return Some(candidate); + } + } + + None +} + +fn spawn_host_process(binary: &Path, config_dir: &Path) -> anyhow::Result { + let tor_dir = config_dir.join("tor"); + let data_dir = tor_dir.join("data"); + std::fs::create_dir_all(&data_dir)?; + + let torrc_path = tor_dir.join("torrc"); + let torrc_content = format!( + "SocksPort 0.0.0.0:{SOCKS_PORT}\nControlPort 0.0.0.0:{CONTROL_PORT}\nCookieAuthentication 0\nDataDirectory {}\n", + data_dir.display() + ); + std::fs::write(&torrc_path, torrc_content)?; + + let child = std::process::Command::new(binary) + .args([ + "-f", + torrc_path + .to_str() + .ok_or_else(|| anyhow::anyhow!("torrc path is not valid UTF-8"))?, + ]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn()?; + + Ok(child) +} + +fn spawn_docker() -> anyhow::Result { + let cmd = format!( + "printf 'SocksPort 0.0.0.0:{SOCKS_PORT}\\nControlPort 0.0.0.0:{CONTROL_PORT}\\nCookieAuthentication 0\\nDataDirectory /var/lib/tor\\n' > /tmp/torrc && exec tor -f /tmp/torrc" + ); + + let output = std::process::Command::new("docker") + .args([ + "run", + "-d", + "--rm", + "-p", + "9050:9050", + "-p", + "9051:9051", + "--entrypoint", + "/bin/sh", + "osminogin/tor-simple", + "-c", + &cmd, + ]) + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(anyhow::anyhow!("{}", stderr)); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn is_docker_daemon_running() -> bool { + std::process::Command::new("docker") + .args(["info"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +fn ensure_docker_daemon() -> anyhow::Result<()> { + if is_docker_daemon_running() { + return Ok(()); + } + + tracing::info!("Docker daemon not running, attempting to start it..."); + + // Platform-appropriate start command — ignore errors, we'll poll below + #[cfg(target_os = "macos")] + let _ = std::process::Command::new("open") + .args(["-a", "Docker"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + + #[cfg(target_os = "linux")] + let _ = std::process::Command::new("systemctl") + .args(["start", "docker"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + + let deadline = Instant::now() + DOCKER_DAEMON_TIMEOUT; + while Instant::now() < deadline { + std::thread::sleep(Duration::from_secs(2)); + if is_docker_daemon_running() { + tracing::info!("Docker daemon is ready"); + return Ok(()); + } + } + + Err(anyhow::anyhow!( + "Docker daemon did not start within {}s. \ + On macOS: open Docker Desktop manually. \ + On Linux: run `sudo systemctl start docker`.", + DOCKER_DAEMON_TIMEOUT.as_secs() + )) +} + +fn wait_for_port(port: u16) -> anyhow::Result<()> { + let deadline = Instant::now() + STARTUP_TIMEOUT; + while Instant::now() < deadline { + if port_reachable(port) { + return Ok(()); + } + std::thread::sleep(POLL_INTERVAL); + } + Err(anyhow::anyhow!( + "Tor did not become reachable on port {} within {:?}", + port, + STARTUP_TIMEOUT + )) +} From c851a8a5a987ab6a0d252dfa00643c3ab5478879 Mon Sep 17 00:00:00 2001 From: keraliss Date: Tue, 5 May 2026 17:42:28 +0530 Subject: [PATCH 2/6] fix tests --- src/maker_manager/mod.rs | 11 ++++++++++- src/tor_manager.rs | 10 ++++++++++ tests/api/mod.rs | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/maker_manager/mod.rs b/src/maker_manager/mod.rs index 8c8a9f3..d37539e 100644 --- a/src/maker_manager/mod.rs +++ b/src/maker_manager/mod.rs @@ -113,6 +113,15 @@ impl MakerManager { /// Loads any previously saved maker configs and re-initializes them (but does NOT start servers). pub fn new(config_dir: PathBuf) -> Result { let tor_manager = TorManager::detect_or_start(&config_dir)?; + Self::new_with_tor(config_dir, tor_manager) + } + + /// Creates a MakerManager without starting or detecting Tor. Use in tests only. + pub fn new_for_testing(config_dir: PathBuf) -> Result { + Self::new_with_tor(config_dir, TorManager::noop()) + } + + fn new_with_tor(config_dir: PathBuf, tor_manager: TorManager) -> Result { let persistence = PersistenceManager::new(config_dir.clone())?; let saved_configs = persistence.load()?; @@ -720,7 +729,7 @@ mod tests { } std::fs::create_dir_all(&config_dir).unwrap(); - let manager = MakerManager::new(config_dir).unwrap(); + let manager = MakerManager::new_for_testing(config_dir).unwrap(); let network_listener = TcpListener::bind("127.0.0.1:0").unwrap(); let rpc_listener = TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/src/tor_manager.rs b/src/tor_manager.rs index cea2ff3..338c950 100644 --- a/src/tor_manager.rs +++ b/src/tor_manager.rs @@ -22,6 +22,16 @@ pub struct TorManager { } impl TorManager { + /// Creates a no-op TorManager that assumes Tor is already managed externally. + /// Use this in tests to avoid starting Tor processes or Docker containers. + pub fn noop() -> Self { + TorManager { + source: TorSource::System, + process: None, + container_id: None, + } + } + pub fn detect_or_start(config_dir: &Path) -> anyhow::Result { if port_reachable(SOCKS_PORT) { tracing::info!( diff --git a/tests/api/mod.rs b/tests/api/mod.rs index 8f845c0..089e0af 100644 --- a/tests/api/mod.rs +++ b/tests/api/mod.rs @@ -35,7 +35,7 @@ pub fn test_app() -> Router { std::fs::remove_dir_all(&config_dir).unwrap(); } std::fs::create_dir_all(&config_dir).unwrap(); - let manager = MakerManager::new(config_dir).expect("MakerManager::new"); + let manager = MakerManager::new_for_testing(config_dir).expect("MakerManager::new_for_testing"); let state = Arc::new(Mutex::new(manager)); api_router().with_state(state) } From cf1047f7f89deb8b94b84e589eaffc125f0fda47 Mon Sep 17 00:00:00 2001 From: keraliss Date: Tue, 5 May 2026 18:13:36 +0530 Subject: [PATCH 3/6] tor localhost binding, control port check, api cleanup --- .github/workflows/docker-build.yml | 5 +++-- frontend/app/api.ts | 21 ++++++++------------ frontend/app/app.css | 4 ++-- frontend/app/components/Toast.tsx | 7 ++++--- frontend/app/main.tsx | 5 +++-- src/maker_manager/mod.rs | 9 ++++++++- src/tor_manager.rs | 31 +++++++++++++++++++----------- 7 files changed, 48 insertions(+), 34 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index a4039ad..993e979 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -43,7 +43,7 @@ jobs: - name: Test container starts and responds run: | - docker run -d --rm --name test-container -p 3000:3000 \ + docker run -d --name test-container -p 3000:3000 \ -e DASHBOARD_HOST=0.0.0.0 -e DASHBOARD_ALLOW_REMOTE=true \ maker-dashboard:test # Wait for the server to be ready @@ -55,10 +55,11 @@ jobs: if [ "$i" -eq 30 ]; then echo "Server failed to start" docker logs test-container + docker rm -f test-container || true exit 1 fi sleep 1 done # Verify API responds curl -sf http://localhost:3000/api/makers | jq . - docker stop test-container || true + docker rm -f test-container || true diff --git a/frontend/app/api.ts b/frontend/app/api.ts index 64f469d..a9023d1 100644 --- a/frontend/app/api.ts +++ b/frontend/app/api.ts @@ -346,6 +346,13 @@ export const fidelity = { // ─── Monitoring ─────────────────────────────────────────────────────────────── +export type TorSource = "system" | "host" | "docker"; + +export interface TorStatusInfo { + source: TorSource; + managed: boolean; +} + export const monitoring = { status: (id: string): Promise => get(`/makers/${id}/status`), torAddress: (id: string): Promise => get(`/makers/${id}/tor-address`), @@ -361,6 +368,7 @@ export const monitoring = { get(`/makers/${id}/rpc-status`), combinedLogs: (lines?: number): Promise => get(`/logs/combined${lines !== undefined ? `?lines=${lines}` : ""}`), + getTorStatus: (): Promise => get("/tor/status"), }; // ─── Bitcoind ───────────────────────────────────────────────────────────────── @@ -393,19 +401,6 @@ export const onboarding = { post("/onboarding/startup-check", body), }; -// ─── Tor ────────────────────────────────────────────────────────────────────── - -export type TorSource = "system" | "host" | "docker"; - -export interface TorStatusInfo { - source: TorSource; - managed: boolean; -} - -export async function getTorStatus(): Promise { - return get("/tor/status"); -} - // ─── Helpers ────────────────────────────────────────────────────────────────── /** Convert satoshis to a BTC string (8 decimal places) */ diff --git a/frontend/app/app.css b/frontend/app/app.css index 56d554e..baeb4b3 100644 --- a/frontend/app/app.css +++ b/frontend/app/app.css @@ -47,7 +47,7 @@ body { } } -@keyframes slideInRight { +@keyframes slide-in-right { from { opacity: 0; transform: translateX(24px); @@ -76,7 +76,7 @@ body { } @utility animate-slide-in-right { - animation: slideInRight 0.35s ease-out both; + animation: slide-in-right 0.35s ease-out both; } @utility animate-shimmer { diff --git a/frontend/app/components/Toast.tsx b/frontend/app/components/Toast.tsx index fb10eba..267df41 100644 --- a/frontend/app/components/Toast.tsx +++ b/frontend/app/components/Toast.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useEffectEvent, useState } from "react"; interface ToastProps { message: string; @@ -8,15 +8,16 @@ interface ToastProps { export function Toast({ message, durationMs = 5000, onDismiss }: ToastProps) { const [visible, setVisible] = useState(true); + const onDismissEvent = useEffectEvent(() => onDismiss()); useEffect(() => { const hide = setTimeout(() => setVisible(false), durationMs - 400); - const dismiss = setTimeout(onDismiss, durationMs); + const dismiss = setTimeout(onDismissEvent, durationMs); return () => { clearTimeout(hide); clearTimeout(dismiss); }; - }, [durationMs, onDismiss]); + }, [durationMs]); if (!visible) return null; diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx index 5fbb58f..b07c1ef 100644 --- a/frontend/app/main.tsx +++ b/frontend/app/main.tsx @@ -8,14 +8,15 @@ import MakerDetails from "./routes/makerDetails"; import AddMaker from "./routes/addMaker"; import MakerSetup from "./routes/makersetup"; import { Toast } from "./components/Toast"; -import { getTorStatus } from "./api"; +import { monitoring } from "./api"; function App() { const [torToast, setTorToast] = useState(null); useEffect(() => { if (sessionStorage.getItem("tor-toast-shown")) return; - getTorStatus() + monitoring + .getTorStatus() .then(({ managed, source }) => { if (managed) { const label = source === "docker" ? "Docker" : "host binary"; diff --git a/src/maker_manager/mod.rs b/src/maker_manager/mod.rs index d37539e..e81be36 100644 --- a/src/maker_manager/mod.rs +++ b/src/maker_manager/mod.rs @@ -112,11 +112,18 @@ impl MakerManager { /// Creates a new MakerManager with persistence at the given config directory. /// Loads any previously saved maker configs and re-initializes them (but does NOT start servers). pub fn new(config_dir: PathBuf) -> Result { - let tor_manager = TorManager::detect_or_start(&config_dir)?; + let tor_manager = TorManager::detect_or_start(&config_dir).unwrap_or_else(|e| { + tracing::warn!( + "Tor could not be started: {}. Tor-dependent makers will fail to start.", + e + ); + TorManager::noop() + }); Self::new_with_tor(config_dir, tor_manager) } /// Creates a MakerManager without starting or detecting Tor. Use in tests only. + #[allow(dead_code)] pub fn new_for_testing(config_dir: PathBuf) -> Result { Self::new_with_tor(config_dir, TorManager::noop()) } diff --git a/src/tor_manager.rs b/src/tor_manager.rs index 338c950..5a2fc54 100644 --- a/src/tor_manager.rs +++ b/src/tor_manager.rs @@ -24,6 +24,7 @@ pub struct TorManager { impl TorManager { /// Creates a no-op TorManager that assumes Tor is already managed externally. /// Use this in tests to avoid starting Tor processes or Docker containers. + #[allow(dead_code)] pub fn noop() -> Self { TorManager { source: TorSource::System, @@ -34,15 +35,23 @@ impl TorManager { pub fn detect_or_start(config_dir: &Path) -> anyhow::Result { if port_reachable(SOCKS_PORT) { - tracing::info!( - "Tor already running on port {}, using system instance", - SOCKS_PORT + if port_reachable(CONTROL_PORT) { + tracing::info!( + "Tor already running on SOCKS port {} and control port {}, using system instance", + SOCKS_PORT, CONTROL_PORT + ); + return Ok(TorManager { + source: TorSource::System, + process: None, + container_id: None, + }); + } + tracing::warn!( + "Tor SOCKS port {} is reachable but control port {} is not; \ + falling through to start a managed instance", + SOCKS_PORT, + CONTROL_PORT ); - return Ok(TorManager { - source: TorSource::System, - process: None, - container_id: None, - }); } if let Some(binary) = find_binary("tor") { @@ -148,7 +157,7 @@ fn spawn_host_process(binary: &Path, config_dir: &Path) -> anyhow::Result anyhow::Result { "-d", "--rm", "-p", - "9050:9050", + "127.0.0.1:9050:9050", "-p", - "9051:9051", + "127.0.0.1:9051:9051", "--entrypoint", "/bin/sh", "osminogin/tor-simple", From 9ff4e0a3f877a7cf7afb04689fa22c9a0789beb6 Mon Sep 17 00:00:00 2001 From: keraliss Date: Fri, 8 May 2026 18:19:27 +0530 Subject: [PATCH 4/6] remove docker dependency, auto download and configure tor --- Cargo.toml | 2 +- src/tor_manager.rs | 379 +++++++++++++++++++++++++++++++-------------- 2 files changed, 260 insertions(+), 121 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 77f8070..8a8dec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,13 +26,13 @@ thiserror = "2.0.18" futures = "0.3.32" clap = { version = "4.5.60", features = ["derive", "env"] } dirs = "6.0.0" +ureq = { version = "2", features = ["json"] } [[test]] name = "api" path = "tests/api/mod.rs" [dev-dependencies] -ureq = { version = "2", features = ["json"] } bitcoind = { version = "0.36", features = [] } bitcoin = { version = "0.32" } log = "0.4" diff --git a/src/tor_manager.rs b/src/tor_manager.rs index 5a2fc54..0a2f284 100644 --- a/src/tor_manager.rs +++ b/src/tor_manager.rs @@ -6,98 +6,113 @@ const SOCKS_PORT: u16 = 9050; const CONTROL_PORT: u16 = 9051; const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); const STARTUP_TIMEOUT: Duration = Duration::from_secs(30); -const DOCKER_DAEMON_TIMEOUT: Duration = Duration::from_secs(60); const POLL_INTERVAL: Duration = Duration::from_millis(500); +// Update this when the Tor Project releases a new stable version. +const TOR_VERSION: &str = "14.0.7"; +const TOR_ARCHIVE_BASE: &str = "https://archive.torproject.org/tor-package-archive/torbrowser"; + enum TorSource { System, HostProcess, - Docker, + Downloaded, } pub struct TorManager { source: TorSource, process: Option, - container_id: Option, } impl TorManager { - /// Creates a no-op TorManager that assumes Tor is already managed externally. - /// Use this in tests to avoid starting Tor processes or Docker containers. #[allow(dead_code)] pub fn noop() -> Self { TorManager { source: TorSource::System, process: None, - container_id: None, } } pub fn detect_or_start(config_dir: &Path) -> anyhow::Result { + // 1. Already running? + tracing::info!( + "Checking if Tor is already running (SOCKS:{} control:{})", + SOCKS_PORT, + CONTROL_PORT + ); if port_reachable(SOCKS_PORT) { if port_reachable(CONTROL_PORT) { tracing::info!( - "Tor already running on SOCKS port {} and control port {}, using system instance", - SOCKS_PORT, CONTROL_PORT + "Tor already running on ports {}/{}, using system instance", + SOCKS_PORT, + CONTROL_PORT ); return Ok(TorManager { source: TorSource::System, process: None, - container_id: None, }); } tracing::warn!( - "Tor SOCKS port {} is reachable but control port {} is not; \ + "SOCKS port {} reachable but control port {} is not; \ falling through to start a managed instance", SOCKS_PORT, CONTROL_PORT ); + } else { + tracing::debug!("Tor not running on port {}", SOCKS_PORT); } + // 2. System tor binary on PATH? + tracing::info!("Searching for tor binary on PATH and known locations"); if let Some(binary) = find_binary("tor") { - tracing::info!( - "Found tor binary at {}, spawning host process", - binary.display() - ); - let child = spawn_host_process(&binary, config_dir)?; + tracing::info!("Found system tor at {}", binary.display()); + let child = spawn_host_process(&binary, None)?; wait_for_port(SOCKS_PORT)?; return Ok(TorManager { source: TorSource::HostProcess, process: Some(child), - container_id: None, }); } + tracing::info!("No system tor binary found"); - if find_binary("docker").is_some() { - tracing::info!("Found docker, ensuring daemon is running..."); - ensure_docker_daemon()?; - tracing::info!("Docker daemon ready, spawning tor container"); - let container_id = spawn_docker() - .map_err(|e| anyhow::anyhow!("Failed to start tor Docker container: {}", e))?; - if let Err(e) = wait_for_port(SOCKS_PORT) { - let _ = std::process::Command::new("docker") - .args(["stop", &container_id]) - .output(); - return Err(e); - } + // 3. Previously downloaded bundle? + let bundle_dir = config_dir.join("tor-bundle"); + let bundle_binary = tor_binary_in_bundle(&bundle_dir); + tracing::info!("Checking for cached bundle at {}", bundle_binary.display()); + if bundle_binary.exists() { + tracing::info!("Using cached Tor bundle"); + let lib_dir = bundle_binary.parent().map(Path::to_path_buf); + let child = spawn_host_process(&bundle_binary, lib_dir.as_deref())?; + wait_for_port(SOCKS_PORT)?; return Ok(TorManager { - source: TorSource::Docker, - process: None, - container_id: Some(container_id), + source: TorSource::Downloaded, + process: Some(child), }); } + tracing::info!("No cached bundle found"); - Err(anyhow::anyhow!( - "Tor is not running and could not be started automatically. \ - Install tor (`brew install tor` / `apt install tor`) or Docker." - )) + // 4. Download and extract + tracing::info!( + "Downloading Tor Expert Bundle v{} for {}/{}", + TOR_VERSION, + std::env::consts::OS, + std::env::consts::ARCH + ); + let binary = download_tor_bundle(&bundle_dir) + .map_err(|e| anyhow::anyhow!("Failed to download Tor Expert Bundle: {}", e))?; + let lib_dir = binary.parent().map(Path::to_path_buf); + let child = spawn_host_process(&binary, lib_dir.as_deref())?; + wait_for_port(SOCKS_PORT)?; + Ok(TorManager { + source: TorSource::Downloaded, + process: Some(child), + }) } pub fn source_label(&self) -> &'static str { match self.source { TorSource::System => "system", TorSource::HostProcess => "host", - TorSource::Docker => "docker", + TorSource::Downloaded => "downloaded", } } } @@ -106,21 +121,13 @@ impl Drop for TorManager { fn drop(&mut self) { match self.source { TorSource::System => {} - TorSource::HostProcess => { + TorSource::HostProcess | TorSource::Downloaded => { if let Some(ref mut child) = self.process { let _ = child.kill(); let _ = child.wait(); tracing::info!("Tor process stopped"); } } - TorSource::Docker => { - if let Some(ref id) = self.container_id { - tracing::info!("Stopping tor Docker container {}", id); - let _ = std::process::Command::new("docker") - .args(["stop", id]) - .output(); - } - } } } } @@ -150,8 +157,18 @@ fn find_binary(name: &str) -> Option { None } -fn spawn_host_process(binary: &Path, config_dir: &Path) -> anyhow::Result { - let tor_dir = config_dir.join("tor"); +fn coinswap_tor_dir() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".coinswap") + .join("tor") +} + +fn spawn_host_process( + binary: &Path, + lib_dir: Option<&Path>, +) -> anyhow::Result { + let tor_dir = coinswap_tor_dir(); let data_dir = tor_dir.join("data"); std::fs::create_dir_all(&data_dir)?; @@ -160,99 +177,221 @@ fn spawn_host_process(binary: &Path, config_dir: &Path) -> anyhow::Result PathBuf { + let name = if cfg!(windows) { "tor.exe" } else { "tor" }; + bundle_dir.join("tor").join(name) } -fn spawn_docker() -> anyhow::Result { - let cmd = format!( - "printf 'SocksPort 0.0.0.0:{SOCKS_PORT}\\nControlPort 0.0.0.0:{CONTROL_PORT}\\nCookieAuthentication 0\\nDataDirectory /var/lib/tor\\n' > /tmp/torrc && exec tor -f /tmp/torrc" - ); +fn download_tor_bundle(bundle_dir: &Path) -> anyhow::Result { + let os = match std::env::consts::OS { + "macos" => "macos", + "windows" => "windows", + _ => "linux", + }; + let arch = match std::env::consts::ARCH { + "aarch64" => "aarch64", + _ => "x86_64", + }; + let ext = if os == "windows" { "zip" } else { "tar.gz" }; + let filename = format!("tor-expert-bundle-{os}-{arch}-{TOR_VERSION}.{ext}"); + let url = format!("{TOR_ARCHIVE_BASE}/{TOR_VERSION}/{filename}"); - let output = std::process::Command::new("docker") - .args([ - "run", - "-d", - "--rm", - "-p", - "127.0.0.1:9050:9050", - "-p", - "127.0.0.1:9051:9051", - "--entrypoint", - "/bin/sh", - "osminogin/tor-simple", - "-c", - &cmd, - ]) - .output()?; + std::fs::create_dir_all(bundle_dir)?; + let archive_path = bundle_dir.join(&filename); - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - return Err(anyhow::anyhow!("{}", stderr)); + tracing::info!("Downloading {}", url); + let response = ureq::get(&url) + .call() + .map_err(|e| anyhow::anyhow!("Download failed: {}", e))?; + { + let mut file = std::fs::File::create(&archive_path)?; + let bytes = std::io::copy(&mut response.into_reader(), &mut file)?; + tracing::info!("Downloaded {:.1} MB", bytes as f64 / 1_048_576.0); } - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) -} + let checksum_url = format!("{TOR_ARCHIVE_BASE}/{TOR_VERSION}/sha256sums-unsigned-build.txt"); + match verify_checksum_from_manifest(&checksum_url, &filename, &archive_path) { + Ok(()) => tracing::info!("SHA256 checksum verified"), + Err(e) => tracing::warn!("Checksum verification skipped: {}", e), + } -fn is_docker_daemon_running() -> bool { - std::process::Command::new("docker") - .args(["info"]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) -} + extract_archive(&archive_path, bundle_dir)?; + let _ = std::fs::remove_file(&archive_path); -fn ensure_docker_daemon() -> anyhow::Result<()> { - if is_docker_daemon_running() { - return Ok(()); + let binary = tor_binary_in_bundle(bundle_dir); + if !binary.exists() { + return Err(anyhow::anyhow!( + "Tor binary not found at {} after extraction", + binary.display() + )); } - tracing::info!("Docker daemon not running, attempting to start it..."); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o755))?; + } - // Platform-appropriate start command — ignore errors, we'll poll below + // Apple Silicon requires all binaries to be signed; ad-hoc signing works without a certificate. #[cfg(target_os = "macos")] - let _ = std::process::Command::new("open") - .args(["-a", "Docker"]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status(); + adhoc_sign_bundle(&binary)?; + + tracing::info!("Tor Expert Bundle ready at {}", binary.display()); + Ok(binary) +} + +fn verify_checksum_from_manifest( + manifest_url: &str, + filename: &str, + file_path: &Path, +) -> anyhow::Result<()> { + let response = ureq::get(manifest_url) + .call() + .map_err(|e| anyhow::anyhow!("Could not fetch checksum manifest: {}", e))?; + let manifest = response.into_string()?; + + let expected_hash = manifest + .lines() + .find(|l| l.contains(filename)) + .and_then(|l| l.split_whitespace().next()) + .ok_or_else(|| anyhow::anyhow!("Filename not found in checksum manifest"))? + .to_string(); + + let actual_hash = sha256_of_file(file_path)?; + if actual_hash.to_lowercase() != expected_hash.to_lowercase() { + return Err(anyhow::anyhow!( + "SHA256 mismatch: expected {}, got {}", + expected_hash, + actual_hash + )); + } + Ok(()) +} + +fn sha256_of_file(path: &Path) -> anyhow::Result { + #[cfg(target_os = "macos")] + let output = std::process::Command::new("shasum") + .args(["-a", "256"]) + .arg(path) + .output()?; #[cfg(target_os = "linux")] - let _ = std::process::Command::new("systemctl") - .args(["start", "docker"]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status(); + let output = std::process::Command::new("sha256sum").arg(path).output()?; - let deadline = Instant::now() + DOCKER_DAEMON_TIMEOUT; - while Instant::now() < deadline { - std::thread::sleep(Duration::from_secs(2)); - if is_docker_daemon_running() { - tracing::info!("Docker daemon is ready"); - return Ok(()); + #[cfg(target_os = "windows")] + let output = std::process::Command::new("certutil") + .args(["-hashfile", path.to_str().unwrap_or(""), "SHA256"]) + .output()?; + + let stdout = String::from_utf8_lossy(&output.stdout); + + #[cfg(target_os = "windows")] + let hash = stdout.lines().nth(1).unwrap_or("").trim().to_string(); + #[cfg(not(target_os = "windows"))] + let hash = stdout.split_whitespace().next().unwrap_or("").to_string(); + + Ok(hash) +} + +fn extract_archive(archive: &Path, dest: &Path) -> anyhow::Result<()> { + let archive_str = archive + .to_str() + .ok_or_else(|| anyhow::anyhow!("archive path is not valid UTF-8"))?; + let dest_str = dest + .to_str() + .ok_or_else(|| anyhow::anyhow!("dest path is not valid UTF-8"))?; + + #[cfg(not(target_os = "windows"))] + { + let status = std::process::Command::new("tar") + .args(["-xzf", archive_str, "-C", dest_str]) + .status()?; + if !status.success() { + return Err(anyhow::anyhow!("tar extraction failed")); } } - Err(anyhow::anyhow!( - "Docker daemon did not start within {}s. \ - On macOS: open Docker Desktop manually. \ - On Linux: run `sudo systemctl start docker`.", - DOCKER_DAEMON_TIMEOUT.as_secs() - )) + #[cfg(target_os = "windows")] + { + let ok = std::process::Command::new("tar") + .args(["-xf", archive_str, "-C", dest_str]) + .status() + .map(|s| s.success()) + .unwrap_or(false); + if !ok { + let ps_cmd = format!( + "Expand-Archive -Force -Path '{}' -DestinationPath '{}'", + archive_str, dest_str + ); + let status = std::process::Command::new("powershell") + .args(["-Command", &ps_cmd]) + .status()?; + if !status.success() { + return Err(anyhow::anyhow!("Failed to extract zip archive")); + } + } + } + + Ok(()) +} + +#[cfg(target_os = "macos")] +fn adhoc_sign_bundle(binary: &Path) -> anyhow::Result<()> { + let lib_dir = binary + .parent() + .ok_or_else(|| anyhow::anyhow!("binary has no parent directory"))?; + + for entry in std::fs::read_dir(lib_dir)? { + let path = entry?.path(); + if path.extension().map_or(false, |e| e == "dylib") { + std::process::Command::new("codesign") + .args(["--force", "--sign", "-"]) + .arg(&path) + .status() + .map_err(|e| anyhow::anyhow!("codesign failed for {}: {}", path.display(), e))?; + } + } + + let status = std::process::Command::new("codesign") + .args(["--force", "--sign", "-"]) + .arg(binary) + .status() + .map_err(|e| anyhow::anyhow!("codesign failed: {}", e))?; + + if !status.success() { + return Err(anyhow::anyhow!("codesign exited with {}", status)); + } + + Ok(()) } fn wait_for_port(port: u16) -> anyhow::Result<()> { From b5a2d620a48d696e97af568c56870c2996ed4ca3 Mon Sep 17 00:00:00 2001 From: keraliss Date: Mon, 11 May 2026 15:05:31 +0530 Subject: [PATCH 5/6] tor download timeout,checksum --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/tor_manager.rs | 88 +++++++++++++++++++++++++--------------------- 3 files changed, 51 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5327186..b7a5cb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1547,11 +1547,13 @@ dependencies = [ "coinswap", "dirs 6.0.0", "futures", + "hex", "http-body-util", "log", "serde", "serde_cbor", "serde_json", + "sha2", "thiserror 2.0.18", "tokio", "tower", diff --git a/Cargo.toml b/Cargo.toml index 8a8dec8..a4b3028 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ futures = "0.3.32" clap = { version = "4.5.60", features = ["derive", "env"] } dirs = "6.0.0" ureq = { version = "2", features = ["json"] } +sha2 = "0.10" +hex = "0.4" [[test]] name = "api" diff --git a/src/tor_manager.rs b/src/tor_manager.rs index 0a2f284..ef2e52d 100644 --- a/src/tor_manager.rs +++ b/src/tor_manager.rs @@ -1,3 +1,4 @@ +use std::io::Read; use std::net::TcpStream; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; @@ -7,6 +8,10 @@ const CONTROL_PORT: u16 = 9051; const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); const STARTUP_TIMEOUT: Duration = Duration::from_secs(30); const POLL_INTERVAL: Duration = Duration::from_millis(500); +const DOWNLOAD_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); +const DOWNLOAD_READ_TIMEOUT: Duration = Duration::from_secs(120); +const MAX_ARCHIVE_BYTES: u64 = 200 * 1024 * 1024; // 200 MB +const MAX_MANIFEST_BYTES: u64 = 1024 * 1024; // 1 MB // Update this when the Tor Project releases a new stable version. const TOR_VERSION: &str = "14.0.7"; @@ -65,7 +70,7 @@ impl TorManager { tracing::info!("Searching for tor binary on PATH and known locations"); if let Some(binary) = find_binary("tor") { tracing::info!("Found system tor at {}", binary.display()); - let child = spawn_host_process(&binary, None)?; + let child = spawn_host_process(&binary, None, &config_dir.join("tor"))?; wait_for_port(SOCKS_PORT)?; return Ok(TorManager { source: TorSource::HostProcess, @@ -81,7 +86,8 @@ impl TorManager { if bundle_binary.exists() { tracing::info!("Using cached Tor bundle"); let lib_dir = bundle_binary.parent().map(Path::to_path_buf); - let child = spawn_host_process(&bundle_binary, lib_dir.as_deref())?; + let child = + spawn_host_process(&bundle_binary, lib_dir.as_deref(), &config_dir.join("tor"))?; wait_for_port(SOCKS_PORT)?; return Ok(TorManager { source: TorSource::Downloaded, @@ -100,7 +106,7 @@ impl TorManager { let binary = download_tor_bundle(&bundle_dir) .map_err(|e| anyhow::anyhow!("Failed to download Tor Expert Bundle: {}", e))?; let lib_dir = binary.parent().map(Path::to_path_buf); - let child = spawn_host_process(&binary, lib_dir.as_deref())?; + let child = spawn_host_process(&binary, lib_dir.as_deref(), &config_dir.join("tor"))?; wait_for_port(SOCKS_PORT)?; Ok(TorManager { source: TorSource::Downloaded, @@ -157,18 +163,11 @@ fn find_binary(name: &str) -> Option { None } -fn coinswap_tor_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(".coinswap") - .join("tor") -} - fn spawn_host_process( binary: &Path, lib_dir: Option<&Path>, + tor_dir: &Path, ) -> anyhow::Result { - let tor_dir = coinswap_tor_dir(); let data_dir = tor_dir.join("data"); std::fs::create_dir_all(&data_dir)?; @@ -221,28 +220,38 @@ fn download_tor_bundle(bundle_dir: &Path) -> anyhow::Result { "aarch64" => "aarch64", _ => "x86_64", }; - let ext = if os == "windows" { "zip" } else { "tar.gz" }; + let ext = "tar.gz"; let filename = format!("tor-expert-bundle-{os}-{arch}-{TOR_VERSION}.{ext}"); let url = format!("{TOR_ARCHIVE_BASE}/{TOR_VERSION}/{filename}"); std::fs::create_dir_all(bundle_dir)?; let archive_path = bundle_dir.join(&filename); + let agent = ureq::AgentBuilder::new() + .timeout_connect(DOWNLOAD_CONNECT_TIMEOUT) + .timeout_read(DOWNLOAD_READ_TIMEOUT) + .build(); + tracing::info!("Downloading {}", url); - let response = ureq::get(&url) + let response = agent + .get(&url) .call() .map_err(|e| anyhow::anyhow!("Download failed: {}", e))?; { let mut file = std::fs::File::create(&archive_path)?; - let bytes = std::io::copy(&mut response.into_reader(), &mut file)?; + let bytes = std::io::copy( + &mut response.into_reader().take(MAX_ARCHIVE_BYTES), + &mut file, + )?; tracing::info!("Downloaded {:.1} MB", bytes as f64 / 1_048_576.0); } let checksum_url = format!("{TOR_ARCHIVE_BASE}/{TOR_VERSION}/sha256sums-unsigned-build.txt"); - match verify_checksum_from_manifest(&checksum_url, &filename, &archive_path) { - Ok(()) => tracing::info!("SHA256 checksum verified"), - Err(e) => tracing::warn!("Checksum verification skipped: {}", e), + if let Err(e) = verify_checksum_from_manifest(&agent, &checksum_url, &filename, &archive_path) { + let _ = std::fs::remove_file(&archive_path); + return Err(anyhow::anyhow!("Checksum verification failed: {}", e)); } + tracing::info!("SHA256 checksum verified"); extract_archive(&archive_path, bundle_dir)?; let _ = std::fs::remove_file(&archive_path); @@ -270,14 +279,21 @@ fn download_tor_bundle(bundle_dir: &Path) -> anyhow::Result { } fn verify_checksum_from_manifest( + agent: &ureq::Agent, manifest_url: &str, filename: &str, file_path: &Path, ) -> anyhow::Result<()> { - let response = ureq::get(manifest_url) + let response = agent + .get(manifest_url) .call() .map_err(|e| anyhow::anyhow!("Could not fetch checksum manifest: {}", e))?; - let manifest = response.into_string()?; + let mut manifest = String::new(); + response + .into_reader() + .take(MAX_MANIFEST_BYTES) + .read_to_string(&mut manifest) + .map_err(|e| anyhow::anyhow!("Failed to read checksum manifest: {}", e))?; let expected_hash = manifest .lines() @@ -298,28 +314,18 @@ fn verify_checksum_from_manifest( } fn sha256_of_file(path: &Path) -> anyhow::Result { - #[cfg(target_os = "macos")] - let output = std::process::Command::new("shasum") - .args(["-a", "256"]) - .arg(path) - .output()?; - - #[cfg(target_os = "linux")] - let output = std::process::Command::new("sha256sum").arg(path).output()?; - - #[cfg(target_os = "windows")] - let output = std::process::Command::new("certutil") - .args(["-hashfile", path.to_str().unwrap_or(""), "SHA256"]) - .output()?; - - let stdout = String::from_utf8_lossy(&output.stdout); - - #[cfg(target_os = "windows")] - let hash = stdout.lines().nth(1).unwrap_or("").trim().to_string(); - #[cfg(not(target_os = "windows"))] - let hash = stdout.split_whitespace().next().unwrap_or("").to_string(); - - Ok(hash) + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + let mut file = std::fs::File::open(path)?; + let mut buf = [0u8; 65536]; + loop { + let n = file.read(&mut buf)?; + if n == 0 { + break; + } + hasher.update(&buf[..n]); + } + Ok(hex::encode(hasher.finalize())) } fn extract_archive(archive: &Path, dest: &Path) -> anyhow::Result<()> { From f0d77faf2e74191a8c1f53bbb6129243dcba1559 Mon Sep 17 00:00:00 2001 From: keraliss Date: Thu, 14 May 2026 14:47:19 +0530 Subject: [PATCH 6/6] add libtor --- .cargo/config.toml | 7 + Cargo.lock | 292 ++++++++++++++++++++++++++++------ Cargo.toml | 5 +- src/api/makers.rs | 15 +- src/tor_manager.rs | 320 +++++--------------------------------- tests/integration_test.rs | 22 +-- 6 files changed, 314 insertions(+), 347 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..c3899d1 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,7 @@ +# libtor-src compiles Tor from C and needs to find OpenSSL. +# On macOS, Homebrew installs OpenSSL in a non-standard prefix, so we hint the +# configure script via CFLAGS/LDFLAGS. On Linux these paths don't exist and +# the flags are harmlessly ignored. +[env] +CFLAGS = "-I/opt/homebrew/opt/openssl@3/include" +LDFLAGS = "-L/opt/homebrew/opt/openssl@3/lib" diff --git a/Cargo.lock b/Cargo.lock index b7a5cb9..962255c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,6 +153,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "autotools" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" +dependencies = [ + "cc", +] + [[package]] name = "axum" version = "0.8.8" @@ -505,7 +514,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -558,6 +567,35 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -701,6 +739,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -709,7 +756,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -731,7 +778,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.116", "unicode-xid", ] @@ -812,7 +859,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -905,6 +952,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -961,7 +1014,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -1458,6 +1511,64 @@ dependencies = [ "libc", ] +[[package]] +name = "libtor" +version = "47.13.0+0.4.7.x" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be588c6a2f02b860a1c0e3b2a59edcb171058f8da71b8ca0ddd7bb40f102c5c" +dependencies = [ + "libtor-derive", + "libtor-sys", + "log", + "rand 0.8.5", + "sha1 0.6.1", +] + +[[package]] +name = "libtor-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177781b25e83853831c5af66320ceaf5e456e1b6d533426fcd9c7544b5543043" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "libtor-src" +version = "47.13.0+0.4.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73bef51ecfbe7e63ce5cb8757ebc59d09dca6985da7f7470931ac22eab00719" +dependencies = [ + "fs_extra", +] + +[[package]] +name = "libtor-sys" +version = "47.13.0+0.4.7.x" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb0bc2cfc5d03851617d33508acc511e46f0c2b3cbc3cda85defcb50efa628bb" +dependencies = [ + "autotools", + "cc", + "libtor-src", + "libz-sys", + "openssl-sys", +] + +[[package]] +name = "libz-sys" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1547,13 +1658,12 @@ dependencies = [ "coinswap", "dirs 6.0.0", "futures", - "hex", "http-body-util", + "libtor", "log", "serde", "serde_cbor", "serde_json", - "sha2", "thiserror 2.0.18", "tokio", "tower", @@ -1694,6 +1804,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "num-traits" version = "0.2.19" @@ -1744,7 +1860,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -1888,6 +2004,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1904,7 +2026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.116", ] [[package]] @@ -2113,7 +2235,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 2.0.116", "walkdir", ] @@ -2176,24 +2298,24 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] @@ -2210,9 +2332,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -2401,7 +2523,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2462,6 +2584,15 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2473,6 +2604,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -2587,6 +2724,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.116" @@ -2612,7 +2760,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2673,7 +2821,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2684,7 +2832,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2706,6 +2854,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -2755,7 +2934,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2879,7 +3058,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2934,7 +3113,7 @@ dependencies = [ "log", "native-tls", "rand 0.9.2", - "sha1", + "sha1 0.10.6", "thiserror 2.0.18", "utf-8", ] @@ -3020,20 +3199,34 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.12.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64 0.22.1", + "cookie_store", "flate2", "log", - "once_cell", - "rustls 0.23.37", + "percent-encoding", + "rustls 0.23.40", "rustls-pki-types", "serde", "serde_json", - "url", - "webpki-roots 0.26.11", + "ureq-proto", + "utf8-zero", + "webpki-roots 1.0.7", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", ] [[package]] @@ -3055,6 +3248,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3100,7 +3299,7 @@ checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -3211,7 +3410,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.116", "wasm-bindgen-shared", ] @@ -3276,18 +3475,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - -[[package]] -name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -3356,7 +3546,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -3367,7 +3557,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -3598,7 +3788,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.116", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3614,7 +3804,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.116", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3681,7 +3871,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", "synstructure", ] @@ -3702,7 +3892,7 @@ checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -3722,7 +3912,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", "synstructure", ] @@ -3772,7 +3962,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a4b3028..cb13be0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,9 +26,7 @@ thiserror = "2.0.18" futures = "0.3.32" clap = { version = "4.5.60", features = ["derive", "env"] } dirs = "6.0.0" -ureq = { version = "2", features = ["json"] } -sha2 = "0.10" -hex = "0.4" +libtor = "47" [[test]] name = "api" @@ -40,3 +38,4 @@ bitcoin = { version = "0.32" } log = "0.4" tower = { version = "0.5", features = ["util"] } http-body-util = "0.1" +ureq = { version = "3", features = ["json"] } diff --git a/src/api/makers.rs b/src/api/makers.rs index 5f86937..f98df25 100644 --- a/src/api/makers.rs +++ b/src/api/makers.rs @@ -136,12 +136,19 @@ async fn create_maker( Json(body): Json, ) -> (StatusCode, Json>) { let mut mgr = state.lock().await; - if mgr.has_maker(&body.id) { + let trimmed_id = body.id.trim().to_string(); + if trimmed_id.is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiResponse::err("Maker ID cannot be empty")), + ); + } + if mgr.has_maker(&trimmed_id) { return ( StatusCode::CONFLICT, Json(ApiResponse::err(format!( "Maker '{}' already exists", - body.id + trimmed_id ))), ); } @@ -207,10 +214,10 @@ async fn create_maker( } } - match mgr.create_maker(body.id.clone(), config) { + match mgr.create_maker(trimmed_id.clone(), config) { Ok(()) => ( StatusCode::CREATED, - Json(ApiResponse::ok(MakerInfo { id: body.id })), + Json(ApiResponse::ok(MakerInfo { id: trimmed_id })), ), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, diff --git a/src/tor_manager.rs b/src/tor_manager.rs index ef2e52d..471dcdd 100644 --- a/src/tor_manager.rs +++ b/src/tor_manager.rs @@ -1,4 +1,3 @@ -use std::io::Read; use std::net::TcpStream; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; @@ -6,39 +5,31 @@ use std::time::{Duration, Instant}; const SOCKS_PORT: u16 = 9050; const CONTROL_PORT: u16 = 9051; const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); -const STARTUP_TIMEOUT: Duration = Duration::from_secs(30); +const STARTUP_TIMEOUT: Duration = Duration::from_secs(60); const POLL_INTERVAL: Duration = Duration::from_millis(500); -const DOWNLOAD_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); -const DOWNLOAD_READ_TIMEOUT: Duration = Duration::from_secs(120); -const MAX_ARCHIVE_BYTES: u64 = 200 * 1024 * 1024; // 200 MB -const MAX_MANIFEST_BYTES: u64 = 1024 * 1024; // 1 MB - -// Update this when the Tor Project releases a new stable version. -const TOR_VERSION: &str = "14.0.7"; -const TOR_ARCHIVE_BASE: &str = "https://archive.torproject.org/tor-package-archive/torbrowser"; enum TorSource { System, HostProcess, - Downloaded, + Embedded, } pub struct TorManager { source: TorSource, process: Option, + _tor_thread: Option>>, } impl TorManager { - #[allow(dead_code)] pub fn noop() -> Self { TorManager { source: TorSource::System, process: None, + _tor_thread: None, } } pub fn detect_or_start(config_dir: &Path) -> anyhow::Result { - // 1. Already running? tracing::info!( "Checking if Tor is already running (SOCKS:{} control:{})", SOCKS_PORT, @@ -54,6 +45,7 @@ impl TorManager { return Ok(TorManager { source: TorSource::System, process: None, + _tor_thread: None, }); } tracing::warn!( @@ -62,55 +54,41 @@ impl TorManager { SOCKS_PORT, CONTROL_PORT ); - } else { - tracing::debug!("Tor not running on port {}", SOCKS_PORT); } - // 2. System tor binary on PATH? - tracing::info!("Searching for tor binary on PATH and known locations"); if let Some(binary) = find_binary("tor") { tracing::info!("Found system tor at {}", binary.display()); - let child = spawn_host_process(&binary, None, &config_dir.join("tor"))?; + let child = spawn_host_process(&binary, &config_dir.join("tor"))?; wait_for_port(SOCKS_PORT)?; return Ok(TorManager { source: TorSource::HostProcess, process: Some(child), + _tor_thread: None, }); } - tracing::info!("No system tor binary found"); - // 3. Previously downloaded bundle? - let bundle_dir = config_dir.join("tor-bundle"); - let bundle_binary = tor_binary_in_bundle(&bundle_dir); - tracing::info!("Checking for cached bundle at {}", bundle_binary.display()); - if bundle_binary.exists() { - tracing::info!("Using cached Tor bundle"); - let lib_dir = bundle_binary.parent().map(Path::to_path_buf); - let child = - spawn_host_process(&bundle_binary, lib_dir.as_deref(), &config_dir.join("tor"))?; - wait_for_port(SOCKS_PORT)?; - return Ok(TorManager { - source: TorSource::Downloaded, - process: Some(child), - }); - } - tracing::info!("No cached bundle found"); + tracing::info!("Starting embedded Tor"); + let data_dir = config_dir.join("tor").join("data"); + std::fs::create_dir_all(&data_dir)?; + + let handle = libtor::Tor::new() + .flag(libtor::TorFlag::DataDirectory( + data_dir.to_string_lossy().into_owned(), + )) + .flag(libtor::TorFlag::SocksPort(SOCKS_PORT)) + .flag(libtor::TorFlag::ControlPort(CONTROL_PORT)) + .flag(libtor::TorFlag::CookieAuthentication( + libtor::TorBool::False, + )) + .start_background(); - // 4. Download and extract - tracing::info!( - "Downloading Tor Expert Bundle v{} for {}/{}", - TOR_VERSION, - std::env::consts::OS, - std::env::consts::ARCH - ); - let binary = download_tor_bundle(&bundle_dir) - .map_err(|e| anyhow::anyhow!("Failed to download Tor Expert Bundle: {}", e))?; - let lib_dir = binary.parent().map(Path::to_path_buf); - let child = spawn_host_process(&binary, lib_dir.as_deref(), &config_dir.join("tor"))?; wait_for_port(SOCKS_PORT)?; + tracing::info!("Embedded Tor ready"); + Ok(TorManager { - source: TorSource::Downloaded, - process: Some(child), + source: TorSource::Embedded, + process: None, + _tor_thread: Some(handle), }) } @@ -118,21 +96,18 @@ impl TorManager { match self.source { TorSource::System => "system", TorSource::HostProcess => "host", - TorSource::Downloaded => "downloaded", + TorSource::Embedded => "embedded", } } } impl Drop for TorManager { fn drop(&mut self) { - match self.source { - TorSource::System => {} - TorSource::HostProcess | TorSource::Downloaded => { - if let Some(ref mut child) = self.process { - let _ = child.kill(); - let _ = child.wait(); - tracing::info!("Tor process stopped"); - } + if let TorSource::HostProcess = self.source { + if let Some(ref mut child) = self.process { + let _ = child.kill(); + let _ = child.wait(); + tracing::info!("Tor process stopped"); } } } @@ -152,22 +127,16 @@ fn find_binary(name: &str) -> Option { } } } - for prefix in ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin"] { let candidate = PathBuf::from(prefix).join(name); if candidate.exists() { return Some(candidate); } } - None } -fn spawn_host_process( - binary: &Path, - lib_dir: Option<&Path>, - tor_dir: &Path, -) -> anyhow::Result { +fn spawn_host_process(binary: &Path, tor_dir: &Path) -> anyhow::Result { let data_dir = tor_dir.join("data"); std::fs::create_dir_all(&data_dir)?; @@ -178,228 +147,21 @@ fn spawn_host_process( ); std::fs::write(&torrc_path, &torrc_content)?; - tracing::info!("Tor config: {}", torrc_path.display()); - tracing::info!("Tor data dir: {}", data_dir.display()); - tracing::debug!("torrc contents:\n{}", torrc_content.trim()); - - let mut cmd = std::process::Command::new(binary); - cmd.args([ - "-f", - torrc_path - .to_str() - .ok_or_else(|| anyhow::anyhow!("torrc path is not valid UTF-8"))?, - ]); - - if let Some(dir) = lib_dir { - #[cfg(target_os = "linux")] - cmd.env("LD_LIBRARY_PATH", dir); - #[cfg(target_os = "macos")] - cmd.env("DYLD_LIBRARY_PATH", dir); - } - tracing::info!("Spawning tor: {}", binary.display()); - cmd.stdout(std::process::Stdio::null()) + std::process::Command::new(binary) + .args([ + "-f", + torrc_path + .to_str() + .ok_or_else(|| anyhow::anyhow!("torrc path is not valid UTF-8"))?, + ]) + .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn() .map_err(Into::into) } -fn tor_binary_in_bundle(bundle_dir: &Path) -> PathBuf { - let name = if cfg!(windows) { "tor.exe" } else { "tor" }; - bundle_dir.join("tor").join(name) -} - -fn download_tor_bundle(bundle_dir: &Path) -> anyhow::Result { - let os = match std::env::consts::OS { - "macos" => "macos", - "windows" => "windows", - _ => "linux", - }; - let arch = match std::env::consts::ARCH { - "aarch64" => "aarch64", - _ => "x86_64", - }; - let ext = "tar.gz"; - let filename = format!("tor-expert-bundle-{os}-{arch}-{TOR_VERSION}.{ext}"); - let url = format!("{TOR_ARCHIVE_BASE}/{TOR_VERSION}/{filename}"); - - std::fs::create_dir_all(bundle_dir)?; - let archive_path = bundle_dir.join(&filename); - - let agent = ureq::AgentBuilder::new() - .timeout_connect(DOWNLOAD_CONNECT_TIMEOUT) - .timeout_read(DOWNLOAD_READ_TIMEOUT) - .build(); - - tracing::info!("Downloading {}", url); - let response = agent - .get(&url) - .call() - .map_err(|e| anyhow::anyhow!("Download failed: {}", e))?; - { - let mut file = std::fs::File::create(&archive_path)?; - let bytes = std::io::copy( - &mut response.into_reader().take(MAX_ARCHIVE_BYTES), - &mut file, - )?; - tracing::info!("Downloaded {:.1} MB", bytes as f64 / 1_048_576.0); - } - - let checksum_url = format!("{TOR_ARCHIVE_BASE}/{TOR_VERSION}/sha256sums-unsigned-build.txt"); - if let Err(e) = verify_checksum_from_manifest(&agent, &checksum_url, &filename, &archive_path) { - let _ = std::fs::remove_file(&archive_path); - return Err(anyhow::anyhow!("Checksum verification failed: {}", e)); - } - tracing::info!("SHA256 checksum verified"); - - extract_archive(&archive_path, bundle_dir)?; - let _ = std::fs::remove_file(&archive_path); - - let binary = tor_binary_in_bundle(bundle_dir); - if !binary.exists() { - return Err(anyhow::anyhow!( - "Tor binary not found at {} after extraction", - binary.display() - )); - } - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o755))?; - } - - // Apple Silicon requires all binaries to be signed; ad-hoc signing works without a certificate. - #[cfg(target_os = "macos")] - adhoc_sign_bundle(&binary)?; - - tracing::info!("Tor Expert Bundle ready at {}", binary.display()); - Ok(binary) -} - -fn verify_checksum_from_manifest( - agent: &ureq::Agent, - manifest_url: &str, - filename: &str, - file_path: &Path, -) -> anyhow::Result<()> { - let response = agent - .get(manifest_url) - .call() - .map_err(|e| anyhow::anyhow!("Could not fetch checksum manifest: {}", e))?; - let mut manifest = String::new(); - response - .into_reader() - .take(MAX_MANIFEST_BYTES) - .read_to_string(&mut manifest) - .map_err(|e| anyhow::anyhow!("Failed to read checksum manifest: {}", e))?; - - let expected_hash = manifest - .lines() - .find(|l| l.contains(filename)) - .and_then(|l| l.split_whitespace().next()) - .ok_or_else(|| anyhow::anyhow!("Filename not found in checksum manifest"))? - .to_string(); - - let actual_hash = sha256_of_file(file_path)?; - if actual_hash.to_lowercase() != expected_hash.to_lowercase() { - return Err(anyhow::anyhow!( - "SHA256 mismatch: expected {}, got {}", - expected_hash, - actual_hash - )); - } - Ok(()) -} - -fn sha256_of_file(path: &Path) -> anyhow::Result { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - let mut file = std::fs::File::open(path)?; - let mut buf = [0u8; 65536]; - loop { - let n = file.read(&mut buf)?; - if n == 0 { - break; - } - hasher.update(&buf[..n]); - } - Ok(hex::encode(hasher.finalize())) -} - -fn extract_archive(archive: &Path, dest: &Path) -> anyhow::Result<()> { - let archive_str = archive - .to_str() - .ok_or_else(|| anyhow::anyhow!("archive path is not valid UTF-8"))?; - let dest_str = dest - .to_str() - .ok_or_else(|| anyhow::anyhow!("dest path is not valid UTF-8"))?; - - #[cfg(not(target_os = "windows"))] - { - let status = std::process::Command::new("tar") - .args(["-xzf", archive_str, "-C", dest_str]) - .status()?; - if !status.success() { - return Err(anyhow::anyhow!("tar extraction failed")); - } - } - - #[cfg(target_os = "windows")] - { - let ok = std::process::Command::new("tar") - .args(["-xf", archive_str, "-C", dest_str]) - .status() - .map(|s| s.success()) - .unwrap_or(false); - if !ok { - let ps_cmd = format!( - "Expand-Archive -Force -Path '{}' -DestinationPath '{}'", - archive_str, dest_str - ); - let status = std::process::Command::new("powershell") - .args(["-Command", &ps_cmd]) - .status()?; - if !status.success() { - return Err(anyhow::anyhow!("Failed to extract zip archive")); - } - } - } - - Ok(()) -} - -#[cfg(target_os = "macos")] -fn adhoc_sign_bundle(binary: &Path) -> anyhow::Result<()> { - let lib_dir = binary - .parent() - .ok_or_else(|| anyhow::anyhow!("binary has no parent directory"))?; - - for entry in std::fs::read_dir(lib_dir)? { - let path = entry?.path(); - if path.extension().map_or(false, |e| e == "dylib") { - std::process::Command::new("codesign") - .args(["--force", "--sign", "-"]) - .arg(&path) - .status() - .map_err(|e| anyhow::anyhow!("codesign failed for {}: {}", path.display(), e))?; - } - } - - let status = std::process::Command::new("codesign") - .args(["--force", "--sign", "-"]) - .arg(binary) - .status() - .map_err(|e| anyhow::anyhow!("codesign failed: {}", e))?; - - if !status.success() { - return Err(anyhow::anyhow!("codesign exited with {}", status)); - } - - Ok(()) -} - fn wait_for_port(port: u16) -> anyhow::Result<()> { let deadline = Instant::now() + STARTUP_TIMEOUT; while Instant::now() < deadline { diff --git a/tests/integration_test.rs b/tests/integration_test.rs index d2db9c7..8a172ac 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -201,9 +201,10 @@ impl ApiClient { fn new(port: u16, creds: RpcCreds) -> Self { Self { base: format!("http://127.0.0.1:{port}/api"), - agent: ureq::AgentBuilder::new() - .timeout(Duration::from_secs(120)) - .build(), + agent: ureq::Agent::config_builder() + .timeout_global(Some(Duration::from_secs(120))) + .build() + .new_agent(), creds, } } @@ -213,7 +214,8 @@ impl ApiClient { .get(&format!("{}{path}", self.base)) .call() .unwrap_or_else(|e| panic!("GET {path} failed: {e}")) - .into_json() + .into_body() + .read_json() .unwrap_or_else(|e| panic!("JSON decode GET {path}: {e}")) } @@ -222,7 +224,8 @@ impl ApiClient { .post(&format!("{}{path}", self.base)) .send_json(body) .unwrap_or_else(|e| panic!("POST {path} failed: {e}")) - .into_json() + .into_body() + .read_json() .unwrap_or_else(|e| panic!("JSON decode POST {path}: {e}")) } @@ -236,10 +239,8 @@ impl ApiClient { .send_json(body) { Ok(resp) => resp - .into_json() - .unwrap_or_else(|e| panic!("JSON decode POST {path}: {e}")), - Err(ureq::Error::Status(_, resp)) => resp - .into_json() + .into_body() + .read_json::() .unwrap_or(serde_json::json!({"success": false})), Err(e) => panic!("POST {path} transport error: {e}"), } @@ -250,7 +251,8 @@ impl ApiClient { .put(&format!("{}{path}", self.base)) .send_json(body) .unwrap_or_else(|e| panic!("PUT {path} failed: {e}")) - .into_json() + .into_body() + .read_json() .unwrap_or_else(|e| panic!("JSON decode PUT {path}: {e}")) }