Build 48: replace Tor with the Nym mixnet; remove arti
Route every relay and HTTP request (nostr relays, NIP-05, price) through a local nym-socks5-client sidecar on 127.0.0.1:1080, so all traffic egresses via the 5-hop Nym mixnet and nothing touches the clear net. - Add src/nym/: SOCKS5 HTTP client (reqwest socks5h), NymWebSocketTransport for the nostr relay pool (tokio-socks dial + TLS/ws handshake over the mixnet), and a sidecar launcher that reuses or spawns nym-socks5-client. - Swap the nostr-sdk transport off ArtiWebSocketTransport; route nip05.rs and price.rs off Tor; revert the clearnet username-lookup shortcut. - Remove the embedded arti Tor client wholesale: the onion-service listener and send-to-onion path in the wallet, the legacy transport GUI tab, the Tor settings page, src/tor/, the webtunnel pluggable transport (Go build + submodule), and all arti crates from Cargo.toml. The Grin node connection is unchanged (chain data, no payment metadata, and never used Tor). The network requester the sidecar routes through is configured via GOBLIN_NYM_PROVIDER / NETWORK_REQUESTER at deploy time.
This commit is contained in:
@@ -5,7 +5,3 @@
|
||||
path = wallet
|
||||
url = https://code.gri.mw/ardocrat/wallet
|
||||
branch = grim
|
||||
[submodule "tor/webtunnel"]
|
||||
path = tor/webtunnel
|
||||
url = https://code.gri.mw/WEB/webtunnel
|
||||
branch = grim
|
||||
|
||||
Generated
+31
-3109
File diff suppressed because it is too large
Load Diff
+8
-22
@@ -2,7 +2,7 @@
|
||||
name = "grim"
|
||||
version = "0.3.6"
|
||||
authors = ["Ardocrat <ardocrat@gri.mw>"]
|
||||
description = "Goblin: a peer-to-peer wallet for Grin. Send and receive instantly with a handle - slatepacks and tor handled for you."
|
||||
description = "Goblin: a peer-to-peer wallet for Grin. Send and receive instantly with a handle - slatepacks and the Nym mixnet handled for you."
|
||||
license = "Apache-2.0"
|
||||
repository = "https://code.gri.mw/GUI/grim"
|
||||
keywords = [ "crypto", "grin", "mimblewimble", "nostr" ]
|
||||
@@ -99,29 +99,15 @@ tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] }
|
||||
regex = "1"
|
||||
base64 = "0.22"
|
||||
hex = "0.4"
|
||||
## clearnet HTTP for the public NIP-05 username lookup (rustls, no native TLS
|
||||
## so it cross-compiles to Android) — read-only name→key, kept off the mixnet
|
||||
## on purpose because it's a "simple API lookup" and speed matters there.
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
## HTTP client routed through the local Nym SOCKS5 sidecar (rustls, no native
|
||||
## TLS so it cross-compiles to Android; `socks` so every request — NIP-05,
|
||||
## price, avatars — goes over the mixnet, never clearnet).
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "socks"] }
|
||||
## SOCKS5 TCP dialer for the nostr relay WebSocket transport over the mixnet.
|
||||
tokio-socks = "0.5"
|
||||
|
||||
## tor
|
||||
arti-client = { version = "0.42.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.42.0", features = ["static"] }
|
||||
tor-config = "0.42.0"
|
||||
fs-mistrust = "0.14.1"
|
||||
tor-hsservice = "0.42.0"
|
||||
tor-hsrproxy = "0.42.0"
|
||||
tor-keymgr = "0.42.0"
|
||||
tor-llcrypto = "0.42.0"
|
||||
tor-hscrypto = "0.42.0"
|
||||
tor-error = "0.42.0"
|
||||
## NIP-98 payload hashing
|
||||
sha2 = "0.10.8"
|
||||
ed25519-dalek = "2.1.1"
|
||||
curve25519-dalek = "4.1.3"
|
||||
hyper-tor = { version = "0.14.32", features = ["full"], package = "hyper" }
|
||||
tls-api = "0.12.0"
|
||||
tls-api-native-tls = "0.12.1"
|
||||
safelog = "0.8.1"
|
||||
|
||||
## stratum server
|
||||
tokio-old = { version = "0.2", features = ["full"], package = "tokio" }
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
|
||||
Goblin is a private, Cash App-style wallet for [GRIN ツ](https://grin.mw) — confidential digital cash on [Mimblewimble](https://github.com/mimblewimble/grin), with no amounts or addresses on the chain.
|
||||
|
||||
Instead of passing slatepack files back and forth, you **pay a `@username` (or an `npub`)** and the payment is delivered for you as an **end-to-end encrypted message over [nostr](https://github.com/nostr-protocol/nips) and Tor**. Relays only ever see ciphertext — never the amount, the sender, or the recipient.
|
||||
Instead of passing slatepack files back and forth, you **pay a `@username` (or an `npub`)** and the payment is delivered for you as an **end-to-end encrypted message over [nostr](https://github.com/nostr-protocol/nips), routed through the [Nym mixnet](https://nym.com)**. Relays only ever see ciphertext — never the amount, the sender, or the recipient — and the mixnet hides who is talking to whom at the network layer.
|
||||
|
||||
Goblin is a fork of the **Grim** egui GRIN wallet: it keeps Grim's full GRIN node/wallet engine and layers a Nostr-native, mobile-first payments experience on top.
|
||||
|
||||
## What it does
|
||||
|
||||
- **Send to people** — pay a `@username` or `npub`; the GRIN slatepack travels as a [NIP-17](https://nips.nostr.com/17) gift-wrapped DM ([kind 1059](https://nostrbook.dev/kinds/1059)) over Tor and is applied automatically by the recipient's wallet. No files to swap, no need to both be online at once.
|
||||
- **Send to people** — pay a `@username` or `npub`; the GRIN slatepack travels as a [NIP-17](https://nips.nostr.com/17) gift-wrapped DM ([kind 1059](https://nostrbook.dev/kinds/1059)) over the Nym mixnet and is applied automatically by the recipient's wallet. No files to swap, no need to both be online at once.
|
||||
- **In-app identity** — a nostr payment key that is deliberately *not* part of your seed, so you can rotate it any time to stay unlinkable without touching your funds. An optional human-readable `@name` (and hosted avatar) comes from the goblin.st identity service.
|
||||
- **Private by construction** — GRIN's address-less, confidential chain; every relay and HTTP request routed through an embedded [arti](https://gitlab.torproject.org/tpo/core/arti) Tor client (webtunnel bridge by default); keys, names and history stay on your device.
|
||||
- **Configurable amount pairing** — show balances against a world currency, Bitcoin, or sats (rates fetched over Tor), or turn the preview off.
|
||||
- **Private by construction** — GRIN's address-less, confidential chain; every relay and HTTP request (relays, NIP-05 lookups, price, avatars) routed through the [Nym mixnet](https://nym.com) via a bundled `nym-socks5-client` sidecar, so nothing touches the clear net; keys, names and history stay on your device.
|
||||
- **Configurable amount pairing** — show balances against a world currency, Bitcoin, or sats (rates fetched over the mixnet), or turn the preview off.
|
||||
- **Cross-platform** — Linux, macOS, Windows, Android, built in pure Rust on [egui](https://github.com/emilk/egui).
|
||||
|
||||
## How a payment travels
|
||||
@@ -23,7 +23,7 @@ Goblin is a fork of the **Grim** egui GRIN wallet: it keeps Grim's full GRIN nod
|
||||
```
|
||||
you ──slatepack──▶ NIP-17 gift wrap (kind 1059, NIP-44 encrypted)
|
||||
│
|
||||
arti / Tor
|
||||
Nym mixnet (5-hop)
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
your relays recipient's DM relays (kind 10050)
|
||||
@@ -46,7 +46,7 @@ cargo build --release
|
||||
./target/release/goblin
|
||||
```
|
||||
|
||||
Tor's webtunnel bridge is built from Go at compile time — install Go first (e.g. `pacman -S go`). Without it the bundled bridge is inert and Tor will not bootstrap on networks that block direct Tor connections.
|
||||
Goblin routes all of its traffic over the [Nym mixnet](https://nym.com) using a `nym-socks5-client` sidecar that runs alongside the wallet and exposes a local SOCKS5 proxy on `127.0.0.1:1080`. Ship the `nym-socks5-client` binary next to the `goblin` executable (or point `GOBLIN_NYM_BIN` at it), and set the network requester it routes through via `GOBLIN_NYM_PROVIDER` (or bake it into `NETWORK_REQUESTER` in `src/nym/sidecar.rs`). If a SOCKS5 endpoint is already listening on `127.0.0.1:1080`, Goblin reuses it.
|
||||
|
||||
### Android
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::{env, fs};
|
||||
|
||||
/// The GRIM commit Goblin forked from; builds count commits on top of it.
|
||||
const GOBLIN_FORK_BASE: &str = "b51a46b";
|
||||
@@ -56,90 +56,7 @@ fn main() {
|
||||
.expect("failed to execute git config for hooks");
|
||||
}
|
||||
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let tor_out_dir = format!("{}/tor", out_dir);
|
||||
let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir);
|
||||
let exists = fs::exists(&webtunnel_file).unwrap();
|
||||
if !exists {
|
||||
// Create empty webtunnel file to allow build with include_bytes! macro.
|
||||
fs::create_dir(&tor_out_dir).unwrap_or_default();
|
||||
fs::File::create(&webtunnel_file).unwrap();
|
||||
}
|
||||
|
||||
let target = env::var("TARGET").unwrap();
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
if target_os == "ios" {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_android = target_os == "android";
|
||||
if is_android {
|
||||
// Set a path to Android Webtunnel binary.
|
||||
let arch = if target.contains("aarch64") {
|
||||
"arm64-v8a"
|
||||
} else if target.contains("arm") {
|
||||
"armeabi-v7a"
|
||||
} else {
|
||||
"x86_64"
|
||||
};
|
||||
let root = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
webtunnel_file = format!(
|
||||
"{}/android/app/src/main/jniLibs/{}/libwebtunnel.so",
|
||||
root, arch
|
||||
);
|
||||
}
|
||||
|
||||
// Build if Webtunnel binary is empty or not exists.
|
||||
let empty = match fs::File::open(&webtunnel_file) {
|
||||
Ok(file) => file.metadata().unwrap().len() == 0,
|
||||
Err(_) => true,
|
||||
};
|
||||
let build = !exists || empty;
|
||||
if build {
|
||||
// Setup GOOS env variable.
|
||||
let go_os = if target_os == "macos" {
|
||||
"darwin"
|
||||
} else {
|
||||
target_os.as_str()
|
||||
};
|
||||
// Setup GOARCH env variable.
|
||||
let go_arch = if target.contains("aarch64") {
|
||||
"arm64"
|
||||
} else if target.contains("arm") {
|
||||
"arm"
|
||||
} else {
|
||||
"amd64"
|
||||
};
|
||||
// Run Webtunnel Go build.
|
||||
let output = if env::consts::OS == "windows" {
|
||||
Command::new("./scripts/webtunnel.bat")
|
||||
.arg(go_os)
|
||||
.arg(go_arch)
|
||||
.arg(&webtunnel_file)
|
||||
.output()
|
||||
} else {
|
||||
Command::new("bash")
|
||||
.arg("./scripts/webtunnel.sh")
|
||||
.arg(go_os)
|
||||
.arg(go_arch)
|
||||
.arg(&webtunnel_file)
|
||||
.output()
|
||||
};
|
||||
if let Ok(out) = output {
|
||||
if out.status.code().is_none() || out.status.code().unwrap() != 0 {
|
||||
panic!("webtunnel go build failed:\n{:?}", out);
|
||||
}
|
||||
}
|
||||
// The build script exits 0 when Go is absent, leaving the placeholder
|
||||
// empty — surface that loudly instead of shipping broken bridges.
|
||||
let still_empty = fs::metadata(&webtunnel_file)
|
||||
.map(|m| m.len() == 0)
|
||||
.unwrap_or(true);
|
||||
if still_empty {
|
||||
println!(
|
||||
"cargo:warning=webtunnel client was not built (is Go installed?) — \
|
||||
Tor webtunnel bridges will not work at runtime"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Goblin routes all traffic over the Nym mixnet via a bundled
|
||||
// `nym-socks5-client` sidecar (see src/nym/); there is no embedded Tor and
|
||||
// thus no webtunnel pluggable-transport binary to build here anymore.
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: Change directory to the script's location
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: Skip if Go not found.
|
||||
where go >nul 2>nul
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Go could not be found
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
set "go_os=%~1"
|
||||
set "go_arch=%~2"
|
||||
set "output_path=%~3"
|
||||
|
||||
echo Go build for os: %go_os%, arch: %go_arch%
|
||||
|
||||
:: Setup vars for Android.
|
||||
if "%go_os%"=="android" (
|
||||
|
||||
:: Setup NDK root path env.
|
||||
if "%ANDROID_NDK_HOME%"=="" (
|
||||
:: Extract ndkVersion from build.gradle
|
||||
:: Equivalent to: cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d ' -f 2
|
||||
for /f "tokens=2 delims='" %%a in ('findstr "ndkVersion" ..\android\app\build.gradle') do (
|
||||
set "NDK_VERSION=%%a"
|
||||
)
|
||||
set "ANDROID_NDK_HOME=%ANDROID_HOME%\ndk\!NDK_VERSION!"
|
||||
)
|
||||
|
||||
:: Setup NDK host path.
|
||||
:: Since this is a Batch script, the host is Windows.
|
||||
set "arch_host=windows-x86_64"
|
||||
|
||||
:: Setup NDK target arch.
|
||||
if "%go_arch%"=="arm64" (
|
||||
set "arch_bin_prefix=aarch64-linux-android"
|
||||
) else if "%go_arch%"=="arm" (
|
||||
set "arch_bin_prefix=armv7a-linux-androideabi"
|
||||
) else (
|
||||
set "arch_bin_prefix=x86_64-linux-android"
|
||||
)
|
||||
|
||||
:: Build for current target.
|
||||
set "CGO_ENABLED=1"
|
||||
set "GOOS=%go_os%"
|
||||
set "GOARCH=%go_arch%"
|
||||
|
||||
:: Define CC and CXX paths
|
||||
set "CC=!ANDROID_NDK_HOME!\toolchains\llvm\prebuilt\!arch_host!\bin\!arch_bin_prefix!35-clang"
|
||||
set "CXX=!ANDROID_NDK_HOME!\toolchains\llvm\prebuilt\!arch_host!\bin\!arch_bin_prefix!35-clang++"
|
||||
|
||||
go build -C "../tor/webtunnel" -ldflags="-s -w" -o "%output_path%" code.gri.mw/WEB/webtunnel/main/client
|
||||
|
||||
) else (
|
||||
set "extra_flag="
|
||||
if "%go_os%"=="windows" (
|
||||
set "extra_flag=-H=windowsgui"
|
||||
)
|
||||
|
||||
set "GOOS=%go_os%"
|
||||
set "GOARCH=%go_arch%"
|
||||
|
||||
:: Build for non-android targets
|
||||
go build -C "../tor/webtunnel" -ldflags="-s -w !extra_flag!" -o "%output_path%" code.gri.mw/WEB/webtunnel/main/client
|
||||
)
|
||||
|
||||
endlocal
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Skip if Go not found.
|
||||
if ! command -v go >/dev/null 2>&1
|
||||
then
|
||||
echo "Go could not be found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
go_os=$1
|
||||
go_arch=$2
|
||||
|
||||
echo "Go build for os: $go_os, arch: $go_arch"
|
||||
|
||||
# Setup vars for Android.
|
||||
if [[ "$go_os" == "android" ]]; then
|
||||
# Setup NDK root path env.
|
||||
if [[ -z "$ANDROID_NDK_HOME" ]]; then
|
||||
NDK_VERSION=$(cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d \' -f 2)
|
||||
ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION
|
||||
fi
|
||||
# Setup NDK host path.
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
arch_host=darwin-x86_64
|
||||
else
|
||||
if [[ "$(uname -m)" == "aarch64" ]]; then
|
||||
arch_host=linux-arm64
|
||||
else
|
||||
arch_host=linux-x86_64
|
||||
fi
|
||||
fi
|
||||
# Setup NDK target arch.
|
||||
if [[ "$go_arch" == "arm64" ]]; then
|
||||
arch_bin_prefix=aarch64-linux-android
|
||||
elif [[ "$go_arch" == "arm" ]]; then
|
||||
arch_bin_prefix=armv7a-linux-androideabi
|
||||
else
|
||||
arch_bin_prefix=x86_64-linux-android
|
||||
fi
|
||||
|
||||
# Build for current target.
|
||||
CGO_ENABLED=1 GOOS=$1 GOARCH=$2 CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_host}/bin/${arch_bin_prefix}35-clang" CXX="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_path}/bin/${arch_bin_prefix}35-clang++" go build -C "../tor/webtunnel" -ldflags="-s -w" -o "$3" code.gri.mw/WEB/webtunnel/main/client
|
||||
else
|
||||
if [[ "$go_os" == "windows" ]]; then
|
||||
extra_flag="-H=windowsgui"
|
||||
fi
|
||||
GOOS=$1 GOARCH=$2 go build -C "../tor/webtunnel" -ldflags="-s -w ${extra_flag}" -o "$3" code.gri.mw/WEB/webtunnel/main/client
|
||||
fi
|
||||
|
||||
@@ -1999,8 +1999,8 @@ impl GoblinWalletView {
|
||||
if settings_row_nav(ui, "nostr-sdk", "0.44") {
|
||||
open_url(ui, "https://github.com/rust-nostr/nostr");
|
||||
}
|
||||
if settings_row_nav(ui, "arti (Tor)", "0.42") {
|
||||
open_url(ui, "https://gitlab.torproject.org/tpo/core/arti");
|
||||
if settings_row_nav(ui, "Nym mixnet", "socks5") {
|
||||
open_url(ui, "https://nym.com");
|
||||
}
|
||||
if settings_row_nav(ui, "egui", "0.33") {
|
||||
open_url(ui, "https://github.com/emilk/egui");
|
||||
|
||||
@@ -20,6 +20,3 @@ pub use interface::*;
|
||||
|
||||
mod network;
|
||||
pub use network::*;
|
||||
|
||||
mod tor;
|
||||
pub use tor::*;
|
||||
|
||||
@@ -1,678 +0,0 @@
|
||||
// Copyright 2025 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use eframe::epaint::RectShape;
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, CursorIcon, Id, Layout, RichText, ScrollArea, Sense, StrokeKind, UiBuilder};
|
||||
use std::fs;
|
||||
use url::Url;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{CLIPBOARD_TEXT, CLOUD_CHECK, NOTCHES, PENCIL, SCAN, TERMINAL};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{
|
||||
CameraScanContent, FilePickContent, FilePickContentType, Modal, TextEdit, View,
|
||||
};
|
||||
use crate::tor::{TorBridge, TorConfig, TorProxy};
|
||||
|
||||
/// Transport settings content.
|
||||
pub struct TorSettingsContent {
|
||||
/// Flag to check if settings were changed.
|
||||
pub settings_changed: bool,
|
||||
|
||||
/// Proxy URL input value for [`Modal`].
|
||||
proxy_url_edit: String,
|
||||
/// Flag to check if entered proxy address was correct.
|
||||
proxy_url_error: bool,
|
||||
|
||||
/// Tor bridge binary path value for [`Modal`].
|
||||
bridge_bin_path_edit: String,
|
||||
/// Button to pick binary file for bridge.
|
||||
bridge_bin_pick_file: FilePickContent,
|
||||
|
||||
/// Tor bridge connection line value for [`Modal`].
|
||||
bridge_conn_line_edit: String,
|
||||
/// Bridge line QR code scanner [`Modal`] content.
|
||||
bridge_qr_scan_content: Option<CameraScanContent>,
|
||||
}
|
||||
|
||||
/// Identifier for proxy URL edit [`Modal`].
|
||||
const PROXY_URL_EDIT_MODAL: &'static str = "tor_proxy_edit_modal";
|
||||
/// Identifier for bridge binary path input [`Modal`].
|
||||
const BRIDGE_BIN_EDIT_MODAL: &'static str = "bridge_bin_edit_modal";
|
||||
/// Identifier for bridge connection line input [`Modal`].
|
||||
const BRIDGE_CONN_LINE_EDIT_MODAL: &'static str = "bridge_conn_line_edit_modal";
|
||||
/// Identifier for [`Modal`] to scan bridge line from QR code.
|
||||
const SCAN_BRIDGE_CONN_LINE_MODAL: &'static str = "scan_bridge_conn_line_modal";
|
||||
|
||||
impl Default for TorSettingsContent {
|
||||
fn default() -> Self {
|
||||
// Setup Tor bridge binary path edit text.
|
||||
let bridge = TorConfig::get_bridge();
|
||||
let (bin_path, conn_line) = if let Some(b) = bridge {
|
||||
(b.binary_path(), b.connection_line())
|
||||
} else {
|
||||
("".to_string(), "".to_string())
|
||||
};
|
||||
Self {
|
||||
settings_changed: false,
|
||||
proxy_url_edit: "".to_string(),
|
||||
proxy_url_error: false,
|
||||
bridge_bin_path_edit: bin_path,
|
||||
bridge_bin_pick_file: FilePickContent::new(FilePickContentType::ItemButton(
|
||||
View::item_rounding(0, 1, true),
|
||||
))
|
||||
.no_parse(),
|
||||
bridge_conn_line_edit: conn_line,
|
||||
bridge_qr_scan_content: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentContainer for TorSettingsContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![
|
||||
PROXY_URL_EDIT_MODAL,
|
||||
BRIDGE_BIN_EDIT_MODAL,
|
||||
BRIDGE_CONN_LINE_EDIT_MODAL,
|
||||
SCAN_BRIDGE_CONN_LINE_MODAL,
|
||||
]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
match modal.id {
|
||||
PROXY_URL_EDIT_MODAL => self.proxy_modal_ui(ui, cb),
|
||||
BRIDGE_BIN_EDIT_MODAL => self.bridge_bin_edit_modal_ui(ui, cb),
|
||||
BRIDGE_CONN_LINE_EDIT_MODAL => self.bridge_conn_line_edit_modal_ui(ui, cb),
|
||||
SCAN_BRIDGE_CONN_LINE_MODAL => {
|
||||
if let Some(content) = self.bridge_qr_scan_content.as_mut() {
|
||||
let mut close = false;
|
||||
content.modal_ui(ui, cb, |res| {
|
||||
// Save connection line after scanning.
|
||||
let line = res.text();
|
||||
let bridge = TorConfig::get_bridge().unwrap();
|
||||
if bridge.connection_line() != line {
|
||||
TorBridge::save_bridge_conn_line(&bridge, line);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
close = true;
|
||||
});
|
||||
if close {
|
||||
self.bridge_qr_scan_content = None;
|
||||
cb.stop_camera();
|
||||
Modal::close();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
ui.label(
|
||||
RichText::new(format!("{}:", t!("wallets.conn_method")))
|
||||
.size(17.0)
|
||||
.color(Colors::inactive_text()),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
|
||||
let mut proxy = TorConfig::get_proxy();
|
||||
let current_proxy = proxy.clone();
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
let name = t!("network_settings.default");
|
||||
View::radio_value(ui, &mut proxy, None, name);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let name = t!("app_settings.proxy");
|
||||
let val = current_proxy
|
||||
.clone()
|
||||
.unwrap_or(TorProxy::SOCKS5(TorProxy::DEFAULT_SOCKS5_URL.to_string()));
|
||||
View::radio_value(ui, &mut proxy, Some(val), name);
|
||||
});
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(6.0);
|
||||
|
||||
if let Some(p) = proxy.as_mut() {
|
||||
ui.label(
|
||||
RichText::new(format!("{}:", t!("app_settings.proxy")))
|
||||
.size(17.0)
|
||||
.color(Colors::inactive_text()),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
let value = TorConfig::get_socks5_proxy();
|
||||
View::radio_value(ui, p, value, "SOCKS5".to_string());
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
let value = TorConfig::get_http_proxy();
|
||||
View::radio_value(ui, p, value, "HTTP".to_string());
|
||||
});
|
||||
});
|
||||
ui.add_space(14.0);
|
||||
// Show proxy settings.
|
||||
self.proxy_item_ui(p.url(), ui);
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
// Check if proxy type was changed to save.
|
||||
if current_proxy != proxy {
|
||||
TorConfig::save_proxy(proxy.clone());
|
||||
self.settings_changed = true;
|
||||
}
|
||||
if proxy.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let bridge = TorConfig::get_bridge();
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(t!("transport.bridges_desc"))
|
||||
.size(17.0)
|
||||
.color(Colors::inactive_text()),
|
||||
);
|
||||
|
||||
// Draw checkbox to enable/disable bridges.
|
||||
View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || {
|
||||
let value = if bridge.is_some() {
|
||||
None
|
||||
} else {
|
||||
let default_bridge = TorConfig::get_webtunnel();
|
||||
self.bridge_bin_path_edit = default_bridge.binary_path();
|
||||
self.bridge_conn_line_edit = default_bridge.connection_line();
|
||||
Some(default_bridge)
|
||||
};
|
||||
TorConfig::save_bridge(value);
|
||||
self.settings_changed = true;
|
||||
});
|
||||
});
|
||||
|
||||
if bridge.is_some() {
|
||||
ui.add_space(6.0);
|
||||
// Show bridge selection for desktop.
|
||||
if View::is_desktop() {
|
||||
let current_bridge = bridge.unwrap();
|
||||
let mut bridge = current_bridge.clone();
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered(|ui| {
|
||||
// Show Webtunnel bridge selector.
|
||||
let webtunnel = TorConfig::get_webtunnel();
|
||||
let name = webtunnel.protocol_name().to_uppercase();
|
||||
View::radio_value(ui, &mut bridge, webtunnel, name);
|
||||
});
|
||||
columns[1].vertical_centered(|ui| {
|
||||
// Show Obfs4 bridge selector.
|
||||
let obfs4 = TorConfig::get_obfs4();
|
||||
let name = obfs4.protocol_name().to_uppercase();
|
||||
View::radio_value(ui, &mut bridge, obfs4, name);
|
||||
});
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
// Show Snowflake bridge selector.
|
||||
let snowflake = TorConfig::get_snowflake();
|
||||
let name = snowflake.protocol_name().to_uppercase();
|
||||
View::radio_value(ui, &mut bridge, snowflake, name);
|
||||
});
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Check if bridge type was changed to save.
|
||||
if current_bridge != bridge {
|
||||
TorConfig::save_bridge(Some(bridge.clone()));
|
||||
self.bridge_bin_path_edit = bridge.binary_path();
|
||||
self.bridge_conn_line_edit = bridge.connection_line();
|
||||
self.settings_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(br) = TorConfig::get_bridge().as_ref() {
|
||||
// Show bridge connection line setup.
|
||||
self.conn_line_ui(ui, br, cb);
|
||||
// Show bridge binary setup for desktop.
|
||||
if View::is_desktop() {
|
||||
self.bridge_bin_ui(ui, br, cb);
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TorSettingsContent {
|
||||
/// Draw proxy edit modal content.
|
||||
fn proxy_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut TorSettingsContent| {
|
||||
let http = "http://";
|
||||
let socks = "socks5://";
|
||||
let url = c.proxy_url_edit.trim().to_string();
|
||||
c.proxy_url_error = Url::parse(url.as_str()).is_err();
|
||||
if !c.proxy_url_error {
|
||||
let proxy = TorConfig::get_proxy().unwrap();
|
||||
if url.contains(socks) {
|
||||
TorConfig::save_proxy(Some(TorProxy::SOCKS5(url)));
|
||||
} else if url.contains(http) {
|
||||
TorConfig::save_proxy(Some(TorProxy::HTTP(url)));
|
||||
} else {
|
||||
match proxy {
|
||||
TorProxy::SOCKS5(_) => {
|
||||
TorConfig::save_proxy(Some(TorProxy::SOCKS5(url)));
|
||||
}
|
||||
TorProxy::HTTP(_) => {
|
||||
TorConfig::save_proxy(Some(TorProxy::HTTP(url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let label = format!("{}:", t!("enter_url"));
|
||||
ui.label(RichText::new(label).size(17.0).color(Colors::gray()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw proxy URL text edit.
|
||||
let mut edit =
|
||||
TextEdit::new(Id::from("proxy_url_edit").with(PROXY_URL_EDIT_MODAL)).paste();
|
||||
edit.ui(ui, &mut self.proxy_url_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
|
||||
// Show error when specified address is incorrect.
|
||||
if self.proxy_url_error {
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
RichText::new(t!("wallets.invalid_url"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()),
|
||||
);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Show modal buttons.
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(
|
||||
ui,
|
||||
t!("modal.cancel"),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
Modal::close();
|
||||
},
|
||||
);
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw proxy item content.
|
||||
fn proxy_item_ui(&mut self, url: String, ui: &mut egui::Ui) {
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
|
||||
// Draw round background.
|
||||
let bg_rect = rect.clone();
|
||||
let item_rounding = View::item_rounding(0, 1, false);
|
||||
ui.painter().rect(
|
||||
bg_rect,
|
||||
item_rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside,
|
||||
);
|
||||
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), PENCIL, None, || {
|
||||
self.proxy_url_edit = url.clone();
|
||||
// Show proxy URL edit modal.
|
||||
Modal::new(PROXY_URL_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("app_settings.proxy"))
|
||||
.show();
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
View::ellipsize_text(ui, url, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
let value = format!("{} {}", CLOUD_CHECK, t!("network_settings.enabled"));
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw bridge connection line setup content.
|
||||
fn conn_line_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
let r = if View::is_desktop() {
|
||||
View::item_rounding(0, 2, false)
|
||||
} else {
|
||||
View::item_rounding(0, 1, false)
|
||||
};
|
||||
let bg = Colors::fill_lite();
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
let res = ui
|
||||
.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect),
|
||||
|ui| {
|
||||
View::item_button(ui, View::item_rounding(0, 1, true), SCAN, None, || {
|
||||
self.show_qr_scan_bridge_modal(cb);
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(
|
||||
layout_size,
|
||||
Layout::left_to_right(Align::Center),
|
||||
|ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let line_text = bridge.connection_line();
|
||||
View::ellipsize_text(ui, line_text, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let line_desc = t!("transport.conn_line").replace(":", "");
|
||||
let value = format!("{} {}", NOTCHES, line_desc);
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
self.bridge_conn_line_edit = bridge.connection_line();
|
||||
// Show connection line edit modal.
|
||||
let title = bridge.protocol_name();
|
||||
Modal::new(BRIDGE_CONN_LINE_EDIT_MODAL)
|
||||
.position(ModalPosition::Center)
|
||||
.title(title)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Show bridge connection line QR code scanner.
|
||||
fn show_qr_scan_bridge_modal(&mut self, cb: &dyn PlatformCallbacks) {
|
||||
self.bridge_qr_scan_content = Some(CameraScanContent::default());
|
||||
// Show QR code scan modal.
|
||||
Modal::new(SCAN_BRIDGE_CONN_LINE_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("scan_qr"))
|
||||
.closeable(false)
|
||||
.show();
|
||||
cb.start_camera();
|
||||
}
|
||||
|
||||
/// Draw bridge connection line input [`Modal`] content.
|
||||
fn bridge_conn_line_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut TorSettingsContent| {
|
||||
let bridge = TorConfig::get_bridge().unwrap();
|
||||
if bridge.connection_line() != c.bridge_conn_line_edit {
|
||||
TorBridge::save_bridge_conn_line(&bridge, c.bridge_conn_line_edit.clone());
|
||||
c.settings_changed = true;
|
||||
}
|
||||
Modal::close();
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(t!("transport.conn_line"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw connection line text edit.
|
||||
ui.vertical_centered(|ui| {
|
||||
let scroll_id = Id::from(BRIDGE_CONN_LINE_EDIT_MODAL);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(3.0);
|
||||
ScrollArea::both()
|
||||
.id_salt(scroll_id)
|
||||
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
|
||||
.max_height(128.0)
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.add_space(7.0);
|
||||
let input_id = scroll_id.with("_input");
|
||||
egui::TextEdit::multiline(&mut self.bridge_conn_line_edit)
|
||||
.id(input_id)
|
||||
.font(egui::TextStyle::Body)
|
||||
.desired_rows(5)
|
||||
.interactive(false)
|
||||
.desired_width(f32::INFINITY)
|
||||
.show(ui);
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
// Draw paste button.
|
||||
let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste"));
|
||||
View::button(ui, paste_text, Colors::white_or_black(false), || {
|
||||
self.bridge_conn_line_edit = cb.get_string_from_buffer();
|
||||
});
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
// Draw button to scan bridge QR code.
|
||||
let scan_text = format!("{} {}", SCAN, t!("scan"));
|
||||
View::button(ui, scan_text, Colors::white_or_black(false), || {
|
||||
self.show_qr_scan_bridge_modal(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show modal buttons.
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(
|
||||
ui,
|
||||
t!("modal.cancel"),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
// Close modal.
|
||||
Modal::close();
|
||||
},
|
||||
);
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw bridge binary setup content.
|
||||
fn bridge_bin_ui(&mut self, ui: &mut egui::Ui, bridge: &TorBridge, cb: &dyn PlatformCallbacks) {
|
||||
// Draw round background.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(56.0);
|
||||
let r = View::item_rounding(1, 2, false);
|
||||
let bg = Colors::fill_lite();
|
||||
let mut bg_shape = RectShape::new(rect, r, bg, View::item_stroke(), StrokeKind::Outside);
|
||||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||||
|
||||
let res = ui
|
||||
.scope_builder(
|
||||
UiBuilder::new()
|
||||
.sense(Sense::click())
|
||||
.layout(Layout::right_to_left(Align::Center))
|
||||
.max_rect(rect),
|
||||
|ui| {
|
||||
self.bridge_bin_pick_file.ui(ui, cb, |path| {
|
||||
if bridge.binary_path() != path {
|
||||
TorBridge::save_bridge_bin_path(bridge, path);
|
||||
self.settings_changed = true;
|
||||
}
|
||||
});
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(
|
||||
layout_size,
|
||||
Layout::left_to_right(Align::Center),
|
||||
|ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(4.0);
|
||||
let bin_text = bridge.binary_path();
|
||||
View::ellipsize_text(ui, bin_text, 18.0, Colors::title(false));
|
||||
ui.add_space(1.0);
|
||||
let bin_desc = t!("transport.bin_file").replace(":", "");
|
||||
let value = format!("{} {}", TERMINAL, bin_desc);
|
||||
ui.label(RichText::new(value).size(15.0).color(Colors::gray()));
|
||||
ui.add_space(3.0);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.response;
|
||||
let clicked = res.clicked() || res.long_touched();
|
||||
// Setup background and cursor.
|
||||
if res.hovered() {
|
||||
res.on_hover_cursor(CursorIcon::PointingHand);
|
||||
bg_shape.fill = Colors::fill();
|
||||
}
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
// Handle clicks on layout.
|
||||
if clicked {
|
||||
self.bridge_bin_path_edit = bridge.binary_path();
|
||||
// Show binary path edit modal.
|
||||
let title = bridge.protocol_name();
|
||||
Modal::new(BRIDGE_BIN_EDIT_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(title)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw bridge binary input [`Modal`] content.
|
||||
fn bridge_bin_edit_modal_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut TorSettingsContent| {
|
||||
let bridge = TorConfig::get_bridge().unwrap();
|
||||
let exists = fs::exists(&c.bridge_bin_path_edit).unwrap_or_default();
|
||||
if !exists {
|
||||
return;
|
||||
}
|
||||
if bridge.binary_path() != c.bridge_bin_path_edit {
|
||||
TorBridge::save_bridge_bin_path(&bridge, c.bridge_bin_path_edit.clone());
|
||||
c.settings_changed = true;
|
||||
}
|
||||
Modal::close();
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new(t!("transport.bin_file"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Draw bridge text edit.
|
||||
let mut edit = TextEdit::new(Id::from(BRIDGE_BIN_EDIT_MODAL)).paste();
|
||||
edit.ui(ui, &mut self.bridge_bin_path_edit, cb);
|
||||
if edit.enter_pressed {
|
||||
on_save(self);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Show modal buttons.
|
||||
ui.scope(|ui| {
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
ui.columns(2, |columns| {
|
||||
columns[0].vertical_centered_justified(|ui| {
|
||||
View::button(
|
||||
ui,
|
||||
t!("modal.cancel"),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
// Close modal.
|
||||
Modal::close();
|
||||
},
|
||||
);
|
||||
});
|
||||
columns[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.save"), Colors::white_or_black(false), || {
|
||||
on_save(self);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ use crate::gui::views::wallets::wallet::account::WalletAccountContent;
|
||||
use crate::gui::views::wallets::wallet::message::MessageInputContent;
|
||||
use crate::gui::views::wallets::wallet::proof::PaymentProofContent;
|
||||
use crate::gui::views::wallets::wallet::request::{InvoiceRequestContent, SendRequestContent};
|
||||
use crate::gui::views::wallets::wallet::transport::WalletTransportContent;
|
||||
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
|
||||
use crate::gui::views::wallets::wallet::{WalletSettingsContent, WalletTransactionsContent};
|
||||
use crate::gui::views::{Content, Modal, View};
|
||||
@@ -45,8 +44,6 @@ pub struct WalletContent {
|
||||
|
||||
/// Account panel content.
|
||||
pub account_content: WalletAccountContent,
|
||||
/// Transport panel content.
|
||||
pub transport_content: WalletTransportContent,
|
||||
|
||||
/// Invoice request creation [`Modal`] content.
|
||||
invoice_content: Option<InvoiceRequestContent>,
|
||||
@@ -184,11 +181,10 @@ impl WalletContent {
|
||||
}
|
||||
|
||||
// Flag to check if account panel is opened.
|
||||
let top_panel_expanded =
|
||||
self.account_content.can_back() || self.transport_content.can_back();
|
||||
let top_panel_expanded = self.account_content.can_back();
|
||||
|
||||
// Show wallet account content.
|
||||
if !self.transport_content.can_back() && show_account {
|
||||
if show_account {
|
||||
egui::TopBottomPanel::top(Id::from("wallet_account").with(wallet.identifier()))
|
||||
.frame(egui::Frame {
|
||||
inner_margin: Margin {
|
||||
@@ -221,46 +217,6 @@ impl WalletContent {
|
||||
});
|
||||
}
|
||||
|
||||
// Show wallet transport content.
|
||||
if !self.account_content.can_back() && show_account {
|
||||
egui::TopBottomPanel::top(Id::from("wallet_transport").with(wallet.identifier()))
|
||||
.frame(egui::Frame {
|
||||
inner_margin: Margin {
|
||||
left: (View::far_left_inset_margin(ui) + View::content_padding()) as i8,
|
||||
right: (View::get_right_inset() + View::content_padding()) as i8,
|
||||
top: 1.0 as i8,
|
||||
bottom: 1.0 as i8,
|
||||
},
|
||||
fill: if top_panel_expanded {
|
||||
if self.transport_content.qr_address_content.is_some() {
|
||||
Colors::FILL_DEEP
|
||||
} else {
|
||||
Colors::fill_lite()
|
||||
}
|
||||
} else {
|
||||
Colors::TRANSPARENT
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
|
||||
self.transport_content.ui(ui, &wallet, cb);
|
||||
});
|
||||
// Draw content divider lines.
|
||||
let r = {
|
||||
let mut r = rect.clone();
|
||||
r.min.x -= View::content_padding() + View::far_left_inset_margin(ui);
|
||||
r.min.y -= 1.0;
|
||||
r.max.x += View::content_padding() + View::get_right_inset();
|
||||
r
|
||||
};
|
||||
if dual_panel && show_wallets_dual {
|
||||
View::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show tab content.
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame {
|
||||
@@ -328,7 +284,6 @@ impl Default for WalletContent {
|
||||
txs_content: Some(WalletTransactionsContent::new(None)),
|
||||
settings_content: None,
|
||||
account_content: WalletAccountContent::default(),
|
||||
transport_content: WalletTransportContent::default(),
|
||||
invoice_content: None,
|
||||
send_content: None,
|
||||
goblin: crate::gui::views::goblin::GoblinWalletView::default(),
|
||||
@@ -343,10 +298,6 @@ impl WalletContent {
|
||||
t!("scan_qr")
|
||||
} else if self.account_content.show_list {
|
||||
t!("wallets.accounts")
|
||||
} else if self.transport_content.settings_content.is_some() {
|
||||
t!("wallets.transport")
|
||||
} else if self.transport_content.qr_address_content.is_some() {
|
||||
t!("network_mining.address")
|
||||
} else if self.settings_content.is_some() {
|
||||
t!("wallets.settings")
|
||||
} else {
|
||||
@@ -356,9 +307,7 @@ impl WalletContent {
|
||||
|
||||
/// Check if it's possible to go back at navigation stack.
|
||||
pub fn can_back(&self) -> bool {
|
||||
self.goblin.overlay_active()
|
||||
|| self.account_content.can_back()
|
||||
|| self.transport_content.can_back()
|
||||
self.goblin.overlay_active() || self.account_content.can_back()
|
||||
}
|
||||
|
||||
/// Navigate back on navigation stack. Returns true if not consumed.
|
||||
@@ -369,9 +318,6 @@ impl WalletContent {
|
||||
if self.account_content.can_back() {
|
||||
self.account_content.back(cb);
|
||||
return false;
|
||||
} else if self.transport_content.can_back() {
|
||||
self.transport_content.back();
|
||||
return false;
|
||||
}
|
||||
self.goblin.on_back()
|
||||
}
|
||||
|
||||
@@ -27,4 +27,3 @@ mod account;
|
||||
mod message;
|
||||
mod proof;
|
||||
mod request;
|
||||
mod transport;
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
// Copyright 2025 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Align, CornerRadius, Layout, RichText, StrokeKind};
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{
|
||||
CIRCLE_HALF, DOTS_THREE_CIRCLE, PLUGS, PLUGS_CONNECTED, POWER, QR_CODE, SHIELD_CHECKERED,
|
||||
SHIELD_SLASH, WARNING_CIRCLE, WRENCH,
|
||||
};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::wallets::wallet::transport::settings::WalletTransportSettingsContent;
|
||||
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
|
||||
use crate::gui::views::{Modal, QrCodeContent, View};
|
||||
use crate::tor::{Tor, TorConfig};
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Wallet transport panel content.
|
||||
pub struct WalletTransportContent {
|
||||
/// QR code address content.
|
||||
pub qr_address_content: Option<QrCodeContent>,
|
||||
|
||||
/// Settings content.
|
||||
pub settings_content: Option<WalletTransportSettingsContent>,
|
||||
}
|
||||
|
||||
impl WalletContentContainer for WalletTransportContent {
|
||||
fn modal_ids(&self) -> Vec<&'static str> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn modal_ui(&mut self, _: &mut egui::Ui, _: &Wallet, _: &Modal, _: &dyn PlatformCallbacks) {}
|
||||
|
||||
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
if let Some(content) = self.qr_address_content.as_mut() {
|
||||
let dark_theme = AppConfig::dark_theme().unwrap_or(false);
|
||||
// Set light theme for better scanning.
|
||||
AppConfig::set_dark_theme(false);
|
||||
crate::setup_visuals(ui.ctx());
|
||||
// Draw QR code content.
|
||||
ui.add_space(6.0);
|
||||
content.ui(ui, cb);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
self.qr_address_content = None;
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
// Set color theme back.
|
||||
AppConfig::set_dark_theme(dark_theme);
|
||||
crate::setup_visuals(ui.ctx());
|
||||
} else if let Some(content) = self.settings_content.as_mut() {
|
||||
let mut closed = false;
|
||||
content.ui(ui, wallet, cb, || {
|
||||
closed = true;
|
||||
});
|
||||
if closed {
|
||||
self.settings_content = None;
|
||||
}
|
||||
} else {
|
||||
self.tor_header_ui(ui, wallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WalletTransportContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
qr_address_content: None,
|
||||
settings_content: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletTransportContent {
|
||||
/// Check if it's possible to go back at navigation stack.
|
||||
pub fn can_back(&self) -> bool {
|
||||
self.settings_content.is_some() || self.qr_address_content.is_some()
|
||||
}
|
||||
|
||||
/// Navigate back on navigation stack.
|
||||
pub fn back(&mut self) {
|
||||
if let Some(content) = self.settings_content.as_ref() {
|
||||
if content.tor_settings_content.settings_changed {
|
||||
Tor::restart();
|
||||
}
|
||||
self.settings_content = None;
|
||||
} else if self.qr_address_content.is_some() {
|
||||
self.qr_address_content = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw Tor transport header content.
|
||||
fn tor_header_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
let data = wallet.get_data();
|
||||
if data.is_none() {
|
||||
return;
|
||||
}
|
||||
let data = data.unwrap();
|
||||
let addr = wallet.slatepack_address().unwrap();
|
||||
|
||||
// Setup layout size.
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.set_height(78.0);
|
||||
|
||||
// Draw round background.
|
||||
let info = data.info;
|
||||
let awaiting_balance = info.amount_awaiting_confirmation > 0
|
||||
|| info.amount_awaiting_finalization > 0
|
||||
|| info.amount_locked > 0;
|
||||
let rounding = if awaiting_balance {
|
||||
View::item_rounding(1, 3, false)
|
||||
} else {
|
||||
View::item_rounding(1, 2, false)
|
||||
};
|
||||
ui.painter().rect(
|
||||
rect,
|
||||
rounding,
|
||||
Colors::fill(),
|
||||
View::item_stroke(),
|
||||
StrokeKind::Outside,
|
||||
);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Show button to show QR code address.
|
||||
let r = if awaiting_balance {
|
||||
View::item_rounding(1, 3, true)
|
||||
} else {
|
||||
View::item_rounding(1, 2, true)
|
||||
};
|
||||
View::item_button(ui, r, QR_CODE, None, || {
|
||||
self.qr_address_content =
|
||||
Some(QrCodeContent::new(addr.clone(), false).with_max_size(320.0));
|
||||
});
|
||||
|
||||
let service_id = &wallet.identifier();
|
||||
// Draw button to enable/disable Tor listener for current wallet.
|
||||
if wallet.foreign_api_port().is_some() && wallet.secret_key().is_some() {
|
||||
let port = wallet.foreign_api_port().unwrap();
|
||||
let key = wallet.secret_key().unwrap();
|
||||
if !Tor::is_service_starting(service_id) {
|
||||
if !Tor::is_service_running(service_id) {
|
||||
let r = CornerRadius::default();
|
||||
View::item_button(ui, r, POWER, Some(Colors::green()), || {
|
||||
Tor::start_service(port, key.clone(), service_id);
|
||||
});
|
||||
} else {
|
||||
let r = CornerRadius::default();
|
||||
View::item_button(ui, r, POWER, Some(Colors::red()), || {
|
||||
Tor::stop_service(service_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Draw button to show Tor transport settings.
|
||||
let button_rounding = View::item_rounding(1, 3, true);
|
||||
View::item_button(ui, button_rounding, WRENCH, None, || {
|
||||
self.settings_content = Some(WalletTransportSettingsContent::default());
|
||||
});
|
||||
|
||||
let layout_size = ui.available_size();
|
||||
ui.allocate_ui_with_layout(
|
||||
layout_size,
|
||||
Layout::left_to_right(Align::Center),
|
||||
|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(3.0);
|
||||
|
||||
let is_running = Tor::is_service_running(service_id);
|
||||
let has_error = Tor::is_service_failed(service_id);
|
||||
let is_starting = Tor::is_service_starting(service_id);
|
||||
let address_color = if is_running && !is_starting {
|
||||
Colors::green()
|
||||
} else if has_error {
|
||||
Colors::red()
|
||||
} else {
|
||||
Colors::inactive_text()
|
||||
};
|
||||
// Show slatepack address text.
|
||||
View::animate_text(ui, addr.clone(), 17.0, address_color, is_starting);
|
||||
ui.add_space(1.0);
|
||||
|
||||
let (icon, text) = if is_starting {
|
||||
(DOTS_THREE_CIRCLE, t!("transport.connecting"))
|
||||
} else if has_error {
|
||||
(WARNING_CIRCLE, t!("transport.conn_error"))
|
||||
} else if is_running {
|
||||
(PLUGS_CONNECTED, t!("transport.connected"))
|
||||
} else if let Some(_) = TorConfig::get_proxy() {
|
||||
(PLUGS_CONNECTED, t!("app_settings.proxy"))
|
||||
} else {
|
||||
(PLUGS, t!("transport.disconnected"))
|
||||
};
|
||||
let status_text = format!("{} {}", icon, text);
|
||||
// Show connection status text.
|
||||
View::ellipsize_text(ui, status_text, 15.0, Colors::text(false));
|
||||
ui.add_space(1.0);
|
||||
|
||||
let bridges_text = if is_starting || has_error {
|
||||
match TorConfig::get_bridge() {
|
||||
None => {
|
||||
format!(
|
||||
"{} {}",
|
||||
SHIELD_SLASH,
|
||||
t!("transport.bridges_disabled")
|
||||
)
|
||||
}
|
||||
Some(b) => {
|
||||
let name = b.protocol_name().to_uppercase();
|
||||
format!(
|
||||
"{} {}",
|
||||
SHIELD_CHECKERED,
|
||||
t!("transport.bridge_name", "b" = name)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
format!("{} {}", CIRCLE_HALF, t!("transport.tor_network"))
|
||||
};
|
||||
// Show bridge info text.
|
||||
ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray()));
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright 2025 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod content;
|
||||
pub use content::*;
|
||||
|
||||
mod settings;
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright 2025 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::RichText;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::views::settings::TorSettingsContent;
|
||||
use crate::gui::views::types::ContentContainer;
|
||||
use crate::tor::Tor;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Wallet transport settings content.
|
||||
pub struct WalletTransportSettingsContent {
|
||||
/// Tor transport content settings.
|
||||
pub tor_settings_content: TorSettingsContent,
|
||||
}
|
||||
|
||||
impl Default for WalletTransportSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tor_settings_content: TorSettingsContent::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletTransportSettingsContent {
|
||||
/// Draw transport settings content.
|
||||
pub fn ui(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
on_close: impl FnOnce(),
|
||||
) {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
// Show Tor settings.
|
||||
self.tor_settings_content.ui(ui, cb);
|
||||
ui.add_space(4.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(t!("transport.tor_autorun_desc"))
|
||||
.size(17.0)
|
||||
.color(Colors::inactive_text()),
|
||||
);
|
||||
// Show Tor service autorun checkbox.
|
||||
let autorun = wallet.auto_start_tor_listener();
|
||||
View::checkbox(ui, autorun, t!("network.autorun"), || {
|
||||
wallet.update_auto_start_tor_listener(!autorun);
|
||||
});
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
if self.tor_settings_content.settings_changed {
|
||||
Tor::restart();
|
||||
}
|
||||
on_close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
}
|
||||
@@ -444,7 +444,6 @@ impl WalletTransactionsContent {
|
||||
let status = match action {
|
||||
WalletTxAction::Finalizing => t!("wallets.tx_finalizing"),
|
||||
WalletTxAction::Posting => t!("wallets.tx_posting"),
|
||||
WalletTxAction::SendingTor => t!("transport.tor_sending"),
|
||||
_ => t!("wallets.tx_cancelling"),
|
||||
};
|
||||
let icon = if error.is_empty() {
|
||||
@@ -641,15 +640,7 @@ impl WalletTransactionsContent {
|
||||
WalletTxAction::Posting => {
|
||||
wallet.task(WalletTask::Post(tx.data.id));
|
||||
}
|
||||
_ => {
|
||||
if let Some(a) = &tx.receiver {
|
||||
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(a) = &tx.receiver {
|
||||
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ impl WalletTransactionContent {
|
||||
if let Some(proof_content) = self.proof_content.as_mut() {
|
||||
// Draw payment proof sharing content.
|
||||
proof_content.share_ui(ui, tx, cb);
|
||||
} else if tx.proof.is_some() && !tx.sending_tor() && tx.action_error.is_none() {
|
||||
} else if tx.proof.is_some() && tx.action_error.is_none() {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(8.0);
|
||||
let label = format!("{} {}", SEAL_CHECK, t!("wallets.payment_proof"));
|
||||
|
||||
+8
-8
@@ -12,20 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! GRIN price preview, fetched over Tor and cached per currency.
|
||||
//! GRIN price preview, fetched over the Nym mixnet and cached per currency.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::tor::Tor;
|
||||
use crate::nym;
|
||||
|
||||
/// Cache refresh interval (seconds).
|
||||
const REFRESH_SECS: i64 = 300;
|
||||
|
||||
/// Minimum delay between fetch attempts for a currency, so a failing fetch
|
||||
/// (e.g. Tor still bootstrapping) does not respawn a thread every frame.
|
||||
/// (e.g. the mixnet still bootstrapping) does not respawn a thread every frame.
|
||||
const RETRY_SECS: i64 = 30;
|
||||
|
||||
lazy_static! {
|
||||
@@ -59,7 +59,7 @@ pub fn grin_rate(vs: &str) -> Option<f64> {
|
||||
cached.map(|(rate, _)| rate)
|
||||
}
|
||||
|
||||
/// Spawn a background refresh over Tor for one currency (deduplicated per code).
|
||||
/// Spawn a background refresh over the mixnet for one currency (deduped per code).
|
||||
fn trigger_refresh(vs: String) {
|
||||
let t = now();
|
||||
{
|
||||
@@ -90,22 +90,22 @@ fn trigger_refresh(vs: String) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Fetch the GRIN/`vs` rate from CoinGecko over Tor.
|
||||
/// Fetch the GRIN/`vs` rate from CoinGecko over the Nym mixnet.
|
||||
async fn fetch_rate(vs: &str) -> Option<f64> {
|
||||
let url = format!(
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies={}",
|
||||
vs
|
||||
);
|
||||
// CoinGecko rejects requests without a User-Agent (403). A static,
|
||||
// non-identifying UA is fine over Tor.
|
||||
// non-identifying UA is fine over the mixnet.
|
||||
let headers = vec![("User-Agent".to_string(), "goblin-wallet".to_string())];
|
||||
let body = Tor::http_request("GET", url, None, headers).await?;
|
||||
let body = nym::http_request("GET", url, None, headers).await?;
|
||||
let parsed: Option<f64> = serde_json::from_str::<serde_json::Value>(&body)
|
||||
.ok()
|
||||
.and_then(|doc| doc.get("grin")?.get(vs)?.as_f64());
|
||||
if parsed.is_none() {
|
||||
log::warn!(
|
||||
"price: unexpected response from rate API (Tor exit blocked?): {}",
|
||||
"price: unexpected response from rate API (mixnet exit blocked?): {}",
|
||||
body.chars().take(120).collect::<String>()
|
||||
);
|
||||
}
|
||||
|
||||
+4
-3
@@ -38,8 +38,8 @@ mod http;
|
||||
pub mod logger;
|
||||
mod node;
|
||||
pub mod nostr;
|
||||
mod nym;
|
||||
mod settings;
|
||||
mod tor;
|
||||
mod wallet;
|
||||
|
||||
/// Upstream GRIM version the fork is based on (third-party credit).
|
||||
@@ -117,8 +117,9 @@ pub fn start(options: NativeOptions, app_creator: eframe::AppCreator) -> eframe:
|
||||
if AppConfig::autostart_node() {
|
||||
Node::start();
|
||||
}
|
||||
// Pre-warm the Tor client so price/NIP-05/nostr are ready at first use.
|
||||
tor::Tor::warm_up();
|
||||
// Pre-warm the Nym mixnet sidecar so price/NIP-05/nostr are ready at first
|
||||
// use. All of Goblin's outbound traffic egresses through it; nothing clearnet.
|
||||
nym::warm_up();
|
||||
// Launch graphical interface.
|
||||
eframe::run_native("Goblin", options, app_creator)
|
||||
}
|
||||
|
||||
+1
-4
@@ -49,10 +49,7 @@ struct AppFilter;
|
||||
impl Filter for AppFilter {
|
||||
fn filter(&self, record: &Record<'_>) -> Response {
|
||||
if let Some(module_path) = record.module_path() {
|
||||
if module_path.starts_with("grin")
|
||||
|| module_path.starts_with("grim")
|
||||
|| module_path.starts_with("arti")
|
||||
{
|
||||
if module_path.starts_with("grin") || module_path.starts_with("grim") {
|
||||
return Response::Neutral;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ use crate::nostr::protocol;
|
||||
use crate::nostr::relays::MAX_DM_RELAYS;
|
||||
use crate::nostr::types::*;
|
||||
use crate::nostr::{NostrConfig, NostrIdentity, NostrStore};
|
||||
use crate::tor::transport::ArtiWebSocketTransport;
|
||||
use crate::nym::NymWebSocketTransport;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// A peer's published nostr profile (kind-0 metadata), used to confirm a
|
||||
@@ -436,7 +436,7 @@ async fn run_service(svc: Arc<NostrService>, wallet: Wallet) {
|
||||
|
||||
let client = Client::builder()
|
||||
.signer(svc.keys.clone())
|
||||
.websocket_transport(ArtiWebSocketTransport)
|
||||
.websocket_transport(NymWebSocketTransport)
|
||||
.build();
|
||||
for relay in &relays {
|
||||
if let Err(e) = client.add_relay(relay.clone()).await {
|
||||
|
||||
+12
-35
@@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
//! NIP-05 username resolution/verification and goblin.st registration,
|
||||
//! all HTTP routed through the embedded Tor client.
|
||||
//! all HTTP routed through the Nym mixnet (the local SOCKS5 sidecar). Nothing
|
||||
//! here touches clearnet.
|
||||
|
||||
use base64::Engine;
|
||||
use nostr_sdk::{EventBuilder, JsonUtil, Keys, Kind, PublicKey, Tag, TagKind};
|
||||
@@ -21,7 +22,7 @@ use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::nostr::relays::HOME_NIP05_DOMAIN;
|
||||
use crate::tor::Tor;
|
||||
use crate::nym;
|
||||
|
||||
/// Result of resolving a NIP-05 identifier.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -68,22 +69,6 @@ fn is_valid_hostname(d: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// A plain clearnet HTTPS GET (rustls). Used only for the public, read-only
|
||||
/// NIP-05 username lookups: those carry no secret and don't need anonymity,
|
||||
/// and routing them over Tor/the mixnet is exactly what made choosing a name
|
||||
/// feel slow. Returns (status, body).
|
||||
async fn clearnet_get(url: &str) -> Option<(u16, String)> {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("goblin-wallet")
|
||||
.timeout(std::time::Duration::from_secs(8))
|
||||
.build()
|
||||
.ok()?;
|
||||
let resp = client.get(url).send().await.ok()?;
|
||||
let status = resp.status().as_u16();
|
||||
let body = resp.text().await.ok()?;
|
||||
Some((status, body))
|
||||
}
|
||||
|
||||
/// Resolve a NIP-05 identifier (user@domain) to a pubkey + relay hints.
|
||||
pub async fn resolve(name: &str, domain: &str) -> Option<Nip05Resolution> {
|
||||
let url = format!(
|
||||
@@ -91,11 +76,7 @@ pub async fn resolve(name: &str, domain: &str) -> Option<Nip05Resolution> {
|
||||
domain,
|
||||
urlencode(name)
|
||||
);
|
||||
// Fast clearnet lookup first; fall back to Tor if clearnet is blocked.
|
||||
let body = match clearnet_get(&url).await {
|
||||
Some((200, b)) => b,
|
||||
_ => Tor::http_request("GET", url, None, vec![]).await?,
|
||||
};
|
||||
let body = nym::http_request("GET", url, None, vec![]).await?;
|
||||
parse_well_known(&body, name)
|
||||
}
|
||||
|
||||
@@ -143,13 +124,9 @@ pub async fn check_availability(server: &str, name: &str) -> Availability {
|
||||
server.trim_end_matches('/'),
|
||||
urlencode(name)
|
||||
);
|
||||
// Fast clearnet lookup first; fall back to Tor if clearnet is blocked.
|
||||
let body = match clearnet_get(&url).await {
|
||||
Some((200, b)) => b,
|
||||
_ => match Tor::http_request("GET", url, None, vec![]).await {
|
||||
let body = match nym::http_request("GET", url, None, vec![]).await {
|
||||
Some(b) => b,
|
||||
None => return Availability::Unknown,
|
||||
},
|
||||
};
|
||||
let Ok(doc) = serde_json::from_str::<Value>(&body) else {
|
||||
return Availability::Unknown;
|
||||
@@ -213,7 +190,7 @@ pub async fn register(server: &str, name: &str, keys: &Keys) -> RegisterResult {
|
||||
("Authorization".to_string(), auth),
|
||||
("Content-Type".to_string(), "application/json".to_string()),
|
||||
];
|
||||
let Some(resp) = Tor::http_request("POST", url, Some(body), headers).await else {
|
||||
let Some(resp) = nym::http_request("POST", url, Some(body), headers).await else {
|
||||
return RegisterResult::Network;
|
||||
};
|
||||
let Ok(doc) = serde_json::from_str::<Value>(&resp) else {
|
||||
@@ -257,7 +234,7 @@ pub async fn transfer(
|
||||
("Authorization".to_string(), auth),
|
||||
("Content-Type".to_string(), "application/json".to_string()),
|
||||
];
|
||||
let Some(resp) = Tor::http_request("POST", url, Some(body), headers).await else {
|
||||
let Some(resp) = nym::http_request("POST", url, Some(body), headers).await else {
|
||||
return Err("network unavailable".to_string());
|
||||
};
|
||||
if resp.contains("\"transferred\":true") {
|
||||
@@ -284,7 +261,7 @@ pub async fn unregister(server: &str, name: &str, keys: &Keys) -> Result<(), Str
|
||||
return Err("couldn't sign the request".to_string());
|
||||
};
|
||||
let headers = vec![("Authorization".to_string(), auth)];
|
||||
match Tor::http_request("DELETE", url, None, headers).await {
|
||||
match nym::http_request("DELETE", url, None, headers).await {
|
||||
Some(resp) if resp.contains("\"released\":true") => Ok(()),
|
||||
Some(resp) => Err(serde_json::from_str::<serde_json::Value>(&resp)
|
||||
.ok()
|
||||
@@ -314,7 +291,7 @@ pub async fn upload_avatar(
|
||||
"application/octet-stream".to_string(),
|
||||
),
|
||||
];
|
||||
match Tor::http_request_bytes("POST", url, Some(png), headers).await {
|
||||
match nym::http_request_bytes("POST", url, Some(png), headers).await {
|
||||
Some((201, raw)) => serde_json::from_slice::<serde_json::Value>(&raw)
|
||||
.ok()
|
||||
.and_then(|v| v.get("avatar").and_then(|h| h.as_str()).map(String::from))
|
||||
@@ -338,7 +315,7 @@ pub async fn delete_avatar(server: &str, name: &str, keys: &Keys) -> Result<(),
|
||||
return Err("couldn't sign the request".to_string());
|
||||
};
|
||||
let headers = vec![("Authorization".to_string(), auth)];
|
||||
match Tor::http_request_bytes("DELETE", url, None, headers).await {
|
||||
match nym::http_request_bytes("DELETE", url, None, headers).await {
|
||||
Some((200, _)) => Ok(()),
|
||||
Some((code, _)) => Err(format!("server error ({code})")),
|
||||
None => Err("network unreachable".to_string()),
|
||||
@@ -350,7 +327,7 @@ pub async fn delete_avatar(server: &str, name: &str, keys: &Keys) -> Result<(),
|
||||
pub async fn fetch_profile(server: &str, name: &str) -> Option<Option<String>> {
|
||||
let server = server.trim_end_matches('/');
|
||||
let url = format!("{}/api/v1/profile/{}", server, urlencode(name));
|
||||
let (code, raw) = Tor::http_request_bytes("GET", url, None, vec![]).await?;
|
||||
let (code, raw) = nym::http_request_bytes("GET", url, None, vec![]).await?;
|
||||
if code == 404 {
|
||||
return Some(None);
|
||||
}
|
||||
@@ -369,7 +346,7 @@ pub async fn fetch_avatar(server: &str, hash: &str) -> Option<Vec<u8>> {
|
||||
}
|
||||
let server = server.trim_end_matches('/');
|
||||
let url = format!("{}/api/v1/avatar/{}.png", server, hash);
|
||||
let (code, raw) = Tor::http_request_bytes("GET", url, None, vec![]).await?;
|
||||
let (code, raw) = nym::http_request_bytes("GET", url, None, vec![]).await?;
|
||||
if code != 200 || raw.len() > 1_048_576 || !raw.starts_with(&[0x89, b'P', b'N', b'G']) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 The Goblin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Nym mixnet transport. Everything Goblin sends — nostr relay traffic and
|
||||
//! every HTTP request (NIP-05, price, avatars) — is routed through a local
|
||||
//! Nym SOCKS5 client (`nym-socks5-client`) that tunnels over the 5-hop mixnet
|
||||
//! to a network requester. This replaces the embedded Tor (arti) client: the
|
||||
//! mixnet breaks the sender↔receiver timing correlation that Mimblewimble's
|
||||
//! interactive slate exchange otherwise leaks at the network layer, and it
|
||||
//! bootstraps in ~2s rather than Tor's tens of seconds. Nothing goes clearnet.
|
||||
|
||||
pub mod sidecar;
|
||||
pub mod transport;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub use sidecar::warm_up;
|
||||
pub use transport::NymWebSocketTransport;
|
||||
|
||||
/// Local SOCKS5 endpoint exposed by the bundled `nym-socks5-client` sidecar.
|
||||
/// `socks5h` keeps DNS resolution inside the proxy so the destination host is
|
||||
/// never resolved on the clear.
|
||||
pub const SOCKS5_HOST: &str = "127.0.0.1";
|
||||
pub const SOCKS5_PORT: u16 = 1080;
|
||||
|
||||
/// `socks5h://127.0.0.1:1080` proxy URL for reqwest.
|
||||
pub fn proxy_url() -> String {
|
||||
format!("socks5h://{SOCKS5_HOST}:{SOCKS5_PORT}")
|
||||
}
|
||||
|
||||
/// `127.0.0.1:1080` for the raw SOCKS5 TCP dialer (relay websockets).
|
||||
pub fn socks5_addr() -> String {
|
||||
format!("{SOCKS5_HOST}:{SOCKS5_PORT}")
|
||||
}
|
||||
|
||||
/// An HTTP request routed over the Nym mixnet via the local SOCKS5 sidecar.
|
||||
/// Mirrors the old `Tor::http_request_bytes` signature so call sites swap 1:1.
|
||||
/// Returns `(status, body)`.
|
||||
pub async fn http_request_bytes(
|
||||
method: &str,
|
||||
url: String,
|
||||
body: Option<Vec<u8>>,
|
||||
headers: Vec<(String, String)>,
|
||||
) -> Option<(u16, Vec<u8>)> {
|
||||
let proxy = reqwest::Proxy::all(proxy_url()).ok()?;
|
||||
let client = reqwest::Client::builder()
|
||||
.proxy(proxy)
|
||||
.user_agent("goblin-wallet")
|
||||
// The mixnet adds deliberate per-hop delay; allow generous time.
|
||||
.timeout(Duration::from_secs(60))
|
||||
.build()
|
||||
.ok()?;
|
||||
let m = reqwest::Method::from_bytes(method.as_bytes()).ok()?;
|
||||
let mut req = client.request(m, &url);
|
||||
for (k, v) in headers {
|
||||
req = req.header(k, v);
|
||||
}
|
||||
if let Some(b) = body {
|
||||
req = req.body(b);
|
||||
}
|
||||
let resp = req.send().await.ok()?;
|
||||
let code = resp.status().as_u16();
|
||||
let bytes = resp.bytes().await.ok()?.to_vec();
|
||||
Some((code, bytes))
|
||||
}
|
||||
|
||||
/// String-bodied convenience wrapper (mirrors the old `Tor::http_request`).
|
||||
pub async fn http_request(
|
||||
method: &str,
|
||||
url: String,
|
||||
body: Option<String>,
|
||||
headers: Vec<(String, String)>,
|
||||
) -> Option<String> {
|
||||
http_request_bytes(method, url, body.map(|b| b.into_bytes()), headers)
|
||||
.await
|
||||
.map(|(_, raw)| String::from_utf8_lossy(&raw).to_string())
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright 2026 The Goblin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Lifecycle for the bundled `nym-socks5-client` sidecar. Goblin doesn't link
|
||||
//! the Nym SDK (its native-lib graph conflicts with ours — see the project
|
||||
//! notes); instead it ships the standalone SOCKS5 client binary and runs it as
|
||||
//! a child process, exposing the mixnet at `127.0.0.1:1080`. Every relay and
|
||||
//! HTTP request in the app is pointed at that port, so all traffic egresses
|
||||
//! through the 5-hop mixnet to our network requester. Nothing goes clearnet.
|
||||
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use log::{error, info, warn};
|
||||
|
||||
use super::{SOCKS5_HOST, SOCKS5_PORT};
|
||||
|
||||
/// Bundled SOCKS5 client binary name.
|
||||
const BIN_NAME: &str = "nym-socks5-client";
|
||||
|
||||
/// Per-app client id; namespaces the config/keys under the Nym data root.
|
||||
const CLIENT_ID: &str = "goblin";
|
||||
|
||||
/// Network requester (the mixnet exit) Goblin routes through — the SOCKS5
|
||||
/// client's `--provider`. Set this to the always-on requester we run on
|
||||
/// us-ea.st (standard exit policy, which permits wss/443 + HTTPS) once it is
|
||||
/// deployed; until then it is configured at runtime via `GOBLIN_NYM_PROVIDER`.
|
||||
/// Empty means "not configured": the sidecar won't be auto-launched, but an
|
||||
/// already-running SOCKS5 endpoint (a dev sidecar / system service) is reused.
|
||||
pub const NETWORK_REQUESTER: &str = "";
|
||||
|
||||
lazy_static! {
|
||||
/// Handle to the spawned child so it is killed when Goblin exits.
|
||||
static ref CHILD: Mutex<Option<Child>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
/// Pre-warm the mixnet transport in the background so relays / NIP-05 / price
|
||||
/// are ready by first use. Mirrors the old `Tor::warm_up()` seam. If a SOCKS5
|
||||
/// endpoint is already listening (a dev sidecar, or a system-managed service),
|
||||
/// it is reused as-is; otherwise the bundled client is launched.
|
||||
pub fn warm_up() {
|
||||
thread::spawn(|| {
|
||||
if port_open(Duration::from_millis(300)) {
|
||||
info!("nym: reusing SOCKS5 sidecar already listening on {SOCKS5_HOST}:{SOCKS5_PORT}");
|
||||
return;
|
||||
}
|
||||
if let Err(e) = launch() {
|
||||
error!("nym: could not start the SOCKS5 sidecar: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// True when something accepts TCP on the SOCKS5 port.
|
||||
fn port_open(timeout: Duration) -> bool {
|
||||
let addr: SocketAddr = match format!("{SOCKS5_HOST}:{SOCKS5_PORT}").parse() {
|
||||
Ok(a) => a,
|
||||
Err(_) => return false,
|
||||
};
|
||||
TcpStream::connect_timeout(&addr, timeout).is_ok()
|
||||
}
|
||||
|
||||
/// Locate the `nym-socks5-client` binary: an explicit `GOBLIN_NYM_BIN`
|
||||
/// override, then alongside the running executable (how release archives ship
|
||||
/// it), then a bare name resolved against `PATH`.
|
||||
fn binary_path() -> PathBuf {
|
||||
if let Ok(p) = std::env::var("GOBLIN_NYM_BIN") {
|
||||
if !p.is_empty() {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let sibling = dir.join(BIN_NAME);
|
||||
if sibling.exists() {
|
||||
return sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from(BIN_NAME)
|
||||
}
|
||||
|
||||
/// The network requester address to register against (`--provider`).
|
||||
fn provider() -> String {
|
||||
std::env::var("GOBLIN_NYM_PROVIDER")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| NETWORK_REQUESTER.to_string())
|
||||
}
|
||||
|
||||
/// `~/.nym/socks5-clients/<id>/config/config.toml` — present once initialized.
|
||||
fn config_marker() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|h| {
|
||||
h.join(".nym")
|
||||
.join("socks5-clients")
|
||||
.join(CLIENT_ID)
|
||||
.join("config")
|
||||
.join("config.toml")
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract (if bundled), initialize (once), then spawn the SOCKS5 client and
|
||||
/// block until its port is accepting connections.
|
||||
fn launch() -> std::io::Result<()> {
|
||||
if provider().is_empty() {
|
||||
warn!(
|
||||
"nym: no network requester configured (set GOBLIN_NYM_PROVIDER or bake \
|
||||
NETWORK_REQUESTER); not launching a sidecar"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let bin = binary_path();
|
||||
ensure_initialized(&bin);
|
||||
|
||||
info!("nym: launching SOCKS5 sidecar ({})", bin.display());
|
||||
let child = Command::new(&bin)
|
||||
.arg("run")
|
||||
.arg("--id")
|
||||
.arg(CLIENT_ID)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
*CHILD.lock().unwrap() = Some(child);
|
||||
|
||||
// The mixnet bootstraps in ~2s; give it generous headroom on cold start.
|
||||
let deadline = Instant::now() + Duration::from_secs(60);
|
||||
while Instant::now() < deadline {
|
||||
if port_open(Duration::from_millis(500)) {
|
||||
info!("nym: SOCKS5 sidecar ready on {SOCKS5_HOST}:{SOCKS5_PORT}");
|
||||
return Ok(());
|
||||
}
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
warn!("nym: SOCKS5 sidecar did not open its port within 60s");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run `init` once, when the client has no config yet. `init` selects a gateway
|
||||
/// and writes keys; it needs the network but no funds (zero-value mode).
|
||||
fn ensure_initialized(bin: &PathBuf) {
|
||||
let needs_init = config_marker().map(|p| !p.exists()).unwrap_or(true);
|
||||
if !needs_init {
|
||||
return;
|
||||
}
|
||||
info!("nym: initializing SOCKS5 client '{CLIENT_ID}'");
|
||||
let res = Command::new(bin)
|
||||
.arg("init")
|
||||
.arg("--id")
|
||||
.arg(CLIENT_ID)
|
||||
.arg("--provider")
|
||||
.arg(provider())
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
match res {
|
||||
Ok(s) if s.success() => info!("nym: SOCKS5 client initialized"),
|
||||
Ok(s) => warn!("nym: SOCKS5 client init exited with {s}"),
|
||||
Err(e) => error!("nym: SOCKS5 client init failed to run: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the sidecar if Goblin spawned one (best-effort; no-op when reusing an
|
||||
/// externally-managed client).
|
||||
#[allow(dead_code)]
|
||||
pub fn shutdown() {
|
||||
if let Some(mut child) = CHILD.lock().unwrap().take() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! WebSocket transport for the Nostr relay pool routed through the embedded
|
||||
//! Tor (arti) client that Goblin already runs for slatepack exchange.
|
||||
//! Every connection uses a fresh isolated circuit.
|
||||
//! WebSocket transport for the Nostr relay pool routed through the local
|
||||
//! `nym-socks5-client` sidecar, so every relay connection traverses the 5-hop
|
||||
//! Nym mixnet. This replaces the arti/Tor transport: instead of dialing a Tor
|
||||
//! data stream we open a SOCKS5 connection to `127.0.0.1:1080`, ask the proxy
|
||||
//! to reach the relay host (`socks5h`-style: the proxy does the DNS, so the
|
||||
//! destination is never resolved on the clear), then run the TLS + websocket
|
||||
//! handshake over that tunnel. Nothing goes clearnet.
|
||||
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
@@ -27,31 +31,30 @@ use nostr_relay_pool::transport::error::TransportError;
|
||||
use nostr_relay_pool::transport::websocket::{WebSocketSink, WebSocketStream, WebSocketTransport};
|
||||
use nostr_sdk::Url;
|
||||
use nostr_sdk::util::BoxedFuture;
|
||||
use tokio_socks::tcp::Socks5Stream;
|
||||
use tokio_tungstenite::tungstenite::Message as TgMessage;
|
||||
|
||||
use crate::tor::Tor;
|
||||
|
||||
/// Error type for transport failures outside the websocket layer.
|
||||
#[derive(Debug)]
|
||||
struct ArtiTransportError(String);
|
||||
struct NymTransportError(String);
|
||||
|
||||
impl fmt::Display for ArtiTransportError {
|
||||
impl fmt::Display for NymTransportError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ArtiTransportError {}
|
||||
impl std::error::Error for NymTransportError {}
|
||||
|
||||
fn terr(msg: impl Into<String>) -> TransportError {
|
||||
TransportError::backend(ArtiTransportError(msg.into()))
|
||||
TransportError::backend(NymTransportError(msg.into()))
|
||||
}
|
||||
|
||||
/// Nostr websocket transport over the embedded arti Tor client.
|
||||
/// Nostr websocket transport over the local Nym SOCKS5 sidecar.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ArtiWebSocketTransport;
|
||||
pub struct NymWebSocketTransport;
|
||||
|
||||
impl WebSocketTransport for ArtiWebSocketTransport {
|
||||
impl WebSocketTransport for NymWebSocketTransport {
|
||||
fn support_ping(&self) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -72,19 +75,17 @@ impl WebSocketTransport for ArtiWebSocketTransport {
|
||||
_ => 443,
|
||||
});
|
||||
|
||||
// Get an isolated Tor client, launching the embedded client if needed.
|
||||
let client = tokio::task::spawn_blocking(Tor::isolated_client_blocking)
|
||||
// Dial the relay host through the local Nym SOCKS5 client. The proxy
|
||||
// resolves the host inside the mixnet, so no clearnet DNS leak.
|
||||
let stream = tokio::time::timeout(
|
||||
timeout,
|
||||
Socks5Stream::connect(crate::nym::socks5_addr().as_str(), (host.as_str(), port)),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| terr(format!("tor client task failed: {e}")))?
|
||||
.ok_or_else(|| terr("tor client is not available"))?;
|
||||
.map_err(|_| terr("nym socks5 connect timeout"))?
|
||||
.map_err(|e| terr(format!("nym socks5 connect failed: {e}")))?;
|
||||
|
||||
// Open a Tor data stream to the relay host.
|
||||
let stream = tokio::time::timeout(timeout, client.connect((host.as_str(), port)))
|
||||
.await
|
||||
.map_err(|_| terr("tor connect timeout"))?
|
||||
.map_err(|e| terr(format!("tor connect failed: {e}")))?;
|
||||
|
||||
// Perform TLS (for wss) + websocket handshake over the Tor stream.
|
||||
// Perform TLS (for wss) + websocket handshake over the mixnet stream.
|
||||
let (ws, _response) = tokio::time::timeout(
|
||||
timeout,
|
||||
tokio_tungstenite::client_async_tls(url.as_str(), stream),
|
||||
@@ -95,7 +96,7 @@ impl WebSocketTransport for ArtiWebSocketTransport {
|
||||
|
||||
let (tx, rx) = ws.split();
|
||||
|
||||
let sink: WebSocketSink = Box::new(ArtiSink(tx)) as WebSocketSink;
|
||||
let sink: WebSocketSink = Box::new(NymSink(tx)) as WebSocketSink;
|
||||
let stream: WebSocketStream = Box::pin(rx.filter_map(|msg| async move {
|
||||
match msg {
|
||||
Ok(tg) => tg_to_message(tg).map(Ok),
|
||||
@@ -122,9 +123,9 @@ fn tg_to_message(msg: TgMessage) -> Option<Message> {
|
||||
}
|
||||
|
||||
/// Sink adapter converting pool messages into tungstenite messages.
|
||||
struct ArtiSink<S>(S);
|
||||
struct NymSink<S>(S);
|
||||
|
||||
impl<S> Sink<Message> for ArtiSink<S>
|
||||
impl<S> Sink<Message> for NymSink<S>
|
||||
where
|
||||
S: Sink<TgMessage, Error = tokio_tungstenite::tungstenite::Error> + Send + Unpin,
|
||||
{
|
||||
@@ -25,7 +25,6 @@ use std::sync::Arc;
|
||||
|
||||
use crate::node::NodeConfig;
|
||||
use crate::settings::AppConfig;
|
||||
use crate::tor::TorConfig;
|
||||
use crate::wallet::ConnectionsConfig;
|
||||
|
||||
lazy_static! {
|
||||
@@ -41,8 +40,6 @@ pub struct Settings {
|
||||
node_config: Arc<RwLock<NodeConfig>>,
|
||||
/// Wallet connections configuration.
|
||||
conn_config: Arc<RwLock<ConnectionsConfig>>,
|
||||
/// Tor server configuration.
|
||||
tor_config: Arc<RwLock<TorConfig>>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -59,11 +56,6 @@ impl Settings {
|
||||
let app_config_path = Settings::config_path(AppConfig::FILE_NAME, None);
|
||||
let app_config = Self::init_config::<AppConfig>(app_config_path);
|
||||
|
||||
// Initialize tor config.
|
||||
let tor_config_path = Settings::config_path(TorConfig::FILE_NAME, None);
|
||||
let mut tor_config = Self::init_config::<TorConfig>(tor_config_path);
|
||||
tor_config.migrate();
|
||||
|
||||
// Setup chain type.
|
||||
let chain_type = &app_config.chain_type;
|
||||
if !global::GLOBAL_CHAIN_TYPE.is_init() {
|
||||
@@ -77,7 +69,6 @@ impl Settings {
|
||||
node_config: Arc::new(RwLock::new(NodeConfig::for_chain_type(chain_type))),
|
||||
conn_config: Arc::new(RwLock::new(ConnectionsConfig::for_chain_type(chain_type))),
|
||||
app_config: Arc::new(RwLock::new(app_config)),
|
||||
tor_config: Arc::new(RwLock::new(tor_config)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,16 +114,6 @@ impl Settings {
|
||||
SETTINGS_STATE.conn_config.write()
|
||||
}
|
||||
|
||||
/// Get tor server configuration to read values.
|
||||
pub fn tor_config_to_read() -> RwLockReadGuard<'static, TorConfig> {
|
||||
SETTINGS_STATE.tor_config.read()
|
||||
}
|
||||
|
||||
/// Get tor server configuration to update values.
|
||||
pub fn tor_config_to_update() -> RwLockWriteGuard<'static, TorConfig> {
|
||||
SETTINGS_STATE.tor_config.write()
|
||||
}
|
||||
|
||||
/// Get base directory path for configuration.
|
||||
pub fn base_path(sub_dir: Option<String>) -> PathBuf {
|
||||
// Check if dir exists.
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::Settings;
|
||||
use crate::tor::{TorBridge, TorProxy};
|
||||
|
||||
const TOR_CONFIG_VERSION: i32 = 1;
|
||||
|
||||
/// Tor configuration.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct TorConfig {
|
||||
/// Proxy for tor connections.
|
||||
proxy: Option<TorProxy>,
|
||||
/// SOCKS5 proxy type.
|
||||
proxy_socks5: TorProxy,
|
||||
/// HTTP proxy type.
|
||||
proxy_http: TorProxy,
|
||||
|
||||
/// Selected bridge type.
|
||||
bridge: Option<TorBridge>,
|
||||
/// Webtunnel bridge type.
|
||||
webtunnel: TorBridge,
|
||||
/// Obfs4 bridge type.
|
||||
obfs4: TorBridge,
|
||||
/// Snowflake bridge type.
|
||||
snowflake: TorBridge,
|
||||
|
||||
/// Config version.
|
||||
ver: Option<i32>,
|
||||
}
|
||||
|
||||
impl Default for TorConfig {
|
||||
fn default() -> Self {
|
||||
let webtunnel = Self::default_webtunnel_bridge();
|
||||
Self {
|
||||
proxy: None,
|
||||
proxy_socks5: TorProxy::HTTP(TorProxy::DEFAULT_SOCKS5_URL.to_string()),
|
||||
proxy_http: TorProxy::HTTP(TorProxy::DEFAULT_HTTP_URL.to_string()),
|
||||
bridge: Some(webtunnel.clone()),
|
||||
webtunnel,
|
||||
obfs4: TorBridge::Obfs4(
|
||||
TorBridge::DEFAULT_OBFS4_BIN_PATH.to_string(),
|
||||
TorBridge::DEFAULT_OBFS4_CONN_LINE.to_string(),
|
||||
),
|
||||
snowflake: TorBridge::Snowflake(
|
||||
TorBridge::DEFAULT_SNOWFLAKE_BIN_PATH.to_string(),
|
||||
TorBridge::DEFAULT_SNOWFLAKE_CONN_LINE.to_string(),
|
||||
),
|
||||
ver: Some(TOR_CONFIG_VERSION),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TorConfig {
|
||||
/// Tor configuration file name.
|
||||
pub const FILE_NAME: &'static str = "tor.toml";
|
||||
|
||||
/// Directory for Tor data files.
|
||||
const DIR_NAME: &'static str = "tor";
|
||||
|
||||
/// Subdirectory name for Tor state.
|
||||
const STATE_SUB_DIR: &'static str = "state";
|
||||
/// Subdirectory name for Tor cache.
|
||||
const CACHE_SUB_DIR: &'static str = "cache";
|
||||
/// Subdirectory name for Tor keystore.
|
||||
const KEYSTORE_DIR: &'static str = "keystore";
|
||||
|
||||
/// Webtunnel binary name.
|
||||
pub const WEBTUNNEL_BIN: &'static str = "webtunnel";
|
||||
/// Webtunnel Android binary name.
|
||||
pub const WEBTUNNEL_ANDROID_BIN: &'static str = "libwebtunnel.so";
|
||||
|
||||
/// Save application configuration to the file.
|
||||
pub fn save(&self) {
|
||||
Settings::write_to_file(self, Settings::config_path(Self::FILE_NAME, None));
|
||||
}
|
||||
|
||||
/// Get base Tor directory path.
|
||||
fn base_path() -> PathBuf {
|
||||
Settings::base_path(Some(Self::DIR_NAME.to_string()))
|
||||
}
|
||||
|
||||
/// Get path from subdirectory name.
|
||||
fn sub_dir_path(name: &str) -> String {
|
||||
let mut base = Self::base_path();
|
||||
base.push(name);
|
||||
base.to_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
/// Get Tor state directory path.
|
||||
pub fn state_path() -> String {
|
||||
Self::sub_dir_path(Self::STATE_SUB_DIR)
|
||||
}
|
||||
|
||||
/// Get Tor cache directory path.
|
||||
pub fn cache_path() -> String {
|
||||
Self::sub_dir_path(Self::CACHE_SUB_DIR)
|
||||
}
|
||||
|
||||
/// Get Tor keystore directory path.
|
||||
pub fn keystore_path() -> String {
|
||||
let mut base = PathBuf::from(Self::state_path());
|
||||
base.push(Self::KEYSTORE_DIR);
|
||||
base.to_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
/// Get default webtunnel bridge.
|
||||
pub fn default_webtunnel_bridge() -> TorBridge {
|
||||
TorBridge::Webtunnel(
|
||||
if egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android {
|
||||
"".to_string()
|
||||
} else {
|
||||
TorConfig::webtunnel_path()
|
||||
},
|
||||
serde_json::to_string(&TorBridge::DEFAULT_WEBTUNNEL_CONN_LINES)
|
||||
.unwrap_or(TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Webtunnel binary path.
|
||||
pub fn webtunnel_path() -> String {
|
||||
let os = egui::os::OperatingSystem::from_target_os();
|
||||
if os == egui::os::OperatingSystem::Android {
|
||||
let base = std::env::var("NATIVE_LIBS_DIR").unwrap_or_default();
|
||||
format!("{}/{}", base, Self::WEBTUNNEL_ANDROID_BIN)
|
||||
} else {
|
||||
let mut base = Self::base_path();
|
||||
base.push(Self::WEBTUNNEL_BIN);
|
||||
base.to_str().unwrap().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Save Tor bridge.
|
||||
pub fn save_bridge(bridge: Option<TorBridge>) {
|
||||
let mut w_tor_config = Settings::tor_config_to_update();
|
||||
w_tor_config.bridge = bridge.clone();
|
||||
if bridge.is_some() {
|
||||
let bridge = bridge.unwrap();
|
||||
match &bridge {
|
||||
TorBridge::Webtunnel(_, _) => w_tor_config.webtunnel = bridge,
|
||||
TorBridge::Obfs4(_, _) => w_tor_config.obfs4 = bridge,
|
||||
TorBridge::Snowflake(_, _) => w_tor_config.snowflake = bridge,
|
||||
}
|
||||
}
|
||||
w_tor_config.save();
|
||||
}
|
||||
|
||||
/// Get current Tor bridge if enabled.
|
||||
pub fn get_bridge() -> Option<TorBridge> {
|
||||
let r_config = Settings::tor_config_to_read();
|
||||
r_config.bridge.clone()
|
||||
}
|
||||
|
||||
/// Get saved Webtunnel bridge.
|
||||
pub fn get_webtunnel() -> TorBridge {
|
||||
let r_config = Settings::tor_config_to_read();
|
||||
r_config.webtunnel.clone()
|
||||
}
|
||||
|
||||
/// Get saved Obfs4 bridge.
|
||||
pub fn get_obfs4() -> TorBridge {
|
||||
let r_config = Settings::tor_config_to_read();
|
||||
r_config.obfs4.clone()
|
||||
}
|
||||
|
||||
/// Get saved Snowflake bridge.
|
||||
pub fn get_snowflake() -> TorBridge {
|
||||
let r_config = Settings::tor_config_to_read();
|
||||
r_config.snowflake.clone()
|
||||
}
|
||||
|
||||
/// Save proxy for Tor connections.
|
||||
pub fn save_proxy(proxy: Option<TorProxy>) {
|
||||
let mut w_config = Settings::tor_config_to_update();
|
||||
w_config.proxy = proxy.clone();
|
||||
if let Some(p) = proxy {
|
||||
match p {
|
||||
TorProxy::SOCKS5(_) => w_config.proxy_socks5 = p,
|
||||
TorProxy::HTTP(_) => w_config.proxy_http = p,
|
||||
}
|
||||
}
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Get used proxy for Tor connections.
|
||||
pub fn get_proxy() -> Option<TorProxy> {
|
||||
let r_config = Settings::tor_config_to_read();
|
||||
r_config.proxy.clone()
|
||||
}
|
||||
|
||||
/// Get saved SOCKS5 proxy.
|
||||
pub fn get_socks5_proxy() -> TorProxy {
|
||||
let r_config = Settings::tor_config_to_read();
|
||||
r_config.proxy_socks5.clone()
|
||||
}
|
||||
|
||||
/// Get saved HTTP proxy.
|
||||
pub fn get_http_proxy() -> TorProxy {
|
||||
let r_config = Settings::tor_config_to_read();
|
||||
r_config.proxy_http.clone()
|
||||
}
|
||||
|
||||
/// Check config version to migrate if needed.
|
||||
pub fn migrate(&mut self) {
|
||||
match self.ver {
|
||||
None => {
|
||||
// Migrate to 1st version.
|
||||
self.bridge = Some(TorConfig::default_webtunnel_bridge());
|
||||
self.ver = Some(1);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
self.save();
|
||||
}
|
||||
}
|
||||
-258
@@ -1,258 +0,0 @@
|
||||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::future::Future;
|
||||
use std::io::Error;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use arti_client::{DataStream, IntoTorAddr, TorClient};
|
||||
use hyper_tor::client::connect::{Connected, Connection};
|
||||
use hyper_tor::http::Uri;
|
||||
use hyper_tor::http::uri::Scheme;
|
||||
use hyper_tor::service::Service;
|
||||
use pin_project::pin_project;
|
||||
use thiserror::Error;
|
||||
use tls_api::TlsConnector as TlsConn; // This is different from tor_rtcompat::TlsConnector
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tor_config::deps::educe::Educe;
|
||||
use tor_rtcompat::Runtime;
|
||||
|
||||
/// Error making or using http connection
|
||||
///
|
||||
/// This error ends up being passed to hyper and bundled up into a [`hyper::Error`]
|
||||
#[derive(Error, Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConnectionError {
|
||||
/// Unsupported URI scheme
|
||||
#[error("unsupported URI scheme in {uri:?}")]
|
||||
UnsupportedUriScheme {
|
||||
/// URI
|
||||
uri: Uri,
|
||||
},
|
||||
|
||||
/// Missing hostname
|
||||
#[error("Missing hostname in {uri:?}")]
|
||||
MissingHostname {
|
||||
/// URI
|
||||
uri: Uri,
|
||||
},
|
||||
|
||||
/// Tor connection failed
|
||||
#[error("Tor connection failed")]
|
||||
Arti(#[from] arti_client::Error),
|
||||
|
||||
/// TLS connection failed
|
||||
#[error("TLS connection failed")]
|
||||
TLS(#[source] Arc<anyhow::Error>),
|
||||
}
|
||||
|
||||
/// We implement this for form's sake
|
||||
impl tor_error::HasKind for ConnectionError {
|
||||
#[rustfmt::skip]
|
||||
fn kind(&self) -> tor_error::ErrorKind {
|
||||
use ConnectionError as CE;
|
||||
use tor_error::ErrorKind as EK;
|
||||
match self {
|
||||
CE::UnsupportedUriScheme{..} => EK::NotImplemented,
|
||||
CE::MissingHostname{..} => EK::BadApiUsage,
|
||||
CE::Arti(e) => e.kind(),
|
||||
CE::TLS(_) => EK::RemoteProtocolViolation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **Main entrypoint**: `hyper` connector to make HTTP\[S] connections via Tor, using Arti.
|
||||
///
|
||||
/// An `ArtiHttpConnector` combines an Arti Tor client, and a TLS implementation,
|
||||
/// in a form that can be provided to hyper
|
||||
/// (e.g. to [`hyper::client::Builder`]'s `build` method)
|
||||
/// so that hyper can speak HTTP and HTTPS to origin servers via Tor.
|
||||
///
|
||||
/// TC is the TLS to used *across* Tor to connect to the origin server.
|
||||
/// For example, it could be a [`tls_api_native_tls::TlsConnector`].
|
||||
/// This is a different Rust type to the TLS used *by* Tor to connect to relays etc.
|
||||
/// It might even be a different underlying TLS implementation
|
||||
/// (although that is usually not a particularly good idea).
|
||||
#[derive(Educe)]
|
||||
#[educe(Clone)] // #[derive(Debug)] infers an unwanted bound TC: Clone
|
||||
pub struct ArtiHttpConnector<R: Runtime, TC: TlsConn> {
|
||||
/// The client
|
||||
client: TorClient<R>,
|
||||
|
||||
/// TLS for using across Tor.
|
||||
tls_conn: Arc<TC>,
|
||||
}
|
||||
|
||||
// #[derive(Clone)] infers a TC: Clone bound
|
||||
|
||||
impl<R: Runtime, TC: TlsConn> ArtiHttpConnector<R, TC> {
|
||||
/// Make a new `ArtiHttpConnector` using an Arti `TorClient` object.
|
||||
pub fn new(client: TorClient<R>, tls_conn: TC) -> Self {
|
||||
let tls_conn = tls_conn.into();
|
||||
Self { client, tls_conn }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type that makes an Arti `DataStream` implement necessary traits to be used as
|
||||
/// a `hyper` connection object (mainly `Connection`).
|
||||
///
|
||||
/// This might represent a bare HTTP connection across Tor,
|
||||
/// or it might represent an HTTPS connection through Tor to an origin server,
|
||||
/// `TC::TlsStream` as the TLS layer.
|
||||
///
|
||||
/// An `ArtiHttpConnection` is constructed by hyper's use of the [`ArtiHttpConnector`]
|
||||
/// implementation of [`hyper::service::Service`],
|
||||
/// and then used by hyper as the transport for hyper's HTTP implementation.
|
||||
#[pin_project]
|
||||
pub struct ArtiHttpConnection<TC: TlsConn> {
|
||||
/// The stream
|
||||
#[pin]
|
||||
inner: MaybeHttpsStream<TC>,
|
||||
}
|
||||
|
||||
/// The actual stream; might be TLS, might not
|
||||
#[pin_project(project = MaybeHttpsStreamProj)]
|
||||
enum MaybeHttpsStream<TC: TlsConn> {
|
||||
/// http
|
||||
Http(Pin<Box<DataStream>>), // Tc:TlsStream is generally boxed; box this one too
|
||||
|
||||
/// https
|
||||
Https(#[pin] TC::TlsStream),
|
||||
}
|
||||
|
||||
impl<TC: TlsConn> Connection for ArtiHttpConnection<TC> {
|
||||
fn connected(&self) -> Connected {
|
||||
Connected::new()
|
||||
}
|
||||
}
|
||||
|
||||
// These trait implementations just defer to the inner `DataStream`; the wrapper type is just
|
||||
// there to implement the `Connection` trait.
|
||||
impl<TC: TlsConn> AsyncRead for ArtiHttpConnection<TC> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_read(cx, buf),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<TC: TlsConn> AsyncWrite for ArtiHttpConnection<TC> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_write(cx, buf),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_flush(cx),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_shutdown(cx),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
/// Are we doing TLS?
|
||||
enum UseTls {
|
||||
/// No
|
||||
Bare,
|
||||
|
||||
/// Yes
|
||||
Tls,
|
||||
}
|
||||
|
||||
/// Convert uri to http\[s\] host and port, and whether to do tls
|
||||
fn uri_to_host_port_tls(uri: Uri) -> Result<(String, u16, UseTls), ConnectionError> {
|
||||
let use_tls = {
|
||||
// Scheme doesn't derive PartialEq so can't be matched on
|
||||
let scheme = uri.scheme();
|
||||
if scheme == Some(&Scheme::HTTP) {
|
||||
UseTls::Bare
|
||||
} else if scheme == Some(&Scheme::HTTPS) {
|
||||
UseTls::Tls
|
||||
} else {
|
||||
return Err(ConnectionError::UnsupportedUriScheme { uri });
|
||||
}
|
||||
};
|
||||
let host = match uri.host() {
|
||||
Some(h) => h,
|
||||
_ => return Err(ConnectionError::MissingHostname { uri }),
|
||||
};
|
||||
let port = uri.port().map(|x| x.as_u16()).unwrap_or(match use_tls {
|
||||
UseTls::Tls => 443,
|
||||
UseTls::Bare => 80,
|
||||
});
|
||||
|
||||
Ok((host.to_owned(), port, use_tls))
|
||||
}
|
||||
|
||||
impl<R: Runtime, TC: TlsConn> Service<Uri> for ArtiHttpConnector<R, TC> {
|
||||
type Response = ArtiHttpConnection<TC>;
|
||||
type Error = ConnectionError;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Uri) -> Self::Future {
|
||||
// `TorClient` objects can be cloned cheaply (the cloned objects refer to the same
|
||||
// underlying handles required to make Tor connections internally).
|
||||
// We use this to avoid the returned future having to borrow `self`.
|
||||
let client = self.client.clone();
|
||||
let tls_conn = self.tls_conn.clone();
|
||||
Box::pin(async move {
|
||||
// Extract the host and port to connect to from the URI.
|
||||
let (host, port, use_tls) = uri_to_host_port_tls(req)?;
|
||||
// Initiate a new Tor connection, producing a `DataStream` if successful.
|
||||
let addr = (&host as &str, port)
|
||||
.into_tor_addr()
|
||||
.map_err(arti_client::Error::from)?;
|
||||
let ds = client.connect(addr).await?;
|
||||
|
||||
let inner = match use_tls {
|
||||
UseTls::Tls => {
|
||||
let conn = tls_conn
|
||||
.connect_impl_tls_stream(&host, ds)
|
||||
.await
|
||||
.map_err(|e| ConnectionError::TLS(e.into()))?;
|
||||
MaybeHttpsStream::Https(conn)
|
||||
}
|
||||
UseTls::Bare => MaybeHttpsStream::Http(Box::new(ds).into()),
|
||||
};
|
||||
|
||||
Ok(ArtiHttpConnection { inner })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod config;
|
||||
pub use config::TorConfig;
|
||||
|
||||
mod tor;
|
||||
pub use tor::Tor;
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
pub mod transport;
|
||||
|
||||
mod http;
|
||||
-976
@@ -1,976 +0,0 @@
|
||||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use arti_client::config::pt::TransportConfigBuilder;
|
||||
use arti_client::config::{CfgPath, TorClientConfigBuilder};
|
||||
use arti_client::{TorClient, TorClientConfig};
|
||||
use bytes::Bytes;
|
||||
use curve25519_dalek::digest::Digest;
|
||||
use ed25519_dalek::hazmat::ExpandedSecretKey;
|
||||
use fs_mistrust::Mistrust;
|
||||
use grin_util::secp::SecretKey;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use lazy_static::lazy_static;
|
||||
use log::error;
|
||||
use parking_lot::RwLock;
|
||||
use safelog::DisplayRedacted;
|
||||
use sha2::Sha512;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::{fs, thread};
|
||||
use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder};
|
||||
use tls_api_native_tls::TlsConnector;
|
||||
use tor_hscrypto::pk::{HsIdKey, HsIdKeypair};
|
||||
use tor_hsrproxy::OnionServiceReverseProxy;
|
||||
use tor_hsrproxy::config::{
|
||||
Encapsulation, ProxyAction, ProxyConfigBuilder, ProxyPattern, ProxyRule, TargetAddr,
|
||||
};
|
||||
use tor_hsservice::config::OnionServiceConfigBuilder;
|
||||
use tor_hsservice::{
|
||||
HsIdKeypairSpecifier, HsIdPublicKeySpecifier, HsNickname, RunningOnionService,
|
||||
};
|
||||
use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector};
|
||||
use tor_llcrypto::pk::ed25519::ExpandedKeypair;
|
||||
use tor_rtcompat::SpawnExt;
|
||||
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
|
||||
|
||||
use crate::http::HttpClient;
|
||||
use crate::tor::http::ArtiHttpConnector;
|
||||
use crate::tor::{TorBridge, TorConfig, TorProxy};
|
||||
|
||||
/// Hard ceiling on an HTTP response body read over Tor from the untrusted
|
||||
/// goblin.st server. NIP-05 JSON and profiles are a few KB; avatars are capped
|
||||
/// at 1 MiB by the caller. 2 MiB leaves headroom while bounding memory.
|
||||
const MAX_RESPONSE_BYTES: usize = 2 * 1024 * 1024;
|
||||
|
||||
lazy_static! {
|
||||
/// Static thread-aware state of Tor to be updated from separate thread.
|
||||
static ref TOR_STATE: Arc<Tor> = Arc::new(Tor::default());
|
||||
}
|
||||
|
||||
/// Tor client to use as SOCKS proxy for requests and to launch Onion services.
|
||||
pub struct Tor {
|
||||
runtime: TokioNativeTlsRuntime,
|
||||
/// Tor client and config.
|
||||
client_config: Arc<RwLock<Option<(TorClient<TokioNativeTlsRuntime>, TorClientConfig)>>>,
|
||||
/// Flag to check if client is launching.
|
||||
client_launching: Arc<AtomicBool>,
|
||||
|
||||
/// Mapping of running Onion services identifiers to proxy.
|
||||
run: Arc<
|
||||
RwLock<
|
||||
BTreeMap<
|
||||
String,
|
||||
(
|
||||
u16,
|
||||
SecretKey,
|
||||
Arc<RunningOnionService>,
|
||||
Arc<OnionServiceReverseProxy>,
|
||||
),
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
/// Mapping of starting Onion services identifiers.
|
||||
start: Arc<RwLock<BTreeMap<String, (u16, SecretKey)>>>,
|
||||
/// Failed Onion services identifiers.
|
||||
fail: Arc<RwLock<BTreeSet<String>>>,
|
||||
/// Checking Onion services identifiers.
|
||||
check: Arc<RwLock<BTreeSet<String>>>,
|
||||
}
|
||||
|
||||
impl Default for Tor {
|
||||
fn default() -> Self {
|
||||
// Extract the bundled webtunnel bridge client when missing or empty
|
||||
// (an empty file means the app was built without Go — see build.rs).
|
||||
let wt_path = TorConfig::webtunnel_path();
|
||||
let wt_missing = fs::metadata(&wt_path).map(|m| m.len() == 0).unwrap_or(true);
|
||||
if wt_missing {
|
||||
let webtunnel = include_bytes!(concat!(env!("OUT_DIR"), "/tor/webtunnel"));
|
||||
if webtunnel.is_empty() {
|
||||
error!(
|
||||
"Tor: bundled webtunnel client is empty (built without Go?), \
|
||||
webtunnel bridges will not work"
|
||||
);
|
||||
} else {
|
||||
if let Some(parent) = std::path::Path::new(&wt_path).parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
match fs::write(&wt_path, webtunnel) {
|
||||
Ok(_) => {
|
||||
// The pluggable transport must be executable.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ =
|
||||
fs::set_permissions(&wt_path, fs::Permissions::from_mode(0o755));
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Tor: webtunnel client extraction failed: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create the arti runtime on a clean thread: creation calls block_on,
|
||||
// which panics (and poisons this lazy static) when the first TOR_STATE
|
||||
// access happens on a thread already inside a tokio runtime context.
|
||||
let runtime = std::thread::Builder::new()
|
||||
.name("tor-runtime-init".to_string())
|
||||
.spawn(TokioNativeTlsRuntime::create)
|
||||
.expect("failed to spawn tor runtime init thread")
|
||||
.join()
|
||||
.expect("tor runtime init thread panicked")
|
||||
.unwrap();
|
||||
Self {
|
||||
runtime,
|
||||
client_config: Arc::new(RwLock::new(None)),
|
||||
client_launching: Arc::new(AtomicBool::new(false)),
|
||||
run: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
start: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
fail: Arc::new(RwLock::new(BTreeSet::new())),
|
||||
check: Arc::new(RwLock::new(BTreeSet::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tor {
|
||||
/// Create Tor configuration returning unused bridges (exclude more than 2 to avoid stuck).
|
||||
fn build_config(
|
||||
bridges: Option<Vec<TorBridge>>,
|
||||
) -> (TorClientConfig, Vec<TorBridge>, Vec<TorBridge>) {
|
||||
let mut builder = TorClientConfigBuilder::from_directories(
|
||||
TorConfig::state_path(),
|
||||
TorConfig::cache_path(),
|
||||
);
|
||||
// Build bridges.
|
||||
let mut bridges = bridges.unwrap_or(vec![]);
|
||||
let max_two_bridges = if bridges.len() > 2 {
|
||||
let two_bridges = bridges.iter().take(2).cloned().collect::<Vec<TorBridge>>();
|
||||
bridges = bridges
|
||||
.iter()
|
||||
.filter(|b| !two_bridges.contains(b))
|
||||
.cloned()
|
||||
.collect::<Vec<TorBridge>>();
|
||||
two_bridges
|
||||
} else {
|
||||
bridges.clone()
|
||||
};
|
||||
for b in max_two_bridges.clone() {
|
||||
Self::build_bridge(&mut builder, b);
|
||||
}
|
||||
builder.address_filter().allow_onion_addrs(true);
|
||||
// Create config.
|
||||
let config = builder.build().unwrap();
|
||||
(config, bridges, max_two_bridges)
|
||||
}
|
||||
|
||||
/// Build bootstrapped client from provided config.
|
||||
fn build_client_bootstrap(config: TorClientConfig) -> Option<TorClient<TokioNativeTlsRuntime>> {
|
||||
let client_res = TorClient::with_runtime(TOR_STATE.runtime.clone())
|
||||
.config(config.clone())
|
||||
.create_unbootstrapped();
|
||||
if client_res.is_err() {
|
||||
return None;
|
||||
}
|
||||
let client = client_res.unwrap();
|
||||
let bootstrapping = Arc::new(AtomicBool::new(true));
|
||||
let bootstrap_success = Arc::new(AtomicBool::new(false));
|
||||
let bootstrapping_t = bootstrapping.clone();
|
||||
let bootstrap_success_t = bootstrap_success.clone();
|
||||
let c = client.clone();
|
||||
client
|
||||
.runtime()
|
||||
.spawn(async move {
|
||||
let task = c.bootstrap();
|
||||
// Bootstrap client with 120s timeout: a cold-cache directory
|
||||
// consensus over a webtunnel bridge regularly needs more than
|
||||
// 60s, and timing it out throws the progress away.
|
||||
if tokio::time::timeout(Duration::from_millis(120000), task)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
bootstrap_success_t.store(true, Ordering::Relaxed);
|
||||
}
|
||||
bootstrapping_t.store(false, Ordering::Relaxed);
|
||||
})
|
||||
.unwrap();
|
||||
// Wait client to finish bootstrap.
|
||||
while bootstrapping.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
if bootstrap_success.load(Ordering::Relaxed) {
|
||||
Some(client)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch Tor client.
|
||||
fn launch() {
|
||||
// Wait client to finish launch and exit on success.
|
||||
while TOR_STATE.client_launching.load(Ordering::Relaxed) {
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
{
|
||||
if TOR_STATE.client_config.read().is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Cleanup onion service keys only. State and cache are KEPT: the cache
|
||||
// holds the directory consensus, and wiping it forced a full re-download
|
||||
// over the (slow) bridge on every app start — minutes of "connecting…"
|
||||
// where a warm cache bootstraps in seconds.
|
||||
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
|
||||
TOR_STATE.client_launching.store(true, Ordering::Relaxed);
|
||||
// Get initial bridges.
|
||||
let initial_bridges = if let Some(b) = TorConfig::get_bridge() {
|
||||
let lines_parse = serde_json::from_str::<Vec<String>>(&b.connection_line());
|
||||
let bridges = lines_parse
|
||||
.unwrap_or_else(|_| b.connection_line().lines().map(|l| l.to_string()).collect())
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let mut bridge = b.clone();
|
||||
bridge.update_conn_line(l.clone());
|
||||
bridge
|
||||
})
|
||||
.collect::<Vec<TorBridge>>();
|
||||
Some(bridges)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Bootstrap client in the loop, trying different bridges. Lead with
|
||||
// the FIRST configured bridge alone: the maintained default
|
||||
// (wt.gri.mw) reliably bootstraps, whereas bundling it with a
|
||||
// second, possibly-rotted public bridge lets arti fixate on the
|
||||
// dead one and burn the whole bootstrap timeout. The rest stay as
|
||||
// fallback, tried in pairs only if the primary is unreachable.
|
||||
let mut default_attempt = false;
|
||||
let mut config_bridges = match &initial_bridges {
|
||||
Some(bridges) if bridges.len() > 1 => {
|
||||
let (config, _, used) = Self::build_config(Some(vec![bridges[0].clone()]));
|
||||
(config, bridges[1..].to_vec(), used)
|
||||
}
|
||||
_ => Self::build_config(initial_bridges),
|
||||
};
|
||||
loop {
|
||||
let (config, unused_bridges, used_bridges) = config_bridges.clone();
|
||||
let client = Self::build_client_bootstrap(config.clone());
|
||||
if let Some(c) = client {
|
||||
// Update bridges order.
|
||||
if let Some(b) = TorConfig::get_bridge() {
|
||||
let lines_parse = serde_json::from_str::<Vec<String>>(&b.connection_line());
|
||||
match lines_parse {
|
||||
Ok(mut lines) => {
|
||||
lines.sort_by_key(|l| {
|
||||
let mut bridge = b.clone();
|
||||
bridge.update_conn_line(l.clone());
|
||||
!used_bridges.contains(&bridge)
|
||||
});
|
||||
let lines_str = serde_json::to_string(&lines).unwrap_or_else(|_| {
|
||||
TorConfig::default_webtunnel_bridge().connection_line()
|
||||
});
|
||||
TorBridge::save_bridge_conn_line(&b, lines_str);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
TOR_STATE.client_config.write().replace((c, config.clone()));
|
||||
break;
|
||||
} else {
|
||||
// Keep state and cache between attempts so directory consensus
|
||||
// progress accumulates instead of restarting from zero.
|
||||
// Check unused bridges to check another part.
|
||||
if !unused_bridges.is_empty() {
|
||||
config_bridges = Self::build_config(Some(unused_bridges));
|
||||
continue;
|
||||
}
|
||||
if !default_attempt {
|
||||
default_attempt = true;
|
||||
// Launch client with default Webtunnel bridges if failed.
|
||||
let add_bridges = TorBridge::DEFAULT_WEBTUNNEL_CONN_LINES
|
||||
.iter()
|
||||
.map(|b| TorBridge::Webtunnel(TorConfig::webtunnel_path(), b.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
config_bridges = Self::build_config(Some(add_bridges));
|
||||
continue;
|
||||
} else if TorConfig::get_bridge().is_some() {
|
||||
// Launch without bridges if all attempts failed.
|
||||
let (config, _, _) = Self::build_config(None);
|
||||
let client = Self::build_client_bootstrap(config.clone());
|
||||
if let Some(c) = client {
|
||||
TOR_STATE.client_config.write().replace((c, config));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
TOR_STATE.client_launching.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Send post request using Tor.
|
||||
pub async fn post(body: String, url: String) -> Option<String> {
|
||||
if let Some(proxy) = TorConfig::get_proxy() {
|
||||
let req: hyper::Request<Full<Bytes>> = hyper::Request::builder()
|
||||
.method(hyper::Method::POST)
|
||||
.uri(url)
|
||||
.body(Full::from(body))
|
||||
.unwrap();
|
||||
let res = match proxy {
|
||||
TorProxy::SOCKS5(url) => HttpClient::send_socks_proxy(url, req).await,
|
||||
TorProxy::HTTP(url) => HttpClient::send_http_proxy(url, req).await,
|
||||
};
|
||||
match res {
|
||||
Ok(res) => match res.into_body().collect().await {
|
||||
Ok(r) => {
|
||||
let body = r.to_bytes().into();
|
||||
match String::from_utf8(body) {
|
||||
Ok(r) => Some(r),
|
||||
Err(e) => {
|
||||
error!("Tor: POST with proxy, response to string error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Tor: POST with proxy, response parse error: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Tor: POST failed with proxy: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(b) = TorConfig::get_bridge() {
|
||||
if !fs::exists(b.binary_path()).unwrap() {
|
||||
error!("Tor: bridge binary not exists");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// Bind the client once: a concurrent Tor restart/close could clear
|
||||
// the config between an is_none() check and an unwrap (TOCTOU panic).
|
||||
let Some((client_cfg, _)) = Self::client_config() else {
|
||||
error!("Tor: client not launched");
|
||||
return None;
|
||||
};
|
||||
// Create http tor-powered client to post data.
|
||||
let client = client_cfg.isolated_client();
|
||||
let tls_conn = match TlsConnector::builder().and_then(|b| b.build()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Tor: TLS connector build failed: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let conn = ArtiHttpConnector::new(client, tls_conn);
|
||||
let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn);
|
||||
// Create request.
|
||||
let req = hyper_tor::Request::builder()
|
||||
.method(hyper_tor::Method::POST)
|
||||
.uri(url)
|
||||
.body(hyper_tor::Body::from(body))
|
||||
.unwrap();
|
||||
// Send request.
|
||||
let mut resp = None;
|
||||
match http.request(req).await {
|
||||
Ok(r) => match hyper_tor::body::to_bytes(r).await {
|
||||
Ok(raw) => resp = Some(String::from_utf8_lossy(&raw).to_string()),
|
||||
Err(e) => {
|
||||
error!("Tor: POST response parse error: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Tor: POST failed: {}", e);
|
||||
}
|
||||
}
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
fn client_config() -> Option<(TorClient<TokioNativeTlsRuntime>, TorClientConfig)> {
|
||||
let r_client_config = TOR_STATE.client_config.read();
|
||||
r_client_config.clone()
|
||||
}
|
||||
|
||||
/// Pre-warm the Tor client in the background so it is already bootstrapped
|
||||
/// by the time the first consumer (price feed, NIP-05, nostr relays) needs
|
||||
/// it — otherwise nothing launches it until a wallet finishes opening.
|
||||
pub fn warm_up() {
|
||||
thread::spawn(|| {
|
||||
let _ = Self::isolated_client_blocking();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get an isolated Tor client for outgoing connections (e.g. Nostr relays),
|
||||
/// launching the embedded client first when needed. Blocking: bootstrap can
|
||||
/// take up to a minute, call from a blocking-friendly context.
|
||||
pub fn isolated_client_blocking() -> Option<TorClient<TokioNativeTlsRuntime>> {
|
||||
if Self::client_config().is_none() {
|
||||
Self::launch();
|
||||
}
|
||||
Self::client_config().map(|(c, _)| c.isolated_client())
|
||||
}
|
||||
|
||||
/// Perform an HTTP request over the embedded Tor client with optional
|
||||
/// headers, returning the response body. Used for NIP-05 and price APIs.
|
||||
/// Retries with a fresh isolated circuit: single-circuit connect failures
|
||||
/// right after bootstrap are common and transient, and fresh circuits
|
||||
/// fail for tens of seconds while arti refreshes its directory consensus
|
||||
/// over the bridge (existing circuits keep working) — back off long
|
||||
/// enough to ride a refresh out.
|
||||
pub async fn http_request(
|
||||
method: &str,
|
||||
url: String,
|
||||
body: Option<String>,
|
||||
headers: Vec<(String, String)>,
|
||||
) -> Option<String> {
|
||||
Self::http_request_bytes(method, url, body.map(|b| b.into_bytes()), headers)
|
||||
.await
|
||||
.map(|(_, raw)| String::from_utf8_lossy(&raw).to_string())
|
||||
}
|
||||
|
||||
/// Bytes-level variant with the response status code — avatar uploads
|
||||
/// and downloads need binary bodies, and a 404/413 must be
|
||||
/// distinguishable from image bytes.
|
||||
pub async fn http_request_bytes(
|
||||
method: &str,
|
||||
url: String,
|
||||
body: Option<Vec<u8>>,
|
||||
headers: Vec<(String, String)>,
|
||||
) -> Option<(u16, Vec<u8>)> {
|
||||
for attempt in 0..5 {
|
||||
if attempt > 0 {
|
||||
tokio::time::sleep(Duration::from_secs(1 << (attempt - 1))).await;
|
||||
}
|
||||
let resp =
|
||||
Self::http_request_once(method, url.clone(), body.clone(), headers.clone()).await;
|
||||
if resp.is_some() {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn http_request_once(
|
||||
method: &str,
|
||||
url: String,
|
||||
body: Option<Vec<u8>>,
|
||||
headers: Vec<(String, String)>,
|
||||
) -> Option<(u16, Vec<u8>)> {
|
||||
// Bind once to avoid a TOCTOU panic if Tor restarts mid-request.
|
||||
let Some((client_cfg, _)) = Self::client_config() else {
|
||||
error!("Tor: client not launched");
|
||||
return None;
|
||||
};
|
||||
let client = client_cfg.isolated_client();
|
||||
let tls_conn = match TlsConnector::builder().and_then(|b| b.build()) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Tor: TLS connector build failed: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let conn = ArtiHttpConnector::new(client, tls_conn);
|
||||
let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn);
|
||||
let mut builder = hyper_tor::Request::builder()
|
||||
.method(match method {
|
||||
"POST" => hyper_tor::Method::POST,
|
||||
"DELETE" => hyper_tor::Method::DELETE,
|
||||
_ => hyper_tor::Method::GET,
|
||||
})
|
||||
.uri(url);
|
||||
for (k, v) in headers {
|
||||
builder = builder.header(k.as_str(), v.as_str());
|
||||
}
|
||||
let req = match builder.body(hyper_tor::Body::from(body.unwrap_or_default())) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Tor: http request build error: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match http.request(req).await {
|
||||
Ok(r) => {
|
||||
let status = r.status().as_u16();
|
||||
match Self::read_body_capped(r.into_body(), MAX_RESPONSE_BYTES).await {
|
||||
Ok(raw) => Some((status, raw)),
|
||||
Err(e) => {
|
||||
error!("Tor: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Tor: http request failed: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a response body, aborting once it exceeds `cap` bytes. goblin.st is
|
||||
/// untrusted (see nip05.rs): a malicious or breached server must not be able
|
||||
/// to stream an unbounded body into memory and OOM the wallet. Streaming the
|
||||
/// frames means a lying `Content-Length` (or none at all) can't get past the
|
||||
/// cap by buffering everything up front.
|
||||
async fn read_body_capped(body: hyper_tor::Body, cap: usize) -> Result<Vec<u8>, String> {
|
||||
use futures::StreamExt;
|
||||
let mut body = body;
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
while let Some(chunk) = body.next().await {
|
||||
let chunk = chunk.map_err(|e| format!("http response read error: {e}"))?;
|
||||
if out.len() + chunk.len() > cap {
|
||||
return Err(format!("http response exceeds {cap} byte cap"));
|
||||
}
|
||||
out.extend_from_slice(&chunk);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Check if Onion service is starting.
|
||||
pub fn is_service_starting(id: &String) -> bool {
|
||||
let r_services = TOR_STATE.start.read();
|
||||
r_services.contains_key(id)
|
||||
}
|
||||
|
||||
/// Check if Onion service is running.
|
||||
pub fn is_service_running(id: &String) -> bool {
|
||||
let r_services = TOR_STATE.run.read();
|
||||
r_services.contains_key(id)
|
||||
}
|
||||
|
||||
/// Check if Onion service failed on start.
|
||||
pub fn is_service_failed(id: &String) -> bool {
|
||||
let r_services = TOR_STATE.fail.read();
|
||||
r_services.contains(id)
|
||||
}
|
||||
|
||||
/// Check if Onion service is checking.
|
||||
pub fn is_service_checking(id: &String) -> bool {
|
||||
let r_services = TOR_STATE.check.read();
|
||||
r_services.contains(id)
|
||||
}
|
||||
|
||||
/// Restart Tor client at separate thread.
|
||||
pub fn restart() {
|
||||
thread::spawn(|| {
|
||||
// Exit if client was not launched.
|
||||
if Self::client_config().is_none() {
|
||||
return;
|
||||
}
|
||||
Self::restart_services();
|
||||
});
|
||||
}
|
||||
|
||||
/// Restart running Onion services.
|
||||
fn restart_services() {
|
||||
// Stop all services saving keys to relaunch.
|
||||
let service_ids = {
|
||||
let r_services = TOR_STATE.run.read().clone();
|
||||
r_services
|
||||
.keys()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
let mut services: BTreeMap<String, (u16, SecretKey)> = TOR_STATE.start.read().clone();
|
||||
for id in service_ids.clone() {
|
||||
if let Some(res) = Self::stop_service(&id) {
|
||||
services.insert(id, res);
|
||||
}
|
||||
}
|
||||
// Put stopped services to start.
|
||||
{
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
*w_services = services.clone();
|
||||
}
|
||||
// Cleanup onion service keys (services re-save them on relaunch). State
|
||||
// and cache are kept so the restart reuses the directory consensus.
|
||||
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
|
||||
{
|
||||
let mut w_client = TOR_STATE.client_config.write();
|
||||
*w_client = None;
|
||||
}
|
||||
// Relaunch client.
|
||||
Self::launch();
|
||||
// Save failed services if client was not created.
|
||||
if Self::client_config().is_none() {
|
||||
for id in service_ids {
|
||||
TOR_STATE.start.write().remove(&id);
|
||||
TOR_STATE.fail.write().insert(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Start services.
|
||||
for id in services.keys() {
|
||||
let (port, key) = services.get(id).unwrap();
|
||||
Self::start_service(port.clone(), key.clone(), &id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop running Onion service returning port and key.
|
||||
pub fn stop_service(id: &String) -> Option<(u16, SecretKey)> {
|
||||
let mut port_key = None;
|
||||
{
|
||||
// Remove service from checking.
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
w_services.remove(id);
|
||||
}
|
||||
// Remove service from starting.
|
||||
{
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
if let Some((port, key)) = w_services.remove(id) {
|
||||
port_key = Some((port, key));
|
||||
}
|
||||
}
|
||||
// Remove service from running.
|
||||
{
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
if let Some((port, key, svc, proxy)) = w_services.remove(id) {
|
||||
proxy.shutdown();
|
||||
drop(proxy);
|
||||
drop(svc);
|
||||
port_key = Some((port, key));
|
||||
}
|
||||
}
|
||||
// Remove client when no running services left.
|
||||
if TOR_STATE.start.read().is_empty() && TOR_STATE.run.read().is_empty() {
|
||||
let mut w_client = TOR_STATE.client_config.write();
|
||||
*w_client = None;
|
||||
// Clear state.
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
|
||||
}
|
||||
port_key
|
||||
}
|
||||
|
||||
/// Start Onion service from listening local port and [`SecretKey`].
|
||||
pub fn start_service(port: u16, key: SecretKey, id: &String) {
|
||||
// Check if service is already running.
|
||||
if Self::is_service_running(id) {
|
||||
return;
|
||||
}
|
||||
let service_id = id.clone();
|
||||
thread::spawn(move || {
|
||||
{
|
||||
// Save starting service.
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.insert(service_id.clone(), (port, key.clone()));
|
||||
// Remove service from failed.
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.remove(&service_id);
|
||||
}
|
||||
|
||||
let on_error = |service_id: String| {
|
||||
// Remove service from starting.
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.remove(&service_id);
|
||||
// Save failed service.
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.insert(service_id);
|
||||
};
|
||||
|
||||
// Check bridge binary existence and permissions.
|
||||
if let Some(bridge) = TorConfig::get_bridge() {
|
||||
if !fs::exists(bridge.binary_path()).unwrap() {
|
||||
on_error(service_id);
|
||||
return;
|
||||
}
|
||||
// Add execute permission for Unix.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(bridge.binary_path()).unwrap().permissions();
|
||||
let mode = perms.mode() | 0o100;
|
||||
perms.set_mode(mode);
|
||||
fs::set_permissions(bridge.binary_path(), perms).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
// Launch client if not exists.
|
||||
Self::launch();
|
||||
let client_config = Self::client_config();
|
||||
if client_config.is_none() {
|
||||
on_error(service_id);
|
||||
return;
|
||||
}
|
||||
let (client, config) = client_config.unwrap();
|
||||
client
|
||||
.runtime()
|
||||
.spawn(async move {
|
||||
// Add service key to keystore.
|
||||
let hs = HsNickname::new(service_id.clone()).unwrap();
|
||||
if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs) {
|
||||
on_error(service_id);
|
||||
return;
|
||||
}
|
||||
// Launch Onion service.
|
||||
let service_config = OnionServiceConfigBuilder::default()
|
||||
.nickname(hs.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
let client_config = Self::client_config();
|
||||
if client_config.is_none() {
|
||||
on_error(service_id.clone());
|
||||
}
|
||||
let c = client_config.unwrap().0.isolated_client();
|
||||
if let Ok(res) = c.launch_onion_service(service_config) {
|
||||
if let Some((service, request)) = res {
|
||||
// Launch service proxy.
|
||||
let addr = SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), port);
|
||||
let run = Self::run_service_proxy(c, addr, request, hs.clone());
|
||||
let proxy = tokio::spawn(run).await.unwrap();
|
||||
let onion_addr = service.onion_address();
|
||||
// Save running service.
|
||||
{
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
let id = service_id.clone();
|
||||
w_services.insert(id, (port, key.clone(), service, proxy));
|
||||
}
|
||||
// Remove service from starting.
|
||||
{
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.remove(&service_id);
|
||||
}
|
||||
// Check service availability.
|
||||
let addr = onion_addr.unwrap().display_unredacted().to_string();
|
||||
let url = format!("http://{}/", addr);
|
||||
if !Self::is_service_checking(&service_id) {
|
||||
Self::check_service(service_id, url)
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
on_error(service_id);
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
/// Check is service is running on check.
|
||||
fn check_running(service_id: &String) -> bool {
|
||||
let running = Tor::is_service_running(service_id) && Self::client_config().is_some();
|
||||
if !running {
|
||||
// Remove service from checking.
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
w_services.remove(service_id);
|
||||
}
|
||||
running
|
||||
}
|
||||
|
||||
/// Check service availability.
|
||||
fn check_service(service_id: String, url: String) {
|
||||
{
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
w_services.insert(service_id.clone());
|
||||
}
|
||||
thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
const MAX_ERRORS: i32 = 16;
|
||||
let mut errors_count = 0;
|
||||
// Wait 10 seconds.
|
||||
tokio::time::sleep(Duration::from_millis(10000)).await;
|
||||
loop {
|
||||
if !Self::check_running(&service_id) {
|
||||
break;
|
||||
}
|
||||
let duration = {
|
||||
// Send request.
|
||||
let tls_conn = TlsConnector::builder().unwrap().build().unwrap();
|
||||
let client_config = Self::client_config();
|
||||
if client_config.is_none() {
|
||||
return;
|
||||
}
|
||||
let client = client_config.unwrap().0.isolated_client();
|
||||
let conn = ArtiHttpConnector::new(client, tls_conn);
|
||||
let http =
|
||||
hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn);
|
||||
let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap();
|
||||
let check = http.get(uri.clone());
|
||||
// Setup error callback.
|
||||
let mut on_error = |service_id: &String| -> bool {
|
||||
if !Self::check_running(service_id) {
|
||||
return true;
|
||||
}
|
||||
// Restart service after maximum amount of errors.
|
||||
errors_count += 1;
|
||||
let max_errors = errors_count >= MAX_ERRORS;
|
||||
if max_errors {
|
||||
{
|
||||
// Remove service from checking.
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
w_services.remove(service_id);
|
||||
}
|
||||
// Restart services.
|
||||
Self::restart();
|
||||
}
|
||||
max_errors
|
||||
};
|
||||
// Check with timeout of 30s.
|
||||
match tokio::time::timeout(Duration::from_millis(30000), check).await {
|
||||
Ok(resp) => {
|
||||
match resp {
|
||||
Ok(_) => {
|
||||
if !Self::check_running(&service_id) {
|
||||
break;
|
||||
}
|
||||
errors_count = 0;
|
||||
// Check again after 60s.
|
||||
Duration::from_millis(60000)
|
||||
}
|
||||
Err(e) => {
|
||||
if on_error(&service_id) {
|
||||
break;
|
||||
}
|
||||
error!(
|
||||
"Tor check failed: {} for {}, errors: {}/{}",
|
||||
e, service_id, errors_count, MAX_ERRORS
|
||||
);
|
||||
// Check again after 5s.
|
||||
Duration::from_millis(5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if on_error(&service_id) {
|
||||
break;
|
||||
}
|
||||
error!(
|
||||
"Tor check for {} timeout, errors: {}/{}",
|
||||
&service_id, errors_count, MAX_ERRORS
|
||||
);
|
||||
// Check again after 5s.
|
||||
Duration::from_millis(5000)
|
||||
}
|
||||
}
|
||||
};
|
||||
// Wait to check service again.
|
||||
tokio::time::sleep(duration).await;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Launch Onion service proxy.
|
||||
async fn run_service_proxy<S>(
|
||||
client: TorClient<TokioNativeTlsRuntime>,
|
||||
addr: SocketAddr,
|
||||
request: S,
|
||||
nickname: HsNickname,
|
||||
) -> Arc<OnionServiceReverseProxy>
|
||||
where
|
||||
S: futures::Stream<Item = tor_hsservice::RendRequest> + Unpin + Send + 'static,
|
||||
{
|
||||
let id = nickname.to_string();
|
||||
let runtime = client.runtime().clone();
|
||||
|
||||
// Setup proxy to forward request from Tor address to local address.
|
||||
let proxy_rule = ProxyRule::new(
|
||||
ProxyPattern::one_port(80).unwrap(),
|
||||
ProxyAction::Forward(Encapsulation::Simple, TargetAddr::Inet(addr)),
|
||||
);
|
||||
let mut proxy_cfg_builder = ProxyConfigBuilder::default();
|
||||
proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]);
|
||||
let proxy = OnionServiceReverseProxy::new(proxy_cfg_builder.build().unwrap());
|
||||
|
||||
// Remove service from failed.
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.remove(&id);
|
||||
|
||||
// Start proxy for launched service.
|
||||
let p = proxy.clone();
|
||||
client
|
||||
.runtime()
|
||||
.spawn(async move {
|
||||
match p.handle_requests(runtime, nickname.clone(), request).await {
|
||||
Ok(()) => {
|
||||
// Remove service from running.
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
w_services.remove(&id);
|
||||
}
|
||||
Err(_) => {
|
||||
if Self::is_service_running(&id) {
|
||||
// Remove service from running.
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
w_services.remove(&id);
|
||||
// Save failed service.
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
proxy
|
||||
}
|
||||
|
||||
/// Save Onion service key to keystore.
|
||||
fn add_service_key(
|
||||
mistrust: &Mistrust,
|
||||
key: &SecretKey,
|
||||
hs_nickname: &HsNickname,
|
||||
) -> tor_keymgr::Result<()> {
|
||||
let arti_store =
|
||||
ArtiNativeKeystore::from_path_and_mistrust(TorConfig::keystore_path(), mistrust)?;
|
||||
|
||||
let key_manager = KeyMgrBuilder::default()
|
||||
.primary_store(Box::new(arti_store))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let expanded_sk =
|
||||
ExpandedSecretKey::from_bytes(Sha512::default().chain_update(key).finalize().as_ref());
|
||||
|
||||
let mut sk_bytes = [0_u8; 64];
|
||||
sk_bytes[0..32].copy_from_slice(&expanded_sk.scalar.to_bytes());
|
||||
sk_bytes[32..64].copy_from_slice(&expanded_sk.hash_prefix);
|
||||
let expanded_kp = ExpandedKeypair::from_secret_key_bytes(sk_bytes).unwrap();
|
||||
|
||||
key_manager.insert(
|
||||
HsIdKey::from(expanded_kp.public().clone()),
|
||||
&HsIdPublicKeySpecifier::new(hs_nickname.clone()),
|
||||
KeystoreSelector::Primary,
|
||||
true,
|
||||
)?;
|
||||
|
||||
key_manager.insert(
|
||||
HsIdKeypair::from(expanded_kp),
|
||||
&HsIdKeypairSpecifier::new(hs_nickname.clone()),
|
||||
KeystoreSelector::Primary,
|
||||
true,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_bridge(builder: &mut TorClientConfigBuilder, bridge: TorBridge) {
|
||||
let bridge_line = format!("Bridge {}", bridge.connection_line());
|
||||
if let Ok(bridge) = bridge_line.parse() {
|
||||
builder.bridges().bridges().push(bridge);
|
||||
}
|
||||
|
||||
// Now configure bridge transport. (Requires the "pt-client" feature)
|
||||
let mut transport = TransportConfigBuilder::default();
|
||||
transport
|
||||
.protocols(vec![bridge.protocol_name().parse().unwrap()])
|
||||
// Specify either the name or the absolute path of pluggable transport client binary,
|
||||
// this may differ from system to system.
|
||||
.path(CfgPath::new(bridge.binary_path().into()))
|
||||
.run_on_startup(true);
|
||||
builder.bridges().transports().push(transport);
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::tor::TorConfig;
|
||||
use egui::os;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Tor connection proxy type.
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub enum TorProxy {
|
||||
/// SOCKS5 proxy URL.
|
||||
SOCKS5(String),
|
||||
/// HTTP proxy URL.
|
||||
HTTP(String),
|
||||
}
|
||||
|
||||
impl TorProxy {
|
||||
/// Default SOCKS5 proxy URL.
|
||||
pub const DEFAULT_SOCKS5_URL: &'static str = "socks5://127.0.0.1:9050";
|
||||
/// Default HTTP proxy URL.
|
||||
pub const DEFAULT_HTTP_URL: &'static str = "http://127.0.0.1:9050";
|
||||
|
||||
/// Get proxy URL.
|
||||
pub fn url(&self) -> String {
|
||||
match self {
|
||||
TorProxy::SOCKS5(url) => url.into(),
|
||||
TorProxy::HTTP(url) => url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tor network bridge type.
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub enum TorBridge {
|
||||
/// Obfs4 bridge with binary path and connection line.
|
||||
Webtunnel(String, String),
|
||||
/// Obfs4 bridge with binary path and connection line.
|
||||
Obfs4(String, String),
|
||||
/// Snowflake bridge with binary path and connection line.
|
||||
Snowflake(String, String),
|
||||
}
|
||||
|
||||
impl TorBridge {
|
||||
/// Default Obfs4 protocol proxy client binary path.
|
||||
pub const DEFAULT_OBFS4_BIN_PATH: &'static str = "/usr/bin/obfs4proxy";
|
||||
/// Default Snowflake protocol client binary path.
|
||||
pub const DEFAULT_SNOWFLAKE_BIN_PATH: &'static str = "/usr/bin/snowflake-client";
|
||||
|
||||
/// Default webtunnel protocol connection line.
|
||||
pub const DEFAULT_WEBTUNNEL_CONN_LINE: &'static str = "webtunnel [2001:db8:beb:5884:ffcc:bfe3:2858:b06b]:443 1E242C749707B4A68A269F0D31311CE36CDFEC28 url=https://wt.gri.mw/74Fm0lKUWWMMjZpKf6iSC0UH";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_1: &'static str = "webtunnel [2001:db8:289b:84cd:4be3:77f1:1cdd:9cb1]:443 D71C8E9C2180D2F35DEBF4A39BFCA6972F076D1C sni-imitation=yandex.ru,google.com,dzen.ru,vk.com,mail.ru,ozon.ru,ya.ru,www.wildberries.ru,rutube.ru,www.avito.ru,ok.ru,vkvideo.ru url=https://streaming.the-forgotten-tales.com/gz9X1VBgl0r1Xfx3dHdNl5Tl";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_2: &'static str = "webtunnel [2001:db8:dee9:5852:b4dc:7e14:21bd:c99b]:443 8ADF1761FA735FDD763781BB94A16EAB64A1CF6C url=https://app01.oneclickhost.eu/WJSgXJRlNnMStkuLZygVJ7lo";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_3: &'static str = "webtunnel [2001:db8:eedb:cae7:a345:4f72:f9cc:5de0]:443 B3C81E7A0CA474270DAA4A2C8633E1CA8935C37D url=https://wordpress.far-east-investment.ru/sORes7268CEUSRD7hAWvJU5A";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_4: &'static str = "webtunnel [2001:db8:945c:e0b9:7e4c:c974:ff00:d4c5]:443 91937F3EFB3BE5169788AC7C8BF07460B7E306DB url=https://kabel.entreri.de/YXbp1dNrJeOF8giAFFYWxvmf";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_5: &'static str = "webtunnel [2001:db8:7d4:9e13:8c7a:7e3:1f62:d790]:443 B7E362F9079D0C908F204581EB019034023BB224 url=https://balades-et-gouts.fr/xt70R9oyJt3B1xj89UCWPdLt";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_6: &'static str = "webtunnel [2001:db8:8c88:1b17:d7ae:cb68:f28e:e31c]:443 C115DAC2FE991CA25DDD43D7D4D398FEA9AA4C01 url=https://foglab.net/t9crLwo4LzFDWHdwcGf9gFrk";
|
||||
pub const ADDITIONAL_WEBTUNNEL_CONN_LINE_7: &'static str = "webtunnel [2001:db8:8ed6:e6c9:5fc9:9f20:a373:2374]:443 1636A2EFFBAA4B162F5FF461A1663EB55C41AE11 url=https://hanoi.delivery/roQFPLtlspWT6yIKeXD6lEci";
|
||||
|
||||
pub const DEFAULT_WEBTUNNEL_CONN_LINES: [&'static str; 8] = [
|
||||
TorBridge::DEFAULT_WEBTUNNEL_CONN_LINE,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_1,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_2,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_3,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_4,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_5,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_6,
|
||||
TorBridge::ADDITIONAL_WEBTUNNEL_CONN_LINE_7,
|
||||
];
|
||||
|
||||
/// Default Obfs4 protocol connection line.
|
||||
pub const DEFAULT_OBFS4_CONN_LINE: &'static str = "obfs4 51.83.248.35:25981 D08B4760D128C1A65506577E063D9D26C2A71815 cert=UJWUh+sIDdOKja/byBM2+qP9AFNl86hkGRFJ/lM1GWKP79eCu3PT4WTXI2gdXYULbQ0EMg iat-mode=0";
|
||||
/// Default Snowflake protocol connection line.
|
||||
pub const DEFAULT_SNOWFLAKE_CONN_LINE: &'static str = "snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.net:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn";
|
||||
|
||||
/// Get bridge protocol name.
|
||||
pub fn protocol_name(&self) -> String {
|
||||
match *self {
|
||||
TorBridge::Webtunnel(_, _) => "webtunnel".to_string(),
|
||||
TorBridge::Obfs4(_, _) => "obfs4".to_string(),
|
||||
TorBridge::Snowflake(_, _) => "snowflake".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get bridge client binary path.
|
||||
pub fn binary_path(&self) -> String {
|
||||
let is_android = os::OperatingSystem::from_target_os() == os::OperatingSystem::Android;
|
||||
match self {
|
||||
TorBridge::Webtunnel(path, _) => {
|
||||
if is_android {
|
||||
TorConfig::webtunnel_path()
|
||||
} else {
|
||||
path.clone()
|
||||
}
|
||||
}
|
||||
TorBridge::Obfs4(path, _) => path.clone(),
|
||||
TorBridge::Snowflake(path, _) => path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get bridge client binary name.
|
||||
pub fn binary_name(&self) -> String {
|
||||
let path = self.binary_path();
|
||||
path.split(std::path::MAIN_SEPARATOR_STR)
|
||||
.last()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Get bridge client connection line.
|
||||
pub fn connection_line(&self) -> String {
|
||||
match self {
|
||||
TorBridge::Webtunnel(_, line) => line.clone(),
|
||||
TorBridge::Obfs4(_, line) => line.clone(),
|
||||
TorBridge::Snowflake(_, line) => line.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update bridge connection line.
|
||||
pub fn update_conn_line(&mut self, l: String) {
|
||||
*self = match TorConfig::get_bridge().unwrap() {
|
||||
TorBridge::Webtunnel(bin, _) => TorBridge::Webtunnel(bin, l.clone()),
|
||||
TorBridge::Obfs4(bin, _) => TorBridge::Obfs4(bin, l.clone()),
|
||||
TorBridge::Snowflake(bin, _) => TorBridge::Snowflake(bin, l.clone()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Save binary path to provided bridge.
|
||||
pub fn save_bridge_bin_path(bridge: &TorBridge, path: String) {
|
||||
match bridge {
|
||||
TorBridge::Webtunnel(_, line) => {
|
||||
TorConfig::save_bridge(Some(TorBridge::Webtunnel(path, line.into())));
|
||||
}
|
||||
TorBridge::Obfs4(_, line) => {
|
||||
TorConfig::save_bridge(Some(TorBridge::Obfs4(path, line.into())));
|
||||
}
|
||||
TorBridge::Snowflake(_, line) => {
|
||||
TorConfig::save_bridge(Some(TorBridge::Snowflake(path, line.into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save connection line to provided bridge.
|
||||
pub fn save_bridge_conn_line(bridge: &TorBridge, line: String) {
|
||||
match bridge {
|
||||
TorBridge::Webtunnel(path, _) => {
|
||||
TorConfig::save_bridge(Some(TorBridge::Webtunnel(path.into(), line)));
|
||||
}
|
||||
TorBridge::Obfs4(path, _) => {
|
||||
TorConfig::save_bridge(Some(TorBridge::Obfs4(path.into(), line)));
|
||||
}
|
||||
TorBridge::Snowflake(path, _) => {
|
||||
TorConfig::save_bridge(Some(TorBridge::Snowflake(path.into(), line)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-22
@@ -23,7 +23,6 @@ use grin_wallet_util::OnionV3Address;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::tor::Tor;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
/// Mnemonic phrase word.
|
||||
@@ -204,7 +203,6 @@ pub enum WalletTxAction {
|
||||
Cancelling,
|
||||
Finalizing,
|
||||
Posting,
|
||||
SendingTor,
|
||||
Deleting,
|
||||
}
|
||||
|
||||
@@ -291,7 +289,6 @@ impl WalletTx {
|
||||
pub fn can_finalize(&self) -> bool {
|
||||
!self.cancelling()
|
||||
&& !self.data.confirmed
|
||||
&& (!self.sending_tor() || self.action_error.is_some())
|
||||
&& (self.data.tx_type == TxLogEntryType::TxSent
|
||||
|| self.data.tx_type == TxLogEntryType::TxReceived)
|
||||
&& (self.state == SlateState::Invoice1 || self.state == SlateState::Standard1)
|
||||
@@ -305,14 +302,6 @@ impl WalletTx {
|
||||
|| self.state == SlateState::Standard3
|
||||
}
|
||||
|
||||
/// Check if transaction is sending over Tor.
|
||||
pub fn sending_tor(&self) -> bool {
|
||||
if let Some(a) = self.action.as_ref() {
|
||||
return a == &WalletTxAction::SendingTor;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if transaction is cancelling.
|
||||
pub fn cancelling(&self) -> bool {
|
||||
if let Some(a) = self.action.as_ref() {
|
||||
@@ -334,7 +323,6 @@ impl WalletTx {
|
||||
!self.cancelling()
|
||||
&& !self.data.confirmed
|
||||
&& !self.broadcasting()
|
||||
&& (!self.sending_tor() || self.action_error.is_some())
|
||||
&& self.data.tx_type != TxLogEntryType::TxReceivedCancelled
|
||||
&& self.data.tx_type != TxLogEntryType::TxSentCancelled
|
||||
}
|
||||
@@ -358,12 +346,9 @@ impl WalletTx {
|
||||
if let Some(a) = &self.action {
|
||||
self.action_error.is_some() && a != &WalletTxAction::Cancelling
|
||||
} else {
|
||||
// Can resend over Tor.
|
||||
!self.data.confirmed
|
||||
&& !self.sending_tor()
|
||||
&& Tor::is_service_running(&wallet.identifier())
|
||||
&& !self.broadcasting()
|
||||
&& self.receiver.is_some()
|
||||
// Goblin's online payments go over nostr; there is no Tor resend.
|
||||
let _ = wallet;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,10 +399,6 @@ pub enum WalletTask {
|
||||
/// * amount
|
||||
/// * receiver
|
||||
Send(u64, Option<SlatepackAddress>),
|
||||
/// Send request over Tor.
|
||||
/// * tx
|
||||
/// * receiver
|
||||
SendTor(TxLogEntry, SlatepackAddress),
|
||||
/// Invoice creation.
|
||||
/// * amount
|
||||
Receive(u64),
|
||||
|
||||
+14
-133
@@ -15,7 +15,6 @@
|
||||
use crate::AppConfig;
|
||||
use crate::node::{Node, NodeConfig};
|
||||
use crate::nostr::{NostrConfig, NostrIdentity, NostrService, NostrStore};
|
||||
use crate::tor::Tor;
|
||||
use crate::wallet::seed::WalletSeed;
|
||||
use crate::wallet::store::TxHeightStore;
|
||||
use crate::wallet::types::{
|
||||
@@ -42,15 +41,13 @@ use grin_wallet_libwallet::api_impl::owner::{
|
||||
};
|
||||
use grin_wallet_libwallet::{
|
||||
Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState,
|
||||
SlateVersion, SlatepackAddress, StatusMessage, StoredProofInfo, TxLogEntry, TxLogEntryType,
|
||||
VersionedSlate, WalletBackend, WalletInitStatus, WalletInst, WalletLCProvider, address,
|
||||
SlatepackAddress, StatusMessage, StoredProofInfo, TxLogEntry, TxLogEntryType, WalletBackend,
|
||||
WalletInitStatus, WalletInst, WalletLCProvider, address,
|
||||
};
|
||||
use grin_wallet_util::OnionV3Address;
|
||||
use log::{error, info};
|
||||
use num_bigint::BigInt;
|
||||
use parking_lot::RwLock;
|
||||
use rand::Rng;
|
||||
use serde_json::{Value, json};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::{SocketAddr, TcpListener, ToSocketAddrs};
|
||||
@@ -62,7 +59,6 @@ use std::sync::{Arc, mpsc};
|
||||
use std::thread::Thread;
|
||||
use std::time::Duration;
|
||||
use std::{fs, path, thread};
|
||||
use tor_config::deps::Itertools;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Contains wallet instance, configuration and state, handles wallet commands.
|
||||
@@ -670,19 +666,6 @@ impl Wallet {
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Check if start of Tor listener on wallet opening is needed.
|
||||
pub fn auto_start_tor_listener(&self) -> bool {
|
||||
let r_config = self.config.read();
|
||||
r_config.enable_tor_listener.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Update start of Tor listener on wallet opening.
|
||||
pub fn update_auto_start_tor_listener(&self, start: bool) {
|
||||
let mut w_config = self.config.write();
|
||||
w_config.enable_tor_listener = Some(start);
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Check if Dandelion usage is needed to post transactions.
|
||||
pub fn can_use_dandelion(&self) -> bool {
|
||||
let r_config = self.config.read();
|
||||
@@ -765,7 +748,6 @@ impl Wallet {
|
||||
}
|
||||
// Close wallet at separate thread.
|
||||
let wallet_close = self.clone();
|
||||
let service_id = wallet_close.identifier();
|
||||
let conn = wallet_close.connection.clone();
|
||||
thread::spawn(move || {
|
||||
wallet_close.closing.store(true, Ordering::Relaxed);
|
||||
@@ -783,8 +765,6 @@ impl Wallet {
|
||||
w_api_server.as_mut().unwrap().0.stop();
|
||||
*w_api_server = None;
|
||||
}
|
||||
// Stop running Tor service.
|
||||
Tor::stop_service(&service_id);
|
||||
// Stop nostr service.
|
||||
{
|
||||
let mut w_nostr = wallet_close.nostr.write();
|
||||
@@ -885,7 +865,7 @@ impl Wallet {
|
||||
let w = lc.wallet_inst()?;
|
||||
let parent_key_id = w.parent_key_id();
|
||||
// Retrieve txs from database.
|
||||
let txs: Vec<TxLogEntry> = w
|
||||
let mut txs: Vec<TxLogEntry> = w
|
||||
.tx_log_iter()?
|
||||
.filter(|tx_entry| tx_entry.parent_key_id == parent_key_id)
|
||||
// Filter transactions to not show txs without slate (usually unspent outputs).
|
||||
@@ -903,18 +883,19 @@ impl Wallet {
|
||||
>= BigInt::from(1)
|
||||
}
|
||||
})
|
||||
// Sort txs by creation date and confirmation status.
|
||||
.sorted_by_key(|tx| -tx.creation_ts.timestamp())
|
||||
// Sort to show unconfirmed at top.
|
||||
.sorted_by_key(|tx| {
|
||||
.collect();
|
||||
// Sort txs by creation date (newest first); sort_by_key is stable so the
|
||||
// follow-up sort keeps this ordering within each group.
|
||||
txs.sort_by_key(|tx| -tx.creation_ts.timestamp());
|
||||
// Then float unconfirmed txs to the top.
|
||||
txs.sort_by_key(|tx| {
|
||||
tx.confirmed
|
||||
|| tx.tx_type == TxLogEntryType::TxReceivedCancelled
|
||||
|| tx.tx_type == TxLogEntryType::TxSentCancelled
|
||||
|| tx.tx_type == TxLogEntryType::TxReverted
|
||||
})
|
||||
});
|
||||
// Apply limit.
|
||||
.take(limit as usize)
|
||||
.collect();
|
||||
txs.truncate(limit as usize);
|
||||
Ok(txs)
|
||||
}
|
||||
|
||||
@@ -1002,10 +983,6 @@ impl Wallet {
|
||||
|
||||
/// Set active account from provided label.
|
||||
pub fn set_active_account(&self, label: &String) -> Result<(), Error> {
|
||||
// Stop service from previous account.
|
||||
let cur_service_id = self.identifier();
|
||||
Tor::stop_service(&cur_service_id);
|
||||
|
||||
// Clear secret key for previous account.
|
||||
{
|
||||
let mut w_key = self.secret_key.write();
|
||||
@@ -1339,48 +1316,6 @@ impl Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send slate to Tor address.
|
||||
async fn send_tor(&self, id: u32, s: &Slate, addr: &SlatepackAddress) -> Result<Slate, Error> {
|
||||
self.on_tx_action(id, Some(WalletTxAction::SendingTor));
|
||||
|
||||
let tor_addr = OnionV3Address::try_from(addr).unwrap().to_http_str();
|
||||
let url = format!("{}/v2/foreign", tor_addr);
|
||||
let slate_send = VersionedSlate::into_version(s.clone(), SlateVersion::V4)?;
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "receive_tx",
|
||||
"id": 1,
|
||||
"params": [
|
||||
slate_send,
|
||||
null,
|
||||
null
|
||||
]
|
||||
})
|
||||
.to_string();
|
||||
// Wait Tor service to launch.
|
||||
while Tor::is_service_starting(&self.identifier()) {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
// Send request to receiver.
|
||||
let req_res = Tor::post(body, url).await;
|
||||
if req_res.is_none() {
|
||||
return Err(Error::GenericError("Tor request error".to_string()));
|
||||
}
|
||||
|
||||
// Parse response.
|
||||
if let Ok(res) = serde_json::from_str::<Value>(&req_res.unwrap()) {
|
||||
if res["error"] != json!(null) {
|
||||
return Err(Error::GenericError("Response error".to_string()));
|
||||
}
|
||||
let slate_value = res["result"]["Ok"].clone();
|
||||
if let Ok(res) = &serde_json::to_string::<Value>(&slate_value) {
|
||||
let res = Slate::deserialize_upgrade(res);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
Err(Error::GenericError("Parse error".to_string()))
|
||||
}
|
||||
|
||||
/// Check if request to send funds is creating.
|
||||
pub fn send_creating(&self) -> bool {
|
||||
self.send_creating.load(Ordering::Relaxed)
|
||||
@@ -1955,31 +1890,17 @@ fn start_sync(wallet: Wallet) -> Thread {
|
||||
|
||||
if wallet.is_open() && !wallet.is_closing() {
|
||||
// Start Foreign API listener if not running.
|
||||
let mut api_server_running = { wallet.foreign_api_server.read().is_some() };
|
||||
let api_server_running = { wallet.foreign_api_server.read().is_some() };
|
||||
if !api_server_running {
|
||||
match start_api_server(&wallet) {
|
||||
Ok(api_server) => {
|
||||
let mut api_server_w = wallet.foreign_api_server.write();
|
||||
*api_server_w = Some(api_server);
|
||||
api_server_running = true;
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Start unfailed Tor service if API server is running.
|
||||
let service_id = wallet.identifier();
|
||||
if wallet.auto_start_tor_listener()
|
||||
&& api_server_running
|
||||
&& !Tor::is_service_failed(&service_id)
|
||||
{
|
||||
let r_foreign_api = wallet.foreign_api_server.read();
|
||||
let api = r_foreign_api.as_ref().unwrap();
|
||||
if let Some(key) = wallet.secret_key() {
|
||||
Tor::start_service(api.1, key, &wallet.identifier());
|
||||
}
|
||||
}
|
||||
|
||||
// Start nostr payment-messaging service (idempotent).
|
||||
if let Some(service) = wallet.nostr_service() {
|
||||
service.start(wallet.clone());
|
||||
@@ -2019,32 +1940,6 @@ fn start_sync(wallet: Wallet) -> Thread {
|
||||
|
||||
/// Handle wallet task.
|
||||
async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
let send_tor = async |tx: TxLogEntry, s: &Slate, r: &SlatepackAddress| match w
|
||||
.send_tor(tx.id, &s, r)
|
||||
.await
|
||||
{
|
||||
Ok(s) => match w.finalize(&s, tx.id) {
|
||||
Ok(s) => match w.post(&s, Some(tx.id)) {
|
||||
Ok(_) => {
|
||||
sync_wallet_data(&w, false);
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("send tor post error: {:?}", e);
|
||||
w.on_tx_error(tx.id, Some(e));
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("send tor finalize error: {:?}", e);
|
||||
w.task(WalletTask::Cancel(tx.id));
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("send tor error: {:?}", e);
|
||||
w.on_tx_error(tx.id, Some(e));
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
};
|
||||
match &t {
|
||||
WalletTask::OpenMessage(m) => {
|
||||
if !w.is_open() || m.is_empty() {
|
||||
@@ -2144,27 +2039,13 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
sync_wallet_data(&w, false);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
if let Some(tx) = tx {
|
||||
if let Some(addr) = r {
|
||||
let id = w.identifier();
|
||||
if Tor::is_service_running(&id) || Tor::is_service_starting(&id) {
|
||||
w.send_creating.store(false, Ordering::Relaxed);
|
||||
send_tor(tx, &s, addr).await;
|
||||
return;
|
||||
} else {
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
} else {
|
||||
// Slatepack send: hand the response slate back to the UI.
|
||||
// (Goblin's online payments go over nostr via NostrSend.)
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
}
|
||||
}
|
||||
w.send_creating.store(false, Ordering::Relaxed);
|
||||
}
|
||||
WalletTask::SendTor(tx, r) => {
|
||||
if let Some(s) = w.get_tx_slate(tx.id) {
|
||||
send_tor(tx.clone(), &s, r).await;
|
||||
}
|
||||
}
|
||||
WalletTask::Receive(a) => {
|
||||
w.invoice_creating.store(true, Ordering::Relaxed);
|
||||
if let Ok(s) = w.issue_invoice(*a) {
|
||||
|
||||
Submodule tor/webtunnel deleted from 8814d4ef97
Reference in New Issue
Block a user