mirror of
https://github.com/hoornet/vega.git
synced 2026-07-25 01:38:11 -07:00
Add configurable HTTP/SOCKS5 network proxy (#10)
Adds a Settings UI to route Vega through an HTTP or SOCKS5 proxy (e.g. Tor).
Covers both network stacks: the webview (proxy_url applied at build time, so
relay WebSocket traffic is proxied) and all five Rust-side plugin-http call
sites (converted to fetchWithProxy). The auto-updater's check and download are
also routed via check({ proxy }), so update traffic no longer bypasses the
proxy and leaks the user's IP.
socks5h:// is intentionally rejected for now: Tauri's webview proxy API only
documents http and socks5, so accepting socks5h globally would imply a DNS-leak
guarantee the webview can't yet make. Tracking a cross-platform DNS-leak test
as follow-up.
By Anderseta.
This commit is contained in:
committed by
GitHub
parent
ca60b4554c
commit
7f4eab58c2
Generated
+1
@@ -5435,6 +5435,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hex",
|
||||
"keyring",
|
||||
"reqwest 0.13.2",
|
||||
"rusqlite",
|
||||
"secp256k1",
|
||||
"serde",
|
||||
|
||||
@@ -18,18 +18,21 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["devtools", "tray-icon"] }
|
||||
tauri = { version = "2", features = ["devtools", "tray-icon", "macos-proxy"] }
|
||||
# Resolves the per-platform data/config roots before Tauri exists — the identifier
|
||||
# migration must run before tauri::Builder, so it cannot use app.path().
|
||||
dirs = "6"
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
# The updater uses reqwest 0.13. Enabling its SOCKS feature here is unified with
|
||||
# that transitive dependency, so updater checks and downloads accept socks5://.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["socks"] }
|
||||
tauri-plugin-process = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
keyring = { version = "3", features = ["apple-native", "windows-native", "linux-native"] }
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
tauri-plugin-http = "2.5.7"
|
||||
tauri-plugin-http = { version = "2.5.7", features = ["socks"] }
|
||||
tauri-plugin-dialog = "2.7.0"
|
||||
tauri-plugin-fs = "2.5.0"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
@@ -42,4 +45,3 @@ futures-util = "0.3"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
webkit2gtk = "2.0"
|
||||
|
||||
|
||||
+156
-6
@@ -4,6 +4,7 @@ use std::sync::Mutex;
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItem},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
WebviewUrl, WebviewWindowBuilder,
|
||||
Manager, WindowEvent,
|
||||
};
|
||||
|
||||
@@ -113,6 +114,128 @@ fn migrate_legacy_data_dirs() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Network proxy ───────────────────────────────────────────────────────────
|
||||
|
||||
const PROXY_SETTINGS_FILE: &str = "proxy.json";
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProxySettings {
|
||||
enabled: bool,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl Default for ProxySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_settings_path(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
|
||||
app.path()
|
||||
.app_data_dir()
|
||||
.map(|dir| dir.join(PROXY_SETTINGS_FILE))
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn load_proxy_settings(path: std::path::PathBuf) -> ProxySettings {
|
||||
let Ok(raw) = std::fs::read_to_string(path) else {
|
||||
return ProxySettings::default();
|
||||
};
|
||||
serde_json::from_str(&raw).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn load_proxy_settings_from_disk(app: &tauri::AppHandle) -> ProxySettings {
|
||||
let Ok(path) = proxy_settings_path(app) else {
|
||||
return ProxySettings::default();
|
||||
};
|
||||
load_proxy_settings(path)
|
||||
}
|
||||
|
||||
fn validate_proxy_settings(settings: &ProxySettings) -> Result<(), String> {
|
||||
if !settings.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let url = settings.url.trim();
|
||||
if url.is_empty() {
|
||||
return Err("Proxy URL is required when proxy is enabled".into());
|
||||
}
|
||||
|
||||
let parsed = url
|
||||
.parse::<tauri::Url>()
|
||||
.map_err(|_| "Proxy URL must be a valid http:// or socks5:// URL".to_string())?;
|
||||
match parsed.scheme() {
|
||||
"http" | "socks5" => {}
|
||||
_ => return Err("Proxy URL must start with http:// or socks5://".into()),
|
||||
}
|
||||
if parsed.host_str().is_none() {
|
||||
return Err("Proxy URL must include a host".into());
|
||||
}
|
||||
if parsed.port_or_known_default().is_none() {
|
||||
return Err("Proxy URL must include a port".into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enabled_proxy_url(settings: &ProxySettings) -> Result<Option<tauri::Url>, String> {
|
||||
if !settings.enabled {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
validate_proxy_settings(settings)?;
|
||||
settings
|
||||
.url
|
||||
.trim()
|
||||
.parse::<tauri::Url>()
|
||||
.map(Some)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_proxy_settings(app: tauri::AppHandle) -> Result<ProxySettings, String> {
|
||||
Ok(load_proxy_settings_from_disk(&app))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_proxy_settings(app: tauri::AppHandle, settings: ProxySettings) -> Result<(), String> {
|
||||
validate_proxy_settings(&settings)?;
|
||||
let path = proxy_settings_path(&app)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let normalized = ProxySettings {
|
||||
enabled: settings.enabled,
|
||||
url: settings.url.trim().to_string(),
|
||||
};
|
||||
let raw = serde_json::to_string_pretty(&normalized).map_err(|e| e.to_string())?;
|
||||
std::fs::write(path, raw).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn create_main_window(app: &tauri::App) -> tauri::Result<tauri::WebviewWindow> {
|
||||
let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
|
||||
.title("Vega")
|
||||
.inner_size(1200.0, 800.0)
|
||||
.min_inner_size(900.0, 600.0);
|
||||
|
||||
match enabled_proxy_url(&load_proxy_settings_from_disk(app.handle())) {
|
||||
Ok(Some(url)) => {
|
||||
builder = builder.proxy_url(url);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
eprintln!("[proxy] Ignoring invalid saved proxy setting: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
builder.build()
|
||||
}
|
||||
|
||||
// ── OS keychain ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Keep legacy keyring service name so existing users don't lose their keys
|
||||
@@ -569,6 +692,8 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.setup(|app| {
|
||||
let main_window = create_main_window(app)?;
|
||||
|
||||
// ── SQLite ───────────────────────────────────────────────────────
|
||||
let data_dir = app.path().app_data_dir()?;
|
||||
let conn = open_db(data_dir.clone())
|
||||
@@ -588,7 +713,6 @@ pub fn run() {
|
||||
// ── WebKit memory tuning for Linux (webkit2gtk) ──────────────────
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let main_window = app.get_webview_window("main").unwrap();
|
||||
main_window.with_webview(|webview| {
|
||||
use webkit2gtk::{CacheModel, SettingsExt, WebContextExt, WebViewExt};
|
||||
let wv = webview.inner();
|
||||
@@ -651,9 +775,8 @@ pub fn run() {
|
||||
// ── Close → hide to tray ─────────────────────────────────────────
|
||||
// Closing the window hides it instead of exiting. Use "Quit" in the
|
||||
// tray menu (or ⌘Q / Alt-F4) to fully exit.
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let window_clone = window.clone();
|
||||
window.on_window_event(move |event| {
|
||||
let window_clone = main_window.clone();
|
||||
main_window.on_window_event(move |event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window_clone.hide();
|
||||
@@ -663,6 +786,8 @@ pub fn run() {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_proxy_settings,
|
||||
save_proxy_settings,
|
||||
store_nsec,
|
||||
load_nsec,
|
||||
delete_nsec,
|
||||
@@ -689,7 +814,7 @@ pub fn run() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{migrate_dir, APP_IDENTIFIER, LEGACY_IDENTIFIER};
|
||||
use super::{enabled_proxy_url, migrate_dir, ProxySettings, APP_IDENTIFIER, LEGACY_IDENTIFIER};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
@@ -817,5 +942,30 @@ mod tests {
|
||||
assert!(!new.exists(), "must not fabricate a data dir for a new user");
|
||||
fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_http_and_socks5_proxy_urls() {
|
||||
for url in ["http://127.0.0.1:8118", "socks5://127.0.0.1:9050"] {
|
||||
let settings = ProxySettings {
|
||||
enabled: true,
|
||||
url: url.into(),
|
||||
};
|
||||
assert!(enabled_proxy_url(&settings).unwrap().is_some(), "{url}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_socks5h_until_webview_support_is_available() {
|
||||
let settings = ProxySettings {
|
||||
enabled: true,
|
||||
url: "socks5h://127.0.0.1:9050".into(),
|
||||
};
|
||||
|
||||
assert!(enabled_proxy_url(&settings).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updater_http_client_supports_socks5_proxy_urls() {
|
||||
assert!(reqwest::Proxy::all("socks5://127.0.0.1:9050").is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,7 @@
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Vega",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 900,
|
||||
"minHeight": 600
|
||||
}
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { useWoTStore } from "../../stores/wot";
|
||||
@@ -11,6 +12,7 @@ import { useBookmarkStore } from "../../stores/bookmark";
|
||||
import { getStoredRelayUrls } from "../../lib/nostr";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import { profileName } from "../../lib/utils";
|
||||
import { refreshProxySettingsCache, type ProxySettings } from "../../lib/proxy";
|
||||
import { NWCWizard } from "./NWCWizard";
|
||||
import { getNotificationSettings, saveNotificationSettings, ensurePermission } from "../../lib/notifications";
|
||||
import {
|
||||
@@ -408,6 +410,139 @@ function NotificationSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function validateProxyUrl(enabled: boolean, url: string): string | null {
|
||||
if (!enabled) return null;
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) return "Proxy URL is required";
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
if (parsed.protocol !== "socks5:" && parsed.protocol !== "http:") {
|
||||
return "Use socks5:// or http://";
|
||||
}
|
||||
if (!parsed.hostname) return "Proxy URL must include a host";
|
||||
if (!parsed.port) return "Proxy URL must include a port";
|
||||
} catch {
|
||||
return "Proxy URL is invalid";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ProxySection() {
|
||||
const [settings, setSettings] = useState<ProxySettings>({ enabled: false, url: "" });
|
||||
const [initial, setInitial] = useState<ProxySettings>({ enabled: false, url: "" });
|
||||
const [status, setStatus] = useState<"loading" | "idle" | "saving" | "saved" | "error">("loading");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
invoke<ProxySettings>("get_proxy_settings")
|
||||
.then((loaded) => {
|
||||
setSettings(loaded);
|
||||
setInitial(loaded);
|
||||
setStatus("idle");
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(String(err));
|
||||
setStatus("error");
|
||||
});
|
||||
}, []);
|
||||
|
||||
const validationError = validateProxyUrl(settings.enabled, settings.url);
|
||||
const dirty = settings.enabled !== initial.enabled || settings.url.trim() !== initial.url.trim();
|
||||
const savedNeedsRestart = status === "saved" && !dirty;
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
setStatus("saving");
|
||||
setError(null);
|
||||
const next = { enabled: settings.enabled, url: settings.url.trim() };
|
||||
try {
|
||||
await invoke("save_proxy_settings", { settings: next });
|
||||
refreshProxySettingsCache();
|
||||
setSettings(next);
|
||||
setInitial(next);
|
||||
setStatus("saved");
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
setStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
setSettings((current) => ({ ...current, enabled: !current.enabled }));
|
||||
setError(null);
|
||||
if (status === "saved") setStatus("idle");
|
||||
};
|
||||
|
||||
const updateUrl = (value: string) => {
|
||||
setSettings((current) => ({ ...current, url: value }));
|
||||
setError(null);
|
||||
if (status === "saved") setStatus("idle");
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-2 text-text-dim">
|
||||
Network Proxy
|
||||
</h2>
|
||||
<p className="text-text-dim text-[11px] mb-3">
|
||||
Route Vega's in-app network traffic through an HTTP or SOCKS5 proxy.
|
||||
</p>
|
||||
<label className="flex items-center gap-3 cursor-pointer group mb-3">
|
||||
<button
|
||||
onClick={toggle}
|
||||
disabled={status === "loading"}
|
||||
className={`w-9 h-5 rounded-full transition-colors relative shrink-0 disabled:opacity-50 ${
|
||||
settings.enabled ? "bg-accent" : "bg-border"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-bg transition-transform ${
|
||||
settings.enabled ? "translate-x-4" : "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-text text-[12px]">Use proxy for connections</span>
|
||||
</label>
|
||||
<div className="flex gap-2 ml-12">
|
||||
<input
|
||||
value={settings.url}
|
||||
onChange={(e) => updateUrl(e.target.value)}
|
||||
placeholder="socks5://127.0.0.1:9050"
|
||||
disabled={!settings.enabled || status === "loading"}
|
||||
className="flex-1 bg-bg border border-border px-3 py-1.5 text-text text-[12px] focus:outline-none focus:border-accent/50 placeholder:text-text-dim disabled:opacity-50 disabled:cursor-not-allowed font-mono"
|
||||
/>
|
||||
<button
|
||||
onClick={saveSettings}
|
||||
disabled={status === "saving" || status === "loading" || !!validationError || !dirty}
|
||||
className="px-3 py-1.5 text-[11px] border border-border text-text-muted hover:text-accent hover:border-accent/40 transition-colors disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
|
||||
>
|
||||
{status === "saving" ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-text-dim text-[10px] mt-1.5 ml-12">
|
||||
Applies after restart. Example: socks5://127.0.0.1:9050
|
||||
</p>
|
||||
{(error || validationError) && (
|
||||
<p className="text-danger text-[10px] mt-1 ml-12">{error ?? validationError}</p>
|
||||
)}
|
||||
{savedNeedsRestart && (
|
||||
<div className="flex items-center gap-3 mt-2 ml-12">
|
||||
<p className="text-accent text-[10px]">Saved. Restart Vega to apply it to all connections.</p>
|
||||
<button
|
||||
onClick={() => relaunch()}
|
||||
className="px-2 py-1 text-[10px] border border-accent/40 text-accent hover:bg-bg-hover transition-colors"
|
||||
>
|
||||
Restart now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeSection() {
|
||||
const { themeId, setTheme } = useUIStore();
|
||||
|
||||
@@ -594,6 +729,7 @@ export function SettingsView() {
|
||||
<EasyReadFontSection />
|
||||
<WalletSection />
|
||||
<NotificationSection />
|
||||
<ProxySection />
|
||||
<ExperimentalSection />
|
||||
<ExportSection />
|
||||
<IdentitySection />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { check } from "@tauri-apps/plugin-updater";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getProxySettings } from "../lib/proxy";
|
||||
|
||||
interface InstallInfo {
|
||||
can_self_update: boolean;
|
||||
@@ -20,6 +21,12 @@ interface UpdateState {
|
||||
dismiss: () => void;
|
||||
}
|
||||
|
||||
async function checkForUpdate() {
|
||||
const settings = await getProxySettings();
|
||||
const proxy = settings.enabled ? settings.url.trim() : "";
|
||||
return check(proxy ? { proxy } : undefined);
|
||||
}
|
||||
|
||||
export function useUpdater(): UpdateState {
|
||||
const [available, setAvailable] = useState(false);
|
||||
const [version, setVersion] = useState<string | null>(null);
|
||||
@@ -42,7 +49,7 @@ export function useUpdater(): UpdateState {
|
||||
// Check for updates ~5 s after startup (non-blocking)
|
||||
const t = setTimeout(async () => {
|
||||
try {
|
||||
const update = await check();
|
||||
const update = await checkForUpdate();
|
||||
if (update?.available) {
|
||||
setAvailable(true);
|
||||
setVersion(update.version);
|
||||
@@ -59,7 +66,7 @@ export function useUpdater(): UpdateState {
|
||||
setInstalling(true);
|
||||
setError(null);
|
||||
try {
|
||||
const update = await check();
|
||||
const update = await checkForUpdate();
|
||||
if (update?.available) {
|
||||
await update.downloadAndInstall();
|
||||
await relaunch();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { fetchWithProxy as fetch } from "../proxy";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { getNDK } from "./core";
|
||||
import { debug } from "../debug";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { fetchWithProxy as fetch } from "../proxy";
|
||||
import type { PodcastEpisode } from "../../types/podcast";
|
||||
import { enrichWithV4V } from "./podcastIndexV4V";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { fetchWithProxy as fetch } from "../proxy";
|
||||
import type { PodcastEpisode, V4VRecipient } from "../../types/podcast";
|
||||
|
||||
const API_KEY = "VKWWTGY25NVCKYJWHSNY";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { fetchWithProxy as fetch } from "../proxy";
|
||||
import type { PodcastEpisode, V4VRecipient } from "../../types/podcast";
|
||||
import { payInvoiceViaNWC, payKeysendViaNWC } from "../lightning/nwc";
|
||||
import { useV4VStore } from "../../stores/v4v";
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
|
||||
|
||||
export interface ProxySettings {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type TauriFetchInput = Parameters<typeof tauriFetch>[0];
|
||||
type TauriFetchInit = Parameters<typeof tauriFetch>[1];
|
||||
type TauriFetchInitWithProxy = NonNullable<TauriFetchInit> & {
|
||||
proxy?: {
|
||||
all?: string;
|
||||
};
|
||||
};
|
||||
|
||||
let proxySettingsPromise: Promise<ProxySettings> | null = null;
|
||||
|
||||
export function getProxySettings(): Promise<ProxySettings> {
|
||||
if (!proxySettingsPromise) {
|
||||
proxySettingsPromise = invoke<ProxySettings>("get_proxy_settings")
|
||||
.then((settings) => settings ?? { enabled: false, url: "" })
|
||||
.catch(() => ({ enabled: false, url: "" }));
|
||||
}
|
||||
return proxySettingsPromise;
|
||||
}
|
||||
|
||||
export function refreshProxySettingsCache(): void {
|
||||
proxySettingsPromise = null;
|
||||
}
|
||||
|
||||
export async function fetchWithProxy(input: TauriFetchInput, init?: TauriFetchInit): Promise<Response> {
|
||||
const settings = await getProxySettings();
|
||||
if (!settings.enabled || !settings.url.trim()) {
|
||||
return tauriFetch(input, init);
|
||||
}
|
||||
|
||||
const proxiedInit: TauriFetchInitWithProxy = {
|
||||
...(init ?? {}),
|
||||
proxy: { all: settings.url.trim() },
|
||||
};
|
||||
return tauriFetch(input, proxiedInit);
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
if (import.meta.env.DEV && !(window as any).__TAURI_INTERNALS__) {
|
||||
const keychainKey = (pubkey: string) => `__dev_nsec_${pubkey}`;
|
||||
const proxyKey = "__dev_proxy_settings";
|
||||
|
||||
const mockInvoke = async (cmd: string, args: Record<string, unknown> = {}): Promise<unknown> => {
|
||||
switch (cmd) {
|
||||
@@ -22,6 +23,13 @@ if (import.meta.env.DEV && !(window as any).__TAURI_INTERNALS__) {
|
||||
case "delete_nsec":
|
||||
localStorage.removeItem(keychainKey(args.pubkey as string));
|
||||
return null;
|
||||
case "get_proxy_settings": {
|
||||
const stored = localStorage.getItem(proxyKey);
|
||||
return stored ? JSON.parse(stored) : { enabled: false, url: "" };
|
||||
}
|
||||
case "save_proxy_settings":
|
||||
localStorage.setItem(proxyKey, JSON.stringify(args.settings));
|
||||
return null;
|
||||
case "db_save_notes":
|
||||
case "db_save_profile":
|
||||
return null;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { fetchWithProxy as fetch } from "./proxy";
|
||||
import { getNDK } from "./nostr";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user