mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-04 01:25:12 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7ae299771 | |||
| c43f141907 | |||
| cd33accb1a | |||
| ca89b917f4 | |||
| 6ad183ab89 | |||
| c83950bee7 | |||
| 0047c80967 | |||
| 3d7bd2b14c | |||
| 8899e58987 | |||
| acf8651bd1 | |||
| ef534ee779 | |||
| 75bb10cf61 | |||
| 6f9e0de633 | |||
| 39c2a9f6f0 | |||
| 4b6f08fca3 | |||
| 24eff75d4e | |||
| 11869855e9 | |||
| 0d1f1f1497 | |||
| e8026d817f | |||
| d1ca4273de |
@@ -11,7 +11,7 @@ permissions:
|
||||
jobs:
|
||||
generate-release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||
if: startsWith(github.event.release.tag_name, 'v') && !github.event.release.prerelease
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
id: get-previous-tag
|
||||
run: |
|
||||
# Get the previous release tag (excluding the current one)
|
||||
CURRENT_TAG="${{ github.ref_name }}"
|
||||
CURRENT_TAG="${{ github.event.release.tag_name }}"
|
||||
PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "$CURRENT_TAG" | head -n 1)
|
||||
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
|
||||
Vendored
+9
@@ -54,6 +54,7 @@
|
||||
"esac",
|
||||
"esbuild",
|
||||
"etree",
|
||||
"firstrun",
|
||||
"flate",
|
||||
"frontmost",
|
||||
"geoip",
|
||||
@@ -66,10 +67,12 @@
|
||||
"hkcu",
|
||||
"hooksconfig",
|
||||
"hookspath",
|
||||
"Hoverable",
|
||||
"icns",
|
||||
"idlelib",
|
||||
"idletime",
|
||||
"idna",
|
||||
"infobars",
|
||||
"Inno",
|
||||
"kdeglobals",
|
||||
"keras",
|
||||
@@ -77,6 +80,7 @@
|
||||
"killall",
|
||||
"Kolkata",
|
||||
"kreadconfig",
|
||||
"langpack",
|
||||
"launchservices",
|
||||
"letterboxing",
|
||||
"libatk",
|
||||
@@ -108,6 +112,7 @@
|
||||
"nobrowse",
|
||||
"noconfirm",
|
||||
"nodecar",
|
||||
"NODELAY",
|
||||
"nodemon",
|
||||
"norestart",
|
||||
"NSIS",
|
||||
@@ -139,6 +144,7 @@
|
||||
"pyoxidizer",
|
||||
"pytest",
|
||||
"pyyaml",
|
||||
"reportingpolicy",
|
||||
"reqwest",
|
||||
"ridedott",
|
||||
"rlib",
|
||||
@@ -149,6 +155,7 @@
|
||||
"screeninfo",
|
||||
"selectables",
|
||||
"serde",
|
||||
"sessionstore",
|
||||
"setpriority",
|
||||
"setsid",
|
||||
"SETTINGCHANGE",
|
||||
@@ -184,6 +191,7 @@
|
||||
"Torbrowser",
|
||||
"tqdm",
|
||||
"trackingprotection",
|
||||
"trailhead",
|
||||
"turbopack",
|
||||
"turtledemo",
|
||||
"udeps",
|
||||
@@ -192,6 +200,7 @@
|
||||
"unrs",
|
||||
"urlencoding",
|
||||
"urllib",
|
||||
"utoipa",
|
||||
"venv",
|
||||
"vercel",
|
||||
"VERYSILENT",
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./dist/types/routes.d.ts" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
"author": "",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"commander": "^14.0.2",
|
||||
"donutbrowser-camoufox-js": "^0.7.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"fingerprint-generator": "^2.1.76",
|
||||
"fingerprint-generator": "^2.1.77",
|
||||
"get-port": "^7.1.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"playwright-core": "^1.56.1",
|
||||
"proxy-chain": "^2.5.9",
|
||||
"playwright-core": "^1.57.0",
|
||||
"proxy-chain": "^2.6.0",
|
||||
"tmp": "^0.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
+11
-11
@@ -2,7 +2,7 @@
|
||||
"name": "donutbrowser",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tauri-apps/api": "^2.9.0",
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.5",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "~2.4.4",
|
||||
@@ -51,17 +51,17 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"color": "^5.0.2",
|
||||
"color": "^5.0.3",
|
||||
"flag-icons": "^7.5.0",
|
||||
"lucide-react": "^0.555.0",
|
||||
"motion": "^12.23.24",
|
||||
"next": "^15.5.6",
|
||||
"next": "^16.0.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"recharts": "2.15.4",
|
||||
"recharts": "3.5.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tauri-plugin-macos-permissions-api": "^2.3.0"
|
||||
@@ -69,14 +69,14 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.3",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tauri-apps/cli": "^2.9.4",
|
||||
"@tauri-apps/cli": "^2.9.5",
|
||||
"@types/color": "^4.2.0",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.3",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.6",
|
||||
"lint-staged": "^16.2.7",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"ts-unused-exports": "^11.0.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
|
||||
Generated
+971
-953
File diff suppressed because it is too large
Load Diff
Generated
+54
-1
@@ -1293,7 +1293,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.13.0"
|
||||
version = "0.13.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -1306,6 +1306,7 @@ dependencies = [
|
||||
"clap",
|
||||
"core-foundation 0.10.1",
|
||||
"directories",
|
||||
"env_logger",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http-body-util",
|
||||
@@ -1458,6 +1459,19 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -2524,6 +2538,30 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -3728,6 +3766,21 @@ dependencies = [
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "donutbrowser"
|
||||
version = "0.13.0"
|
||||
version = "0.13.2"
|
||||
description = "Simple Yet Powerful Anti-Detect Browser"
|
||||
authors = ["zhom@github"]
|
||||
edition = "2021"
|
||||
@@ -39,6 +39,7 @@ tauri-plugin-dialog = "2"
|
||||
tauri-plugin-macos-permissions = "2"
|
||||
tauri-plugin-log = "2"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
directories = "6"
|
||||
reqwest = { version = "0.12", features = ["json", "stream", "socks"] }
|
||||
|
||||
@@ -5,7 +5,15 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:default",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-emit",
|
||||
"core:event:allow-emit-to",
|
||||
"core:event:allow-unlisten",
|
||||
"core:image:default",
|
||||
"core:menu:default",
|
||||
"core:path:default",
|
||||
"core:tray:default",
|
||||
"core:webview:default",
|
||||
"core:window:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-close",
|
||||
|
||||
@@ -564,7 +564,6 @@ impl ApiClient {
|
||||
let cached_data: CachedGithubData = serde_json::from_str(&content).ok()?;
|
||||
|
||||
// Always use cached GitHub releases - cache never expires, only gets updated with new versions
|
||||
log::info!("Using cached GitHub releases for {browser}");
|
||||
Some(cached_data.releases)
|
||||
}
|
||||
|
||||
|
||||
@@ -999,6 +999,22 @@ impl AppAutoUpdater {
|
||||
// Clean up backup after successful installation
|
||||
let _ = fs::remove_dir_all(&backup_path);
|
||||
|
||||
// Clean up old "Donut Browser.app" if it exists (from before the project rename)
|
||||
if let Some(parent_dir) = current_app_path.parent() {
|
||||
let old_app_path = parent_dir.join("Donut Browser.app");
|
||||
if old_app_path.exists() && old_app_path != current_app_path {
|
||||
log::info!(
|
||||
"Removing old 'Donut Browser.app' from: {}",
|
||||
old_app_path.display()
|
||||
);
|
||||
if let Err(e) = fs::remove_dir_all(&old_app_path) {
|
||||
log::warn!("Warning: Failed to remove old 'Donut Browser.app': {e}");
|
||||
} else {
|
||||
log::info!("Successfully removed old 'Donut Browser.app'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,12 @@ fn build_proxy_url(
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() {
|
||||
// Initialize logger to write to stderr (which will be redirected to file)
|
||||
env_logger::Builder::from_default_env()
|
||||
.filter_level(log::LevelFilter::Debug)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
// Set up panic handler to log panics before process exits
|
||||
std::panic::set_hook(Box::new(|panic_info| {
|
||||
log::error!("PANIC in proxy worker: {:?}", panic_info);
|
||||
|
||||
+14
-25
@@ -624,22 +624,9 @@ impl Browser for FirefoxBrowser {
|
||||
args.push("--headless".to_string());
|
||||
}
|
||||
|
||||
// Use -no-remote for browsers that require it for security (Mullvad, Tor) or when remote debugging
|
||||
match self.browser_type {
|
||||
BrowserType::MullvadBrowser | BrowserType::TorBrowser => {
|
||||
args.push("-no-remote".to_string());
|
||||
}
|
||||
BrowserType::Firefox
|
||||
| BrowserType::FirefoxDeveloper
|
||||
| BrowserType::Zen
|
||||
| BrowserType::Camoufox => {
|
||||
// Use -no-remote when remote debugging to avoid conflicts
|
||||
if remote_debugging_port.is_some() {
|
||||
args.push("-no-remote".to_string());
|
||||
}
|
||||
// Don't use -no-remote for normal launches so we can communicate with existing instances
|
||||
}
|
||||
_ => {}
|
||||
// Use -no-remote when remote debugging to avoid conflicts with existing instances
|
||||
if remote_debugging_port.is_some() {
|
||||
args.push("-no-remote".to_string());
|
||||
}
|
||||
|
||||
// Firefox-based browsers use profile directory and user.js for proxy configuration
|
||||
@@ -737,6 +724,12 @@ impl Browser for ChromiumBrowser {
|
||||
"--disable-background-timer-throttling".to_string(),
|
||||
"--crash-server-url=".to_string(),
|
||||
"--disable-updater".to_string(),
|
||||
// Disable quit confirmation and session restore prompts
|
||||
"--disable-session-crashed-bubble".to_string(),
|
||||
"--hide-crash-restore-bubble".to_string(),
|
||||
"--disable-infobars".to_string(),
|
||||
// Disable QUIC/HTTP3 to ensure traffic goes through HTTP proxy
|
||||
"--disable-quic".to_string(),
|
||||
];
|
||||
|
||||
// Add remote debugging if requested
|
||||
@@ -1096,30 +1089,26 @@ mod tests {
|
||||
"Firefox should include debugging port"
|
||||
);
|
||||
|
||||
// Test Mullvad Browser (should always use -no-remote)
|
||||
// Test Mullvad Browser (no special flags without remote debugging)
|
||||
let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser);
|
||||
let args = browser
|
||||
.create_launch_args("/path/to/profile", None, None, None, false)
|
||||
.expect("Failed to create launch args for Mullvad Browser");
|
||||
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
|
||||
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
|
||||
|
||||
// Test Tor Browser (should always use -no-remote)
|
||||
// Test Tor Browser (no special flags without remote debugging)
|
||||
let browser = FirefoxBrowser::new(BrowserType::TorBrowser);
|
||||
let args = browser
|
||||
.create_launch_args("/path/to/profile", None, None, None, false)
|
||||
.expect("Failed to create launch args for Tor Browser");
|
||||
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
|
||||
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
|
||||
|
||||
// Test Zen Browser (should not use -no-remote for normal launch)
|
||||
// Test Zen Browser (no special flags without remote debugging)
|
||||
let browser = FirefoxBrowser::new(BrowserType::Zen);
|
||||
let args = browser
|
||||
.create_launch_args("/path/to/profile", None, None, None, false)
|
||||
.expect("Failed to create launch args for Zen Browser");
|
||||
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
|
||||
assert!(
|
||||
!args.contains(&"-no-remote".to_string()),
|
||||
"Zen Browser should not use -no-remote for normal launch"
|
||||
);
|
||||
|
||||
// Test headless mode
|
||||
let args = browser
|
||||
|
||||
+114
-113
@@ -1071,7 +1071,7 @@ impl ProfileManager {
|
||||
|
||||
fn get_common_firefox_preferences(&self) -> Vec<String> {
|
||||
vec![
|
||||
// Disable default browser updates
|
||||
// Disable default browser check
|
||||
"user_pref(\"browser.shell.checkDefaultBrowser\", false);".to_string(),
|
||||
"user_pref(\"browser.shell.skipDefaultBrowserCheckOnFirstRun\", true);".to_string(),
|
||||
"user_pref(\"browser.preferences.moreFromMozilla\", false);".to_string(),
|
||||
@@ -1086,27 +1086,58 @@ impl ProfileManager {
|
||||
// Keep extension updates enabled
|
||||
"user_pref(\"extensions.update.enabled\", true);".to_string(),
|
||||
"user_pref(\"extensions.update.autoUpdateDefault\", true);".to_string(),
|
||||
// Completely disable browser update checking
|
||||
"user_pref(\"app.update.enabled\", false);".to_string(),
|
||||
"user_pref(\"app.update.staging.enabled\", false);".to_string(),
|
||||
"user_pref(\"app.update.timerFirstInterval\", -1);".to_string(),
|
||||
"user_pref(\"app.update.download.maxAttempts\", 0);".to_string(),
|
||||
"user_pref(\"app.update.elevate.maxAttempts\", 0);".to_string(),
|
||||
"user_pref(\"app.update.disabledForTesting\", true);".to_string(),
|
||||
"user_pref(\"app.update.auto\", false);".to_string(),
|
||||
"user_pref(\"app.update.mode\", 0);".to_string(),
|
||||
"user_pref(\"app.update.promptWaitTime\", -1);".to_string(),
|
||||
"user_pref(\"app.update.service.enabled\", false);".to_string(),
|
||||
"user_pref(\"app.update.staging.enabled\", false);".to_string(),
|
||||
"user_pref(\"app.update.silent\", true);".to_string(),
|
||||
"user_pref(\"app.update.disabledForTesting\", true);".to_string(),
|
||||
// Prevent update URL access entirely
|
||||
"user_pref(\"app.update.url\", \"\");".to_string(),
|
||||
"user_pref(\"app.update.url.manual\", \"\");".to_string(),
|
||||
"user_pref(\"app.update.url.details\", \"\");".to_string(),
|
||||
// Disable update timing/scheduling
|
||||
"user_pref(\"app.update.timerFirstInterval\", 999999999);".to_string(),
|
||||
"user_pref(\"app.update.interval\", 999999999);".to_string(),
|
||||
"user_pref(\"app.update.background.interval\", 999999999);".to_string(),
|
||||
"user_pref(\"app.update.idletime\", 999999999);".to_string(),
|
||||
"user_pref(\"app.update.promptWaitTime\", 999999999);".to_string(),
|
||||
// Disable update attempts
|
||||
"user_pref(\"app.update.download.maxAttempts\", 0);".to_string(),
|
||||
"user_pref(\"app.update.elevate.maxAttempts\", 0);".to_string(),
|
||||
"user_pref(\"app.update.checkInstallTime\", false);".to_string(),
|
||||
"user_pref(\"app.update.interval\", -1);".to_string(),
|
||||
"user_pref(\"app.update.background.interval\", -1);".to_string(),
|
||||
"user_pref(\"app.update.idletime\", -1);".to_string(),
|
||||
// Suppress additional update UI/prompts
|
||||
// Suppress update UI/prompts/notifications
|
||||
"user_pref(\"app.update.doorhanger\", false);".to_string(),
|
||||
"user_pref(\"app.update.badge\", false);".to_string(),
|
||||
"user_pref(\"app.update.notifyDuringDownload\", false);".to_string(),
|
||||
"user_pref(\"app.update.background.scheduling.enabled\", false);".to_string(),
|
||||
"user_pref(\"app.update.background.enabled\", false);".to_string(),
|
||||
// Disable BITS (Windows Background Intelligent Transfer Service) updates
|
||||
"user_pref(\"app.update.BITS.enabled\", false);".to_string(),
|
||||
// Disable language pack updates
|
||||
"user_pref(\"app.update.langpack.enabled\", false);".to_string(),
|
||||
// Suppress upgrade dialogs on startup
|
||||
"user_pref(\"browser.startup.upgradeDialog.enabled\", false);".to_string(),
|
||||
// Disable update ping telemetry
|
||||
"user_pref(\"toolkit.telemetry.updatePing.enabled\", false);".to_string(),
|
||||
// Zen browser specific - disable welcome screen and updates
|
||||
"user_pref(\"zen.welcome-screen.seen\", true);".to_string(),
|
||||
"user_pref(\"zen.updates.enabled\", false);".to_string(),
|
||||
"user_pref(\"zen.updates.check-for-updates\", false);".to_string(),
|
||||
// Additional first-run suppressions
|
||||
"user_pref(\"app.normandy.first_run\", false);".to_string(),
|
||||
"user_pref(\"trailhead.firstrun.didSeeAboutWelcome\", true);".to_string(),
|
||||
"user_pref(\"datareporting.policy.dataSubmissionPolicyBypassNotification\", true);"
|
||||
.to_string(),
|
||||
"user_pref(\"toolkit.telemetry.reportingpolicy.firstRun\", false);".to_string(),
|
||||
// Disable quit confirmation dialogs
|
||||
"user_pref(\"browser.warnOnQuit\", false);".to_string(),
|
||||
"user_pref(\"browser.showQuitWarning\", false);".to_string(),
|
||||
"user_pref(\"browser.tabs.warnOnClose\", false);".to_string(),
|
||||
"user_pref(\"browser.tabs.warnOnCloseOtherTabs\", false);".to_string(),
|
||||
"user_pref(\"browser.sessionstore.warnOnQuit\", false);".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1128,114 +1159,76 @@ impl ProfileManager {
|
||||
|
||||
let mut preferences = Vec::new();
|
||||
|
||||
// Get the UUID directory (parent of profile data directory)
|
||||
let uuid_dir = profile_data_path
|
||||
.parent()
|
||||
.ok_or("Invalid profile path - cannot find UUID directory")?;
|
||||
|
||||
// Add common Firefox preferences (like disabling default browser check)
|
||||
preferences.extend(self.get_common_firefox_preferences());
|
||||
|
||||
// Use embedded PAC template instead of reading from file
|
||||
const PAC_TEMPLATE: &str = r#"function FindProxyForURL(url, host) {
|
||||
return "{{proxy_url}}";
|
||||
}"#;
|
||||
// Determine which proxy settings to use
|
||||
let effective_proxy = internal_proxy.unwrap_or(proxy);
|
||||
let proxy_host = &effective_proxy.host;
|
||||
let proxy_port = effective_proxy.port;
|
||||
|
||||
// Format proxy URL based on type and whether we have an internal proxy
|
||||
let proxy_url = if let Some(internal) = internal_proxy {
|
||||
// Use internal proxy (local proxy) as the primary proxy
|
||||
// This is the local proxy that forwards to the upstream proxy
|
||||
log::info!(
|
||||
"Applying local proxy settings to Firefox profile: {}:{}",
|
||||
internal.host,
|
||||
internal.port
|
||||
);
|
||||
format!("HTTP {}:{}", internal.host, internal.port)
|
||||
} else {
|
||||
// Use user-configured proxy directly (upstream proxy)
|
||||
log::info!(
|
||||
"Applying upstream proxy settings to Firefox profile: {}:{} ({})",
|
||||
proxy.host,
|
||||
proxy.port,
|
||||
proxy.proxy_type
|
||||
);
|
||||
match proxy.proxy_type.as_str() {
|
||||
"http" => format!("HTTP {}:{}", proxy.host, proxy.port),
|
||||
"https" => format!("HTTPS {}:{}", proxy.host, proxy.port),
|
||||
"socks4" => format!("SOCKS4 {}:{}", proxy.host, proxy.port),
|
||||
"socks5" => format!("SOCKS5 {}:{}", proxy.host, proxy.port),
|
||||
_ => return Err(format!("Unsupported proxy type: {}", proxy.proxy_type).into()),
|
||||
}
|
||||
};
|
||||
// Check if this is a SOCKS proxy (only possible when using upstream directly)
|
||||
let is_socks =
|
||||
internal_proxy.is_none() && (proxy.proxy_type == "socks4" || proxy.proxy_type == "socks5");
|
||||
|
||||
// Replace placeholders in PAC file
|
||||
let pac_content = PAC_TEMPLATE
|
||||
.replace("{{proxy_url}}", &proxy_url)
|
||||
.replace("{{proxy_credentials}}", ""); // Credentials are now handled by the PAC file
|
||||
|
||||
// Save PAC file in UUID directory
|
||||
let pac_path = uuid_dir.join("proxy.pac");
|
||||
log::info!(
|
||||
"Creating PAC file at: {} with proxy: {}",
|
||||
pac_path.display(),
|
||||
proxy_url
|
||||
);
|
||||
fs::write(&pac_path, &pac_content)?;
|
||||
log::info!(
|
||||
"Created PAC file at: {} with content: {}",
|
||||
pac_path.display(),
|
||||
pac_content
|
||||
"Applying manual proxy settings to Firefox profile: {}:{} (is_internal: {}, is_socks: {})",
|
||||
proxy_host,
|
||||
proxy_port,
|
||||
internal_proxy.is_some(),
|
||||
is_socks
|
||||
);
|
||||
|
||||
// Configure Firefox to use the PAC file
|
||||
// Convert path to absolute and properly format for file:// URL
|
||||
let pac_path_absolute = pac_path.canonicalize().unwrap_or_else(|_| pac_path.clone());
|
||||
let pac_url = if cfg!(windows) {
|
||||
// Windows: file:///C:/path/to/file.pac
|
||||
format!(
|
||||
"file:///{}",
|
||||
pac_path_absolute.to_string_lossy().replace('\\', "/")
|
||||
)
|
||||
// Use MANUAL proxy configuration (type 1) instead of PAC file (type 2)
|
||||
// PAC files with file:// URLs are blocked by privacy-focused browsers like Zen and Mullvad
|
||||
// Manual proxy configuration works reliably across all Firefox variants
|
||||
preferences.push("user_pref(\"network.proxy.type\", 1);".to_string());
|
||||
|
||||
if is_socks {
|
||||
// SOCKS proxy configuration
|
||||
preferences.extend([
|
||||
format!("user_pref(\"network.proxy.socks\", \"{}\");", proxy_host),
|
||||
format!("user_pref(\"network.proxy.socks_port\", {});", proxy_port),
|
||||
format!(
|
||||
"user_pref(\"network.proxy.socks_version\", {});",
|
||||
if proxy.proxy_type == "socks5" { 5 } else { 4 }
|
||||
),
|
||||
"user_pref(\"network.proxy.http\", \"\");".to_string(),
|
||||
"user_pref(\"network.proxy.http_port\", 0);".to_string(),
|
||||
"user_pref(\"network.proxy.ssl\", \"\");".to_string(),
|
||||
"user_pref(\"network.proxy.ssl_port\", 0);".to_string(),
|
||||
]);
|
||||
} else {
|
||||
// Unix/macOS: file:///absolute/path/to/file.pac (three slashes for absolute path)
|
||||
format!("file://{}", pac_path_absolute.to_string_lossy())
|
||||
};
|
||||
|
||||
log::info!("PAC file path (absolute): {}", pac_path_absolute.display());
|
||||
log::info!("PAC file URL for Firefox: {}", pac_url);
|
||||
// HTTP/HTTPS proxy configuration (including our internal local proxy)
|
||||
preferences.extend([
|
||||
format!("user_pref(\"network.proxy.http\", \"{}\");", proxy_host),
|
||||
format!("user_pref(\"network.proxy.http_port\", {});", proxy_port),
|
||||
format!("user_pref(\"network.proxy.ssl\", \"{}\");", proxy_host),
|
||||
format!("user_pref(\"network.proxy.ssl_port\", {});", proxy_port),
|
||||
format!("user_pref(\"network.proxy.ftp\", \"{}\");", proxy_host),
|
||||
format!("user_pref(\"network.proxy.ftp_port\", {});", proxy_port),
|
||||
"user_pref(\"network.proxy.socks\", \"\");".to_string(),
|
||||
"user_pref(\"network.proxy.socks_port\", 0);".to_string(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Common proxy settings - keep it simple like proxy-chain expected
|
||||
preferences.extend([
|
||||
"user_pref(\"network.proxy.type\", 2);".to_string(),
|
||||
format!(
|
||||
"user_pref(\"network.proxy.autoconfig_url\", \"{}\");",
|
||||
pac_url
|
||||
),
|
||||
"user_pref(\"network.proxy.failover_direct\", false);".to_string(),
|
||||
"user_pref(\"network.proxy.socks_remote_dns\", true);".to_string(),
|
||||
"user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(),
|
||||
"user_pref(\"signon.autologin.proxy\", true);".to_string(),
|
||||
"user_pref(\"network.proxy.share_proxy_settings\", false);".to_string(),
|
||||
"user_pref(\"network.automatic-ntlm-auth.allow-proxies\", false);".to_string(),
|
||||
"user_pref(\"network.auth-use-sspi\", false);".to_string(),
|
||||
"user_pref(\"network.proxy.autoconfig_url\", \"\");".to_string(),
|
||||
// Disable QUIC/HTTP3 - it bypasses HTTP proxy
|
||||
"user_pref(\"network.http.http3.enable\", false);".to_string(),
|
||||
"user_pref(\"network.http.http3.enabled\", false);".to_string(),
|
||||
]);
|
||||
|
||||
// Write settings to user.js file
|
||||
let user_js_content = preferences.join("\n");
|
||||
fs::write(user_js_path, &user_js_content)?;
|
||||
log::info!("Updated user.js with proxy settings. PAC URL: {}", pac_url);
|
||||
if let Some(internal) = internal_proxy {
|
||||
log::info!(
|
||||
"Firefox will use LOCAL proxy: {}:{} (which forwards to upstream)",
|
||||
internal.host,
|
||||
internal.port
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"Firefox will use UPSTREAM proxy directly: {}:{}",
|
||||
proxy.host,
|
||||
proxy.port
|
||||
);
|
||||
}
|
||||
log::info!(
|
||||
"Updated user.js with manual proxy settings: {}:{}",
|
||||
proxy_host,
|
||||
proxy_port
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1397,20 +1390,28 @@ mod tests {
|
||||
assert!(user_js_path.exists(), "user.js should be created");
|
||||
|
||||
let content = fs::read_to_string(&user_js_path).expect("Should read user.js");
|
||||
|
||||
// Check for manual proxy configuration (type 1) instead of PAC (type 2)
|
||||
// Manual proxy is used because PAC file:// URLs are blocked by privacy browsers like Zen
|
||||
assert!(
|
||||
content.contains("network.proxy.type"),
|
||||
"Should contain proxy type setting"
|
||||
content.contains("network.proxy.type\", 1"),
|
||||
"Should set proxy type to 1 (manual)"
|
||||
);
|
||||
assert!(content.contains("2"), "Should set proxy type to 2 (PAC)");
|
||||
|
||||
// Check that PAC file was created
|
||||
let pac_path = uuid_dir.join("proxy.pac");
|
||||
assert!(pac_path.exists(), "proxy.pac should be created");
|
||||
|
||||
let pac_content = fs::read_to_string(&pac_path).expect("Should read proxy.pac");
|
||||
assert!(
|
||||
pac_content.contains("FindProxyForURL"),
|
||||
"PAC file should contain FindProxyForURL function"
|
||||
content.contains("network.proxy.http\", \"proxy.example.com\""),
|
||||
"Should set HTTP proxy host"
|
||||
);
|
||||
assert!(
|
||||
content.contains("network.proxy.http_port\", 8080"),
|
||||
"Should set HTTP proxy port"
|
||||
);
|
||||
assert!(
|
||||
content.contains("network.proxy.ssl\", \"proxy.example.com\""),
|
||||
"Should set SSL proxy host"
|
||||
);
|
||||
assert!(
|
||||
content.contains("network.proxy.ssl_port\", 8080"),
|
||||
"Should set SSL proxy port"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,7 +491,6 @@ impl ProxyManager {
|
||||
"https://ipinfo.io/ip",
|
||||
"https://icanhazip.com",
|
||||
"https://ifconfig.co/ip",
|
||||
"https://ipecho.net/plain",
|
||||
];
|
||||
|
||||
// Create HTTP client with proxy
|
||||
|
||||
@@ -59,18 +59,12 @@ pub async fn start_proxy_process_with_profile(
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.stdout(Stdio::null());
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let log_path = std::path::PathBuf::from("/tmp").join(format!("donut-proxy-{}.log", id));
|
||||
if let Ok(file) = std::fs::File::create(&log_path) {
|
||||
log::error!("Proxy worker stderr will be logged to: {:?}", log_path);
|
||||
cmd.stderr(Stdio::from(file));
|
||||
} else {
|
||||
cmd.stderr(Stdio::null());
|
||||
}
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
// Always log to file for diagnostics (both debug and release builds)
|
||||
let log_path = std::path::PathBuf::from("/tmp").join(format!("donut-proxy-{}.log", id));
|
||||
if let Ok(file) = std::fs::File::create(&log_path) {
|
||||
log::info!("Proxy worker stderr will be logged to: {:?}", log_path);
|
||||
cmd.stderr(Stdio::from(file));
|
||||
} else {
|
||||
cmd.stderr(Stdio::null());
|
||||
}
|
||||
|
||||
|
||||
@@ -367,6 +367,13 @@ async fn handle_http(
|
||||
// This is faster and more reliable than trying to use hyper-proxy with version conflicts
|
||||
use reqwest::Client;
|
||||
|
||||
log::error!(
|
||||
"DEBUG: Handling HTTP request: {} {} (host: {:?})",
|
||||
req.method(),
|
||||
req.uri(),
|
||||
req.uri().host()
|
||||
);
|
||||
|
||||
// Extract domain for traffic tracking
|
||||
let domain = req
|
||||
.uri()
|
||||
@@ -609,7 +616,12 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::er
|
||||
// This ensures the process doesn't exit even if there are no active connections
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((mut stream, _)) => {
|
||||
Ok((mut stream, peer_addr)) => {
|
||||
// Enable TCP_NODELAY to ensure small packets are sent immediately
|
||||
// This is critical for CONNECT responses to be sent before tunneling begins
|
||||
let _ = stream.set_nodelay(true);
|
||||
log::error!("DEBUG: Accepted connection from {:?}", peer_addr);
|
||||
|
||||
let upstream = upstream_url.clone();
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
@@ -617,9 +629,13 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::er
|
||||
// CONNECT requests need special handling for tunneling
|
||||
let mut peek_buffer = [0u8; 8];
|
||||
match stream.read(&mut peek_buffer).await {
|
||||
Ok(n) if n >= 7 => {
|
||||
Ok(0) => {
|
||||
log::error!("DEBUG: Connection closed immediately (0 bytes read)");
|
||||
}
|
||||
Ok(n) => {
|
||||
let request_start = String::from_utf8_lossy(&peek_buffer[..n.min(7)]);
|
||||
if request_start.starts_with("CONNECT") {
|
||||
log::error!("DEBUG: Read {} bytes, starts with: {:?}", n, request_start);
|
||||
if n >= 7 && request_start.starts_with("CONNECT") {
|
||||
// Handle CONNECT request manually for tunneling
|
||||
let mut full_request = Vec::with_capacity(4096);
|
||||
full_request.extend_from_slice(&peek_buffer[..n]);
|
||||
@@ -651,7 +667,14 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::er
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Not CONNECT - reconstruct stream with consumed bytes prepended
|
||||
|
||||
// Not CONNECT (or partial read) - reconstruct stream with consumed bytes prepended
|
||||
// This is critical: we MUST prepend any bytes we consumed, even if < 7 bytes
|
||||
log::error!(
|
||||
"DEBUG: Non-CONNECT request, first {} bytes: {:?}",
|
||||
n,
|
||||
String::from_utf8_lossy(&peek_buffer[..n])
|
||||
);
|
||||
let prepended_bytes = peek_buffer[..n].to_vec();
|
||||
let prepended_reader = PrependReader {
|
||||
prepended: prepended_bytes,
|
||||
@@ -664,17 +687,10 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::er
|
||||
if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
|
||||
log::error!("Error serving connection: {:?}", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// For non-CONNECT requests, use hyper's HTTP handling
|
||||
let io = TokioIo::new(stream);
|
||||
let service = service_fn(move |req| handle_request(req, upstream.clone()));
|
||||
|
||||
if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
|
||||
log::error!("Error serving connection: {:?}", err);
|
||||
Err(e) => {
|
||||
log::error!("Error reading from connection: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -807,6 +823,9 @@ async fn handle_connect_from_buffer(
|
||||
}
|
||||
};
|
||||
|
||||
// Enable TCP_NODELAY on target stream for immediate data transfer
|
||||
let _ = target_stream.set_nodelay(true);
|
||||
|
||||
// Send 200 Connection Established response to client
|
||||
// CRITICAL: Must flush after writing to ensure response is sent before tunneling
|
||||
client_stream
|
||||
|
||||
@@ -17,6 +17,19 @@ pub struct BandwidthDataPoint {
|
||||
pub bytes_received: u64,
|
||||
}
|
||||
|
||||
/// Individual domain access data point for time-series tracking
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DomainAccessPoint {
|
||||
/// Unix timestamp in seconds
|
||||
pub timestamp: u64,
|
||||
/// Domain name
|
||||
pub domain: String,
|
||||
/// Bytes sent in this request
|
||||
pub bytes_sent: u64,
|
||||
/// Bytes received in this request
|
||||
pub bytes_received: u64,
|
||||
}
|
||||
|
||||
/// Domain access information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DomainAccess {
|
||||
@@ -78,9 +91,12 @@ pub struct TrafficStats {
|
||||
/// Bandwidth data points (time-series, 1 point per second, stored indefinitely)
|
||||
#[serde(default)]
|
||||
pub bandwidth_history: Vec<BandwidthDataPoint>,
|
||||
/// Domain access statistics
|
||||
/// Domain access statistics (aggregated all-time)
|
||||
#[serde(default)]
|
||||
pub domains: HashMap<String, DomainAccess>,
|
||||
/// Domain access history (time-series for filtering by period)
|
||||
#[serde(default)]
|
||||
pub domain_access_history: Vec<DomainAccessPoint>,
|
||||
/// Unique IPs accessed
|
||||
#[serde(default)]
|
||||
pub unique_ips: Vec<String>,
|
||||
@@ -99,6 +115,7 @@ impl TrafficStats {
|
||||
total_requests: 0,
|
||||
bandwidth_history: Vec::new(),
|
||||
domains: HashMap::new(),
|
||||
domain_access_history: Vec::new(),
|
||||
unique_ips: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -163,6 +180,7 @@ impl TrafficStats {
|
||||
let now = current_timestamp();
|
||||
self.total_requests += 1;
|
||||
|
||||
// Update aggregated domain stats
|
||||
let entry = self
|
||||
.domains
|
||||
.entry(domain.to_string())
|
||||
@@ -179,6 +197,14 @@ impl TrafficStats {
|
||||
entry.bytes_sent += bytes_sent;
|
||||
entry.bytes_received += bytes_received;
|
||||
entry.last_access = now;
|
||||
|
||||
// Add to domain access history for time-period filtering
|
||||
self.domain_access_history.push(DomainAccessPoint {
|
||||
timestamp: now,
|
||||
domain: domain.to_string(),
|
||||
bytes_sent,
|
||||
bytes_received,
|
||||
});
|
||||
}
|
||||
|
||||
/// Record an IP address access
|
||||
@@ -362,6 +388,14 @@ fn merge_traffic_stats(dest: &mut TrafficStats, src: &TrafficStats) {
|
||||
entry.last_access = entry.last_access.max(access.last_access);
|
||||
}
|
||||
|
||||
// Merge domain access history
|
||||
let mut combined_domain_history: Vec<DomainAccessPoint> = dest.domain_access_history.clone();
|
||||
for point in &src.domain_access_history {
|
||||
combined_domain_history.push(point.clone());
|
||||
}
|
||||
combined_domain_history.sort_by_key(|p| p.timestamp);
|
||||
dest.domain_access_history = combined_domain_history;
|
||||
|
||||
// Merge unique IPs
|
||||
for ip in &src.unique_ips {
|
||||
if !dest.unique_ips.contains(ip) {
|
||||
@@ -557,7 +591,9 @@ pub struct FilteredTrafficStats {
|
||||
/// Period stats: bytes sent/received within the requested period
|
||||
pub period_bytes_sent: u64,
|
||||
pub period_bytes_received: u64,
|
||||
/// Domain access statistics (always full, as it's already aggregated)
|
||||
/// Period requests within the requested period
|
||||
pub period_requests: u64,
|
||||
/// Domain access statistics filtered to requested time period
|
||||
pub domains: HashMap<String, DomainAccess>,
|
||||
/// Unique IPs accessed
|
||||
pub unique_ips: Vec<String>,
|
||||
@@ -586,10 +622,45 @@ pub fn get_traffic_stats_for_period(
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Calculate period totals
|
||||
// Calculate period totals for bandwidth
|
||||
let period_bytes_sent: u64 = filtered_history.iter().map(|dp| dp.bytes_sent).sum();
|
||||
let period_bytes_received: u64 = filtered_history.iter().map(|dp| dp.bytes_received).sum();
|
||||
|
||||
// Filter and aggregate domain stats for the period
|
||||
let mut filtered_domains: HashMap<String, DomainAccess> = HashMap::new();
|
||||
let mut period_requests: u64 = 0;
|
||||
|
||||
for access in stats
|
||||
.domain_access_history
|
||||
.iter()
|
||||
.filter(|a| a.timestamp >= cutoff)
|
||||
{
|
||||
period_requests += 1;
|
||||
let entry = filtered_domains
|
||||
.entry(access.domain.clone())
|
||||
.or_insert(DomainAccess {
|
||||
domain: access.domain.clone(),
|
||||
request_count: 0,
|
||||
bytes_sent: 0,
|
||||
bytes_received: 0,
|
||||
first_access: access.timestamp,
|
||||
last_access: access.timestamp,
|
||||
});
|
||||
|
||||
entry.request_count += 1;
|
||||
entry.bytes_sent += access.bytes_sent;
|
||||
entry.bytes_received += access.bytes_received;
|
||||
entry.first_access = entry.first_access.min(access.timestamp);
|
||||
entry.last_access = entry.last_access.max(access.timestamp);
|
||||
}
|
||||
|
||||
// If no domain_access_history exists (old data), fall back to all-time domains
|
||||
let domains = if stats.domain_access_history.is_empty() {
|
||||
stats.domains
|
||||
} else {
|
||||
filtered_domains
|
||||
};
|
||||
|
||||
Some(FilteredTrafficStats {
|
||||
profile_id: stats.profile_id,
|
||||
session_start: stats.session_start,
|
||||
@@ -600,7 +671,8 @@ pub fn get_traffic_stats_for_period(
|
||||
bandwidth_history: filtered_history,
|
||||
period_bytes_sent,
|
||||
period_bytes_received,
|
||||
domains: stats.domains,
|
||||
period_requests,
|
||||
domains,
|
||||
unique_ips: stats.unique_ips,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Donut",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"identifier": "com.donutbrowser",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
|
||||
|
||||
@@ -69,11 +69,11 @@ export function BandwidthMiniChart({
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"relative flex items-center gap-1.5 px-2 rounded cursor-pointer hover:bg-accent/50 transition-colors min-w-[130px] border-none bg-transparent",
|
||||
"relative flex items-center gap-1.5 px-2 rounded cursor-pointer hover:bg-accent/50 transition-colors min-w-[120px] border-none bg-transparent",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 h-3">
|
||||
<div className="flex-1 h-3 pointer-events-none">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
@@ -106,6 +106,8 @@ export function BandwidthMiniChart({
|
||||
strokeWidth={1}
|
||||
fill="url(#bandwidthGradient)"
|
||||
isAnimationActive={false}
|
||||
dot={false}
|
||||
activeDot={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -331,7 +331,9 @@ const TagsCell = React.memo<{
|
||||
ref={containerRef as unknown as React.RefObject<HTMLButtonElement>}
|
||||
className={cn(
|
||||
"flex overflow-hidden gap-1 items-center px-2 py-1 h-6 w-full bg-transparent rounded border-none cursor-pointer",
|
||||
isDisabled ? "opacity-60" : "cursor-pointer hover:bg-accent/50",
|
||||
isDisabled
|
||||
? "opacity-60 cursor-not-allowed"
|
||||
: "cursor-pointer hover:bg-accent/50",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isDisabled) setOpenTagsEditorFor(profile.id);
|
||||
@@ -354,7 +356,7 @@ const TagsCell = React.memo<{
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-48 h-6 cursor-pointer">
|
||||
<div className="w-40 h-6 cursor-pointer">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{ButtonContent}</TooltipTrigger>
|
||||
{hiddenCount > 0 && (
|
||||
@@ -380,13 +382,13 @@ const TagsCell = React.memo<{
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-48 h-6 relative",
|
||||
"w-40 h-6 relative",
|
||||
isDisabled && "opacity-60 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={editorRef}
|
||||
className="absolute top-0 left-0 z-50 w-48 min-h-6 bg-popover rounded-md shadow-md"
|
||||
className="absolute top-0 left-0 z-50 w-40 min-h-6 bg-popover rounded-md shadow-md"
|
||||
>
|
||||
<MultipleSelector
|
||||
value={valueOptions}
|
||||
@@ -1451,8 +1453,9 @@ export function ProfilesDataTable({
|
||||
size="sm"
|
||||
disabled={!canLaunch || isLaunching || isStopping}
|
||||
className={cn(
|
||||
"cursor-pointer min-w-[70px] h-7",
|
||||
!canLaunch && "opacity-50",
|
||||
"min-w-[70px] h-7",
|
||||
!canLaunch && "opacity-50 cursor-not-allowed",
|
||||
canLaunch && "cursor-pointer",
|
||||
)}
|
||||
onClick={() =>
|
||||
isRunning
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import * as React from "react";
|
||||
import type { TooltipProps } from "recharts";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
@@ -12,10 +11,7 @@ import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import type {
|
||||
NameType,
|
||||
ValueType,
|
||||
} from "recharts/types/component/DefaultTooltipContent";
|
||||
import type { TooltipContentProps } from "recharts/types/component/Tooltip";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -30,6 +26,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
Tooltip as UITooltip,
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { FilteredTrafficStats } from "@/types";
|
||||
|
||||
type TimePeriod =
|
||||
@@ -94,6 +95,53 @@ function getSecondsForPeriod(period: TimePeriod): number {
|
||||
}
|
||||
}
|
||||
|
||||
const TruncatedDomain = React.memo<{ domain: string }>(({ domain }) => {
|
||||
const ref = React.useRef<HTMLSpanElement>(null);
|
||||
const [isTruncated, setIsTruncated] = React.useState(false);
|
||||
|
||||
const checkTruncation = React.useCallback(() => {
|
||||
if (ref.current) {
|
||||
setIsTruncated(ref.current.scrollWidth > ref.current.clientWidth);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
checkTruncation();
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver(checkTruncation);
|
||||
if (ref.current) {
|
||||
resizeObserver.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [checkTruncation]);
|
||||
|
||||
const content = (
|
||||
<span ref={ref} className="truncate max-w-[200px] block">
|
||||
{domain}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (!isTruncated) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<UITooltip>
|
||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{domain}</p>
|
||||
</TooltipContent>
|
||||
</UITooltip>
|
||||
);
|
||||
});
|
||||
|
||||
TruncatedDomain.displayName = "TruncatedDomain";
|
||||
|
||||
export function TrafficDetailsDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -140,7 +188,7 @@ export function TrafficDetailsDialog({
|
||||
|
||||
// Tooltip render function
|
||||
const renderTooltip = React.useCallback(
|
||||
(props: TooltipProps<ValueType, NameType>) => {
|
||||
(props: TooltipContentProps<number, string>) => {
|
||||
const { active, payload, label } = props;
|
||||
if (!active || !payload?.length) return null;
|
||||
|
||||
@@ -356,9 +404,11 @@ export function TrafficDetailsDialog({
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded-lg p-3">
|
||||
<p className="text-xs text-muted-foreground">Total Requests</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Requests ({timePeriod === "all" ? "total" : timePeriod})
|
||||
</p>
|
||||
<p className="text-lg font-semibold">
|
||||
{(stats?.total_requests || 0).toLocaleString()}
|
||||
{(stats?.period_requests || 0).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,14 +416,14 @@ export function TrafficDetailsDialog({
|
||||
{/* Total Stats (smaller, under period stats) */}
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground border-t pt-4">
|
||||
<div>
|
||||
<span className="font-medium">Total:</span>{" "}
|
||||
<span className="font-medium">All-time traffic:</span>{" "}
|
||||
{formatBytes(
|
||||
(stats?.total_bytes_sent || 0) +
|
||||
(stats?.total_bytes_received || 0),
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Requests:</span>{" "}
|
||||
<span className="font-medium">All-time requests:</span>{" "}
|
||||
{stats?.total_requests?.toLocaleString() || 0}
|
||||
</div>
|
||||
</div>
|
||||
@@ -389,7 +439,8 @@ export function TrafficDetailsDialog({
|
||||
{topDomainsByTraffic.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">
|
||||
Top Domains by Traffic
|
||||
Top Domains by Traffic (
|
||||
{timePeriod === "all" ? "all time" : timePeriod})
|
||||
</h3>
|
||||
<div className="border rounded-md">
|
||||
<div className="grid grid-cols-[1fr_80px_80px_80px] gap-2 px-3 py-2 text-xs font-medium text-muted-foreground border-b bg-muted/30">
|
||||
@@ -408,9 +459,7 @@ export function TrafficDetailsDialog({
|
||||
<span className="text-xs text-muted-foreground w-4 shrink-0">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="truncate" title={domain.domain}>
|
||||
{domain.domain}
|
||||
</span>
|
||||
<TruncatedDomain domain={domain.domain} />
|
||||
</div>
|
||||
<span className="text-right text-muted-foreground">
|
||||
{domain.request_count.toLocaleString()}
|
||||
@@ -432,7 +481,8 @@ export function TrafficDetailsDialog({
|
||||
{topDomainsByRequests.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">
|
||||
Top Domains by Requests
|
||||
Top Domains by Requests (
|
||||
{timePeriod === "all" ? "all time" : timePeriod})
|
||||
</h3>
|
||||
<div className="border rounded-md">
|
||||
<div className="grid grid-cols-[1fr_80px_100px] gap-2 px-3 py-2 text-xs font-medium text-muted-foreground border-b bg-muted/30">
|
||||
@@ -450,9 +500,7 @@ export function TrafficDetailsDialog({
|
||||
<span className="text-xs text-muted-foreground w-4 shrink-0">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="truncate" title={domain.domain}>
|
||||
{domain.domain}
|
||||
</span>
|
||||
<TruncatedDomain domain={domain.domain} />
|
||||
</div>
|
||||
<span className="text-right text-muted-foreground">
|
||||
{domain.request_count.toLocaleString()}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
import type {
|
||||
Props as DefaultLegendContentProps,
|
||||
LegendPayload,
|
||||
} from "recharts/types/component/DefaultLegendContent";
|
||||
import type { Payload } from "recharts/types/component/DefaultTooltipContent";
|
||||
import type { TooltipContentProps } from "recharts/types/component/Tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -105,13 +111,15 @@ const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
TooltipContentProps<number, string> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
labelClassName?: string;
|
||||
color?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
@@ -187,15 +195,15 @@ const ChartTooltipContent = React.forwardRef<
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item, index) => {
|
||||
.filter((item: Payload<number, string>) => item.type !== "none")
|
||||
.map((item: Payload<number, string>, index: number) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
const indicatorColor = color || item.payload?.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
key={String(item.dataKey ?? index)}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center",
|
||||
@@ -264,7 +272,7 @@ const ChartLegend = RechartsPrimitive.Legend;
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
Pick<DefaultLegendContentProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}
|
||||
@@ -289,8 +297,8 @@ const ChartLegendContent = React.forwardRef<
|
||||
)}
|
||||
>
|
||||
{payload
|
||||
.filter((item) => item.type !== "none")
|
||||
.map((item) => {
|
||||
.filter((item: LegendPayload) => item.type !== "none")
|
||||
.map((item: LegendPayload) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
|
||||
@@ -320,6 +320,7 @@ export interface FilteredTrafficStats {
|
||||
bandwidth_history: BandwidthDataPoint[];
|
||||
period_bytes_sent: number;
|
||||
period_bytes_received: number;
|
||||
period_requests: number;
|
||||
domains: Record<string, DomainAccess>;
|
||||
unique_ips: string[];
|
||||
}
|
||||
|
||||
+4
-2
@@ -13,7 +13,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
@@ -29,7 +29,9 @@
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"next-env.d.ts",
|
||||
"dist/types/**/*.ts"
|
||||
"dist/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"dist/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "nodecar", "src-tauri/target"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user