Compare commits

...

39 Commits

Author SHA1 Message Date
zhom 1c5444928d refactor: flush consistently 2025-12-02 23:06:30 +04:00
zhom 85f8630389 chore: version bump 2025-12-02 22:31:21 +04:00
zhom 57ead61139 refactor: reduce cpu usage for proxies 2025-12-02 22:30:40 +04:00
zhom ef00c59063 docs: add cla 2025-12-02 20:32:00 +04:00
zhom a61f42b645 chore: version bump 2025-12-02 13:00:19 +04:00
zhom 3dd66069b5 build: rerun tauri_build on binaries change 2025-12-02 12:59:50 +04:00
zhom 14c7ded062 chore: fix release notes escape character 2025-12-02 12:52:02 +04:00
zhom d58b68fd50 chore: version bump 2025-12-02 11:01:09 +04:00
zhom 3e69fea338 fix: show progress on macos 2025-12-02 10:01:48 +04:00
zhom fe2125beba chore: fix issue validation workflow 2025-12-02 01:05:21 +04:00
zhom 23cfa84998 chore: prevent code injection 2025-12-02 00:29:12 +04:00
zhom 3e3ec29f58 chore: version bump 2025-12-02 00:07:40 +04:00
zhom b1b91e94c0 chore: add permissions for webview 2025-12-02 00:06:54 +04:00
zhom c624196dbb chore: update release notes triggering logic 2025-12-01 23:55:59 +04:00
zhom b24568043c chore: linting 2025-12-01 15:21:22 +04:00
zhom d201cc90d1 chore: version bump 2025-12-01 13:41:36 +04:00
zhom a118ccc349 chore: linting 2025-12-01 12:29:21 +04:00
zhom effe229067 refactor: remove mullvad and tor browser artifacts 2025-12-01 12:25:21 +04:00
dependabot[bot] 98a8369f60 deps(deps): bump the frontend-dependencies group across 1 directory with 9 updates (#147)
Bumps the frontend-dependencies group with 1 update in the / directory: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome).


Updates `@biomejs/biome` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

Updates `@biomejs/cli-darwin-arm64` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

Updates `@biomejs/cli-darwin-x64` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-arm64-musl` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-arm64` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-x64-musl` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

Updates `@biomejs/cli-linux-x64` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-arm64` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

Updates `@biomejs/cli-win32-x64` from 2.2.3 to 2.3.8
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.3.8/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.3.8
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-arm64"
  dependency-version: 2.3.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-darwin-x64"
  dependency-version: 2.3.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64-musl"
  dependency-version: 2.3.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-arm64"
  dependency-version: 2.3.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64-musl"
  dependency-version: 2.3.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-linux-x64"
  dependency-version: 2.3.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-arm64"
  dependency-version: 2.3.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
- dependency-name: "@biomejs/cli-win32-x64"
  dependency-version: 2.3.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: frontend-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-30 22:16:04 +00:00
zhom f7ae299771 chore: version bump 2025-12-01 01:47:32 +04:00
zhom c43f141907 refactor: better error handling 2025-12-01 01:46:28 +04:00
zhom cd33accb1a refactor: show tooltip for truncated text 2025-12-01 00:45:41 +04:00
zhom ca89b917f4 refactor: respect selected timeframe for top domains 2025-12-01 00:15:23 +04:00
zhom 6ad183ab89 style: profile table cleanup 2025-11-30 23:29:07 +04:00
zhom c83950bee7 chore: codegen 2025-11-30 22:43:10 +04:00
zhom 0047c80967 style: make the row chart shorter 2025-11-30 21:28:19 +04:00
zhom 3d7bd2b14c chore: version bump 2025-11-30 21:25:25 +04:00
zhom 8899e58987 chore: simplify tsconfig 2025-11-30 21:18:56 +04:00
zhom acf8651bd1 refactor: fix types after dependency upgrade 2025-11-30 21:16:26 +04:00
zhom ef534ee779 chore: update major dependencies 2025-11-30 21:06:09 +04:00
zhom 75bb10cf61 chore: remove ipecho from domain checkers 2025-11-30 21:03:31 +04:00
zhom 6f9e0de633 chore: update dependencies 2025-11-30 20:59:19 +04:00
zhom 39c2a9f6f0 refactor: disable quit confirmations in browser 2025-11-30 20:59:04 +04:00
zhom 4b6f08fca3 refactor: disable more update-related settings 2025-11-30 20:44:59 +04:00
zhom 24eff75d4e chore: cleanup logs 2025-11-30 20:42:06 +04:00
zhom 11869855e9 build: make permissions more explicit 2025-11-30 20:40:34 +04:00
zhom 0d1f1f1497 refactor: suppress first-run warnings 2025-11-30 20:40:10 +04:00
zhom e8026d817f refactor: clean up old binary after installation 2025-11-30 20:39:34 +04:00
zhom d1ca4273de chore: check tag name instead of ref 2025-11-30 20:08:25 +04:00
46 changed files with 1857 additions and 2763 deletions
+3 -2
View File
@@ -115,13 +115,14 @@ jobs:
- name: Parse validation result and take action
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RESPONSE_FILE: ${{ steps.validate.outputs.response-file }}
RESPONSE: ${{ steps.validate.outputs.response }}
run: |
# Prefer reading from the response file to avoid output truncation
RESPONSE_FILE='${{ steps.validate.outputs.response-file }}'
if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then
RAW_OUTPUT=$(cat "$RESPONSE_FILE")
else
RAW_OUTPUT='${{ steps.validate.outputs.response }}'
RAW_OUTPUT="$RESPONSE"
fi
# Extract JSON if wrapped in markdown code fences; otherwise use raw
+43 -15
View File
@@ -1,8 +1,10 @@
name: Generate Release Notes
on:
release:
types: [published]
workflow_run:
workflows: ["Release"]
types:
- completed
permissions:
contents: write
@@ -11,19 +13,40 @@ permissions:
jobs:
generate-release-notes:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') && !github.event.release.prerelease
if: github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'v')
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 #v6.0.0
with:
fetch-depth: 0 # Fetch full history to compare with previous release
fetch-depth: 0
- name: Get release info
id: get-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ github.event.workflow_run.head_branch }}
run: |
echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT
# Get release info by tag
RELEASE_INFO=$(gh api /repos/${{ github.repository }}/releases/tags/$TAG_NAME)
RELEASE_ID=$(echo "$RELEASE_INFO" | jq -r '.id')
IS_PRERELEASE=$(echo "$RELEASE_INFO" | jq -r '.prerelease')
echo "release-id=$RELEASE_ID" >> $GITHUB_OUTPUT
echo "is-prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT
if [ "$IS_PRERELEASE" = "true" ]; then
echo "Skipping release notes generation for prerelease"
fi
- name: Get previous release tag
id: get-previous-tag
if: steps.get-release.outputs.is-prerelease == 'false'
env:
CURRENT_TAG: ${{ steps.get-release.outputs.tag-name }}
run: |
# Get the previous release tag (excluding the current one)
CURRENT_TAG="${{ github.ref_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
@@ -38,16 +61,16 @@ jobs:
- name: Get commit messages between releases
id: get-commits
if: steps.get-release.outputs.is-prerelease == 'false'
env:
PREVIOUS_TAG: ${{ steps.get-previous-tag.outputs.previous-tag }}
CURRENT_TAG: ${{ steps.get-previous-tag.outputs.current-tag }}
run: |
# Get commit messages between previous and current release
PREVIOUS_TAG="${{ steps.get-previous-tag.outputs.previous-tag }}"
CURRENT_TAG="${{ steps.get-previous-tag.outputs.current-tag }}"
# Get commit log with detailed format
COMMIT_LOG=$(git log --pretty=format:"- %s (%h by %an)" $PREVIOUS_TAG..$CURRENT_TAG --no-merges)
COMMIT_LOG=$(git log --pretty=format:"- %s (%h by %an)" "$PREVIOUS_TAG".."$CURRENT_TAG" --no-merges)
# Get changed files summary
CHANGED_FILES=$(git diff --name-status $PREVIOUS_TAG..$CURRENT_TAG | head -20)
CHANGED_FILES=$(git diff --name-status "$PREVIOUS_TAG".."$CURRENT_TAG" | head -20)
# Save to files for AI processing
echo "$COMMIT_LOG" > commits.txt
@@ -58,6 +81,7 @@ jobs:
- name: Generate release notes with AI
id: generate-notes
if: steps.get-release.outputs.is-prerelease == 'false'
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
with:
prompt-file: commits.txt
@@ -101,23 +125,27 @@ jobs:
model: gpt-4o
- name: Update release with generated notes
if: steps.get-release.outputs.is-prerelease == 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RESPONSE_FILE: ${{ steps.generate-notes.outputs.response-file }}
RESPONSE_OUTPUT: ${{ steps.generate-notes.outputs.response }}
RELEASE_ID: ${{ steps.get-release.outputs.release-id }}
run: |
# Prefer reading from the response file to avoid output truncation
RESPONSE_FILE='${{ steps.generate-notes.outputs.response-file }}'
if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then
RELEASE_NOTES=$(cat "$RESPONSE_FILE")
else
RELEASE_NOTES='${{ steps.generate-notes.outputs.response }}'
RELEASE_NOTES="$RESPONSE_OUTPUT"
fi
# Update the release with the generated notes
gh api --method PATCH /repos/${{ github.repository }}/releases/${{ github.event.release.id }} \
gh api --method PATCH /repos/${{ github.repository }}/releases/"$RELEASE_ID" \
--field body="$RELEASE_NOTES"
echo "✅ Release notes updated successfully!"
- name: Cleanup
if: always()
run: |
rm -f commits.txt changes.txt
+9 -3
View File
@@ -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",
@@ -101,13 +105,12 @@
"mstone",
"msvc",
"msys",
"Mullvad",
"mullvadbrowser",
"mypy",
"noarchive",
"nobrowse",
"noconfirm",
"nodecar",
"NODELAY",
"nodemon",
"norestart",
"NSIS",
@@ -139,6 +142,7 @@
"pyoxidizer",
"pytest",
"pyyaml",
"reportingpolicy",
"reqwest",
"ridedott",
"rlib",
@@ -149,6 +153,7 @@
"screeninfo",
"selectables",
"serde",
"sessionstore",
"setpriority",
"setsid",
"SETTINGCHANGE",
@@ -181,9 +186,9 @@
"timedatectl",
"titlebar",
"tkinter",
"Torbrowser",
"tqdm",
"trackingprotection",
"trailhead",
"turbopack",
"turtledemo",
"udeps",
@@ -192,6 +197,7 @@
"unrs",
"urlencoding",
"urllib",
"utoipa",
"venv",
"vercel",
"VERYSILENT",
+10
View File
@@ -12,6 +12,16 @@ Do keep in mind before you start working on an issue / posting a PR:
- Confirm if other contributors are working on the same issue
- Check if the feature aligns with our roadmap and project goals
## Contributor License Agreement
By contributing to Donut Browser, you agree that your contributions will be licensed under the same terms as the project. You must agree to our [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md) before your contributions can be accepted. This agreement ensures that:
- Your contributions can be used in the open source version of Donut Browser (licensed under AGPL-3.0)
- Donut Browser can offer commercial licenses for the software, including your contributions
- You retain all rights to use your contributions for any other purpose
When you submit your first pull request, you acknowledge that you agree to the terms of the Contributor License Agreement.
## Tips & Things to Consider
- PRs with tests are highly appreciated
+15
View File
@@ -0,0 +1,15 @@
# Donut Browser Software Grant and Contributor License Agreement ("Agreement")
This agreement is based on the Apache Software Foundation Contributor License Agreement. (v r190612)
Thank you for your interest in the Donut Browser project ("Donut Browser" or "the Project"). In order to clarify the intellectual property license granted with Contributions from any person or entity, Donut Browser must have a Contributor License Agreement (CLA) on file that has been agreed to by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Donut Browser and its users; it does not change your rights to use your own Contributions for any other purpose. This Agreement allows an individual to contribute to Donut Browser on that individual's own behalf, or an entity (the "Corporation") to submit Contributions to Donut Browser, to authorize Contributions submitted by its designated employees to Donut Browser, and to grant copyright and patent licenses thereto.
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Donut Browser. Except for the license granted herein to Donut Browser and recipients of software distributed by Donut Browser, You reserve all right, title, and interest in and to Your Contributions.
1. Definitions. "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Donut Browser. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "Contribution" shall mean any work, as well as any modifications or additions to an existing work, that is intentionally submitted by You to Donut Browser for inclusion in, or documentation of, any of the products owned or managed by Donut Browser (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Donut Browser or its representatives, including but not limited to communication on electronic mailing lists, source code control systems (such as GitHub), and issue tracking systems that are managed by, or on behalf of, Donut Browser for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Donut Browser and to recipients of software distributed by Donut Browser a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works under any license terms, including but not limited to the GNU Affero General Public License version 3 (AGPL-3.0) and any commercial or proprietary license terms that Donut Browser may choose to offer. This grant includes the right for Donut Browser to offer the Work, including Your Contributions, under multiple licenses simultaneously (dual or multi-licensing), including both open source and commercial licenses.
3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Donut Browser and to recipients of software distributed by Donut Browser a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) were submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
4. You represent that You are legally entitled to grant the above license. If You are an individual, and if Your employer(s) has rights to intellectual property that you create that includes Your Contributions, you represent that You have received permission to make Contributions on behalf of that employer, or that Your employer has waived such rights for your Contributions to Donut Browser. If You are a Corporation, any individual who makes a contribution from an account associated with You will be considered authorized to Contribute on Your behalf.
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may submit it to Donut Browser separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
+3
View File
@@ -29,6 +29,9 @@
}
},
"css": {
"parser": {
"tailwindDirectives": true
},
"formatter": {
"quoteStyle": "double"
}
+1 -1
View File
@@ -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.
+4 -4
View File
@@ -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"
+12 -12
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.13.0",
"version": "0.13.7",
"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,32 +51,32 @@
"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"
},
"devDependencies": {
"@biomejs/biome": "2.2.3",
"@biomejs/biome": "2.3.8",
"@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",
+1008 -990
View File
File diff suppressed because it is too large Load Diff
+54 -1
View File
@@ -1293,7 +1293,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.13.0"
version = "0.13.7"
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"
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.13.0"
version = "0.13.7"
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"] }
+4
View File
@@ -42,6 +42,10 @@ fn main() {
println!("cargo:rerun-if-changed=src/proxy_runner.rs");
println!("cargo:rerun-if-changed=src/proxy_storage.rs");
// Tell Cargo to rebuild when binaries directory contents change
// This ensures tauri_build is re-run after sidecar binaries are copied
println!("cargo:rerun-if-changed=binaries");
// Only run tauri_build if all external binaries exist
// This allows building donut-proxy sidecar without the other binaries present
if external_binaries_exist() {
+10 -1
View File
@@ -3,9 +3,18 @@
"identifier": "default",
"description": "enables the default permissions",
"windows": ["main"],
"webviews": ["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",
-385
View File
@@ -292,7 +292,6 @@ pub fn is_browser_version_nightly(
// This will be handled in the API parsing, so this fallback is for cached versions
is_nightly_version(version)
}
"mullvad-browser" | "tor-browser" => is_nightly_version(version),
"chromium" => {
// Chromium builds are generally stable snapshots
false
@@ -349,7 +348,6 @@ pub struct ApiClient {
firefox_dev_api_base: String,
github_api_base: String,
chromium_api_base: String,
tor_archive_base: String,
}
impl ApiClient {
@@ -366,7 +364,6 @@ impl ApiClient {
github_api_base: "https://api.github.com".to_string(),
chromium_api_base: "https://commondatastorage.googleapis.com/chromium-browser-snapshots"
.to_string(),
tor_archive_base: "https://archive.torproject.org/tor-package-archive/torbrowser".to_string(),
}
}
@@ -439,7 +436,6 @@ impl ApiClient {
firefox_dev_api_base: String,
github_api_base: String,
chromium_api_base: String,
tor_archive_base: String,
) -> Self {
Self {
client: Client::new(),
@@ -447,7 +443,6 @@ impl ApiClient {
firefox_dev_api_base,
github_api_base,
chromium_api_base,
tor_archive_base,
}
}
@@ -564,7 +559,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)
}
@@ -724,45 +718,6 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_mullvad_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_github_releases("mullvad") {
return Ok(cached_releases);
}
}
log::info!("Fetching Mullvad releases from GitHub API");
let base_url = format!(
"{}/repos/mullvad/mullvad-browser/releases",
self.github_api_base
);
let releases = self.fetch_github_releases_multiple_pages(&base_url).await?;
let mut releases: Vec<GithubRelease> = releases
.into_iter()
.map(|mut release| {
release.is_nightly = release.prerelease;
release
})
.collect();
// Sort releases using the new version sorting system
sort_github_releases(&mut releases);
// Cache the results (unless bypassing cache)
if !no_caching {
if let Err(e) = self.save_cached_github_releases("mullvad", &releases) {
log::error!("Failed to cache Mullvad releases: {e}");
}
}
Ok(releases)
}
pub async fn fetch_zen_releases_with_caching(
&self,
no_caching: bool,
@@ -1103,107 +1058,6 @@ impl ApiClient {
Ok(compatible_releases)
}
pub async fn fetch_tor_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_versions("tor-browser") {
return Ok(cached_releases);
}
}
log::info!("Fetching TOR releases from archive...");
let url = format!("{}/", self.tor_archive_base);
let html = self
.client
.get(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
.send()
.await?
.text()
.await?;
// Parse HTML to extract version directories
let mut version_candidates = Vec::new();
// Look for directory links in the HTML
for line in html.lines() {
if line.contains("<a href=\"") && line.contains("/\">") {
// Extract the directory name from the href attribute
if let Some(start) = line.find("<a href=\"") {
let start = start + 9; // Length of "<a href=\""
if let Some(end) = line[start..].find("/\">") {
let version = &line[start..start + end];
// Skip parent directory and non-version entries
if version != ".."
&& !version.is_empty()
&& version.chars().next().unwrap_or('a').is_ascii_digit()
{
version_candidates.push(version.to_string());
}
}
}
}
}
// Sort version candidates using the new version sorting system
sort_versions(&mut version_candidates);
// Only check the first 10 versions to avoid being too slow
let mut version_strings = Vec::new();
for version in version_candidates.into_iter().take(10) {
// Check if this version has a macOS DMG file
if let Ok(has_macos) = self.check_tor_version_has_macos(&version).await {
if has_macos {
version_strings.push(version);
}
}
// Add a small delay to avoid overwhelming the server
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
// Convert to BrowserRelease objects
let releases: Vec<BrowserRelease> = version_strings
.into_iter()
.map(|version| BrowserRelease {
version: version.clone(),
date: "".to_string(), // TOR archive doesn't provide structured dates
is_prerelease: false, // Assume all archived versions are stable
})
.collect();
// Cache the results (unless bypassing cache)
if !no_caching {
if let Err(e) = self.save_cached_versions("tor-browser", &releases) {
log::error!("Failed to cache TOR versions: {e}");
}
}
Ok(releases)
}
async fn check_tor_version_has_macos(
&self,
version: &str,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("{}/{version}/", self.tor_archive_base);
let html = self
.client
.get(&url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
.send()
.await?
.text()
.await?;
// Check if there's a macOS DMG file in this version directory
Ok(html.contains("tor-browser-macos-") && html.contains(".dmg"))
}
/// Check if a Zen twilight release has been updated by comparing file size
pub async fn check_twilight_update(
&self,
@@ -1303,7 +1157,6 @@ mod tests {
base_url.clone(), // firefox_dev_api_base
base_url.clone(), // github_api_base
base_url.clone(), // chromium_api_base
base_url.clone(), // tor_archive_base
)
}
@@ -1527,47 +1380,6 @@ mod tests {
assert_eq!(releases[0].version, "140.0b1");
}
#[tokio::test]
async fn test_mullvad_api() {
let server = setup_mock_server().await;
let client = create_test_client(&server);
let mock_response = r#"[
{
"tag_name": "14.5a6",
"name": "Mullvad Browser 14.5a6",
"prerelease": true,
"published_at": "2024-01-15T10:00:00Z",
"assets": [
{
"name": "mullvad-browser-macos-14.5a6.dmg",
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg",
"size": 100000000
}
]
}
]"#;
Mock::given(method("GET"))
.and(path("/repos/mullvad/mullvad-browser/releases"))
.and(query_param("per_page", "100"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_response)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let result = client.fetch_mullvad_releases_with_caching(true).await;
assert!(result.is_ok());
let releases = result.unwrap();
assert!(!releases.is_empty());
assert_eq!(releases[0].tag_name, "14.5a6");
assert!(releases[0].is_nightly);
}
#[tokio::test]
async fn test_zen_api() {
let server = setup_mock_server().await;
@@ -1721,125 +1533,6 @@ mod tests {
assert!(!releases[0].is_prerelease);
}
#[tokio::test]
async fn test_tor_api() {
let server = setup_mock_server().await;
let client = create_test_client(&server);
let mock_html = r#"
<html>
<body>
<a href="../">../</a>
<a href="14.0.4/">14.0.4/</a>
<a href="14.0.3/">14.0.3/</a>
</body>
</html>
"#;
let version_html = r#"
<html>
<body>
<a href="tor-browser-macos-14.0.4.dmg">tor-browser-macos-14.0.4.dmg</a>
</body>
</html>
"#;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_html)
.insert_header("content-type", "text/html"),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/14.0.4/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(version_html)
.insert_header("content-type", "text/html"),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/14.0.3/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(version_html.replace("14.0.4", "14.0.3"))
.insert_header("content-type", "text/html"),
)
.mount(&server)
.await;
let result = client.fetch_tor_releases_with_caching(true).await;
assert!(result.is_ok());
let releases = result.unwrap();
assert!(!releases.is_empty());
assert_eq!(releases[0].version, "14.0.4");
}
#[tokio::test]
async fn test_tor_version_check() {
let server = setup_mock_server().await;
let client = create_test_client(&server);
let version_html = r#"
<html>
<body>
<a href="tor-browser-macos-14.0.4.dmg">tor-browser-macos-14.0.4.dmg</a>
</body>
</html>
"#;
Mock::given(method("GET"))
.and(path("/14.0.4/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(version_html)
.insert_header("content-type", "text/html"),
)
.mount(&server)
.await;
let result = client.check_tor_version_has_macos("14.0.4").await;
assert!(result.is_ok());
assert!(result.unwrap());
}
#[tokio::test]
async fn test_tor_version_check_no_macos() {
let server = setup_mock_server().await;
let client = create_test_client(&server);
let version_html = r#"
<html>
<body>
<a href="tor-browser-linux-14.0.4.tar.xz">tor-browser-linux-14.0.4.tar.xz</a>
</body>
</html>
"#;
Mock::given(method("GET"))
.and(path("/14.0.5/"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(version_html)
.insert_header("content-type", "text/html"),
)
.mount(&server)
.await;
let result = client.check_tor_version_has_macos("14.0.5").await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_is_nightly_version() {
assert!(is_nightly_version("1.2.3a1"));
@@ -1911,84 +1604,6 @@ mod tests {
assert!(result.is_err());
}
#[tokio::test]
async fn test_mullvad_pagination_two_pages() {
let server = setup_mock_server().await;
let client = create_test_client(&server);
// Page 1 response with Link: rel="next" header
let mock_page1 = r#"[
{
"tag_name": "100.0",
"name": "Mullvad Browser 100.0",
"prerelease": false,
"published_at": "2024-07-01T00:00:00Z",
"assets": [
{ "name": "mullvad-browser-macos-100.0.dmg", "browser_download_url": "https://example.com/100.0.dmg", "size": 1 }
]
}
]"#;
// Page 2 response
let mock_page2 = r#"[
{
"tag_name": "99.0",
"name": "Mullvad Browser 99.0",
"prerelease": false,
"published_at": "2024-06-01T00:00:00Z",
"assets": [
{ "name": "mullvad-browser-macos-99.0.dmg", "browser_download_url": "https://example.com/99.0.dmg", "size": 1 }
]
}
]"#;
// Mock page 1
Mock::given(method("GET"))
.and(path("/repos/mullvad/mullvad-browser/releases"))
.and(query_param("per_page", "100"))
.and(query_param("page", "1"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_page1)
.insert_header("content-type", "application/json")
.insert_header(
"link",
format!(
"<{}?per_page=100&page=2>; rel=\"next\", <{}?per_page=100&page=2>; rel=\"last\"",
server.uri().to_string() + "/repos/mullvad/mullvad-browser/releases",
server.uri().to_string() + "/repos/mullvad/mullvad-browser/releases"
),
),
)
.mount(&server)
.await;
// Mock page 2
Mock::given(method("GET"))
.and(path("/repos/mullvad/mullvad-browser/releases"))
.and(query_param("per_page", "100"))
.and(query_param("page", "2"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(mock_page2)
.insert_header("content-type", "application/json"),
)
.mount(&server)
.await;
let result = client.fetch_mullvad_releases_with_caching(true).await;
assert!(result.is_ok());
let releases = result.unwrap();
// We currently only fetch 1 page intentionally; ensure we at least got page 1
assert_eq!(
releases.len(),
1,
"Should fetch only the first page of results"
);
assert_eq!(releases[0].tag_name, "100.0");
}
#[test]
fn test_camoufox_beta_version_parsing() {
// Test specific Camoufox beta versions that are causing issues
+33 -1
View File
@@ -784,6 +784,20 @@ impl AppAutoUpdater {
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let file_path = dest_dir.join(filename);
// First, try to get the file size via HEAD request
// This is more reliable than GET content-length for some CDN configurations
// especially when dealing with redirects (like GitHub releases)
let head_size = self
.client
.head(download_url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
.send()
.await
.ok()
.and_then(|r| r.content_length());
log::info!("HEAD request for download size: {:?} bytes", head_size);
let response = self
.client
.get(download_url)
@@ -795,7 +809,9 @@ impl AppAutoUpdater {
return Err(format!("Download failed with status: {}", response.status()).into());
}
let total_size = response.content_length().unwrap_or(0);
// Use HEAD size if available, otherwise fall back to GET content-length
let total_size = head_size.or(response.content_length()).unwrap_or(0);
log::info!("Final download size: {} bytes", total_size);
let mut file = fs::File::create(&file_path)?;
let mut stream = response.bytes_stream();
let mut downloaded = 0u64;
@@ -999,6 +1015,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(())
}
+6
View File
@@ -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);
+13 -95
View File
@@ -13,39 +13,33 @@ pub struct ProxySettings {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum BrowserType {
MullvadBrowser,
Chromium,
Firefox,
FirefoxDeveloper,
Brave,
Zen,
TorBrowser,
Camoufox,
}
impl BrowserType {
pub fn as_str(&self) -> &'static str {
match self {
BrowserType::MullvadBrowser => "mullvad-browser",
BrowserType::Chromium => "chromium",
BrowserType::Firefox => "firefox",
BrowserType::FirefoxDeveloper => "firefox-developer",
BrowserType::Brave => "brave",
BrowserType::Zen => "zen",
BrowserType::TorBrowser => "tor-browser",
BrowserType::Camoufox => "camoufox",
}
}
pub fn from_str(s: &str) -> Result<Self, String> {
match s {
"mullvad-browser" => Ok(BrowserType::MullvadBrowser),
"chromium" => Ok(BrowserType::Chromium),
"firefox" => Ok(BrowserType::Firefox),
"firefox-developer" => Ok(BrowserType::FirefoxDeveloper),
"brave" => Ok(BrowserType::Brave),
"zen" => Ok(BrowserType::Zen),
"tor-browser" => Ok(BrowserType::TorBrowser),
"camoufox" => Ok(BrowserType::Camoufox),
_ => Err(format!("Unknown browser type: {s}")),
}
@@ -92,9 +86,7 @@ mod macos {
let binding = entry.file_name();
let name = binding.to_string_lossy();
name.starts_with("firefox")
|| name.starts_with("mullvad")
|| name.starts_with("zen")
|| name.starts_with("tor")
|| name.starts_with("camoufox")
|| name.contains("Browser")
})
@@ -190,28 +182,9 @@ mod linux {
browser_subdir.join("firefox"),
browser_subdir.join("firefox-bin"),
],
BrowserType::MullvadBrowser => {
vec![
browser_subdir.join("firefox"),
browser_subdir.join("mullvad-browser"),
browser_subdir.join("firefox-bin"),
]
}
BrowserType::Zen => {
vec![browser_subdir.join("zen"), browser_subdir.join("zen-bin")]
}
BrowserType::TorBrowser => {
vec![
// Common Tor Browser launchers
browser_subdir.join("tor-browser"),
// Firefox-based binaries
browser_subdir.join("firefox"),
browser_subdir.join("firefox-bin"),
// Sometimes packaged similarly to Firefox
install_dir.join("firefox").join("firefox"),
install_dir.join("firefox").join("firefox-bin"),
]
}
BrowserType::Camoufox => {
vec![
install_dir.join("camoufox-bin"),
@@ -303,23 +276,9 @@ mod linux {
install_dir.join("firefox").join("firefox"),
]
}
BrowserType::MullvadBrowser => {
vec![
browser_subdir.join("mullvad-browser"),
browser_subdir.join("firefox-bin"),
browser_subdir.join("firefox"),
]
}
BrowserType::Zen => {
vec![browser_subdir.join("zen"), browser_subdir.join("zen-bin")]
}
BrowserType::TorBrowser => {
vec![
browser_subdir.join("tor-browser"),
browser_subdir.join("firefox-bin"),
browser_subdir.join("firefox"),
]
}
BrowserType::Camoufox => {
vec![
install_dir.join("camoufox-bin"),
@@ -424,9 +383,7 @@ mod windows {
if path.extension().is_some_and(|ext| ext == "exe") {
let name = path.file_stem().unwrap_or_default().to_string_lossy();
if name.starts_with("firefox")
|| name.starts_with("mullvad")
|| name.starts_with("zen")
|| name.starts_with("tor")
|| name.starts_with("camoufox")
|| name.contains("browser")
{
@@ -510,9 +467,7 @@ mod windows {
if path.extension().is_some_and(|ext| ext == "exe") {
let name = path.file_stem().unwrap_or_default().to_string_lossy();
if name.starts_with("firefox")
|| name.starts_with("mullvad")
|| name.starts_with("zen")
|| name.starts_with("tor")
|| name.starts_with("camoufox")
|| name.contains("browser")
{
@@ -624,22 +579,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 +679,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
@@ -910,11 +858,9 @@ impl BrowserFactory {
pub fn create_browser(&self, browser_type: BrowserType) -> Box<dyn Browser> {
match browser_type {
BrowserType::MullvadBrowser
| BrowserType::Firefox
| BrowserType::FirefoxDeveloper
| BrowserType::Zen
| BrowserType::TorBrowser => Box::new(FirefoxBrowser::new(browser_type)),
BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => {
Box::new(FirefoxBrowser::new(browser_type))
}
BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)),
BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()),
}
@@ -992,20 +938,14 @@ mod tests {
#[test]
fn test_browser_type_conversions() {
// Test as_str
assert_eq!(BrowserType::MullvadBrowser.as_str(), "mullvad-browser");
assert_eq!(BrowserType::Firefox.as_str(), "firefox");
assert_eq!(BrowserType::FirefoxDeveloper.as_str(), "firefox-developer");
assert_eq!(BrowserType::Chromium.as_str(), "chromium");
assert_eq!(BrowserType::Brave.as_str(), "brave");
assert_eq!(BrowserType::Zen.as_str(), "zen");
assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser");
assert_eq!(BrowserType::Camoufox.as_str(), "camoufox");
// Test from_str - use expect with descriptive messages instead of unwrap
assert_eq!(
BrowserType::from_str("mullvad-browser").expect("mullvad-browser should be valid"),
BrowserType::MullvadBrowser
);
assert_eq!(
BrowserType::from_str("firefox").expect("firefox should be valid"),
BrowserType::Firefox
@@ -1026,10 +966,6 @@ mod tests {
BrowserType::from_str("zen").expect("zen should be valid"),
BrowserType::Zen
);
assert_eq!(
BrowserType::from_str("tor-browser").expect("tor-browser should be valid"),
BrowserType::TorBrowser
);
assert_eq!(
BrowserType::from_str("camoufox").expect("camoufox should be valid"),
BrowserType::Camoufox
@@ -1096,30 +1032,12 @@ mod tests {
"Firefox should include debugging port"
);
// Test Mullvad Browser (should always use -no-remote)
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"]);
// Test Tor Browser (should always use -no-remote)
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"]);
// 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
+119 -237
View File
@@ -33,29 +33,6 @@ impl BrowserRunner {
&BROWSER_RUNNER
}
// Helper function to check if a process matches TOR/Mullvad browser
fn is_tor_or_mullvad_browser(
&self,
exe_name: &str,
cmd: &[std::ffi::OsString],
browser_type: &str,
) -> bool {
#[cfg(target_os = "macos")]
return platform_browser::macos::is_tor_or_mullvad_browser(exe_name, cmd, browser_type);
#[cfg(target_os = "windows")]
return platform_browser::windows::is_tor_or_mullvad_browser(exe_name, cmd, browser_type);
#[cfg(target_os = "linux")]
return platform_browser::linux::is_tor_or_mullvad_browser(exe_name, cmd, browser_type);
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
let _ = (exe_name, cmd, browser_type);
false
}
}
pub fn get_binaries_dir(&self) -> PathBuf {
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
@@ -395,148 +372,117 @@ impl BrowserRunner {
profile.id
);
// For TOR and Mullvad browsers, we need to find the actual browser process
// because they use launcher scripts that spawn the real browser process
let mut actual_pid = launcher_pid;
if matches!(
browser_type,
BrowserType::TorBrowser | BrowserType::MullvadBrowser
) {
// Wait a moment for the actual browser process to start
tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await;
// Find the actual browser process
let system = System::new_all();
for (pid, process) in system.processes() {
let process_name = process.name().to_str().unwrap_or("");
let process_cmd = process.cmd();
let pid_u32 = pid.as_u32();
// Skip if this is the launcher process itself
if pid_u32 == launcher_pid {
continue;
}
if self.is_tor_or_mullvad_browser(process_name, process_cmd, &profile.browser) {
log::info!(
"Found actual {} browser process: PID {} ({})",
profile.browser,
pid_u32,
process_name
);
actual_pid = pid_u32;
break;
}
}
}
// On macOS, when launching via `open -a`, the child PID is the `open` helper.
// Resolve and store the actual browser PID for all browser types.
#[cfg(target_os = "macos")]
{
// Give the browser a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
let actual_pid = {
#[cfg(target_os = "macos")]
{
// Give the browser a moment to start
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
let system = System::new_all();
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_data_path_str = profile_data_path.to_string_lossy();
let system = System::new_all();
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let profile_data_path_str = profile_data_path.to_string_lossy();
for (pid, process) in system.processes() {
let cmd = process.cmd();
if cmd.is_empty() {
continue;
}
let mut resolved_pid = launcher_pid;
// Determine if this process matches the intended browser type
let exe_name_lower = process.name().to_string_lossy().to_lowercase();
let is_correct_browser = match profile.browser.as_str() {
"firefox" => {
exe_name_lower.contains("firefox")
&& !exe_name_lower.contains("developer")
&& !exe_name_lower.contains("tor")
&& !exe_name_lower.contains("mullvad")
&& !exe_name_lower.contains("camoufox")
for (pid, process) in system.processes() {
let cmd = process.cmd();
if cmd.is_empty() {
continue;
}
"firefox-developer" => {
// More flexible detection for Firefox Developer Edition
(exe_name_lower.contains("firefox") && exe_name_lower.contains("developer"))
|| (exe_name_lower.contains("firefox")
&& cmd.iter().any(|arg| {
let arg_str = arg.to_str().unwrap_or("");
arg_str.contains("Developer")
|| arg_str.contains("developer")
|| arg_str.contains("FirefoxDeveloperEdition")
|| arg_str.contains("firefox-developer")
}))
|| exe_name_lower == "firefox" // Firefox Developer might just show as "firefox"
}
"mullvad-browser" => {
self.is_tor_or_mullvad_browser(&exe_name_lower, cmd, "mullvad-browser")
}
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name_lower, cmd, "tor-browser"),
"zen" => exe_name_lower.contains("zen"),
"chromium" => exe_name_lower.contains("chromium") || exe_name_lower.contains("chrome"),
"brave" => exe_name_lower.contains("brave") || exe_name_lower.contains("Brave"),
_ => false,
};
if !is_correct_browser {
continue;
}
// Determine if this process matches the intended browser type
let exe_name_lower = process.name().to_string_lossy().to_lowercase();
let is_correct_browser = match profile.browser.as_str() {
"firefox" => {
exe_name_lower.contains("firefox")
&& !exe_name_lower.contains("developer")
&& !exe_name_lower.contains("camoufox")
}
"firefox-developer" => {
// More flexible detection for Firefox Developer Edition
(exe_name_lower.contains("firefox") && exe_name_lower.contains("developer"))
|| (exe_name_lower.contains("firefox")
&& cmd.iter().any(|arg| {
let arg_str = arg.to_str().unwrap_or("");
arg_str.contains("Developer")
|| arg_str.contains("developer")
|| arg_str.contains("FirefoxDeveloperEdition")
|| arg_str.contains("firefox-developer")
}))
|| exe_name_lower == "firefox" // Firefox Developer might just show as "firefox"
}
"zen" => exe_name_lower.contains("zen"),
"chromium" => exe_name_lower.contains("chromium") || exe_name_lower.contains("chrome"),
"brave" => exe_name_lower.contains("brave") || exe_name_lower.contains("Brave"),
_ => false,
};
// Check for profile path match
let profile_path_match = if matches!(
profile.browser.as_str(),
"firefox" | "firefox-developer" | "tor-browser" | "mullvad-browser" | "zen"
) {
// Firefox-based browsers: look for -profile argument followed by path
let mut found_profile_arg = false;
for (i, arg) in cmd.iter().enumerate() {
if let Some(arg_str) = arg.to_str() {
if arg_str == "-profile" && i + 1 < cmd.len() {
if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) {
if next_arg == profile_data_path_str {
found_profile_arg = true;
break;
if !is_correct_browser {
continue;
}
// Check for profile path match
let profile_path_match = if matches!(
profile.browser.as_str(),
"firefox" | "firefox-developer" | "zen"
) {
// Firefox-based browsers: look for -profile argument followed by path
let mut found_profile_arg = false;
for (i, arg) in cmd.iter().enumerate() {
if let Some(arg_str) = arg.to_str() {
if arg_str == "-profile" && i + 1 < cmd.len() {
if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) {
if next_arg == profile_data_path_str {
found_profile_arg = true;
break;
}
}
}
}
// Also check for combined -profile=path format
if arg_str == format!("-profile={profile_data_path_str}") {
found_profile_arg = true;
break;
}
// Check if the argument is the profile path directly
if arg_str == profile_data_path_str {
found_profile_arg = true;
break;
// Also check for combined -profile=path format
if arg_str == format!("-profile={profile_data_path_str}") {
found_profile_arg = true;
break;
}
// Check if the argument is the profile path directly
if arg_str == profile_data_path_str {
found_profile_arg = true;
break;
}
}
}
}
found_profile_arg
} else {
// Chromium-based browsers: look for --user-data-dir argument
cmd.iter().any(|s| {
if let Some(arg) = s.to_str() {
arg == format!("--user-data-dir={profile_data_path_str}")
|| arg == profile_data_path_str
} else {
false
}
})
};
found_profile_arg
} else {
// Chromium-based browsers: look for --user-data-dir argument
cmd.iter().any(|s| {
if let Some(arg) = s.to_str() {
arg == format!("--user-data-dir={profile_data_path_str}")
|| arg == profile_data_path_str
} else {
false
}
})
};
if profile_path_match {
let pid_u32 = pid.as_u32();
if pid_u32 != launcher_pid {
actual_pid = pid_u32;
break;
if profile_path_match {
let pid_u32 = pid.as_u32();
if pid_u32 != launcher_pid {
resolved_pid = pid_u32;
break;
}
}
}
resolved_pid
}
}
#[cfg(not(target_os = "macos"))]
{
launcher_pid
}
};
// Update profile with process info and save
let mut updated_profile = profile.clone();
@@ -552,11 +498,7 @@ impl BrowserRunner {
if profile.proxy_id.is_some()
&& matches!(
browser_type,
BrowserType::Firefox
| BrowserType::FirefoxDeveloper
| BrowserType::Zen
| BrowserType::TorBrowser
| BrowserType::MullvadBrowser
BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen
)
{
// Proxy settings for Firefox-based browsers are applied via user.js file
@@ -714,48 +656,9 @@ impl BrowserRunner {
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
return Err("Unsupported platform".into());
}
BrowserType::MullvadBrowser | BrowserType::TorBrowser => {
#[cfg(target_os = "macos")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::macos::open_url_in_existing_browser_tor_mullvad(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(target_os = "windows")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::windows::open_url_in_existing_browser_tor_mullvad(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(target_os = "linux")]
{
let profiles_dir = self.profile_manager.get_profiles_dir();
return platform_browser::linux::open_url_in_existing_browser_tor_mullvad(
&updated_profile,
url,
browser_type,
&browser_dir,
&profiles_dir,
)
.await;
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
return Err("Unsupported platform".into());
BrowserType::Camoufox => {
// Camoufox uses nodecar for launching, URL opening is handled differently
Err("URL opening in existing Camoufox instance is not supported".into())
}
BrowserType::Chromium | BrowserType::Brave => {
#[cfg(target_os = "macos")]
@@ -800,10 +703,6 @@ impl BrowserRunner {
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
return Err("Unsupported platform".into());
}
BrowserType::Camoufox => {
// This should never be reached due to the early return above, but handle it just in case
Err("Camoufox URL opening should be handled in the early return above".into())
}
}
}
@@ -848,7 +747,7 @@ impl BrowserRunner {
// For Firefox-based browsers, apply PAC/user.js to point to the local proxy
if matches!(
profile.browser.as_str(),
"firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser"
"firefox" | "firefox-developer" | "zen"
) {
let profiles_dir = self.profile_manager.get_profiles_dir();
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
@@ -949,19 +848,6 @@ impl BrowserRunner {
if let Some(url_ref) = url.as_ref() {
log::info!("Opening URL in existing browser: {url_ref}");
// For TOR/Mullvad browsers, add extra verification
if matches!(
final_profile.browser.as_str(),
"tor-browser" | "mullvad-browser"
) {
log::info!("TOR/Mullvad browser detected - ensuring we have correct PID");
if final_profile.process_id.is_none() {
log::info!(
"ERROR: No PID found for running TOR/Mullvad browser - this should not happen"
);
return Err("No PID found for running browser".into());
}
}
match self
.open_url_in_existing_browser(
app_handle.clone(),
@@ -978,18 +864,22 @@ impl BrowserRunner {
Err(e) => {
log::info!("Failed to open URL in existing browser: {e}");
// For Mullvad and Tor browsers, don't fall back to new instance since they use -no-remote
// and can't have multiple instances with the same profile
match final_profile.browser.as_str() {
"mullvad-browser" | "tor-browser" => {
Err(format!("Failed to open URL in existing {} browser. Cannot launch new instance due to profile conflict: {}", final_profile.browser, e).into())
}
_ => {
log::info!("Falling back to new instance for browser: {}", final_profile.browser);
// Fallback to launching a new instance for other browsers
self.launch_browser_internal(app_handle.clone(), &final_profile, url, internal_proxy_settings, None, false).await
}
}
// Fall back to launching a new instance
log::info!(
"Falling back to new instance for browser: {}",
final_profile.browser
);
// Fallback to launching a new instance for other browsers
self
.launch_browser_internal(
app_handle.clone(),
&final_profile,
url,
internal_proxy_settings,
None,
false,
)
.await
}
}
} else {
@@ -1316,8 +1206,6 @@ impl BrowserRunner {
"firefox" => {
exe_name.contains("firefox")
&& !exe_name.contains("developer")
&& !exe_name.contains("tor")
&& !exe_name.contains("mullvad")
&& !exe_name.contains("camoufox")
}
"firefox-developer" => {
@@ -1333,8 +1221,6 @@ impl BrowserRunner {
}))
|| exe_name == "firefox" // Firefox Developer might just show as "firefox"
}
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"),
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"),
"zen" => exe_name.contains("zen"),
"chromium" => exe_name.contains("chromium") || exe_name.contains("chrome"),
"brave" => exe_name.contains("brave") || exe_name.contains("Brave"),
@@ -1349,7 +1235,7 @@ impl BrowserRunner {
let profile_path_match = if matches!(
profile.browser.as_str(),
"firefox" | "firefox-developer" | "tor-browser" | "mullvad-browser" | "zen"
"firefox" | "firefox-developer" | "zen"
) {
// Firefox-based browsers: look for -profile argument followed by path
let mut found_profile_arg = false;
@@ -1598,8 +1484,6 @@ impl BrowserRunner {
"firefox" => {
exe_name.contains("firefox")
&& !exe_name.contains("developer")
&& !exe_name.contains("tor")
&& !exe_name.contains("mullvad")
&& !exe_name.contains("camoufox")
}
"firefox-developer" => {
@@ -1615,8 +1499,6 @@ impl BrowserRunner {
}))
|| exe_name == "firefox" // Firefox Developer might just show as "firefox"
}
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"),
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"),
"zen" => exe_name.contains("zen"),
"chromium" => exe_name.contains("chromium") || exe_name.contains("chrome"),
"brave" => exe_name.contains("brave") || exe_name.contains("Brave"),
@@ -1630,7 +1512,7 @@ impl BrowserRunner {
// Check for profile path match with improved logic
let profile_path_match = if matches!(
profile.browser.as_str(),
"firefox" | "firefox-developer" | "tor-browser" | "mullvad-browser" | "zen"
"firefox" | "firefox-developer" | "zen"
) {
// Firefox-based browsers: look for -profile argument followed by path
let mut found_profile_arg = false;
@@ -1792,7 +1674,7 @@ pub async fn launch_browser_profile(
// For Firefox-based browsers, always apply PAC/user.js to point to the local proxy
if matches!(
profile_for_launch.browser.as_str(),
"firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser"
"firefox" | "firefox-developer" | "zen"
) {
let profiles_dir = browser_runner.profile_manager.get_profiles_dir();
let profile_path = profiles_dir
-250
View File
@@ -54,14 +54,6 @@ impl BrowserVersionManager {
match browser {
"firefox" | "firefox-developer" => Ok(true),
"mullvad-browser" => {
// Mullvad doesn't support ARM64 on Windows and Linux
if arch == "arm64" && (os == "windows" || os == "linux") {
Ok(false)
} else {
Ok(true)
}
}
"zen" => {
// Zen supports all platforms and architectures
Ok(true)
@@ -78,14 +70,6 @@ impl BrowserVersionManager {
Ok(true)
}
}
"tor-browser" => {
// TOR Browser doesn't support ARM64 on Windows and Linux
if arch == "arm64" && (os == "windows" || os == "linux") {
Ok(false)
} else {
Ok(true)
}
}
"camoufox" => {
// Camoufox supports all platforms and architectures according to the JS code
Ok(true)
@@ -99,11 +83,9 @@ impl BrowserVersionManager {
let all_browsers = vec![
"firefox",
"firefox-developer",
"mullvad-browser",
"zen",
"brave",
"chromium",
"tor-browser",
"camoufox",
];
@@ -238,11 +220,9 @@ impl BrowserVersionManager {
let fresh_versions = match browser {
"firefox" => self.fetch_firefox_versions(true).await?, // Always fetch fresh for merging
"firefox-developer" => self.fetch_firefox_developer_versions(true).await?,
"mullvad-browser" => self.fetch_mullvad_versions(true).await?,
"zen" => self.fetch_zen_versions(true).await?,
"brave" => self.fetch_brave_versions(true).await?,
"chromium" => self.fetch_chromium_versions(true).await?,
"tor-browser" => self.fetch_tor_versions(true).await?,
"camoufox" => self.fetch_camoufox_versions(true).await?,
_ => return Err(format!("Unsupported browser: {browser}").into()),
};
@@ -356,27 +336,6 @@ impl BrowserVersionManager {
})
.collect()
}
"mullvad-browser" => {
let releases = self.fetch_mullvad_releases_detailed(true).await?;
merged_versions
.into_iter()
.map(|version| {
if let Some(release) = releases.iter().find(|r| r.tag_name == version) {
BrowserVersionInfo {
version: release.tag_name.clone(),
is_prerelease: release.is_nightly,
date: release.published_at.clone(),
}
} else {
BrowserVersionInfo {
version: version.clone(),
is_prerelease: false, // Mullvad usually stable releases
date: "".to_string(),
}
}
})
.collect()
}
"zen" => {
let releases = self.fetch_zen_releases_detailed(true).await?;
merged_versions
@@ -444,31 +403,6 @@ impl BrowserVersionManager {
})
.collect()
}
"tor-browser" => {
let releases = self.fetch_tor_releases_detailed(true).await?;
merged_versions
.into_iter()
.map(|version| {
if let Some(release) = releases.iter().find(|r| r.version == version) {
BrowserVersionInfo {
version: release.version.clone(),
is_prerelease: crate::api_client::is_browser_version_nightly(
"tor-browser",
&release.version,
None,
),
date: release.date.clone(),
}
} else {
BrowserVersionInfo {
version: version.clone(),
is_prerelease: false, // TOR Browser usually stable releases
date: "".to_string(),
}
}
})
.collect()
}
"camoufox" => {
let releases = self.fetch_camoufox_releases_detailed(true).await?;
merged_versions
@@ -602,50 +536,6 @@ impl BrowserVersionManager {
is_archive,
})
}
"mullvad-browser" => {
// Mullvad Browser doesn't support ARM64 on Windows and Linux
if arch == "arm64" && (os == "windows" || os == "linux") {
return Err(format!("Mullvad Browser doesn't support ARM64 on {os}").into());
}
let (platform_str, filename, is_archive) = match os.as_str() {
"windows" => {
if arch == "arm64" {
return Err("Mullvad Browser doesn't support ARM64 on Windows".into());
}
(
"windows-x86_64",
format!("mullvad-browser-windows-x86_64-{version}.exe"),
false,
)
}
"linux" => {
if arch == "arm64" {
return Err("Mullvad Browser doesn't support ARM64 on Linux".into());
}
(
"x86_64",
format!("mullvad-browser-x86_64-{version}.tar.xz"),
true,
)
}
"macos" => (
"macos",
format!("mullvad-browser-macos-{version}.dmg"),
true,
),
_ => return Err(format!("Unsupported platform for Mullvad Browser: {os}").into()),
};
Ok(DownloadInfo {
url: format!(
"https://github.com/mullvad/mullvad-browser/releases/download/{version}/mullvad-browser-{platform_str}-{version}{}",
if os == "windows" { ".exe" } else if os == "linux" { ".tar.xz" } else { ".dmg" }
),
filename,
is_archive,
})
}
"zen" => {
let (asset_name, filename, is_archive) = match (&os[..], &arch[..]) {
("windows", "x64") => ("zen.installer.exe", format!("zen-{version}.exe"), false),
@@ -731,46 +621,6 @@ impl BrowserVersionManager {
is_archive: true,
})
}
"tor-browser" => {
// TOR Browser doesn't support ARM64 on Windows and Linux
if arch == "arm64" && (os == "windows" || os == "linux") {
return Err(format!("TOR Browser doesn't support ARM64 on {os}").into());
}
let (platform_str, filename, is_archive) = match os.as_str() {
"windows" => {
if arch == "arm64" {
return Err("TOR Browser doesn't support ARM64 on Windows".into());
}
(
"windows-x86_64-portable",
format!("tor-browser-windows-x86_64-portable-{version}.exe"),
false,
)
}
"linux" => {
if arch == "arm64" {
return Err("TOR Browser doesn't support ARM64 on Linux".into());
}
(
"linux-x86_64",
format!("tor-browser-linux-x86_64-{version}.tar.xz"),
true,
)
}
"macos" => ("macos", format!("tor-browser-macos-{version}.dmg"), true),
_ => return Err(format!("Unsupported platform for TOR Browser: {os}").into()),
};
Ok(DownloadInfo {
url: format!(
"https://archive.torproject.org/tor-package-archive/torbrowser/{version}/tor-browser-{platform_str}-{version}{}",
if os == "windows" { ".exe" } else if os == "linux" { ".tar.xz" } else { ".dmg" }
),
filename,
is_archive,
})
}
"camoufox" => {
// Camoufox downloads from GitHub releases with pattern: camoufox-{version}-{release}-{os}.{arch}.zip
let (os_name, arch_name) = match (&os[..], &arch[..]) {
@@ -864,24 +714,6 @@ impl BrowserVersionManager {
.await
}
async fn fetch_mullvad_versions(
&self,
no_caching: bool,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let releases = self.fetch_mullvad_releases_detailed(no_caching).await?;
Ok(releases.into_iter().map(|r| r.tag_name).collect())
}
async fn fetch_mullvad_releases_detailed(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.fetch_mullvad_releases_with_caching(no_caching)
.await
}
async fn fetch_zen_versions(
&self,
no_caching: bool,
@@ -971,24 +803,6 @@ impl BrowserVersionManager {
.await
}
async fn fetch_tor_versions(
&self,
no_caching: bool,
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let releases = self.fetch_tor_releases_detailed(no_caching).await?;
Ok(releases.into_iter().map(|r| r.version).collect())
}
async fn fetch_tor_releases_detailed(
&self,
no_caching: bool,
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
self
.api_client
.fetch_tor_releases_with_caching(no_caching)
.await
}
async fn fetch_camoufox_versions(
&self,
no_caching: bool,
@@ -1036,7 +850,6 @@ mod tests {
base_url.clone(), // firefox_dev_api_base
base_url.clone(), // github_api_base
base_url.clone(), // chromium_api_base
base_url.clone(), // tor_archive_base
)
}
@@ -1130,40 +943,6 @@ mod tests {
.url
.contains("/pub/devedition/releases/139.0b1/"));
// Test Mullvad Browser
let mullvad_info = service
.get_download_info("mullvad-browser", "14.5a6")
.unwrap();
#[cfg(target_os = "macos")]
{
assert_eq!(mullvad_info.filename, "mullvad-browser-macos-14.5a6.dmg");
assert!(mullvad_info.url.contains("mullvad-browser-macos-14.5a6"));
assert!(mullvad_info.is_archive);
}
#[cfg(target_os = "linux")]
{
assert_eq!(
mullvad_info.filename,
"mullvad-browser-x86_64-14.5a6.tar.xz"
);
assert!(mullvad_info.url.contains("mullvad-browser-x86_64-14.5a6"));
assert!(mullvad_info.is_archive);
}
#[cfg(target_os = "windows")]
{
assert_eq!(
mullvad_info.filename,
"mullvad-browser-windows-x86_64-14.5a6.exe"
);
assert!(mullvad_info
.url
.contains("mullvad-browser-windows-x86_64-14.5a6"));
assert!(!mullvad_info.is_archive);
}
// Test Zen Browser
let zen_info = service.get_download_info("zen", "1.11b").unwrap();
@@ -1188,35 +967,6 @@ mod tests {
assert!(!zen_info.is_archive);
}
// Test Tor Browser
let tor_info = service.get_download_info("tor-browser", "14.0.4").unwrap();
#[cfg(target_os = "macos")]
{
assert_eq!(tor_info.filename, "tor-browser-macos-14.0.4.dmg");
assert!(tor_info.url.contains("tor-browser-macos-14.0.4"));
assert!(tor_info.is_archive);
}
#[cfg(target_os = "linux")]
{
assert_eq!(tor_info.filename, "tor-browser-linux-x86_64-14.0.4.tar.xz");
assert!(tor_info.url.contains("tor-browser-linux-x86_64-14.0.4"));
assert!(tor_info.is_archive);
}
#[cfg(target_os = "windows")]
{
assert_eq!(
tor_info.filename,
"tor-browser-windows-x86_64-portable-14.0.4.exe"
);
assert!(tor_info
.url
.contains("tor-browser-windows-x86_64-portable-14.0.4"));
assert!(!tor_info.is_archive);
}
// Test Chromium
let chromium_info = service.get_download_info("chromium", "1465660").unwrap();
-86
View File
@@ -145,30 +145,6 @@ impl Downloader {
Ok(asset_url)
}
BrowserType::MullvadBrowser => {
// For Mullvad, verify the asset exists
let releases = self
.api_client
.fetch_mullvad_releases_with_caching(true)
.await?;
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Mullvad version {version} not found"))?;
// Get platform and architecture info
let (os, arch) = Self::get_platform_info();
// Find the appropriate asset
let asset_url = self
.find_mullvad_asset(&release.assets, &os, &arch)
.ok_or(format!(
"No compatible asset found for Mullvad version {version} on {os}/{arch}"
))?;
Ok(asset_url)
}
BrowserType::Camoufox => {
// For Camoufox, verify the asset exists and find the correct download URL
let releases = self
@@ -327,46 +303,6 @@ impl Downloader {
asset.map(|a| a.browser_download_url.clone())
}
/// Find the appropriate Mullvad asset for the current platform and architecture
fn find_mullvad_asset(
&self,
assets: &[crate::browser::GithubAsset],
os: &str,
arch: &str,
) -> Option<String> {
// Mullvad asset naming patterns:
// Windows: mullvad-browser-windows-x86_64-VERSION.exe
// macOS: mullvad-browser-macos-VERSION.dmg
// Linux: mullvad-browser-x86_64-VERSION.tar.xz
let asset = match (os, arch) {
("windows", "x64") => assets.iter().find(|asset| {
asset.name.contains("windows")
&& asset.name.contains("x86_64")
&& asset.name.ends_with(".exe")
}),
("windows", "arm64") => {
// Mullvad doesn't support ARM64 on Windows
None
}
("macos", _) => assets
.iter()
.find(|asset| asset.name.contains("macos") && asset.name.ends_with(".dmg")),
("linux", "x64") => assets.iter().find(|asset| {
asset.name.contains("x86_64")
&& asset.name.ends_with(".tar.xz")
&& !asset.name.contains("windows")
}),
("linux", "arm64") => {
// Mullvad doesn't support ARM64 on Linux
None
}
_ => None,
};
asset.map(|a| a.browser_download_url.clone())
}
/// Find the appropriate Camoufox asset for the current platform and architecture
fn find_camoufox_asset(
&self,
@@ -937,7 +873,6 @@ mod tests {
base_url.clone(), // firefox_dev_api_base
base_url.clone(), // github_api_base
base_url.clone(), // chromium_api_base
base_url.clone(), // tor_archive_base
)
}
@@ -984,27 +919,6 @@ mod tests {
assert_eq!(url, download_info.url);
}
#[tokio::test]
async fn test_resolve_tor_download_url() {
let server = setup_mock_server().await;
let api_client = create_test_api_client(&server);
let downloader = Downloader::new_with_api_client(api_client);
let download_info = DownloadInfo {
url: "https://archive.torproject.org/tor-package-archive/torbrowser/14.0.4/tor-browser-macos-14.0.4.dmg".to_string(),
filename: "tor-test.dmg".to_string(),
is_archive: true,
};
let result = downloader
.resolve_download_url(BrowserType::TorBrowser, "14.0.4", &download_info)
.await;
assert!(result.is_ok());
let url = result.unwrap();
assert_eq!(url, download_info.url);
}
#[tokio::test]
async fn test_download_browser_with_progress() {
let server = setup_mock_server().await;
+1 -23
View File
@@ -868,9 +868,6 @@ impl Extractor {
"chromium.exe",
"zen.exe",
"brave.exe",
"tor-browser.exe",
"tor.exe",
"mullvad-browser.exe",
];
// First try priority executable names
@@ -937,8 +934,6 @@ impl Extractor {
|| file_name.contains("chromium")
|| file_name.contains("zen")
|| file_name.contains("brave")
|| file_name.contains("tor")
|| file_name.contains("mullvad")
|| file_name.contains("browser")
{
return Ok(path);
@@ -1012,15 +1007,6 @@ impl Extractor {
"brave-browser-beta",
"brave-browser-dev",
"brave-bin",
// Tor Browser variants
"tor-browser",
"torbrowser-launcher",
"tor-browser_en-US",
"start-tor-browser",
"Browser/start-tor-browser",
// Mullvad Browser
"mullvad-browser",
"mullvad-browser-bin",
// Camoufox variants
"camoufox",
"camoufox-bin",
@@ -1049,19 +1035,14 @@ impl Extractor {
"chromium",
"brave",
"zen",
"tor-browser",
"mullvad-browser",
"camoufox",
".",
"./",
"firefox",
"mullvad-browser",
"tor-browser_en-US",
"Browser",
"browser",
"opt/google/chrome",
"opt/brave.com/brave",
"opt/mullvad-browser",
"opt/camoufox",
"usr/lib/firefox",
"usr/lib/chromium",
@@ -1159,8 +1140,7 @@ impl Extractor {
|| name_lower.contains("chrome")
|| name_lower.contains("brave")
|| name_lower.contains("zen")
|| name_lower.contains("tor")
|| name_lower.contains("mullvad")
|| name_lower.contains("camoufox")
|| name_lower.ends_with(".appimage")
|| !name_lower.contains('.')
{
@@ -1215,8 +1195,6 @@ impl Extractor {
|| name_lower.contains("chrome")
|| name_lower.contains("brave")
|| name_lower.contains("zen")
|| name_lower.contains("tor")
|| name_lower.contains("mullvad")
|| name_lower.contains("camoufox")
|| file_name.ends_with(".AppImage")
{
+2 -1
View File
@@ -594,7 +594,8 @@ pub fn run() {
// Periodically broadcast browser running status to the frontend
let app_handle_status = app.handle().clone();
tauri::async_runtime::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(500));
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut last_running_states: std::collections::HashMap<String, bool> =
std::collections::HashMap::new();
-248
View File
@@ -1,6 +1,5 @@
use crate::browser::{create_browser, BrowserType};
use crate::profile::BrowserProfile;
use std::ffi::OsString;
use std::path::Path;
use std::process::Command;
@@ -10,40 +9,6 @@ pub mod macos {
use super::*;
use sysinfo::{Pid, System};
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
match browser_type {
"mullvad-browser" => {
let has_mullvad_in_exe = exe_name.contains("mullvad");
let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin");
let has_mullvad_in_cmd = cmd.iter().any(|arg| {
let arg_str = arg.to_str().unwrap_or("");
arg_str.contains("Mullvad Browser.app")
|| arg_str.contains("mullvad")
|| arg_str.contains("Mullvad")
|| arg_str.contains("/Applications/Mullvad Browser.app/")
|| arg_str.contains("MullvadBrowser")
});
has_mullvad_in_exe || (has_firefox_exe && has_mullvad_in_cmd)
}
"tor-browser" => {
let has_tor_in_exe = exe_name.contains("tor");
let has_firefox_exe = exe_name == "firefox" || exe_name.contains("firefox-bin");
let has_tor_in_cmd = cmd.iter().any(|arg| {
let arg_str = arg.to_str().unwrap_or("");
arg_str.contains("Tor Browser.app")
|| arg_str.contains("tor-browser")
|| arg_str.contains("TorBrowser")
|| arg_str.contains("/Applications/Tor Browser.app/")
|| arg_str.contains("TorBrowser-Data")
});
has_tor_in_exe || (has_firefox_exe && has_tor_in_cmd)
}
_ => false,
}
}
pub async fn launch_browser_process(
executable_path: &std::path::Path,
args: &[String],
@@ -375,122 +340,6 @@ end try
descendants
}
pub async fn open_url_in_existing_browser_tor_mullvad(
profile: &BrowserProfile,
url: &str,
browser_type: BrowserType,
browser_dir: &Path,
_profiles_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let pid = profile.process_id.unwrap();
log::info!("Opening URL in TOR/Mullvad browser using file-based approach (PID: {pid})");
// Method 1: Try using a temporary HTML file approach
log::info!("Attempting file-based URL opening for TOR/Mullvad browser");
let temp_dir = std::env::temp_dir();
let temp_file_name = format!("donut_browser_url_{}.html", std::process::id());
let temp_file_path = temp_dir.join(&temp_file_name);
let html_content = format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url={url}">
<title>Redirecting...</title>
<script>
window.location.href = "{url}";
</script>
</head>
<body>
<p>Redirecting to <a href="{url}">{url}</a>...</p>
</body>
</html>"#
);
match std::fs::write(&temp_file_path, html_content) {
Ok(()) => {
log::info!("Created temporary HTML file: {temp_file_path:?}");
let browser = create_browser(browser_type.clone());
if let Ok(executable_path) = browser.get_executable_path(browser_dir) {
let open_result = Command::new("open")
.args([
"-a",
executable_path.to_str().unwrap(),
temp_file_path.to_str().unwrap(),
])
.output();
// Clean up the temporary file after a short delay
let temp_file_path_clone = temp_file_path.clone();
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
let _ = std::fs::remove_file(temp_file_path_clone);
});
match open_result {
Ok(output) if output.status.success() => {
log::info!("Successfully opened URL using file-based approach");
return Ok(());
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
log::info!("File-based approach failed: {stderr}");
}
Err(e) => {
log::info!("File-based approach error: {e}");
}
}
}
let _ = std::fs::remove_file(&temp_file_path);
}
Err(e) => {
log::info!("Failed to create temporary HTML file: {e}");
}
}
// Method 2: Try using the 'open' command directly with the URL
log::info!("Attempting direct URL opening with 'open' command");
let browser = create_browser(browser_type.clone());
if let Ok(executable_path) = browser.get_executable_path(browser_dir) {
let direct_open_result = Command::new("open")
.args(["-a", executable_path.to_str().unwrap(), url])
.output();
match direct_open_result {
Ok(output) if output.status.success() => {
log::info!("Successfully opened URL using direct 'open' command");
return Ok(());
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
log::info!("Direct 'open' command failed: {stderr}");
}
Err(e) => {
log::info!("Direct 'open' command error: {e}");
}
}
}
// If all methods fail, return a helpful error message
Err(
format!(
"Failed to open URL in existing TOR/Mullvad browser (PID: {pid}). All methods failed:\n\
1. File-based approach failed\n\
2. Direct 'open' command failed\n\
\n\
This may be due to browser security restrictions or the browser process may have changed.\n\
Try closing and reopening the browser, or manually paste the URL: {url}"
)
.into(),
)
}
pub async fn open_url_in_existing_browser_chromium(
profile: &BrowserProfile,
url: &str,
@@ -622,42 +471,6 @@ end try
pub mod windows {
use super::*;
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
let exe_lower = exe_name.to_lowercase();
// Check for Firefox-based browsers first by executable name
let is_firefox_family = exe_lower.contains("firefox") || exe_lower.contains(".exe");
if !is_firefox_family {
return false;
}
// Check command arguments for profile paths and browser-specific indicators
let cmd_line = cmd
.iter()
.map(|s| s.to_string_lossy().to_lowercase())
.collect::<Vec<_>>()
.join(" ");
match browser_type {
"tor-browser" => {
// Check for TOR browser specific paths and arguments
cmd_line.contains("tor")
|| cmd_line.contains("browser\\torbrowser")
|| cmd_line.contains("tor-browser")
|| cmd_line.contains("profile") && (cmd_line.contains("tor") || cmd_line.contains("tbb"))
}
"mullvad-browser" => {
// Check for Mullvad browser specific paths and arguments
cmd_line.contains("mullvad")
|| cmd_line.contains("browser\\mullvadbrowser")
|| cmd_line.contains("mullvad-browser")
|| cmd_line.contains("profile") && cmd_line.contains("mullvad")
}
_ => false,
}
}
pub async fn launch_browser_process(
executable_path: &std::path::Path,
args: &[String],
@@ -782,48 +595,6 @@ pub mod windows {
Ok(())
}
pub async fn open_url_in_existing_browser_tor_mullvad(
profile: &BrowserProfile,
url: &str,
browser_type: BrowserType,
browser_dir: &Path,
profiles_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// On Windows, TOR and Mullvad browsers can sometimes accept URLs via command line
// even with -no-remote, by launching a new instance that hands off to existing one
let browser = create_browser(browser_type.clone());
let executable_path = browser
.get_executable_path(browser_dir)
.map_err(|e| format!("Failed to get executable path: {}", e))?;
let mut cmd = Command::new(&executable_path);
let profile_data_path = profile.get_profile_data_path(profiles_dir);
cmd.args(["-profile", &profile_data_path.to_string_lossy(), url]);
// Set working directory
if let Some(parent_dir) = browser_dir
.parent()
.or_else(|| browser_dir.ancestors().nth(1))
{
cmd.current_dir(parent_dir);
}
let output = cmd.output()?;
if !output.status.success() {
return Err(
format!(
"Failed to open URL in existing {}: {}. Note: TOR and Mullvad browsers may require manual URL opening for security reasons.",
browser_type.as_str(),
String::from_utf8_lossy(&output.stderr)
)
.into(),
);
}
Ok(())
}
pub async fn open_url_in_existing_browser_chromium(
profile: &BrowserProfile,
url: &str,
@@ -909,15 +680,6 @@ pub mod windows {
pub mod linux {
use super::*;
pub fn is_tor_or_mullvad_browser(
_exe_name: &str,
_cmd: &[OsString],
_browser_type: &str,
) -> bool {
// Linux implementation would go here
false
}
pub async fn launch_browser_process(
executable_path: &std::path::Path,
args: &[String],
@@ -1074,16 +836,6 @@ pub mod linux {
Ok(())
}
pub async fn open_url_in_existing_browser_tor_mullvad(
_profile: &BrowserProfile,
_url: &str,
_browser_type: BrowserType,
_browser_dir: &Path,
_profiles_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Err("Opening URLs in existing Firefox-based browsers is not supported on Linux when using -no-remote".into())
}
pub async fn open_url_in_existing_browser_chromium(
profile: &BrowserProfile,
url: &str,
+124 -173
View File
@@ -750,7 +750,7 @@ impl ProfileManager {
// For non-camoufox browsers, use the existing PID-based logic
let inner_profile = profile.clone();
let system = System::new_all();
let mut system = System::new();
let mut is_running = false;
let mut found_pid: Option<u32> = None;
@@ -765,10 +765,8 @@ impl ProfileManager {
let profile_path_match = cmd.iter().any(|s| {
let arg = s.to_str().unwrap_or("");
// For Firefox-based browsers, check for exact profile path match
if profile.browser == "tor-browser"
|| profile.browser == "firefox"
if profile.browser == "firefox"
|| profile.browser == "firefox-developer"
|| profile.browser == "mullvad-browser"
|| profile.browser == "zen"
{
arg == profile_data_path_str
@@ -794,6 +792,8 @@ impl ProfileManager {
// If we didn't find the browser with the stored PID, search all processes
if !is_running {
// Refresh all processes only when we need to search (expensive but necessary)
system.refresh_all();
for (pid, process) in system.processes() {
let cmd = process.cmd();
if cmd.len() >= 2 {
@@ -803,13 +803,9 @@ impl ProfileManager {
"firefox" => {
exe_name.contains("firefox")
&& !exe_name.contains("developer")
&& !exe_name.contains("tor")
&& !exe_name.contains("mullvad")
&& !exe_name.contains("camoufox")
}
"firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"),
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"),
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"),
"zen" => exe_name.contains("zen"),
"chromium" => exe_name.contains("chromium"),
"brave" => exe_name.contains("brave"),
@@ -832,10 +828,8 @@ impl ProfileManager {
// Camoufox uses user_data_dir like Chromium browsers
arg.contains(&format!("--user-data-dir={profile_data_path_str}"))
|| arg == profile_data_path_str
} else if profile.browser == "tor-browser"
|| profile.browser == "firefox"
} else if profile.browser == "firefox"
|| profile.browser == "firefox-developer"
|| profile.browser == "mullvad-browser"
|| profile.browser == "zen"
{
arg == profile_data_path_str
@@ -882,7 +876,6 @@ impl ProfileManager {
None => inner_profile.clone(),
};
let previous_pid = latest_profile.process_id;
let mut merged = latest_profile.clone();
if let Some(pid) = found_pid {
@@ -898,13 +891,6 @@ impl ProfileManager {
if let Err(e) = self.save_profile(&merged) {
log::warn!("Warning: Failed to clear profile PID: {e}");
}
// Stop any associated proxy immediately when the browser stops
if let Some(old_pid) = previous_pid {
let _ = crate::proxy_manager::PROXY_MANAGER
.stop_proxy(app_handle.clone(), old_pid)
.await;
}
}
// Emit profile update event to frontend
@@ -982,18 +968,12 @@ impl ProfileManager {
None => profile.clone(),
};
if let Some(old_pid) = latest.process_id {
if latest.process_id.is_some() {
latest.process_id = None;
if let Err(e) = self.save_profile(&latest) {
log::warn!("Warning: Failed to clear Camoufox profile process info: {e}");
}
// Stop any proxy tied to this old PID immediately
let _ = crate::proxy_manager::PROXY_MANAGER
.stop_proxy(app_handle.clone(), old_pid)
.await;
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &latest) {
log::warn!("Warning: Failed to emit profile update event: {e}");
}
@@ -1018,7 +998,7 @@ impl ProfileManager {
None => profile.clone(),
};
if let Some(old_pid) = latest.process_id {
if latest.process_id.is_some() {
latest.process_id = None;
if let Err(e2) = self.save_profile(&latest) {
log::warn!(
@@ -1026,11 +1006,6 @@ impl ProfileManager {
);
}
// Best-effort stop of proxy tied to old PID
let _ = crate::proxy_manager::PROXY_MANAGER
.stop_proxy(app_handle.clone(), old_pid)
.await;
// Emit profile update event to frontend
if let Err(e3) = app_handle.emit("profile-updated", &latest) {
log::warn!("Warning: Failed to emit profile update event: {e3}");
@@ -1042,36 +1017,9 @@ impl ProfileManager {
}
}
// Helper function to check if a process matches TOR/Mullvad browser
fn is_tor_or_mullvad_browser(
&self,
exe_name: &str,
cmd: &[std::ffi::OsString],
browser_type: &str,
) -> bool {
#[cfg(target_os = "macos")]
return crate::platform_browser::macos::is_tor_or_mullvad_browser(exe_name, cmd, browser_type);
#[cfg(target_os = "windows")]
return crate::platform_browser::windows::is_tor_or_mullvad_browser(
exe_name,
cmd,
browser_type,
);
#[cfg(target_os = "linux")]
return crate::platform_browser::linux::is_tor_or_mullvad_browser(exe_name, cmd, browser_type);
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
let _ = (exe_name, cmd, browser_type);
false
}
}
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 +1034,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 +1107,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
// 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(())
}
@@ -1283,7 +1224,9 @@ mod tests {
let temp_dir = TempDir::new().unwrap();
// Mock the base directories by setting environment variables
std::env::set_var("HOME", temp_dir.path());
unsafe {
std::env::set_var("HOME", temp_dir.path());
}
let profile_manager = ProfileManager::instance();
(profile_manager, temp_dir)
@@ -1397,20 +1340,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"
);
}
}
-18
View File
@@ -59,9 +59,6 @@ impl ProfileImporter {
// Detect Zen Browser profiles
detected_profiles.extend(self.detect_zen_browser_profiles()?);
// NOTE: Mullvad and Tor Browser profile imports are no longer supported.
// We intentionally do not detect these profiles to avoid offering them in the UI.
// Remove duplicates based on path
let mut seen_paths = HashSet::new();
let unique_profiles: Vec<DetectedProfile> = detected_profiles
@@ -495,9 +492,7 @@ impl ProfileImporter {
"firefox-developer" => "Firefox Developer",
"chromium" => "Chrome/Chromium",
"brave" => "Brave",
"mullvad-browser" => "Mullvad Browser",
"zen" => "Zen Browser",
"tor-browser" => "Tor Browser",
_ => "Unknown Browser",
}
}
@@ -509,11 +504,6 @@ impl ProfileImporter {
browser_type: &str,
new_profile_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Disable imports for Mullvad and Tor browsers
if browser_type == "mullvad-browser" || browser_type == "tor-browser" {
return Err("Importing Mullvad Browser or Tor Browser profiles is not supported".into());
}
// Validate that source path exists
let source_path = Path::new(source_path);
if !source_path.exists() {
@@ -685,15 +675,7 @@ mod tests {
"Chrome/Chromium"
);
assert_eq!(importer.get_browser_display_name("brave"), "Brave");
assert_eq!(
importer.get_browser_display_name("mullvad-browser"),
"Mullvad Browser"
);
assert_eq!(importer.get_browser_display_name("zen"), "Zen Browser");
assert_eq!(
importer.get_browser_display_name("tor-browser"),
"Tor Browser"
);
assert_eq!(
importer.get_browser_display_name("unknown"),
"Unknown Browser"
+119 -61
View File
@@ -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
@@ -596,11 +595,6 @@ impl ProxyManager {
browser_pid: u32,
profile_id: Option<&str>,
) -> Result<ProxySettings, String> {
// First, proactively cleanup any dead proxies so we don't accidentally reuse stale ones
let _ = self.cleanup_dead_proxies(app_handle.clone()).await;
// If we have a previous proxy tied to this profile, and the upstream settings are changing,
// stop it before starting a new one so the change takes effect immediately.
if let Some(name) = profile_id {
// Check if we have an active proxy recorded for this profile
let maybe_existing_id = {
@@ -626,30 +620,29 @@ impl ProxyManager {
&& existing.upstream_host == desired_host
&& existing.upstream_port == desired_port;
if !is_same_upstream {
// Stop the previous proxy tied to this profile (best effort)
// We don't know the original PID mapping that created it; iterate to find its key
let pid_to_stop = {
let proxies = self.active_proxies.lock().unwrap();
proxies.iter().find_map(|(pid, info)| {
if info.id == existing_id {
Some(*pid)
} else {
None
}
})
};
if let Some(pid) = pid_to_stop {
let _ = self.stop_proxy(app_handle.clone(), pid).await;
if is_same_upstream {
// Settings match - can reuse existing proxy
// Just update the PID mapping if needed
let proxies = self.active_proxies.lock().unwrap();
if proxies.contains_key(&browser_pid) {
// Already mapped, reuse it
return Ok(ProxySettings {
proxy_type: "http".to_string(),
host: "127.0.0.1".to_string(),
port: existing.local_port,
username: None,
password: None,
});
}
// Need to add this PID to the mapping - we'll do that after starting
}
// Settings differ - we'll create a new proxy, but don't stop the old one
// It will be cleaned up by periodic cleanup if it becomes dead
}
}
}
// Check if we already have a proxy for this browser PID. If it exists but the upstream
// settings don't match the newly requested ones, stop it and create a new proxy so that
// changes take effect immediately.
let mut needs_restart = false;
// Check if we already have a proxy for this browser PID
// If settings match, reuse it; otherwise create a new one (don't stop the old one)
{
let proxies = self.active_proxies.lock().unwrap();
if let Some(existing) = proxies.get(&browser_pid) {
@@ -664,7 +657,7 @@ impl ProxyManager {
&& existing.upstream_port == desired_port;
if is_same_upstream {
// Check if profile_id matches - if not, we need to restart to update tracking
// Check if profile_id matches
let profile_id_matches = match (profile_id, &existing.profile_id) {
(Some(ref new_id), Some(ref old_id)) => new_id == old_id,
(None, None) => true,
@@ -672,7 +665,7 @@ impl ProxyManager {
};
if profile_id_matches {
// Reuse existing local proxy (profile_id matches)
// Reuse existing local proxy (settings and profile_id match)
return Ok(ProxySettings {
proxy_type: "http".to_string(),
host: "127.0.0.1".to_string(),
@@ -680,28 +673,15 @@ impl ProxyManager {
username: None,
password: None,
});
} else {
// Profile ID changed - need to restart proxy to update tracking
log::info!(
"Profile ID changed for proxy {}: {:?} -> {:?}, restarting proxy",
existing.id,
existing.profile_id,
profile_id
);
needs_restart = true;
}
} else {
// Upstream changed; we must restart the local proxy so that traffic is routed correctly
needs_restart = true;
// Profile ID changed - we'll create a new proxy but don't stop the old one
// It will be cleaned up by periodic cleanup if it becomes dead
}
// Upstream changed - we'll create a new proxy but don't stop the old one
// It will be cleaned up by periodic cleanup if it becomes dead
}
}
if needs_restart {
// Best-effort stop of the old proxy for this PID before starting a new one
let _ = self.stop_proxy(app_handle.clone(), browser_pid).await;
}
// Start a new proxy using the donut-proxy binary with the correct CLI interface
let mut proxy_cmd = app_handle
.shell()
@@ -956,30 +936,108 @@ impl ProxyManager {
}
}
// Check if a process is still running
fn is_process_running(&self, pid: u32) -> bool {
use sysinfo::{Pid, System};
let system = System::new_all();
system.process(Pid::from(pid as usize)).is_some()
}
// Clean up proxies for dead browser processes
// Only clean up orphaned config files where the proxy process itself is dead
pub async fn cleanup_dead_proxies(
&self,
app_handle: tauri::AppHandle,
) -> Result<Vec<u32>, String> {
let dead_pids = {
let proxies = self.active_proxies.lock().unwrap();
proxies
.keys()
.filter(|&&pid| pid != 0 && !self.is_process_running(pid)) // Skip temporary PID 0
.copied()
.collect::<Vec<u32>>()
// Don't stop proxies for dead browser processes - let them run indefinitely
// The proxy processes are idle and don't consume CPU when not in use
// Only clean up config files where the proxy process itself is dead (see below)
let dead_pids: Vec<u32> = Vec::new();
// Clean up orphaned proxy configs (only where proxy process is definitely dead)
// IMPORTANT: Only clean up configs where the proxy process itself is dead
// If the proxy process is running (even if idle), leave it alone
// The user doesn't care if proxy processes run indefinitely as long as they're not consuming CPU
let orphaned_configs = {
use crate::proxy_storage::{is_process_running, list_proxy_configs};
use std::time::{SystemTime, UNIX_EPOCH};
let all_configs = list_proxy_configs();
let tracked_proxy_ids: std::collections::HashSet<String> = {
let proxies = self.active_proxies.lock().unwrap();
proxies.values().map(|p| p.id.clone()).collect()
};
// Get current time for grace period check
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
all_configs
.into_iter()
.filter(|config| {
// If proxy is tracked in active_proxies, it's definitely not orphaned
if tracked_proxy_ids.contains(&config.id) {
return false;
}
// Extract creation time from proxy ID (format: proxy_{timestamp}_{random})
// This gives us a grace period for newly created proxies
let proxy_age = config
.id
.strip_prefix("proxy_")
.and_then(|s| s.split('_').next())
.and_then(|s| s.parse::<u64>().ok())
.map(|created_at| now.saturating_sub(created_at))
.unwrap_or(0);
// Grace period: don't clean up proxies created in the last 120 seconds
// This prevents race conditions during startup (increased from 60 to 120 for safety)
if proxy_age < 120 {
log::debug!(
"Skipping cleanup of proxy {} - too new (age: {}s)",
config.id,
proxy_age
);
return false;
}
// ONLY clean up if we can verify the proxy process is dead
// If proxy process is running, leave it alone (even if idle)
if let Some(proxy_pid) = config.pid {
// Check if proxy process is actually dead
if !is_process_running(proxy_pid) {
// Proxy process is dead, clean up the config file
log::info!(
"Proxy {} process (PID {}) is dead, will clean up config",
config.id,
proxy_pid
);
return true;
}
// Proxy process is running - leave it alone
log::debug!(
"Skipping cleanup of proxy {} - process (PID {}) is still running",
config.id,
proxy_pid
);
return false;
}
// No PID in config - can't verify if process is dead
// Be conservative: don't clean up (might be starting up or PID not set yet)
log::debug!(
"Skipping cleanup of proxy {} - no PID in config (might be starting up)",
config.id
);
false
})
.collect::<Vec<_>>()
};
for dead_pid in &dead_pids {
log::info!("Cleaning up proxy for dead browser process PID: {dead_pid}");
let _ = self.stop_proxy(app_handle.clone(), *dead_pid).await;
// Clean up orphaned config files (proxy process is dead)
for config in orphaned_configs {
log::info!(
"Cleaning up orphaned proxy config: {} (proxy process is dead)",
config.id
);
// Just delete the config file - the process is already dead
use crate::proxy_storage::delete_proxy_config;
delete_proxy_config(&config.id);
}
// Emit event for reactive UI updates
+6 -12
View File
@@ -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());
}
+59 -16
View File
@@ -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()
@@ -595,11 +602,35 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::er
// Start a background task to periodically flush traffic stats to disk
tokio::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut last_activity_time = std::time::Instant::now();
let mut last_byte_count = 0u64;
loop {
interval.tick().await;
if let Some(tracker) = get_traffic_tracker() {
if let Err(e) = tracker.flush_to_disk() {
log::error!("Failed to flush traffic stats: {}", e);
let (sent, recv, requests) = tracker.get_snapshot();
let current_bytes = sent + recv;
let bytes_changed = current_bytes != last_byte_count;
let time_since_activity = last_activity_time.elapsed();
let has_traffic = current_bytes > 0 || requests > 0;
// Always flush if we have traffic, or if bytes changed, or if it's been less than 30s since activity
// This ensures traffic is always persisted, even during active periods
let should_flush =
has_traffic || bytes_changed || time_since_activity < std::time::Duration::from_secs(30);
if should_flush {
if let Err(e) = tracker.flush_to_disk() {
log::error!("Failed to flush traffic stats: {}", e);
} else {
// Update tracking state after successful flush
if has_traffic || bytes_changed {
last_activity_time = std::time::Instant::now();
}
// After flush, bytes are reset to 0, so update last_byte_count
last_byte_count = 0;
}
}
}
}
@@ -609,7 +640,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 +653,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 +691,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 +711,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 +847,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
+1 -1
View File
@@ -133,6 +133,6 @@ pub fn generate_proxy_id() -> String {
pub fn is_process_running(pid: u32) -> bool {
use sysinfo::{Pid, System};
let system = System::new_all();
let system = System::new();
system.process(Pid::from(pid as usize)).is_some()
}
+76 -4
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut",
"version": "0.13.0",
"version": "0.13.7",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
+2 -6
View File
@@ -30,13 +30,11 @@ import { showErrorToast, showToast } from "@/lib/toast-utils";
import type { BrowserProfile, CamoufoxConfig } from "@/types";
type BrowserTypeString =
| "mullvad-browser"
| "firefox"
| "firefox-developer"
| "chromium"
| "brave"
| "zen"
| "tor-browser"
| "camoufox";
interface PendingUrl {
@@ -648,9 +646,7 @@ export default function Home() {
if (profiles.length === 0) return;
const deprecatedProfiles = profiles.filter(
(p) =>
["tor-browser", "mullvad-browser"].includes(p.browser) ||
(p.release_type === "nightly" && p.browser !== "firefox-developer"),
(p) => p.release_type === "nightly" && p.browser !== "firefox-developer",
);
if (deprecatedProfiles.length > 0) {
@@ -661,7 +657,7 @@ export default function Home() {
id: "deprecated-profiles-warning",
type: "error",
title: "Some profiles will be deprecated soon",
description: `The following profiles will be deprecated soon: ${deprecatedNames}. Tor Browser, Mullvad Browser, and nightly profiles (except Firefox Developers Edition) will be removed in upcoming versions. Please check GitHub for migration instructions.`,
description: `The following profiles will be deprecated soon: ${deprecatedNames}. Nightly profiles (except Firefox Developers Edition) will be removed in upcoming versions. Please check GitHub for migration instructions.`,
duration: 15000,
action: {
label: "Learn more",
+4 -2
View File
@@ -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>
+3 -16
View File
@@ -42,13 +42,11 @@ const getCurrentOS = (): CamoufoxOS => {
import { RippleButton } from "./ui/ripple";
type BrowserTypeString =
| "mullvad-browser"
| "firefox"
| "firefox-developer"
| "chromium"
| "brave"
| "zen"
| "tor-browser"
| "camoufox";
interface CreateProfileDialogProps {
@@ -92,14 +90,6 @@ const browserOptions: BrowserOption[] = [
value: "zen",
label: "Zen Browser",
},
{
value: "mullvad-browser",
label: "Mullvad Browser",
},
{
value: "tor-browser",
label: "Tor Browser",
},
];
export function CreateProfileDialog({
@@ -429,12 +419,9 @@ export function CreateProfileDialog({
isBrowserVersionAvailable,
]);
// Filter supported browsers for regular browsers (excluding mullvad and tor)
const regularBrowsers = browserOptions.filter(
(browser) =>
supportedBrowsers.includes(browser.value) &&
browser.value !== "mullvad-browser" &&
browser.value !== "tor-browser",
// Filter supported browsers for regular browsers
const regularBrowsers = browserOptions.filter((browser) =>
supportedBrowsers.includes(browser.value),
);
return (
+1 -4
View File
@@ -62,10 +62,7 @@ export function ImportProfileDialog({
const { supportedBrowsers, isLoading: isLoadingSupport } =
useBrowserSupport();
// Exclude browsers that are no longer supported for import
const importableBrowsers = supportedBrowsers.filter(
(b) => b !== "mullvad-browser" && b !== "tor-browser",
);
const importableBrowsers = supportedBrowsers;
const loadDetectedProfiles = useCallback(async () => {
setIsLoading(true);
-1
View File
@@ -287,7 +287,6 @@ const MultipleSelector = React.forwardRef<
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, groupBy, open, triggerSearchOnFocus, onSearch]);
// biome-ignore lint/correctness/noNestedComponentDefinitions: public code, TODO: fix
const CreatableItem = () => {
if (!creatable) return undefined;
if (
+13 -34
View File
@@ -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
@@ -1677,19 +1680,12 @@ export function ProfilesDataTable({
? (meta.storedProxies.find((p) => p.id === effectiveProxyId) ??
null)
: null;
const displayName =
profile.browser === "tor-browser"
? "Not supported"
: effectiveProxy
? effectiveProxy.name
: "Not Selected";
const displayName = effectiveProxy
? effectiveProxy.name
: "Not Selected";
const profileHasProxy = Boolean(effectiveProxy);
const tooltipText =
profile.browser === "tor-browser"
? "Proxies are not supported for TOR browser"
: profileHasProxy && effectiveProxy
? effectiveProxy.name
: null;
profileHasProxy && effectiveProxy ? effectiveProxy.name : null;
const isSelectorOpen = meta.openProxySelectorFor === profile.id;
// When profile is running, show bandwidth chart instead of proxy selector
@@ -1714,23 +1710,6 @@ export function ProfilesDataTable({
);
}
if (profile.browser === "tor-browser") {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex gap-2 items-center">
<span className="text-sm text-muted-foreground">
Not supported
</span>
</span>
</TooltipTrigger>
{(tooltipText || displayName.length > 10) && (
<TooltipContent>{tooltipText || displayName}</TooltipContent>
)}
</Tooltip>
);
}
return (
<div className="flex gap-2 items-center">
<Popover
+1 -5
View File
@@ -142,11 +142,7 @@ export function ProfileSelectorDialog({
const runningAvailableProfile = profiles.find((profile) => {
const isRunning = runningProfiles.has(profile.id);
// Simple check without browserState dependency
return (
isRunning &&
profile.browser !== "tor-browser" &&
profile.browser !== "mullvad-browser"
);
return isRunning;
});
if (runningAvailableProfile) {
+1 -8
View File
@@ -49,10 +49,9 @@ export function ProxyAssignmentDialog({
setIsAssigning(true);
setError(null);
try {
// Filter out TOR browser profiles as they don't support proxies
const validProfiles = selectedProfiles.filter((profileId) => {
const profile = profiles.find((p) => p.id === profileId);
return profile && profile.browser !== "tor-browser";
return profile;
});
if (validProfiles.length === 0) {
@@ -119,15 +118,9 @@ export function ProxyAssignmentDialog({
(p: BrowserProfile) => p.id === profileId,
);
const displayName = profile ? profile.name : profileId;
const isTorBrowser = profile?.browser === "tor-browser";
return (
<li key={profileId} className="truncate">
{displayName}
{isTorBrowser && (
<span className="ml-2 text-xs text-muted-foreground">
(TOR - no proxy support)
</span>
)}
</li>
);
})}
+66 -18
View File
@@ -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()}
+16 -8
View File
@@ -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);
+6 -10
View File
@@ -3,7 +3,7 @@ import { getBrowserDisplayName } from "@/lib/browser-utils";
import type { BrowserProfile } from "@/types";
/**
* Hook for managing browser state and enforcing single-instance rules for Tor and Mullvad browsers
* Hook for managing browser state
*/
export function useBrowserState(
profiles: BrowserProfile[],
@@ -22,8 +22,8 @@ export function useBrowserState(
* Check if a browser type allows only one instance to run at a time
*/
const isSingleInstanceBrowser = useCallback(
(browserType: string): boolean => {
return browserType === "tor-browser" || browserType === "mullvad-browser";
(_browserType: string): boolean => {
return false; // No browsers currently require single instance
},
[],
);
@@ -102,7 +102,7 @@ export function useBrowserState(
return false;
}
// For single-instance browsers (Tor and Mullvad)
// For single-instance browsers
if (isSingleInstanceBrowser(profile.browser)) {
const runningInstancesOfType = profiles.filter(
(p) => p.browser === profile.browser && runningProfiles.has(p.id),
@@ -195,9 +195,7 @@ export function useBrowserState(
isSingleInstanceBrowser(profile.browser) &&
!canLaunchProfile(profile)
) {
const browserDisplayName =
profile.browser === "tor-browser" ? "TOR" : "Mullvad";
return `Only one ${browserDisplayName} browser instance can run at a time. Stop the running ${browserDisplayName} browser first.`;
return `Only one instance of this browser can run at a time. Stop the running browser first.`;
}
return "";
@@ -242,8 +240,6 @@ export function useBrowserState(
}
if (isSingleInstanceBrowser(profile.browser)) {
const browserDisplayName =
profile.browser === "tor-browser" ? "TOR" : "Mullvad";
const runningInstancesOfType = profiles.filter(
(p) => p.browser === profile.browser && runningProfiles.has(p.id),
);
@@ -252,7 +248,7 @@ export function useBrowserState(
const runningProfileNames = runningInstancesOfType
.map((p) => p.name)
.join(", ");
return `${browserDisplayName} browser is already running (${runningProfileNames}). Only one instance can run at a time.`;
return `${getBrowserDisplayName(profile.browser)} browser is already running (${runningProfileNames}). Only one instance can run at a time.`;
}
}
+1 -7
View File
@@ -4,7 +4,7 @@
*/
import { FaChrome, FaFirefox, FaShieldAlt } from "react-icons/fa";
import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si";
import { SiBrave } from "react-icons/si";
import { ZenBrowser } from "@/components/icons/zen-browser";
/**
@@ -14,11 +14,9 @@ export function getBrowserDisplayName(browserType: string): string {
const browserNames: Record<string, string> = {
firefox: "Firefox",
"firefox-developer": "Firefox Developer Edition",
"mullvad-browser": "Mullvad Browser",
zen: "Zen Browser",
brave: "Brave",
chromium: "Chromium",
"tor-browser": "Tor Browser",
camoufox: "Anti-Detect",
};
@@ -30,8 +28,6 @@ export function getBrowserDisplayName(browserType: string): string {
*/
export function getBrowserIcon(browserType: string) {
switch (browserType) {
case "mullvad-browser":
return SiMullvad;
case "chromium":
return FaChrome;
case "brave":
@@ -41,8 +37,6 @@ export function getBrowserIcon(browserType: string) {
return FaFirefox;
case "zen":
return ZenBrowser;
case "tor-browser":
return SiTorbrowser;
case "camoufox":
return FaShieldAlt;
default:
+1
View File
@@ -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
View File
@@ -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"]
}