mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-10 23:03:42 +02:00
refactor: fully deprecate camoufox
This commit is contained in:
@@ -41,15 +41,12 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: browser
|
||||
- type: input
|
||||
id: wayfern_version
|
||||
attributes:
|
||||
label: Which browser is affected?
|
||||
options:
|
||||
- Wayfern
|
||||
- Camoufox
|
||||
- Both
|
||||
- Not browser-specific
|
||||
label: Wayfern version
|
||||
description: Settings → About, or the version shown when creating a profile. Use "unknown" if not browser-specific.
|
||||
placeholder: e.g. 138.0.7204.50 or unknown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
const get = (k) => fields[k] || '';
|
||||
fs.writeFileSync('/tmp/issue-os.txt', get('Operating System'));
|
||||
fs.writeFileSync('/tmp/issue-version.txt', get('Donut Browser version'));
|
||||
fs.writeFileSync('/tmp/issue-browser.txt', get('Which browser is affected?'));
|
||||
fs.writeFileSync('/tmp/issue-wayfern-version.txt', get('Wayfern version'));
|
||||
fs.writeFileSync('/tmp/issue-repro.txt', get('Steps to reproduce'));
|
||||
fs.writeFileSync('/tmp/issue-logs.txt', get('Error logs or screenshots'));
|
||||
fs.writeFileSync('/tmp/issue-what.txt', get('What happened?') || get('What do you want?'));
|
||||
@@ -102,17 +102,7 @@ jobs:
|
||||
its API, MCP server, and the bundled `donut-sync` self-hosted server.
|
||||
- **Wayfern** — a Chromium fork maintained by zhom (the same maintainer). Wayfern
|
||||
bugs are in-scope here unless they are obviously upstream Chromium issues.
|
||||
- **Camoufox** — a Firefox fork by daijro, used by Donut but maintained in a
|
||||
separate repository. Bugs about Camoufox's *internal* behavior are outside
|
||||
the scope of this project.
|
||||
- Bugs about Camoufox's *internal* behavior (page rendering, JS engine,
|
||||
dropdowns, form widgets, fingerprinting *as Camoufox implements it*,
|
||||
checkbox/radio quirks) are out of scope here. Ask the user to first
|
||||
search https://github.com/daijro/camoufox/issues for a matching report,
|
||||
and if they don't find one, to open it there themselves.
|
||||
- Bugs about how Donut *launches, configures, or downloads* Camoufox are
|
||||
in-scope here.
|
||||
- **Forks of Wayfern or Camoufox** (e.g. CloverLabsAI, VulpineOS) are NOT
|
||||
- **Forks of Wayfern** (e.g. CloverLabsAI, VulpineOS) are NOT
|
||||
supported. Feature requests asking for them are out of scope.
|
||||
|
||||
# PAID vs FREE FEATURES
|
||||
@@ -121,7 +111,7 @@ jobs:
|
||||
|
||||
## Free (no account required)
|
||||
- Unlimited local profiles
|
||||
- Chromium (Wayfern) and Firefox (Camoufox) browser engines
|
||||
- Chromium (Wayfern) anti-detect browser engine
|
||||
- Proxy support (HTTP/SOCKS5)
|
||||
- VPN support (WireGuard)
|
||||
- Profile Management API & MCP (list / create / launch / kill / config)
|
||||
@@ -147,13 +137,8 @@ jobs:
|
||||
differently ("worked in 0.21", "went from 2 to 8 false positives"). Do NOT
|
||||
dismiss as "known issue" / "expected" / "false positive in Tauri apps". Ask
|
||||
which exact version was the last working one and what changed.
|
||||
- **Out-of-scope (upstream Camoufox)**: report is about Camoufox's own
|
||||
behavior. Tell the user it's outside the scope of this project and ask
|
||||
them to search the Camoufox repo and, if no matching issue exists, file
|
||||
one there. Do NOT say the maintainer doesn't contribute / can't fix it
|
||||
— keep it strictly about project scope. Do not collect logs.
|
||||
- **Fork-support request**: asks the maintainer to support an alternative
|
||||
Wayfern/Camoufox fork. Acknowledge in one neutral sentence — do NOT call it
|
||||
Wayfern fork. Acknowledge in one neutral sentence — do NOT call it
|
||||
"clear", "reasonable", "well-thought-out", etc.
|
||||
- **AI-generated / template-violating report**: report doesn't follow the
|
||||
template, may cite "official documentation" via context7, deepwiki, or any
|
||||
@@ -214,7 +199,7 @@ jobs:
|
||||
Return ONLY valid JSON. No preamble, no code fences. Schema:
|
||||
{
|
||||
"language": "en" or ISO 639-1 code,
|
||||
"classification": one of ["bug-in-scope", "bug-upstream-camoufox", "bug-template-violation", "feature-request", "fork-request", "regression", "ai-generated-junk", "question", "other"],
|
||||
"classification": one of ["bug-in-scope", "bug-template-violation", "feature-request", "fork-request", "regression", "ai-generated-junk", "question", "other"],
|
||||
"operating_system": "macos" | "windows" | "linux" | "unknown",
|
||||
"is_paid_feature": true | false,
|
||||
"user_followed_template": true | false,
|
||||
@@ -225,13 +210,12 @@ jobs:
|
||||
}
|
||||
|
||||
Classification guidance:
|
||||
- "bug-upstream-camoufox": Camoufox-internal behavior (rendering, dropdowns, JS, fingerprint impl). NOT how Donut launches it.
|
||||
- "bug-template-violation": missing or filled-in nonsense for required template fields.
|
||||
- "ai-generated-junk": cites fabricated "official docs" (context7, deepwiki, non-donutbrowser URLs) or has the polished AI-spam shape (long, structured, fabricated certainty).
|
||||
- "fork-request": asks for support of CloverLabsAI/VulpineOS/etc. forks.
|
||||
- "regression": user names a prior version that worked.
|
||||
|
||||
File selection: pick files that an experienced reviewer would actually look at to act on this issue. If the issue is upstream-Camoufox, fork-request, or junk, set files_to_read to []. Otherwise pick concrete files relevant to the symptoms.
|
||||
File selection: pick files that an experienced reviewer would actually look at to act on this issue. If the issue is a fork-request or junk, set files_to_read to []. Otherwise pick concrete files relevant to the symptoms.
|
||||
TRIAGE_TAIL
|
||||
} > /tmp/triage-system.txt
|
||||
wc -c /tmp/triage-system.txt
|
||||
@@ -292,7 +276,7 @@ jobs:
|
||||
- name: Read files chosen by triage
|
||||
run: |
|
||||
: > /tmp/file-context.txt
|
||||
# files_to_read may be empty (e.g. upstream Camoufox) — that's fine.
|
||||
# files_to_read may be empty (e.g. fork-request or junk) — that's fine.
|
||||
jq -r '.files_to_read[]? // empty' /tmp/triage.json | while IFS= read -r filepath; do
|
||||
filepath=$(echo "$filepath" | xargs)
|
||||
[ -z "$filepath" ] && continue
|
||||
@@ -329,7 +313,7 @@ jobs:
|
||||
|
||||
## Output shape
|
||||
- One sentence acknowledging the report.
|
||||
- Then **Missing information** — only if there is anything actually missing. Skip this section if the user already provided OS, version, browser, repro steps, and any logs the situation calls for.
|
||||
- Then **Missing information** — only if there is anything actually missing. Skip this section if the user already provided OS, Donut Browser version, Wayfern version, repro steps, and any logs the situation calls for.
|
||||
- Maximum 15 lines.
|
||||
- No labels, no `Label:` line, no markdown headings other than `**Missing information**`.
|
||||
- No closing pleasantries ("please let me know", "happy to help", etc.).
|
||||
@@ -347,8 +331,7 @@ jobs:
|
||||
The triage classification (`triage.classification`) determines the response shape:
|
||||
|
||||
- `bug-in-scope`: ask for what is missing using the user's reported OS log path. Be concrete about how to obtain logs.
|
||||
- `bug-upstream-camoufox`: redirect ONLY. One sentence acknowledging, then say this is outside the scope of this project — ask the user to first search https://github.com/daijro/camoufox/issues for a matching report and, if none exists, to open one there themselves. Do NOT phrase it as "the maintainer does not contribute" or anything personal — keep it strictly about scope. Do NOT ask for Donut logs. Stop after that.
|
||||
- `bug-template-violation` or `ai-generated-junk`: politely ask the user to refile using the bug-report template (the Operating System, Donut Browser version, Which browser, Steps to reproduce, Error logs sections). If they cited "documentation" from any non-`donutbrowser.com`/non-`github.com/zhom` URL (e.g. context7, deepwiki), gently note that those are AI-generated third-party summaries and the only authoritative sources are this repo and donutbrowser.com.
|
||||
- `bug-template-violation` or `ai-generated-junk`: politely ask the user to refile using the bug-report template (the Operating System, Donut Browser version, Wayfern version, Steps to reproduce, Error logs sections). If they cited "documentation" from any non-`donutbrowser.com`/non-`github.com/zhom` URL (e.g. context7, deepwiki), gently note that those are AI-generated third-party summaries and the only authoritative sources are this repo and donutbrowser.com.
|
||||
- `feature-request`: one neutral sentence acknowledging, then ask only what is genuinely needed (concrete use case, whether a workaround would suffice). Do NOT validate.
|
||||
- `fork-request`: one neutral sentence acknowledging the request. Note that this would substantially increase support burden and the maintainer evaluates such requests on a case-by-case basis. Ask whether the alternative fork supports all platforms the user uses (macOS / Windows / Linux). No "clear enhancement" language.
|
||||
- `regression`: do NOT call known/expected. Ask which exact previous version was the last working one, what changed in the user's environment between then and now, and the specific delta in symptoms.
|
||||
|
||||
Vendored
-1
@@ -21,7 +21,6 @@
|
||||
"Buildx",
|
||||
"busctl",
|
||||
"CAMOU",
|
||||
"camoufox",
|
||||
"catppuccin",
|
||||
"cdylib",
|
||||
"certifi",
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ codeql database analyze /tmp/codeql-rust --format=sarifv2.1.0 --output=/tmp/rust
|
||||
- **Backend**: Tauri (Rust), `src-tauri/src/`
|
||||
- **Proxy Worker**: Detached process for proxy tunneling, `src-tauri/src/bin/proxy_server.rs`
|
||||
- **Sync**: Cloud sync via S3-compatible storage, `src-tauri/src/sync/`, `donut-sync/`
|
||||
- **Browsers**: Camoufox (Firefox-based) and Wayfern (Chromium-based)
|
||||
- **Browsers**: Wayfern (Chromium-based anti-detect)
|
||||
|
||||
## Getting Help
|
||||
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"src-tauri/src/camoufox/data/*.json",
|
||||
"src-tauri/src/camoufox/data/*.xml",
|
||||
"src-tauri/src/territory_info.xml",
|
||||
"src/i18n/locales/*.json",
|
||||
# Auto-generated from commit subjects by release.yml; typos here originate
|
||||
# in commit messages, which are immutable, so don't spell-check it.
|
||||
|
||||
Generated
+4
-219
@@ -194,9 +194,6 @@ name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
@@ -641,7 +638,7 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq 0.4.2",
|
||||
"constant_time_eq",
|
||||
"cpufeatures 0.3.0",
|
||||
]
|
||||
|
||||
@@ -1223,12 +1220,6 @@ dependencies = [
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
@@ -1561,12 +1552,6 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "0.3.100"
|
||||
@@ -1627,17 +1612,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -1822,11 +1796,10 @@ dependencies = [
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"once_cell",
|
||||
"playwright",
|
||||
"quick-xml 0.40.1",
|
||||
"rand 0.10.1",
|
||||
"regex-lite",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"resvg",
|
||||
"ring",
|
||||
"rusqlite",
|
||||
@@ -1869,7 +1842,7 @@ dependencies = [
|
||||
"windows 0.62.2",
|
||||
"winreg 0.56.0",
|
||||
"wiremock",
|
||||
"zip 8.6.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2584,11 +2557,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3704,17 +3675,6 @@ dependencies = [
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzxd"
|
||||
version = "0.2.6"
|
||||
@@ -4523,16 +4483,6 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -4680,29 +4630,6 @@ version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "playwright"
|
||||
version = "0.0.23"
|
||||
source = "git+https://github.com/zhom/playwright-rust?branch=master#0482f839aa24507268d9e3ee47c2be859eeef4f2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"dirs",
|
||||
"futures",
|
||||
"itertools",
|
||||
"log",
|
||||
"paste",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"strong",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.9.0"
|
||||
@@ -5259,48 +5186,6 @@ dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower",
|
||||
"tower-http 0.6.11",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
@@ -6315,15 +6200,6 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strong"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cbe0fc7652d95bcd84f61cd036181b395f329ef45b25169b69a42f72cb6975f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -6562,7 +6438,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -7166,18 +7042,6 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tfo"
|
||||
version = "0.4.3"
|
||||
@@ -8858,15 +8722,6 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "y4m"
|
||||
version = "0.8.0"
|
||||
@@ -9051,36 +8906,6 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"aes 0.8.4",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq 0.3.1",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"deflate64",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"hmac",
|
||||
"indexmap 2.14.0",
|
||||
"lzma-rs",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"xz2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "8.6.0"
|
||||
@@ -9100,46 +8925,6 @@ version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
|
||||
@@ -91,8 +91,6 @@ http-body-util = "0.1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
async-socks5 = "0.6"
|
||||
|
||||
# Camoufox/Playwright integration
|
||||
playwright = { git = "https://github.com/zhom/playwright-rust", branch = "master" }
|
||||
|
||||
# Wayfern CDP integration
|
||||
tokio-tungstenite = { version = "0.29", features = ["native-tls"] }
|
||||
|
||||
+51
-1500
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
use crate::browser::ProxySettings;
|
||||
use crate::camoufox_manager::CamoufoxConfig;
|
||||
use crate::events;
|
||||
use crate::group_manager::GROUP_MANAGER;
|
||||
use crate::profile::manager::ProfileManager;
|
||||
@@ -35,7 +34,6 @@ pub struct ApiProfile {
|
||||
pub last_launch: Option<u64>,
|
||||
pub release_type: String,
|
||||
#[schema(value_type = Object)]
|
||||
pub camoufox_config: Option<serde_json::Value>,
|
||||
pub group_id: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub is_running: bool,
|
||||
@@ -57,7 +55,7 @@ pub struct ApiProfileResponse {
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateProfileRequest {
|
||||
pub name: String,
|
||||
/// Browser engine. Must be `"wayfern"` (anti-detect Chromium) or `"camoufox"`
|
||||
/// Browser engine. Must be `"wayfern"` (anti-detect Chromium)
|
||||
/// (anti-detect Firefox). Any other value (e.g. `"chromium"`) is rejected with
|
||||
/// 400.
|
||||
pub browser: String,
|
||||
@@ -70,12 +68,11 @@ pub struct CreateProfileRequest {
|
||||
pub vpn_id: Option<String>,
|
||||
pub launch_hook: Option<String>,
|
||||
pub release_type: Option<String>,
|
||||
/// Camoufox fingerprint/config. Send only when `browser` is `"camoufox"`.
|
||||
/// Wayfern fingerprint/config. Send only when `browser` is `"wayfern"`.
|
||||
/// Omit it, or pass an empty object `{}`, to have a fresh fingerprint
|
||||
/// generated automatically at creation. Provide a `fingerprint` field to
|
||||
/// pin a specific one.
|
||||
#[schema(value_type = Object)]
|
||||
pub camoufox_config: Option<serde_json::Value>,
|
||||
/// Wayfern fingerprint/config. Send only when `browser` is `"wayfern"`.
|
||||
/// Omit it, or pass an empty object `{}`, to have a fresh fingerprint
|
||||
/// generated automatically at creation. Provide a `fingerprint` field to
|
||||
@@ -98,7 +95,6 @@ pub struct UpdateProfileRequest {
|
||||
pub launch_hook: Option<String>,
|
||||
pub release_type: Option<String>,
|
||||
#[schema(value_type = Object)]
|
||||
pub camoufox_config: Option<serde_json::Value>,
|
||||
pub group_id: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub extension_group_id: Option<String>,
|
||||
@@ -682,14 +678,6 @@ pub async fn get_api_server_status() -> Result<Option<u16>, String> {
|
||||
Ok(server_guard.get_port())
|
||||
}
|
||||
|
||||
/// Serialize a browser config (camoufox/wayfern) to JSON for an API response.
|
||||
/// Viewing a profile's fingerprint is available to every API caller; only
|
||||
/// editing it (via `update_profile`) and launching/killing profiles
|
||||
/// programmatically require an active paid plan.
|
||||
fn config_to_api_value<T: serde::Serialize>(config: Option<&T>) -> Option<serde_json::Value> {
|
||||
serde_json::to_value(config?).ok()
|
||||
}
|
||||
|
||||
// API Handlers - Profiles
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -720,7 +708,6 @@ async fn get_profiles() -> Result<Json<ApiProfilesResponse>, StatusCode> {
|
||||
process_id: profile.process_id,
|
||||
last_launch: profile.last_launch,
|
||||
release_type: profile.release_type.clone(),
|
||||
camoufox_config: config_to_api_value(profile.camoufox_config.as_ref()),
|
||||
group_id: profile.group_id.clone(),
|
||||
tags: profile.tags.clone(),
|
||||
is_running: profile.process_id.is_some(), // Simple check based on process_id
|
||||
@@ -774,7 +761,6 @@ async fn get_profile(
|
||||
process_id: profile.process_id,
|
||||
last_launch: profile.last_launch,
|
||||
release_type: profile.release_type.clone(),
|
||||
camoufox_config: config_to_api_value(profile.camoufox_config.as_ref()),
|
||||
group_id: profile.group_id.clone(),
|
||||
tags: profile.tags.clone(),
|
||||
is_running: profile.process_id.is_some(), // Simple check based on process_id
|
||||
@@ -792,12 +778,12 @@ async fn get_profile(
|
||||
|
||||
/// Create a profile.
|
||||
///
|
||||
/// - `browser` must be `"wayfern"` or `"camoufox"`; any other value is rejected
|
||||
/// - `browser` must be `"wayfern"`; any other value is rejected
|
||||
/// with 400.
|
||||
/// - `version` is optional: omit it or pass `"latest"` to use the newest
|
||||
/// already-downloaded version of that browser. The version must be present
|
||||
/// locally (this endpoint does not download new versions); 400 if none is.
|
||||
/// - Omitting the matching `wayfern_config`/`camoufox_config`, or passing an
|
||||
/// - Omitting the matching `wayfern_config`, or passing an
|
||||
/// empty object `{}`, generates a fresh fingerprint automatically.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
@@ -821,16 +807,16 @@ async fn create_profile(
|
||||
) -> Result<Json<ApiProfileResponse>, (StatusCode, String)> {
|
||||
let profile_manager = ProfileManager::instance();
|
||||
|
||||
// Only Wayfern and Camoufox profiles are launchable; the rest of the system
|
||||
// Only Wayfern profiles are launchable; the rest of the system
|
||||
// (fingerprint generation, launch, run) supports nothing else. Reject anything
|
||||
// else up front — otherwise the profile is created with no fingerprint and an
|
||||
// unrecognized browser, then crashes with a 500 on /run. Mirrors the MCP
|
||||
// create_profile validation.
|
||||
if request.browser != "wayfern" && request.browser != "camoufox" {
|
||||
if request.browser != "wayfern" {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!(
|
||||
"Invalid browser \"{}\". Must be \"wayfern\" (anti-detect Chromium) or \"camoufox\" (anti-detect Firefox).",
|
||||
"Invalid browser \"{}\". Must be \"wayfern\" (anti-detect Chromium).",
|
||||
request.browser
|
||||
),
|
||||
));
|
||||
@@ -863,13 +849,6 @@ async fn create_profile(
|
||||
}
|
||||
};
|
||||
|
||||
// Parse camoufox config if provided
|
||||
let camoufox_config = if let Some(config) = &request.camoufox_config {
|
||||
serde_json::from_value(config.clone()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Parse wayfern config if provided
|
||||
let wayfern_config = if let Some(config) = &request.wayfern_config {
|
||||
serde_json::from_value(config.clone()).ok()
|
||||
@@ -905,7 +884,6 @@ async fn create_profile(
|
||||
request.release_type.as_deref().unwrap_or("stable"),
|
||||
request.proxy_id.clone(),
|
||||
request.vpn_id.clone(),
|
||||
camoufox_config,
|
||||
wayfern_config,
|
||||
request.group_id.clone(),
|
||||
false,
|
||||
@@ -947,7 +925,6 @@ async fn create_profile(
|
||||
process_id: profile.process_id,
|
||||
last_launch: profile.last_launch,
|
||||
release_type: profile.release_type,
|
||||
camoufox_config: config_to_api_value(profile.camoufox_config.as_ref()),
|
||||
group_id: profile.group_id,
|
||||
tags: profile.tags,
|
||||
is_running: false,
|
||||
@@ -1054,30 +1031,6 @@ async fn update_profile(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(camoufox_config) = request.camoufox_config {
|
||||
// Editing a profile's fingerprint config is part of the cross-OS fingerprint
|
||||
// capability (GUI, API, MCP). Viewing it is free; mutating it is not.
|
||||
if !crate::cloud_auth::CLOUD_AUTH
|
||||
.can_use_cross_os_fingerprints()
|
||||
.await
|
||||
{
|
||||
return Err(StatusCode::PAYMENT_REQUIRED);
|
||||
}
|
||||
let config: Result<CamoufoxConfig, _> = serde_json::from_value(camoufox_config);
|
||||
match config {
|
||||
Ok(config) => {
|
||||
if profile_manager
|
||||
.update_camoufox_config(state.app_handle.clone(), &id, config)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
Err(_) => return Err(StatusCode::BAD_REQUEST),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(group_id) = request.group_id {
|
||||
if profile_manager
|
||||
.assign_profiles_to_group(&state.app_handle, vec![id.clone()], Some(group_id))
|
||||
@@ -2444,7 +2397,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn create_profile_request_allows_omitting_version_and_configs() {
|
||||
// Minimal body: no version, no wayfern_config/camoufox_config. Must
|
||||
// Minimal body: no version, no wayfern_config. Must
|
||||
// deserialize (version resolves to latest-downloaded at the handler; an
|
||||
// absent config triggers fresh-fingerprint generation).
|
||||
let json = r#"{"name": "p", "browser": "wayfern"}"#;
|
||||
@@ -2453,16 +2406,14 @@ mod tests {
|
||||
assert_eq!(parsed.browser, "wayfern");
|
||||
assert!(parsed.version.is_none());
|
||||
assert!(parsed.wayfern_config.is_none());
|
||||
assert!(parsed.camoufox_config.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_profile_browser_validation_matches_supported_engines() {
|
||||
// The handler rejects anything that isn't a launchable engine; this is the
|
||||
// same predicate it uses, kept in lockstep with MCP's create_profile.
|
||||
let is_valid = |b: &str| b == "wayfern" || b == "camoufox";
|
||||
let is_valid = |b: &str| b == "wayfern";
|
||||
assert!(is_valid("wayfern"));
|
||||
assert!(is_valid("camoufox"));
|
||||
assert!(!is_valid("chromium"));
|
||||
assert!(!is_valid("firefox"));
|
||||
assert!(!is_valid(""));
|
||||
|
||||
+21
-167
@@ -13,7 +13,6 @@ pub struct UpdateNotification {
|
||||
pub current_version: String,
|
||||
pub new_version: String,
|
||||
pub affected_profiles: Vec<String>,
|
||||
pub is_stable_update: bool,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
@@ -231,18 +230,10 @@ impl AutoUpdater {
|
||||
available_versions: &[BrowserVersionInfo],
|
||||
) -> Result<Option<UpdateNotification>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let current_version = &profile.version;
|
||||
let is_current_nightly =
|
||||
crate::api_client::is_browser_version_nightly(&profile.browser, current_version, None);
|
||||
|
||||
// Find the best available update
|
||||
let best_update = available_versions
|
||||
.iter()
|
||||
.filter(|v| {
|
||||
// Only consider versions newer than current
|
||||
self.is_version_newer(&v.version, current_version)
|
||||
&& crate::api_client::is_browser_version_nightly(&profile.browser, &v.version, None)
|
||||
== is_current_nightly
|
||||
})
|
||||
.filter(|v| self.is_version_newer(&v.version, current_version))
|
||||
.max_by(|a, b| self.compare_versions(&a.version, &b.version));
|
||||
|
||||
if let Some(update_version) = best_update {
|
||||
@@ -255,7 +246,6 @@ impl AutoUpdater {
|
||||
current_version: current_version.clone(),
|
||||
new_version: update_version.version.clone(),
|
||||
affected_profiles: vec![profile.name.clone()],
|
||||
is_stable_update: !update_version.is_prerelease,
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
@@ -291,12 +281,7 @@ impl AutoUpdater {
|
||||
|
||||
let mut result: Vec<UpdateNotification> = grouped.into_values().collect();
|
||||
|
||||
// Sort by priority: stable updates first, then by timestamp
|
||||
result.sort_by(|a, b| match (a.is_stable_update, b.is_stable_update) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => b.timestamp.cmp(&a.timestamp),
|
||||
});
|
||||
result.sort_by_key(|b| std::cmp::Reverse(b.timestamp));
|
||||
|
||||
result
|
||||
}
|
||||
@@ -338,7 +323,6 @@ impl AutoUpdater {
|
||||
current_version: profile.version.clone(),
|
||||
new_version: new_version.to_string(),
|
||||
affected_profiles: vec![profile.name.clone()],
|
||||
is_stable_update: true,
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
@@ -510,15 +494,6 @@ impl AutoUpdater {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Only update stable->stable and nightly->nightly
|
||||
let is_profile_nightly =
|
||||
crate::api_client::is_browser_version_nightly(&profile.browser, &profile.version, None);
|
||||
let is_latest_nightly =
|
||||
crate::api_client::is_browser_version_nightly(&profile.browser, &latest, None);
|
||||
if is_profile_nightly != is_latest_nightly {
|
||||
return None;
|
||||
}
|
||||
|
||||
match self
|
||||
.profile_manager
|
||||
.update_profile_version(app_handle, &profile.id.to_string(), &latest)
|
||||
@@ -595,15 +570,6 @@ impl AutoUpdater {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only update stable->stable and nightly->nightly
|
||||
let is_profile_nightly =
|
||||
crate::api_client::is_browser_version_nightly(&browser, &profile.version, None);
|
||||
let is_latest_nightly =
|
||||
crate::api_client::is_browser_version_nightly(&browser, &latest_version, None);
|
||||
if is_profile_nightly != is_latest_nightly {
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.profile_manager.update_profile_version(
|
||||
app_handle,
|
||||
&profile.id.to_string(),
|
||||
@@ -686,7 +652,6 @@ mod tests {
|
||||
launch_hook: None,
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
camoufox_config: None,
|
||||
wayfern_config: None,
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
@@ -708,10 +673,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_test_version_info(version: &str, is_prerelease: bool) -> BrowserVersionInfo {
|
||||
fn create_test_version_info(version: &str) -> BrowserVersionInfo {
|
||||
BrowserVersionInfo {
|
||||
version: version.to_string(),
|
||||
is_prerelease,
|
||||
date: "2024-01-01".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -753,111 +717,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camoufox_beta_version_comparison() {
|
||||
fn test_check_profile_update_picks_newer_wayfern_version() {
|
||||
let updater = AutoUpdater::instance();
|
||||
|
||||
// Test the exact user-reported scenario: 135.0.1beta24 vs 135.0beta22
|
||||
assert!(
|
||||
updater.is_version_newer("135.0.1beta24", "135.0beta22"),
|
||||
"135.0.1beta24 should be newer than 135.0beta22"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
updater.compare_versions("135.0.1beta24", "135.0beta22"),
|
||||
std::cmp::Ordering::Greater,
|
||||
"135.0.1beta24 should compare as greater than 135.0beta22"
|
||||
);
|
||||
|
||||
// Test other camoufox beta version combinations
|
||||
assert!(
|
||||
updater.is_version_newer("135.0.5beta24", "135.0.5beta22"),
|
||||
"135.0.5beta24 should be newer than 135.0.5beta22"
|
||||
);
|
||||
|
||||
assert!(
|
||||
updater.is_version_newer("135.0.1beta1", "135.0beta1"),
|
||||
"135.0.1beta1 should be newer than 135.0beta1 due to patch version"
|
||||
);
|
||||
|
||||
// Test that older versions are not considered newer
|
||||
assert!(
|
||||
!updater.is_version_newer("135.0beta22", "135.0.1beta24"),
|
||||
"135.0beta22 should NOT be newer than 135.0.1beta24"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_beta_version_ordering_comprehensive() {
|
||||
let updater = AutoUpdater::instance();
|
||||
|
||||
// Test various beta version patterns that could appear in camoufox
|
||||
let test_cases = vec![
|
||||
("135.0.1beta24", "135.0beta22", true), // User reported case
|
||||
("135.0.5beta24", "135.0.5beta22", true), // Same patch, different beta
|
||||
("135.1beta1", "135.0beta99", true), // Higher minor beats beta number
|
||||
("136.0beta1", "135.9.9beta99", true), // Higher major beats everything
|
||||
("135.0.1beta1", "135.0beta1", true), // Patch version matters
|
||||
("135.0beta22", "135.0.1beta24", false), // Reverse of user case
|
||||
];
|
||||
|
||||
for (newer, older, should_be_newer) in test_cases {
|
||||
let result = updater.is_version_newer(newer, older);
|
||||
assert_eq!(
|
||||
result,
|
||||
should_be_newer,
|
||||
"Expected {} {} {} but got {}",
|
||||
newer,
|
||||
if should_be_newer { ">" } else { "<=" },
|
||||
older,
|
||||
if result { "true" } else { "false" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_profile_update_stable_to_stable() {
|
||||
let updater = AutoUpdater::instance();
|
||||
let profile = create_test_profile("test", "firefox", "1.0.0");
|
||||
let profile = create_test_profile("test", "wayfern", "138.0.7204.49");
|
||||
let versions = vec![
|
||||
create_test_version_info("1.0.1", false), // stable, newer
|
||||
create_test_version_info("1.1.0-alpha", true), // alpha, should be ignored
|
||||
create_test_version_info("0.9.0", false), // stable, older
|
||||
create_test_version_info("138.0.7204.50"),
|
||||
create_test_version_info("138.0.7204.48"),
|
||||
];
|
||||
|
||||
let result = updater.check_profile_update(&profile, &versions).unwrap();
|
||||
assert!(result.is_some());
|
||||
|
||||
let update = result.unwrap();
|
||||
assert_eq!(update.new_version, "1.0.1");
|
||||
assert!(update.is_stable_update);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_profile_update_alpha_to_alpha() {
|
||||
let updater = AutoUpdater::instance();
|
||||
let profile = create_test_profile("test", "firefox", "1.0.0-alpha");
|
||||
let versions = vec![
|
||||
create_test_version_info("1.0.1", false), // stable, should be included
|
||||
create_test_version_info("1.1.0-alpha", true), // alpha, newer
|
||||
create_test_version_info("0.9.0-alpha", true), // alpha, older
|
||||
];
|
||||
|
||||
let result = updater.check_profile_update(&profile, &versions).unwrap();
|
||||
assert!(result.is_some());
|
||||
|
||||
let update = result.unwrap();
|
||||
// Should pick the newest version (alpha user can upgrade to stable or newer alpha)
|
||||
assert_eq!(update.new_version, "1.1.0-alpha");
|
||||
assert!(!update.is_stable_update);
|
||||
assert_eq!(result.unwrap().new_version, "138.0.7204.50");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_profile_update_no_update_available() {
|
||||
let updater = AutoUpdater::instance();
|
||||
let profile = create_test_profile("test", "firefox", "1.0.0");
|
||||
let profile = create_test_profile("test", "wayfern", "138.0.7204.50");
|
||||
let versions = vec![
|
||||
create_test_version_info("0.9.0", false), // older
|
||||
create_test_version_info("1.0.0", false), // same version
|
||||
create_test_version_info("138.0.7204.49"),
|
||||
create_test_version_info("138.0.7204.50"),
|
||||
];
|
||||
|
||||
let result = updater.check_profile_update(&profile, &versions).unwrap();
|
||||
@@ -869,50 +748,27 @@ mod tests {
|
||||
let updater = AutoUpdater::instance();
|
||||
let notifications = vec![
|
||||
UpdateNotification {
|
||||
id: "firefox_1.0.0_to_1.1.0_profile1".to_string(),
|
||||
browser: "firefox".to_string(),
|
||||
current_version: "1.0.0".to_string(),
|
||||
new_version: "1.1.0".to_string(),
|
||||
id: "wayfern_138.0.7204.49_to_138.0.7204.50_profile1".to_string(),
|
||||
browser: "wayfern".to_string(),
|
||||
current_version: "138.0.7204.49".to_string(),
|
||||
new_version: "138.0.7204.50".to_string(),
|
||||
affected_profiles: vec!["profile1".to_string()],
|
||||
is_stable_update: true,
|
||||
timestamp: 1000,
|
||||
},
|
||||
UpdateNotification {
|
||||
id: "firefox_1.0.0_to_1.1.0_profile2".to_string(),
|
||||
browser: "firefox".to_string(),
|
||||
current_version: "1.0.0".to_string(),
|
||||
new_version: "1.1.0".to_string(),
|
||||
id: "wayfern_138.0.7204.49_to_138.0.7204.50_profile2".to_string(),
|
||||
browser: "wayfern".to_string(),
|
||||
current_version: "138.0.7204.49".to_string(),
|
||||
new_version: "138.0.7204.50".to_string(),
|
||||
affected_profiles: vec!["profile2".to_string()],
|
||||
is_stable_update: true,
|
||||
timestamp: 1001,
|
||||
},
|
||||
UpdateNotification {
|
||||
id: "chrome_1.0.0_to_1.1.0-alpha".to_string(),
|
||||
browser: "chrome".to_string(),
|
||||
current_version: "1.0.0".to_string(),
|
||||
new_version: "1.1.0-alpha".to_string(),
|
||||
affected_profiles: vec!["profile3".to_string()],
|
||||
is_stable_update: false,
|
||||
timestamp: 1002,
|
||||
},
|
||||
];
|
||||
|
||||
let grouped = updater.group_update_notifications(notifications);
|
||||
|
||||
assert_eq!(grouped.len(), 2);
|
||||
|
||||
// Find the Firefox notification
|
||||
let firefox_notification = grouped.iter().find(|n| n.browser == "firefox").unwrap();
|
||||
assert_eq!(firefox_notification.affected_profiles.len(), 2);
|
||||
assert!(firefox_notification
|
||||
.affected_profiles
|
||||
.contains(&"profile1".to_string()));
|
||||
assert!(firefox_notification
|
||||
.affected_profiles
|
||||
.contains(&"profile2".to_string()));
|
||||
|
||||
// Stable updates should come first
|
||||
assert!(grouped[0].is_stable_update);
|
||||
assert_eq!(grouped.len(), 1);
|
||||
assert_eq!(grouped[0].affected_profiles.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -956,7 +812,6 @@ mod tests {
|
||||
current_version: "1.0.0".to_string(),
|
||||
new_version: "1.1.0".to_string(),
|
||||
affected_profiles: vec!["profile1".to_string()],
|
||||
is_stable_update: true,
|
||||
timestamp: 1000,
|
||||
});
|
||||
|
||||
@@ -1094,7 +949,6 @@ mod tests {
|
||||
current_version: "1.0.0".to_string(),
|
||||
new_version: "1.1.0".to_string(),
|
||||
affected_profiles: vec!["profile1".to_string()],
|
||||
is_stable_update: true,
|
||||
timestamp: 1000,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,21 +13,18 @@ pub struct ProxySettings {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum BrowserType {
|
||||
Camoufox,
|
||||
Wayfern,
|
||||
}
|
||||
|
||||
impl BrowserType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
BrowserType::Camoufox => "camoufox",
|
||||
BrowserType::Wayfern => "wayfern",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Result<Self, String> {
|
||||
match s {
|
||||
"camoufox" => Ok(BrowserType::Camoufox),
|
||||
"wayfern" => Ok(BrowserType::Wayfern),
|
||||
_ => Err(format!("Unknown browser type: {s}")),
|
||||
}
|
||||
@@ -54,135 +51,6 @@ pub trait Browser: Send + Sync {
|
||||
mod macos {
|
||||
use super::*;
|
||||
|
||||
pub fn get_firefox_executable_path(
|
||||
install_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Find the .app directory
|
||||
let app_path = std::fs::read_dir(install_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.ok_or("Browser app not found")?;
|
||||
|
||||
// Construct the browser executable path
|
||||
let mut executable_dir = app_path.path();
|
||||
executable_dir.push("Contents");
|
||||
executable_dir.push("MacOS");
|
||||
|
||||
// Find executables matching the browser name pattern
|
||||
let candidates: Vec<_> = std::fs::read_dir(&executable_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| {
|
||||
let binding = entry.file_name();
|
||||
let name = binding.to_string_lossy();
|
||||
name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("Browser")
|
||||
})
|
||||
.map(|entry| entry.path())
|
||||
.collect();
|
||||
|
||||
if candidates.is_empty() {
|
||||
return Err("No executable found in MacOS directory".into());
|
||||
}
|
||||
|
||||
// For Camoufox, validate architecture compatibility
|
||||
let executable_path = if candidates.iter().any(|p| {
|
||||
p.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| n.starts_with("camoufox"))
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
// Find the executable that matches the current architecture
|
||||
let current_arch = if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"arm64"
|
||||
} else {
|
||||
return Err("Unsupported architecture".into());
|
||||
};
|
||||
|
||||
// Try to find an executable that matches the current architecture
|
||||
// Use file command to check architecture
|
||||
let mut found_executable = None;
|
||||
let mut file_command_available = true;
|
||||
|
||||
for candidate in &candidates {
|
||||
match std::process::Command::new("file").arg(candidate).output() {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
if let Ok(output_str) = String::from_utf8(output.stdout) {
|
||||
let is_compatible = if current_arch == "x86_64" {
|
||||
output_str.contains("x86_64") || output_str.contains("i386")
|
||||
} else {
|
||||
output_str.contains("arm64") || output_str.contains("aarch64")
|
||||
};
|
||||
|
||||
if is_compatible {
|
||||
found_executable = Some(candidate.clone());
|
||||
log::info!(
|
||||
"Found compatible Camoufox executable for {}: {}",
|
||||
current_arch,
|
||||
candidate.display()
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
log::warn!(
|
||||
"Skipping incompatible Camoufox executable: {} (architecture: {})",
|
||||
candidate.display(),
|
||||
output_str.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Failed to check architecture for {}: file command returned non-zero exit code",
|
||||
candidate.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to check architecture for {} using file command: {}",
|
||||
candidate.display(),
|
||||
e
|
||||
);
|
||||
file_command_available = false;
|
||||
// Continue checking other candidates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no compatible executable found but we have candidates, use the first one
|
||||
// (fallback for cases where file command isn't available or failed)
|
||||
if found_executable.is_none() && !candidates.is_empty() {
|
||||
if !file_command_available {
|
||||
log::warn!(
|
||||
"file command not available, using first candidate: {}",
|
||||
candidates[0].display()
|
||||
);
|
||||
} else {
|
||||
log::warn!(
|
||||
"No compatible executable found for architecture {}, using first candidate: {}",
|
||||
current_arch,
|
||||
candidates[0].display()
|
||||
);
|
||||
}
|
||||
found_executable = Some(candidates[0].clone());
|
||||
}
|
||||
|
||||
found_executable.ok_or_else(|| {
|
||||
format!(
|
||||
"No compatible Camoufox executable found for architecture {}. Available executables: {:?}",
|
||||
current_arch,
|
||||
candidates
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
// For other browsers, use the first matching executable
|
||||
candidates[0].clone()
|
||||
};
|
||||
|
||||
Ok(executable_path)
|
||||
}
|
||||
|
||||
pub fn get_wayfern_executable_path(
|
||||
install_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
@@ -224,18 +92,6 @@ mod macos {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool {
|
||||
// On macOS, check for .app files
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
if entry.path().extension().is_some_and(|ext| ext == "app") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn prepare_executable(_executable_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// On macOS, no special preparation needed
|
||||
@@ -248,43 +104,6 @@ mod linux {
|
||||
use super::*;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
pub fn get_firefox_executable_path(
|
||||
install_dir: &Path,
|
||||
browser_type: &BrowserType,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Expected structure examples:
|
||||
// - Firefox/Firefox Developer on Linux often extract to: install_dir/firefox/firefox
|
||||
// - Some archives may extract directly under: install_dir/firefox or install_dir/firefox-bin
|
||||
// - For some flavors we may have: install_dir/<browser_type>/<binary>
|
||||
let _browser_subdir = install_dir.join(browser_type.as_str());
|
||||
|
||||
// Try common firefox executable locations (nested and flat)
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Camoufox => {
|
||||
vec![
|
||||
install_dir.join("camoufox-bin"),
|
||||
install_dir.join("camoufox"),
|
||||
]
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for executable_path in &possible_executables {
|
||||
if executable_path.exists() && executable_path.is_file() {
|
||||
return Ok(executable_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Err(
|
||||
format!(
|
||||
"Executable not found for {} in {}",
|
||||
browser_type.as_str(),
|
||||
install_dir.display(),
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_chromium_executable_path(
|
||||
install_dir: &Path,
|
||||
browser_type: &BrowserType,
|
||||
@@ -317,32 +136,6 @@ mod linux {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_firefox_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
||||
// Expected structure (most common):
|
||||
// install_dir/<browser>/<binary>
|
||||
// However, Firefox Developer tarballs often extract to a "firefox" subfolder
|
||||
// rather than "firefox-developer". Support both layouts.
|
||||
let _browser_subdir = install_dir.join(browser_type.as_str());
|
||||
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Camoufox => {
|
||||
vec![
|
||||
install_dir.join("camoufox-bin"),
|
||||
install_dir.join("camoufox"),
|
||||
]
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
for exe_path in &possible_executables {
|
||||
if exe_path.exists() && exe_path.is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
||||
let possible_executables = match browser_type {
|
||||
BrowserType::Wayfern => vec![
|
||||
@@ -391,43 +184,6 @@ mod linux {
|
||||
mod windows {
|
||||
use super::*;
|
||||
|
||||
pub fn get_firefox_executable_path(
|
||||
install_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// On Windows, look for firefox.exe
|
||||
let possible_paths = [
|
||||
install_dir.join("firefox.exe"),
|
||||
install_dir.join("firefox").join("firefox.exe"),
|
||||
install_dir.join("bin").join("firefox.exe"),
|
||||
];
|
||||
|
||||
for path in &possible_paths {
|
||||
if path.exists() && path.is_file() {
|
||||
return Ok(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Look for any .exe file that might be the browser
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||
let name = path
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase();
|
||||
if name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("browser")
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Firefox executable not found in Windows installation directory".into())
|
||||
}
|
||||
|
||||
pub fn get_chromium_executable_path(
|
||||
install_dir: &Path,
|
||||
browser_type: &BrowserType,
|
||||
@@ -472,42 +228,6 @@ mod windows {
|
||||
Err("Chromium/Wayfern executable not found in Windows installation directory".into())
|
||||
}
|
||||
|
||||
pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool {
|
||||
// On Windows, check for .exe files
|
||||
let possible_executables = [
|
||||
install_dir.join("firefox.exe"),
|
||||
install_dir.join("firefox").join("firefox.exe"),
|
||||
install_dir.join("bin").join("firefox.exe"),
|
||||
];
|
||||
|
||||
for exe_path in &possible_executables {
|
||||
if exe_path.exists() && exe_path.is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any .exe file that looks like a browser
|
||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||
let name = path
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase();
|
||||
if name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("browser")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
||||
// On Windows, check for .exe files
|
||||
let possible_executables = match browser_type {
|
||||
@@ -557,94 +277,6 @@ mod windows {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CamoufoxBrowser;
|
||||
|
||||
impl CamoufoxBrowser {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Browser for CamoufoxBrowser {
|
||||
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::get_firefox_executable_path(install_dir);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::get_firefox_executable_path(install_dir, &BrowserType::Camoufox);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::get_firefox_executable_path(install_dir);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
Err("Unsupported platform".into())
|
||||
}
|
||||
|
||||
fn create_launch_args(
|
||||
&self,
|
||||
profile_path: &str,
|
||||
_proxy_settings: Option<&ProxySettings>,
|
||||
url: Option<String>,
|
||||
remote_debugging_port: Option<u16>,
|
||||
headless: bool,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||
// For Camoufox, we handle launching through the camoufox launcher
|
||||
// This method won't be used directly, but we provide basic Firefox args as fallback
|
||||
let mut args = vec![
|
||||
"-profile".to_string(),
|
||||
profile_path.to_string(),
|
||||
"-no-remote".to_string(),
|
||||
];
|
||||
|
||||
// Add remote debugging if requested
|
||||
if let Some(port) = remote_debugging_port {
|
||||
args.push("--start-debugger-server".to_string());
|
||||
args.push(port.to_string());
|
||||
}
|
||||
|
||||
// Add headless mode if requested
|
||||
if headless {
|
||||
args.push("--headless".to_string());
|
||||
}
|
||||
|
||||
if let Some(url) = url {
|
||||
args.push(url);
|
||||
}
|
||||
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool {
|
||||
let install_dir = binaries_dir.join("camoufox").join(version);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::is_firefox_version_downloaded(&install_dir);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::is_firefox_version_downloaded(&install_dir, &BrowserType::Camoufox);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::is_firefox_version_downloaded(&install_dir);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
false
|
||||
}
|
||||
|
||||
fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "macos")]
|
||||
return macos::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return linux::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return windows::prepare_executable(executable_path);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
Err("Unsupported platform".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Wayfern is a Chromium-based anti-detect browser with CDP-based fingerprint injection
|
||||
pub struct WayfernBrowser;
|
||||
|
||||
@@ -765,7 +397,6 @@ impl BrowserFactory {
|
||||
|
||||
pub fn create_browser(&self, browser_type: BrowserType) -> Box<dyn Browser> {
|
||||
match browser_type {
|
||||
BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()),
|
||||
BrowserType::Wayfern => Box::new(WayfernBrowser::new()),
|
||||
}
|
||||
}
|
||||
@@ -852,292 +483,6 @@ pub struct GithubAsset {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_browser_type_conversions() {
|
||||
// Test as_str
|
||||
assert_eq!(BrowserType::Camoufox.as_str(), "camoufox");
|
||||
assert_eq!(BrowserType::Wayfern.as_str(), "wayfern");
|
||||
|
||||
// Test from_str
|
||||
assert_eq!(
|
||||
BrowserType::from_str("camoufox").expect("camoufox should be valid"),
|
||||
BrowserType::Camoufox
|
||||
);
|
||||
assert_eq!(
|
||||
BrowserType::from_str("wayfern").expect("wayfern should be valid"),
|
||||
BrowserType::Wayfern
|
||||
);
|
||||
|
||||
// Test invalid browser type - these should properly fail
|
||||
let invalid_result = BrowserType::from_str("invalid");
|
||||
assert!(
|
||||
invalid_result.is_err(),
|
||||
"Invalid browser type should return error"
|
||||
);
|
||||
|
||||
let empty_result = BrowserType::from_str("");
|
||||
assert!(empty_result.is_err(), "Empty string should return error");
|
||||
|
||||
assert!(
|
||||
BrowserType::from_str("firefox").is_err(),
|
||||
"Removed browser types should return error"
|
||||
);
|
||||
assert!(
|
||||
BrowserType::from_str("chromium").is_err(),
|
||||
"Removed browser types should return error"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camoufox_launch_args() {
|
||||
let browser = CamoufoxBrowser::new();
|
||||
let args = browser
|
||||
.create_launch_args("/path/to/profile", None, None, None, false)
|
||||
.expect("Failed to create launch args for Camoufox");
|
||||
assert!(args.contains(&"-profile".to_string()));
|
||||
assert!(args.contains(&"/path/to/profile".to_string()));
|
||||
assert!(args.contains(&"-no-remote".to_string()));
|
||||
|
||||
let args = browser
|
||||
.create_launch_args(
|
||||
"/path/to/profile",
|
||||
None,
|
||||
Some("https://example.com".to_string()),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create launch args for Camoufox with URL");
|
||||
assert!(args.contains(&"https://example.com".to_string()));
|
||||
|
||||
// Test with remote debugging
|
||||
let args = browser
|
||||
.create_launch_args("/path/to/profile", None, None, Some(9222), false)
|
||||
.expect("Failed to create launch args for Camoufox with remote debugging");
|
||||
assert!(args.contains(&"--start-debugger-server".to_string()));
|
||||
assert!(args.contains(&"9222".to_string()));
|
||||
|
||||
// Test headless mode
|
||||
let args = browser
|
||||
.create_launch_args("/path/to/profile", None, None, None, true)
|
||||
.expect("Failed to create launch args for Camoufox headless");
|
||||
assert!(
|
||||
args.contains(&"--headless".to_string()),
|
||||
"Browser should include headless flag when requested"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wayfern_launch_args() {
|
||||
let browser = WayfernBrowser::new();
|
||||
let args = browser
|
||||
.create_launch_args("/path/to/profile", None, None, None, false)
|
||||
.expect("Failed to create launch args for Wayfern");
|
||||
|
||||
assert!(
|
||||
args.contains(&"--user-data-dir=/path/to/profile".to_string()),
|
||||
"Wayfern args should contain user-data-dir"
|
||||
);
|
||||
assert!(
|
||||
args.contains(&"--no-default-browser-check".to_string()),
|
||||
"Wayfern args should contain no-default-browser-check"
|
||||
);
|
||||
assert!(
|
||||
args.contains(&"--disable-background-mode".to_string()),
|
||||
"Wayfern args should contain disable-background-mode"
|
||||
);
|
||||
assert!(
|
||||
args.contains(&"--disable-component-update".to_string()),
|
||||
"Wayfern args should contain disable-component-update"
|
||||
);
|
||||
|
||||
let args_with_url = browser
|
||||
.create_launch_args(
|
||||
"/path/to/profile",
|
||||
None,
|
||||
Some("https://example.com".to_string()),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create launch args for Wayfern with URL");
|
||||
assert!(
|
||||
args_with_url.contains(&"https://example.com".to_string()),
|
||||
"Wayfern args should contain the URL"
|
||||
);
|
||||
assert_eq!(
|
||||
args_with_url.last().expect("Args should not be empty"),
|
||||
"https://example.com"
|
||||
);
|
||||
|
||||
// Test remote debugging
|
||||
let args_with_debug = browser
|
||||
.create_launch_args("/path/to/profile", None, None, Some(9222), false)
|
||||
.expect("Failed to create launch args for Wayfern with remote debugging");
|
||||
assert!(
|
||||
args_with_debug.contains(&"--remote-debugging-port=9222".to_string()),
|
||||
"Wayfern args should contain remote debugging port"
|
||||
);
|
||||
|
||||
// Test headless mode
|
||||
let args_headless = browser
|
||||
.create_launch_args("/path/to/profile", None, None, None, true)
|
||||
.expect("Failed to create launch args for Wayfern headless");
|
||||
assert!(
|
||||
args_headless.contains(&"--headless=new".to_string()),
|
||||
"Wayfern args should contain headless flag when requested"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxy_settings_creation() {
|
||||
let proxy = ProxySettings {
|
||||
proxy_type: "http".to_string(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8080,
|
||||
username: None,
|
||||
password: None,
|
||||
};
|
||||
|
||||
assert_eq!(proxy.proxy_type, "http");
|
||||
assert_eq!(proxy.host, "127.0.0.1");
|
||||
assert_eq!(proxy.port, 8080);
|
||||
|
||||
// Test different proxy types
|
||||
let socks_proxy = ProxySettings {
|
||||
proxy_type: "socks5".to_string(),
|
||||
host: "proxy.example.com".to_string(),
|
||||
port: 1080,
|
||||
username: None,
|
||||
password: None,
|
||||
};
|
||||
|
||||
assert_eq!(socks_proxy.proxy_type, "socks5");
|
||||
assert_eq!(socks_proxy.host, "proxy.example.com");
|
||||
assert_eq!(socks_proxy.port, 1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_downloaded_check() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let binaries_dir = temp_dir.path();
|
||||
|
||||
// Create a mock Camoufox browser installation
|
||||
let browser_dir = binaries_dir.join("camoufox").join("135.0.1");
|
||||
fs::create_dir_all(&browser_dir).expect("Failed to create browser directory");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let app_dir = browser_dir.join("Camoufox.app");
|
||||
fs::create_dir_all(&app_dir).expect("Failed to create Camoufox.app directory");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let executable_path = browser_dir.join("camoufox");
|
||||
fs::write(&executable_path, "mock executable").expect("Failed to write mock executable");
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = executable_path
|
||||
.metadata()
|
||||
.expect("Failed to get file metadata")
|
||||
.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&executable_path, permissions)
|
||||
.expect("Failed to set executable permissions");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let executable_path = browser_dir.join("firefox.exe");
|
||||
fs::write(&executable_path, "mock executable").expect("Failed to write mock executable");
|
||||
}
|
||||
|
||||
let browser = CamoufoxBrowser::new();
|
||||
assert!(browser.is_version_downloaded("135.0.1", binaries_dir));
|
||||
assert!(!browser.is_version_downloaded("999.0", binaries_dir));
|
||||
|
||||
// Test with Wayfern browser
|
||||
let wayfern_dir = binaries_dir.join("wayfern").join("1.0.0");
|
||||
fs::create_dir_all(&wayfern_dir).expect("Failed to create wayfern directory");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let wayfern_app_dir = wayfern_dir.join("Chromium.app");
|
||||
fs::create_dir_all(wayfern_app_dir.join("Contents").join("MacOS"))
|
||||
.expect("Failed to create Chromium.app structure");
|
||||
|
||||
let executable_path = wayfern_app_dir
|
||||
.join("Contents")
|
||||
.join("MacOS")
|
||||
.join("Chromium");
|
||||
fs::write(&executable_path, "mock executable")
|
||||
.expect("Failed to write mock Wayfern executable");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let executable_path = wayfern_dir.join("chromium");
|
||||
fs::write(&executable_path, "mock executable")
|
||||
.expect("Failed to write mock wayfern executable");
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = executable_path
|
||||
.metadata()
|
||||
.expect("Failed to get wayfern metadata")
|
||||
.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&executable_path, permissions)
|
||||
.expect("Failed to set wayfern permissions");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let executable_path = wayfern_dir.join("chromium.exe");
|
||||
fs::write(&executable_path, "mock executable").expect("Failed to write mock chromium.exe");
|
||||
}
|
||||
|
||||
let wayfern_browser = WayfernBrowser::new();
|
||||
assert!(
|
||||
wayfern_browser.is_version_downloaded("1.0.0", binaries_dir),
|
||||
"Wayfern version should be detected as downloaded"
|
||||
);
|
||||
assert!(
|
||||
!wayfern_browser.is_version_downloaded("9.9.9", binaries_dir),
|
||||
"Non-existent Wayfern version should not be detected as downloaded"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_downloaded_no_app_directory() {
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
||||
let binaries_dir = temp_dir.path();
|
||||
|
||||
// Create browser directory but no proper executable structure
|
||||
let browser_dir = binaries_dir.join("camoufox").join("135.0.1");
|
||||
fs::create_dir_all(&browser_dir).expect("Failed to create browser directory");
|
||||
|
||||
// Create some other files but no proper executable structure
|
||||
fs::write(browser_dir.join("readme.txt"), "Some content").expect("Failed to write readme file");
|
||||
|
||||
let browser = CamoufoxBrowser::new();
|
||||
assert!(
|
||||
!browser.is_version_downloaded("135.0.1", binaries_dir),
|
||||
"Camoufox version should not be detected without proper executable structure"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_browser_type_clone_and_debug() {
|
||||
let browser_type = BrowserType::Camoufox;
|
||||
let cloned = browser_type.clone();
|
||||
assert_eq!(browser_type, cloned);
|
||||
|
||||
// Test Debug trait
|
||||
let debug_str = format!("{browser_type:?}");
|
||||
assert!(debug_str.contains("Camoufox"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxy_settings_serialization() {
|
||||
@@ -1177,17 +522,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camoufox_config_has_no_executable_path() {
|
||||
// Verify CamoufoxConfig does not store executable_path
|
||||
let config = crate::camoufox_manager::CamoufoxConfig::default();
|
||||
let json = serde_json::to_value(&config).unwrap();
|
||||
assert!(
|
||||
json.get("executable_path").is_none(),
|
||||
"CamoufoxConfig should not have executable_path field"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_profile_data_path_is_dynamic() {
|
||||
use crate::profile::BrowserProfile;
|
||||
@@ -1203,7 +537,6 @@ mod tests {
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
camoufox_config: None,
|
||||
wayfern_config: None,
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
|
||||
+7
-1200
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,10 @@
|
||||
use crate::api_client::{sort_versions, ApiClient, BrowserRelease};
|
||||
use crate::browser::GithubRelease;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BrowserVersionInfo {
|
||||
pub version: String,
|
||||
pub is_prerelease: bool,
|
||||
pub date: String,
|
||||
}
|
||||
|
||||
@@ -20,7 +18,6 @@ pub struct BrowserVersionsResult {
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BrowserReleaseTypes {
|
||||
pub stable: Option<String>,
|
||||
pub nightly: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -53,33 +50,8 @@ impl BrowserVersionManager {
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
match browser {
|
||||
"firefox" | "firefox-developer" => Ok(true),
|
||||
"zen" => {
|
||||
// Zen supports all platforms and architectures
|
||||
Ok(true)
|
||||
}
|
||||
"brave" => {
|
||||
// Brave supports all platforms and architectures
|
||||
Ok(true)
|
||||
}
|
||||
"chromium" => {
|
||||
// Chromium doesn't support ARM64 on Linux
|
||||
if arch == "arm64" && os == "linux" {
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
"camoufox" => {
|
||||
// Camoufox supports all platforms and architectures according to the JS code
|
||||
Ok(true)
|
||||
}
|
||||
"wayfern" => {
|
||||
// Wayfern support depends on version.json downloads availability
|
||||
// Currently supports macos-arm64 and linux-x64
|
||||
let platform_key = format!("{os}-{arch}");
|
||||
// Check dynamically, but allow the browser to appear even if platform not available yet
|
||||
// The actual download will fail gracefully if not supported
|
||||
Ok(matches!(
|
||||
platform_key.as_str(),
|
||||
"macos-arm64"
|
||||
@@ -96,15 +68,7 @@ impl BrowserVersionManager {
|
||||
|
||||
/// Get list of browsers supported on the current platform
|
||||
pub fn get_supported_browsers(&self) -> Vec<String> {
|
||||
let all_browsers = vec![
|
||||
"firefox",
|
||||
"firefox-developer",
|
||||
"zen",
|
||||
"brave",
|
||||
"chromium",
|
||||
"camoufox",
|
||||
"wayfern",
|
||||
];
|
||||
let all_browsers = vec!["wayfern"];
|
||||
|
||||
all_browsers
|
||||
.into_iter()
|
||||
@@ -115,13 +79,6 @@ impl BrowserVersionManager {
|
||||
|
||||
/// Get cached browser versions immediately (returns None if no cache exists)
|
||||
pub fn get_cached_browser_versions(&self, browser: &str) -> Option<Vec<String>> {
|
||||
if browser == "brave" {
|
||||
return self
|
||||
.api_client
|
||||
.get_cached_github_releases("brave")
|
||||
.map(|releases| releases.into_iter().map(|r| r.tag_name).collect());
|
||||
}
|
||||
|
||||
self
|
||||
.api_client
|
||||
.load_cached_versions(browser)
|
||||
@@ -133,20 +90,6 @@ impl BrowserVersionManager {
|
||||
&self,
|
||||
browser: &str,
|
||||
) -> Option<Vec<BrowserVersionInfo>> {
|
||||
if browser == "brave" {
|
||||
if let Some(releases) = self.api_client.get_cached_github_releases("brave") {
|
||||
let detailed_info: Vec<BrowserVersionInfo> = releases
|
||||
.into_iter()
|
||||
.map(|r| BrowserVersionInfo {
|
||||
version: r.tag_name,
|
||||
is_prerelease: r.is_nightly,
|
||||
date: r.published_at,
|
||||
})
|
||||
.collect();
|
||||
return Some(detailed_info);
|
||||
}
|
||||
}
|
||||
|
||||
let cached_releases = self.api_client.load_cached_versions(browser)?;
|
||||
|
||||
// Convert cached versions to detailed info (without dates since cache doesn't store them)
|
||||
@@ -154,7 +97,6 @@ impl BrowserVersionManager {
|
||||
.into_iter()
|
||||
.map(|r| BrowserVersionInfo {
|
||||
version: r.version,
|
||||
is_prerelease: r.is_prerelease,
|
||||
date: r.date,
|
||||
})
|
||||
.collect();
|
||||
@@ -167,44 +109,25 @@ impl BrowserVersionManager {
|
||||
self.api_client.is_cache_expired(browser)
|
||||
}
|
||||
|
||||
/// Get latest stable and nightly versions for a browser (cached first)
|
||||
/// Get the latest Wayfern version (cached first)
|
||||
pub async fn get_browser_release_types(
|
||||
&self,
|
||||
browser: &str,
|
||||
) -> Result<BrowserReleaseTypes, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Try to get from cache first
|
||||
if browser != "wayfern" {
|
||||
return Err(format!("Unsupported browser: {browser}").into());
|
||||
}
|
||||
|
||||
if let Some(cached_versions) = self.get_cached_browser_versions_detailed(browser) {
|
||||
let latest_stable = cached_versions
|
||||
.iter()
|
||||
.find(|v| !v.is_prerelease)
|
||||
.map(|v| v.version.clone());
|
||||
|
||||
let latest_nightly = cached_versions
|
||||
.iter()
|
||||
.find(|v| v.is_prerelease)
|
||||
.map(|v| v.version.clone());
|
||||
|
||||
return Ok(BrowserReleaseTypes {
|
||||
stable: latest_stable,
|
||||
nightly: latest_nightly,
|
||||
stable: cached_versions.first().map(|v| v.version.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?;
|
||||
|
||||
let latest_stable = detailed_versions
|
||||
.iter()
|
||||
.find(|v| !v.is_prerelease)
|
||||
.map(|v| v.version.clone());
|
||||
|
||||
let latest_nightly = detailed_versions
|
||||
.iter()
|
||||
.find(|v| v.is_prerelease)
|
||||
.map(|v| v.version.clone());
|
||||
|
||||
Ok(BrowserReleaseTypes {
|
||||
stable: latest_stable,
|
||||
nightly: latest_nightly,
|
||||
stable: detailed_versions.first().map(|v| v.version.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -235,12 +158,6 @@ impl BrowserVersionManager {
|
||||
|
||||
// Fetch fresh versions from API
|
||||
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?,
|
||||
"zen" => self.fetch_zen_versions(true).await?,
|
||||
"brave" => self.fetch_brave_versions(true).await?,
|
||||
"chromium" => self.fetch_chromium_versions(true).await?,
|
||||
"camoufox" => self.fetch_camoufox_versions(true).await?,
|
||||
"wayfern" => self.fetch_wayfern_versions(true).await?,
|
||||
_ => return Err(format!("Unsupported browser: {browser}").into()),
|
||||
};
|
||||
@@ -262,13 +179,12 @@ impl BrowserVersionManager {
|
||||
crate::api_client::sort_versions(&mut merged_versions);
|
||||
|
||||
// Save the merged cache (unless explicitly bypassing cache)
|
||||
if !no_caching && browser != "brave" {
|
||||
if !no_caching {
|
||||
let merged_releases: Vec<BrowserRelease> = merged_versions
|
||||
.iter()
|
||||
.map(|v| BrowserRelease {
|
||||
version: v.clone(),
|
||||
date: "".to_string(),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(browser, v, None),
|
||||
})
|
||||
.collect();
|
||||
if let Err(e) = self
|
||||
@@ -305,157 +221,14 @@ impl BrowserVersionManager {
|
||||
// Since we don't have detailed date/prerelease info for cached versions,
|
||||
// we'll fetch fresh detailed info and map it to our merged versions
|
||||
let detailed_info: Vec<BrowserVersionInfo> = match browser {
|
||||
"firefox" => {
|
||||
let releases = self.fetch_firefox_releases_detailed(true).await?;
|
||||
merged_versions
|
||||
.into_iter()
|
||||
.map(|version| {
|
||||
// Try to find matching release info, otherwise create basic info
|
||||
if let Some(release) = releases.iter().find(|r| r.version == version) {
|
||||
BrowserVersionInfo {
|
||||
version: release.version.clone(),
|
||||
is_prerelease: release.is_prerelease,
|
||||
date: release.date.clone(),
|
||||
}
|
||||
} else {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(
|
||||
"firefox", &version, None,
|
||||
),
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
"firefox-developer" => {
|
||||
let releases = self.fetch_firefox_developer_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: release.is_prerelease,
|
||||
date: release.date.clone(),
|
||||
}
|
||||
} else {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(
|
||||
"firefox-developer",
|
||||
&version,
|
||||
None,
|
||||
),
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
"zen" => {
|
||||
let releases = self.fetch_zen_releases_detailed(true).await?;
|
||||
merged_versions
|
||||
.into_iter()
|
||||
// Filter out twilight releases at the detailed level too
|
||||
.filter(|version| version.to_lowercase() != "twilight")
|
||||
.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: crate::api_client::is_browser_version_nightly("zen", &version, None),
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
"brave" => {
|
||||
let releases = self.fetch_brave_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: crate::api_client::is_browser_version_nightly(
|
||||
"brave", &version, None,
|
||||
),
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
"chromium" => {
|
||||
let releases = self.fetch_chromium_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: release.is_prerelease,
|
||||
date: release.date.clone(),
|
||||
}
|
||||
} else {
|
||||
BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: false, // Chromium usually stable releases
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
"camoufox" => {
|
||||
let releases = self.fetch_camoufox_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, // Camoufox usually stable releases
|
||||
date: "".to_string(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
"wayfern" => {
|
||||
// Wayfern only has one version from version.json
|
||||
merged_versions
|
||||
.into_iter()
|
||||
.map(|version| BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
is_prerelease: false, // Wayfern releases are always stable
|
||||
date: "".to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Unsupported browser: {browser}").into());
|
||||
}
|
||||
"wayfern" => merged_versions
|
||||
.into_iter()
|
||||
.map(|version| BrowserVersionInfo {
|
||||
version: version.clone(),
|
||||
date: "".to_string(),
|
||||
})
|
||||
.collect(),
|
||||
_ => return Err(format!("Unsupported browser: {browser}").into()),
|
||||
};
|
||||
|
||||
Ok(detailed_info)
|
||||
@@ -493,7 +266,6 @@ impl BrowserVersionManager {
|
||||
.map(|v| BrowserRelease {
|
||||
version: v.clone(),
|
||||
date: "".to_string(),
|
||||
is_prerelease: crate::api_client::is_browser_version_nightly(browser, v, None),
|
||||
})
|
||||
.collect();
|
||||
if let Err(e) = self.api_client.save_cached_versions(browser, &releases) {
|
||||
@@ -512,170 +284,6 @@ impl BrowserVersionManager {
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
match browser {
|
||||
"firefox" => {
|
||||
let (platform_path, filename, is_archive) = match (&os[..], &arch[..]) {
|
||||
("windows", "x64") => ("win64", format!("Firefox Setup {version}.exe"), false),
|
||||
("windows", "arm64") => (
|
||||
"win64-aarch64",
|
||||
format!("Firefox Setup {version}.exe"),
|
||||
false,
|
||||
),
|
||||
("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.xz"), true),
|
||||
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true),
|
||||
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
|
||||
_ => {
|
||||
return Err(
|
||||
format!("Unsupported platform/architecture for Firefox: {os}/{arch}").into(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://download-installer.cdn.mozilla.net/pub/firefox/releases/{version}/{platform_path}/en-US/{filename}"
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"firefox-developer" => {
|
||||
let (platform_path, filename, is_archive) = match (&os[..], &arch[..]) {
|
||||
("windows", "x64") => ("win64", format!("Firefox Setup {version}.exe"), false),
|
||||
("windows", "arm64") => (
|
||||
"win64-aarch64",
|
||||
format!("Firefox Setup {version}.exe"),
|
||||
false,
|
||||
),
|
||||
("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.xz"), true),
|
||||
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true),
|
||||
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
|
||||
_ => {
|
||||
return Err(
|
||||
format!("Unsupported platform/architecture for Firefox Developer: {os}/{arch}")
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://download-installer.cdn.mozilla.net/pub/devedition/releases/{version}/{platform_path}/en-US/{filename}"
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"zen" => {
|
||||
let (asset_name, filename, is_archive) = match (&os[..], &arch[..]) {
|
||||
("windows", "x64") => ("zen.installer.exe", format!("zen-{version}.exe"), false),
|
||||
("windows", "arm64") => (
|
||||
"zen.installer-arm64.exe",
|
||||
format!("zen-{version}-arm64.exe"),
|
||||
false,
|
||||
),
|
||||
("linux", "x64") => (
|
||||
"zen.linux-x86_64.tar.xz",
|
||||
format!("zen-{version}-x86_64.tar.xz"),
|
||||
true,
|
||||
),
|
||||
("linux", "arm64") => (
|
||||
"zen.linux-aarch64.tar.xz",
|
||||
format!("zen-{version}-aarch64.tar.xz"),
|
||||
true,
|
||||
),
|
||||
("macos", _) => (
|
||||
"zen.macos-universal.dmg",
|
||||
format!("zen-{version}.dmg"),
|
||||
true,
|
||||
),
|
||||
_ => {
|
||||
return Err(format!("Unsupported platform/architecture for Zen: {os}/{arch}").into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/zen-browser/desktop/releases/download/{version}/{asset_name}"
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"brave" => {
|
||||
let (filename, is_archive) = match (&os[..], &arch[..]) {
|
||||
("windows", _) => (format!("brave-{version}.exe"), false),
|
||||
("linux", "x64") => (format!("brave-browser-{version}-linux-amd64.zip"), true),
|
||||
("linux", "arm64") => (format!("brave-browser-{version}-linux-arm64.zip"), true),
|
||||
("macos", _) => ("Brave-Browser-universal.dmg".to_string(), true),
|
||||
_ => {
|
||||
return Err(format!("Unsupported platform/architecture for Brave: {os}/{arch}").into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/brave/brave-browser/releases/download/{version}/{filename}"
|
||||
),
|
||||
filename,
|
||||
is_archive,
|
||||
})
|
||||
}
|
||||
"chromium" => {
|
||||
let platform_str = match (&os[..], &arch[..]) {
|
||||
("windows", "x64") => "Win_x64",
|
||||
("windows", "arm64") => "Win_Arm64",
|
||||
("linux", "x64") => "Linux_x64",
|
||||
("linux", "arm64") => return Err("Chromium doesn't support ARM64 on Linux".into()),
|
||||
("macos", "x64") => "Mac",
|
||||
("macos", "arm64") => "Mac_Arm",
|
||||
_ => {
|
||||
return Err(
|
||||
format!("Unsupported platform/architecture for Chromium: {os}/{arch}").into(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let (archive_name, filename) = match os.as_str() {
|
||||
"windows" => ("chrome-win.zip", format!("chromium-{version}-win.zip")),
|
||||
"linux" => ("chrome-linux.zip", format!("chromium-{version}-linux.zip")),
|
||||
"macos" => ("chrome-mac.zip", format!("chromium-{version}-mac.zip")),
|
||||
_ => return Err(format!("Unsupported platform for Chromium: {os}").into()),
|
||||
};
|
||||
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://commondatastorage.googleapis.com/chromium-browser-snapshots/{platform_str}/{version}/{archive_name}"
|
||||
),
|
||||
filename,
|
||||
is_archive: true,
|
||||
})
|
||||
}
|
||||
"camoufox" => {
|
||||
// Camoufox downloads from GitHub releases with pattern: camoufox-{version}-{release}-{os}.{arch}.zip
|
||||
let (os_name, arch_name) = match (&os[..], &arch[..]) {
|
||||
("windows", "x64") => ("win", "x86_64"),
|
||||
("windows", "arm64") => ("win", "arm64"),
|
||||
("linux", "x64") => ("lin", "x86_64"),
|
||||
("linux", "arm64") => ("lin", "arm64"),
|
||||
("macos", "x64") => ("mac", "x86_64"),
|
||||
("macos", "arm64") => ("mac", "arm64"),
|
||||
_ => {
|
||||
return Err(
|
||||
format!("Unsupported platform/architecture for Camoufox: {os}/{arch}").into(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Note: We provide a placeholder URL here since Camoufox requires dynamic resolution
|
||||
// The actual URL will be resolved in download.rs resolve_download_url
|
||||
Ok(DownloadInfo {
|
||||
url: format!(
|
||||
"https://github.com/daijro/camoufox/releases/download/{version}/camoufox-{{version}}-{{release}}-{os_name}.{arch_name}.zip"
|
||||
),
|
||||
filename: format!("camoufox-{version}-{os_name}.{arch_name}.zip"),
|
||||
is_archive: true,
|
||||
})
|
||||
}
|
||||
"wayfern" => {
|
||||
// Wayfern downloads from https://download.wayfern.com/
|
||||
// File naming: wayfern-{chromium_version}-{platform}-{arch}.{ext}
|
||||
@@ -728,153 +336,6 @@ impl BrowserVersionManager {
|
||||
(os.to_string(), arch.to_string())
|
||||
}
|
||||
|
||||
// Private helper methods for each browser type
|
||||
|
||||
async fn fetch_firefox_versions(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let releases = self.fetch_firefox_releases_detailed(no_caching).await?;
|
||||
Ok(releases.into_iter().map(|r| r.version).collect())
|
||||
}
|
||||
|
||||
async fn fetch_firefox_releases_detailed(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self
|
||||
.api_client
|
||||
.fetch_firefox_releases_with_caching(no_caching)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_firefox_developer_versions(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let releases = self
|
||||
.fetch_firefox_developer_releases_detailed(no_caching)
|
||||
.await?;
|
||||
Ok(releases.into_iter().map(|r| r.version).collect())
|
||||
}
|
||||
|
||||
async fn fetch_firefox_developer_releases_detailed(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self
|
||||
.api_client
|
||||
.fetch_firefox_developer_releases_with_caching(no_caching)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_zen_versions(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let releases = self.fetch_zen_releases_detailed(no_caching).await?;
|
||||
Ok(
|
||||
releases
|
||||
.into_iter()
|
||||
.filter(|r| r.tag_name.to_lowercase() != "twilight")
|
||||
.map(|r| r.tag_name)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn fetch_zen_releases_detailed(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self
|
||||
.api_client
|
||||
.fetch_zen_releases_with_caching(no_caching)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_brave_versions(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let releases = self.fetch_brave_releases_detailed(no_caching).await?;
|
||||
// Persist a lightweight versions cache with accurate prerelease info for Brave
|
||||
let converted: Vec<BrowserRelease> = releases
|
||||
.iter()
|
||||
.map(|r| BrowserRelease {
|
||||
version: r.tag_name.clone(),
|
||||
date: r.published_at.clone(),
|
||||
is_prerelease: r.is_nightly,
|
||||
})
|
||||
.collect();
|
||||
// Always save so that other callers without release_name can classify correctly
|
||||
if let Err(e) = self.api_client.save_cached_versions("brave", &converted) {
|
||||
log::error!("Failed to persist Brave versions cache: {e}");
|
||||
}
|
||||
|
||||
Ok(releases.into_iter().map(|r| r.tag_name).collect())
|
||||
}
|
||||
|
||||
async fn fetch_brave_releases_detailed(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let releases = self
|
||||
.api_client
|
||||
.fetch_brave_releases_with_caching(no_caching)
|
||||
.await?;
|
||||
|
||||
// Save a parallel versions cache for Brave with accurate prerelease flags
|
||||
let converted: Vec<BrowserRelease> = releases
|
||||
.iter()
|
||||
.map(|r| BrowserRelease {
|
||||
version: r.tag_name.clone(),
|
||||
date: r.published_at.clone(),
|
||||
is_prerelease: r.is_nightly,
|
||||
})
|
||||
.collect();
|
||||
if let Err(e) = self.api_client.save_cached_versions("brave", &converted) {
|
||||
log::error!("Failed to persist Brave versions cache: {e}");
|
||||
}
|
||||
|
||||
Ok(releases)
|
||||
}
|
||||
|
||||
async fn fetch_chromium_versions(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let releases = self.fetch_chromium_releases_detailed(no_caching).await?;
|
||||
Ok(releases.into_iter().map(|r| r.version).collect())
|
||||
}
|
||||
|
||||
async fn fetch_chromium_releases_detailed(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self
|
||||
.api_client
|
||||
.fetch_chromium_releases_with_caching(no_caching)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_camoufox_versions(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let releases = self.fetch_camoufox_releases_detailed(no_caching).await?;
|
||||
Ok(releases.into_iter().map(|r| r.tag_name).collect())
|
||||
}
|
||||
|
||||
async fn fetch_camoufox_releases_detailed(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
self
|
||||
.api_client
|
||||
.fetch_camoufox_releases_with_caching(no_caching)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_wayfern_versions(
|
||||
&self,
|
||||
no_caching: bool,
|
||||
@@ -912,37 +373,14 @@ pub async fn get_browser_release_types(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use wiremock::MockServer;
|
||||
|
||||
async fn setup_mock_server() -> MockServer {
|
||||
MockServer::start().await
|
||||
}
|
||||
|
||||
fn create_test_api_client(server: &MockServer) -> ApiClient {
|
||||
let base_url = server.uri();
|
||||
ApiClient::new_with_base_urls(
|
||||
base_url.clone(), // firefox_api_base
|
||||
base_url.clone(), // firefox_dev_api_base
|
||||
base_url.clone(), // github_api_base
|
||||
base_url.clone(), // chromium_api_base
|
||||
)
|
||||
}
|
||||
|
||||
fn create_test_service(_api_client: ApiClient) -> &'static BrowserVersionManager {
|
||||
BrowserVersionManager::instance()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_browser_version_manager_creation() {
|
||||
let _ = BrowserVersionManager::instance();
|
||||
// Test passes if we can create the service without panicking
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unsupported_browser() {
|
||||
let server = setup_mock_server().await;
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
let service = BrowserVersionManager::instance();
|
||||
|
||||
let result = service.fetch_browser_versions("unsupported", false).await;
|
||||
assert!(
|
||||
@@ -962,141 +400,48 @@ mod tests {
|
||||
fn test_get_download_info() {
|
||||
let service = BrowserVersionManager::instance();
|
||||
|
||||
// Test Firefox - platform-specific expectations
|
||||
let firefox_info = service.get_download_info("firefox", "139.0").unwrap();
|
||||
let wayfern_info = service.get_download_info("wayfern", "1.0.0").unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
{
|
||||
assert_eq!(firefox_info.filename, "Firefox 139.0.dmg");
|
||||
assert!(firefox_info.is_archive);
|
||||
assert_eq!(wayfern_info.filename, "wayfern-1.0.0-macos-arm64.dmg");
|
||||
assert!(wayfern_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
||||
{
|
||||
assert_eq!(firefox_info.filename, "firefox-139.0.tar.xz");
|
||||
assert!(firefox_info.is_archive);
|
||||
assert_eq!(wayfern_info.filename, "wayfern-1.0.0-macos-x64.dmg");
|
||||
assert!(wayfern_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
{
|
||||
assert_eq!(firefox_info.filename, "Firefox Setup 139.0.exe");
|
||||
assert!(!firefox_info.is_archive);
|
||||
assert_eq!(wayfern_info.filename, "wayfern-1.0.0-linux-x64.tar.xz");
|
||||
assert!(wayfern_info.is_archive);
|
||||
}
|
||||
|
||||
assert!(firefox_info
|
||||
.url
|
||||
.contains("download-installer.cdn.mozilla.net"));
|
||||
assert!(firefox_info.url.contains("/pub/firefox/releases/139.0/"));
|
||||
|
||||
// Test Firefox Developer
|
||||
let firefox_dev_info = service
|
||||
.get_download_info("firefox-developer", "139.0b1")
|
||||
.unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
|
||||
{
|
||||
assert_eq!(firefox_dev_info.filename, "Firefox 139.0b1.dmg");
|
||||
assert!(firefox_dev_info.is_archive);
|
||||
assert_eq!(wayfern_info.filename, "wayfern-1.0.0-linux-arm64.tar.xz");
|
||||
assert!(wayfern_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
||||
{
|
||||
assert_eq!(firefox_dev_info.filename, "firefox-139.0b1.tar.xz");
|
||||
assert!(firefox_dev_info.is_archive);
|
||||
assert_eq!(wayfern_info.filename, "wayfern-1.0.0-windows-x64.zip");
|
||||
assert!(wayfern_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
|
||||
{
|
||||
assert_eq!(firefox_dev_info.filename, "Firefox Setup 139.0b1.exe");
|
||||
assert!(!firefox_dev_info.is_archive);
|
||||
assert_eq!(wayfern_info.filename, "wayfern-1.0.0-windows-arm64.zip");
|
||||
assert!(wayfern_info.is_archive);
|
||||
}
|
||||
|
||||
assert!(firefox_dev_info
|
||||
.url
|
||||
.contains("download-installer.cdn.mozilla.net"));
|
||||
assert!(firefox_dev_info
|
||||
.url
|
||||
.contains("/pub/devedition/releases/139.0b1/"));
|
||||
assert!(wayfern_info.url.contains("download.wayfern.com"));
|
||||
|
||||
// Test Zen Browser
|
||||
let zen_info = service.get_download_info("zen", "1.11b").unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(zen_info.filename, "zen-1.11b.dmg");
|
||||
assert!(zen_info.url.contains("zen.macos-universal.dmg"));
|
||||
assert!(zen_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(zen_info.filename, "zen-1.11b-x86_64.tar.xz");
|
||||
assert!(zen_info.url.contains("zen.linux-x86_64.tar.xz"));
|
||||
assert!(zen_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(zen_info.filename, "zen-1.11b.exe");
|
||||
assert!(zen_info.url.contains("zen.installer.exe"));
|
||||
assert!(!zen_info.is_archive);
|
||||
}
|
||||
|
||||
// Test Chromium
|
||||
let chromium_info = service.get_download_info("chromium", "1465660").unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660-mac.zip");
|
||||
assert!(chromium_info.url.contains("chrome-mac.zip"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660-linux.zip");
|
||||
assert!(chromium_info.url.contains("chrome-linux.zip"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(chromium_info.filename, "chromium-1465660-win.zip");
|
||||
assert!(chromium_info.url.contains("chrome-win.zip"));
|
||||
}
|
||||
|
||||
assert!(chromium_info.is_archive);
|
||||
|
||||
// Test Brave - Note: Brave uses dynamic URL resolution, so get_download_info provides a template URL
|
||||
let brave_info = service.get_download_info("brave", "v1.81.9").unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg");
|
||||
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg");
|
||||
assert!(brave_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert_eq!(brave_info.filename, "brave-browser-v1.81.9-linux-amd64.zip");
|
||||
assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/brave-browser-v1.81.9-linux-amd64.zip");
|
||||
assert!(brave_info.is_archive);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
assert_eq!(brave_info.filename, "brave-v1.81.9.exe");
|
||||
assert_eq!(
|
||||
brave_info.url,
|
||||
"https://github.com/brave/brave-browser/releases/download/v1.81.9/brave-v1.81.9.exe"
|
||||
);
|
||||
assert!(!brave_info.is_archive);
|
||||
}
|
||||
|
||||
// Test unsupported browser
|
||||
let unsupported_result = service.get_download_info("unsupported", "1.0.0");
|
||||
let unsupported_result = service.get_download_info("firefox", "1.0.0");
|
||||
assert!(unsupported_result.is_err());
|
||||
|
||||
log::info!("Download info test passed for all browsers");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,677 +0,0 @@
|
||||
//! Camoufox configuration builder.
|
||||
//!
|
||||
//! Converts fingerprints to Camoufox configuration format and builds launch options.
|
||||
|
||||
use rand::RngExt;
|
||||
use serde_yaml;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::camoufox::data;
|
||||
use crate::camoufox::env_vars;
|
||||
use crate::camoufox::fingerprint::types::*;
|
||||
use crate::camoufox::fonts;
|
||||
use crate::camoufox::geolocation;
|
||||
use crate::camoufox::presets;
|
||||
use crate::camoufox::webgl;
|
||||
|
||||
/// Browserforge mapping from YAML.
|
||||
type BrowserforgeMapping = HashMap<String, serde_yaml::Value>;
|
||||
|
||||
/// Load the browserforge mapping from embedded YAML.
|
||||
fn load_browserforge_mapping() -> BrowserforgeMapping {
|
||||
serde_yaml::from_str(data::BROWSERFORGE_YML).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Convert a fingerprint to Camoufox configuration.
|
||||
pub fn from_browserforge(
|
||||
fingerprint: &Fingerprint,
|
||||
ff_version: Option<u32>,
|
||||
) -> HashMap<String, serde_json::Value> {
|
||||
let mapping = load_browserforge_mapping();
|
||||
let mut config = HashMap::new();
|
||||
|
||||
// Convert fingerprint to a JSON value for easier traversal
|
||||
let fp_json = serde_json::to_value(fingerprint).unwrap_or_default();
|
||||
|
||||
// Apply mappings recursively
|
||||
cast_to_properties(&mut config, &mapping, &fp_json, ff_version);
|
||||
|
||||
// Handle window.screenX and window.screenY
|
||||
handle_screen_xy(&mut config, &fingerprint.screen);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Recursively cast fingerprint properties to Camoufox config format.
|
||||
fn cast_to_properties(
|
||||
config: &mut HashMap<String, serde_json::Value>,
|
||||
mapping: &BrowserforgeMapping,
|
||||
fingerprint: &serde_json::Value,
|
||||
ff_version: Option<u32>,
|
||||
) {
|
||||
if let serde_json::Value::Object(fp_obj) = fingerprint {
|
||||
for (key, mapping_value) in mapping {
|
||||
let fp_value = fp_obj.get(key);
|
||||
|
||||
match mapping_value {
|
||||
serde_yaml::Value::String(target_key) => {
|
||||
if let Some(value) = fp_value {
|
||||
let mut final_value = value.clone();
|
||||
|
||||
// Handle negative screen values
|
||||
if target_key.starts_with("screen.") {
|
||||
if let Some(num) = final_value.as_i64() {
|
||||
if num < 0 {
|
||||
final_value = serde_json::json!(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace Firefox version in user agent strings
|
||||
if let (Some(version), Some(s)) = (ff_version, final_value.as_str()) {
|
||||
let replaced = replace_ff_version(s, version);
|
||||
final_value = serde_json::json!(replaced);
|
||||
}
|
||||
|
||||
config.insert(target_key.clone(), final_value);
|
||||
}
|
||||
}
|
||||
serde_yaml::Value::Mapping(nested_mapping) => {
|
||||
if let Some(nested_fp) = fp_value {
|
||||
let nested: BrowserforgeMapping = nested_mapping
|
||||
.iter()
|
||||
.filter_map(|(k, v)| k.as_str().map(|ks| (ks.to_string(), v.clone())))
|
||||
.collect();
|
||||
cast_to_properties(config, &nested, nested_fp, ff_version);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace Firefox version in user agent and related strings.
|
||||
fn replace_ff_version(s: &str, version: u32) -> String {
|
||||
// Match patterns like "135.0" (Firefox version) and replace with new version
|
||||
let re = regex_lite::Regex::new(r"(?<!\d)(1[0-9]{2})(\.0)(?!\d)").unwrap_or_else(|_| {
|
||||
// Fallback - just do simple replacement
|
||||
regex_lite::Regex::new(r"Firefox/\d+").unwrap()
|
||||
});
|
||||
|
||||
re.replace_all(s, format!("{}.0", version).as_str())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Handle window.screenX and window.screenY generation.
|
||||
fn handle_screen_xy(config: &mut HashMap<String, serde_json::Value>, screen: &ScreenFingerprint) {
|
||||
if config.contains_key("window.screenY") {
|
||||
return;
|
||||
}
|
||||
|
||||
let screen_x = screen.screen_x;
|
||||
if screen_x == 0 {
|
||||
config.insert("window.screenX".to_string(), serde_json::json!(0));
|
||||
config.insert("window.screenY".to_string(), serde_json::json!(0));
|
||||
return;
|
||||
}
|
||||
|
||||
if (-50..=50).contains(&screen_x) {
|
||||
config.insert("window.screenY".to_string(), serde_json::json!(screen_x));
|
||||
return;
|
||||
}
|
||||
|
||||
let screen_y = screen.avail_height as i32 - screen.outer_height as i32;
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let y = if screen_y == 0 {
|
||||
0
|
||||
} else if screen_y > 0 {
|
||||
rng.random_range(0..=screen_y)
|
||||
} else {
|
||||
rng.random_range(screen_y..=0)
|
||||
};
|
||||
|
||||
config.insert("window.screenY".to_string(), serde_json::json!(y));
|
||||
}
|
||||
|
||||
/// GeoIP option - can be an IP address string or auto-detect.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GeoIPOption {
|
||||
/// Auto-detect IP (fetch public IP, optionally through proxy)
|
||||
Auto,
|
||||
/// Use a specific IP address
|
||||
IP(String),
|
||||
}
|
||||
|
||||
/// Configuration builder for Camoufox launch.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CamoufoxConfigBuilder {
|
||||
fingerprint: Option<Fingerprint>,
|
||||
operating_system: Option<String>,
|
||||
screen_constraints: Option<ScreenConstraints>,
|
||||
block_images: bool,
|
||||
block_webrtc: bool,
|
||||
block_webgl: bool,
|
||||
custom_fonts: Option<Vec<String>>,
|
||||
custom_fonts_only: bool,
|
||||
firefox_prefs: HashMap<String, serde_json::Value>,
|
||||
proxy: Option<ProxyConfig>,
|
||||
headless: bool,
|
||||
ff_version: Option<u32>,
|
||||
extra_config: HashMap<String, serde_json::Value>,
|
||||
geoip: Option<GeoIPOption>,
|
||||
}
|
||||
|
||||
/// Proxy configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyConfig {
|
||||
pub server: String,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub bypass: Option<String>,
|
||||
}
|
||||
|
||||
impl ProxyConfig {
|
||||
/// Parse a proxy URL string into ProxyConfig.
|
||||
/// Supports formats like:
|
||||
/// - "http://host:port"
|
||||
/// - "http://user:pass@host:port"
|
||||
/// - "socks5://user:pass@host:port"
|
||||
pub fn from_url(url: &str) -> Result<Self, ConfigError> {
|
||||
let parsed = url::Url::parse(url).map_err(|e| ConfigError::InvalidProxy(e.to_string()))?;
|
||||
|
||||
let host = parsed
|
||||
.host_str()
|
||||
.ok_or_else(|| ConfigError::InvalidProxy("Missing host".to_string()))?;
|
||||
|
||||
let port = parsed.port().unwrap_or(8080);
|
||||
let scheme = parsed.scheme();
|
||||
|
||||
let server = format!("{scheme}://{host}:{port}");
|
||||
|
||||
let username = if !parsed.username().is_empty() {
|
||||
Some(parsed.username().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let password = parsed.password().map(String::from);
|
||||
|
||||
Ok(Self {
|
||||
server,
|
||||
username,
|
||||
password,
|
||||
bypass: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CamoufoxConfigBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CamoufoxConfigBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
fingerprint: None,
|
||||
operating_system: None,
|
||||
screen_constraints: None,
|
||||
block_images: false,
|
||||
block_webrtc: false,
|
||||
block_webgl: false,
|
||||
custom_fonts: None,
|
||||
custom_fonts_only: false,
|
||||
firefox_prefs: HashMap::new(),
|
||||
proxy: None,
|
||||
headless: false,
|
||||
ff_version: None,
|
||||
extra_config: HashMap::new(),
|
||||
geoip: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fingerprint(mut self, fp: Fingerprint) -> Self {
|
||||
self.fingerprint = Some(fp);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn operating_system(mut self, os: &str) -> Self {
|
||||
self.operating_system = Some(os.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn screen_constraints(mut self, constraints: ScreenConstraints) -> Self {
|
||||
self.screen_constraints = Some(constraints);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block_images(mut self, block: bool) -> Self {
|
||||
self.block_images = block;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block_webrtc(mut self, block: bool) -> Self {
|
||||
self.block_webrtc = block;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block_webgl(mut self, block: bool) -> Self {
|
||||
self.block_webgl = block;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn custom_fonts(mut self, fonts: Vec<String>) -> Self {
|
||||
self.custom_fonts = Some(fonts);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn custom_fonts_only(mut self, only: bool) -> Self {
|
||||
self.custom_fonts_only = only;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn firefox_pref<V: Into<serde_json::Value>>(mut self, key: &str, value: V) -> Self {
|
||||
self.firefox_prefs.insert(key.to_string(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
|
||||
self.proxy = Some(proxy);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn headless(mut self, headless: bool) -> Self {
|
||||
self.headless = headless;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ff_version(mut self, version: u32) -> Self {
|
||||
self.ff_version = Some(version);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn extra_config<V: Into<serde_json::Value>>(mut self, key: &str, value: V) -> Self {
|
||||
self.extra_config.insert(key.to_string(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set GeoIP option for geolocation-based fingerprinting.
|
||||
/// Use `GeoIPOption::Auto` to auto-detect public IP (optionally through proxy).
|
||||
/// Use `GeoIPOption::IP(ip_string)` to use a specific IP address.
|
||||
pub fn geoip(mut self, option: GeoIPOption) -> Self {
|
||||
self.geoip = Some(option);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the complete Camoufox launch configuration.
|
||||
///
|
||||
/// Prefers a real-fingerprint preset (matched against the Camoufox build's
|
||||
/// Firefox version via `presets::preset_line_for`) when no explicit
|
||||
/// fingerprint was passed. Falls back to the Bayesian network-based
|
||||
/// synthesizer when presets are unavailable, so callers without a known
|
||||
/// Firefox version (or with no preset for the requested OS) still get a
|
||||
/// valid config — matching pre-v150 behaviour byte-for-byte.
|
||||
pub fn build(self) -> Result<CamoufoxLaunchConfig, ConfigError> {
|
||||
let mut rng = rand::rng();
|
||||
let ff_version = self.ff_version;
|
||||
|
||||
// 1) The caller supplied a fingerprint outright — honour it and skip
|
||||
// presets entirely. This is the path tests and advanced consumers
|
||||
// use to inject deterministic fixtures.
|
||||
// 2) Otherwise, try a bundled preset for the requested OS / FF line.
|
||||
// 3) Fall back to the Bayesian generator. This is also the path that
|
||||
// runs for users whose Camoufox binary has no readable `version.json`
|
||||
// (`ff_version == None`), or whose OS has no presets bundled.
|
||||
let (mut config, target_os) = if let Some(fp) = self.fingerprint {
|
||||
let target_os = env_vars::determine_ua_os(&fp.navigator.user_agent);
|
||||
// `from_browserforge` already runs `handle_screen_xy` internally.
|
||||
let config = from_browserforge(&fp, ff_version);
|
||||
(config, target_os)
|
||||
} else if let Some(preset) =
|
||||
presets::get_random_preset(self.operating_system.as_deref(), ff_version)
|
||||
{
|
||||
let mut config = presets::from_preset(&preset, ff_version);
|
||||
let target_os = config
|
||||
.get("navigator.userAgent")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(env_vars::determine_ua_os)
|
||||
.or_else(|| {
|
||||
// Last-resort heuristic from the platform string — keeps target_os
|
||||
// sensible even if a preset somehow omits the user agent.
|
||||
config
|
||||
.get("navigator.platform")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|p| match p {
|
||||
"Win32" => "windows",
|
||||
"MacIntel" => "macos",
|
||||
_ => "linux",
|
||||
})
|
||||
})
|
||||
.unwrap_or("macos");
|
||||
// Presets don't carry multi-monitor offsets, so default screenX/Y to
|
||||
// (0, 0) — matches what real single-display users send.
|
||||
config
|
||||
.entry("window.screenX".to_string())
|
||||
.or_insert(serde_json::json!(0));
|
||||
config
|
||||
.entry("window.screenY".to_string())
|
||||
.or_insert(serde_json::json!(0));
|
||||
(config, target_os)
|
||||
} else {
|
||||
let generator = crate::camoufox::fingerprint::FingerprintGenerator::new()?;
|
||||
let options = FingerprintOptions {
|
||||
operating_system: self.operating_system.clone(),
|
||||
browsers: Some(vec!["firefox".to_string()]),
|
||||
devices: Some(vec!["desktop".to_string()]),
|
||||
screen: self.screen_constraints,
|
||||
..Default::default()
|
||||
};
|
||||
let fingerprint = generator.get_fingerprint(&options)?.fingerprint;
|
||||
let target_os = env_vars::determine_ua_os(&fingerprint.navigator.user_agent);
|
||||
let config = from_browserforge(&fingerprint, ff_version);
|
||||
(config, target_os)
|
||||
};
|
||||
|
||||
// Note: we used to spoof `window.history.length` to a random value in
|
||||
// [1, 5] here. Newer Camoufox builds clamp the docShell session history
|
||||
// to this value, which disables the toolbar back/forward buttons when
|
||||
// the spoof rolls a small number. The fingerprint value drifts on every
|
||||
// user navigation anyway, so a constant spoof is detectable and not
|
||||
// worth the broken navigation UX.
|
||||
|
||||
// Add fonts
|
||||
if !self.custom_fonts_only {
|
||||
let system_fonts = fonts::get_fonts_for_os(target_os);
|
||||
let fonts = if let Some(custom) = &self.custom_fonts {
|
||||
let mut all_fonts = system_fonts;
|
||||
for font in custom {
|
||||
if !all_fonts.contains(font) {
|
||||
all_fonts.push(font.clone());
|
||||
}
|
||||
}
|
||||
all_fonts
|
||||
} else {
|
||||
system_fonts
|
||||
};
|
||||
config.insert("fonts".to_string(), serde_json::json!(fonts));
|
||||
} else if let Some(custom) = &self.custom_fonts {
|
||||
config.insert("fonts".to_string(), serde_json::json!(custom));
|
||||
}
|
||||
|
||||
// Add font spacing seed
|
||||
config.insert(
|
||||
"fonts:spacing_seed".to_string(),
|
||||
serde_json::json!(rng.random_range(0..1_073_741_824u32)),
|
||||
);
|
||||
|
||||
// Build Firefox preferences
|
||||
let mut firefox_prefs = self.firefox_prefs;
|
||||
|
||||
if self.block_images {
|
||||
firefox_prefs.insert(
|
||||
"permissions.default.image".to_string(),
|
||||
serde_json::json!(2),
|
||||
);
|
||||
}
|
||||
|
||||
if self.block_webrtc {
|
||||
firefox_prefs.insert(
|
||||
"media.peerconnection.enabled".to_string(),
|
||||
serde_json::json!(false),
|
||||
);
|
||||
}
|
||||
|
||||
if self.block_webgl {
|
||||
firefox_prefs.insert("webgl.disabled".to_string(), serde_json::json!(true));
|
||||
} else {
|
||||
// Sample and add WebGL configuration
|
||||
match webgl::sample_webgl(target_os, None, None) {
|
||||
Ok(webgl_data) => {
|
||||
for (key, value) in webgl_data.config {
|
||||
config.insert(key, value);
|
||||
}
|
||||
firefox_prefs.insert("webgl.force-enabled".to_string(), serde_json::json!(true));
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to sample WebGL config: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Canvas anti-fingerprinting
|
||||
config.insert(
|
||||
"canvas:aaOffset".to_string(),
|
||||
serde_json::json!(rng.random_range(-50..=50)),
|
||||
);
|
||||
config.insert("canvas:aaCapOffset".to_string(), serde_json::json!(true));
|
||||
|
||||
// Add extra config (user-provided)
|
||||
for (key, value) in self.extra_config {
|
||||
config.insert(key, value);
|
||||
}
|
||||
|
||||
// Hardcoded Camoufox settings (cannot be overridden)
|
||||
// Disable theming to prevent fingerprinting via browser theme
|
||||
config.insert("disableTheming".to_string(), serde_json::json!(true));
|
||||
// Hide cursor in headless mode
|
||||
config.insert("showcursor".to_string(), serde_json::json!(false));
|
||||
|
||||
Ok(CamoufoxLaunchConfig {
|
||||
fingerprint_config: config,
|
||||
firefox_prefs,
|
||||
proxy: self.proxy,
|
||||
headless: self.headless,
|
||||
target_os: target_os.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the complete Camoufox launch configuration with async geolocation support.
|
||||
/// This method should be used when geoip option is set to Auto.
|
||||
pub async fn build_async(self) -> Result<CamoufoxLaunchConfig, ConfigError> {
|
||||
// Get full proxy URL (with credentials) for IP detection
|
||||
let proxy_url = self.proxy.as_ref().map(|p| {
|
||||
if let (Some(user), Some(pass)) = (&p.username, &p.password) {
|
||||
// Reconstruct URL with credentials: scheme://user:pass@host:port
|
||||
if let Ok(mut parsed) = url::Url::parse(&p.server) {
|
||||
let _ = parsed.set_username(user);
|
||||
let _ = parsed.set_password(Some(pass));
|
||||
parsed.to_string()
|
||||
} else {
|
||||
p.server.clone()
|
||||
}
|
||||
} else if let Some(user) = &p.username {
|
||||
if let Ok(mut parsed) = url::Url::parse(&p.server) {
|
||||
let _ = parsed.set_username(user);
|
||||
parsed.to_string()
|
||||
} else {
|
||||
p.server.clone()
|
||||
}
|
||||
} else {
|
||||
p.server.clone()
|
||||
}
|
||||
});
|
||||
let geoip_option = self.geoip.clone();
|
||||
let block_webrtc = self.block_webrtc;
|
||||
|
||||
// Build base config first
|
||||
let mut launch_config = self.build()?;
|
||||
|
||||
// Handle geolocation if geoip option is set
|
||||
if let Some(geoip) = geoip_option {
|
||||
let ip = match geoip {
|
||||
GeoIPOption::Auto => {
|
||||
// Fetch public IP, optionally through proxy
|
||||
geolocation::fetch_public_ip(proxy_url.as_deref())
|
||||
.await
|
||||
.map_err(geolocation::GeolocationError::from)?
|
||||
}
|
||||
GeoIPOption::IP(ip_str) => {
|
||||
if !geolocation::validate_ip(&ip_str) {
|
||||
return Err(ConfigError::Geolocation(
|
||||
geolocation::GeolocationError::InvalidIP(ip_str),
|
||||
));
|
||||
}
|
||||
ip_str
|
||||
}
|
||||
};
|
||||
|
||||
// Get geolocation from IP
|
||||
match geolocation::get_geolocation(&ip) {
|
||||
Ok(geo) => {
|
||||
// Add geolocation config
|
||||
for (key, value) in geo.as_config() {
|
||||
launch_config.fingerprint_config.insert(key, value);
|
||||
}
|
||||
|
||||
// Add WebRTC IP spoofing if not blocked
|
||||
if !block_webrtc {
|
||||
if geolocation::is_ipv4(&ip) {
|
||||
launch_config
|
||||
.fingerprint_config
|
||||
.insert("webrtc:ipv4".to_string(), serde_json::json!(ip));
|
||||
} else if geolocation::is_ipv6(&ip) {
|
||||
launch_config
|
||||
.fingerprint_config
|
||||
.insert("webrtc:ipv6".to_string(), serde_json::json!(ip));
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Applied geolocation from IP {}: {} ({})",
|
||||
ip,
|
||||
geo.locale.as_string(),
|
||||
geo.timezone
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to get geolocation for IP {}: {}", ip, e);
|
||||
// Continue without geolocation rather than failing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(launch_config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete Camoufox launch configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CamoufoxLaunchConfig {
|
||||
pub fingerprint_config: HashMap<String, serde_json::Value>,
|
||||
pub firefox_prefs: HashMap<String, serde_json::Value>,
|
||||
pub proxy: Option<ProxyConfig>,
|
||||
pub headless: bool,
|
||||
pub target_os: String,
|
||||
}
|
||||
|
||||
impl CamoufoxLaunchConfig {
|
||||
/// Get environment variables for launching Camoufox.
|
||||
pub fn get_env_vars(&self) -> Result<HashMap<String, String>, serde_json::Error> {
|
||||
env_vars::config_to_env_vars(&self.fingerprint_config)
|
||||
}
|
||||
|
||||
/// Get the config as JSON string.
|
||||
pub fn config_json(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(&self.fingerprint_config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for configuration operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("Fingerprint generation error: {0}")]
|
||||
Fingerprint(#[from] crate::camoufox::fingerprint::FingerprintError),
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("WebGL error: {0}")]
|
||||
WebGL(#[from] webgl::WebGLError),
|
||||
|
||||
#[error("Invalid proxy configuration: {0}")]
|
||||
InvalidProxy(String),
|
||||
|
||||
#[error("Geolocation error: {0}")]
|
||||
Geolocation(#[from] crate::camoufox::geolocation::GeolocationError),
|
||||
}
|
||||
|
||||
/// Get Firefox version from executable path.
|
||||
pub fn get_firefox_version(executable_path: &Path) -> Option<u32> {
|
||||
// Try to read version.json from the same directory
|
||||
let version_path = executable_path.parent()?.join("version.json");
|
||||
|
||||
if let Ok(content) = std::fs::read_to_string(&version_path) {
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||
if let Some(version_str) = json.get("version").and_then(|v| v.as_str()) {
|
||||
// Parse major version from "135.0" or similar
|
||||
let major: u32 = version_str.split('.').next()?.parse().ok()?;
|
||||
return Some(major);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_builder() {
|
||||
let config = CamoufoxConfigBuilder::new()
|
||||
.operating_system("windows")
|
||||
.block_images(true)
|
||||
.build();
|
||||
|
||||
assert!(config.is_ok());
|
||||
let config = config.unwrap();
|
||||
assert!(config
|
||||
.firefox_prefs
|
||||
.contains_key("permissions.default.image"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_ff_version() {
|
||||
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0";
|
||||
let replaced = replace_ff_version(ua, 140);
|
||||
assert!(replaced.contains("140.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_browserforge() {
|
||||
let fingerprint = Fingerprint {
|
||||
screen: ScreenFingerprint {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
avail_width: 1920,
|
||||
avail_height: 1040,
|
||||
color_depth: 24,
|
||||
pixel_depth: 24,
|
||||
inner_width: 1903,
|
||||
inner_height: 969,
|
||||
outer_width: 1920,
|
||||
outer_height: 1040,
|
||||
..Default::default()
|
||||
},
|
||||
navigator: NavigatorFingerprint {
|
||||
user_agent: "Mozilla/5.0 Firefox/135.0".to_string(),
|
||||
platform: "Win32".to_string(),
|
||||
language: "en-US".to_string(),
|
||||
languages: vec!["en-US".to_string()],
|
||||
hardware_concurrency: 8,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = from_browserforge(&fingerprint, Some(140));
|
||||
|
||||
assert!(config.contains_key("navigator.userAgent"));
|
||||
assert!(config.contains_key("screen.width"));
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
[
|
||||
"chrome/143.0.0.0|2",
|
||||
"safari/18.3.1|2",
|
||||
"chrome/101.0.4951.54|2",
|
||||
"chrome/139.0.0.0|2",
|
||||
"safari/16.6|2",
|
||||
"safari/26.2|2",
|
||||
"safari/18.6|2",
|
||||
"safari/26.1|2",
|
||||
"chrome/142.0.0.0|2",
|
||||
"chrome/141.0.0.0|2",
|
||||
"safari/18.7.3|2",
|
||||
"edge/143.0.0.0|2",
|
||||
"safari/18.4|2",
|
||||
"safari/17.3.1|2",
|
||||
"chrome/135.0.0.0|2",
|
||||
"safari/18.5|2",
|
||||
"safari/18.7.2|2",
|
||||
"chrome/143.0.0.0|1",
|
||||
"chrome/128.0.0.0|2",
|
||||
"chrome/131.0.0.0|2",
|
||||
"safari/26.3|2",
|
||||
"safari/26.0.1|2",
|
||||
"chrome/114.0.0.0|2",
|
||||
"safari/18.1|2",
|
||||
"firefox/147.0|2",
|
||||
"safari/17.5|2",
|
||||
"chrome/140.0.0.0|2",
|
||||
"safari/16.6.1|2",
|
||||
"firefox/146.0|2",
|
||||
"chrome/124.0.0.0|1",
|
||||
"chrome/34.0.1847.114|2",
|
||||
"chrome/130.0.0.0|2",
|
||||
"safari/15.6.7|2",
|
||||
"chrome/144.0.0.0|2",
|
||||
"safari/18.3|2",
|
||||
"safari/16.4|2",
|
||||
"chrome/141.0.7390.122|1",
|
||||
"firefox/140.0|2",
|
||||
"chrome/138.0.0.0|2",
|
||||
"firefox/135.0|2",
|
||||
"safari/17.6|2",
|
||||
"chrome/132.0.0.0|2",
|
||||
"chrome/109.0.0.0|2",
|
||||
"chrome/92.0.4515.131|2",
|
||||
"chrome/136.0.0.0|2",
|
||||
"edge/142.0.0.0|2",
|
||||
"chrome/125.0.0.0|2",
|
||||
"safari/17.8|2",
|
||||
"edge/143.0.0.0|1",
|
||||
"chrome/123.0.0.0|2",
|
||||
"chrome/137.0.0.0|2",
|
||||
"chrome/129.0.0.0|2",
|
||||
"chrome/126.0.0.0|2",
|
||||
"safari/26.0|2",
|
||||
"chrome/133.0.0.0|2",
|
||||
"chrome/119.0.0.0|2",
|
||||
"chrome/145.0.0.0|2",
|
||||
"firefox/145.0|2",
|
||||
"safari/17.3|2",
|
||||
"safari/18.2|2",
|
||||
"safari/16.5.2|2",
|
||||
"safari/17.4|2",
|
||||
"chrome/120.0.0.0|2",
|
||||
"chrome/116.0.0.0|2",
|
||||
"firefox/141.0|2",
|
||||
"safari/17.4.1|2",
|
||||
"chrome/134.0.0.0|2",
|
||||
"safari/15.4|2",
|
||||
"safari/18.1.1|2",
|
||||
"edge/144.0.0.0|2",
|
||||
"firefox/144.0|2",
|
||||
"safari/16.3|2",
|
||||
"safari/13.0.3|2",
|
||||
"chrome/131.0.6778.33|2",
|
||||
"edge/145.0.0.0|2",
|
||||
"edge/139.0.0.0|2",
|
||||
"safari/17.1|2",
|
||||
"chrome/133.0.0.0|1",
|
||||
"chrome/121.0.0.0|2",
|
||||
"chrome/124.0.0.0|2",
|
||||
"chrome/127.0.0.0|2",
|
||||
"chrome/122.0.6261.95|2",
|
||||
"chrome/91.0.4450.0|2",
|
||||
"edge/134.0.0.0|2",
|
||||
"chrome/134.0.6998.179|2",
|
||||
"chrome/122.0.0.0|2",
|
||||
"firefox/128.0|2",
|
||||
"chrome/142.0.0.0|1",
|
||||
"safari/18.7|2",
|
||||
"safari/17.8.1|2",
|
||||
"firefox/115.0|2",
|
||||
"safari/17.2|2",
|
||||
"chrome/117.0.0.0|2",
|
||||
"safari/18.0.1|2",
|
||||
"chrome/139.0.7258.5|2",
|
||||
"edge/140.0.0.0|2",
|
||||
"safari/16.5|2",
|
||||
"safari/18.6.2|2",
|
||||
"firefox/136.0|2",
|
||||
"safari/17.2.1|2",
|
||||
"safari/18.0|2",
|
||||
"safari/15.6.1|2",
|
||||
"safari/26.2|1",
|
||||
"safari/17.1.2|2",
|
||||
"safari/17.7|2",
|
||||
"safari/16.2|2",
|
||||
"edge/122.0.0.0|2",
|
||||
"chrome/139.0.0.0|1",
|
||||
"safari/17.0|2",
|
||||
"firefox/139.0|2",
|
||||
"chrome/101.0.9316.173|2",
|
||||
"chrome/101.0.4951.64|2",
|
||||
"chrome/141.0.0.0|1",
|
||||
"safari/15.5|2",
|
||||
"safari/18.6|1",
|
||||
"chrome/112.0.0.0|2",
|
||||
"edge/135.0.0.0|2",
|
||||
"chrome/140.0.0.0|1"
|
||||
]
|
||||
@@ -1,68 +0,0 @@
|
||||
# Mappings of Browserforge fingerprints to Camoufox config properties.
|
||||
|
||||
navigator:
|
||||
# Note: Browserforge tends to have outdated UAs.
|
||||
# The version will be replaced in Camoufox.
|
||||
userAgent: navigator.userAgent
|
||||
# userAgentData not in Firefox
|
||||
doNotTrack: navigator.doNotTrack
|
||||
appCodeName: navigator.appCodeName
|
||||
appName: navigator.appName
|
||||
appVersion: navigator.appVersion
|
||||
oscpu: navigator.oscpu
|
||||
# webdriver is always True
|
||||
# Locale is now implemented separately:
|
||||
# language: navigator.language
|
||||
# languages: navigator.languages
|
||||
platform: navigator.platform
|
||||
# deviceMemory not in Firefox
|
||||
hardwareConcurrency: navigator.hardwareConcurrency
|
||||
product: navigator.product
|
||||
# Never override productSub #105
|
||||
# productSub: navigator.productSub
|
||||
# vendor is not necessary
|
||||
# vendorSub is not necessary
|
||||
maxTouchPoints: navigator.maxTouchPoints
|
||||
extraProperties:
|
||||
# Note: Changing pdfViewerEnabled is not recommended. This will be kept to True.
|
||||
globalPrivacyControl: navigator.globalPrivacyControl
|
||||
|
||||
screen:
|
||||
# hasHDR is not implemented in Camoufox
|
||||
availLeft: screen.availLeft
|
||||
availTop: screen.availTop
|
||||
availWidth: screen.availWidth
|
||||
availHeight: screen.availHeight
|
||||
height: screen.height
|
||||
width: screen.width
|
||||
colorDepth: screen.colorDepth
|
||||
pixelDepth: screen.pixelDepth
|
||||
# devicePixelRatio is not recommended. Any value other than 1.0 is suspicious.
|
||||
pageXOffset: screen.pageXOffset
|
||||
pageYOffset: screen.pageYOffset
|
||||
outerHeight: window.outerHeight
|
||||
outerWidth: window.outerWidth
|
||||
innerHeight: window.innerHeight
|
||||
innerWidth: window.innerWidth
|
||||
screenX: window.screenX
|
||||
screenY: window.screenY
|
||||
# Tends to generate out of bounds (network inconsistencies):
|
||||
# clientWidth: document.body.clientWidth
|
||||
# clientHeight: document.body.clientHeight
|
||||
|
||||
# videoCard:
|
||||
# renderer: webgl:renderer
|
||||
# vendor: webgl:vendor
|
||||
|
||||
headers:
|
||||
# headers.User-Agent is redundant with navigator.userAgent
|
||||
# headers.Accept-Language is redundant with locale:*
|
||||
Accept-Encoding: headers.Accept-Encoding
|
||||
|
||||
battery:
|
||||
charging: battery:charging
|
||||
chargingTime: battery:chargingTime
|
||||
dischargingTime: battery:dischargingTime
|
||||
|
||||
# Unsupported: videoCodecs, audioCodecs, pluginsData, multimediaDevices
|
||||
# Fonts are listed through the launcher.
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,822 +0,0 @@
|
||||
{
|
||||
"win": [
|
||||
"Arial",
|
||||
"Arial Black",
|
||||
"Bahnschrift",
|
||||
"Calibri",
|
||||
"Calibri Light",
|
||||
"Cambria",
|
||||
"Cambria Math",
|
||||
"Candara",
|
||||
"Candara Light",
|
||||
"Comic Sans MS",
|
||||
"Consolas",
|
||||
"Constantia",
|
||||
"Corbel",
|
||||
"Corbel Light",
|
||||
"Courier New",
|
||||
"Ebrima",
|
||||
"Franklin Gothic Medium",
|
||||
"Gabriola",
|
||||
"Gadugi",
|
||||
"Georgia",
|
||||
"HoloLens MDL2 Assets",
|
||||
"Impact",
|
||||
"Ink Free",
|
||||
"Javanese Text",
|
||||
"Leelawadee UI",
|
||||
"Leelawadee UI Semilight",
|
||||
"Lucida Console",
|
||||
"Lucida Sans Unicode",
|
||||
"MS Gothic",
|
||||
"MS PGothic",
|
||||
"MS UI Gothic",
|
||||
"MV Boli",
|
||||
"Malgun Gothic",
|
||||
"Malgun Gothic Semilight",
|
||||
"Marlett",
|
||||
"Microsoft Himalaya",
|
||||
"Microsoft JhengHei",
|
||||
"Microsoft JhengHei Light",
|
||||
"Microsoft JhengHei UI",
|
||||
"Microsoft JhengHei UI Light",
|
||||
"Microsoft New Tai Lue",
|
||||
"Microsoft PhagsPa",
|
||||
"Microsoft Sans Serif",
|
||||
"Microsoft Tai Le",
|
||||
"Microsoft YaHei",
|
||||
"Microsoft YaHei Light",
|
||||
"Microsoft YaHei UI",
|
||||
"Microsoft YaHei UI Light",
|
||||
"Microsoft Yi Baiti",
|
||||
"MingLiU-ExtB",
|
||||
"MingLiU_HKSCS-ExtB",
|
||||
"Mongolian Baiti",
|
||||
"Myanmar Text",
|
||||
"NSimSun",
|
||||
"Nirmala UI",
|
||||
"Nirmala UI Semilight",
|
||||
"PMingLiU-ExtB",
|
||||
"Palatino Linotype",
|
||||
"Segoe Fluent Icons",
|
||||
"Segoe MDL2 Assets",
|
||||
"Segoe Print",
|
||||
"Segoe Script",
|
||||
"Segoe UI",
|
||||
"Segoe UI Black",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Historic",
|
||||
"Segoe UI Light",
|
||||
"Segoe UI Semibold",
|
||||
"Segoe UI Semilight",
|
||||
"Segoe UI Symbol",
|
||||
"Segoe UI Variable",
|
||||
"SimSun",
|
||||
"SimSun-ExtB",
|
||||
"Sitka",
|
||||
"Sitka Text",
|
||||
"Sylfaen",
|
||||
"Symbol",
|
||||
"Tahoma",
|
||||
"Times New Roman",
|
||||
"Trebuchet MS",
|
||||
"Twemoji Mozilla",
|
||||
"Verdana",
|
||||
"Webdings",
|
||||
"Wingdings",
|
||||
"Yu Gothic",
|
||||
"Yu Gothic Light",
|
||||
"Yu Gothic Medium",
|
||||
"Yu Gothic UI",
|
||||
"Yu Gothic UI Light",
|
||||
"Yu Gothic UI Semibold",
|
||||
"Yu Gothic UI Semilight",
|
||||
"\u5b8b\u4f53",
|
||||
"\u5fae\u8edf\u6b63\u9ed1\u9ad4",
|
||||
"\u5fae\u8edf\u6b63\u9ed1\u9ad4 Light",
|
||||
"\u5fae\u8f6f\u96c5\u9ed1",
|
||||
"\u5fae\u8f6f\u96c5\u9ed1 Light",
|
||||
"\u65b0\u5b8b\u4f53",
|
||||
"\u65b0\u7d30\u660e\u9ad4-ExtB",
|
||||
"\u6e38\u30b4\u30b7\u30c3\u30af",
|
||||
"\u6e38\u30b4\u30b7\u30c3\u30af Light",
|
||||
"\u6e38\u30b4\u30b7\u30c3\u30af Medium",
|
||||
"\u7d30\u660e\u9ad4-ExtB",
|
||||
"\u7d30\u660e\u9ad4_HKSCS-ExtB",
|
||||
"\ub9d1\uc740 \uace0\ub515",
|
||||
"\ub9d1\uc740 \uace0\ub515 Semilight",
|
||||
"\uff2d\uff33 \u30b4\u30b7\u30c3\u30af",
|
||||
"\uff2d\uff33 \uff30\u30b4\u30b7\u30c3\u30af"
|
||||
],
|
||||
"mac": [
|
||||
".Al Bayan PUA",
|
||||
".Al Nile PUA",
|
||||
".Al Tarikh PUA",
|
||||
".Apple Color Emoji UI",
|
||||
".Apple SD Gothic NeoI",
|
||||
".Aqua Kana",
|
||||
".Aqua Kana Bold",
|
||||
".Aqua \u304b\u306a",
|
||||
".Aqua \u304b\u306a \u30dc\u30fc\u30eb\u30c9",
|
||||
".Arial Hebrew Desk Interface",
|
||||
".Baghdad PUA",
|
||||
".Beirut PUA",
|
||||
".Damascus PUA",
|
||||
".DecoType Naskh PUA",
|
||||
".Diwan Kufi PUA",
|
||||
".Farah PUA",
|
||||
".Geeza Pro Interface",
|
||||
".Geeza Pro PUA",
|
||||
".Helvetica LT MM",
|
||||
".Hiragino Kaku Gothic Interface",
|
||||
".Hiragino Sans GB Interface",
|
||||
".Keyboard",
|
||||
".KufiStandardGK PUA",
|
||||
".LastResort",
|
||||
".Lucida Grande UI",
|
||||
".Muna PUA",
|
||||
".Nadeem PUA",
|
||||
".New York",
|
||||
".Noto Nastaliq Urdu UI",
|
||||
".PingFang HK",
|
||||
".PingFang SC",
|
||||
".PingFang TC",
|
||||
".SF Arabic",
|
||||
".SF Arabic Rounded",
|
||||
".SF Compact",
|
||||
".SF Compact Rounded",
|
||||
".SF NS",
|
||||
".SF NS Mono",
|
||||
".SF NS Rounded",
|
||||
".Sana PUA",
|
||||
".Savoye LET CC.",
|
||||
".ThonburiUI",
|
||||
".ThonburiUIWatch",
|
||||
".\u82f9\u65b9-\u6e2f",
|
||||
".\u82f9\u65b9-\u7b80",
|
||||
".\u82f9\u65b9-\u7e41",
|
||||
".\u860b\u65b9-\u6e2f",
|
||||
".\u860b\u65b9-\u7c21",
|
||||
".\u860b\u65b9-\u7e41",
|
||||
"Academy Engraved LET",
|
||||
"Al Bayan",
|
||||
"Al Nile",
|
||||
"Al Tarikh",
|
||||
"American Typewriter",
|
||||
"Andale Mono",
|
||||
"Apple Braille",
|
||||
"Apple Chancery",
|
||||
"Apple Color Emoji",
|
||||
"Apple SD Gothic Neo",
|
||||
"Apple SD \uc0b0\ub3cc\uace0\ub515 Neo",
|
||||
"Apple Symbols",
|
||||
"AppleGothic",
|
||||
"AppleMyungjo",
|
||||
"Arial",
|
||||
"Arial Black",
|
||||
"Arial Hebrew",
|
||||
"Arial Hebrew Scholar",
|
||||
"Arial Narrow",
|
||||
"Arial Rounded MT Bold",
|
||||
"Arial Unicode MS",
|
||||
"Athelas",
|
||||
"Avenir",
|
||||
"Avenir Black",
|
||||
"Avenir Black Oblique",
|
||||
"Avenir Book",
|
||||
"Avenir Heavy",
|
||||
"Avenir Light",
|
||||
"Avenir Medium",
|
||||
"Avenir Next",
|
||||
"Avenir Next Condensed",
|
||||
"Avenir Next Condensed Demi Bold",
|
||||
"Avenir Next Condensed Heavy",
|
||||
"Avenir Next Condensed Medium",
|
||||
"Avenir Next Condensed Ultra Light",
|
||||
"Avenir Next Demi Bold",
|
||||
"Avenir Next Heavy",
|
||||
"Avenir Next Medium",
|
||||
"Avenir Next Ultra Light",
|
||||
"Ayuthaya",
|
||||
"Baghdad",
|
||||
"Bangla MN",
|
||||
"Bangla Sangam MN",
|
||||
"Baskerville",
|
||||
"Beirut",
|
||||
"Big Caslon",
|
||||
"Bodoni 72",
|
||||
"Bodoni 72 Oldstyle",
|
||||
"Bodoni 72 Smallcaps",
|
||||
"Bodoni Ornaments",
|
||||
"Bradley Hand",
|
||||
"Brush Script MT",
|
||||
"Chalkboard",
|
||||
"Chalkboard SE",
|
||||
"Chalkduster",
|
||||
"Charter",
|
||||
"Charter Black",
|
||||
"Cochin",
|
||||
"Comic Sans MS",
|
||||
"Copperplate",
|
||||
"Corsiva Hebrew",
|
||||
"Courier",
|
||||
"Courier New",
|
||||
"Czcionka systemowa",
|
||||
"DIN Alternate",
|
||||
"DIN Condensed",
|
||||
"Damascus",
|
||||
"DecoType Naskh",
|
||||
"Devanagari MT",
|
||||
"Devanagari Sangam MN",
|
||||
"Didot",
|
||||
"Diwan Kufi",
|
||||
"Diwan Thuluth",
|
||||
"Euphemia UCAS",
|
||||
"Farah",
|
||||
"Farisi",
|
||||
"Font Sistem",
|
||||
"Font de sistem",
|
||||
"Font di sistema",
|
||||
"Font sustava",
|
||||
"Fonte do Sistema",
|
||||
"Futura",
|
||||
"GB18030 Bitmap",
|
||||
"Galvji",
|
||||
"Geeza Pro",
|
||||
"Geneva",
|
||||
"Georgia",
|
||||
"Gill Sans",
|
||||
"Grantha Sangam MN",
|
||||
"Gujarati MT",
|
||||
"Gujarati Sangam MN",
|
||||
"Gurmukhi MN",
|
||||
"Gurmukhi MT",
|
||||
"Gurmukhi Sangam MN",
|
||||
"Heiti SC",
|
||||
"Heiti TC",
|
||||
"Heiti-\uac04\uccb4",
|
||||
"Heiti-\ubc88\uccb4",
|
||||
"Helvetica",
|
||||
"Helvetica Neue",
|
||||
"Herculanum",
|
||||
"Hiragino Kaku Gothic Pro",
|
||||
"Hiragino Kaku Gothic Pro W3",
|
||||
"Hiragino Kaku Gothic Pro W6",
|
||||
"Hiragino Kaku Gothic ProN",
|
||||
"Hiragino Kaku Gothic ProN W3",
|
||||
"Hiragino Kaku Gothic ProN W6",
|
||||
"Hiragino Kaku Gothic Std",
|
||||
"Hiragino Kaku Gothic Std W8",
|
||||
"Hiragino Kaku Gothic StdN",
|
||||
"Hiragino Kaku Gothic StdN W8",
|
||||
"Hiragino Maru Gothic Pro",
|
||||
"Hiragino Maru Gothic Pro W4",
|
||||
"Hiragino Maru Gothic ProN",
|
||||
"Hiragino Maru Gothic ProN W4",
|
||||
"Hiragino Mincho Pro",
|
||||
"Hiragino Mincho Pro W3",
|
||||
"Hiragino Mincho Pro W6",
|
||||
"Hiragino Mincho ProN",
|
||||
"Hiragino Mincho ProN W3",
|
||||
"Hiragino Mincho ProN W6",
|
||||
"Hiragino Sans",
|
||||
"Hiragino Sans GB",
|
||||
"Hiragino Sans GB W3",
|
||||
"Hiragino Sans GB W6",
|
||||
"Hiragino Sans W0",
|
||||
"Hiragino Sans W1",
|
||||
"Hiragino Sans W2",
|
||||
"Hiragino Sans W3",
|
||||
"Hiragino Sans W4",
|
||||
"Hiragino Sans W5",
|
||||
"Hiragino Sans W6",
|
||||
"Hiragino Sans W7",
|
||||
"Hiragino Sans W8",
|
||||
"Hiragino Sans W9",
|
||||
"Hoefler Text",
|
||||
"Hoefler Text Ornaments",
|
||||
"ITF Devanagari",
|
||||
"ITF Devanagari Marathi",
|
||||
"Impact",
|
||||
"InaiMathi",
|
||||
"Iowan Old Style",
|
||||
"Iowan Old Style Black",
|
||||
"J\u00e4rjestelm\u00e4fontti",
|
||||
"Kailasa",
|
||||
"Kannada MN",
|
||||
"Kannada Sangam MN",
|
||||
"Kefa",
|
||||
"Khmer MN",
|
||||
"Khmer Sangam MN",
|
||||
"Kohinoor Bangla",
|
||||
"Kohinoor Devanagari",
|
||||
"Kohinoor Gujarati",
|
||||
"Kohinoor Telugu",
|
||||
"Kokonor",
|
||||
"Krungthep",
|
||||
"KufiStandardGK",
|
||||
"Lao MN",
|
||||
"Lao Sangam MN",
|
||||
"Lucida Grande",
|
||||
"Luminari",
|
||||
"Malayalam MN",
|
||||
"Malayalam Sangam MN",
|
||||
"Marion",
|
||||
"Marker Felt",
|
||||
"Menlo",
|
||||
"Microsoft Sans Serif",
|
||||
"Mishafi",
|
||||
"Mishafi Gold",
|
||||
"Monaco",
|
||||
"Mshtakan",
|
||||
"Mukta Mahee",
|
||||
"MuktaMahee Bold",
|
||||
"MuktaMahee ExtraBold",
|
||||
"MuktaMahee ExtraLight",
|
||||
"MuktaMahee Light",
|
||||
"MuktaMahee Medium",
|
||||
"MuktaMahee Regular",
|
||||
"MuktaMahee SemiBold",
|
||||
"Muna",
|
||||
"Myanmar MN",
|
||||
"Myanmar Sangam MN",
|
||||
"Nadeem",
|
||||
"New Peninim MT",
|
||||
"Noteworthy",
|
||||
"Noto Nastaliq Urdu",
|
||||
"Noto Sans Adlam",
|
||||
"Noto Sans Armenian",
|
||||
"Noto Sans Armenian Blk",
|
||||
"Noto Sans Armenian ExtBd",
|
||||
"Noto Sans Armenian ExtLt",
|
||||
"Noto Sans Armenian Light",
|
||||
"Noto Sans Armenian Med",
|
||||
"Noto Sans Armenian SemBd",
|
||||
"Noto Sans Armenian Thin",
|
||||
"Noto Sans Avestan",
|
||||
"Noto Sans Bamum",
|
||||
"Noto Sans Bassa Vah",
|
||||
"Noto Sans Batak",
|
||||
"Noto Sans Bhaiksuki",
|
||||
"Noto Sans Brahmi",
|
||||
"Noto Sans Buginese",
|
||||
"Noto Sans Buhid",
|
||||
"Noto Sans CanAborig",
|
||||
"Noto Sans Canadian Aboriginal",
|
||||
"Noto Sans Carian",
|
||||
"Noto Sans CaucAlban",
|
||||
"Noto Sans Caucasian Albanian",
|
||||
"Noto Sans Chakma",
|
||||
"Noto Sans Cham",
|
||||
"Noto Sans Coptic",
|
||||
"Noto Sans Cuneiform",
|
||||
"Noto Sans Cypriot",
|
||||
"Noto Sans Duployan",
|
||||
"Noto Sans EgyptHiero",
|
||||
"Noto Sans Egyptian Hieroglyphs",
|
||||
"Noto Sans Elbasan",
|
||||
"Noto Sans Glagolitic",
|
||||
"Noto Sans Gothic",
|
||||
"Noto Sans Gunjala Gondi",
|
||||
"Noto Sans Hanifi Rohingya",
|
||||
"Noto Sans HanifiRohg",
|
||||
"Noto Sans Hanunoo",
|
||||
"Noto Sans Hatran",
|
||||
"Noto Sans ImpAramaic",
|
||||
"Noto Sans Imperial Aramaic",
|
||||
"Noto Sans InsPahlavi",
|
||||
"Noto Sans InsParthi",
|
||||
"Noto Sans Inscriptional Pahlavi",
|
||||
"Noto Sans Inscriptional Parthian",
|
||||
"Noto Sans Javanese",
|
||||
"Noto Sans Kaithi",
|
||||
"Noto Sans Kannada",
|
||||
"Noto Sans Kannada Black",
|
||||
"Noto Sans Kannada ExtraBold",
|
||||
"Noto Sans Kannada ExtraLight",
|
||||
"Noto Sans Kannada Light",
|
||||
"Noto Sans Kannada Medium",
|
||||
"Noto Sans Kannada SemiBold",
|
||||
"Noto Sans Kannada Thin",
|
||||
"Noto Sans Kayah Li",
|
||||
"Noto Sans Kharoshthi",
|
||||
"Noto Sans Khojki",
|
||||
"Noto Sans Khudawadi",
|
||||
"Noto Sans Lepcha",
|
||||
"Noto Sans Limbu",
|
||||
"Noto Sans Linear A",
|
||||
"Noto Sans Linear B",
|
||||
"Noto Sans Lisu",
|
||||
"Noto Sans Lycian",
|
||||
"Noto Sans Lydian",
|
||||
"Noto Sans Mahajani",
|
||||
"Noto Sans Mandaic",
|
||||
"Noto Sans Manichaean",
|
||||
"Noto Sans Marchen",
|
||||
"Noto Sans Masaram Gondi",
|
||||
"Noto Sans Meetei Mayek",
|
||||
"Noto Sans Mende Kikakui",
|
||||
"Noto Sans Meroitic",
|
||||
"Noto Sans Miao",
|
||||
"Noto Sans Modi",
|
||||
"Noto Sans Mongolian",
|
||||
"Noto Sans Mro",
|
||||
"Noto Sans Multani",
|
||||
"Noto Sans Myanmar",
|
||||
"Noto Sans Myanmar Blk",
|
||||
"Noto Sans Myanmar ExtBd",
|
||||
"Noto Sans Myanmar ExtLt",
|
||||
"Noto Sans Myanmar Light",
|
||||
"Noto Sans Myanmar Med",
|
||||
"Noto Sans Myanmar SemBd",
|
||||
"Noto Sans Myanmar Thin",
|
||||
"Noto Sans NKo",
|
||||
"Noto Sans Nabataean",
|
||||
"Noto Sans New Tai Lue",
|
||||
"Noto Sans Newa",
|
||||
"Noto Sans Ol Chiki",
|
||||
"Noto Sans Old Hungarian",
|
||||
"Noto Sans Old Italic",
|
||||
"Noto Sans Old North Arabian",
|
||||
"Noto Sans Old Permic",
|
||||
"Noto Sans Old Persian",
|
||||
"Noto Sans Old South Arabian",
|
||||
"Noto Sans Old Turkic",
|
||||
"Noto Sans OldHung",
|
||||
"Noto Sans OldNorArab",
|
||||
"Noto Sans OldSouArab",
|
||||
"Noto Sans Oriya",
|
||||
"Noto Sans Osage",
|
||||
"Noto Sans Osmanya",
|
||||
"Noto Sans Pahawh Hmong",
|
||||
"Noto Sans Palmyrene",
|
||||
"Noto Sans Pau Cin Hau",
|
||||
"Noto Sans PhagsPa",
|
||||
"Noto Sans Phoenician",
|
||||
"Noto Sans PsaPahlavi",
|
||||
"Noto Sans Psalter Pahlavi",
|
||||
"Noto Sans Rejang",
|
||||
"Noto Sans Samaritan",
|
||||
"Noto Sans Saurashtra",
|
||||
"Noto Sans Sharada",
|
||||
"Noto Sans Siddham",
|
||||
"Noto Sans Sora Sompeng",
|
||||
"Noto Sans SoraSomp",
|
||||
"Noto Sans Sundanese",
|
||||
"Noto Sans Syloti Nagri",
|
||||
"Noto Sans Syriac",
|
||||
"Noto Sans Tagalog",
|
||||
"Noto Sans Tagbanwa",
|
||||
"Noto Sans Tai Le",
|
||||
"Noto Sans Tai Tham",
|
||||
"Noto Sans Tai Viet",
|
||||
"Noto Sans Takri",
|
||||
"Noto Sans Thaana",
|
||||
"Noto Sans Tifinagh",
|
||||
"Noto Sans Tirhuta",
|
||||
"Noto Sans Ugaritic",
|
||||
"Noto Sans Vai",
|
||||
"Noto Sans Wancho",
|
||||
"Noto Sans Warang Citi",
|
||||
"Noto Sans Yi",
|
||||
"Noto Sans Zawgyi",
|
||||
"Noto Sans Zawgyi Blk",
|
||||
"Noto Sans Zawgyi ExtBd",
|
||||
"Noto Sans Zawgyi ExtLt",
|
||||
"Noto Sans Zawgyi Light",
|
||||
"Noto Sans Zawgyi Med",
|
||||
"Noto Sans Zawgyi SemBd",
|
||||
"Noto Sans Zawgyi Thin",
|
||||
"Noto Serif Ahom",
|
||||
"Noto Serif Balinese",
|
||||
"Noto Serif Hmong Nyiakeng",
|
||||
"Noto Serif Myanmar",
|
||||
"Noto Serif Myanmar Blk",
|
||||
"Noto Serif Myanmar ExtBd",
|
||||
"Noto Serif Myanmar ExtLt",
|
||||
"Noto Serif Myanmar Light",
|
||||
"Noto Serif Myanmar Med",
|
||||
"Noto Serif Myanmar SemBd",
|
||||
"Noto Serif Myanmar Thin",
|
||||
"Noto Serif Yezidi",
|
||||
"Optima",
|
||||
"Oriya MN",
|
||||
"Oriya Sangam MN",
|
||||
"PT Mono",
|
||||
"PT Sans",
|
||||
"PT Sans Caption",
|
||||
"PT Sans Narrow",
|
||||
"PT Serif",
|
||||
"PT Serif Caption",
|
||||
"Palatino",
|
||||
"Papyrus",
|
||||
"Party LET",
|
||||
"Phosphate",
|
||||
"Ph\u00f4ng ch\u1eef H\u1ec7 th\u1ed1ng",
|
||||
"PingFang HK",
|
||||
"PingFang SC",
|
||||
"PingFang TC",
|
||||
"Plantagenet Cherokee",
|
||||
"Police syst\u00e8me",
|
||||
"Raanana",
|
||||
"Rendszerbet\u0171t\u00edpus",
|
||||
"Rockwell",
|
||||
"STIX Two Math",
|
||||
"STIX Two Text",
|
||||
"STIXGeneral",
|
||||
"STIXIntegralsD",
|
||||
"STIXIntegralsSm",
|
||||
"STIXIntegralsUp",
|
||||
"STIXIntegralsUpD",
|
||||
"STIXIntegralsUpSm",
|
||||
"STIXNonUnicode",
|
||||
"STIXSizeFiveSym",
|
||||
"STIXSizeFourSym",
|
||||
"STIXSizeOneSym",
|
||||
"STIXSizeThreeSym",
|
||||
"STIXSizeTwoSym",
|
||||
"STIXVariants",
|
||||
"STSong",
|
||||
"Sana",
|
||||
"Sathu",
|
||||
"Savoye LET",
|
||||
"Seravek",
|
||||
"Seravek ExtraLight",
|
||||
"Seravek Light",
|
||||
"Seravek Medium",
|
||||
"Shree Devanagari 714",
|
||||
"SignPainter",
|
||||
"SignPainter-HouseScript",
|
||||
"Silom",
|
||||
"Sinhala MN",
|
||||
"Sinhala Sangam MN",
|
||||
"Sistem Fontu",
|
||||
"Skia",
|
||||
"Snell Roundhand",
|
||||
"Songti SC",
|
||||
"Songti TC",
|
||||
"Sukhumvit Set",
|
||||
"Superclarendon",
|
||||
"Symbol",
|
||||
"Systeemlettertype",
|
||||
"System Font",
|
||||
"Systemschrift",
|
||||
"Systemskrift",
|
||||
"Systemtypsnitt",
|
||||
"Syst\u00e9mov\u00e9 p\u00edsmo",
|
||||
"Tahoma",
|
||||
"Tamil MN",
|
||||
"Tamil Sangam MN",
|
||||
"Telugu MN",
|
||||
"Telugu Sangam MN",
|
||||
"Thonburi",
|
||||
"Times",
|
||||
"Times New Roman",
|
||||
"Tipo de letra del sistema",
|
||||
"Tipo de letra do sistema",
|
||||
"Tipus de lletra del sistema",
|
||||
"Trattatello",
|
||||
"Trebuchet MS",
|
||||
"Verdana",
|
||||
"Waseem",
|
||||
"Webdings",
|
||||
"Wingdings",
|
||||
"Wingdings 2",
|
||||
"Wingdings 3",
|
||||
"Zapf Dingbats",
|
||||
"Zapfino",
|
||||
"\u0393\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2",
|
||||
"\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0439 \u0448\u0440\u0438\u0444\u0442",
|
||||
"\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u0448\u0440\u0438\u0444\u0442",
|
||||
"\u05d2\u05d5\u05e4\u05df \u05de\u05e2\u05e8\u05db\u05ea",
|
||||
"\u0627\u0644\u0628\u064a\u0627\u0646",
|
||||
"\u0627\u0644\u062a\u0627\u0631\u064a\u062e",
|
||||
"\u0627\u0644\u0646\u064a\u0644",
|
||||
"\u0628\u063a\u062f\u0627\u062f",
|
||||
"\u0628\u064a\u0631\u0648\u062a",
|
||||
"\u062c\u064a\u0632\u0629",
|
||||
"\u062e\u0637 \u0627\u0644\u0646\u0638\u0627\u0645",
|
||||
"\u062f\u0645\u0634\u0642",
|
||||
"\u062f\u064a\u0648\u0627\u0646 \u062b\u0644\u062b",
|
||||
"\u062f\u064a\u0648\u0627\u0646 \u0643\u0648\u0641\u064a",
|
||||
"\u0635\u0646\u0639\u0627\u0621",
|
||||
"\u0641\u0627\u0631\u0633\u064a",
|
||||
"\u0641\u0631\u062d",
|
||||
"\u0643\u0648\u0641\u064a",
|
||||
"\u0645\u0646\u0649",
|
||||
"\u0645\u0650\u0635\u062d\u0641\u064a",
|
||||
"\u0645\u0650\u0635\u062d\u0641\u064a \u0630\u0647\u0628\u064a",
|
||||
"\u0646\u062f\u064a\u0645",
|
||||
"\u0646\u0633\u062e",
|
||||
"\u0648\u0633\u064a\u0645",
|
||||
"\u0906\u0908\u0970\u091f\u0940\u0970\u090f\u092b\u093c\u0970 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940",
|
||||
"\u0906\u0908\u0970\u091f\u0940\u0970\u090f\u092b\u093c\u0970 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u092e\u0930\u093e\u0920\u0940",
|
||||
"\u0915\u094b\u0939\u093f\u0928\u0942\u0930 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940",
|
||||
"\u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u090f\u092e\u0970\u091f\u0940\u0970",
|
||||
"\u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u0938\u0902\u0917\u092e \u090f\u092e\u0970\u090f\u0928\u0970",
|
||||
"\u0936\u094d\u0930\u0940 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u096d\u0967\u096a",
|
||||
"\u0e41\u0e1a\u0e1a\u0e2d\u0e31\u0e01\u0e29\u0e23\u0e23\u0e30\u0e1a\u0e1a",
|
||||
"\u2e41\u7175\u6120\u82a9\u82c8",
|
||||
"\u30b7\u30b9\u30c6\u30e0\u30d5\u30a9\u30f3\u30c8",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 Pro",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 Pro W4",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 ProN",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 ProN W4",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Std",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Std W8",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 StdN",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 StdN W8",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587 W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587 W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W0",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W1",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W2",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W4",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W5",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W7",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W8",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W9",
|
||||
"\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587",
|
||||
"\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587 W3",
|
||||
"\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587 W6",
|
||||
"\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587",
|
||||
"\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587 W3",
|
||||
"\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587 W6",
|
||||
"\u5b8b\u4f53-\u7b80",
|
||||
"\u5b8b\u4f53-\u7e41",
|
||||
"\u5b8b\u9ad4-\u7c21",
|
||||
"\u5b8b\u9ad4-\u7e41",
|
||||
"\u7cfb\u7d71\u5b57\u9ad4",
|
||||
"\u7cfb\u7edf\u5b57\u4f53",
|
||||
"\u82f9\u65b9-\u6e2f",
|
||||
"\u82f9\u65b9-\u7b80",
|
||||
"\u82f9\u65b9-\u7e41",
|
||||
"\u8371\u8389\u834d\u836d\u8a70\u8353\u2050\u726f",
|
||||
"\u8371\u8389\u834d\u836d\u8a70\u8353\u2053\u7464",
|
||||
"\u8371\u8389\u834d\u836d\u8a70\u8353\u8356\u8362\u834e",
|
||||
"\u8371\u8389\u834d\u836d\u8adb\u8353\u2050\u726f",
|
||||
"\u8371\u8389\u834d\u836d\u96be\u92a9\u2050\u726f",
|
||||
"\u860b\u65b9-\u6e2f",
|
||||
"\u860b\u65b9-\u7c21",
|
||||
"\u860b\u65b9-\u7e41",
|
||||
"\u9ed1\u4f53-\u7b80",
|
||||
"\u9ed1\u4f53-\u7e41",
|
||||
"\u9ed1\u9ad4-\u7c21",
|
||||
"\u9ed1\u9ad4-\u7e41",
|
||||
"\u9ed2\u4f53-\u7c21",
|
||||
"\u9ed2\u4f53-\u7e41",
|
||||
"\uc2dc\uc2a4\ud15c \uc11c\uccb4"
|
||||
],
|
||||
"lin": [
|
||||
"Arimo",
|
||||
"Cousine",
|
||||
"Noto Naskh Arabic",
|
||||
"Noto Sans Adlam",
|
||||
"Noto Sans Armenian",
|
||||
"Noto Sans Balinese",
|
||||
"Noto Sans Bamum",
|
||||
"Noto Sans Bassa Vah",
|
||||
"Noto Sans Batak",
|
||||
"Noto Sans Bengali",
|
||||
"Noto Sans Buginese",
|
||||
"Noto Sans Buhid",
|
||||
"Noto Sans Canadian Aboriginal",
|
||||
"Noto Sans Chakma",
|
||||
"Noto Sans Cham",
|
||||
"Noto Sans Cherokee",
|
||||
"Noto Sans Coptic",
|
||||
"Noto Sans Deseret",
|
||||
"Noto Sans Devanagari",
|
||||
"Noto Sans Elbasan",
|
||||
"Noto Sans Ethiopic",
|
||||
"Noto Sans Georgian",
|
||||
"Noto Sans Grantha",
|
||||
"Noto Sans Gujarati",
|
||||
"Noto Sans Gunjala Gondi",
|
||||
"Noto Sans Gurmukhi",
|
||||
"Noto Sans Hanifi Rohingya",
|
||||
"Noto Sans Hanunoo",
|
||||
"Noto Sans Hebrew",
|
||||
"Noto Sans JP",
|
||||
"Noto Sans Javanese",
|
||||
"Noto Sans KR",
|
||||
"Noto Sans Kannada",
|
||||
"Noto Sans Kayah Li",
|
||||
"Noto Sans Khmer",
|
||||
"Noto Sans Khojki",
|
||||
"Noto Sans Khudawadi",
|
||||
"Noto Sans Lao",
|
||||
"Noto Sans Lepcha",
|
||||
"Noto Sans Limbu",
|
||||
"Noto Sans Lisu",
|
||||
"Noto Sans Mahajani",
|
||||
"Noto Sans Malayalam",
|
||||
"Noto Sans Mandaic",
|
||||
"Noto Sans Masaram Gondi",
|
||||
"Noto Sans Medefaidrin",
|
||||
"Noto Sans Meetei Mayek",
|
||||
"Noto Sans Mende Kikakui",
|
||||
"Noto Sans Miao",
|
||||
"Noto Sans Modi",
|
||||
"Noto Sans Mongolian",
|
||||
"Noto Sans Mro",
|
||||
"Noto Sans Multani",
|
||||
"Noto Sans Myanmar",
|
||||
"Noto Sans NKo",
|
||||
"Noto Sans New Tai Lue",
|
||||
"Noto Sans Newa",
|
||||
"Noto Sans Ol Chiki",
|
||||
"Noto Sans Oriya",
|
||||
"Noto Sans Osage",
|
||||
"Noto Sans Osmanya",
|
||||
"Noto Sans Pahawh Hmong",
|
||||
"Noto Sans Pau Cin Hau",
|
||||
"Noto Sans Rejang",
|
||||
"Noto Sans Runic",
|
||||
"Noto Sans SC",
|
||||
"Noto Sans Samaritan",
|
||||
"Noto Sans Saurashtra",
|
||||
"Noto Sans Sharada",
|
||||
"Noto Sans Shavian",
|
||||
"Noto Sans Sinhala",
|
||||
"Noto Sans Sora Sompeng",
|
||||
"Noto Sans Soyombo",
|
||||
"Noto Sans Sundanese",
|
||||
"Noto Sans Syloti Nagri",
|
||||
"Noto Sans Symbols",
|
||||
"Noto Sans Symbols 2",
|
||||
"Noto Sans Syriac",
|
||||
"Noto Sans TC",
|
||||
"Noto Sans Tagalog",
|
||||
"Noto Sans Tagbanwa",
|
||||
"Noto Sans Tai Le",
|
||||
"Noto Sans Tai Tham",
|
||||
"Noto Sans Tai Viet",
|
||||
"Noto Sans Takri",
|
||||
"Noto Sans Tamil",
|
||||
"Noto Sans Telugu",
|
||||
"Noto Sans Thaana",
|
||||
"Noto Sans Thai",
|
||||
"Noto Sans Tifinagh",
|
||||
"Noto Sans Tifinagh APT",
|
||||
"Noto Sans Tifinagh Adrar",
|
||||
"Noto Sans Tifinagh Agraw Imazighen",
|
||||
"Noto Sans Tifinagh Ahaggar",
|
||||
"Noto Sans Tifinagh Air",
|
||||
"Noto Sans Tifinagh Azawagh",
|
||||
"Noto Sans Tifinagh Ghat",
|
||||
"Noto Sans Tifinagh Hawad",
|
||||
"Noto Sans Tifinagh Rhissa Ixa",
|
||||
"Noto Sans Tifinagh SIL",
|
||||
"Noto Sans Tifinagh Tawellemmet",
|
||||
"Noto Sans Tirhuta",
|
||||
"Noto Sans Vai",
|
||||
"Noto Sans Wancho",
|
||||
"Noto Sans Warang Citi",
|
||||
"Noto Sans Yi",
|
||||
"Noto Sans Zanabazar Square",
|
||||
"Noto Serif Armenian",
|
||||
"Noto Serif Balinese",
|
||||
"Noto Serif Bengali",
|
||||
"Noto Serif Devanagari",
|
||||
"Noto Serif Dogra",
|
||||
"Noto Serif Ethiopic",
|
||||
"Noto Serif Georgian",
|
||||
"Noto Serif Grantha",
|
||||
"Noto Serif Gujarati",
|
||||
"Noto Serif Gurmukhi",
|
||||
"Noto Serif Hebrew",
|
||||
"Noto Serif Kannada",
|
||||
"Noto Serif Khmer",
|
||||
"Noto Serif Khojki",
|
||||
"Noto Serif Lao",
|
||||
"Noto Serif Malayalam",
|
||||
"Noto Serif Myanmar",
|
||||
"Noto Serif NP Hmong",
|
||||
"Noto Serif Sinhala",
|
||||
"Noto Serif Tamil",
|
||||
"Noto Serif Telugu",
|
||||
"Noto Serif Thai",
|
||||
"Noto Serif Tibetan",
|
||||
"Noto Serif Yezidi",
|
||||
"STIX Two Math",
|
||||
"Tinos",
|
||||
"Twemoji Mozilla"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -1,164 +0,0 @@
|
||||
{
|
||||
"safari": [
|
||||
"Referer",
|
||||
"Origin",
|
||||
"Content-Type",
|
||||
"Accept",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"User-Agent",
|
||||
"Content-Length",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Connection",
|
||||
"Host",
|
||||
"Cookie",
|
||||
"Sec-Fetch-Dest",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-Site",
|
||||
":method",
|
||||
":scheme",
|
||||
":authority",
|
||||
":path",
|
||||
"referer",
|
||||
"origin",
|
||||
"content-type",
|
||||
"accept",
|
||||
"user-agent",
|
||||
"content-length",
|
||||
"accept-encoding",
|
||||
"accept-language",
|
||||
"cookie",
|
||||
"sec-fetch-dest",
|
||||
"sec-fetch-mode",
|
||||
"sec-fetch-site"
|
||||
],
|
||||
"chrome": [
|
||||
"Host",
|
||||
"Connection",
|
||||
"Content-Length",
|
||||
"Cache-Control",
|
||||
"sec-ch-ua",
|
||||
"sec-ch-ua-mobile",
|
||||
"sec-ch-ua-platform",
|
||||
"Origin",
|
||||
"Content-Type",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"User-Agent",
|
||||
"Accept",
|
||||
"Sec-Fetch-Site",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-User",
|
||||
"Sec-Fetch-Dest",
|
||||
"Referer",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Cookie",
|
||||
":method",
|
||||
":authority",
|
||||
":scheme",
|
||||
":path",
|
||||
"content-length",
|
||||
"cache-control",
|
||||
"sec-ch-ua",
|
||||
"sec-ch-ua-mobile",
|
||||
"sec-ch-ua-platform",
|
||||
"origin",
|
||||
"content-type",
|
||||
"upgrade-insecure-requests",
|
||||
"user-agent",
|
||||
"accept",
|
||||
"sec-fetch-site",
|
||||
"sec-fetch-mode",
|
||||
"sec-fetch-user",
|
||||
"sec-fetch-dest",
|
||||
"referer",
|
||||
"accept-encoding",
|
||||
"accept-language",
|
||||
"cookie",
|
||||
"priority"
|
||||
],
|
||||
"firefox": [
|
||||
"Host",
|
||||
"User-Agent",
|
||||
"Accept",
|
||||
"Accept-Language",
|
||||
"Accept-Encoding",
|
||||
"Content-Type",
|
||||
"Content-Length",
|
||||
"Origin",
|
||||
"Connection",
|
||||
"Referer",
|
||||
"Cookie",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"Sec-Fetch-Dest",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-Site",
|
||||
"Sec-Fetch-User",
|
||||
"Priority",
|
||||
":method",
|
||||
":path",
|
||||
":authority",
|
||||
":scheme",
|
||||
"user-agent",
|
||||
"accept",
|
||||
"accept-language",
|
||||
"accept-encoding",
|
||||
"content-type",
|
||||
"content-length",
|
||||
"origin",
|
||||
"referer",
|
||||
"cookie",
|
||||
"upgrade-insecure-requests",
|
||||
"sec-fetch-dest",
|
||||
"sec-fetch-mode",
|
||||
"sec-fetch-site",
|
||||
"sec-fetch-user",
|
||||
"priority",
|
||||
"te"
|
||||
],
|
||||
"edge": [
|
||||
"Host",
|
||||
"Connection",
|
||||
"Content-Length",
|
||||
"Cache-Control",
|
||||
"sec-ch-ua",
|
||||
"sec-ch-ua-mobile",
|
||||
"sec-ch-ua-platform",
|
||||
"Origin",
|
||||
"Content-Type",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"User-Agent",
|
||||
"Accept",
|
||||
"Sec-Fetch-Site",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-User",
|
||||
"Sec-Fetch-Dest",
|
||||
"Referer",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Cookie",
|
||||
":method",
|
||||
":authority",
|
||||
":scheme",
|
||||
":path",
|
||||
"content-length",
|
||||
"cache-control",
|
||||
"sec-ch-ua",
|
||||
"sec-ch-ua-mobile",
|
||||
"sec-ch-ua-platform",
|
||||
"origin",
|
||||
"content-type",
|
||||
"upgrade-insecure-requests",
|
||||
"user-agent",
|
||||
"accept",
|
||||
"sec-fetch-site",
|
||||
"sec-fetch-mode",
|
||||
"sec-fetch-user",
|
||||
"sec-fetch-dest",
|
||||
"referer",
|
||||
"accept-encoding",
|
||||
"accept-language",
|
||||
"cookie",
|
||||
"priority"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -1,27 +0,0 @@
|
||||
pub const FINGERPRINT_NETWORK_ZIP: &[u8] = include_bytes!("fingerprint-network-definition.zip");
|
||||
pub const INPUT_NETWORK_ZIP: &[u8] = include_bytes!("input-network-definition.zip");
|
||||
pub const HEADER_NETWORK_ZIP: &[u8] = include_bytes!("header-network-definition.zip");
|
||||
pub const BROWSER_HELPER_JSON: &str = include_str!("browser-helper-file.json");
|
||||
pub const HEADERS_ORDER_JSON: &str = include_str!("headers-order.json");
|
||||
pub const FONTS_JSON: &str = include_str!("fonts.json");
|
||||
pub const BROWSERFORGE_YML: &str = include_str!("browserforge.yml");
|
||||
pub const WEBGL_DATA_DB: &[u8] = include_bytes!("webgl_data.db");
|
||||
pub const TERRITORY_INFO_XML: &str = include_str!("territoryInfo.xml");
|
||||
|
||||
/// Real fingerprint presets bundled with the original Camoufox v135 line
|
||||
/// (Firefox <= 148). Frozen upstream — kept around so users who haven't
|
||||
/// upgraded their Camoufox binary keep getting matched fingerprints.
|
||||
/// Mirrors `pythonlib/camoufox/fingerprint-presets.json` upstream.
|
||||
pub const FINGERPRINT_PRESETS_V135_JSON: &str = include_str!("fingerprint-presets-v135.json");
|
||||
|
||||
/// Real fingerprint presets for every Camoufox release after the v135 line
|
||||
/// (currently Firefox 149+ via the v150 build). This file is expected to
|
||||
/// be refreshed regularly as upstream Camoufox tracks newer Firefox
|
||||
/// releases — we keep the upstream filename here so each refresh is a
|
||||
/// straight `cp` from `pythonlib/camoufox/fingerprint-presets-v150.json`.
|
||||
pub const FINGERPRINT_PRESETS_NEWER_JSON: &str = include_str!("fingerprint-presets-v150.json");
|
||||
|
||||
/// Firefox major version at which the newer preset bundle takes over from
|
||||
/// the frozen v135 bundle. Matches `PRESETS_V150_MIN_FF` in
|
||||
/// `pythonlib/camoufox/fingerprints.py`.
|
||||
pub const PRESETS_NEWER_MIN_FF: u32 = 149;
|
||||
Binary file not shown.
@@ -1,142 +0,0 @@
|
||||
//! Environment variable handling for Camoufox configuration.
|
||||
//!
|
||||
//! Camoufox reads its configuration from environment variables named CAMOU_CONFIG_1, CAMOU_CONFIG_2, etc.
|
||||
//! The configuration JSON is chunked to fit within environment variable size limits.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Maximum chunk size for environment variables on Windows.
|
||||
const CHUNK_SIZE_WINDOWS: usize = 2047;
|
||||
|
||||
/// Maximum chunk size for environment variables on Unix systems.
|
||||
const CHUNK_SIZE_UNIX: usize = 32767;
|
||||
|
||||
/// Get the chunk size for the current platform.
|
||||
fn get_chunk_size() -> usize {
|
||||
if cfg!(windows) {
|
||||
CHUNK_SIZE_WINDOWS
|
||||
} else {
|
||||
CHUNK_SIZE_UNIX
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Camoufox config map to environment variables.
|
||||
///
|
||||
/// The config is serialized to JSON and split into chunks that fit within
|
||||
/// environment variable size limits. Each chunk is stored in a variable
|
||||
/// named CAMOU_CONFIG_1, CAMOU_CONFIG_2, etc.
|
||||
pub fn config_to_env_vars(
|
||||
config: &HashMap<String, serde_json::Value>,
|
||||
) -> Result<HashMap<String, String>, serde_json::Error> {
|
||||
let config_json = serde_json::to_string(config)?;
|
||||
Ok(chunk_config_string(&config_json))
|
||||
}
|
||||
|
||||
/// Split a config string into chunks and create environment variable map.
|
||||
pub fn chunk_config_string(config_str: &str) -> HashMap<String, String> {
|
||||
let chunk_size = get_chunk_size();
|
||||
let mut env_vars = HashMap::new();
|
||||
|
||||
for (i, chunk) in config_str.as_bytes().chunks(chunk_size).enumerate() {
|
||||
let chunk_str = String::from_utf8_lossy(chunk).to_string();
|
||||
let env_name = format!("CAMOU_CONFIG_{}", i + 1);
|
||||
env_vars.insert(env_name, chunk_str);
|
||||
}
|
||||
|
||||
env_vars
|
||||
}
|
||||
|
||||
/// Determine the target OS from a user agent string.
|
||||
pub fn determine_ua_os(user_agent: &str) -> &'static str {
|
||||
let ua_lower = user_agent.to_lowercase();
|
||||
|
||||
if ua_lower.contains("mac os") || ua_lower.contains("macos") || ua_lower.contains("macintosh") {
|
||||
"mac"
|
||||
} else if ua_lower.contains("windows") {
|
||||
"win"
|
||||
} else {
|
||||
"lin"
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the fontconfig path environment variable for Linux.
|
||||
pub fn get_fontconfig_env(target_os: &str, camoufox_path: &std::path::Path) -> Option<String> {
|
||||
if cfg!(target_os = "linux") {
|
||||
let fontconfig_dir = camoufox_path.join("fontconfig").join(target_os);
|
||||
if fontconfig_dir.exists() {
|
||||
return Some(fontconfig_dir.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chunk_small_config() {
|
||||
let config = r#"{"navigator.userAgent": "Mozilla/5.0"}"#;
|
||||
let env_vars = chunk_config_string(config);
|
||||
|
||||
assert_eq!(env_vars.len(), 1);
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_1"));
|
||||
assert_eq!(env_vars.get("CAMOU_CONFIG_1").unwrap(), config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_large_config() {
|
||||
// Create a config string larger than the chunk size
|
||||
let chunk_size = get_chunk_size();
|
||||
let large_value = "x".repeat(chunk_size * 2 + 100);
|
||||
let config = format!(r#"{{"key": "{}"}}"#, large_value);
|
||||
|
||||
let env_vars = chunk_config_string(&config);
|
||||
|
||||
// Should have at least 2 chunks
|
||||
assert!(env_vars.len() >= 2);
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_1"));
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_2"));
|
||||
|
||||
// Reconstruct and verify
|
||||
let mut reconstructed = String::new();
|
||||
let mut i = 1;
|
||||
while let Some(chunk) = env_vars.get(&format!("CAMOU_CONFIG_{}", i)) {
|
||||
reconstructed.push_str(chunk);
|
||||
i += 1;
|
||||
}
|
||||
assert_eq!(reconstructed, config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_ua_os_windows() {
|
||||
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0";
|
||||
assert_eq!(determine_ua_os(ua), "win");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_ua_os_macos() {
|
||||
let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0";
|
||||
assert_eq!(determine_ua_os(ua), "mac");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_ua_os_linux() {
|
||||
let ua = "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0";
|
||||
assert_eq!(determine_ua_os(ua), "lin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_to_env_vars() {
|
||||
let mut config = HashMap::new();
|
||||
config.insert(
|
||||
"navigator.userAgent".to_string(),
|
||||
serde_json::json!("Mozilla/5.0 Firefox/135.0"),
|
||||
);
|
||||
config.insert("screen.width".to_string(), serde_json::json!(1920));
|
||||
|
||||
let env_vars = config_to_env_vars(&config).unwrap();
|
||||
assert!(!env_vars.is_empty());
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_1"));
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
//! Bayesian network for fingerprint generation.
|
||||
//!
|
||||
//! Loads pre-trained probability distributions from ZIP files and samples fingerprints.
|
||||
|
||||
use super::bayesian_node::{BayesianNode, NodeDefinition};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, Read};
|
||||
use zip::ZipArchive;
|
||||
|
||||
/// Network definition structure from the ZIP file.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NetworkDefinition {
|
||||
pub nodes: Vec<NodeDefinition>,
|
||||
}
|
||||
|
||||
/// A Bayesian network for generating consistent fingerprints.
|
||||
pub struct BayesianNetwork {
|
||||
nodes_in_sampling_order: Vec<BayesianNode>,
|
||||
nodes_by_name: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl BayesianNetwork {
|
||||
/// Load a Bayesian network from embedded ZIP file bytes.
|
||||
pub fn from_zip_bytes(zip_bytes: &[u8]) -> Result<Self, BayesianNetworkError> {
|
||||
let cursor = Cursor::new(zip_bytes);
|
||||
let mut archive = ZipArchive::new(cursor)?;
|
||||
|
||||
// Find and read the JSON file from the ZIP
|
||||
let mut json_content = String::new();
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
if file.name().ends_with(".json") {
|
||||
file.read_to_string(&mut json_content)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if json_content.is_empty() {
|
||||
return Err(BayesianNetworkError::NoJsonInZip);
|
||||
}
|
||||
|
||||
let definition: NetworkDefinition = serde_json::from_str(&json_content)?;
|
||||
|
||||
let mut nodes_in_sampling_order = Vec::with_capacity(definition.nodes.len());
|
||||
let mut nodes_by_name = HashMap::with_capacity(definition.nodes.len());
|
||||
|
||||
for (i, node_def) in definition.nodes.into_iter().enumerate() {
|
||||
nodes_by_name.insert(node_def.name.clone(), i);
|
||||
nodes_in_sampling_order.push(BayesianNode::new(node_def));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
nodes_in_sampling_order,
|
||||
nodes_by_name,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a node by name.
|
||||
pub fn get_node(&self, name: &str) -> Option<&BayesianNode> {
|
||||
self
|
||||
.nodes_by_name
|
||||
.get(name)
|
||||
.map(|&i| &self.nodes_in_sampling_order[i])
|
||||
}
|
||||
|
||||
/// Get possible values for a node.
|
||||
pub fn get_possible_values(&self, name: &str) -> Option<Vec<String>> {
|
||||
self
|
||||
.get_node(name)
|
||||
.map(|node| node.possible_values().to_vec())
|
||||
}
|
||||
|
||||
/// Generate a random sample from the network.
|
||||
///
|
||||
/// `input_values` contains already known node values that should not be overwritten.
|
||||
pub fn generate_sample(&self, input_values: &HashMap<String, String>) -> HashMap<String, String> {
|
||||
let mut sample = input_values.clone();
|
||||
|
||||
for node in &self.nodes_in_sampling_order {
|
||||
if !sample.contains_key(node.name()) {
|
||||
let value = node.sample(&sample);
|
||||
sample.insert(node.name().to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
sample
|
||||
}
|
||||
|
||||
/// Generate a random sample consistent with the given value restrictions.
|
||||
///
|
||||
/// Uses backtracking to find a valid configuration.
|
||||
/// Returns `None` if no consistent sample can be generated.
|
||||
pub fn generate_consistent_sample_when_possible(
|
||||
&self,
|
||||
value_possibilities: &HashMap<String, Vec<String>>,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
self.recursively_generate_consistent_sample(HashMap::new(), value_possibilities, 0)
|
||||
}
|
||||
|
||||
fn recursively_generate_consistent_sample(
|
||||
&self,
|
||||
sample_so_far: HashMap<String, String>,
|
||||
value_possibilities: &HashMap<String, Vec<String>>,
|
||||
depth: usize,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
if depth >= self.nodes_in_sampling_order.len() {
|
||||
return Some(sample_so_far);
|
||||
}
|
||||
|
||||
let node = &self.nodes_in_sampling_order[depth];
|
||||
let mut banned_values: Vec<String> = Vec::new();
|
||||
let mut sample_so_far = sample_so_far;
|
||||
|
||||
loop {
|
||||
let sample_value = node.sample_according_to_restrictions(
|
||||
&sample_so_far,
|
||||
value_possibilities.get(node.name()).map(|v| v.as_slice()),
|
||||
&banned_values,
|
||||
);
|
||||
|
||||
let Some(value) = sample_value else {
|
||||
break;
|
||||
};
|
||||
|
||||
sample_so_far.insert(node.name().to_string(), value.clone());
|
||||
|
||||
if let Some(complete_sample) = self.recursively_generate_consistent_sample(
|
||||
sample_so_far.clone(),
|
||||
value_possibilities,
|
||||
depth + 1,
|
||||
) {
|
||||
return Some(complete_sample);
|
||||
}
|
||||
|
||||
banned_values.push(value);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur when working with Bayesian networks.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BayesianNetworkError {
|
||||
#[error("ZIP file error: {0}")]
|
||||
Zip(#[from] zip::result::ZipError),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("No JSON file found in ZIP archive")]
|
||||
NoJsonInZip,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_load_input_network() {
|
||||
let zip_bytes = include_bytes!("../data/input-network-definition.zip");
|
||||
let network = BayesianNetwork::from_zip_bytes(zip_bytes);
|
||||
assert!(
|
||||
network.is_ok(),
|
||||
"Failed to load input network: {:?}",
|
||||
network.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_sample_from_input_network() {
|
||||
let zip_bytes = include_bytes!("../data/input-network-definition.zip");
|
||||
let network = BayesianNetwork::from_zip_bytes(zip_bytes).unwrap();
|
||||
|
||||
let sample = network.generate_sample(&HashMap::new());
|
||||
assert!(!sample.is_empty(), "Sample should not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_consistent_sample() {
|
||||
let zip_bytes = include_bytes!("../data/input-network-definition.zip");
|
||||
let network = BayesianNetwork::from_zip_bytes(zip_bytes).unwrap();
|
||||
|
||||
let mut constraints = HashMap::new();
|
||||
constraints.insert("*OPERATING_SYSTEM".to_string(), vec!["windows".to_string()]);
|
||||
|
||||
let sample = network.generate_consistent_sample_when_possible(&constraints);
|
||||
assert!(sample.is_some(), "Should generate a consistent sample");
|
||||
|
||||
if let Some(s) = sample {
|
||||
assert_eq!(s.get("*OPERATING_SYSTEM"), Some(&"windows".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
//! Bayesian network node implementation for fingerprint generation.
|
||||
//!
|
||||
//! Implements weighted random sampling from conditional probability distributions.
|
||||
|
||||
use rand::RngExt;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Node definition from the network JSON file.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NodeDefinition {
|
||||
pub name: String,
|
||||
pub parent_names: Vec<String>,
|
||||
pub possible_values: Vec<String>,
|
||||
pub conditional_probabilities: ConditionalProbabilities,
|
||||
}
|
||||
|
||||
/// Conditional probability structure - can be nested or terminal.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ConditionalProbabilities {
|
||||
#[serde(default)]
|
||||
pub deeper: Option<HashMap<String, ConditionalProbabilities>>,
|
||||
#[serde(default)]
|
||||
pub skip: Option<Box<ConditionalProbabilities>>,
|
||||
#[serde(flatten)]
|
||||
pub probabilities: HashMap<String, f64>,
|
||||
}
|
||||
|
||||
impl ConditionalProbabilities {
|
||||
/// Check if this is a terminal node (has actual probabilities, not deeper nesting)
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
self.deeper.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// A single node in the Bayesian network.
|
||||
pub struct BayesianNode {
|
||||
definition: NodeDefinition,
|
||||
}
|
||||
|
||||
impl BayesianNode {
|
||||
pub fn new(definition: NodeDefinition) -> Self {
|
||||
Self { definition }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.definition.name
|
||||
}
|
||||
|
||||
pub fn parent_names(&self) -> &[String] {
|
||||
&self.definition.parent_names
|
||||
}
|
||||
|
||||
pub fn possible_values(&self) -> &[String] {
|
||||
&self.definition.possible_values
|
||||
}
|
||||
|
||||
/// Get the probability distribution given parent node values.
|
||||
fn get_probabilities_given_known_values(
|
||||
&self,
|
||||
parent_values: &HashMap<String, String>,
|
||||
) -> HashMap<String, f64> {
|
||||
let mut probabilities = &self.definition.conditional_probabilities;
|
||||
|
||||
for parent_name in &self.definition.parent_names {
|
||||
if let Some(deeper) = &probabilities.deeper {
|
||||
if let Some(parent_value) = parent_values.get(parent_name) {
|
||||
if let Some(next_level) = deeper.get(parent_value) {
|
||||
probabilities = next_level;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Use skip if parent value not found in deeper
|
||||
if let Some(skip) = &probabilities.skip {
|
||||
probabilities = skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
probabilities.probabilities.clone()
|
||||
}
|
||||
|
||||
/// Randomly sample from the given values using the given probabilities.
|
||||
fn sample_random_value_from_possibilities(
|
||||
possible_values: &[String],
|
||||
total_probability: f64,
|
||||
probabilities: &HashMap<String, f64>,
|
||||
) -> String {
|
||||
if possible_values.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut rng = rand::rng();
|
||||
let anchor = rng.random::<f64>() * total_probability;
|
||||
let mut cumulative = 0.0;
|
||||
|
||||
for value in possible_values {
|
||||
if let Some(&prob) = probabilities.get(value) {
|
||||
cumulative += prob;
|
||||
if cumulative > anchor {
|
||||
return value.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
possible_values.first().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Sample a value from the conditional distribution given parent values.
|
||||
pub fn sample(&self, parent_values: &HashMap<String, String>) -> String {
|
||||
let probabilities = self.get_probabilities_given_known_values(parent_values);
|
||||
let possible_values: Vec<String> = probabilities.keys().cloned().collect();
|
||||
|
||||
Self::sample_random_value_from_possibilities(&possible_values, 1.0, &probabilities)
|
||||
}
|
||||
|
||||
/// Sample according to restrictions on possible values.
|
||||
///
|
||||
/// Returns `None` if no valid value can be sampled.
|
||||
pub fn sample_according_to_restrictions(
|
||||
&self,
|
||||
parent_values: &HashMap<String, String>,
|
||||
value_possibilities: Option<&[String]>,
|
||||
banned_values: &[String],
|
||||
) -> Option<String> {
|
||||
let probabilities = self.get_probabilities_given_known_values(parent_values);
|
||||
let values_in_distribution: Vec<String> = probabilities.keys().cloned().collect();
|
||||
|
||||
let possible_values = value_possibilities.unwrap_or(&values_in_distribution);
|
||||
|
||||
let mut valid_values = Vec::new();
|
||||
let mut total_probability = 0.0;
|
||||
|
||||
for value in possible_values {
|
||||
if !banned_values.contains(value) && values_in_distribution.contains(value) {
|
||||
if let Some(&prob) = probabilities.get(value) {
|
||||
valid_values.push(value.clone());
|
||||
total_probability += prob;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if valid_values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self::sample_random_value_from_possibilities(
|
||||
&valid_values,
|
||||
total_probability,
|
||||
&probabilities,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_node() -> BayesianNode {
|
||||
let mut probs = HashMap::new();
|
||||
probs.insert("1920".to_string(), 0.5);
|
||||
probs.insert("1366".to_string(), 0.3);
|
||||
probs.insert("1536".to_string(), 0.2);
|
||||
|
||||
let definition = NodeDefinition {
|
||||
name: "screen.width".to_string(),
|
||||
parent_names: vec![],
|
||||
possible_values: vec!["1920".to_string(), "1366".to_string(), "1536".to_string()],
|
||||
conditional_probabilities: ConditionalProbabilities {
|
||||
deeper: None,
|
||||
skip: None,
|
||||
probabilities: probs,
|
||||
},
|
||||
};
|
||||
|
||||
BayesianNode::new(definition)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_returns_valid_value() {
|
||||
let node = create_test_node();
|
||||
let parent_values = HashMap::new();
|
||||
|
||||
for _ in 0..100 {
|
||||
let value = node.sample(&parent_values);
|
||||
assert!(
|
||||
node.possible_values().contains(&value),
|
||||
"Sampled value '{}' not in possible values",
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_with_restrictions() {
|
||||
let node = create_test_node();
|
||||
let parent_values = HashMap::new();
|
||||
|
||||
let allowed = vec!["1920".to_string()];
|
||||
let banned = vec![];
|
||||
|
||||
let value = node.sample_according_to_restrictions(&parent_values, Some(&allowed), &banned);
|
||||
|
||||
assert_eq!(value, Some("1920".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_with_banned_values() {
|
||||
let node = create_test_node();
|
||||
let parent_values = HashMap::new();
|
||||
|
||||
let banned = vec!["1920".to_string(), "1366".to_string()];
|
||||
|
||||
for _ in 0..100 {
|
||||
let value = node.sample_according_to_restrictions(&parent_values, None, &banned);
|
||||
assert_eq!(value, Some("1536".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_returns_none_when_all_banned() {
|
||||
let node = create_test_node();
|
||||
let parent_values = HashMap::new();
|
||||
|
||||
let banned = vec!["1920".to_string(), "1366".to_string(), "1536".to_string()];
|
||||
|
||||
let value = node.sample_according_to_restrictions(&parent_values, None, &banned);
|
||||
assert!(value.is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
//! Fingerprint generation module.
|
||||
//!
|
||||
//! Generates realistic browser fingerprints using Bayesian networks trained on real browser data.
|
||||
|
||||
pub mod bayesian_network;
|
||||
pub mod bayesian_node;
|
||||
pub mod types;
|
||||
|
||||
use bayesian_network::{BayesianNetwork, BayesianNetworkError};
|
||||
use std::collections::HashMap;
|
||||
use types::*;
|
||||
|
||||
use crate::camoufox::data;
|
||||
|
||||
/// Fingerprint generator using Bayesian networks.
|
||||
pub struct FingerprintGenerator {
|
||||
fingerprint_network: BayesianNetwork,
|
||||
input_network: BayesianNetwork,
|
||||
header_network: BayesianNetwork,
|
||||
browser_helper: Vec<BrowserHttpInfo>,
|
||||
headers_order: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Parsed browser/HTTP version info.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrowserHttpInfo {
|
||||
pub name: String,
|
||||
pub version: Vec<u32>,
|
||||
pub http_version: String,
|
||||
pub complete_string: String,
|
||||
}
|
||||
|
||||
impl BrowserHttpInfo {
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
if s == MISSING_VALUE_DATASET_TOKEN {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = s.split('|').collect();
|
||||
if parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let browser_string = parts[0];
|
||||
let http_version = parts[1].to_string();
|
||||
|
||||
let browser_parts: Vec<&str> = browser_string.split('/').collect();
|
||||
if browser_parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = browser_parts[0].to_string();
|
||||
let version: Vec<u32> = browser_parts[1]
|
||||
.split('.')
|
||||
.filter_map(|v| v.parse().ok())
|
||||
.collect();
|
||||
|
||||
Some(Self {
|
||||
name,
|
||||
version,
|
||||
http_version,
|
||||
complete_string: s.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn major_version(&self) -> u32 {
|
||||
self.version.first().copied().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for fingerprint generation.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FingerprintError {
|
||||
#[error("Bayesian network error: {0}")]
|
||||
Network(#[from] BayesianNetworkError),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("Failed to generate consistent fingerprint after {0} attempts")]
|
||||
GenerationFailed(u32),
|
||||
|
||||
#[error("No valid fingerprint generated")]
|
||||
NoValidFingerprint,
|
||||
}
|
||||
|
||||
impl FingerprintGenerator {
|
||||
/// Create a new fingerprint generator.
|
||||
pub fn new() -> Result<Self, FingerprintError> {
|
||||
let fingerprint_network = BayesianNetwork::from_zip_bytes(data::FINGERPRINT_NETWORK_ZIP)?;
|
||||
let input_network = BayesianNetwork::from_zip_bytes(data::INPUT_NETWORK_ZIP)?;
|
||||
let header_network = BayesianNetwork::from_zip_bytes(data::HEADER_NETWORK_ZIP)?;
|
||||
|
||||
let browser_strings: Vec<String> = serde_json::from_str(data::BROWSER_HELPER_JSON)?;
|
||||
let browser_helper: Vec<BrowserHttpInfo> = browser_strings
|
||||
.iter()
|
||||
.filter_map(|s| BrowserHttpInfo::parse(s))
|
||||
.collect();
|
||||
|
||||
let headers_order: HashMap<String, Vec<String>> =
|
||||
serde_json::from_str(data::HEADERS_ORDER_JSON)?;
|
||||
|
||||
Ok(Self {
|
||||
fingerprint_network,
|
||||
input_network,
|
||||
header_network,
|
||||
browser_helper,
|
||||
headers_order,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a fingerprint with matching headers.
|
||||
pub fn get_fingerprint(
|
||||
&self,
|
||||
options: &FingerprintOptions,
|
||||
) -> Result<FingerprintWithHeaders, FingerprintError> {
|
||||
const MAX_RETRIES: u32 = 10;
|
||||
|
||||
// Build constraints from options
|
||||
let mut value_possibilities = self.build_constraints(options);
|
||||
|
||||
// Handle screen constraints
|
||||
let screen_values = if let Some(screen_constraints) = &options.screen {
|
||||
self.filter_screen_values(screen_constraints)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(sv) = screen_values {
|
||||
value_possibilities.insert("screen".to_string(), sv);
|
||||
}
|
||||
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
// Generate input sample consistent with constraints
|
||||
let input_sample = self
|
||||
.input_network
|
||||
.generate_consistent_sample_when_possible(&value_possibilities);
|
||||
|
||||
let Some(input_sample) = input_sample else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Generate header sample from input
|
||||
let header_sample = self.header_network.generate_sample(&input_sample);
|
||||
|
||||
// Extract user agent
|
||||
let user_agent = header_sample
|
||||
.get("user-agent")
|
||||
.or_else(|| header_sample.get("User-Agent"))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build fingerprint constraints with the generated user agent
|
||||
let mut fp_constraints = value_possibilities.clone();
|
||||
fp_constraints.insert("userAgent".to_string(), vec![user_agent.clone()]);
|
||||
|
||||
// Generate fingerprint sample
|
||||
let fingerprint_sample = self
|
||||
.fingerprint_network
|
||||
.generate_consistent_sample_when_possible(&fp_constraints);
|
||||
|
||||
let Some(fp_sample) = fingerprint_sample else {
|
||||
log::debug!(
|
||||
"Failed to generate fingerprint on attempt {}, retrying",
|
||||
attempt + 1
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Transform the sample to a Fingerprint struct
|
||||
match self.transform_sample(&fp_sample, &header_sample, options) {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) => {
|
||||
log::debug!(
|
||||
"Failed to transform fingerprint on attempt {}: {}",
|
||||
attempt + 1,
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(FingerprintError::GenerationFailed(MAX_RETRIES))
|
||||
}
|
||||
|
||||
/// Build constraint map from options.
|
||||
fn build_constraints(&self, options: &FingerprintOptions) -> HashMap<String, Vec<String>> {
|
||||
let mut constraints = HashMap::new();
|
||||
|
||||
// Operating system constraint
|
||||
if let Some(os) = &options.operating_system {
|
||||
constraints.insert(OPERATING_SYSTEM_NODE_NAME.to_string(), vec![os.clone()]);
|
||||
}
|
||||
|
||||
// Device constraint (default to desktop)
|
||||
let devices = options
|
||||
.devices
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec!["desktop".to_string()]);
|
||||
constraints.insert(DEVICE_NODE_NAME.to_string(), devices);
|
||||
|
||||
// Browser constraint
|
||||
let browsers = options
|
||||
.browsers
|
||||
.clone()
|
||||
.unwrap_or_else(|| SUPPORTED_BROWSERS.iter().map(|s| s.to_string()).collect());
|
||||
|
||||
let http_version = options
|
||||
.http_version
|
||||
.clone()
|
||||
.unwrap_or_else(|| "2".to_string());
|
||||
|
||||
// Filter browser helper entries by browser names and HTTP version
|
||||
let browser_http_values: Vec<String> = self
|
||||
.browser_helper
|
||||
.iter()
|
||||
.filter(|bh| browsers.contains(&bh.name) && bh.http_version == http_version)
|
||||
.map(|bh| bh.complete_string.clone())
|
||||
.collect();
|
||||
|
||||
if !browser_http_values.is_empty() {
|
||||
constraints.insert(BROWSER_HTTP_NODE_NAME.to_string(), browser_http_values);
|
||||
}
|
||||
|
||||
constraints
|
||||
}
|
||||
|
||||
/// Filter screen values based on constraints.
|
||||
fn filter_screen_values(&self, constraints: &ScreenConstraints) -> Option<Vec<String>> {
|
||||
let possible_values = self.fingerprint_network.get_possible_values("screen")?;
|
||||
|
||||
let filtered: Vec<String> = possible_values
|
||||
.into_iter()
|
||||
.filter(|screen_str| {
|
||||
// Screen values are stored as "*STRINGIFIED*{...json...}"
|
||||
if let Some(json_str) = screen_str.strip_prefix(STRINGIFIED_PREFIX) {
|
||||
if let Ok(screen) = serde_json::from_str::<serde_json::Value>(json_str) {
|
||||
let width = screen["width"].as_u64().unwrap_or(0) as u32;
|
||||
let height = screen["height"].as_u64().unwrap_or(0) as u32;
|
||||
return constraints.matches(width, height);
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
if filtered.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform raw sample data into a Fingerprint struct.
|
||||
fn transform_sample(
|
||||
&self,
|
||||
fp_sample: &HashMap<String, String>,
|
||||
header_sample: &HashMap<String, String>,
|
||||
options: &FingerprintOptions,
|
||||
) -> Result<FingerprintWithHeaders, FingerprintError> {
|
||||
// Parse values, handling STRINGIFIED prefix and MISSING_VALUE token
|
||||
let mut parsed: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
|
||||
for (key, value) in fp_sample {
|
||||
if value == MISSING_VALUE_DATASET_TOKEN {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsed_value = if let Some(json_str) = value.strip_prefix(STRINGIFIED_PREFIX) {
|
||||
serde_json::from_str(json_str)?
|
||||
} else {
|
||||
serde_json::Value::String(value.clone())
|
||||
};
|
||||
|
||||
parsed.insert(key.clone(), parsed_value);
|
||||
}
|
||||
|
||||
// Check if screen was generated
|
||||
let screen_value = parsed.get("screen");
|
||||
if screen_value.is_none() {
|
||||
return Err(FingerprintError::NoValidFingerprint);
|
||||
}
|
||||
|
||||
// Extract screen fingerprint
|
||||
let screen = if let Some(screen_val) = screen_value {
|
||||
serde_json::from_value(screen_val.clone()).unwrap_or_default()
|
||||
} else {
|
||||
ScreenFingerprint::default()
|
||||
};
|
||||
|
||||
// Build languages from Accept-Language header
|
||||
let accept_language = header_sample
|
||||
.get("accept-language")
|
||||
.or_else(|| header_sample.get("Accept-Language"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "en-US".to_string());
|
||||
|
||||
let languages: Vec<String> = accept_language
|
||||
.split(',')
|
||||
.map(|s| s.split(';').next().unwrap_or(s).trim().to_string())
|
||||
.collect();
|
||||
|
||||
let language = languages
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "en-US".to_string());
|
||||
|
||||
// Build navigator fingerprint
|
||||
let navigator = NavigatorFingerprint {
|
||||
user_agent: get_string(&parsed, "userAgent"),
|
||||
user_agent_data: parsed
|
||||
.get("userAgentData")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok()),
|
||||
do_not_track: parsed
|
||||
.get("doNotTrack")
|
||||
.and_then(|v| v.as_str().map(String::from)),
|
||||
app_code_name: get_string_or(&parsed, "appCodeName", "Mozilla"),
|
||||
app_name: get_string_or(&parsed, "appName", "Netscape"),
|
||||
app_version: get_string(&parsed, "appVersion"),
|
||||
oscpu: parsed
|
||||
.get("oscpu")
|
||||
.and_then(|v| v.as_str().map(String::from)),
|
||||
webdriver: parsed
|
||||
.get("webdriver")
|
||||
.and_then(|v| v.as_str().map(String::from)),
|
||||
language,
|
||||
languages,
|
||||
platform: get_string(&parsed, "platform"),
|
||||
device_memory: parsed
|
||||
.get("deviceMemory")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
hardware_concurrency: parsed
|
||||
.get("hardwareConcurrency")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(4),
|
||||
product: get_string_or(&parsed, "product", "Gecko"),
|
||||
product_sub: get_string(&parsed, "productSub"),
|
||||
vendor: get_string(&parsed, "vendor"),
|
||||
vendor_sub: get_string(&parsed, "vendorSub"),
|
||||
max_touch_points: parsed
|
||||
.get("maxTouchPoints")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0),
|
||||
extra_properties: parsed
|
||||
.get("extraProperties")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok()),
|
||||
};
|
||||
|
||||
// Build video card (will be filled later by WebGL sampler)
|
||||
let video_card = parsed
|
||||
.get("videoCard")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build other components
|
||||
let audio_codecs = parsed
|
||||
.get("audioCodecs")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let video_codecs = parsed
|
||||
.get("videoCodecs")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let plugins_data = parsed
|
||||
.get("pluginsData")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let battery = parsed
|
||||
.get("battery")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok());
|
||||
|
||||
let multimedia_devices = parsed
|
||||
.get("multimediaDevices")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let fonts = parsed
|
||||
.get("fonts")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let fingerprint = Fingerprint {
|
||||
screen,
|
||||
navigator,
|
||||
video_codecs,
|
||||
audio_codecs,
|
||||
plugins_data,
|
||||
battery,
|
||||
video_card,
|
||||
multimedia_devices,
|
||||
fonts,
|
||||
mock_web_rtc: options.mock_web_rtc,
|
||||
slim: options.slim,
|
||||
};
|
||||
|
||||
// Build headers (filter out internal nodes and missing values)
|
||||
let headers: Headers = header_sample
|
||||
.iter()
|
||||
.filter(|(k, v)| !k.starts_with('*') && *v != MISSING_VALUE_DATASET_TOKEN)
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
|
||||
// Order headers
|
||||
let ordered_headers = self.order_headers(&headers, &fingerprint.navigator.user_agent);
|
||||
|
||||
Ok(FingerprintWithHeaders {
|
||||
fingerprint,
|
||||
headers: ordered_headers,
|
||||
})
|
||||
}
|
||||
|
||||
/// Order headers according to browser-specific ordering.
|
||||
fn order_headers(&self, headers: &Headers, user_agent: &str) -> Headers {
|
||||
let browser = detect_browser_from_ua(user_agent);
|
||||
let order = self.headers_order.get(browser).cloned().unwrap_or_default();
|
||||
|
||||
let mut ordered = HashMap::new();
|
||||
|
||||
// Add headers in order
|
||||
for header_name in &order {
|
||||
if let Some(value) = headers.get(header_name) {
|
||||
ordered.insert(header_name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining headers not in order
|
||||
for (key, value) in headers {
|
||||
if !order.contains(key) {
|
||||
ordered.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
ordered
|
||||
}
|
||||
}
|
||||
|
||||
fn get_string(map: &HashMap<String, serde_json::Value>, key: &str) -> String {
|
||||
map
|
||||
.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_string_or(map: &HashMap<String, serde_json::Value>, key: &str, default: &str) -> String {
|
||||
map
|
||||
.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
fn detect_browser_from_ua(user_agent: &str) -> &str {
|
||||
let ua_lower = user_agent.to_lowercase();
|
||||
if ua_lower.contains("firefox") {
|
||||
"firefox"
|
||||
} else if ua_lower.contains("edg/") || ua_lower.contains("edge") {
|
||||
"edge"
|
||||
} else if ua_lower.contains("chrome") {
|
||||
"chrome"
|
||||
} else if ua_lower.contains("safari") {
|
||||
"safari"
|
||||
} else {
|
||||
"chrome"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_generator() {
|
||||
let generator = FingerprintGenerator::new();
|
||||
assert!(
|
||||
generator.is_ok(),
|
||||
"Failed to create generator: {:?}",
|
||||
generator.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_fingerprint() {
|
||||
let generator = FingerprintGenerator::new().unwrap();
|
||||
let options = FingerprintOptions::default();
|
||||
|
||||
let result = generator.get_fingerprint(&options);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate fingerprint: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
if let Ok(fp) = result {
|
||||
assert!(!fp.fingerprint.navigator.user_agent.is_empty());
|
||||
assert!(fp.fingerprint.screen.width > 0);
|
||||
assert!(fp.fingerprint.screen.height > 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_firefox_fingerprint() {
|
||||
let generator = FingerprintGenerator::new().unwrap();
|
||||
let options = FingerprintOptions {
|
||||
browsers: Some(vec!["firefox".to_string()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = generator.get_fingerprint(&options);
|
||||
assert!(result.is_ok(), "Failed to generate Firefox fingerprint");
|
||||
|
||||
if let Ok(fp) = result {
|
||||
assert!(
|
||||
fp.fingerprint
|
||||
.navigator
|
||||
.user_agent
|
||||
.to_lowercase()
|
||||
.contains("firefox"),
|
||||
"User agent should contain Firefox: {}",
|
||||
fp.fingerprint.navigator.user_agent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_with_screen_constraints() {
|
||||
let generator = FingerprintGenerator::new().unwrap();
|
||||
let options = FingerprintOptions {
|
||||
screen: Some(ScreenConstraints {
|
||||
min_width: Some(1900),
|
||||
max_width: Some(1920),
|
||||
min_height: Some(1000),
|
||||
max_height: Some(1100),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = generator.get_fingerprint(&options);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate fingerprint with screen constraints"
|
||||
);
|
||||
|
||||
if let Ok(fp) = result {
|
||||
assert!(
|
||||
fp.fingerprint.screen.width >= 1900 && fp.fingerprint.screen.width <= 1920,
|
||||
"Screen width {} should be between 1900 and 1920",
|
||||
fp.fingerprint.screen.width
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_browser_http_info_parse() {
|
||||
let info = BrowserHttpInfo::parse("chrome/143.0.0.0|2");
|
||||
assert!(info.is_some());
|
||||
let info = info.unwrap();
|
||||
assert_eq!(info.name, "chrome");
|
||||
assert_eq!(info.major_version(), 143);
|
||||
assert_eq!(info.http_version, "2");
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
//! Fingerprint type definitions.
|
||||
//!
|
||||
//! These types represent browser fingerprints that can be injected into Camoufox.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A complete browser fingerprint.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Fingerprint {
|
||||
pub screen: ScreenFingerprint,
|
||||
pub navigator: NavigatorFingerprint,
|
||||
#[serde(default)]
|
||||
pub video_codecs: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub audio_codecs: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub plugins_data: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub battery: Option<BatteryFingerprint>,
|
||||
pub video_card: VideoCard,
|
||||
#[serde(default)]
|
||||
pub multimedia_devices: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub fonts: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub mock_web_rtc: bool,
|
||||
#[serde(default)]
|
||||
pub slim: bool,
|
||||
}
|
||||
|
||||
/// Screen-related fingerprint properties.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScreenFingerprint {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub avail_width: u32,
|
||||
pub avail_height: u32,
|
||||
#[serde(default)]
|
||||
pub avail_top: u32,
|
||||
#[serde(default)]
|
||||
pub avail_left: u32,
|
||||
pub color_depth: u32,
|
||||
pub pixel_depth: u32,
|
||||
#[serde(default = "default_device_pixel_ratio")]
|
||||
pub device_pixel_ratio: f64,
|
||||
#[serde(default)]
|
||||
pub page_x_offset: f64,
|
||||
#[serde(default)]
|
||||
pub page_y_offset: f64,
|
||||
pub inner_width: u32,
|
||||
pub inner_height: u32,
|
||||
pub outer_width: u32,
|
||||
pub outer_height: u32,
|
||||
#[serde(default)]
|
||||
pub screen_x: i32,
|
||||
#[serde(default)]
|
||||
pub screen_y: i32,
|
||||
#[serde(default)]
|
||||
pub client_width: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub client_height: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub has_hdr: bool,
|
||||
}
|
||||
|
||||
fn default_device_pixel_ratio() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Brand information for User-Agent Client Hints.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Brand {
|
||||
pub brand: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// User-Agent Client Hints data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserAgentData {
|
||||
#[serde(default)]
|
||||
pub brands: Vec<Brand>,
|
||||
#[serde(default)]
|
||||
pub mobile: bool,
|
||||
#[serde(default)]
|
||||
pub platform: String,
|
||||
#[serde(default)]
|
||||
pub architecture: String,
|
||||
#[serde(default)]
|
||||
pub bitness: String,
|
||||
#[serde(default)]
|
||||
pub full_version_list: Vec<Brand>,
|
||||
#[serde(default)]
|
||||
pub model: String,
|
||||
#[serde(default)]
|
||||
pub platform_version: String,
|
||||
#[serde(default)]
|
||||
pub ua_full_version: String,
|
||||
}
|
||||
|
||||
/// Extra navigator properties.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExtraProperties {
|
||||
#[serde(default)]
|
||||
pub vendor_flavors: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub is_bluetooth_supported: bool,
|
||||
#[serde(default)]
|
||||
pub global_privacy_control: Option<bool>,
|
||||
#[serde(default = "default_pdf_viewer_enabled")]
|
||||
pub pdf_viewer_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub installed_apps: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn default_pdf_viewer_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Navigator-related fingerprint properties.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NavigatorFingerprint {
|
||||
pub user_agent: String,
|
||||
#[serde(default)]
|
||||
pub user_agent_data: Option<UserAgentData>,
|
||||
#[serde(default)]
|
||||
pub do_not_track: Option<String>,
|
||||
#[serde(default = "default_app_code_name")]
|
||||
pub app_code_name: String,
|
||||
#[serde(default = "default_app_name")]
|
||||
pub app_name: String,
|
||||
#[serde(default)]
|
||||
pub app_version: String,
|
||||
#[serde(default)]
|
||||
pub oscpu: Option<String>,
|
||||
#[serde(default)]
|
||||
pub webdriver: Option<String>,
|
||||
pub language: String,
|
||||
pub languages: Vec<String>,
|
||||
pub platform: String,
|
||||
#[serde(default)]
|
||||
pub device_memory: Option<u32>,
|
||||
pub hardware_concurrency: u32,
|
||||
#[serde(default = "default_product")]
|
||||
pub product: String,
|
||||
#[serde(default)]
|
||||
pub product_sub: String,
|
||||
#[serde(default)]
|
||||
pub vendor: String,
|
||||
#[serde(default)]
|
||||
pub vendor_sub: String,
|
||||
#[serde(default)]
|
||||
pub max_touch_points: u32,
|
||||
#[serde(default)]
|
||||
pub extra_properties: Option<ExtraProperties>,
|
||||
}
|
||||
|
||||
fn default_app_code_name() -> String {
|
||||
"Mozilla".to_string()
|
||||
}
|
||||
|
||||
fn default_app_name() -> String {
|
||||
"Netscape".to_string()
|
||||
}
|
||||
|
||||
fn default_product() -> String {
|
||||
"Gecko".to_string()
|
||||
}
|
||||
|
||||
/// WebGL video card information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct VideoCard {
|
||||
pub vendor: String,
|
||||
pub renderer: String,
|
||||
}
|
||||
|
||||
/// Battery status fingerprint.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BatteryFingerprint {
|
||||
pub charging: bool,
|
||||
pub charging_time: f64,
|
||||
pub discharging_time: f64,
|
||||
pub level: f64,
|
||||
}
|
||||
|
||||
/// HTTP headers for a fingerprint.
|
||||
pub type Headers = HashMap<String, String>;
|
||||
|
||||
/// A fingerprint combined with matching HTTP headers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FingerprintWithHeaders {
|
||||
pub fingerprint: Fingerprint,
|
||||
pub headers: Headers,
|
||||
}
|
||||
|
||||
/// Options for generating fingerprints.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FingerprintOptions {
|
||||
/// Target operating system: "windows", "macos", "linux"
|
||||
pub operating_system: Option<String>,
|
||||
/// Target browser: "firefox", "chrome", "safari", "edge"
|
||||
pub browsers: Option<Vec<String>>,
|
||||
/// Target device type: "desktop", "mobile"
|
||||
pub devices: Option<Vec<String>>,
|
||||
/// Locales for Accept-Language header
|
||||
pub locales: Option<Vec<String>>,
|
||||
/// HTTP version: "1" or "2"
|
||||
pub http_version: Option<String>,
|
||||
/// Screen dimension constraints
|
||||
pub screen: Option<ScreenConstraints>,
|
||||
/// Whether to mock WebRTC
|
||||
pub mock_web_rtc: bool,
|
||||
/// Slim mode (fewer evasions)
|
||||
pub slim: bool,
|
||||
}
|
||||
|
||||
/// Constraints for screen dimensions.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ScreenConstraints {
|
||||
pub min_width: Option<u32>,
|
||||
pub max_width: Option<u32>,
|
||||
pub min_height: Option<u32>,
|
||||
pub max_height: Option<u32>,
|
||||
}
|
||||
|
||||
impl ScreenConstraints {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_min_width(mut self, width: u32) -> Self {
|
||||
self.min_width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_width(mut self, width: u32) -> Self {
|
||||
self.max_width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_min_height(mut self, height: u32) -> Self {
|
||||
self.min_height = Some(height);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_height(mut self, height: u32) -> Self {
|
||||
self.max_height = Some(height);
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if a screen size matches these constraints.
|
||||
pub fn matches(&self, width: u32, height: u32) -> bool {
|
||||
if let Some(min_w) = self.min_width {
|
||||
if width < min_w {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(max_w) = self.max_width {
|
||||
if width > max_w {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(min_h) = self.min_height {
|
||||
if height < min_h {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(max_h) = self.max_height {
|
||||
if height > max_h {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants used in fingerprint generation.
|
||||
pub const MISSING_VALUE_DATASET_TOKEN: &str = "*MISSING_VALUE*";
|
||||
pub const STRINGIFIED_PREFIX: &str = "*STRINGIFIED*";
|
||||
|
||||
/// Special node names in the Bayesian networks.
|
||||
pub const BROWSER_HTTP_NODE_NAME: &str = "*BROWSER_HTTP";
|
||||
pub const OPERATING_SYSTEM_NODE_NAME: &str = "*OPERATING_SYSTEM";
|
||||
pub const DEVICE_NODE_NAME: &str = "*DEVICE";
|
||||
|
||||
/// Supported browsers.
|
||||
pub const SUPPORTED_BROWSERS: &[&str] = &["chrome", "firefox", "safari", "edge"];
|
||||
|
||||
/// Supported operating systems.
|
||||
pub const SUPPORTED_OPERATING_SYSTEMS: &[&str] = &["windows", "macos", "linux", "android", "ios"];
|
||||
|
||||
/// Supported devices.
|
||||
pub const SUPPORTED_DEVICES: &[&str] = &["desktop", "mobile"];
|
||||
|
||||
/// Supported HTTP versions.
|
||||
pub const SUPPORTED_HTTP_VERSIONS: &[&str] = &["1", "2"];
|
||||
@@ -1,83 +0,0 @@
|
||||
//! OS-specific font lists for Camoufox.
|
||||
//!
|
||||
//! Provides default system fonts for Windows, macOS, and Linux.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::camoufox::data;
|
||||
|
||||
/// Get fonts for the target OS.
|
||||
pub fn get_fonts_for_os(target_os: &str) -> Vec<String> {
|
||||
let fonts_map: HashMap<String, Vec<String>> =
|
||||
serde_json::from_str(data::FONTS_JSON).unwrap_or_default();
|
||||
|
||||
let os_key = match target_os {
|
||||
"win" | "windows" => "win",
|
||||
"mac" | "macos" => "mac",
|
||||
"lin" | "linux" => "lin",
|
||||
_ => "win", // Default to Windows fonts
|
||||
};
|
||||
|
||||
fonts_map.get(os_key).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get fonts for the target OS with additional custom fonts.
|
||||
pub fn get_fonts_with_custom(target_os: &str, custom_fonts: Option<&[String]>) -> Vec<String> {
|
||||
let mut fonts = get_fonts_for_os(target_os);
|
||||
|
||||
if let Some(custom) = custom_fonts {
|
||||
// Add custom fonts, avoiding duplicates
|
||||
for font in custom {
|
||||
if !fonts.contains(font) {
|
||||
fonts.push(font.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fonts
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_fonts_for_windows() {
|
||||
let fonts = get_fonts_for_os("win");
|
||||
assert!(!fonts.is_empty());
|
||||
assert!(fonts.contains(&"Arial".to_string()));
|
||||
assert!(fonts.contains(&"Calibri".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_fonts_for_macos() {
|
||||
let fonts = get_fonts_for_os("mac");
|
||||
assert!(!fonts.is_empty());
|
||||
assert!(fonts.contains(&"Helvetica".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_fonts_for_linux() {
|
||||
let fonts = get_fonts_for_os("lin");
|
||||
assert!(!fonts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_fonts_with_custom() {
|
||||
let custom = vec!["MyCustomFont".to_string()];
|
||||
let fonts = get_fonts_with_custom("win", Some(&custom));
|
||||
|
||||
assert!(fonts.contains(&"MyCustomFont".to_string()));
|
||||
assert!(fonts.contains(&"Arial".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fonts_no_duplicates() {
|
||||
let custom = vec!["Arial".to_string()]; // Arial already exists in Windows fonts
|
||||
let fonts = get_fonts_with_custom("win", Some(&custom));
|
||||
|
||||
// Count occurrences of Arial
|
||||
let arial_count = fonts.iter().filter(|f| *f == "Arial").count();
|
||||
assert_eq!(arial_count, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
//! Camoufox browser launcher using playwright-rust.
|
||||
//!
|
||||
//! Provides functionality to launch Camoufox browser instances with fingerprint injection.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use playwright::api::{Browser, BrowserContext, Playwright, ProxySettings};
|
||||
use playwright::Error as PlaywrightError;
|
||||
|
||||
use crate::camoufox::config::{CamoufoxConfigBuilder, CamoufoxLaunchConfig, ProxyConfig};
|
||||
use crate::camoufox::fingerprint::types::{Fingerprint, ScreenConstraints};
|
||||
|
||||
/// Camoufox launcher for creating browser instances.
|
||||
pub struct CamoufoxLauncher {
|
||||
playwright: Arc<Playwright>,
|
||||
executable_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Error type for launcher operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LauncherError {
|
||||
#[error("Playwright error: {0}")]
|
||||
Playwright(PlaywrightError),
|
||||
|
||||
#[error("Playwright Arc error: {0}")]
|
||||
PlaywrightArc(#[from] Arc<PlaywrightError>),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(#[from] crate::camoufox::config::ConfigError),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Camoufox executable not found at: {0}")]
|
||||
ExecutableNotFound(PathBuf),
|
||||
|
||||
#[error("Failed to generate environment variables: {0}")]
|
||||
EnvVars(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Options for launching Camoufox.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LaunchOptions {
|
||||
/// Operating system to spoof: "windows", "macos", "linux"
|
||||
pub os: Option<String>,
|
||||
/// Block all images
|
||||
pub block_images: bool,
|
||||
/// Block WebRTC entirely
|
||||
pub block_webrtc: bool,
|
||||
/// Block WebGL (not recommended unless necessary)
|
||||
pub block_webgl: bool,
|
||||
/// Screen dimension constraints
|
||||
pub screen: Option<ScreenConstraints>,
|
||||
/// Fixed window size [width, height]
|
||||
pub window: Option<(u32, u32)>,
|
||||
/// Custom fingerprint (if not provided, one will be generated)
|
||||
pub fingerprint: Option<Fingerprint>,
|
||||
/// Run in headless mode
|
||||
pub headless: bool,
|
||||
/// Custom fonts to load
|
||||
pub fonts: Option<Vec<String>>,
|
||||
/// Only use custom fonts (disable OS fonts)
|
||||
pub custom_fonts_only: bool,
|
||||
/// Firefox user preferences
|
||||
pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
|
||||
/// Proxy configuration
|
||||
pub proxy: Option<ProxyConfig>,
|
||||
/// Additional browser arguments
|
||||
pub args: Option<Vec<String>>,
|
||||
/// Additional environment variables
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// Profile/user data directory
|
||||
pub user_data_dir: Option<PathBuf>,
|
||||
/// Enable debug output
|
||||
pub debug: bool,
|
||||
}
|
||||
|
||||
impl CamoufoxLauncher {
|
||||
/// Create a new Camoufox launcher.
|
||||
pub async fn new(executable_path: impl AsRef<Path>) -> Result<Self, LauncherError> {
|
||||
let executable_path = executable_path.as_ref().to_path_buf();
|
||||
|
||||
if !executable_path.exists() {
|
||||
return Err(LauncherError::ExecutableNotFound(executable_path));
|
||||
}
|
||||
|
||||
let playwright = Playwright::initialize()
|
||||
.await
|
||||
.map_err(LauncherError::Playwright)?;
|
||||
|
||||
Ok(Self {
|
||||
playwright: Arc::new(playwright),
|
||||
executable_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Launch a new Camoufox browser instance.
|
||||
pub async fn launch(&self, options: LaunchOptions) -> Result<Browser, LauncherError> {
|
||||
let config = self.build_config(&options)?;
|
||||
|
||||
if options.debug {
|
||||
log::debug!("Camoufox config: {:?}", config.fingerprint_config);
|
||||
}
|
||||
|
||||
// Get environment variables
|
||||
let env_vars = config.get_env_vars()?;
|
||||
|
||||
// Build launch arguments
|
||||
let mut args = options.args.clone().unwrap_or_default();
|
||||
|
||||
// Add headless flag if needed
|
||||
if options.headless {
|
||||
args.push("--headless".to_string());
|
||||
}
|
||||
|
||||
// Merge environment variables
|
||||
let mut env = options.env.clone().unwrap_or_default();
|
||||
for (key, value) in env_vars {
|
||||
env.insert(key, value);
|
||||
}
|
||||
|
||||
// Handle fontconfig on Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
if let Some(fontconfig_path) =
|
||||
crate::camoufox::env_vars::get_fontconfig_env(&config.target_os, &self.executable_path)
|
||||
{
|
||||
env.insert("FONTCONFIG_PATH".to_string(), fontconfig_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Build Firefox user prefs
|
||||
let mut firefox_prefs = config.firefox_prefs.clone();
|
||||
if let Some(user_prefs) = options.firefox_user_prefs {
|
||||
for (key, value) in user_prefs {
|
||||
firefox_prefs.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the Firefox browser type
|
||||
let firefox = self.playwright.firefox();
|
||||
|
||||
// Build launch options
|
||||
let mut launch_options = firefox.launcher();
|
||||
launch_options = launch_options.executable(&self.executable_path);
|
||||
launch_options = launch_options.headless(options.headless);
|
||||
|
||||
// Add args
|
||||
if !args.is_empty() {
|
||||
launch_options = launch_options.args(&args);
|
||||
}
|
||||
|
||||
// Add environment as serde_json::Map
|
||||
if !env.is_empty() {
|
||||
let env_map: serde_json::Map<String, serde_json::Value> = env
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, serde_json::Value::String(v)))
|
||||
.collect();
|
||||
launch_options = launch_options.env(env_map);
|
||||
}
|
||||
|
||||
// Add proxy if configured
|
||||
if let Some(proxy) = &config.proxy {
|
||||
let proxy_settings = ProxySettings {
|
||||
server: proxy.server.clone(),
|
||||
username: proxy.username.clone(),
|
||||
password: proxy.password.clone(),
|
||||
bypass: proxy.bypass.clone(),
|
||||
};
|
||||
launch_options = launch_options.proxy(proxy_settings);
|
||||
}
|
||||
|
||||
// Add Firefox preferences
|
||||
if !firefox_prefs.is_empty() {
|
||||
let prefs_map: serde_json::Map<String, serde_json::Value> =
|
||||
firefox_prefs.into_iter().collect();
|
||||
launch_options = launch_options.firefox_user_prefs(prefs_map);
|
||||
}
|
||||
|
||||
// Launch the browser
|
||||
let browser = launch_options.launch().await?;
|
||||
|
||||
Ok(browser)
|
||||
}
|
||||
|
||||
/// Launch a persistent browser context.
|
||||
pub async fn launch_persistent_context(
|
||||
&self,
|
||||
user_data_dir: impl AsRef<Path>,
|
||||
options: LaunchOptions,
|
||||
) -> Result<BrowserContext, LauncherError> {
|
||||
let config = self.build_config(&options)?;
|
||||
|
||||
if options.debug {
|
||||
log::debug!("Camoufox config: {:?}", config.fingerprint_config);
|
||||
}
|
||||
|
||||
// Get environment variables
|
||||
let env_vars = config.get_env_vars()?;
|
||||
|
||||
// Build launch arguments
|
||||
let mut args = options.args.clone().unwrap_or_default();
|
||||
|
||||
if options.headless {
|
||||
args.push("--headless".to_string());
|
||||
}
|
||||
|
||||
// Merge environment variables
|
||||
let mut env = options.env.clone().unwrap_or_default();
|
||||
for (key, value) in env_vars {
|
||||
env.insert(key, value);
|
||||
}
|
||||
|
||||
// Handle fontconfig on Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
if let Some(fontconfig_path) =
|
||||
crate::camoufox::env_vars::get_fontconfig_env(&config.target_os, &self.executable_path)
|
||||
{
|
||||
env.insert("FONTCONFIG_PATH".to_string(), fontconfig_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Build Firefox user prefs
|
||||
let mut firefox_prefs = config.firefox_prefs.clone();
|
||||
if let Some(user_prefs) = options.firefox_user_prefs {
|
||||
for (key, value) in user_prefs {
|
||||
firefox_prefs.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the Firefox browser type
|
||||
let firefox = self.playwright.firefox();
|
||||
|
||||
// Build persistent context options
|
||||
let mut context_options = firefox.persistent_context_launcher(user_data_dir.as_ref());
|
||||
context_options = context_options.executable(&self.executable_path);
|
||||
context_options = context_options.headless(options.headless);
|
||||
|
||||
// Add args
|
||||
if !args.is_empty() {
|
||||
context_options = context_options.args(&args);
|
||||
}
|
||||
|
||||
// Add environment as serde_json::Map
|
||||
if !env.is_empty() {
|
||||
let env_map: serde_json::Map<String, serde_json::Value> = env
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, serde_json::Value::String(v)))
|
||||
.collect();
|
||||
context_options = context_options.env(env_map);
|
||||
}
|
||||
|
||||
// Add proxy if configured
|
||||
if let Some(proxy) = &config.proxy {
|
||||
let proxy_settings = ProxySettings {
|
||||
server: proxy.server.clone(),
|
||||
username: proxy.username.clone(),
|
||||
password: proxy.password.clone(),
|
||||
bypass: proxy.bypass.clone(),
|
||||
};
|
||||
context_options = context_options.proxy(proxy_settings);
|
||||
}
|
||||
|
||||
// Note: PersistentContextLauncher doesn't support firefox_user_prefs
|
||||
// Firefox preferences should be set via about:config or prefs.js in the profile
|
||||
|
||||
// Launch the persistent context
|
||||
let context = context_options.launch().await?;
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
/// Build Camoufox configuration from launch options.
|
||||
fn build_config(&self, options: &LaunchOptions) -> Result<CamoufoxLaunchConfig, LauncherError> {
|
||||
let mut builder = CamoufoxConfigBuilder::new();
|
||||
|
||||
if let Some(os) = &options.os {
|
||||
builder = builder.operating_system(os);
|
||||
}
|
||||
|
||||
if let Some(screen) = &options.screen {
|
||||
builder = builder.screen_constraints(screen.clone());
|
||||
}
|
||||
|
||||
if let Some(fingerprint) = &options.fingerprint {
|
||||
builder = builder.fingerprint(fingerprint.clone());
|
||||
}
|
||||
|
||||
builder = builder.block_images(options.block_images);
|
||||
builder = builder.block_webrtc(options.block_webrtc);
|
||||
builder = builder.block_webgl(options.block_webgl);
|
||||
builder = builder.headless(options.headless);
|
||||
|
||||
if let Some(fonts) = &options.fonts {
|
||||
builder = builder.custom_fonts(fonts.clone());
|
||||
}
|
||||
|
||||
builder = builder.custom_fonts_only(options.custom_fonts_only);
|
||||
|
||||
if let Some(proxy) = &options.proxy {
|
||||
builder = builder.proxy(proxy.clone());
|
||||
}
|
||||
|
||||
// Get Firefox version from executable
|
||||
if let Some(version) = crate::camoufox::config::get_firefox_version(&self.executable_path) {
|
||||
builder = builder.ff_version(version);
|
||||
}
|
||||
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
/// Get the executable path.
|
||||
pub fn executable_path(&self) -> &Path {
|
||||
&self.executable_path
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to launch Camoufox with default settings.
|
||||
pub async fn launch_camoufox(
|
||||
executable_path: impl AsRef<Path>,
|
||||
options: LaunchOptions,
|
||||
) -> Result<Browser, LauncherError> {
|
||||
let launcher = CamoufoxLauncher::new(executable_path).await?;
|
||||
launcher.launch(options).await
|
||||
}
|
||||
|
||||
/// Convenience function to launch a persistent Camoufox context.
|
||||
pub async fn launch_persistent_camoufox(
|
||||
executable_path: impl AsRef<Path>,
|
||||
user_data_dir: impl AsRef<Path>,
|
||||
options: LaunchOptions,
|
||||
) -> Result<BrowserContext, LauncherError> {
|
||||
let launcher = CamoufoxLauncher::new(executable_path).await?;
|
||||
launcher
|
||||
.launch_persistent_context(user_data_dir, options)
|
||||
.await
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
//! Camoufox browser integration module.
|
||||
//!
|
||||
//! Provides native Rust support for launching Camoufox browsers with realistic
|
||||
//! fingerprint injection using playwright-rust.
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! This module replaces the previous Node.js-based nodecar implementation with
|
||||
//! a pure Rust solution. Key components:
|
||||
//!
|
||||
//! - **Fingerprint Generation**: Bayesian network-based fingerprint generation
|
||||
//! - **WebGL Sampling**: Realistic WebGL configurations from a SQLite database
|
||||
//! - **Configuration Builder**: Converts fingerprints to Camoufox config format
|
||||
//! - **Launcher**: playwright-rust integration for browser launching
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use donutbrowser_lib::camoufox::{CamoufoxLauncher, LaunchOptions};
|
||||
//!
|
||||
//! async fn launch_browser() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let launcher = CamoufoxLauncher::new("/path/to/camoufox").await?;
|
||||
//!
|
||||
//! let options = LaunchOptions {
|
||||
//! os: Some("windows".to_string()),
|
||||
//! headless: false,
|
||||
//! ..Default::default()
|
||||
//! };
|
||||
//!
|
||||
//! let browser = launcher.launch(options).await?;
|
||||
//!
|
||||
//! // Use the browser...
|
||||
//!
|
||||
//! browser.close().await?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod config;
|
||||
pub mod data;
|
||||
pub mod env_vars;
|
||||
pub mod fingerprint;
|
||||
pub mod fonts;
|
||||
pub mod geolocation;
|
||||
pub mod launcher;
|
||||
pub mod presets;
|
||||
pub mod webgl;
|
||||
|
||||
// Re-export main types for convenience
|
||||
pub use config::{
|
||||
CamoufoxConfigBuilder, CamoufoxLaunchConfig, ConfigError, GeoIPOption, ProxyConfig,
|
||||
};
|
||||
pub use fingerprint::types::{
|
||||
Fingerprint, FingerprintOptions, FingerprintWithHeaders, NavigatorFingerprint, ScreenConstraints,
|
||||
ScreenFingerprint, VideoCard,
|
||||
};
|
||||
pub use fingerprint::{FingerprintError, FingerprintGenerator};
|
||||
pub use geolocation::{
|
||||
fetch_public_ip, get_geolocation, is_geoip_available, is_ipv4, is_ipv6, validate_ip, Geolocation,
|
||||
GeolocationError, Locale, LocaleSelector,
|
||||
};
|
||||
pub use launcher::{
|
||||
launch_camoufox, launch_persistent_camoufox, CamoufoxLauncher, LaunchOptions, LauncherError,
|
||||
};
|
||||
pub use webgl::{sample_webgl, WebGLData, WebGLError};
|
||||
|
||||
/// Unified error type for all Camoufox operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CamoufoxError {
|
||||
#[error("Launcher error: {0}")]
|
||||
Launcher(#[from] LauncherError),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(#[from] ConfigError),
|
||||
|
||||
#[error("Fingerprint error: {0}")]
|
||||
Fingerprint(#[from] FingerprintError),
|
||||
|
||||
#[error("WebGL error: {0}")]
|
||||
WebGL(#[from] WebGLError),
|
||||
|
||||
#[error("Geolocation error: {0}")]
|
||||
Geolocation(#[from] GeolocationError),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_fingerprint_generation() {
|
||||
let generator = FingerprintGenerator::new().unwrap();
|
||||
let options = FingerprintOptions {
|
||||
browsers: Some(vec!["firefox".to_string()]),
|
||||
operating_system: Some("windows".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = generator.get_fingerprint(&options);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let fp = result.unwrap();
|
||||
assert!(!fp.fingerprint.navigator.user_agent.is_empty());
|
||||
assert!(fp.fingerprint.screen.width > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_builder() {
|
||||
let config = CamoufoxConfigBuilder::new()
|
||||
.operating_system("windows")
|
||||
.block_images(false)
|
||||
.build();
|
||||
|
||||
assert!(config.is_ok());
|
||||
|
||||
let config = config.unwrap();
|
||||
assert!(!config.fingerprint_config.is_empty());
|
||||
assert!(config
|
||||
.fingerprint_config
|
||||
.contains_key("navigator.userAgent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webgl_sampling() {
|
||||
let result = webgl::sample_webgl("win", None, None);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let webgl_data = result.unwrap();
|
||||
assert!(!webgl_data.vendor.is_empty());
|
||||
assert!(!webgl_data.renderer.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fonts() {
|
||||
let fonts = fonts::get_fonts_for_os("win");
|
||||
assert!(!fonts.is_empty());
|
||||
assert!(fonts.contains(&"Arial".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_vars() {
|
||||
let mut config = std::collections::HashMap::new();
|
||||
config.insert(
|
||||
"navigator.userAgent".to_string(),
|
||||
serde_json::json!("Mozilla/5.0"),
|
||||
);
|
||||
|
||||
let env_vars = env_vars::config_to_env_vars(&config).unwrap();
|
||||
assert!(!env_vars.is_empty());
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_1"));
|
||||
}
|
||||
}
|
||||
@@ -1,405 +0,0 @@
|
||||
//! Real-fingerprint preset support for Camoufox.
|
||||
//!
|
||||
//! Mirrors the preset-selection logic from
|
||||
//! `pythonlib/camoufox/fingerprints.py` (`_select_presets_file`,
|
||||
//! `load_presets`, `get_random_preset`, `from_preset`).
|
||||
//!
|
||||
//! Camoufox ships two bundled preset files:
|
||||
//! - `fingerprint-presets-v135.json` — real fingerprints harvested from
|
||||
//! browsers running Firefox ≤148. The frozen "v135 line" — kept around
|
||||
//! so users who haven't upgraded their Camoufox binary keep getting
|
||||
//! consistent fingerprints.
|
||||
//! - `fingerprint-presets-v150.json` — the *newer* bundle, refreshed by
|
||||
//! upstream as Camoufox tracks newer Firefox versions. This is the
|
||||
//! bundle every newer Camoufox release uses; we make no assumption that
|
||||
//! Firefox 150 is the ceiling.
|
||||
//!
|
||||
//! At launch we know the bundled Firefox version (see
|
||||
//! `config::get_firefox_version`) and pick `v135` or `newer` accordingly.
|
||||
//! The split point lives in `data::PRESETS_NEWER_MIN_FF` (currently 149)
|
||||
//! and is the only number we hard-code — anything ≥ that gets the newer
|
||||
//! bundle, regardless of how far Firefox itself has moved on.
|
||||
//!
|
||||
//! Falling back to Bayesian-network synthesis (the previous default) is
|
||||
//! still possible when no preset matches the requested OS.
|
||||
|
||||
use rand::prelude::IndexedRandom;
|
||||
use regex_lite::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::camoufox::data;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Navigator {
|
||||
#[serde(rename = "userAgent")]
|
||||
pub user_agent: Option<String>,
|
||||
pub platform: Option<String>,
|
||||
#[serde(rename = "hardwareConcurrency")]
|
||||
pub hardware_concurrency: Option<u32>,
|
||||
#[serde(rename = "maxTouchPoints")]
|
||||
pub max_touch_points: Option<u32>,
|
||||
pub oscpu: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Screen {
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
#[serde(rename = "colorDepth")]
|
||||
pub color_depth: Option<u32>,
|
||||
#[serde(rename = "availWidth")]
|
||||
pub avail_width: Option<u32>,
|
||||
#[serde(rename = "availHeight")]
|
||||
pub avail_height: Option<u32>,
|
||||
#[serde(rename = "devicePixelRatio")]
|
||||
pub device_pixel_ratio: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct WebGl {
|
||||
#[serde(rename = "unmaskedVendor")]
|
||||
pub unmasked_vendor: Option<String>,
|
||||
#[serde(rename = "unmaskedRenderer")]
|
||||
pub unmasked_renderer: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Preset {
|
||||
#[serde(default)]
|
||||
pub navigator: Option<Navigator>,
|
||||
#[serde(default)]
|
||||
pub screen: Option<Screen>,
|
||||
#[serde(default)]
|
||||
pub webgl: Option<WebGl>,
|
||||
#[serde(default)]
|
||||
pub timezone: Option<String>,
|
||||
#[serde(default)]
|
||||
pub fonts: Option<Vec<String>>,
|
||||
#[serde(rename = "speechVoices", default)]
|
||||
pub speech_voices: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PresetBundle {
|
||||
/// Bundle schema version — upstream writes this as a JSON integer (e.g.
|
||||
/// `1`), so we accept any JSON shape here and ignore it. Only the
|
||||
/// `presets` map matters at runtime.
|
||||
#[allow(dead_code)]
|
||||
#[serde(default)]
|
||||
pub version: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub presets: HashMap<String, Vec<Preset>>,
|
||||
}
|
||||
|
||||
/// Which Camoufox release line the active binary belongs to. Determines
|
||||
/// which preset bundle to load. The set is intentionally just two-valued:
|
||||
/// the legacy v135 line and "everything newer" — upstream refreshes the
|
||||
/// newer bundle as Firefox versions advance, but our routing logic stays
|
||||
/// the same.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PresetLine {
|
||||
V135,
|
||||
Newer,
|
||||
}
|
||||
|
||||
/// Pick the preset line that matches a Firefox major version, mirroring
|
||||
/// `_select_presets_file` in the Python lib. Unknown / very old versions
|
||||
/// fall back to the v135 bundle so the older Camoufox builds keep working.
|
||||
pub fn preset_line_for(ff_version: Option<u32>) -> PresetLine {
|
||||
match ff_version {
|
||||
Some(v) if v >= data::PRESETS_NEWER_MIN_FF => PresetLine::Newer,
|
||||
_ => PresetLine::V135,
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache the parsed bundles forever — they're static, embedded data and
|
||||
/// parsing the newer file twice would waste a few megs of CPU work on
|
||||
/// every launch.
|
||||
static V135_BUNDLE: OnceLock<Option<PresetBundle>> = OnceLock::new();
|
||||
static NEWER_BUNDLE: OnceLock<Option<PresetBundle>> = OnceLock::new();
|
||||
|
||||
fn parse_bundle(json: &str) -> Option<PresetBundle> {
|
||||
match serde_json::from_str::<PresetBundle>(json) {
|
||||
Ok(b) => Some(b),
|
||||
Err(e) => {
|
||||
log::warn!("camoufox preset bundle failed to parse: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_presets(line: PresetLine) -> Option<&'static PresetBundle> {
|
||||
let slot = match line {
|
||||
PresetLine::V135 => &V135_BUNDLE,
|
||||
PresetLine::Newer => &NEWER_BUNDLE,
|
||||
};
|
||||
slot
|
||||
.get_or_init(|| match line {
|
||||
PresetLine::V135 => parse_bundle(data::FINGERPRINT_PRESETS_V135_JSON),
|
||||
PresetLine::Newer => parse_bundle(data::FINGERPRINT_PRESETS_NEWER_JSON),
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Normalize the OS string the rest of the codebase uses ("macos", "windows",
|
||||
/// "linux") to the preset key. Returns `None` for OSes that don't have any
|
||||
/// presets bundled.
|
||||
fn normalize_os(os: &str) -> Option<&'static str> {
|
||||
match os {
|
||||
"windows" | "win" => Some("windows"),
|
||||
"macos" | "mac" | "darwin" => Some("macos"),
|
||||
"linux" | "lin" => Some("linux"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick a random preset for the requested OS. `None` if there are no
|
||||
/// presets bundled for that OS (which can happen in tests with reduced
|
||||
/// fixtures, or if a new OS is added before its preset bundle ships).
|
||||
pub fn get_random_preset(os: Option<&str>, ff_version: Option<u32>) -> Option<Preset> {
|
||||
let bundle = load_presets(preset_line_for(ff_version))?;
|
||||
|
||||
let candidates: Vec<&Preset> = match os.and_then(normalize_os) {
|
||||
Some(os_key) => bundle.presets.get(os_key).map(|v| v.iter().collect())?,
|
||||
None => bundle.presets.values().flatten().collect(),
|
||||
};
|
||||
if candidates.is_empty() {
|
||||
return None;
|
||||
}
|
||||
candidates.choose(&mut rand::rng()).map(|p| (*p).clone())
|
||||
}
|
||||
|
||||
/// Match python's `from_preset` — translate a real-fingerprint preset into
|
||||
/// the CAMOU_CONFIG-style HashMap the rest of the launcher expects.
|
||||
///
|
||||
/// The caller is responsible for filling in fonts, voices, and the random
|
||||
/// seeds; those are intentionally left out here so each call site can layer
|
||||
/// its own RNG and font policy.
|
||||
pub fn from_preset(preset: &Preset, ff_version: Option<u32>) -> HashMap<String, serde_json::Value> {
|
||||
let mut config: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
|
||||
if let Some(nav) = &preset.navigator {
|
||||
if let Some(ua) = &nav.user_agent {
|
||||
let ua = if let Some(v) = ff_version {
|
||||
rewrite_ua_firefox_version(ua, v)
|
||||
} else {
|
||||
ua.clone()
|
||||
};
|
||||
config.insert("navigator.userAgent".to_string(), serde_json::json!(ua));
|
||||
}
|
||||
if let Some(p) = &nav.platform {
|
||||
config.insert("navigator.platform".to_string(), serde_json::json!(p));
|
||||
}
|
||||
if let Some(hc) = nav.hardware_concurrency {
|
||||
config.insert(
|
||||
"navigator.hardwareConcurrency".to_string(),
|
||||
serde_json::json!(hc),
|
||||
);
|
||||
}
|
||||
if let Some(mtp) = nav.max_touch_points {
|
||||
config.insert(
|
||||
"navigator.maxTouchPoints".to_string(),
|
||||
serde_json::json!(mtp),
|
||||
);
|
||||
}
|
||||
// navigator.oscpu — explicit, or derived from the platform.
|
||||
let oscpu = nav.oscpu.clone().or_else(|| {
|
||||
nav.platform.as_deref().and_then(|plat| match plat {
|
||||
"MacIntel" => Some("Intel Mac OS X 10.15".to_string()),
|
||||
"Win32" => Some("Windows NT 10.0; Win64; x64".to_string()),
|
||||
p if p.to_ascii_lowercase().contains("linux") => Some("Linux x86_64".to_string()),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(o) = oscpu {
|
||||
config.insert("navigator.oscpu".to_string(), serde_json::json!(o));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(s) = &preset.screen {
|
||||
if let Some(w) = s.width {
|
||||
config.insert("screen.width".to_string(), serde_json::json!(w));
|
||||
}
|
||||
if let Some(h) = s.height {
|
||||
config.insert("screen.height".to_string(), serde_json::json!(h));
|
||||
}
|
||||
if let Some(cd) = s.color_depth {
|
||||
config.insert("screen.colorDepth".to_string(), serde_json::json!(cd));
|
||||
config.insert("screen.pixelDepth".to_string(), serde_json::json!(cd));
|
||||
}
|
||||
if let Some(aw) = s.avail_width {
|
||||
config.insert("screen.availWidth".to_string(), serde_json::json!(aw));
|
||||
}
|
||||
if let Some(ah) = s.avail_height {
|
||||
config.insert("screen.availHeight".to_string(), serde_json::json!(ah));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(w) = &preset.webgl {
|
||||
if let Some(v) = &w.unmasked_vendor {
|
||||
config.insert("webGl:vendor".to_string(), serde_json::json!(v));
|
||||
}
|
||||
if let Some(r) = &w.unmasked_renderer {
|
||||
config.insert("webGl:renderer".to_string(), serde_json::json!(r));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tz) = &preset.timezone {
|
||||
config.insert("timezone".to_string(), serde_json::json!(tz));
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn rewrite_ua_firefox_version(ua: &str, version: u32) -> String {
|
||||
let firefox_re = Regex::new(r"Firefox/\d+\.0").expect("static regex");
|
||||
let rv_re = Regex::new(r"rv:\d+\.0").expect("static regex");
|
||||
let first = firefox_re.replace_all(ua, format!("Firefox/{version}.0"));
|
||||
rv_re
|
||||
.replace_all(&first, format!("rv:{version}.0"))
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn picks_v135_for_old_firefox() {
|
||||
assert_eq!(preset_line_for(Some(135)), PresetLine::V135);
|
||||
assert_eq!(preset_line_for(Some(148)), PresetLine::V135);
|
||||
assert_eq!(preset_line_for(None), PresetLine::V135);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picks_newer_for_anything_past_the_legacy_line() {
|
||||
// The threshold is data::PRESETS_NEWER_MIN_FF (currently 149).
|
||||
// Future Firefox versions all share the same bundle — there's
|
||||
// intentionally no per-version routing past v135.
|
||||
assert_eq!(preset_line_for(Some(149)), PresetLine::Newer);
|
||||
assert_eq!(preset_line_for(Some(150)), PresetLine::Newer);
|
||||
assert_eq!(preset_line_for(Some(160)), PresetLine::Newer);
|
||||
assert_eq!(preset_line_for(Some(200)), PresetLine::Newer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_bundles_parse_and_cover_all_platforms() {
|
||||
for (line, json) in [
|
||||
(PresetLine::V135, data::FINGERPRINT_PRESETS_V135_JSON),
|
||||
(PresetLine::Newer, data::FINGERPRINT_PRESETS_NEWER_JSON),
|
||||
] {
|
||||
let bundle: PresetBundle =
|
||||
serde_json::from_str(json).unwrap_or_else(|e| panic!("bundle {line:?} parse error: {e}"));
|
||||
for os in ["macos", "windows", "linux"] {
|
||||
let presets = bundle.presets.get(os).unwrap_or_else(|| {
|
||||
panic!("bundle {line:?} is missing presets for {os}");
|
||||
});
|
||||
assert!(
|
||||
!presets.is_empty(),
|
||||
"bundle {line:?} has zero presets for {os}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_preset_returns_for_each_os() {
|
||||
for os in ["macos", "windows", "linux"] {
|
||||
let preset = get_random_preset(Some(os), Some(150)).expect("preset");
|
||||
assert!(preset.navigator.is_some(), "navigator present for {os}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_preset_rewrites_firefox_version() {
|
||||
let preset = Preset {
|
||||
navigator: Some(Navigator {
|
||||
user_agent: Some(
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0".to_string(),
|
||||
),
|
||||
platform: Some("Linux x86_64".to_string()),
|
||||
hardware_concurrency: Some(8),
|
||||
max_touch_points: Some(0),
|
||||
oscpu: None,
|
||||
}),
|
||||
screen: None,
|
||||
webgl: None,
|
||||
timezone: None,
|
||||
fonts: None,
|
||||
speech_voices: None,
|
||||
};
|
||||
let config = from_preset(&preset, Some(150));
|
||||
let ua = config
|
||||
.get("navigator.userAgent")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap();
|
||||
assert!(ua.contains("Firefox/150.0"), "got: {ua}");
|
||||
assert!(ua.contains("rv:150.0"), "got: {ua}");
|
||||
// oscpu derived from "Linux x86_64" platform
|
||||
assert_eq!(
|
||||
config
|
||||
.get("navigator.oscpu")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap(),
|
||||
"Linux x86_64"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_preset_derives_oscpu_for_mac_and_win() {
|
||||
let mut preset = Preset {
|
||||
navigator: Some(Navigator {
|
||||
user_agent: None,
|
||||
platform: Some("MacIntel".to_string()),
|
||||
hardware_concurrency: None,
|
||||
max_touch_points: None,
|
||||
oscpu: None,
|
||||
}),
|
||||
screen: None,
|
||||
webgl: None,
|
||||
timezone: None,
|
||||
fonts: None,
|
||||
speech_voices: None,
|
||||
};
|
||||
assert_eq!(
|
||||
from_preset(&preset, None)
|
||||
.get("navigator.oscpu")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap(),
|
||||
"Intel Mac OS X 10.15"
|
||||
);
|
||||
preset.navigator.as_mut().unwrap().platform = Some("Win32".to_string());
|
||||
assert_eq!(
|
||||
from_preset(&preset, None)
|
||||
.get("navigator.oscpu")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap(),
|
||||
"Windows NT 10.0; Win64; x64"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_color_depth_fills_both_keys() {
|
||||
let preset = Preset {
|
||||
navigator: None,
|
||||
screen: Some(Screen {
|
||||
width: Some(1920),
|
||||
height: Some(1080),
|
||||
color_depth: Some(24),
|
||||
avail_width: Some(1920),
|
||||
avail_height: Some(1050),
|
||||
device_pixel_ratio: Some(1.0),
|
||||
}),
|
||||
webgl: None,
|
||||
timezone: None,
|
||||
fonts: None,
|
||||
speech_voices: None,
|
||||
};
|
||||
let config = from_preset(&preset, None);
|
||||
assert_eq!(config.get("screen.colorDepth").unwrap(), 24);
|
||||
assert_eq!(config.get("screen.pixelDepth").unwrap(), 24);
|
||||
assert_eq!(config.get("screen.availWidth").unwrap(), 1920);
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
//! WebGL fingerprint sampling from SQLite database.
|
||||
//!
|
||||
//! Samples realistic WebGL configurations based on OS-specific probability distributions.
|
||||
|
||||
use rand::RngExt;
|
||||
use rusqlite::{Connection, Result as SqliteResult};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::camoufox::data;
|
||||
|
||||
/// WebGL fingerprint data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebGLData {
|
||||
pub vendor: String,
|
||||
pub renderer: String,
|
||||
pub config: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Error type for WebGL operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum WebGLError {
|
||||
#[error("SQLite error: {0}")]
|
||||
Sqlite(#[from] rusqlite::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("No WebGL data found for OS: {0}")]
|
||||
NoDataForOS(String),
|
||||
|
||||
#[error("Invalid vendor/renderer combination for OS {os}: {vendor}/{renderer}")]
|
||||
InvalidCombination {
|
||||
os: String,
|
||||
vendor: String,
|
||||
renderer: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Sample a WebGL configuration for the given OS.
|
||||
///
|
||||
/// If `vendor` and `renderer` are provided, returns the specific configuration.
|
||||
/// Otherwise, randomly samples based on OS-specific probability weights.
|
||||
pub fn sample_webgl(
|
||||
os: &str,
|
||||
vendor: Option<&str>,
|
||||
renderer: Option<&str>,
|
||||
) -> Result<WebGLData, WebGLError> {
|
||||
// Write embedded database to a temporary file
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
temp_file.write_all(data::WEBGL_DATA_DB)?;
|
||||
let db_path = temp_file.path();
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
// Validate OS
|
||||
let os_column = match os {
|
||||
"win" | "windows" => "win",
|
||||
"mac" | "macos" => "mac",
|
||||
"lin" | "linux" => "lin",
|
||||
_ => return Err(WebGLError::NoDataForOS(os.to_string())),
|
||||
};
|
||||
|
||||
if let (Some(v), Some(r)) = (vendor, renderer) {
|
||||
sample_specific(&conn, os_column, v, r)
|
||||
} else {
|
||||
sample_random(&conn, os_column)
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_specific(
|
||||
conn: &Connection,
|
||||
os_column: &str,
|
||||
vendor: &str,
|
||||
renderer: &str,
|
||||
) -> Result<WebGLData, WebGLError> {
|
||||
let query = format!(
|
||||
"SELECT vendor, renderer, data, {} FROM webgl_fingerprints WHERE vendor = ?1 AND renderer = ?2",
|
||||
os_column
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
let mut rows = stmt.query([vendor, renderer])?;
|
||||
|
||||
if let Some(row) = rows.next()? {
|
||||
let weight: f64 = row.get(3)?;
|
||||
if weight <= 0.0 {
|
||||
return Err(WebGLError::InvalidCombination {
|
||||
os: os_column.to_string(),
|
||||
vendor: vendor.to_string(),
|
||||
renderer: renderer.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let data_json: String = row.get(2)?;
|
||||
let config: HashMap<String, serde_json::Value> = serde_json::from_str(&data_json)?;
|
||||
|
||||
Ok(WebGLData {
|
||||
vendor: vendor.to_string(),
|
||||
renderer: renderer.to_string(),
|
||||
config,
|
||||
})
|
||||
} else {
|
||||
Err(WebGLError::InvalidCombination {
|
||||
os: os_column.to_string(),
|
||||
vendor: vendor.to_string(),
|
||||
renderer: renderer.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_random(conn: &Connection, os_column: &str) -> Result<WebGLData, WebGLError> {
|
||||
let query = format!(
|
||||
"SELECT vendor, renderer, data, {} FROM webgl_fingerprints WHERE {} > 0",
|
||||
os_column, os_column
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
let rows: Vec<(String, String, String, f64)> = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, f64>(3)?,
|
||||
))
|
||||
})?
|
||||
.collect::<SqliteResult<Vec<_>>>()?;
|
||||
|
||||
if rows.is_empty() {
|
||||
return Err(WebGLError::NoDataForOS(os_column.to_string()));
|
||||
}
|
||||
|
||||
// Calculate total weight
|
||||
let total_weight: f64 = rows.iter().map(|(_, _, _, w)| w).sum();
|
||||
|
||||
// Weighted random selection
|
||||
let mut rng = rand::rng();
|
||||
let threshold = rng.random::<f64>() * total_weight;
|
||||
let mut cumulative = 0.0;
|
||||
|
||||
for (vendor, renderer, data_json, weight) in &rows {
|
||||
cumulative += *weight;
|
||||
if cumulative >= threshold {
|
||||
let config: HashMap<String, serde_json::Value> = serde_json::from_str(data_json)?;
|
||||
return Ok(WebGLData {
|
||||
vendor: vendor.clone(),
|
||||
renderer: renderer.clone(),
|
||||
config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to last row
|
||||
let (vendor, renderer, data_json, _) = rows.last().unwrap();
|
||||
let config: HashMap<String, serde_json::Value> = serde_json::from_str(data_json)?;
|
||||
Ok(WebGLData {
|
||||
vendor: vendor.clone(),
|
||||
renderer: renderer.clone(),
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all possible vendor/renderer pairs for each OS.
|
||||
pub fn get_possible_pairs() -> Result<HashMap<String, Vec<(String, String)>>, WebGLError> {
|
||||
// Write embedded database to a temporary file
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
temp_file.write_all(data::WEBGL_DATA_DB)?;
|
||||
let db_path = temp_file.path();
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for os in &["win", "mac", "lin"] {
|
||||
let query = format!(
|
||||
"SELECT DISTINCT vendor, renderer FROM webgl_fingerprints WHERE {} > 0 ORDER BY {} DESC",
|
||||
os, os
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
let pairs: Vec<(String, String)> = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})?
|
||||
.collect::<SqliteResult<Vec<_>>>()?;
|
||||
|
||||
result.insert(os.to_string(), pairs);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sample_webgl_windows() {
|
||||
let result = sample_webgl("win", None, None);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to sample WebGL for Windows: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let data = result.unwrap();
|
||||
assert!(!data.vendor.is_empty());
|
||||
assert!(!data.renderer.is_empty());
|
||||
assert!(!data.config.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_webgl_macos() {
|
||||
let result = sample_webgl("mac", None, None);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to sample WebGL for macOS: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_webgl_linux() {
|
||||
let result = sample_webgl("lin", None, None);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to sample WebGL for Linux: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_possible_pairs() {
|
||||
let result = get_possible_pairs();
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to get possible pairs: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let pairs = result.unwrap();
|
||||
assert!(pairs.contains_key("win"));
|
||||
assert!(pairs.contains_key("mac"));
|
||||
assert!(pairs.contains_key("lin"));
|
||||
assert!(!pairs.get("win").unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,855 +0,0 @@
|
||||
use crate::browser_runner::BrowserRunner;
|
||||
use crate::camoufox::{CamoufoxConfigBuilder, GeoIPOption, ScreenConstraints};
|
||||
use crate::profile::BrowserProfile;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tauri::AppHandle;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CamoufoxConfig {
|
||||
pub proxy: Option<String>,
|
||||
pub screen_max_width: Option<u32>,
|
||||
pub screen_max_height: Option<u32>,
|
||||
pub screen_min_width: Option<u32>,
|
||||
pub screen_min_height: Option<u32>,
|
||||
pub geoip: Option<serde_json::Value>, // Can be String or bool
|
||||
pub block_images: Option<bool>,
|
||||
pub block_webrtc: Option<bool>,
|
||||
pub block_webgl: Option<bool>,
|
||||
pub fingerprint: Option<String>, // JSON string of the complete fingerprint config
|
||||
pub randomize_fingerprint_on_launch: Option<bool>, // Generate new fingerprint on every launch
|
||||
pub os: Option<String>, // Operating system for fingerprint generation: "windows", "macos", or "linux"
|
||||
}
|
||||
|
||||
impl Default for CamoufoxConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
proxy: None,
|
||||
screen_max_width: None,
|
||||
screen_max_height: None,
|
||||
screen_min_width: None,
|
||||
screen_min_height: None,
|
||||
geoip: Some(serde_json::Value::Bool(true)),
|
||||
block_images: None,
|
||||
block_webrtc: None,
|
||||
block_webgl: None,
|
||||
fingerprint: None,
|
||||
randomize_fingerprint_on_launch: None,
|
||||
os: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
pub struct CamoufoxLaunchResult {
|
||||
pub id: String,
|
||||
#[serde(alias = "process_id")]
|
||||
pub processId: Option<u32>,
|
||||
#[serde(alias = "profile_path")]
|
||||
pub profilePath: Option<String>,
|
||||
pub url: Option<String>,
|
||||
pub cdp_port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CamoufoxInstance {
|
||||
#[allow(dead_code)]
|
||||
id: String,
|
||||
process_id: Option<u32>,
|
||||
profile_path: Option<String>,
|
||||
url: Option<String>,
|
||||
cdp_port: Option<u16>,
|
||||
}
|
||||
|
||||
struct CamoufoxManagerInner {
|
||||
instances: HashMap<String, CamoufoxInstance>,
|
||||
}
|
||||
|
||||
pub struct CamoufoxManager {
|
||||
inner: Arc<AsyncMutex<CamoufoxManagerInner>>,
|
||||
}
|
||||
|
||||
impl CamoufoxManager {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(AsyncMutex::new(CamoufoxManagerInner {
|
||||
instances: HashMap::new(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instance() -> &'static CamoufoxManager {
|
||||
&CAMOUFOX_LAUNCHER
|
||||
}
|
||||
|
||||
async fn find_free_port() -> Result<u16, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
|
||||
let port = listener.local_addr()?.port();
|
||||
drop(listener);
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_cdp_port(&self, profile_path: &str) -> Option<u16> {
|
||||
let inner = self.inner.lock().await;
|
||||
let target_path = std::path::Path::new(profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf());
|
||||
|
||||
for instance in inner.instances.values() {
|
||||
if let Some(path) = &instance.profile_path {
|
||||
let instance_path = std::path::Path::new(path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(path).to_path_buf());
|
||||
if instance_path == target_path {
|
||||
return instance.cdp_port;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_profiles_dir(&self) -> PathBuf {
|
||||
crate::app_dirs::profiles_dir()
|
||||
}
|
||||
|
||||
/// Generate Camoufox fingerprint configuration during profile creation
|
||||
pub async fn generate_fingerprint_config(
|
||||
&self,
|
||||
_app_handle: &AppHandle,
|
||||
profile: &crate::profile::BrowserProfile,
|
||||
config: &CamoufoxConfig,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Get executable path
|
||||
let executable_path = BrowserRunner::instance()
|
||||
.get_browser_executable_path(profile)
|
||||
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?;
|
||||
|
||||
// Build the config using CamoufoxConfigBuilder
|
||||
let mut builder = CamoufoxConfigBuilder::new()
|
||||
.block_images(config.block_images.unwrap_or(false))
|
||||
.block_webrtc(config.block_webrtc.unwrap_or(false))
|
||||
.block_webgl(config.block_webgl.unwrap_or(false));
|
||||
|
||||
// Set operating system
|
||||
if let Some(os) = &config.os {
|
||||
builder = builder.operating_system(os);
|
||||
}
|
||||
|
||||
// Build screen constraints if provided
|
||||
if config.screen_min_width.is_some()
|
||||
|| config.screen_max_width.is_some()
|
||||
|| config.screen_min_height.is_some()
|
||||
|| config.screen_max_height.is_some()
|
||||
{
|
||||
let screen_constraints = ScreenConstraints {
|
||||
min_width: config.screen_min_width,
|
||||
max_width: config.screen_max_width,
|
||||
min_height: config.screen_min_height,
|
||||
max_height: config.screen_max_height,
|
||||
};
|
||||
builder = builder.screen_constraints(screen_constraints);
|
||||
}
|
||||
|
||||
// Parse proxy if provided
|
||||
if let Some(proxy_str) = &config.proxy {
|
||||
let proxy_config = crate::camoufox::ProxyConfig::from_url(proxy_str)
|
||||
.map_err(|e| format!("Failed to parse proxy URL: {e}"))?;
|
||||
builder = builder.proxy(proxy_config);
|
||||
}
|
||||
|
||||
// Set Firefox version from executable
|
||||
if let Some(version) = crate::camoufox::config::get_firefox_version(&executable_path) {
|
||||
builder = builder.ff_version(version);
|
||||
}
|
||||
|
||||
// Handle geoip option
|
||||
if let Some(geoip_value) = &config.geoip {
|
||||
match geoip_value {
|
||||
serde_json::Value::Bool(true) => {
|
||||
// Auto-detect IP (through proxy if set)
|
||||
builder = builder.geoip(GeoIPOption::Auto);
|
||||
}
|
||||
serde_json::Value::String(ip) => {
|
||||
// Use specific IP
|
||||
builder = builder.geoip(GeoIPOption::IP(ip.clone()));
|
||||
}
|
||||
_ => {
|
||||
// geoip: false or other values - don't apply geolocation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the config (async to handle geoip)
|
||||
let launch_config = builder
|
||||
.build_async()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to build Camoufox config: {e}"))?;
|
||||
|
||||
// Return the fingerprint config as JSON
|
||||
let config_json = serde_json::to_string(&launch_config.fingerprint_config)
|
||||
.map_err(|e| format!("Failed to serialize config: {e}"))?;
|
||||
|
||||
Ok(config_json)
|
||||
}
|
||||
|
||||
/// Launch Camoufox browser by directly spawning the process
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn launch_camoufox(
|
||||
&self,
|
||||
_app_handle: &AppHandle,
|
||||
profile: &crate::profile::BrowserProfile,
|
||||
profile_path: &str,
|
||||
config: &CamoufoxConfig,
|
||||
url: Option<&str>,
|
||||
remote_debugging_port: Option<u16>,
|
||||
headless: bool,
|
||||
) -> Result<CamoufoxLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let custom_config = if let Some(existing_fingerprint) = &config.fingerprint {
|
||||
log::info!("Using existing fingerprint from profile metadata");
|
||||
existing_fingerprint.clone()
|
||||
} else {
|
||||
return Err("No fingerprint provided".into());
|
||||
};
|
||||
|
||||
// Get executable path
|
||||
let executable_path = BrowserRunner::instance()
|
||||
.get_browser_executable_path(profile)
|
||||
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?;
|
||||
|
||||
// Parse the fingerprint config JSON
|
||||
let mut fingerprint_config: HashMap<String, serde_json::Value> =
|
||||
serde_json::from_str(&custom_config)
|
||||
.map_err(|e| format!("Failed to parse fingerprint config: {e}"))?;
|
||||
|
||||
// Strip `window.history.length` even when present in a previously-saved
|
||||
// fingerprint. Newer Camoufox clamps the docShell session history to the
|
||||
// spoofed value, which disables the toolbar back/forward buttons. See
|
||||
// the matching note in camoufox/config.rs.
|
||||
fingerprint_config.remove("window.history.length");
|
||||
|
||||
// Convert to environment variables using CAMOU_CONFIG chunking
|
||||
let env_vars = crate::camoufox::env_vars::config_to_env_vars(&fingerprint_config)
|
||||
.map_err(|e| format!("Failed to convert config to env vars: {e}"))?;
|
||||
|
||||
// Build command arguments
|
||||
// Note: We intentionally do NOT use -no-remote to allow opening URLs in existing instances
|
||||
// via Firefox's remote messaging mechanism. This enables "open in new tab" functionality
|
||||
// when Donut is set as the default browser.
|
||||
let mut args = vec![
|
||||
"-profile".to_string(),
|
||||
std::path::Path::new(profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf())
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
];
|
||||
|
||||
let cdp_port = match remote_debugging_port {
|
||||
Some(p) => p,
|
||||
None => Self::find_free_port().await?,
|
||||
};
|
||||
args.push(format!("--remote-debugging-port={cdp_port}"));
|
||||
|
||||
// Add URL if provided
|
||||
if let Some(url) = url {
|
||||
args.push("-new-tab".to_string());
|
||||
args.push(url.to_string());
|
||||
}
|
||||
|
||||
// Add headless flag when requested via the API or via the CAMOUFOX_HEADLESS
|
||||
// env var (used by integration tests)
|
||||
if headless || std::env::var("CAMOUFOX_HEADLESS").is_ok() {
|
||||
args.push("--headless".to_string());
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Launching Camoufox: {:?} with args: {:?}",
|
||||
executable_path,
|
||||
args
|
||||
);
|
||||
|
||||
// Spawn the browser process. Camoufox prints NSS/PSM and proxy failures
|
||||
// to stderr (e.g. cert errors, CONNECT failures) and the user otherwise
|
||||
// sees only an opaque "Secure Connection Failed" page — capture stderr
|
||||
// to a per-launch file so diagnostics survive without a TTY.
|
||||
let stderr_log_path = std::env::temp_dir().join(format!("camoufox-stderr-{}.log", profile.id));
|
||||
let mut command = TokioCommand::new(&executable_path);
|
||||
command
|
||||
.args(&args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null());
|
||||
|
||||
match std::fs::File::create(&stderr_log_path) {
|
||||
Ok(file) => {
|
||||
log::info!(
|
||||
"Camoufox stderr will be logged to: {}",
|
||||
stderr_log_path.display()
|
||||
);
|
||||
command.stderr(Stdio::from(file));
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Failed to open Camoufox stderr log {}: {e}",
|
||||
stderr_log_path.display()
|
||||
);
|
||||
command.stderr(Stdio::null());
|
||||
}
|
||||
}
|
||||
|
||||
// Add environment variables
|
||||
for (key, value) in &env_vars {
|
||||
command.env(key, value);
|
||||
}
|
||||
|
||||
// Handle fontconfig on Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
let target_os = config.os.as_deref().unwrap_or("linux");
|
||||
if let Some(fontconfig_path) =
|
||||
crate::camoufox::env_vars::get_fontconfig_env(target_os, &executable_path)
|
||||
{
|
||||
command.env("FONTCONFIG_PATH", fontconfig_path);
|
||||
}
|
||||
}
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn Camoufox process: {e}"))?;
|
||||
|
||||
let process_id = child.id();
|
||||
let instance_id = format!("camoufox_{}", process_id.unwrap_or(0));
|
||||
|
||||
log::info!("Camoufox launched with PID: {:?}", process_id);
|
||||
|
||||
// Watch the child so its exit status (signal / non-zero code) lands in
|
||||
// the log. Without this, all we see is "PID X is no longer running" via
|
||||
// the periodic sysinfo poll, with no clue why it died.
|
||||
let watch_profile_path = profile_path.to_string();
|
||||
tokio::spawn(async move {
|
||||
match child.wait().await {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
log::info!(
|
||||
"Camoufox PID {:?} for {} exited cleanly (status=0)",
|
||||
process_id,
|
||||
watch_profile_path
|
||||
);
|
||||
} else {
|
||||
log::warn!(
|
||||
"Camoufox PID {:?} for {} exited abnormally: {}",
|
||||
process_id,
|
||||
watch_profile_path,
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to await Camoufox PID {:?} exit: {}", process_id, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store the instance
|
||||
let instance = CamoufoxInstance {
|
||||
id: instance_id.clone(),
|
||||
process_id,
|
||||
profile_path: Some(profile_path.to_string()),
|
||||
url: url.map(String::from),
|
||||
cdp_port: Some(cdp_port),
|
||||
};
|
||||
|
||||
let launch_result = CamoufoxLaunchResult {
|
||||
id: instance_id.clone(),
|
||||
processId: process_id,
|
||||
profilePath: Some(profile_path.to_string()),
|
||||
url: url.map(String::from),
|
||||
cdp_port: Some(cdp_port),
|
||||
};
|
||||
|
||||
{
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.instances.insert(instance_id, instance);
|
||||
}
|
||||
|
||||
Ok(launch_result)
|
||||
}
|
||||
|
||||
/// Stop a Camoufox process by ID
|
||||
pub async fn stop_camoufox(
|
||||
&self,
|
||||
_app_handle: &AppHandle,
|
||||
id: &str,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Get the process ID from our tracking
|
||||
let process_id = {
|
||||
let inner = self.inner.lock().await;
|
||||
inner
|
||||
.instances
|
||||
.get(id)
|
||||
.and_then(|instance| instance.process_id)
|
||||
};
|
||||
|
||||
if let Some(pid) = process_id {
|
||||
// Kill the process
|
||||
let success = self.kill_process(pid);
|
||||
|
||||
if success {
|
||||
// Remove from our tracking
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.instances.remove(id);
|
||||
log::info!("Stopped Camoufox instance {} (PID: {})", id, pid);
|
||||
}
|
||||
|
||||
Ok(success)
|
||||
} else {
|
||||
// No process ID found, just remove from tracking
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.instances.remove(id);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Kill a process by PID
|
||||
fn kill_process(&self, pid: u32) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
let result = std::process::Command::new("kill")
|
||||
.args(["-TERM", &pid.to_string()])
|
||||
.status();
|
||||
|
||||
match result {
|
||||
Ok(status) => status.success() || status.signal() == Some(0),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to kill process {}: {}", pid, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let result = std::process::Command::new("taskkill")
|
||||
.args(["/PID", &pid.to_string(), "/T"])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.status();
|
||||
|
||||
match result {
|
||||
Ok(status) => status.success(),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to kill process {}: {}", pid, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find Camoufox server by profile path (for integration with browser_runner)
|
||||
/// This method first checks in-memory instances, then scans system processes
|
||||
/// to detect Camoufox instances that may have been started before the app restarted.
|
||||
pub async fn find_camoufox_by_profile(
|
||||
&self,
|
||||
profile_path: &str,
|
||||
) -> Result<Option<CamoufoxLaunchResult>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// First clean up any dead instances
|
||||
self.cleanup_dead_instances().await?;
|
||||
|
||||
// Convert paths to canonical form for comparison
|
||||
let target_path = std::path::Path::new(profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf());
|
||||
|
||||
// Check in-memory instances first
|
||||
{
|
||||
let inner = self.inner.lock().await;
|
||||
|
||||
for (id, instance) in inner.instances.iter() {
|
||||
if let Some(instance_profile_path) = &instance.profile_path {
|
||||
let instance_path = std::path::Path::new(instance_profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(instance_profile_path).to_path_buf());
|
||||
|
||||
if instance_path == target_path {
|
||||
// Verify the server is actually running by checking the process
|
||||
if let Some(process_id) = instance.process_id {
|
||||
if self.is_server_running(process_id).await {
|
||||
// Found running Camoufox instance
|
||||
return Ok(Some(CamoufoxLaunchResult {
|
||||
id: id.clone(),
|
||||
processId: instance.process_id,
|
||||
profilePath: instance.profile_path.clone(),
|
||||
url: instance.url.clone(),
|
||||
cdp_port: instance.cdp_port,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in in-memory instances, scan system processes
|
||||
// This handles the case where the app was restarted but Camoufox is still running
|
||||
if let Some((pid, found_profile_path, cdp_port)) =
|
||||
self.find_camoufox_process_by_profile(&target_path)
|
||||
{
|
||||
log::info!(
|
||||
"Found running Camoufox process (PID: {}) for profile path via system scan",
|
||||
pid
|
||||
);
|
||||
|
||||
// Register this instance in our tracking
|
||||
let instance_id = format!("recovered_{}", pid);
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.instances.insert(
|
||||
instance_id.clone(),
|
||||
CamoufoxInstance {
|
||||
id: instance_id.clone(),
|
||||
process_id: Some(pid),
|
||||
profile_path: Some(found_profile_path.clone()),
|
||||
url: None,
|
||||
cdp_port,
|
||||
},
|
||||
);
|
||||
|
||||
return Ok(Some(CamoufoxLaunchResult {
|
||||
id: instance_id,
|
||||
processId: Some(pid),
|
||||
profilePath: Some(found_profile_path),
|
||||
url: None,
|
||||
cdp_port,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Scan system processes to find a Camoufox process using a specific profile path
|
||||
fn find_camoufox_process_by_profile(
|
||||
&self,
|
||||
target_path: &std::path::Path,
|
||||
) -> Option<(u32, String, Option<u16>)> {
|
||||
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
|
||||
|
||||
let system = System::new_with_specifics(
|
||||
RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()),
|
||||
);
|
||||
|
||||
let target_path_str = target_path.to_string_lossy();
|
||||
|
||||
for (pid, process) in system.processes() {
|
||||
let cmd = process.cmd();
|
||||
if cmd.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a Camoufox/Firefox process
|
||||
let exe_name = process.name().to_string_lossy().to_lowercase();
|
||||
let is_firefox_like = exe_name.contains("firefox")
|
||||
|| exe_name.contains("camoufox")
|
||||
|| exe_name.contains("firefox-bin");
|
||||
|
||||
if !is_firefox_like {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut matched = false;
|
||||
let mut found_profile_path = None;
|
||||
let mut cdp_port: Option<u16> = None;
|
||||
|
||||
// Check if the command line contains our profile path
|
||||
for (i, arg) in cmd.iter().enumerate() {
|
||||
if let Some(arg_str) = arg.to_str() {
|
||||
// Check for -profile argument followed by our path
|
||||
if arg_str == "-profile" && i + 1 < cmd.len() {
|
||||
if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) {
|
||||
let cmd_path = std::path::Path::new(next_arg)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(next_arg).to_path_buf());
|
||||
|
||||
if cmd_path == target_path {
|
||||
matched = true;
|
||||
found_profile_path = Some(next_arg.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if the argument contains the profile path directly
|
||||
if !matched && arg_str.contains(&*target_path_str) {
|
||||
matched = true;
|
||||
found_profile_path = Some(target_path_str.to_string());
|
||||
}
|
||||
|
||||
if let Some(port_val) = arg_str.strip_prefix("--remote-debugging-port=") {
|
||||
cdp_port = port_val.parse().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
if let Some(profile_path) = found_profile_path {
|
||||
return Some((pid.as_u32(), profile_path, cdp_port));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if servers are still alive and clean up dead instances
|
||||
pub async fn cleanup_dead_instances(
|
||||
&self,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut dead_instances = Vec::new();
|
||||
let mut instances_to_remove = Vec::new();
|
||||
|
||||
{
|
||||
let inner = self.inner.lock().await;
|
||||
|
||||
for (id, instance) in inner.instances.iter() {
|
||||
if let Some(process_id) = instance.process_id {
|
||||
if !self.is_server_running(process_id).await {
|
||||
log::info!(
|
||||
"Camoufox instance {} (PID {}) is no longer running; profile_path={:?}",
|
||||
id,
|
||||
process_id,
|
||||
instance.profile_path
|
||||
);
|
||||
dead_instances.push(id.clone());
|
||||
instances_to_remove.push(id.clone());
|
||||
}
|
||||
} else {
|
||||
log::info!("Camoufox instance {} has no PID, marking as dead", id);
|
||||
dead_instances.push(id.clone());
|
||||
instances_to_remove.push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !instances_to_remove.is_empty() {
|
||||
let mut inner = self.inner.lock().await;
|
||||
for id in &instances_to_remove {
|
||||
inner.instances.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(dead_instances)
|
||||
}
|
||||
|
||||
/// Check if a Camoufox server is running with the given process ID
|
||||
async fn is_server_running(&self, process_id: u32) -> bool {
|
||||
// Check if the process is still running
|
||||
use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System};
|
||||
|
||||
let system = System::new_with_specifics(
|
||||
RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()),
|
||||
);
|
||||
if let Some(process) = system.process(Pid::from(process_id as usize)) {
|
||||
// Check if this is actually a Camoufox process by looking at the command line
|
||||
let cmd = process.cmd();
|
||||
let is_camoufox = cmd.iter().any(|arg| {
|
||||
let arg_str = arg.to_str().unwrap_or("");
|
||||
arg_str.contains("camoufox-worker") || arg_str.contains("camoufox")
|
||||
});
|
||||
|
||||
if is_camoufox {
|
||||
// Found running Camoufox process
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl CamoufoxManager {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn launch_camoufox_profile(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
profile: BrowserProfile,
|
||||
config: CamoufoxConfig,
|
||||
url: Option<String>,
|
||||
override_profile_path: Option<std::path::PathBuf>,
|
||||
remote_debugging_port: Option<u16>,
|
||||
headless: bool,
|
||||
) -> Result<CamoufoxLaunchResult, String> {
|
||||
// Get profile path
|
||||
let profile_path = if let Some(ref override_path) = override_profile_path {
|
||||
override_path.clone()
|
||||
} else {
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
profile.get_profile_data_path(&profiles_dir)
|
||||
};
|
||||
let profile_path_str = profile_path.to_string_lossy();
|
||||
|
||||
// Check if there's already a running instance for this profile
|
||||
if let Ok(Some(existing)) = self.find_camoufox_by_profile(&profile_path_str).await {
|
||||
// If there's an existing instance, stop it first to avoid conflicts
|
||||
let _ = self.stop_camoufox(&app_handle, &existing.id).await;
|
||||
}
|
||||
|
||||
// Clean up any dead instances before launching
|
||||
let _ = self.cleanup_dead_instances().await;
|
||||
|
||||
// For ephemeral profiles, write Firefox prefs to minimize disk writes
|
||||
if override_profile_path.is_some() {
|
||||
let user_js_path = profile_path.join("user.js");
|
||||
let prefs = concat!(
|
||||
"user_pref(\"browser.cache.disk.enable\", false);\n",
|
||||
"user_pref(\"browser.cache.memory.enable\", true);\n",
|
||||
"user_pref(\"browser.sessionstore.resume_from_crash\", false);\n",
|
||||
"user_pref(\"browser.sessionstore.max_tabs_undo\", 0);\n",
|
||||
"user_pref(\"browser.sessionstore.max_windows_undo\", 0);\n",
|
||||
"user_pref(\"places.history.enabled\", false);\n",
|
||||
"user_pref(\"browser.formfill.enable\", false);\n",
|
||||
"user_pref(\"signon.rememberSignons\", false);\n",
|
||||
"user_pref(\"browser.bookmarks.max_backups\", 0);\n",
|
||||
"user_pref(\"browser.shell.checkDefaultBrowser\", false);\n",
|
||||
"user_pref(\"toolkit.crashreporter.enabled\", false);\n",
|
||||
"user_pref(\"browser.pagethumbnails.capturing_disabled\", true);\n",
|
||||
"user_pref(\"browser.download.manager.addToRecentDocs\", false);\n",
|
||||
);
|
||||
if let Err(e) = std::fs::write(&user_js_path, prefs) {
|
||||
log::warn!("Failed to write ephemeral user.js: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Patch user.js with Camoufox-specific overrides on every launch. This
|
||||
// always runs (not gated on the proxy being set) because Camoufox's
|
||||
// bundled camoufox.cfg ships defaults that break basic browser features
|
||||
// and we need to override them per-profile.
|
||||
{
|
||||
let user_js_path = profile_path.join("user.js");
|
||||
let mut prefs = String::new();
|
||||
|
||||
// Preserve existing user.js lines, but strip any keys we're about to
|
||||
// re-emit so they never duplicate.
|
||||
let managed_keys = [
|
||||
"network.proxy.",
|
||||
"network.http.http3.enable",
|
||||
"network.http.http3.enabled",
|
||||
"xpinstall.signatures.required",
|
||||
"extensions.startupScanScopes",
|
||||
"browser.sessionhistory.max_entries",
|
||||
"browser.sessionhistory.max_total_viewers",
|
||||
];
|
||||
if let Ok(existing) = std::fs::read_to_string(&user_js_path) {
|
||||
for line in existing.lines() {
|
||||
if !managed_keys.iter().any(|k| line.contains(k)) {
|
||||
prefs.push_str(line);
|
||||
prefs.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Camoufox's bundled camoufox.cfg sets these to 0, which makes
|
||||
// docShell remember zero prior pages and leaves the toolbar
|
||||
// back/forward buttons permanently disabled no matter how much
|
||||
// the user navigates. Restore Firefox defaults.
|
||||
prefs.push_str(
|
||||
"user_pref(\"browser.sessionhistory.max_entries\", 50);\n\
|
||||
user_pref(\"browser.sessionhistory.max_total_viewers\", -1);\n",
|
||||
);
|
||||
|
||||
// Required for sideloaded extensions:
|
||||
// - signatures.required=false accepts unsigned .xpi (Camoufox is built
|
||||
// without MOZ_REQUIRE_SIGNING so this is honored).
|
||||
// - startupScanScopes=1 rescans SCOPE_PROFILE on each launch so newly
|
||||
// dropped .xpi files in <profile>/extensions/ get registered.
|
||||
prefs.push_str(
|
||||
"user_pref(\"xpinstall.signatures.required\", false);\n\
|
||||
user_pref(\"extensions.startupScanScopes\", 1);\n",
|
||||
);
|
||||
|
||||
// Disable HTTP/3 / QUIC. Camoufox always sits behind the local
|
||||
// donut-proxy, and Firefox-150's QUIC stack bypasses configured HTTP
|
||||
// proxies and goes direct UDP to the remote host. With an upstream
|
||||
// proxy that's the only allowed egress, that traffic silently fails
|
||||
// and pages won't load. (Chromium suppresses QUIC under a proxy on
|
||||
// its own, so Wayfern doesn't need the equivalent toggle.) Both
|
||||
// pref names are emitted because they've been renamed across FF
|
||||
// versions and either could be the active one at runtime.
|
||||
prefs.push_str(
|
||||
"user_pref(\"network.http.http3.enable\", false);\n\
|
||||
user_pref(\"network.http.http3.enabled\", false);\n",
|
||||
);
|
||||
|
||||
if let Some(proxy_str) = &config.proxy {
|
||||
if let Ok(parsed) = url::Url::parse(proxy_str) {
|
||||
let host = parsed.host_str().unwrap_or("127.0.0.1");
|
||||
let port = parsed.port().unwrap_or(8080);
|
||||
let scheme = parsed.scheme();
|
||||
|
||||
if scheme == "socks5" || scheme == "socks4" {
|
||||
prefs.push_str(&format!(
|
||||
"user_pref(\"network.proxy.type\", 1);\n\
|
||||
user_pref(\"network.proxy.socks\", \"{host}\");\n\
|
||||
user_pref(\"network.proxy.socks_port\", {port});\n\
|
||||
user_pref(\"network.proxy.socks_version\", {});\n\
|
||||
user_pref(\"network.proxy.socks_remote_dns\", true);\n",
|
||||
if scheme == "socks5" { 5 } else { 4 }
|
||||
));
|
||||
} else {
|
||||
// HTTP/HTTPS proxy
|
||||
prefs.push_str(&format!(
|
||||
"user_pref(\"network.proxy.type\", 1);\n\
|
||||
user_pref(\"network.proxy.http\", \"{host}\");\n\
|
||||
user_pref(\"network.proxy.http_port\", {port});\n\
|
||||
user_pref(\"network.proxy.ssl\", \"{host}\");\n\
|
||||
user_pref(\"network.proxy.ssl_port\", {port});\n\
|
||||
user_pref(\"network.proxy.no_proxies_on\", \"\");\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = std::fs::write(&user_js_path, prefs) {
|
||||
log::warn!("Failed to write user.js: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
.launch_camoufox(
|
||||
&app_handle,
|
||||
&profile,
|
||||
&profile_path_str,
|
||||
&config,
|
||||
url.as_deref(),
|
||||
remote_debugging_port,
|
||||
headless,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to launch Camoufox: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let default_config = CamoufoxConfig::default();
|
||||
|
||||
// Verify defaults
|
||||
assert_eq!(default_config.geoip, Some(serde_json::Value::Bool(true)));
|
||||
assert_eq!(default_config.proxy, None);
|
||||
assert_eq!(default_config.fingerprint, None);
|
||||
assert_eq!(default_config.randomize_fingerprint_on_launch, None);
|
||||
assert_eq!(default_config.os, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Global singleton instance
|
||||
lazy_static::lazy_static! {
|
||||
static ref CAMOUFOX_LAUNCHER: CamoufoxManager = CamoufoxManager::new();
|
||||
}
|
||||
@@ -220,14 +220,6 @@ impl CookieManager {
|
||||
Err(format!("Cookie database not found at: {}", path.display()))
|
||||
}
|
||||
}
|
||||
"camoufox" => {
|
||||
let path = profile_data_path.join("cookies.sqlite");
|
||||
if path.exists() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(format!("Cookie database not found at: {}", path.display()))
|
||||
}
|
||||
}
|
||||
_ => Err(format!(
|
||||
"Unsupported browser type for cookie operations: {}",
|
||||
profile.browser
|
||||
@@ -253,13 +245,6 @@ impl CookieManager {
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
"camoufox" => {
|
||||
let path = profile_data_path.join("cookies.sqlite");
|
||||
if !path.exists() {
|
||||
Self::create_empty_firefox_cookies_db(&path)?;
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
_ => Err(format!(
|
||||
"Unsupported browser type for cookie operations: {}",
|
||||
profile.browser
|
||||
@@ -318,39 +303,6 @@ impl CookieManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an empty Firefox-format cookies.sqlite database at `path`.
|
||||
fn create_empty_firefox_cookies_db(path: &Path) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create cookie directory: {e}"))?;
|
||||
}
|
||||
let conn =
|
||||
Connection::open(path).map_err(|e| format!("Failed to create cookie database: {e}"))?;
|
||||
conn
|
||||
.execute_batch(
|
||||
"CREATE TABLE moz_cookies (
|
||||
id INTEGER PRIMARY KEY,
|
||||
originAttributes TEXT NOT NULL DEFAULT '',
|
||||
name TEXT,
|
||||
value TEXT,
|
||||
host TEXT,
|
||||
path TEXT,
|
||||
expiry INTEGER,
|
||||
lastAccessed INTEGER,
|
||||
creationTime INTEGER,
|
||||
isSecure INTEGER,
|
||||
isHttpOnly INTEGER,
|
||||
inBrowserElement INTEGER DEFAULT 0,
|
||||
sameSite INTEGER DEFAULT 0,
|
||||
rawSameSite INTEGER DEFAULT 0,
|
||||
schemeMap INTEGER DEFAULT 0,
|
||||
CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)
|
||||
);",
|
||||
)
|
||||
.map_err(|e| format!("Failed to initialize cookie database schema: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert Chrome timestamp (Windows epoch, microseconds) to Unix timestamp (seconds)
|
||||
fn chrome_time_to_unix(chrome_time: i64) -> i64 {
|
||||
if chrome_time == 0 {
|
||||
@@ -367,40 +319,6 @@ impl CookieManager {
|
||||
(unix_time + Self::WINDOWS_EPOCH_DIFF) * 1_000_000
|
||||
}
|
||||
|
||||
/// Read cookies from a Firefox/Camoufox profile
|
||||
fn read_firefox_cookies(db_path: &Path) -> Result<Vec<UnifiedCookie>, String> {
|
||||
let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT name, value, host, path, expiry, isSecure, isHttpOnly,
|
||||
sameSite, creationTime, lastAccessed
|
||||
FROM moz_cookies",
|
||||
)
|
||||
.map_err(|e| format!("Failed to prepare statement: {e}"))?;
|
||||
|
||||
let cookies = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(UnifiedCookie {
|
||||
name: row.get(0)?,
|
||||
value: row.get(1)?,
|
||||
domain: row.get(2)?,
|
||||
path: row.get(3)?,
|
||||
expires: row.get(4)?,
|
||||
is_secure: row.get::<_, i32>(5)? != 0,
|
||||
is_http_only: row.get::<_, i32>(6)? != 0,
|
||||
same_site: row.get(7)?,
|
||||
creation_time: row.get::<_, i64>(8)? / 1_000_000,
|
||||
last_accessed: row.get::<_, i64>(9)? / 1_000_000,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Failed to query cookies: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Failed to collect cookies: {e}"))?;
|
||||
|
||||
Ok(cookies)
|
||||
}
|
||||
|
||||
/// Read cookies from a Chrome/Wayfern profile.
|
||||
/// Handles encrypted cookies by decrypting encrypted_value using the profile's encryption key.
|
||||
fn read_chrome_cookies(
|
||||
@@ -464,98 +382,6 @@ impl CookieManager {
|
||||
Ok(cookies)
|
||||
}
|
||||
|
||||
/// Write cookies to a Firefox/Camoufox profile.
|
||||
///
|
||||
/// Firefox's `moz_cookies.expiry` is "seconds since Unix epoch", so `expiry = 0`
|
||||
/// is interpreted as 1970-01-01 and purged on read. To let imported session
|
||||
/// cookies survive browser restart, we rewrite them to a far-future expiry.
|
||||
///
|
||||
/// `schemeMap` is a bitfield (1 = HTTP, 2 = HTTPS, 3 = both). Setting it based
|
||||
/// on `is_secure` preserves Firefox's scheme-bound cookie enforcement.
|
||||
fn write_firefox_cookies(
|
||||
db_path: &Path,
|
||||
cookies: &[UnifiedCookie],
|
||||
) -> Result<(usize, usize), String> {
|
||||
let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?;
|
||||
|
||||
let mut copied = 0;
|
||||
let mut replaced = 0;
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
// Session cookies get 30 days of persistence so they survive restart.
|
||||
let session_cookie_expiry = now + 30 * 86400;
|
||||
|
||||
for cookie in cookies {
|
||||
let expiry = if cookie.expires > 0 {
|
||||
cookie.expires
|
||||
} else {
|
||||
session_cookie_expiry
|
||||
};
|
||||
// schemeMap bitfield: 1 = HTTP, 2 = HTTPS
|
||||
let scheme_map: i32 = if cookie.is_secure { 2 } else { 1 };
|
||||
|
||||
let existing: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT id FROM moz_cookies WHERE host = ?1 AND name = ?2 AND path = ?3",
|
||||
params![&cookie.domain, &cookie.name, &cookie.path],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.ok();
|
||||
|
||||
if existing.is_some() {
|
||||
conn
|
||||
.execute(
|
||||
"UPDATE moz_cookies SET value = ?1, expiry = ?2, isSecure = ?3,
|
||||
isHttpOnly = ?4, sameSite = ?5, rawSameSite = ?5,
|
||||
lastAccessed = ?6, schemeMap = ?7
|
||||
WHERE host = ?8 AND name = ?9 AND path = ?10",
|
||||
params![
|
||||
&cookie.value,
|
||||
expiry,
|
||||
cookie.is_secure as i32,
|
||||
cookie.is_http_only as i32,
|
||||
cookie.same_site,
|
||||
cookie.last_accessed * 1_000_000,
|
||||
scheme_map,
|
||||
&cookie.domain,
|
||||
&cookie.name,
|
||||
&cookie.path,
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to update cookie: {e}"))?;
|
||||
replaced += 1;
|
||||
} else {
|
||||
conn
|
||||
.execute(
|
||||
"INSERT INTO moz_cookies
|
||||
(originAttributes, name, value, host, path, expiry, lastAccessed,
|
||||
creationTime, isSecure, isHttpOnly, sameSite, rawSameSite, schemeMap)
|
||||
VALUES ('', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11)",
|
||||
params![
|
||||
&cookie.name,
|
||||
&cookie.value,
|
||||
&cookie.domain,
|
||||
&cookie.path,
|
||||
expiry,
|
||||
cookie.last_accessed * 1_000_000,
|
||||
cookie.creation_time * 1_000_000,
|
||||
cookie.is_secure as i32,
|
||||
cookie.is_http_only as i32,
|
||||
cookie.same_site,
|
||||
scheme_map,
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert cookie: {e}"))?;
|
||||
copied += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((copied, replaced))
|
||||
}
|
||||
|
||||
/// Write cookies to a Chrome/Wayfern profile.
|
||||
///
|
||||
/// Always writes values as plaintext in the `value` column with an empty
|
||||
@@ -578,8 +404,7 @@ impl CookieManager {
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
// Session cookies get 30 days of persistence so they survive restart,
|
||||
// mirroring write_firefox_cookies. A login imported from another browser is
|
||||
// Session cookies get 30 days of persistence so they survive restart.
|
||||
// routinely exported as a session cookie; writing it as memory-only
|
||||
// (is_persistent = 0) makes Chromium drop it on the next flush, so the
|
||||
// imported account silently signs out on relaunch. Persisting it with a real
|
||||
@@ -684,7 +509,6 @@ impl CookieManager {
|
||||
let db_path = Self::get_cookie_db_path(profile, &profiles_dir)?;
|
||||
|
||||
let cookies = match profile.browser.as_str() {
|
||||
"camoufox" => Self::read_firefox_cookies(&db_path)?,
|
||||
"wayfern" => {
|
||||
let key = Self::get_chrome_encryption_key(profile, &profiles_dir);
|
||||
Self::read_chrome_cookies(&db_path, key.as_ref())?
|
||||
@@ -791,10 +615,6 @@ impl CookieManager {
|
||||
let conn = Self::open_cookie_db_readonly(&db_path)?;
|
||||
|
||||
let (count_sql, domain_sql) = match profile.browser.as_str() {
|
||||
"camoufox" => (
|
||||
"SELECT COUNT(*) FROM moz_cookies",
|
||||
"SELECT host, COUNT(*) FROM moz_cookies GROUP BY host ORDER BY COUNT(*) DESC, host ASC",
|
||||
),
|
||||
"wayfern" => (
|
||||
"SELECT COUNT(*) FROM cookies",
|
||||
"SELECT host_key, COUNT(*) FROM cookies GROUP BY host_key ORDER BY COUNT(*) DESC, host_key ASC",
|
||||
@@ -869,7 +689,6 @@ impl CookieManager {
|
||||
|
||||
let source_db_path = Self::get_cookie_db_path(source, &profiles_dir)?;
|
||||
let all_cookies = match source.browser.as_str() {
|
||||
"camoufox" => Self::read_firefox_cookies(&source_db_path)?,
|
||||
"wayfern" => {
|
||||
let key = Self::get_chrome_encryption_key(source, &profiles_dir);
|
||||
Self::read_chrome_cookies(&source_db_path, key.as_ref())?
|
||||
@@ -941,7 +760,6 @@ impl CookieManager {
|
||||
};
|
||||
|
||||
let write_result = match target.browser.as_str() {
|
||||
"camoufox" => Self::write_firefox_cookies(&target_db_path, &cookies_to_copy),
|
||||
"wayfern" => Self::write_chrome_cookies(&target_db_path, &cookies_to_copy),
|
||||
_ => {
|
||||
results.push(CookieCopyResult {
|
||||
@@ -1207,7 +1025,6 @@ impl CookieManager {
|
||||
let db_path = Self::ensure_cookie_db_path(profile, &profiles_dir)?;
|
||||
|
||||
let write_result = match profile.browser.as_str() {
|
||||
"camoufox" => Self::write_firefox_cookies(&db_path, &cookies),
|
||||
"wayfern" => Self::write_chrome_cookies(&db_path, &cookies),
|
||||
_ => return Err(format!("Unsupported browser type: {}", profile.browser)),
|
||||
};
|
||||
@@ -1504,33 +1321,6 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Set up a minimal Firefox moz_cookies SQLite schema for testing writes.
|
||||
fn create_firefox_cookies_db(path: &Path) {
|
||||
let conn = Connection::open(path).unwrap();
|
||||
conn
|
||||
.execute_batch(
|
||||
"CREATE TABLE moz_cookies (
|
||||
id INTEGER PRIMARY KEY,
|
||||
originAttributes TEXT NOT NULL DEFAULT '',
|
||||
name TEXT,
|
||||
value TEXT,
|
||||
host TEXT,
|
||||
path TEXT,
|
||||
expiry INTEGER,
|
||||
lastAccessed INTEGER,
|
||||
creationTime INTEGER,
|
||||
isSecure INTEGER,
|
||||
isHttpOnly INTEGER,
|
||||
inBrowserElement INTEGER DEFAULT 0,
|
||||
sameSite INTEGER DEFAULT 0,
|
||||
rawSameSite INTEGER DEFAULT 0,
|
||||
schemeMap INTEGER DEFAULT 0,
|
||||
CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_chrome_cookies_stores_plaintext_values() {
|
||||
let tmp = std::env::temp_dir().join(format!("donut_cookie_test_{}.db", uuid::Uuid::new_v4()));
|
||||
@@ -1692,168 +1482,6 @@ mod tests {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
|
||||
/// Wayfern → Camoufox: write cookies to a Chrome DB, read them back, and
|
||||
/// verify they land in a Firefox DB with values intact, correct schemeMap,
|
||||
/// and non-expired timestamps. This is the path exercised by the
|
||||
/// "copy cookies between profiles of different browser types" feature.
|
||||
#[test]
|
||||
fn test_wayfern_cookies_transfer_to_camoufox() {
|
||||
let chrome_db =
|
||||
std::env::temp_dir().join(format!("donut_xbrowser_chrome_{}.db", uuid::Uuid::new_v4()));
|
||||
let ff_db = std::env::temp_dir().join(format!("donut_xbrowser_ff_{}.db", uuid::Uuid::new_v4()));
|
||||
create_chrome_cookies_db(&chrome_db);
|
||||
create_firefox_cookies_db(&ff_db);
|
||||
|
||||
// Simulate cookies in a Wayfern profile: a persistent cookie and a
|
||||
// session cookie, both from a real-world HTTPS site.
|
||||
let source_cookies = vec![
|
||||
UnifiedCookie {
|
||||
name: "c_user".to_string(),
|
||||
value: "100012345678".to_string(),
|
||||
domain: ".facebook.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 1900000000, // persistent, far in the future
|
||||
is_secure: true,
|
||||
is_http_only: true,
|
||||
same_site: 0,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
},
|
||||
UnifiedCookie {
|
||||
name: "xs".to_string(),
|
||||
value: "sessionvalue".to_string(),
|
||||
domain: ".facebook.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 0, // session cookie
|
||||
is_secure: true,
|
||||
is_http_only: true,
|
||||
same_site: 1,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
},
|
||||
];
|
||||
CookieManager::write_chrome_cookies(&chrome_db, &source_cookies).unwrap();
|
||||
|
||||
// Read back from the Chrome DB (as if reading from the Wayfern profile).
|
||||
let from_chrome = CookieManager::read_chrome_cookies(&chrome_db, None).unwrap();
|
||||
assert_eq!(from_chrome.len(), 2);
|
||||
let c_user_src = from_chrome.iter().find(|c| c.name == "c_user").unwrap();
|
||||
assert_eq!(c_user_src.value, "100012345678");
|
||||
let xs_src = from_chrome.iter().find(|c| c.name == "xs").unwrap();
|
||||
assert_eq!(xs_src.value, "sessionvalue");
|
||||
|
||||
// Write them into the Camoufox (Firefox) DB.
|
||||
let (inserted, replaced) = CookieManager::write_firefox_cookies(&ff_db, &from_chrome).unwrap();
|
||||
assert_eq!(inserted, 2);
|
||||
assert_eq!(replaced, 0);
|
||||
|
||||
// Read back from Firefox and verify values survived the round trip.
|
||||
let from_ff = CookieManager::read_firefox_cookies(&ff_db).unwrap();
|
||||
assert_eq!(from_ff.len(), 2);
|
||||
let c_user = from_ff.iter().find(|c| c.name == "c_user").unwrap();
|
||||
assert_eq!(c_user.value, "100012345678");
|
||||
assert_eq!(c_user.domain, ".facebook.com");
|
||||
assert!(c_user.is_secure);
|
||||
assert!(c_user.is_http_only);
|
||||
let xs = from_ff.iter().find(|c| c.name == "xs").unwrap();
|
||||
assert_eq!(xs.value, "sessionvalue");
|
||||
|
||||
// Raw DB checks against the Firefox schema — these would catch the bugs
|
||||
// on the Chrome path (plaintext, correct expiry, correct schemeMap).
|
||||
let conn = Connection::open(&ff_db).unwrap();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
let (c_user_expiry, c_user_scheme): (i64, i32) = conn
|
||||
.query_row(
|
||||
"SELECT expiry, schemeMap FROM moz_cookies WHERE name = ?1",
|
||||
params!["c_user"],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
c_user_expiry > now,
|
||||
"persistent cookie must not be expired in firefox (expiry={c_user_expiry}, now={now})"
|
||||
);
|
||||
assert_eq!(c_user_scheme, 2, "HTTPS cookie must have schemeMap=2");
|
||||
|
||||
let (xs_expiry, xs_scheme): (i64, i32) = conn
|
||||
.query_row(
|
||||
"SELECT expiry, schemeMap FROM moz_cookies WHERE name = ?1",
|
||||
params!["xs"],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
xs_expiry > now,
|
||||
"session cookie must be rewritten to a future expiry (got {xs_expiry}, now={now})"
|
||||
);
|
||||
assert_eq!(xs_scheme, 2);
|
||||
|
||||
let _ = std::fs::remove_file(&chrome_db);
|
||||
let _ = std::fs::remove_file(&ff_db);
|
||||
}
|
||||
|
||||
/// Camoufox → Wayfern: the reverse direction. Ensures the Chrome writer
|
||||
/// still produces plaintext values / empty encrypted_value when fed cookies
|
||||
/// that originated in Firefox.
|
||||
#[test]
|
||||
fn test_camoufox_cookies_transfer_to_wayfern() {
|
||||
let ff_db =
|
||||
std::env::temp_dir().join(format!("donut_xbrowser_rev_ff_{}.db", uuid::Uuid::new_v4()));
|
||||
let chrome_db = std::env::temp_dir().join(format!(
|
||||
"donut_xbrowser_rev_chrome_{}.db",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
create_firefox_cookies_db(&ff_db);
|
||||
create_chrome_cookies_db(&chrome_db);
|
||||
|
||||
let source_cookies = vec![UnifiedCookie {
|
||||
name: "sessionid".to_string(),
|
||||
value: "abc123def456".to_string(),
|
||||
domain: ".example.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 1900000000,
|
||||
is_secure: true,
|
||||
is_http_only: false,
|
||||
same_site: 1,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
}];
|
||||
CookieManager::write_firefox_cookies(&ff_db, &source_cookies).unwrap();
|
||||
|
||||
let from_ff = CookieManager::read_firefox_cookies(&ff_db).unwrap();
|
||||
assert_eq!(from_ff.len(), 1);
|
||||
assert_eq!(from_ff[0].value, "abc123def456");
|
||||
|
||||
CookieManager::write_chrome_cookies(&chrome_db, &from_ff).unwrap();
|
||||
|
||||
let from_chrome = CookieManager::read_chrome_cookies(&chrome_db, None).unwrap();
|
||||
assert_eq!(from_chrome.len(), 1);
|
||||
assert_eq!(from_chrome[0].value, "abc123def456");
|
||||
|
||||
// Verify the raw DB state on the Chrome side — plaintext value, empty
|
||||
// encrypted_value, persistent, HTTPS.
|
||||
let conn = Connection::open(&chrome_db).unwrap();
|
||||
let (value, encrypted, is_persistent, source_scheme): (String, Vec<u8>, i32, i32) = conn
|
||||
.query_row(
|
||||
"SELECT value, encrypted_value, is_persistent, source_scheme
|
||||
FROM cookies WHERE name = ?1",
|
||||
params!["sessionid"],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(value, "abc123def456");
|
||||
assert!(encrypted.is_empty());
|
||||
assert_eq!(is_persistent, 1);
|
||||
assert_eq!(source_scheme, 2);
|
||||
|
||||
let _ = std::fs::remove_file(&ff_db);
|
||||
let _ = std::fs::remove_file(&chrome_db);
|
||||
}
|
||||
|
||||
/// Regression: decrypting a real v10-encrypted Chromium cookie with the
|
||||
/// correct PBKDF2 iterations and the `SHA-256(host_key)` integrity-prefix
|
||||
/// strip. Captured from a real Wayfern profile:
|
||||
@@ -1975,37 +1603,4 @@ mod tests {
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Same regression, Firefox side: a fresh Camoufox profile has no
|
||||
/// `cookies.sqlite` until the browser launches.
|
||||
#[test]
|
||||
fn test_create_empty_firefox_cookies_db_then_write() {
|
||||
let dir = std::env::temp_dir().join(format!("donut_empty_ff_{}", uuid::Uuid::new_v4()));
|
||||
let db_path = dir.join("cookies.sqlite");
|
||||
assert!(!db_path.exists());
|
||||
|
||||
CookieManager::create_empty_firefox_cookies_db(&db_path).unwrap();
|
||||
assert!(db_path.exists());
|
||||
|
||||
let cookies = vec![UnifiedCookie {
|
||||
name: "sid".to_string(),
|
||||
value: "ff-session".to_string(),
|
||||
domain: ".example.org".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 1900000000,
|
||||
is_secure: true,
|
||||
is_http_only: false,
|
||||
same_site: 1,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
}];
|
||||
let (inserted, _) = CookieManager::write_firefox_cookies(&db_path, &cookies).unwrap();
|
||||
assert_eq!(inserted, 1);
|
||||
|
||||
let read = CookieManager::read_firefox_cookies(&db_path).unwrap();
|
||||
assert_eq!(read.len(), 1);
|
||||
assert_eq!(read[0].value, "ff-session");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,9 +921,9 @@ impl DownloadedBrowsersRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if GeoIP database is missing for Camoufox profiles
|
||||
// Check if GeoIP database is missing for Wayfern profiles
|
||||
if self.geoip_downloader.check_missing_geoip_database()? {
|
||||
log::info!("GeoIP database is missing for Camoufox profiles, downloading...");
|
||||
log::info!("GeoIP database is missing for Wayfern profiles, downloading...");
|
||||
|
||||
match self
|
||||
.geoip_downloader
|
||||
@@ -931,7 +931,7 @@ impl DownloadedBrowsersRegistry {
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
downloaded.push("GeoIP database for Camoufox".to_string());
|
||||
downloaded.push("GeoIP database".to_string());
|
||||
log::info!("GeoIP database downloaded successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -1265,7 +1265,7 @@ pub async fn ensure_active_browsers_downloaded(
|
||||
let version_manager = crate::browser_version_manager::BrowserVersionManager::instance();
|
||||
let mut downloaded = Vec::new();
|
||||
|
||||
for browser in &["wayfern", "camoufox"] {
|
||||
for browser in &["wayfern"] {
|
||||
// Check if any version is already downloaded
|
||||
let existing = registry.get_downloaded_versions(browser);
|
||||
if !existing.is_empty() {
|
||||
@@ -1300,7 +1300,7 @@ pub async fn ensure_active_browsers_downloaded(
|
||||
// Retry transient failures a few times. Each attempt is wrapped in an overall
|
||||
// timeout so that a hang anywhere in the download pipeline (version resolution,
|
||||
// a stalled stream, extraction) cannot block the next browser forever. This is
|
||||
// the core of the bug fix: Wayfern going first must never starve Camoufox.
|
||||
// the core of the bug fix: Wayfern downloads proceed independently.
|
||||
const MAX_ATTEMPTS: u32 = 3;
|
||||
const ATTEMPT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(600);
|
||||
let mut succeeded = false;
|
||||
@@ -1359,7 +1359,7 @@ pub async fn ensure_active_browsers_downloaded(
|
||||
}
|
||||
|
||||
if !succeeded {
|
||||
// Do NOT abort the whole routine: continue so the next browser (Camoufox)
|
||||
// Do NOT abort the whole routine: continue with remaining browsers
|
||||
// still gets its chance even though this one failed/timed out.
|
||||
log::warn!("Giving up on auto-download of {browser} {version} after {MAX_ATTEMPTS} attempts");
|
||||
}
|
||||
|
||||
+5
-203
@@ -41,7 +41,6 @@ pub struct Downloader {
|
||||
registry: &'static crate::downloaded_browsers_registry::DownloadedBrowsersRegistry,
|
||||
version_service: &'static crate::browser_version_manager::BrowserVersionManager,
|
||||
extractor: &'static crate::extraction::Extractor,
|
||||
geoip_downloader: &'static crate::geoip_downloader::GeoIPDownloader,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
@@ -60,7 +59,6 @@ impl Downloader {
|
||||
registry: crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(),
|
||||
version_service: crate::browser_version_manager::BrowserVersionManager::instance(),
|
||||
extractor: crate::extraction::Extractor::instance(),
|
||||
geoip_downloader: crate::geoip_downloader::GeoIPDownloader::instance(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +74,6 @@ impl Downloader {
|
||||
registry: crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(),
|
||||
version_service: crate::browser_version_manager::BrowserVersionManager::instance(),
|
||||
extractor: crate::extraction::Extractor::instance(),
|
||||
geoip_downloader: crate::geoip_downloader::GeoIPDownloader::instance(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,34 +124,6 @@ impl Downloader {
|
||||
_download_info: &DownloadInfo,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
match browser_type {
|
||||
BrowserType::Camoufox => {
|
||||
// For Camoufox, verify the asset exists and find the correct download URL
|
||||
let releases = self
|
||||
.api_client
|
||||
.fetch_camoufox_releases_with_caching(true)
|
||||
.await?;
|
||||
|
||||
let release = releases
|
||||
.iter()
|
||||
.find(|r| r.tag_name == version)
|
||||
.or_else(|| {
|
||||
log::info!("Camoufox: requested version {version} not found, using latest available");
|
||||
releases.first()
|
||||
})
|
||||
.ok_or("No Camoufox releases found".to_string())?;
|
||||
|
||||
// Get platform and architecture info
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
|
||||
// Find the appropriate asset
|
||||
let asset_url = self
|
||||
.find_camoufox_asset(&release.assets, &os, &arch)
|
||||
.ok_or(format!(
|
||||
"No compatible asset found for Camoufox version {version} on {os}/{arch}"
|
||||
))?;
|
||||
|
||||
Ok(asset_url)
|
||||
}
|
||||
BrowserType::Wayfern => {
|
||||
// For Wayfern, get the download URL from version.json
|
||||
let version_info = self
|
||||
@@ -214,97 +183,6 @@ impl Downloader {
|
||||
(os.to_string(), arch.to_string())
|
||||
}
|
||||
|
||||
/// Find the appropriate Camoufox asset for the current platform and architecture
|
||||
fn find_camoufox_asset(
|
||||
&self,
|
||||
assets: &[crate::browser::GithubAsset],
|
||||
os: &str,
|
||||
arch: &str,
|
||||
) -> Option<String> {
|
||||
// Camoufox asset naming pattern: camoufox-{version}-beta.{number}-{os}.{arch}.zip
|
||||
// Example: camoufox-135.0.1-beta.24-lin.x86_64.zip
|
||||
let (os_name, arch_name) = match (os, arch) {
|
||||
("windows", "x64") => ("win", "x86_64"),
|
||||
("windows", "arm64") => ("win", "arm64"),
|
||||
("linux", "x64") => ("lin", "x86_64"),
|
||||
("linux", "arm64") => ("lin", "arm64"),
|
||||
("macos", "x64") => ("mac", "x86_64"),
|
||||
("macos", "arm64") => ("mac", "arm64"),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Use ends_with for precise matching to avoid false positives
|
||||
// The separator before OS is a dash: -lin.x86_64.zip, -mac.arm64.zip, etc.
|
||||
let pattern = format!("-{os_name}.{arch_name}.zip");
|
||||
let asset = assets.iter().find(|asset| {
|
||||
let name = asset.name.to_lowercase();
|
||||
name.starts_with("camoufox-") && name.ends_with(&pattern)
|
||||
});
|
||||
|
||||
if let Some(asset) = asset {
|
||||
log::info!(
|
||||
"Selected Camoufox asset for {}/{}: {}",
|
||||
os,
|
||||
arch,
|
||||
asset.name
|
||||
);
|
||||
Some(asset.browser_download_url.clone())
|
||||
} else {
|
||||
log::warn!(
|
||||
"No matching Camoufox asset found for {}/{} with pattern '{}'. Available assets: {:?}",
|
||||
os,
|
||||
arch,
|
||||
pattern,
|
||||
assets.iter().map(|a| &a.name).collect::<Vec<_>>()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure version.json exists in the Camoufox installation directory.
|
||||
/// Creates the file if it doesn't exist, using the version from the tag name.
|
||||
async fn ensure_camoufox_version_json(
|
||||
&self,
|
||||
browser_dir: &Path,
|
||||
version: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// The browser_dir is typically: binaries/camoufox/<version>/
|
||||
// Find the executable directory within it
|
||||
let version_json_locations = vec![
|
||||
browser_dir.join("version.json"),
|
||||
browser_dir.join("camoufox").join("version.json"),
|
||||
];
|
||||
|
||||
// Check if version.json already exists in any expected location
|
||||
for location in &version_json_locations {
|
||||
if location.exists() {
|
||||
log::info!("version.json already exists at: {}", location.display());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the Firefox version from the Camoufox version tag
|
||||
// Format: "135.0.1-beta.24" -> Firefox version is "135.0.1" (or just "135.0")
|
||||
let firefox_version = version.split('-').next().unwrap_or(version);
|
||||
|
||||
// Create version.json in the browser directory
|
||||
let version_json_path = browser_dir.join("version.json");
|
||||
let version_data = serde_json::json!({
|
||||
"version": firefox_version
|
||||
});
|
||||
|
||||
let version_json_str = serde_json::to_string_pretty(&version_data)?;
|
||||
tokio::fs::write(&version_json_path, version_json_str).await?;
|
||||
|
||||
log::info!(
|
||||
"Created version.json at {} with Firefox version: {}",
|
||||
version_json_path.display(),
|
||||
firefox_version
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_browser<R: tauri::Runtime>(
|
||||
&self,
|
||||
_app_handle: &tauri::AppHandle<R>,
|
||||
@@ -626,7 +504,7 @@ impl Downloader {
|
||||
return Err("Please accept Wayfern Terms and Conditions before downloading browsers".into());
|
||||
}
|
||||
|
||||
// For Wayfern/Camoufox, resolve the actual available version from the API
|
||||
// For Wayfern, resolve the actual available version from the API
|
||||
let version = if browser_str == "wayfern" {
|
||||
match self
|
||||
.api_client
|
||||
@@ -642,21 +520,6 @@ impl Downloader {
|
||||
}
|
||||
_ => version,
|
||||
}
|
||||
} else if browser_str == "camoufox" {
|
||||
match self
|
||||
.api_client
|
||||
.fetch_camoufox_releases_with_caching(true)
|
||||
.await
|
||||
{
|
||||
Ok(releases) if !releases.is_empty() && releases[0].tag_name != version => {
|
||||
log::info!(
|
||||
"Camoufox: requested {version}, using available {}",
|
||||
releases[0].tag_name
|
||||
);
|
||||
releases[0].tag_name.clone()
|
||||
}
|
||||
_ => version,
|
||||
}
|
||||
} else {
|
||||
version
|
||||
};
|
||||
@@ -681,8 +544,6 @@ impl Downloader {
|
||||
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
|
||||
let browser = create_browser(browser_type.clone());
|
||||
|
||||
// Use injected registry instance
|
||||
|
||||
let binaries_dir = crate::app_dirs::binaries_dir();
|
||||
|
||||
// Check if registry thinks it's downloaded, but also verify files actually exist
|
||||
@@ -899,34 +760,6 @@ impl Downloader {
|
||||
error_details.push_str("\nDirectory does not exist!");
|
||||
}
|
||||
|
||||
// For Camoufox on Linux, provide specific expected files
|
||||
if browser_str == "camoufox" && cfg!(target_os = "linux") {
|
||||
let camoufox_subdir = browser_dir.join("camoufox");
|
||||
error_details.push_str("\nExpected Camoufox executable locations:");
|
||||
error_details.push_str(&format!("\n {}/camoufox-bin", camoufox_subdir.display()));
|
||||
error_details.push_str(&format!("\n {}/camoufox", camoufox_subdir.display()));
|
||||
|
||||
if camoufox_subdir.exists() {
|
||||
error_details.push_str(&format!(
|
||||
"\nCamoufox subdirectory exists: {}",
|
||||
camoufox_subdir.display()
|
||||
));
|
||||
if let Ok(entries) = std::fs::read_dir(&camoufox_subdir) {
|
||||
error_details.push_str("\nFiles in camoufox subdirectory:");
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let file_type = if path.is_dir() { "DIR" } else { "FILE" };
|
||||
error_details.push_str(&format!("\n {} {}", file_type, path.display()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error_details.push_str(&format!(
|
||||
"\nCamoufox subdirectory does not exist: {}",
|
||||
camoufox_subdir.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Do not delete files on verification failure; keep archive for manual retry.
|
||||
let _ = self.registry.remove_browser(&browser_str, &version);
|
||||
let _ = self.registry.save();
|
||||
@@ -979,38 +812,6 @@ impl Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
// If this is Camoufox, automatically download GeoIP database and create version.json
|
||||
if browser_str == "camoufox" {
|
||||
// Check if GeoIP database is already available
|
||||
if !crate::geoip_downloader::GeoIPDownloader::is_geoip_database_available() {
|
||||
log::info!("Downloading GeoIP database for Camoufox...");
|
||||
|
||||
match self
|
||||
.geoip_downloader
|
||||
.download_geoip_database(app_handle)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
log::info!("GeoIP database downloaded successfully");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to download GeoIP database: {e}");
|
||||
// Don't fail the browser download if GeoIP download fails
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("GeoIP database already available");
|
||||
}
|
||||
|
||||
// Create version.json if it doesn't exist
|
||||
if let Err(e) = self
|
||||
.ensure_camoufox_version_json(&browser_dir, &version)
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed to create version.json for Camoufox: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Emit completion
|
||||
let progress = DownloadProgress {
|
||||
browser: browser_str.clone(),
|
||||
@@ -1245,7 +1046,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// A different browser's in-progress state must be left untouched.
|
||||
let other = "camoufox-9.9.9".to_string();
|
||||
let other = "chromium-9.9.9".to_string();
|
||||
{
|
||||
let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap();
|
||||
downloading.insert(other.clone());
|
||||
@@ -1265,12 +1066,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
is_downloading("camoufox", "9.9.9"),
|
||||
is_downloading("chromium", "9.9.9"),
|
||||
"unrelated browser's download state must be preserved"
|
||||
);
|
||||
|
||||
// Cleanup so we don't leak global state into other tests.
|
||||
clear_download_state_for_browser("camoufox");
|
||||
clear_download_state_for_browser("wayfern");
|
||||
clear_download_state_for_browser("chromium");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ mod tests {
|
||||
BrowserProfile {
|
||||
id,
|
||||
name: "test".to_string(),
|
||||
browser: "camoufox".to_string(),
|
||||
browser: "wayfern".to_string(),
|
||||
version: "1.0".to_string(),
|
||||
proxy_id: None,
|
||||
vpn_id: None,
|
||||
@@ -264,7 +264,6 @@ mod tests {
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
camoufox_config: None,
|
||||
wayfern_config: None,
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
|
||||
@@ -27,11 +27,6 @@ pub struct Extension {
|
||||
pub author: Option<String>,
|
||||
#[serde(default)]
|
||||
pub homepage_url: Option<String>,
|
||||
/// Firefox extension ID from `browser_specific_settings.gecko.id` (or
|
||||
/// `applications.gecko.id` in old manifests). Firefox refuses to load a
|
||||
/// sideloaded .xpi unless the filename matches this value.
|
||||
#[serde(default)]
|
||||
pub gecko_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -69,9 +64,7 @@ fn extension_groups_file() -> PathBuf {
|
||||
|
||||
fn determine_browser_compatibility(file_type: &str) -> Vec<String> {
|
||||
match file_type {
|
||||
"xpi" => vec!["firefox".to_string()],
|
||||
"crx" => vec!["chromium".to_string()],
|
||||
"zip" => vec!["chromium".to_string(), "firefox".to_string()],
|
||||
"crx" | "zip" => vec!["chromium".to_string()],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
@@ -79,7 +72,7 @@ fn determine_browser_compatibility(file_type: &str) -> Vec<String> {
|
||||
fn get_file_type(file_name: &str) -> Option<String> {
|
||||
let ext = file_name.rsplit('.').next()?.to_lowercase();
|
||||
match ext.as_str() {
|
||||
"xpi" | "crx" | "zip" => Some(ext),
|
||||
"crx" | "zip" => Some(ext),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -162,32 +155,6 @@ fn extract_manifest_metadata(
|
||||
(name, version, description, author, homepage_url)
|
||||
}
|
||||
|
||||
/// Read `browser_specific_settings.gecko.id` (or the legacy
|
||||
/// `applications.gecko.id`) from the extension's manifest.json. Firefox uses
|
||||
/// this value as the canonical add-on ID; sideloaded .xpi files must be named
|
||||
/// `<gecko_id>.xpi` to be picked up.
|
||||
fn extract_gecko_id(file_data: &[u8], file_type: &str) -> Option<String> {
|
||||
let zip_start = if file_type == "crx" {
|
||||
find_zip_start(file_data)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let cursor = std::io::Cursor::new(&file_data[zip_start..]);
|
||||
let mut archive = zip::ZipArchive::new(cursor).ok()?;
|
||||
let mut manifest_content = String::new();
|
||||
std::io::Read::read_to_string(
|
||||
&mut archive.by_name("manifest.json").ok()?,
|
||||
&mut manifest_content,
|
||||
)
|
||||
.ok()?;
|
||||
let manifest: serde_json::Value = serde_json::from_str(&manifest_content).ok()?;
|
||||
manifest
|
||||
.pointer("/browser_specific_settings/gecko/id")
|
||||
.or_else(|| manifest.pointer("/applications/gecko/id"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn extract_icon_from_archive(file_data: &[u8], file_type: &str) -> Option<(Vec<u8>, String)> {
|
||||
let zip_start = if file_type == "crx" {
|
||||
find_zip_start(file_data)
|
||||
@@ -305,6 +272,9 @@ impl ExtensionManager {
|
||||
get_file_type(&file_name).ok_or_else(|| format!("Unsupported file type: {file_name}"))?;
|
||||
|
||||
let browser_compatibility = determine_browser_compatibility(&file_type);
|
||||
if browser_compatibility.is_empty() {
|
||||
return Err(format!("Unsupported file type: {file_name}").into());
|
||||
}
|
||||
let now = now_secs();
|
||||
|
||||
let (manifest_name, version, description, author, homepage_url) =
|
||||
@@ -316,7 +286,6 @@ impl ExtensionManager {
|
||||
name
|
||||
};
|
||||
|
||||
let gecko_id = extract_gecko_id(&file_data, &file_type);
|
||||
let ext = Extension {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: final_name,
|
||||
@@ -331,7 +300,6 @@ impl ExtensionManager {
|
||||
description,
|
||||
author,
|
||||
homepage_url,
|
||||
gecko_id,
|
||||
};
|
||||
|
||||
let file_dir = self.get_file_dir(&ext.id);
|
||||
@@ -448,7 +416,6 @@ impl ExtensionManager {
|
||||
ext.name = mn;
|
||||
}
|
||||
}
|
||||
ext.gecko_id = extract_gecko_id(&data, &new_file_type);
|
||||
|
||||
if let Some((icon_data, icon_ext)) = extract_icon_from_archive(&data, &new_file_type) {
|
||||
let icon_path = self.get_extension_dir(id).join(format!("icon.{icon_ext}"));
|
||||
@@ -863,7 +830,6 @@ impl ExtensionManager {
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let group = self.get_group(group_id)?;
|
||||
let browser_type = match browser {
|
||||
"camoufox" => "firefox",
|
||||
"wayfern" => "chromium",
|
||||
_ => return Err(format!("Extensions are not supported for browser '{browser}'").into()),
|
||||
};
|
||||
@@ -892,7 +858,7 @@ impl ExtensionManager {
|
||||
pub fn install_extensions_for_profile(
|
||||
&self,
|
||||
profile: &crate::profile::BrowserProfile,
|
||||
profile_data_path: &std::path::Path,
|
||||
_profile_data_path: &std::path::Path,
|
||||
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||
let group_id = match &profile.extension_group_id {
|
||||
Some(id) => id,
|
||||
@@ -904,91 +870,40 @@ impl ExtensionManager {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let browser_type = match profile.browser.as_str() {
|
||||
"camoufox" => "firefox",
|
||||
"wayfern" => "chromium",
|
||||
_ => return Ok(Vec::new()),
|
||||
};
|
||||
if profile.browser.as_str() != "wayfern" {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut extension_paths = Vec::new();
|
||||
|
||||
match browser_type {
|
||||
"firefox" => {
|
||||
let extensions_dir = profile_data_path.join("extensions");
|
||||
// Clear existing extensions
|
||||
if extensions_dir.exists() {
|
||||
fs::remove_dir_all(&extensions_dir)?;
|
||||
// Unpack Chromium extensions and return paths for --load-extension
|
||||
let unpacked_base = extensions_base_dir().join("unpacked");
|
||||
if unpacked_base.exists() {
|
||||
fs::remove_dir_all(&unpacked_base)?;
|
||||
}
|
||||
fs::create_dir_all(&unpacked_base)?;
|
||||
|
||||
for ext_id in &group.extension_ids {
|
||||
if let Ok(ext) = self.get_extension(ext_id) {
|
||||
if !ext.browser_compatibility.contains(&"chromium".to_string()) {
|
||||
continue;
|
||||
}
|
||||
fs::create_dir_all(&extensions_dir)?;
|
||||
let src_file = self.get_file_dir(ext_id).join(&ext.file_name);
|
||||
if src_file.exists() {
|
||||
let unpack_dir = unpacked_base.join(ext_id);
|
||||
fs::create_dir_all(&unpack_dir)?;
|
||||
|
||||
for ext_id in &group.extension_ids {
|
||||
if let Ok(ext) = self.get_extension(ext_id) {
|
||||
if !ext.browser_compatibility.contains(&"firefox".to_string()) {
|
||||
continue;
|
||||
// Extract .crx or .zip
|
||||
match Self::unpack_extension(&src_file, &unpack_dir) {
|
||||
Ok(()) => {
|
||||
extension_paths.push(unpack_dir.to_string_lossy().to_string());
|
||||
}
|
||||
let src_file = self.get_file_dir(ext_id).join(&ext.file_name);
|
||||
if !src_file.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Firefox/Camoufox only loads sideloaded .xpi files whose filename
|
||||
// matches `browser_specific_settings.gecko.id` from the manifest.
|
||||
// Prefer the cached value; fall back to reading the manifest now
|
||||
// for extensions added before the field existed.
|
||||
let gecko_id = if let Some(ref id) = ext.gecko_id {
|
||||
Some(id.clone())
|
||||
} else if let Ok(data) = fs::read(&src_file) {
|
||||
extract_gecko_id(&data, &ext.file_type)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(gecko_id) = gecko_id else {
|
||||
log::warn!(
|
||||
"Skipping Firefox extension '{}': could not determine gecko id from manifest.json",
|
||||
ext.name
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let dest = extensions_dir.join(format!("{gecko_id}.xpi"));
|
||||
fs::copy(&src_file, &dest)?;
|
||||
extension_paths.push(dest.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
"chromium" => {
|
||||
// For Chromium, unpack extensions and return paths for --load-extension
|
||||
let unpacked_base = extensions_base_dir().join("unpacked");
|
||||
if unpacked_base.exists() {
|
||||
fs::remove_dir_all(&unpacked_base)?;
|
||||
}
|
||||
fs::create_dir_all(&unpacked_base)?;
|
||||
|
||||
for ext_id in &group.extension_ids {
|
||||
if let Ok(ext) = self.get_extension(ext_id) {
|
||||
if !ext.browser_compatibility.contains(&"chromium".to_string()) {
|
||||
continue;
|
||||
}
|
||||
let src_file = self.get_file_dir(ext_id).join(&ext.file_name);
|
||||
if src_file.exists() {
|
||||
let unpack_dir = unpacked_base.join(ext_id);
|
||||
fs::create_dir_all(&unpack_dir)?;
|
||||
|
||||
// Extract .crx or .zip
|
||||
match Self::unpack_extension(&src_file, &unpack_dir) {
|
||||
Ok(()) => {
|
||||
extension_paths.push(unpack_dir.to_string_lossy().to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to unpack extension '{}': {}", ext.name, e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to unpack extension '{}': {}", ext.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(extension_paths)
|
||||
@@ -1066,10 +981,8 @@ impl ExtensionManager {
|
||||
}
|
||||
|
||||
let needs_meta_backfill = ext.version.is_none() && ext.description.is_none();
|
||||
let needs_gecko_backfill =
|
||||
ext.gecko_id.is_none() && ext.browser_compatibility.iter().any(|b| b == "firefox");
|
||||
|
||||
if needs_meta_backfill || needs_gecko_backfill {
|
||||
if needs_meta_backfill {
|
||||
let file_path = file_dir.join(&ext.file_name);
|
||||
if let Ok(file_data) = fs::read(&file_path) {
|
||||
let mut updated_ext = ext.clone();
|
||||
@@ -1100,13 +1013,6 @@ impl ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
if needs_gecko_backfill {
|
||||
if let Some(gid) = extract_gecko_id(&file_data, &ext.file_type) {
|
||||
updated_ext.gecko_id = Some(gid);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
let metadata_path = self.get_metadata_path(&ext.id);
|
||||
if let Ok(json) = serde_json::to_string_pretty(&updated_ext) {
|
||||
@@ -1321,27 +1227,24 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_get_file_type() {
|
||||
assert_eq!(get_file_type("ublock.xpi"), Some("xpi".to_string()));
|
||||
assert_eq!(get_file_type("ext.crx"), Some("crx".to_string()));
|
||||
assert_eq!(get_file_type("ext.zip"), Some("zip".to_string()));
|
||||
assert_eq!(get_file_type("ublock.xpi"), None);
|
||||
assert_eq!(get_file_type("readme.txt"), None);
|
||||
assert_eq!(get_file_type("noext"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_browser_compatibility() {
|
||||
assert_eq!(
|
||||
determine_browser_compatibility("xpi"),
|
||||
vec!["firefox".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
determine_browser_compatibility("crx"),
|
||||
vec!["chromium".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
determine_browser_compatibility("zip"),
|
||||
vec!["chromium".to_string(), "firefox".to_string()]
|
||||
vec!["chromium".to_string()]
|
||||
);
|
||||
assert_eq!(determine_browser_compatibility("xpi"), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1359,13 +1262,13 @@ mod tests {
|
||||
let ext = mgr
|
||||
.add_extension(
|
||||
"Test Ext".to_string(),
|
||||
"test.xpi".to_string(),
|
||||
"test.zip".to_string(),
|
||||
vec![0, 1, 2, 3],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(ext.name, "Test Ext");
|
||||
assert_eq!(ext.file_type, "xpi");
|
||||
assert_eq!(ext.browser_compatibility, vec!["firefox".to_string()]);
|
||||
assert_eq!(ext.file_type, "zip");
|
||||
assert_eq!(ext.browser_compatibility, vec!["chromium".to_string()]);
|
||||
|
||||
// Get
|
||||
let fetched = mgr.get_extension(&ext.id).unwrap();
|
||||
@@ -1407,7 +1310,7 @@ mod tests {
|
||||
let ext = mgr
|
||||
.add_extension(
|
||||
"Test Ext".to_string(),
|
||||
"test.xpi".to_string(),
|
||||
"test.zip".to_string(),
|
||||
vec![0, 1, 2, 3],
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1432,26 +1335,21 @@ mod tests {
|
||||
|
||||
let mgr = ExtensionManager::new();
|
||||
|
||||
let ext = mgr
|
||||
let chrome_ext = mgr
|
||||
.add_extension(
|
||||
"Firefox Ext".to_string(),
|
||||
"test.xpi".to_string(),
|
||||
"Chromium Ext".to_string(),
|
||||
"test.crx".to_string(),
|
||||
vec![0, 1, 2, 3],
|
||||
)
|
||||
.unwrap();
|
||||
let chrome_group = mgr.create_group("Chromium Group".to_string()).unwrap();
|
||||
mgr
|
||||
.add_extension_to_group(&chrome_group.id, &chrome_ext.id)
|
||||
.unwrap();
|
||||
|
||||
let group = mgr.create_group("Firefox Group".to_string()).unwrap();
|
||||
mgr.add_extension_to_group(&group.id, &ext.id).unwrap();
|
||||
|
||||
// Compatible with camoufox (firefox-based)
|
||||
assert!(mgr
|
||||
.validate_group_compatibility(&group.id, "camoufox")
|
||||
.validate_group_compatibility(&chrome_group.id, "wayfern")
|
||||
.is_ok());
|
||||
|
||||
// Incompatible with wayfern (chromium-based)
|
||||
assert!(mgr
|
||||
.validate_group_compatibility(&group.id, "wayfern")
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1474,7 +1372,7 @@ mod tests {
|
||||
let mgr = ExtensionManager::new();
|
||||
|
||||
let ext = mgr
|
||||
.add_extension("Test".to_string(), "test.xpi".to_string(), vec![0, 1, 2, 3])
|
||||
.add_extension("Test".to_string(), "test.zip".to_string(), vec![0, 1, 2, 3])
|
||||
.unwrap();
|
||||
|
||||
let group = mgr.create_group("G1".to_string()).unwrap();
|
||||
|
||||
@@ -67,17 +67,15 @@ impl Extractor {
|
||||
exe_path: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Determine browser type from the destination directory path
|
||||
let browser_type = if dest_dir.to_string_lossy().contains("camoufox") {
|
||||
"camoufox"
|
||||
} else if dest_dir.to_string_lossy().contains("wayfern") {
|
||||
let browser_type = if dest_dir.to_string_lossy().contains("wayfern") {
|
||||
"wayfern"
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// For Camoufox and Wayfern on Linux, we expect the executable directly under version directory
|
||||
// e.g., binaries/camoufox/<version>/camoufox, without an extra subdirectory
|
||||
if browser_type == "camoufox" || browser_type == "wayfern" {
|
||||
// For Wayfern on Linux, we expect the executable directly under version directory
|
||||
// e.g., binaries/wayfern/<version>/wayfern, without an extra subdirectory
|
||||
if browser_type == "wayfern" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -977,13 +975,7 @@ impl Extractor {
|
||||
);
|
||||
|
||||
// Look for .exe files, preferring main browser executables
|
||||
let priority_exe_names = [
|
||||
"firefox.exe",
|
||||
"chrome.exe",
|
||||
"chromium.exe",
|
||||
"camoufox.exe",
|
||||
"wayfern.exe",
|
||||
];
|
||||
let priority_exe_names = ["firefox.exe", "chrome.exe", "chromium.exe", "wayfern.exe"];
|
||||
|
||||
// First try priority executable names
|
||||
for exe_name in &priority_exe_names {
|
||||
@@ -1049,7 +1041,6 @@ impl Extractor {
|
||||
|| file_name.contains("chrome")
|
||||
|| file_name.contains("chromium")
|
||||
|| file_name.contains("browser")
|
||||
|| file_name.contains("camoufox")
|
||||
|| file_name.contains("wayfern")
|
||||
{
|
||||
return Ok(path);
|
||||
@@ -1098,7 +1089,7 @@ impl Extractor {
|
||||
|
||||
// Enhanced list of common browser executable names
|
||||
let exe_names = [
|
||||
// Firefox variants (used by Camoufox)
|
||||
// Firefox variants
|
||||
"firefox",
|
||||
"firefox-bin",
|
||||
// Chrome/Chromium variants (used by Wayfern)
|
||||
@@ -1106,10 +1097,6 @@ impl Extractor {
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
"chromium-bin",
|
||||
// Camoufox variants
|
||||
"camoufox",
|
||||
"camoufox-bin",
|
||||
"camoufox-browser",
|
||||
// Wayfern variants
|
||||
"wayfern",
|
||||
"wayfern-bin",
|
||||
@@ -1136,16 +1123,13 @@ impl Extractor {
|
||||
"firefox",
|
||||
"chrome",
|
||||
"chromium",
|
||||
"camoufox",
|
||||
"wayfern",
|
||||
".",
|
||||
"./",
|
||||
"Browser",
|
||||
"browser",
|
||||
"opt/camoufox",
|
||||
"usr/lib/firefox",
|
||||
"usr/lib/chromium",
|
||||
"usr/lib/camoufox",
|
||||
"usr/share/applications",
|
||||
"usr/bin",
|
||||
"AppRun",
|
||||
@@ -1239,7 +1223,6 @@ impl Extractor {
|
||||
|| name_lower.contains("chrome")
|
||||
|| name_lower.contains("brave")
|
||||
|| name_lower.contains("zen")
|
||||
|| name_lower.contains("camoufox")
|
||||
|| name_lower.contains("wayfern")
|
||||
|| name_lower.ends_with(".appimage")
|
||||
|| !name_lower.contains('.')
|
||||
@@ -1295,7 +1278,6 @@ impl Extractor {
|
||||
|| name_lower.contains("chrome")
|
||||
|| name_lower.contains("brave")
|
||||
|| name_lower.contains("zen")
|
||||
|| name_lower.contains("camoufox")
|
||||
|| name_lower.contains("wayfern")
|
||||
|| file_name.ends_with(".AppImage")
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::browser::GithubRelease;
|
||||
use crate::events;
|
||||
use crate::profile::manager::ProfileManager;
|
||||
use directories::BaseDirs;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
@@ -46,30 +45,12 @@ impl GeoIPDownloader {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
fn get_cache_dir() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let base_dirs = BaseDirs::new().ok_or("Failed to determine base directories")?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let cache_dir = base_dirs
|
||||
.data_local_dir()
|
||||
.join("camoufox")
|
||||
.join("camoufox")
|
||||
.join("Cache");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
Ok(cache_dir)
|
||||
fn get_cache_dir() -> PathBuf {
|
||||
crate::app_dirs::cache_dir()
|
||||
}
|
||||
|
||||
fn get_mmdb_file_path() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(Self::get_cache_dir()?.join("GeoLite2-City.mmdb"))
|
||||
pub fn get_mmdb_file_path() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(Self::get_cache_dir().join("GeoLite2-City.mmdb"))
|
||||
}
|
||||
|
||||
pub fn is_geoip_database_available() -> bool {
|
||||
@@ -100,7 +81,7 @@ impl GeoIPDownloader {
|
||||
now.saturating_sub(timestamp) > SEVEN_DAYS
|
||||
}
|
||||
|
||||
/// Check if GeoIP database is missing or stale for Camoufox profiles
|
||||
/// Check if GeoIP database is missing or stale for Wayfern fingerprint geo.
|
||||
pub fn check_missing_geoip_database(
|
||||
&self,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
@@ -108,9 +89,9 @@ impl GeoIPDownloader {
|
||||
.list_profiles()
|
||||
.map_err(|e| format!("Failed to list profiles: {e}"))?;
|
||||
|
||||
let has_camoufox_profiles = profiles.iter().any(|profile| profile.browser == "camoufox");
|
||||
let needs_geoip = profiles.iter().any(|profile| profile.browser == "wayfern");
|
||||
|
||||
if has_camoufox_profiles {
|
||||
if needs_geoip {
|
||||
return Ok(!Self::is_geoip_database_available() || Self::is_geoip_stale());
|
||||
}
|
||||
|
||||
@@ -169,7 +150,7 @@ impl GeoIPDownloader {
|
||||
.ok_or("No compatible GeoIP database asset found")?;
|
||||
|
||||
// Create cache directory
|
||||
let cache_dir = Self::get_cache_dir()?;
|
||||
let cache_dir = Self::get_cache_dir();
|
||||
fs::create_dir_all(&cache_dir).await?;
|
||||
|
||||
let mmdb_path = Self::get_mmdb_file_path()?;
|
||||
@@ -406,10 +387,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_get_cache_dir() {
|
||||
let cache_dir = GeoIPDownloader::get_cache_dir();
|
||||
assert!(cache_dir.is_ok());
|
||||
|
||||
let path = cache_dir.unwrap();
|
||||
assert!(path.to_string_lossy().contains("camoufox"));
|
||||
assert!(!cache_dir.as_os_str().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
//! Geolocation support for Camoufox fingerprinting.
|
||||
//!
|
||||
//! This module provides IP-based geolocation lookup using the MaxMind GeoLite2 database,
|
||||
//! IP-based geolocation lookup using the MaxMind GeoLite2 database,
|
||||
//! and locale generation based on country/territory information.
|
||||
|
||||
use crate::camoufox::data;
|
||||
use crate::geoip_downloader::GeoIPDownloader;
|
||||
use directories::BaseDirs;
|
||||
use maxminddb::{geoip2, Reader};
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::Reader as XmlReader;
|
||||
use rand::RngExt;
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
// Re-export IP utilities for backward compatibility
|
||||
pub use crate::ip_utils::{fetch_public_ip, is_ipv4, is_ipv6, validate_ip, IpError};
|
||||
const TERRITORY_INFO_XML: &str = include_str!("territory_info.xml");
|
||||
|
||||
pub use crate::ip_utils::IpError;
|
||||
|
||||
/// Geolocation error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GeolocationError {
|
||||
#[error("GeoIP database not found. Please download it first.")]
|
||||
@@ -42,23 +37,17 @@ pub enum GeolocationError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Network error: {0}")]
|
||||
Network(String),
|
||||
|
||||
#[error("IP error: {0}")]
|
||||
Ip(#[from] IpError),
|
||||
}
|
||||
|
||||
/// Locale information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Locale {
|
||||
pub language: String,
|
||||
pub region: Option<String>,
|
||||
pub script: Option<String>,
|
||||
}
|
||||
|
||||
impl Locale {
|
||||
/// Format locale as a string (e.g., "en-US").
|
||||
pub fn as_string(&self) -> String {
|
||||
if let Some(region) = &self.region {
|
||||
format!("{}-{}", self.language, region)
|
||||
@@ -66,84 +55,30 @@ impl Locale {
|
||||
self.language.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to config format for Camoufox.
|
||||
pub fn as_config(&self) -> HashMap<String, serde_json::Value> {
|
||||
let mut config = HashMap::new();
|
||||
|
||||
if let Some(region) = &self.region {
|
||||
config.insert(
|
||||
"locale:region".to_string(),
|
||||
serde_json::json!(region.to_uppercase()),
|
||||
);
|
||||
}
|
||||
|
||||
config.insert(
|
||||
"locale:language".to_string(),
|
||||
serde_json::json!(self.language.clone()),
|
||||
);
|
||||
|
||||
if let Some(script) = &self.script {
|
||||
config.insert("locale:script".to_string(), serde_json::json!(script));
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// Geolocation information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Geolocation {
|
||||
pub locale: Locale,
|
||||
pub longitude: f64,
|
||||
pub latitude: f64,
|
||||
pub timezone: String,
|
||||
pub accuracy: Option<f64>,
|
||||
}
|
||||
|
||||
impl Geolocation {
|
||||
/// Convert to config format for Camoufox.
|
||||
pub fn as_config(&self) -> HashMap<String, serde_json::Value> {
|
||||
let mut config = self.locale.as_config();
|
||||
|
||||
config.insert(
|
||||
"geolocation:longitude".to_string(),
|
||||
serde_json::json!(self.longitude),
|
||||
);
|
||||
config.insert(
|
||||
"geolocation:latitude".to_string(),
|
||||
serde_json::json!(self.latitude),
|
||||
);
|
||||
config.insert("timezone".to_string(), serde_json::json!(self.timezone));
|
||||
|
||||
if let Some(accuracy) = self.accuracy {
|
||||
config.insert(
|
||||
"geolocation:accuracy".to_string(),
|
||||
serde_json::json!(accuracy),
|
||||
);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// Territory language population data.
|
||||
struct LanguagePopulation {
|
||||
language: String,
|
||||
population_percent: f64,
|
||||
}
|
||||
|
||||
/// Statistical locale selector based on territory language populations.
|
||||
pub struct LocaleSelector {
|
||||
territories: HashMap<String, Vec<LanguagePopulation>>,
|
||||
}
|
||||
|
||||
impl LocaleSelector {
|
||||
/// Create a new locale selector by parsing territory info XML.
|
||||
pub fn new() -> Result<Self, GeolocationError> {
|
||||
let mut territories: HashMap<String, Vec<LanguagePopulation>> = HashMap::new();
|
||||
|
||||
let mut reader = XmlReader::from_str(data::TERRITORY_INFO_XML);
|
||||
let mut reader = XmlReader::from_str(TERRITORY_INFO_XML);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut current_territory: Option<String> = None;
|
||||
@@ -158,14 +93,12 @@ impl LocaleSelector {
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
if name_str == "territory" {
|
||||
// Save previous territory if exists
|
||||
if let Some(code) = current_territory.take() {
|
||||
if !current_languages.is_empty() {
|
||||
territories.insert(code, std::mem::take(&mut current_languages));
|
||||
}
|
||||
}
|
||||
|
||||
// Get territory type attribute
|
||||
for attr in e.attributes().flatten() {
|
||||
if attr.key.as_ref() == b"type" {
|
||||
current_territory = Some(String::from_utf8_lossy(&attr.value).to_uppercase());
|
||||
@@ -199,7 +132,6 @@ impl LocaleSelector {
|
||||
let name_ref = e.name();
|
||||
let name = std::str::from_utf8(name_ref.as_ref()).unwrap_or("");
|
||||
if name == "territory" {
|
||||
// Save territory
|
||||
if let Some(code) = current_territory.take() {
|
||||
if !current_languages.is_empty() {
|
||||
territories.insert(code, std::mem::take(&mut current_languages));
|
||||
@@ -220,7 +152,7 @@ impl LocaleSelector {
|
||||
Ok(Self { territories })
|
||||
}
|
||||
|
||||
/// Get a locale for a given region/country code.
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn from_region(&self, region: &str) -> Result<Locale, GeolocationError> {
|
||||
let region_upper = region.to_uppercase();
|
||||
|
||||
@@ -233,7 +165,6 @@ impl LocaleSelector {
|
||||
return Err(GeolocationError::NoLanguageData(region.to_string()));
|
||||
}
|
||||
|
||||
// Weighted random selection based on population percentage
|
||||
let total: f64 = languages.iter().map(|l| l.population_percent).sum();
|
||||
let mut rng = rand::rng();
|
||||
let target = rng.random::<f64>() * total;
|
||||
@@ -249,7 +180,6 @@ impl LocaleSelector {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first language
|
||||
let first_lang = &languages[0].language;
|
||||
Ok(normalize_locale(&format!(
|
||||
"{}-{}",
|
||||
@@ -266,8 +196,6 @@ impl Default for LocaleSelector {
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize a locale string to standard format.
|
||||
/// Handles formats like "en-US", "zh-Hant-US", "zh-Hans-CN".
|
||||
fn normalize_locale(locale: &str) -> Locale {
|
||||
let parts: Vec<&str> = locale.split('-').collect();
|
||||
|
||||
@@ -276,73 +204,22 @@ fn normalize_locale(locale: &str) -> Locale {
|
||||
.map(|s| s.to_lowercase())
|
||||
.unwrap_or_else(|| "en".to_string());
|
||||
|
||||
// A 4-letter part is a script subtag (e.g. "Hant", "Hans", "Cyrl").
|
||||
// A 2-letter or 3-digit part is a region subtag (e.g. "US", "CN").
|
||||
let mut explicit_script: Option<String> = None;
|
||||
let mut region: Option<String> = None;
|
||||
let mut region = None;
|
||||
|
||||
for part in parts.iter().skip(1) {
|
||||
if part.len() == 4 && part.chars().all(|c| c.is_ascii_alphabetic()) {
|
||||
explicit_script = Some(part[..1].to_uppercase() + &part[1..].to_lowercase());
|
||||
} else {
|
||||
region = Some(part.to_uppercase());
|
||||
// Script subtag (e.g. Hans/Hant) — ignored; Wayfern fingerprint uses language+region only.
|
||||
continue;
|
||||
}
|
||||
region = Some(part.to_uppercase());
|
||||
}
|
||||
|
||||
let script = if explicit_script.is_some() {
|
||||
explicit_script
|
||||
} else {
|
||||
match language.as_str() {
|
||||
"zh" => {
|
||||
if region.as_deref() == Some("TW") || region.as_deref() == Some("HK") {
|
||||
Some("Hant".to_string())
|
||||
} else {
|
||||
Some("Hans".to_string())
|
||||
}
|
||||
}
|
||||
"sr" => Some("Cyrl".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
Locale {
|
||||
language,
|
||||
region,
|
||||
script,
|
||||
}
|
||||
Locale { language, region }
|
||||
}
|
||||
|
||||
/// Get the path to the GeoIP MMDB file.
|
||||
fn get_mmdb_path() -> Result<PathBuf, GeolocationError> {
|
||||
let base_dirs = BaseDirs::new().ok_or(GeolocationError::DatabaseNotFound)?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let cache_dir = base_dirs
|
||||
.data_local_dir()
|
||||
.join("camoufox")
|
||||
.join("camoufox")
|
||||
.join("Cache");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
Ok(cache_dir.join("GeoLite2-City.mmdb"))
|
||||
}
|
||||
|
||||
/// Check if the GeoIP database is available.
|
||||
pub fn is_geoip_available() -> bool {
|
||||
GeoIPDownloader::is_geoip_database_available()
|
||||
}
|
||||
|
||||
/// Get geolocation information for an IP address.
|
||||
pub fn get_geolocation(ip: &str) -> Result<Geolocation, GeolocationError> {
|
||||
let mmdb_path = get_mmdb_path()?;
|
||||
let mmdb_path =
|
||||
GeoIPDownloader::get_mmdb_file_path().map_err(|_| GeolocationError::DatabaseNotFound)?;
|
||||
|
||||
if !mmdb_path.exists() {
|
||||
return Err(GeolocationError::DatabaseNotFound);
|
||||
@@ -362,7 +239,6 @@ pub fn get_geolocation(ip: &str) -> Result<Geolocation, GeolocationError> {
|
||||
.map_err(|e| GeolocationError::LocationNotFound(e.to_string()))?
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound(ip.to_string()))?;
|
||||
|
||||
// Extract location data
|
||||
let location = &city.location;
|
||||
|
||||
let longitude = location
|
||||
@@ -376,14 +252,12 @@ pub fn get_geolocation(ip: &str) -> Result<Geolocation, GeolocationError> {
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound("No timezone".to_string()))?
|
||||
.to_string();
|
||||
|
||||
// Get country code
|
||||
let country = &city.country;
|
||||
let iso_code = country
|
||||
.iso_code
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound("No country code".to_string()))?
|
||||
.to_uppercase();
|
||||
|
||||
// Get locale from territory data
|
||||
let selector = LocaleSelector::new()?;
|
||||
let locale = selector.from_region(&iso_code)?;
|
||||
|
||||
@@ -392,7 +266,6 @@ pub fn get_geolocation(ip: &str) -> Result<Geolocation, GeolocationError> {
|
||||
longitude,
|
||||
latitude,
|
||||
timezone,
|
||||
accuracy: location.accuracy_radius.map(|r| r as f64),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -410,7 +283,6 @@ mod tests {
|
||||
fn test_locale_from_region() {
|
||||
let selector = LocaleSelector::new().unwrap();
|
||||
|
||||
// Test common regions
|
||||
let us_locale = selector.from_region("US");
|
||||
assert!(us_locale.is_ok());
|
||||
let us = us_locale.unwrap();
|
||||
@@ -427,14 +299,12 @@ mod tests {
|
||||
let locale = Locale {
|
||||
language: "en".to_string(),
|
||||
region: Some("US".to_string()),
|
||||
script: None,
|
||||
};
|
||||
assert_eq!(locale.as_string(), "en-US");
|
||||
|
||||
let locale_no_region = Locale {
|
||||
language: "en".to_string(),
|
||||
region: None,
|
||||
script: None,
|
||||
};
|
||||
assert_eq!(locale_no_region.as_string(), "en");
|
||||
}
|
||||
@@ -444,25 +314,13 @@ mod tests {
|
||||
let locale = normalize_locale("en-US");
|
||||
assert_eq!(locale.language, "en");
|
||||
assert_eq!(locale.region, Some("US".to_string()));
|
||||
assert!(locale.script.is_none());
|
||||
|
||||
let zh_tw = normalize_locale("zh-TW");
|
||||
assert_eq!(zh_tw.language, "zh");
|
||||
assert_eq!(zh_tw.region, Some("TW".to_string()));
|
||||
assert_eq!(zh_tw.script, Some("Hant".to_string()));
|
||||
|
||||
let zh_cn = normalize_locale("zh-CN");
|
||||
assert_eq!(zh_cn.script, Some("Hans".to_string()));
|
||||
|
||||
// 3-part locale: language-script-region
|
||||
let zh_hant_us = normalize_locale("zh-Hant-US");
|
||||
assert_eq!(zh_hant_us.language, "zh");
|
||||
assert_eq!(zh_hant_us.region, Some("US".to_string()));
|
||||
assert_eq!(zh_hant_us.script, Some("Hant".to_string()));
|
||||
|
||||
let zh_hans_us = normalize_locale("zh-Hans-US");
|
||||
assert_eq!(zh_hans_us.language, "zh");
|
||||
assert_eq!(zh_hans_us.region, Some("US".to_string()));
|
||||
assert_eq!(zh_hans_us.script, Some("Hans".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,6 @@ use std::str::FromStr;
|
||||
pub enum IpError {
|
||||
#[error("Network error: {0}")]
|
||||
Network(String),
|
||||
|
||||
#[error("Invalid IP address: {0}")]
|
||||
InvalidIP(String),
|
||||
}
|
||||
|
||||
/// Validate an IP address (IPv4 or IPv6).
|
||||
@@ -20,24 +17,6 @@ pub fn validate_ip(ip: &str) -> bool {
|
||||
IpAddr::from_str(ip).is_ok()
|
||||
}
|
||||
|
||||
/// Check if an IP is IPv4.
|
||||
pub fn is_ipv4(ip: &str) -> bool {
|
||||
if let Ok(addr) = IpAddr::from_str(ip) {
|
||||
addr.is_ipv4()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an IP is IPv6.
|
||||
pub fn is_ipv6(ip: &str) -> bool {
|
||||
if let Ok(addr) = IpAddr::from_str(ip) {
|
||||
addr.is_ipv6()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch public IP address, optionally through a proxy.
|
||||
pub async fn fetch_public_ip(proxy: Option<&str>) -> Result<String, IpError> {
|
||||
let urls = [
|
||||
@@ -114,18 +93,4 @@ mod tests {
|
||||
assert!(!validate_ip("invalid"));
|
||||
assert!(!validate_ip("256.256.256.256"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_ipv4() {
|
||||
assert!(is_ipv4("8.8.8.8"));
|
||||
assert!(!is_ipv4("2001:4860:4860::8888"));
|
||||
assert!(!is_ipv4("invalid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_ipv6() {
|
||||
assert!(is_ipv6("2001:4860:4860::8888"));
|
||||
assert!(!is_ipv6("8.8.8.8"));
|
||||
assert!(!is_ipv6("invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-39
@@ -22,8 +22,6 @@ mod auto_updater;
|
||||
mod browser;
|
||||
mod browser_runner;
|
||||
mod browser_version_manager;
|
||||
pub mod camoufox;
|
||||
mod camoufox_manager;
|
||||
mod default_browser;
|
||||
pub mod dns_blocklist;
|
||||
mod downloaded_browsers_registry;
|
||||
@@ -32,6 +30,7 @@ mod ephemeral_dirs;
|
||||
mod extension_manager;
|
||||
mod extraction;
|
||||
mod geoip_downloader;
|
||||
mod geolocation;
|
||||
mod group_manager;
|
||||
mod human_typing;
|
||||
mod ip_utils;
|
||||
@@ -69,10 +68,9 @@ use browser_runner::{
|
||||
|
||||
use profile::manager::{
|
||||
check_browser_status, clone_profile, create_browser_profile_new, delete_profile,
|
||||
list_browser_profiles, rename_profile, update_camoufox_config, update_profile_dns_blocklist,
|
||||
update_profile_launch_hook, update_profile_note, update_profile_proxy,
|
||||
update_profile_proxy_bypass_rules, update_profile_tags, update_profile_vpn,
|
||||
update_profile_window_color, update_wayfern_config,
|
||||
list_browser_profiles, rename_profile, update_profile_dns_blocklist, update_profile_launch_hook,
|
||||
update_profile_note, update_profile_proxy, update_profile_proxy_bypass_rules,
|
||||
update_profile_tags, update_profile_vpn, update_profile_window_color, update_wayfern_config,
|
||||
};
|
||||
|
||||
use profile::password::{
|
||||
@@ -1188,7 +1186,6 @@ async fn generate_sample_fingerprint(
|
||||
launch_hook: None,
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
camoufox_config: None,
|
||||
wayfern_config: None,
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
@@ -1209,15 +1206,7 @@ async fn generate_sample_fingerprint(
|
||||
updated_at: None,
|
||||
};
|
||||
|
||||
if browser == "camoufox" {
|
||||
let config: crate::camoufox_manager::CamoufoxConfig =
|
||||
serde_json::from_str(&config_json).map_err(|e| format!("Failed to parse config: {e}"))?;
|
||||
let manager = crate::camoufox_manager::CamoufoxManager::instance();
|
||||
manager
|
||||
.generate_fingerprint_config(&app_handle, &temp_profile, &config)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to generate fingerprint: {e}"))
|
||||
} else if browser == "wayfern" {
|
||||
if browser == "wayfern" {
|
||||
let config: crate::wayfern_manager::WayfernConfig =
|
||||
serde_json::from_str(&config_json).map_err(|e| format!("Failed to parse config: {e}"))?;
|
||||
let manager = crate::wayfern_manager::WayfernManager::instance();
|
||||
@@ -1849,26 +1838,6 @@ pub fn run() {
|
||||
}
|
||||
});
|
||||
|
||||
// Start Camoufox cleanup task
|
||||
let _app_handle_cleanup = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let camoufox_manager = crate::camoufox_manager::CamoufoxManager::instance();
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
match camoufox_manager.cleanup_dead_instances().await {
|
||||
Ok(_) => {
|
||||
// Cleanup completed silently
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error during Camoufox cleanup: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check and download GeoIP database at startup if needed
|
||||
let app_handle_geoip = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
@@ -1879,7 +1848,7 @@ pub fn run() {
|
||||
match geoip_downloader.check_missing_geoip_database() {
|
||||
Ok(true) => {
|
||||
log::info!(
|
||||
"GeoIP database is missing for Camoufox profiles, downloading at startup..."
|
||||
"GeoIP database is missing for Wayfern profiles, downloading at startup..."
|
||||
);
|
||||
let geoip_downloader = GeoIPDownloader::instance();
|
||||
if let Err(e) = geoip_downloader
|
||||
@@ -1892,7 +1861,7 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
// No Camoufox profiles or GeoIP database already available
|
||||
// No Wayfern profiles or GeoIP database already available
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check GeoIP database status at startup: {e}");
|
||||
@@ -2301,7 +2270,6 @@ pub fn run() {
|
||||
import_proxies_json,
|
||||
parse_txt_proxies,
|
||||
import_proxies_from_parsed,
|
||||
update_camoufox_config,
|
||||
update_wayfern_config,
|
||||
generate_sample_fingerprint,
|
||||
get_profile_groups,
|
||||
|
||||
+28
-75
@@ -507,7 +507,7 @@ impl McpServer {
|
||||
vec![
|
||||
McpTool {
|
||||
name: "list_profiles".to_string(),
|
||||
description: "List all Wayfern and Camoufox browser profiles".to_string(),
|
||||
description: "List all Wayfern browser profiles".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
@@ -614,7 +614,7 @@ impl McpServer {
|
||||
},
|
||||
"browser": {
|
||||
"type": "string",
|
||||
"enum": ["wayfern", "camoufox"],
|
||||
"enum": ["wayfern"],
|
||||
"description": "Browser engine to use"
|
||||
},
|
||||
"proxy_id": {
|
||||
@@ -1049,7 +1049,7 @@ impl McpServer {
|
||||
// Fingerprint management tools
|
||||
McpTool {
|
||||
name: "get_profile_fingerprint".to_string(),
|
||||
description: "Get the fingerprint configuration for a Wayfern or Camoufox profile"
|
||||
description: "Get the fingerprint configuration for a Wayfern profile"
|
||||
.to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
@@ -1065,7 +1065,7 @@ impl McpServer {
|
||||
McpTool {
|
||||
name: "update_profile_fingerprint".to_string(),
|
||||
description:
|
||||
"Update the fingerprint configuration for a Wayfern or Camoufox profile. Requires an active Pro subscription."
|
||||
"Update the fingerprint configuration for a Wayfern profile. Requires an active Pro subscription."
|
||||
.to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
@@ -1208,7 +1208,7 @@ impl McpServer {
|
||||
// Cookie management tools
|
||||
McpTool {
|
||||
name: "import_profile_cookies".to_string(),
|
||||
description: "Import cookies into a Wayfern or Camoufox profile from a JSON array (Puppeteer / EditThisCookie format) or a Netscape cookies.txt. Format is auto-detected. The browser must not be running.".to_string(),
|
||||
description: "Import cookies into a Wayfern profile from a JSON array (Puppeteer / EditThisCookie format) or a Netscape cookies.txt. Format is auto-detected. The browser must not be running.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1902,11 +1902,9 @@ impl McpServer {
|
||||
message: format!("Failed to list profiles: {e}"),
|
||||
})?;
|
||||
|
||||
// Filter to only Wayfern and Camoufox profiles
|
||||
let filtered: Vec<&BrowserProfile> = profiles
|
||||
.iter()
|
||||
.filter(|p| p.browser == "wayfern" || p.browser == "camoufox")
|
||||
.collect();
|
||||
// Filter to only Wayfern profiles
|
||||
let filtered: Vec<&BrowserProfile> =
|
||||
profiles.iter().filter(|p| p.browser == "wayfern").collect();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"content": [{
|
||||
@@ -1943,11 +1941,11 @@ impl McpServer {
|
||||
message: format!("Profile not found: {profile_id}"),
|
||||
})?;
|
||||
|
||||
// Check if it's a Wayfern or Camoufox profile
|
||||
if profile.browser != "wayfern" && profile.browser != "camoufox" {
|
||||
// Check if it's a Wayfern profile
|
||||
if profile.browser != "wayfern" {
|
||||
return Err(McpError {
|
||||
code: -32000,
|
||||
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
|
||||
message: "MCP only supports Wayfern profiles".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2000,11 +1998,11 @@ impl McpServer {
|
||||
message: format!("Profile not found: {profile_id}"),
|
||||
})?;
|
||||
|
||||
// Check if it's a Wayfern or Camoufox profile
|
||||
if profile.browser != "wayfern" && profile.browser != "camoufox" {
|
||||
// Check if it's a Wayfern profile
|
||||
if profile.browser != "wayfern" {
|
||||
return Err(McpError {
|
||||
code: -32000,
|
||||
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
|
||||
message: "MCP only supports Wayfern profiles".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2082,11 +2080,11 @@ impl McpServer {
|
||||
message: format!("Profile not found: {profile_id}"),
|
||||
})?;
|
||||
|
||||
// Check if it's a Wayfern or Camoufox profile
|
||||
if profile.browser != "wayfern" && profile.browser != "camoufox" {
|
||||
// Check if it's a Wayfern profile
|
||||
if profile.browser != "wayfern" {
|
||||
return Err(McpError {
|
||||
code: -32000,
|
||||
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
|
||||
message: "MCP only supports Wayfern profiles".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2173,9 +2171,9 @@ impl McpServer {
|
||||
lines.push(format!("{profile_id}: not found"));
|
||||
continue;
|
||||
};
|
||||
if profile.browser != "wayfern" && profile.browser != "camoufox" {
|
||||
if profile.browser != "wayfern" {
|
||||
lines.push(format!(
|
||||
"{profile_id}: unsupported browser (MCP supports Wayfern/Camoufox)"
|
||||
"{profile_id}: unsupported browser (MCP supports Wayfern)"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
@@ -2298,10 +2296,10 @@ impl McpServer {
|
||||
message: "Missing browser".to_string(),
|
||||
})?;
|
||||
|
||||
if browser != "wayfern" && browser != "camoufox" {
|
||||
if browser != "wayfern" {
|
||||
return Err(McpError {
|
||||
code: -32602,
|
||||
message: "browser must be 'wayfern' or 'camoufox'".to_string(),
|
||||
message: "browser must be 'wayfern'".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2350,7 +2348,6 @@ impl McpServer {
|
||||
proxy_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
group_id,
|
||||
false,
|
||||
None,
|
||||
@@ -2594,11 +2591,11 @@ impl McpServer {
|
||||
message: format!("Profile not found: {profile_id}"),
|
||||
})?;
|
||||
|
||||
// Check if it's a Wayfern or Camoufox profile
|
||||
if profile.browser != "wayfern" && profile.browser != "camoufox" {
|
||||
// Check if it's a Wayfern profile
|
||||
if profile.browser != "wayfern" {
|
||||
return Err(McpError {
|
||||
code: -32000,
|
||||
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
|
||||
message: "MCP only supports Wayfern profiles".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3507,23 +3504,6 @@ impl McpServer {
|
||||
})?;
|
||||
|
||||
let fingerprint_info = match profile.browser.as_str() {
|
||||
"camoufox" => {
|
||||
let config = profile
|
||||
.camoufox_config
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
serde_json::json!({
|
||||
"browser": "camoufox",
|
||||
"fingerprint": config.fingerprint,
|
||||
"os": config.os,
|
||||
"randomize_fingerprint_on_launch": config.randomize_fingerprint_on_launch,
|
||||
"screen_max_width": config.screen_max_width,
|
||||
"screen_max_height": config.screen_max_height,
|
||||
"screen_min_width": config.screen_min_width,
|
||||
"screen_min_height": config.screen_min_height,
|
||||
})
|
||||
}
|
||||
"wayfern" => {
|
||||
let config = profile.wayfern_config.as_ref().cloned().unwrap_or_default();
|
||||
serde_json::json!({
|
||||
@@ -3540,7 +3520,7 @@ impl McpServer {
|
||||
_ => {
|
||||
return Err(McpError {
|
||||
code: -32000,
|
||||
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
|
||||
message: "MCP only supports Wayfern profiles".to_string(),
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -3612,29 +3592,6 @@ impl McpServer {
|
||||
})?;
|
||||
|
||||
match profile.browser.as_str() {
|
||||
"camoufox" => {
|
||||
let mut config = profile
|
||||
.camoufox_config
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if let Some(fp) = fingerprint {
|
||||
config.fingerprint = Some(fp.to_string());
|
||||
}
|
||||
if let Some(os_val) = os {
|
||||
config.os = Some(os_val.to_string());
|
||||
}
|
||||
if let Some(r) = randomize {
|
||||
config.randomize_fingerprint_on_launch = Some(r);
|
||||
}
|
||||
ProfileManager::instance()
|
||||
.update_camoufox_config(app_handle.clone(), profile_id, config)
|
||||
.await
|
||||
.map_err(|e| McpError {
|
||||
code: -32000,
|
||||
message: format!("Failed to update camoufox config: {e}"),
|
||||
})?;
|
||||
}
|
||||
"wayfern" => {
|
||||
let mut config = profile.wayfern_config.as_ref().cloned().unwrap_or_default();
|
||||
if let Some(fp) = fingerprint {
|
||||
@@ -3657,7 +3614,7 @@ impl McpServer {
|
||||
_ => {
|
||||
return Err(McpError {
|
||||
code: -32000,
|
||||
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
|
||||
message: "MCP only supports Wayfern profiles".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4007,10 +3964,6 @@ impl McpServer {
|
||||
crate::wayfern_manager::WayfernManager::instance()
|
||||
.get_cdp_port(&profile_path_str)
|
||||
.await
|
||||
} else if profile.browser == "camoufox" {
|
||||
crate::camoufox_manager::CamoufoxManager::instance()
|
||||
.get_cdp_port(&profile_path_str)
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -4409,10 +4362,10 @@ impl McpServer {
|
||||
message: format!("Profile not found: {profile_id}"),
|
||||
})?;
|
||||
|
||||
if profile.browser != "wayfern" && profile.browser != "camoufox" {
|
||||
if profile.browser != "wayfern" {
|
||||
return Err(McpError {
|
||||
code: -32000,
|
||||
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
|
||||
message: "MCP only supports Wayfern profiles".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use std::process::Command;
|
||||
fn cmd_matches_profile_path(cmd: &[std::ffi::OsString], profile_path: &str) -> bool {
|
||||
let args: Vec<&str> = cmd.iter().filter_map(|a| a.to_str()).collect();
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
// Exact argument equality (Firefox/Camoufox: `-profile <path>`; some launchers
|
||||
// Exact argument equality (Firefox: `-profile <path>`; some launchers
|
||||
// pass the path as its own arg).
|
||||
if *arg == profile_path {
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::api_client::is_browser_version_nightly;
|
||||
use crate::browser::{create_browser, BrowserType, ProxySettings};
|
||||
use crate::camoufox_manager::CamoufoxConfig;
|
||||
use crate::browser::{create_browser, BrowserType};
|
||||
use crate::cloud_auth::CLOUD_AUTH;
|
||||
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
|
||||
use crate::events;
|
||||
@@ -27,14 +25,12 @@ fn atomic_write(path: &Path, data: &[u8]) -> std::io::Result<()> {
|
||||
}
|
||||
|
||||
pub struct ProfileManager {
|
||||
camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager,
|
||||
wayfern_manager: &'static crate::wayfern_manager::WayfernManager,
|
||||
}
|
||||
|
||||
impl ProfileManager {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(),
|
||||
wayfern_manager: crate::wayfern_manager::WayfernManager::instance(),
|
||||
}
|
||||
}
|
||||
@@ -80,7 +76,6 @@ impl ProfileManager {
|
||||
release_type: &str,
|
||||
proxy_id: Option<String>,
|
||||
vpn_id: Option<String>,
|
||||
camoufox_config: Option<CamoufoxConfig>,
|
||||
wayfern_config: Option<WayfernConfig>,
|
||||
group_id: Option<String>,
|
||||
ephemeral: bool,
|
||||
@@ -103,6 +98,14 @@ impl ProfileManager {
|
||||
|
||||
log::info!("Attempting to create profile: {name}");
|
||||
|
||||
if browser == "camoufox" {
|
||||
return Err(
|
||||
serde_json::json!({ "code": "CAMOUFOX_REMOVED" })
|
||||
.to_string()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if a profile with this name already exists (case insensitive)
|
||||
let existing_profiles = self.list_profiles()?;
|
||||
if existing_profiles
|
||||
@@ -125,113 +128,6 @@ impl ProfileManager {
|
||||
create_dir_all(&profile_data_dir)?;
|
||||
}
|
||||
|
||||
// For Camoufox profiles, generate fingerprint during creation
|
||||
let final_camoufox_config = if browser == "camoufox" {
|
||||
let mut config = camoufox_config.unwrap_or_else(|| {
|
||||
log::info!("Creating default Camoufox config for profile: {name}");
|
||||
crate::camoufox_manager::CamoufoxConfig::default()
|
||||
});
|
||||
|
||||
// Pass upstream proxy information to config for fingerprint generation
|
||||
if let Some(proxy_id_ref) = &proxy_id {
|
||||
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
|
||||
// For fingerprint generation, pass upstream proxy directly with credentials if present
|
||||
let proxy_url = if let (Some(username), Some(password)) =
|
||||
(&proxy_settings.username, &proxy_settings.password)
|
||||
{
|
||||
format!(
|
||||
"{}://{}:{}@{}:{}",
|
||||
proxy_settings.proxy_type.to_lowercase(),
|
||||
username,
|
||||
password,
|
||||
proxy_settings.host,
|
||||
proxy_settings.port
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}://{}:{}",
|
||||
proxy_settings.proxy_type.to_lowercase(),
|
||||
proxy_settings.host,
|
||||
proxy_settings.port
|
||||
)
|
||||
};
|
||||
config.proxy = Some(proxy_url);
|
||||
log::info!(
|
||||
"Using upstream proxy for Camoufox fingerprint generation: {}://{}:{}",
|
||||
proxy_settings.proxy_type.to_lowercase(),
|
||||
proxy_settings.host,
|
||||
proxy_settings.port
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate fingerprint if not already provided
|
||||
if config.fingerprint.is_none() {
|
||||
log::info!("Generating fingerprint for Camoufox profile: {name}");
|
||||
|
||||
// Use the camoufox launcher to generate the config
|
||||
|
||||
// Create a temporary profile for fingerprint generation
|
||||
let temp_profile = BrowserProfile {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: name.to_string(),
|
||||
browser: browser.to_string(),
|
||||
version: version.to_string(),
|
||||
proxy_id: proxy_id.clone(),
|
||||
vpn_id: None,
|
||||
launch_hook: launch_hook.clone(),
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: release_type.to_string(),
|
||||
camoufox_config: None,
|
||||
wayfern_config: None,
|
||||
group_id: group_id.clone(),
|
||||
tags: Vec::new(),
|
||||
note: None,
|
||||
window_color: None,
|
||||
sync_mode: SyncMode::Disabled,
|
||||
encryption_salt: None,
|
||||
last_sync: None,
|
||||
host_os: None,
|
||||
ephemeral: false,
|
||||
extension_group_id: None,
|
||||
proxy_bypass_rules: Vec::new(),
|
||||
created_by_id: None,
|
||||
created_by_email: None,
|
||||
dns_blocklist: None,
|
||||
password_protected: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
};
|
||||
|
||||
match self
|
||||
.camoufox_manager
|
||||
.generate_fingerprint_config(app_handle, &temp_profile, &config)
|
||||
.await
|
||||
{
|
||||
Ok(generated_fingerprint) => {
|
||||
config.fingerprint = Some(generated_fingerprint);
|
||||
log::info!("Successfully generated fingerprint for profile: {name}");
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(
|
||||
format!("Failed to generate fingerprint for Camoufox profile '{name}': {e}").into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("Using provided fingerprint for Camoufox profile: {name}");
|
||||
}
|
||||
|
||||
// Clear the proxy from config after fingerprint generation
|
||||
// Browser launch should always use local proxy, never direct to upstream
|
||||
config.proxy = None;
|
||||
|
||||
Some(config)
|
||||
} else {
|
||||
camoufox_config.clone()
|
||||
};
|
||||
|
||||
// For Wayfern profiles, generate fingerprint during creation
|
||||
let final_wayfern_config = if browser == "wayfern" {
|
||||
let mut config = wayfern_config.unwrap_or_else(|| {
|
||||
@@ -288,7 +184,6 @@ impl ProfileManager {
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: release_type.to_string(),
|
||||
camoufox_config: None,
|
||||
wayfern_config: None,
|
||||
group_id: group_id.clone(),
|
||||
tags: Vec::new(),
|
||||
@@ -360,7 +255,6 @@ impl ProfileManager {
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: release_type.to_string(),
|
||||
camoufox_config: final_camoufox_config,
|
||||
wayfern_config: final_wayfern_config,
|
||||
group_id: group_id.clone(),
|
||||
tags: Vec::new(),
|
||||
@@ -398,31 +292,6 @@ impl ProfileManager {
|
||||
|
||||
log::info!("Profile '{name}' created successfully with ID: {profile_id}");
|
||||
|
||||
// `apply_proxy_settings_to_profile` writes a Firefox-style user.js
|
||||
// with the upstream proxy host. That is wrong for both supported
|
||||
// browser types:
|
||||
// - Camoufox: camoufox_manager rewrites user.js at every launch with
|
||||
// the local donut-proxy host; writing the upstream here leaves a
|
||||
// stale, wrong proxy in user.js until the next launch.
|
||||
// - Wayfern: Chromium gets its proxy via `--proxy-pac-url=` at launch
|
||||
// (see wayfern_manager.rs) and never reads user.js.
|
||||
// So we only call it for any unrecognized browser type that might be
|
||||
// a true Firefox-family target (none currently). Ephemeral profiles
|
||||
// skip regardless because their data dir is created at launch time.
|
||||
if !ephemeral && !matches!(browser, "camoufox" | "wayfern") {
|
||||
if let Some(proxy_id_ref) = &proxy_id {
|
||||
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
|
||||
self.apply_proxy_settings_to_profile(&profile_data_dir, &proxy_settings, None)?;
|
||||
} else {
|
||||
// Proxy ID provided but not found, disable proxy
|
||||
self.disable_proxy_settings_in_profile(&profile_data_dir)?;
|
||||
}
|
||||
} else {
|
||||
// Create user.js with common Firefox preferences but no proxy
|
||||
self.disable_proxy_settings_in_profile(&profile_data_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit profile creation event
|
||||
if let Err(e) = events::emit_empty("profiles-changed") {
|
||||
log::warn!("Warning: Failed to emit profiles-changed event: {e}");
|
||||
@@ -698,12 +567,7 @@ impl ProfileManager {
|
||||
// Update version
|
||||
profile.version = version.to_string();
|
||||
|
||||
// Update the release_type based on the version and browser
|
||||
profile.release_type = if is_browser_version_nightly(&profile.browser, version, None) {
|
||||
"nightly".to_string()
|
||||
} else {
|
||||
"stable".to_string()
|
||||
};
|
||||
profile.release_type = "stable".to_string();
|
||||
|
||||
// Save the updated profile
|
||||
self.save_profile(&profile)?;
|
||||
@@ -1096,7 +960,6 @@ impl ProfileManager {
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: source.release_type,
|
||||
camoufox_config: source.camoufox_config,
|
||||
wayfern_config: source.wayfern_config,
|
||||
group_id: source.group_id,
|
||||
tags: source.tags,
|
||||
@@ -1146,68 +1009,6 @@ impl ProfileManager {
|
||||
Ok(new_profile)
|
||||
}
|
||||
|
||||
pub async fn update_camoufox_config(
|
||||
&self,
|
||||
app_handle: tauri::AppHandle,
|
||||
profile_id: &str,
|
||||
config: CamoufoxConfig,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Find the profile by ID
|
||||
let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err(
|
||||
|_| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Invalid profile ID: {profile_id}").into()
|
||||
},
|
||||
)?;
|
||||
let profiles =
|
||||
self
|
||||
.list_profiles()
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to list profiles: {e}").into()
|
||||
})?;
|
||||
let mut profile = profiles
|
||||
.into_iter()
|
||||
.find(|p| p.id == profile_uuid)
|
||||
.ok_or_else(|| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Profile with ID '{profile_id}' not found").into()
|
||||
})?;
|
||||
|
||||
// Check if the browser is currently running using the comprehensive status check
|
||||
let is_running = self
|
||||
.check_browser_status(app_handle.clone(), &profile)
|
||||
.await?;
|
||||
|
||||
if is_running {
|
||||
return Err(
|
||||
"Cannot update Camoufox configuration while browser is running. Please stop the browser first.".into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Update the Camoufox configuration
|
||||
profile.camoufox_config = Some(config);
|
||||
|
||||
// Save the updated profile
|
||||
self
|
||||
.save_profile(&profile)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to save profile: {e}").into()
|
||||
})?;
|
||||
|
||||
crate::sync::queue_profile_sync_if_eligible(&profile);
|
||||
|
||||
log::info!(
|
||||
"Camoufox configuration updated for profile '{}' (ID: {}).",
|
||||
profile.name,
|
||||
profile_id
|
||||
);
|
||||
|
||||
// Emit profile config update event
|
||||
if let Err(e) = events::emit_empty("profiles-changed") {
|
||||
log::warn!("Warning: Failed to emit profiles-changed event: {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_wayfern_config(
|
||||
&self,
|
||||
app_handle: tauri::AppHandle,
|
||||
@@ -1323,44 +1124,6 @@ impl ProfileManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Update on-disk browser profile config immediately.
|
||||
// Both supported browser types ignore this write (Camoufox rewrites
|
||||
// user.js at launch with the local donut-proxy host, Wayfern takes its
|
||||
// proxy via `--proxy-pac-url=` and never reads user.js), and for
|
||||
// Camoufox specifically writing the upstream host here would leave a
|
||||
// stale, wrong proxy in user.js until the next launch.
|
||||
if !matches!(profile.browser.as_str(), "camoufox" | "wayfern") {
|
||||
if let Some(proxy_id_ref) = &proxy_id {
|
||||
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
self
|
||||
.apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to apply proxy settings: {e}").into()
|
||||
})?;
|
||||
} else {
|
||||
// Proxy ID provided but proxy not found, disable proxy
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
self
|
||||
.disable_proxy_settings_in_profile(&profile_path)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to disable proxy settings: {e}").into()
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
// No proxy ID provided, disable proxy
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
self
|
||||
.disable_proxy_settings_in_profile(&profile_path)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to disable proxy settings: {e}").into()
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit profile update event so frontend UIs can refresh immediately (e.g. proxy manager)
|
||||
if let Err(e) = events::emit("profile-updated", &profile) {
|
||||
log::warn!("Warning: Failed to emit profile update event: {e}");
|
||||
@@ -1481,17 +1244,12 @@ impl ProfileManager {
|
||||
app_handle: tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Handle Camoufox profiles using CamoufoxManager-based status checking
|
||||
if profile.browser == "camoufox" {
|
||||
return self.check_camoufox_status(&app_handle, profile).await;
|
||||
}
|
||||
|
||||
// Handle Wayfern profiles using WayfernManager-based status checking
|
||||
if profile.browser == "wayfern" {
|
||||
return self.check_wayfern_status(&app_handle, profile).await;
|
||||
}
|
||||
|
||||
// For non-camoufox browsers, use the existing PID-based logic
|
||||
// For non-wayfern browsers, use the existing PID-based logic
|
||||
let inner_profile = profile.clone();
|
||||
let system = System::new_with_specifics(
|
||||
RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()),
|
||||
@@ -1510,18 +1268,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 == "camoufox" {
|
||||
arg == profile_data_path_str
|
||||
|| arg == format!("-profile={profile_data_path_str}")
|
||||
|| (arg == "-profile"
|
||||
&& cmd
|
||||
.iter()
|
||||
.any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str))
|
||||
} else {
|
||||
// For Chromium-based browsers (Wayfern), check for user-data-dir
|
||||
arg.contains(&format!("--user-data-dir={profile_data_path_str}"))
|
||||
|| arg == profile_data_path_str
|
||||
}
|
||||
arg.contains(&format!("--user-data-dir={profile_data_path_str}"))
|
||||
|| arg == profile_data_path_str
|
||||
});
|
||||
|
||||
if profile_path_match {
|
||||
@@ -1539,7 +1287,6 @@ impl ProfileManager {
|
||||
// Check if this is the right browser executable first
|
||||
let exe_name = process.name().to_string_lossy().to_lowercase();
|
||||
let is_correct_browser = match profile.browser.as_str() {
|
||||
"camoufox" => exe_name.contains("camoufox") || exe_name.contains("firefox"),
|
||||
"wayfern" => {
|
||||
exe_name.contains("wayfern")
|
||||
|| exe_name.contains("chromium")
|
||||
@@ -1559,18 +1306,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 == "camoufox" {
|
||||
arg == profile_data_path_str
|
||||
|| arg == format!("-profile={profile_data_path_str}")
|
||||
|| (arg == "-profile"
|
||||
&& cmd
|
||||
.iter()
|
||||
.any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str))
|
||||
} else {
|
||||
// For Chromium-based browsers (Wayfern), check for user-data-dir
|
||||
arg.contains(&format!("--user-data-dir={profile_data_path_str}"))
|
||||
|| arg == profile_data_path_str
|
||||
}
|
||||
arg.contains(&format!("--user-data-dir={profile_data_path_str}"))
|
||||
|| arg == profile_data_path_str
|
||||
});
|
||||
|
||||
if profile_path_match {
|
||||
@@ -1644,146 +1381,6 @@ impl ProfileManager {
|
||||
Ok(is_running)
|
||||
}
|
||||
|
||||
// Check Camoufox status using CamoufoxManager
|
||||
async fn check_camoufox_status(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let launcher = self.camoufox_manager;
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_data_path =
|
||||
crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir);
|
||||
let profile_path_str = profile_data_path.to_string_lossy();
|
||||
|
||||
// Check if there's a running Camoufox instance for this profile
|
||||
match launcher.find_camoufox_by_profile(&profile_path_str).await {
|
||||
Ok(Some(camoufox_process)) => {
|
||||
// Found a running instance, update profile with process info if changed
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
|
||||
let metadata_file = profile_uuid_dir.join("metadata.json");
|
||||
let metadata_exists = metadata_file.exists();
|
||||
|
||||
if metadata_exists {
|
||||
// Load latest to avoid overwriting other fields
|
||||
let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => profile.clone(),
|
||||
};
|
||||
|
||||
if latest.process_id != camoufox_process.processId {
|
||||
let old_pid = latest.process_id;
|
||||
latest.process_id = camoufox_process.processId;
|
||||
if let Err(e) = self.save_profile(&latest) {
|
||||
log::warn!("Warning: Failed to update Camoufox profile with process info: {e}");
|
||||
}
|
||||
if let (Some(prev), Some(new)) = (old_pid, camoufox_process.processId) {
|
||||
let _ = crate::proxy_manager::PROXY_MANAGER.update_proxy_pid(prev, new);
|
||||
}
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = events::emit("profile-updated", &latest) {
|
||||
log::warn!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Camoufox process has started for profile '{}' with PID: {:?}",
|
||||
profile.name,
|
||||
camoufox_process.processId
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
Ok(None) => {
|
||||
// No running instance found, clear process ID if set and stop proxy
|
||||
if profile.ephemeral {
|
||||
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
|
||||
}
|
||||
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
|
||||
let metadata_file = profile_uuid_dir.join("metadata.json");
|
||||
let metadata_exists = metadata_file.exists();
|
||||
|
||||
if metadata_exists {
|
||||
let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => profile.clone(),
|
||||
};
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
if let Some(updated) = crate::auto_updater::AutoUpdater::instance()
|
||||
.update_profile_to_latest_installed(app_handle, &latest)
|
||||
{
|
||||
latest = updated;
|
||||
}
|
||||
|
||||
if let Err(e) = events::emit("profile-updated", &latest) {
|
||||
log::warn!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
// Error checking status, assume not running and clear process ID
|
||||
log::warn!("Warning: Failed to check Camoufox status: {e}");
|
||||
if profile.ephemeral {
|
||||
crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string());
|
||||
}
|
||||
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
|
||||
let metadata_file = profile_uuid_dir.join("metadata.json");
|
||||
let metadata_exists = metadata_file.exists();
|
||||
|
||||
if metadata_exists {
|
||||
let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => profile.clone(),
|
||||
};
|
||||
|
||||
if latest.process_id.is_some() {
|
||||
latest.process_id = None;
|
||||
if let Err(e2) = self.save_profile(&latest) {
|
||||
log::warn!(
|
||||
"Warning: Failed to clear Camoufox profile process info after error: {e2}"
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(updated) = crate::auto_updater::AutoUpdater::instance()
|
||||
.update_profile_to_latest_installed(app_handle, &latest)
|
||||
{
|
||||
latest = updated;
|
||||
}
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e3) = events::emit("profile-updated", &latest) {
|
||||
log::warn!("Warning: Failed to emit profile update event: {e3}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check Wayfern status using WayfernManager
|
||||
async fn check_wayfern_status(
|
||||
&self,
|
||||
@@ -1880,212 +1477,6 @@ impl ProfileManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_common_firefox_preferences(&self) -> Vec<String> {
|
||||
vec![
|
||||
// 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(),
|
||||
"user_pref(\"services.sync.prefs.sync.browser.startup.upgradeDialog.enabled\", false);"
|
||||
.to_string(),
|
||||
// Disable welcome / first-run screens
|
||||
"user_pref(\"browser.aboutwelcome.enabled\", false);".to_string(),
|
||||
"user_pref(\"browser.startup.homepage_override.mstone\", \"ignore\");".to_string(),
|
||||
"user_pref(\"startup.homepage_welcome_url\", \"\");".to_string(),
|
||||
"user_pref(\"startup.homepage_welcome_url.additional\", \"\");".to_string(),
|
||||
"user_pref(\"startup.homepage_override_url\", \"\");".to_string(),
|
||||
// Keep extension updates enabled and allow sideloaded extensions.
|
||||
// - autoDisableScopes=0: profile-installed extensions are enabled by default.
|
||||
// - startupScanScopes=1: rescan SCOPE_PROFILE on each launch so freshly
|
||||
// dropped .xpi files in <profile>/extensions/ get registered.
|
||||
// - signatures.required=false: accept unsigned/dev .xpi files. Camoufox
|
||||
// is built without MOZ_REQUIRE_SIGNING so this is honored.
|
||||
"user_pref(\"extensions.update.enabled\", true);".to_string(),
|
||||
"user_pref(\"extensions.update.autoUpdateDefault\", true);".to_string(),
|
||||
"user_pref(\"extensions.autoDisableScopes\", 0);".to_string(),
|
||||
"user_pref(\"extensions.startupScanScopes\", 1);".to_string(),
|
||||
"user_pref(\"xpinstall.signatures.required\", false);".to_string(),
|
||||
// Completely disable browser update checking
|
||||
"user_pref(\"app.update.enabled\", false);".to_string(),
|
||||
"user_pref(\"app.update.auto\", false);".to_string(),
|
||||
"user_pref(\"app.update.mode\", 0);".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(),
|
||||
// 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(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn apply_proxy_settings_to_profile(
|
||||
&self,
|
||||
profile_data_path: &Path,
|
||||
proxy: &ProxySettings,
|
||||
internal_proxy: Option<&ProxySettings>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let user_js_path = profile_data_path.join("user.js");
|
||||
let prefs_js_path = profile_data_path.join("prefs.js");
|
||||
|
||||
// Remove prefs.js if it exists to ensure Firefox reads user.js instead
|
||||
// Firefox may cache proxy settings in prefs.js, so we need to clear it
|
||||
if prefs_js_path.exists() {
|
||||
log::info!("Removing prefs.js to ensure Firefox reads updated user.js settings");
|
||||
let _ = fs::remove_file(&prefs_js_path);
|
||||
}
|
||||
|
||||
let mut preferences = Vec::new();
|
||||
|
||||
// Add common Firefox preferences (like disabling default browser check)
|
||||
preferences.extend(self.get_common_firefox_preferences());
|
||||
|
||||
// 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;
|
||||
|
||||
// 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");
|
||||
|
||||
log::info!(
|
||||
"Applying manual proxy settings to Firefox profile: {}:{} (is_internal: {}, is_socks: {})",
|
||||
proxy_host,
|
||||
proxy_port,
|
||||
internal_proxy.is_some(),
|
||||
is_socks
|
||||
);
|
||||
|
||||
// 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 {
|
||||
// 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.no_proxies_on\", \"\");".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 manual proxy settings: {}:{}",
|
||||
proxy_host,
|
||||
proxy_port
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_proxy_settings_in_profile(
|
||||
&self,
|
||||
profile_data_path: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let user_js_path = profile_data_path.join("user.js");
|
||||
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());
|
||||
|
||||
preferences.push("user_pref(\"network.proxy.type\", 0);".to_string());
|
||||
preferences.push("user_pref(\"network.proxy.failover_direct\", true);".to_string());
|
||||
|
||||
// Create a direct proxy PAC file in UUID directory
|
||||
let pac_content = "function FindProxyForURL(url, host) { return 'DIRECT'; }";
|
||||
let pac_path = uuid_dir.join("proxy.pac");
|
||||
fs::write(&pac_path, pac_content)?;
|
||||
let pac_url =
|
||||
url::Url::from_file_path(&pac_path).map_err(|_| "Failed to convert PAC path to file URL")?;
|
||||
preferences.push(format!(
|
||||
"user_pref(\"network.proxy.autoconfig_url\", \"{}\");",
|
||||
pac_url.as_str()
|
||||
));
|
||||
|
||||
fs::write(user_js_path, preferences.join("\n"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2127,25 +1518,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_common_firefox_preferences() {
|
||||
let (manager, _temp_dir) = create_test_profile_manager();
|
||||
|
||||
let prefs = manager.get_common_firefox_preferences();
|
||||
assert!(!prefs.is_empty(), "Should return non-empty preferences");
|
||||
|
||||
// Check for some expected preferences
|
||||
let prefs_string = prefs.join("\n");
|
||||
assert!(
|
||||
prefs_string.contains("browser.shell.checkDefaultBrowser"),
|
||||
"Should contain default browser check preference"
|
||||
);
|
||||
assert!(
|
||||
prefs_string.contains("app.update.enabled"),
|
||||
"Should contain update preference"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_binaries_dir() {
|
||||
let (manager, _temp_dir) = create_test_profile_manager();
|
||||
@@ -2163,109 +1535,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disable_proxy_settings_in_profile() {
|
||||
let (manager, temp_dir) = create_test_profile_manager();
|
||||
|
||||
// Create a test profile directory
|
||||
let profile_dir = temp_dir.path().join("test_profile");
|
||||
fs::create_dir_all(&profile_dir).expect("Should create profile directory");
|
||||
|
||||
let result = manager.disable_proxy_settings_in_profile(&profile_dir);
|
||||
assert!(result.is_ok(), "Should successfully disable proxy settings");
|
||||
|
||||
// Check that user.js was created
|
||||
let user_js_path = profile_dir.join("user.js");
|
||||
assert!(user_js_path.exists(), "user.js should be created");
|
||||
|
||||
let content = fs::read_to_string(&user_js_path).expect("Should read user.js");
|
||||
assert!(
|
||||
content.contains("network.proxy.type"),
|
||||
"Should contain proxy type setting"
|
||||
);
|
||||
assert!(
|
||||
content.contains("0"),
|
||||
"Should set proxy type to 0 (no proxy)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_proxy_settings_to_profile() {
|
||||
let (manager, temp_dir) = create_test_profile_manager();
|
||||
|
||||
// Create a test profile directory structure
|
||||
let uuid_dir = temp_dir.path().join("test_uuid");
|
||||
let profile_dir = uuid_dir.join("profile");
|
||||
fs::create_dir_all(&profile_dir).expect("Should create profile directory");
|
||||
|
||||
let proxy_settings = ProxySettings {
|
||||
proxy_type: "http".to_string(),
|
||||
host: "proxy.example.com".to_string(),
|
||||
port: 8080,
|
||||
username: Some("user".to_string()),
|
||||
password: Some("pass".to_string()),
|
||||
};
|
||||
|
||||
let result = manager.apply_proxy_settings_to_profile(&profile_dir, &proxy_settings, None);
|
||||
assert!(result.is_ok(), "Should successfully apply proxy settings");
|
||||
|
||||
// Check that user.js was created
|
||||
let user_js_path = profile_dir.join("user.js");
|
||||
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\", 1"),
|
||||
"Should set proxy type to 1 (manual)"
|
||||
);
|
||||
assert!(
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pac_url_encodes_spaces_in_path() {
|
||||
let (manager, temp_dir) = create_test_profile_manager();
|
||||
|
||||
let uuid_dir = temp_dir.path().join("path with spaces");
|
||||
let profile_dir = uuid_dir.join("profile");
|
||||
fs::create_dir_all(&profile_dir).expect("Should create profile directory");
|
||||
|
||||
let result = manager.disable_proxy_settings_in_profile(&profile_dir);
|
||||
assert!(result.is_ok(), "Should handle paths with spaces");
|
||||
|
||||
let user_js = fs::read_to_string(profile_dir.join("user.js")).unwrap();
|
||||
let pac_line = user_js
|
||||
.lines()
|
||||
.find(|l| l.contains("autoconfig_url"))
|
||||
.expect("Should have autoconfig_url preference");
|
||||
|
||||
assert!(
|
||||
!pac_line.contains("path with spaces"),
|
||||
"PAC URL should not contain raw spaces: {pac_line}"
|
||||
);
|
||||
assert!(
|
||||
pac_line.contains("path%20with%20spaces"),
|
||||
"PAC URL should percent-encode spaces: {pac_line}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_launch_hook_accepts_http_and_https() {
|
||||
let http =
|
||||
@@ -2339,7 +1608,6 @@ pub async fn create_browser_profile_with_group(
|
||||
release_type: String,
|
||||
proxy_id: Option<String>,
|
||||
vpn_id: Option<String>,
|
||||
camoufox_config: Option<CamoufoxConfig>,
|
||||
wayfern_config: Option<WayfernConfig>,
|
||||
group_id: Option<String>,
|
||||
ephemeral: bool,
|
||||
@@ -2356,7 +1624,6 @@ pub async fn create_browser_profile_with_group(
|
||||
&release_type,
|
||||
proxy_id,
|
||||
vpn_id,
|
||||
camoufox_config,
|
||||
wayfern_config,
|
||||
group_id,
|
||||
ephemeral,
|
||||
@@ -2527,17 +1794,13 @@ pub async fn create_browser_profile_new(
|
||||
release_type: String,
|
||||
proxy_id: Option<String>,
|
||||
vpn_id: Option<String>,
|
||||
camoufox_config: Option<CamoufoxConfig>,
|
||||
wayfern_config: Option<WayfernConfig>,
|
||||
group_id: Option<String>,
|
||||
ephemeral: Option<bool>,
|
||||
dns_blocklist: Option<String>,
|
||||
launch_hook: Option<String>,
|
||||
) -> Result<BrowserProfile, String> {
|
||||
let fingerprint_os = camoufox_config
|
||||
.as_ref()
|
||||
.and_then(|c| c.os.as_deref())
|
||||
.or_else(|| wayfern_config.as_ref().and_then(|c| c.os.as_deref()));
|
||||
let fingerprint_os = wayfern_config.as_ref().and_then(|c| c.os.as_deref());
|
||||
|
||||
if !crate::cloud_auth::CLOUD_AUTH
|
||||
.is_fingerprint_os_allowed(fingerprint_os)
|
||||
@@ -2560,7 +1823,6 @@ pub async fn create_browser_profile_new(
|
||||
release_type,
|
||||
proxy_id,
|
||||
vpn_id,
|
||||
camoufox_config,
|
||||
wayfern_config,
|
||||
group_id,
|
||||
ephemeral.unwrap_or(false),
|
||||
@@ -2570,34 +1832,6 @@ pub async fn create_browser_profile_new(
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_camoufox_config(
|
||||
app_handle: tauri::AppHandle,
|
||||
profile_id: String,
|
||||
config: CamoufoxConfig,
|
||||
) -> Result<(), String> {
|
||||
if config.fingerprint.is_some()
|
||||
&& !crate::cloud_auth::CLOUD_AUTH
|
||||
.can_use_cross_os_fingerprints()
|
||||
.await
|
||||
{
|
||||
return Err(serde_json::json!({ "code": "FINGERPRINT_REQUIRES_PRO" }).to_string());
|
||||
}
|
||||
|
||||
if !crate::cloud_auth::CLOUD_AUTH
|
||||
.is_fingerprint_os_allowed(config.os.as_deref())
|
||||
.await
|
||||
{
|
||||
return Err("Fingerprint OS spoofing requires an active Pro subscription".to_string());
|
||||
}
|
||||
|
||||
let profile_manager = ProfileManager::instance();
|
||||
profile_manager
|
||||
.update_camoufox_config(app_handle, &profile_id, config)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to update Camoufox config: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_wayfern_config(
|
||||
app_handle: tauri::AppHandle,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::camoufox_manager::CamoufoxConfig;
|
||||
use crate::wayfern_manager::WayfernConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -38,9 +37,7 @@ pub struct BrowserProfile {
|
||||
#[serde(default)]
|
||||
pub last_launch: Option<u64>,
|
||||
#[serde(default = "default_release_type")]
|
||||
pub release_type: String, // "stable" or "nightly"
|
||||
#[serde(default)]
|
||||
pub camoufox_config: Option<CamoufoxConfig>, // Camoufox configuration
|
||||
pub release_type: String,
|
||||
#[serde(default)]
|
||||
pub wayfern_config: Option<WayfernConfig>, // Wayfern configuration
|
||||
#[serde(default)]
|
||||
@@ -115,7 +112,6 @@ impl BrowserProfile {
|
||||
self
|
||||
.host_os
|
||||
.as_deref()
|
||||
.or_else(|| self.camoufox_config.as_ref().and_then(|c| c.os.as_deref()))
|
||||
.or_else(|| self.wayfern_config.as_ref().and_then(|c| c.os.as_deref()))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::collections::HashSet;
|
||||
use std::fs::{self, create_dir_all};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::camoufox_manager::CamoufoxConfig;
|
||||
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
|
||||
use crate::profile::types::{get_host_os, BrowserProfile, SyncMode};
|
||||
use crate::profile::ProfileManager;
|
||||
@@ -21,11 +20,9 @@ pub struct DetectedProfile {
|
||||
}
|
||||
|
||||
fn map_browser_type(browser: &str) -> &str {
|
||||
// Firefox-based sources map to the now-deprecated Camoufox. They are no longer
|
||||
// detected for import; the mapping is kept only so the import command can
|
||||
// recognize and REJECT them. Everything else maps to Wayfern.
|
||||
// Legacy Firefox-family sources map to Wayfern at import time.
|
||||
match browser {
|
||||
"firefox" | "firefox-developer" | "zen" | "camoufox" => "camoufox",
|
||||
"firefox" | "firefox-developer" | "zen" | "camoufox" => "wayfern",
|
||||
_ => "wayfern",
|
||||
}
|
||||
}
|
||||
@@ -218,7 +215,7 @@ impl ProfileImporter {
|
||||
"chromium" => "Chrome/Chromium",
|
||||
"brave" => "Brave",
|
||||
"zen" => "Zen Browser",
|
||||
"camoufox" => "Camoufox",
|
||||
|
||||
"wayfern" => "Wayfern",
|
||||
_ => "Unknown Browser",
|
||||
}
|
||||
@@ -232,7 +229,6 @@ impl ProfileImporter {
|
||||
browser_type: &str,
|
||||
new_profile_name: &str,
|
||||
proxy_id: Option<String>,
|
||||
_camoufox_config: Option<CamoufoxConfig>,
|
||||
wayfern_config: Option<WayfernConfig>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source_path = Path::new(source_path);
|
||||
@@ -268,10 +264,6 @@ impl ProfileImporter {
|
||||
|
||||
let version = self.get_default_version_for_browser(mapped)?;
|
||||
|
||||
// Camoufox import is removed; only Wayfern profiles are imported now, so the
|
||||
// imported profile never carries a Camoufox config.
|
||||
let final_camoufox_config: Option<CamoufoxConfig> = None;
|
||||
|
||||
let final_wayfern_config = if mapped == "wayfern" {
|
||||
let mut config = wayfern_config.unwrap_or_default();
|
||||
|
||||
@@ -312,7 +304,6 @@ impl ProfileImporter {
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
camoufox_config: None,
|
||||
wayfern_config: None,
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
@@ -367,7 +358,6 @@ impl ProfileImporter {
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
camoufox_config: final_camoufox_config,
|
||||
wayfern_config: final_wayfern_config,
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
@@ -465,19 +455,9 @@ pub async fn import_browser_profile(
|
||||
browser_type: String,
|
||||
new_profile_name: String,
|
||||
proxy_id: Option<String>,
|
||||
camoufox_config: Option<CamoufoxConfig>,
|
||||
wayfern_config: Option<WayfernConfig>,
|
||||
) -> Result<(), String> {
|
||||
// Camoufox is deprecated — Firefox-based profiles (which map to Camoufox) can
|
||||
// no longer be imported. Reject them before doing any work.
|
||||
if map_browser_type(&browser_type) == "camoufox" {
|
||||
return Err(serde_json::json!({ "code": "CAMOUFOX_IMPORT_DEPRECATED" }).to_string());
|
||||
}
|
||||
|
||||
let fingerprint_os = camoufox_config
|
||||
.as_ref()
|
||||
.and_then(|c| c.os.as_deref())
|
||||
.or_else(|| wayfern_config.as_ref().and_then(|c| c.os.as_deref()));
|
||||
let fingerprint_os = wayfern_config.as_ref().and_then(|c| c.os.as_deref());
|
||||
|
||||
if !crate::cloud_auth::CLOUD_AUTH
|
||||
.is_fingerprint_os_allowed(fingerprint_os)
|
||||
@@ -494,7 +474,6 @@ pub async fn import_browser_profile(
|
||||
&browser_type,
|
||||
&new_profile_name,
|
||||
proxy_id,
|
||||
camoufox_config,
|
||||
wayfern_config,
|
||||
)
|
||||
.await
|
||||
@@ -546,12 +525,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_map_browser_type() {
|
||||
assert_eq!(map_browser_type("firefox"), "camoufox");
|
||||
assert_eq!(map_browser_type("firefox-developer"), "camoufox");
|
||||
assert_eq!(map_browser_type("zen"), "camoufox");
|
||||
assert_eq!(map_browser_type("firefox"), "wayfern");
|
||||
assert_eq!(map_browser_type("firefox-developer"), "wayfern");
|
||||
assert_eq!(map_browser_type("zen"), "wayfern");
|
||||
assert_eq!(map_browser_type("chromium"), "wayfern");
|
||||
assert_eq!(map_browser_type("brave"), "wayfern");
|
||||
assert_eq!(map_browser_type("camoufox"), "camoufox");
|
||||
assert_eq!(map_browser_type("camoufox"), "wayfern");
|
||||
assert_eq!(map_browser_type("wayfern"), "wayfern");
|
||||
assert_eq!(map_browser_type("something_else"), "wayfern");
|
||||
}
|
||||
|
||||
@@ -1487,9 +1487,8 @@ impl ProxyManager {
|
||||
profile_id: Option<&str>,
|
||||
bypass_rules: Vec<String>,
|
||||
blocklist_file: Option<String>,
|
||||
// Protocol the local worker serves the browser: "http" (Camoufox) or
|
||||
// "socks5" (Wayfern). Reflected in the returned ProxySettings.proxy_type
|
||||
// so the caller formats the right local proxy URL scheme.
|
||||
// Protocol the local worker serves the browser: "socks5" (Wayfern). Reflected in
|
||||
// the returned ProxySettings.proxy_type so the caller formats the right local proxy URL scheme.
|
||||
local_protocol: &str,
|
||||
) -> Result<ProxySettings, String> {
|
||||
if let Some(name) = profile_id {
|
||||
|
||||
@@ -16,8 +16,7 @@ pub struct ProxyConfig {
|
||||
pub bypass_rules: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub blocklist_file: Option<String>,
|
||||
/// Protocol the local worker serves to the browser: "http" (default, used
|
||||
/// by Camoufox/Firefox) or "socks5" (used by Wayfern/Chromium so QUIC and
|
||||
/// Protocol the local worker serves to the browser: "socks5" (Wayfern/Chromium so QUIC and
|
||||
/// WebRTC UDP can be proxied without leaking the real IP). Independent of
|
||||
/// `upstream_url`, which is the real upstream proxy/VPN this worker dials.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -73,7 +73,7 @@ const CRITICAL_FILE_PATTERNS: &[&str] = &[
|
||||
"Secure Preferences",
|
||||
"Web Data",
|
||||
"Extension Cookies",
|
||||
// Firefox/Camoufox equivalents
|
||||
// Chromium profile equivalents
|
||||
"cookies.sqlite",
|
||||
"key4.db",
|
||||
"logins.json",
|
||||
|
||||
@@ -111,10 +111,7 @@ impl SynchronizerManager {
|
||||
.clone();
|
||||
|
||||
if leader.browser != "wayfern" {
|
||||
return Err(
|
||||
"Synchronizer only supports Wayfern profiles. Camoufox profiles cannot be used."
|
||||
.to_string(),
|
||||
);
|
||||
return Err("Synchronizer only supports Wayfern profiles.".to_string());
|
||||
}
|
||||
|
||||
// Check leader is not already running
|
||||
|
||||
@@ -254,9 +254,9 @@ impl VersionUpdater {
|
||||
) -> Result<Vec<BackgroundUpdateResult>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let supported_browsers = self.browser_version_manager.get_supported_browsers();
|
||||
|
||||
// Only fetch versions for active browsers (wayfern, camoufox) plus any
|
||||
// Only fetch versions for active browsers (wayfern) plus any
|
||||
// deprecated browsers that still have existing profiles
|
||||
let active_browsers = ["wayfern", "camoufox"];
|
||||
let active_browsers = ["wayfern"];
|
||||
let browsers_with_profiles: std::collections::HashSet<String> =
|
||||
crate::profile::ProfileManager::instance()
|
||||
.list_profiles()
|
||||
|
||||
@@ -154,7 +154,7 @@ impl WayfernManager {
|
||||
///
|
||||
/// Keys are the camelCase fields Wayfern uses in its fingerprint
|
||||
/// (`windowOuterWidth`, `screenAvailWidth`, …) — NOT the dotted
|
||||
/// Camoufox-style keys. Preference order, matching how the fingerprint
|
||||
/// Preference order, matching how the fingerprint
|
||||
/// describes the window:
|
||||
/// 1. `windowOuterWidth` / `windowOuterHeight` — the real window size.
|
||||
/// 2. `screenAvailWidth` / `screenAvailHeight` — usable screen area.
|
||||
@@ -325,7 +325,7 @@ impl WayfernManager {
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch public IP: {e}"))?,
|
||||
};
|
||||
crate::camoufox::geolocation::get_geolocation(&ip)
|
||||
crate::geolocation::get_geolocation(&ip)
|
||||
.map_err(|e| format!("Failed to get geolocation for IP {ip}: {e}"))
|
||||
}
|
||||
.await;
|
||||
|
||||
@@ -246,7 +246,7 @@ fn create_test_profile_bundle_with_bypass_rules(temp_dir: &Path, bypass_rules: &
|
||||
let metadata = json!({
|
||||
"id": "test-bypass-profile-id",
|
||||
"name": "Bypass Rules Profile",
|
||||
"browser": "camoufox",
|
||||
"browser": "wayfern",
|
||||
"version": "120.0.0",
|
||||
"release_type": "stable",
|
||||
"sync_enabled": true,
|
||||
@@ -802,7 +802,7 @@ async fn test_profile_bypass_rules_sync() {
|
||||
let metadata = extract_bundle(&downloaded, &extract_dir);
|
||||
|
||||
assert_eq!(metadata["name"], "Bypass Rules Profile");
|
||||
assert_eq!(metadata["browser"], "camoufox");
|
||||
assert_eq!(metadata["browser"], "wayfern");
|
||||
|
||||
let synced_rules = metadata["proxy_bypass_rules"]
|
||||
.as_array()
|
||||
|
||||
+23
-96
@@ -7,8 +7,6 @@ import { useOnborda } from "onborda";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AccountPage } from "@/components/account-page";
|
||||
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
|
||||
import { CamoufoxDeprecationDialog } from "@/components/camoufox-deprecation-dialog";
|
||||
import { CloneProfileDialog } from "@/components/clone-profile-dialog";
|
||||
import { CloseConfirmDialog } from "@/components/close-confirm-dialog";
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
@@ -43,6 +41,7 @@ import { SyncAllDialog } from "@/components/sync-all-dialog";
|
||||
import { SyncConfigDialog } from "@/components/sync-config-dialog";
|
||||
import { SyncFollowerDialog } from "@/components/sync-follower-dialog";
|
||||
import { ThankYouDialog } from "@/components/thank-you-dialog";
|
||||
import { WayfernConfigDialog } from "@/components/wayfern-config-dialog";
|
||||
import { WayfernTermsDialog } from "@/components/wayfern-terms-dialog";
|
||||
import { WelcomeDialog } from "@/components/welcome-dialog";
|
||||
import { WindowResizeWarningDialog } from "@/components/window-resize-warning-dialog";
|
||||
@@ -78,14 +77,9 @@ import {
|
||||
showSyncProgressToast,
|
||||
showToast,
|
||||
} from "@/lib/toast-utils";
|
||||
import type {
|
||||
BrowserProfile,
|
||||
CamoufoxConfig,
|
||||
SyncSettings,
|
||||
WayfernConfig,
|
||||
} from "@/types";
|
||||
import type { BrowserProfile, SyncSettings, WayfernConfig } from "@/types";
|
||||
|
||||
type BrowserTypeString = "camoufox" | "wayfern";
|
||||
type BrowserTypeString = "wayfern";
|
||||
|
||||
interface PendingUrl {
|
||||
id: string;
|
||||
@@ -268,8 +262,7 @@ export default function Home() {
|
||||
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
|
||||
const [proxyManagementDialogOpen, setProxyManagementDialogOpen] =
|
||||
useState(false);
|
||||
const [camoufoxConfigDialogOpen, setCamoufoxConfigDialogOpen] =
|
||||
useState(false);
|
||||
const [wayfernConfigDialogOpen, setWayfernConfigDialogOpen] = useState(false);
|
||||
const [groupManagementDialogOpen, setGroupManagementDialogOpen] =
|
||||
useState(false);
|
||||
const [extensionManagementDialogOpen, setExtensionManagementDialogOpen] =
|
||||
@@ -306,7 +299,7 @@ export default function Home() {
|
||||
const [selectedProfiles, setSelectedProfiles] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
|
||||
const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] =
|
||||
const [currentProfileForWayfernConfig, setCurrentProfileForWayfernConfig] =
|
||||
useState<BrowserProfile | null>(null);
|
||||
const [cloneProfile, setCloneProfile] = useState<BrowserProfile | null>(null);
|
||||
const [passwordDialogProfile, setPasswordDialogProfile] =
|
||||
@@ -315,8 +308,6 @@ export default function Home() {
|
||||
useState<PasswordDialogMode>("set");
|
||||
const pendingLaunchAfterUnlockRef = useRef<BrowserProfile | null>(null);
|
||||
const [windowResizeWarningOpen, setWindowResizeWarningOpen] = useState(false);
|
||||
const [windowResizeWarningBrowserType, setWindowResizeWarningBrowserType] =
|
||||
useState<string | undefined>(undefined);
|
||||
const windowResizeWarningResolver = useRef<
|
||||
((proceed: boolean) => void) | null
|
||||
>(null);
|
||||
@@ -534,7 +525,7 @@ export default function Home() {
|
||||
console.log("Found missing binaries:", missingBinaries);
|
||||
}
|
||||
if (missingGeoIP) {
|
||||
console.log("Found missing GeoIP database for Camoufox");
|
||||
console.log("Found missing GeoIP database");
|
||||
}
|
||||
|
||||
// Group missing binaries by browser type to avoid concurrent downloads
|
||||
@@ -556,9 +547,9 @@ export default function Home() {
|
||||
|
||||
if (missingGeoIP) {
|
||||
if (missingList) {
|
||||
missingList += ", GeoIP database for Camoufox";
|
||||
missingList += ", GeoIP database";
|
||||
} else {
|
||||
missingList = "GeoIP database for Camoufox";
|
||||
missingList = "GeoIP database";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -776,33 +767,11 @@ export default function Home() {
|
||||
}
|
||||
}, [handleUrlOpen, t]);
|
||||
|
||||
const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => {
|
||||
setCurrentProfileForCamoufoxConfig(profile);
|
||||
setCamoufoxConfigDialogOpen(true);
|
||||
const handleConfigureWayfern = useCallback((profile: BrowserProfile) => {
|
||||
setCurrentProfileForWayfernConfig(profile);
|
||||
setWayfernConfigDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleSaveCamoufoxConfig = useCallback(
|
||||
async (profile: BrowserProfile, config: CamoufoxConfig) => {
|
||||
try {
|
||||
await invoke("update_camoufox_config", {
|
||||
profileId: profile.id,
|
||||
config,
|
||||
});
|
||||
// No need to manually reload - useProfileEvents will handle the update
|
||||
setCamoufoxConfigDialogOpen(false);
|
||||
} catch (err: unknown) {
|
||||
console.error("Failed to update camoufox config:", err);
|
||||
showErrorToast(
|
||||
t("errors.updateCamoufoxConfigFailed", {
|
||||
error: JSON.stringify(err),
|
||||
}),
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const handleSaveWayfernConfig = useCallback(
|
||||
async (profile: BrowserProfile, config: WayfernConfig) => {
|
||||
try {
|
||||
@@ -811,7 +780,7 @@ export default function Home() {
|
||||
config,
|
||||
});
|
||||
// No need to manually reload - useProfileEvents will handle the update
|
||||
setCamoufoxConfigDialogOpen(false);
|
||||
setWayfernConfigDialogOpen(false);
|
||||
} catch (err: unknown) {
|
||||
console.error("Failed to update wayfern config:", err);
|
||||
showErrorToast(
|
||||
@@ -831,7 +800,6 @@ export default function Home() {
|
||||
releaseType: string;
|
||||
proxyId?: string;
|
||||
vpnId?: string;
|
||||
camoufoxConfig?: CamoufoxConfig;
|
||||
wayfernConfig?: WayfernConfig;
|
||||
groupId?: string;
|
||||
extensionGroupId?: string;
|
||||
@@ -850,7 +818,6 @@ export default function Home() {
|
||||
releaseType: profileData.releaseType,
|
||||
proxyId: profileData.proxyId,
|
||||
vpnId: profileData.vpnId,
|
||||
camoufoxConfig: profileData.camoufoxConfig,
|
||||
wayfernConfig: profileData.wayfernConfig,
|
||||
groupId:
|
||||
profileData.groupId ??
|
||||
@@ -926,7 +893,7 @@ export default function Home() {
|
||||
}
|
||||
|
||||
// Show one-time warning about window resizing for fingerprinted browsers
|
||||
if (profile.browser === "camoufox" || profile.browser === "wayfern") {
|
||||
if (profile.browser === "wayfern") {
|
||||
try {
|
||||
const dismissed = await invoke<boolean>(
|
||||
"get_window_resize_warning_dismissed",
|
||||
@@ -934,7 +901,6 @@ export default function Home() {
|
||||
if (!dismissed) {
|
||||
const proceed = await new Promise<boolean>((resolve) => {
|
||||
windowResizeWarningResolver.current = resolve;
|
||||
setWindowResizeWarningBrowserType(profile.browser);
|
||||
setWindowResizeWarningOpen(true);
|
||||
});
|
||||
if (!proceed) {
|
||||
@@ -1138,9 +1104,7 @@ export default function Home() {
|
||||
const handleBulkCopyCookies = useCallback(() => {
|
||||
if (selectedProfiles.length === 0) return;
|
||||
const eligibleProfiles = profiles.filter(
|
||||
(p) =>
|
||||
selectedProfiles.includes(p.id) &&
|
||||
(p.browser === "wayfern" || p.browser === "camoufox"),
|
||||
(p) => selectedProfiles.includes(p.id) && p.browser === "wayfern",
|
||||
);
|
||||
if (eligibleProfiles.length === 0) {
|
||||
showErrorToast(t("errors.cookieCopyUnsupportedBrowser"));
|
||||
@@ -1400,7 +1364,7 @@ export default function Home() {
|
||||
void checkMissingBinaries();
|
||||
}
|
||||
|
||||
// Proactively download Wayfern and Camoufox if not already available
|
||||
// Proactively download Wayfern if not already available
|
||||
if (!profilesLoading) {
|
||||
void invoke("ensure_active_browsers_downloaded").catch((err: unknown) => {
|
||||
console.error("Failed to auto-download browsers:", err);
|
||||
@@ -1523,40 +1487,6 @@ export default function Home() {
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
// Show warning for non-wayfern/camoufox profiles (support ending March 15, 2026)
|
||||
useEffect(() => {
|
||||
if (profiles.length === 0) return;
|
||||
|
||||
const unsupportedProfiles = profiles.filter(
|
||||
(p) => p.browser !== "wayfern" && p.browser !== "camoufox",
|
||||
);
|
||||
|
||||
if (unsupportedProfiles.length > 0) {
|
||||
const unsupportedNames = unsupportedProfiles
|
||||
.map((p) => p.name)
|
||||
.join(", ");
|
||||
|
||||
showToast({
|
||||
id: "browser-support-ending-warning",
|
||||
type: "error",
|
||||
title: t("browserSupport.endingSoonTitle"),
|
||||
description: t("browserSupport.endingSoonDescription", {
|
||||
profiles: unsupportedNames,
|
||||
}),
|
||||
duration: 15000,
|
||||
action: {
|
||||
label: t("common.buttons.learnMore"),
|
||||
onClick: () => {
|
||||
const event = new CustomEvent("url-open-request", {
|
||||
detail: "https://github.com/zhom/donutbrowser/discussions",
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [profiles, t]);
|
||||
|
||||
// Re-check Wayfern terms when a browser download completes
|
||||
useEffect(() => {
|
||||
let unlisten: (() => void) | null = null;
|
||||
@@ -1639,7 +1569,6 @@ export default function Home() {
|
||||
return (
|
||||
<div className="flex h-dvh flex-col bg-background font-(family-name:--font-geist-sans)">
|
||||
<CloseConfirmDialog />
|
||||
<CamoufoxDeprecationDialog profiles={profiles} />
|
||||
<HomeHeader
|
||||
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
|
||||
searchQuery={searchQuery}
|
||||
@@ -1668,7 +1597,7 @@ export default function Home() {
|
||||
onRemovePassword={handleRemovePassword}
|
||||
onDeleteProfile={handleDeleteProfile}
|
||||
onRenameProfile={handleRenameProfile}
|
||||
onConfigureCamoufox={handleConfigureCamoufox}
|
||||
onConfigureWayfern={handleConfigureWayfern}
|
||||
onCopyCookiesToProfile={handleCopyCookiesToProfile}
|
||||
onOpenCookieManagement={handleOpenCookieManagement}
|
||||
runningProfiles={runningProfiles}
|
||||
@@ -1913,17 +1842,16 @@ export default function Home() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<CamoufoxConfigDialog
|
||||
isOpen={camoufoxConfigDialogOpen}
|
||||
<WayfernConfigDialog
|
||||
isOpen={wayfernConfigDialogOpen}
|
||||
onClose={() => {
|
||||
setCamoufoxConfigDialogOpen(false);
|
||||
setWayfernConfigDialogOpen(false);
|
||||
}}
|
||||
profile={currentProfileForCamoufoxConfig}
|
||||
onSave={handleSaveCamoufoxConfig}
|
||||
onSaveWayfern={handleSaveWayfernConfig}
|
||||
profile={currentProfileForWayfernConfig}
|
||||
onSave={handleSaveWayfernConfig}
|
||||
isRunning={
|
||||
currentProfileForCamoufoxConfig
|
||||
? runningProfiles.has(currentProfileForCamoufoxConfig.id)
|
||||
currentProfileForWayfernConfig
|
||||
? runningProfiles.has(currentProfileForWayfernConfig.id)
|
||||
: false
|
||||
}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
@@ -2116,7 +2044,6 @@ export default function Home() {
|
||||
|
||||
<WindowResizeWarningDialog
|
||||
isOpen={windowResizeWarningOpen}
|
||||
browserType={windowResizeWarningBrowserType}
|
||||
onResult={(proceed) => {
|
||||
setWindowResizeWarningOpen(false);
|
||||
windowResizeWarningResolver.current?.(proceed);
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { WayfernConfigForm } from "@/components/wayfern-config-form";
|
||||
import type {
|
||||
BrowserProfile,
|
||||
CamoufoxConfig,
|
||||
CamoufoxOS,
|
||||
WayfernConfig,
|
||||
} from "@/types";
|
||||
|
||||
const getCurrentOS = (): CamoufoxOS => {
|
||||
if (typeof navigator === "undefined") return "linux";
|
||||
const platform = navigator.platform.toLowerCase();
|
||||
if (platform.includes("win")) return "windows";
|
||||
if (platform.includes("mac")) return "macos";
|
||||
return "linux";
|
||||
};
|
||||
|
||||
import { LoadingButton } from "./loading-button";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface CamoufoxConfigDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
profile: BrowserProfile | null;
|
||||
onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise<void>;
|
||||
onSaveWayfern?: (
|
||||
profile: BrowserProfile,
|
||||
config: CamoufoxConfig,
|
||||
) => Promise<void>;
|
||||
isRunning?: boolean;
|
||||
crossOsUnlocked?: boolean;
|
||||
}
|
||||
|
||||
export function CamoufoxConfigDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
profile,
|
||||
onSave,
|
||||
onSaveWayfern,
|
||||
isRunning = false,
|
||||
crossOsUnlocked = false,
|
||||
}: CamoufoxConfigDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
// Use union type to support both Camoufox and Wayfern configs
|
||||
const [config, setConfig] = useState<CamoufoxConfig | WayfernConfig>(() => ({
|
||||
geoip: true,
|
||||
os: getCurrentOS(),
|
||||
}));
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const isAntiDetectBrowser =
|
||||
profile?.browser === "camoufox" || profile?.browser === "wayfern";
|
||||
|
||||
// Initialize config when profile changes
|
||||
useEffect(() => {
|
||||
if (profile && isAntiDetectBrowser) {
|
||||
const profileConfig =
|
||||
profile.browser === "wayfern"
|
||||
? profile.wayfern_config
|
||||
: profile.camoufox_config;
|
||||
setConfig(
|
||||
profileConfig || {
|
||||
geoip: true,
|
||||
os: getCurrentOS(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [profile, isAntiDetectBrowser]);
|
||||
|
||||
const updateConfig = (
|
||||
key: keyof CamoufoxConfig | keyof WayfernConfig,
|
||||
value: unknown,
|
||||
) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!profile) return;
|
||||
|
||||
// Validate fingerprint JSON if it exists
|
||||
if (config.fingerprint) {
|
||||
try {
|
||||
JSON.parse(config.fingerprint);
|
||||
} catch (_error) {
|
||||
const { toast } = await import("sonner");
|
||||
toast.error(t("camoufoxDialog.invalidFingerprint"), {
|
||||
description: t("camoufoxDialog.invalidFingerprintDescription"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
if (profile.browser === "wayfern" && onSaveWayfern) {
|
||||
await onSaveWayfern(profile, config as CamoufoxConfig);
|
||||
} else {
|
||||
await onSave(profile, config as CamoufoxConfig);
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to save config:", error);
|
||||
const { toast } = await import("sonner");
|
||||
toast.error(t("camoufoxDialog.saveFailed"), {
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("camoufoxDialog.unknownError"),
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Reset config to original when closing without saving
|
||||
if (profile && isAntiDetectBrowser) {
|
||||
const profileConfig =
|
||||
profile.browser === "wayfern"
|
||||
? profile.wayfern_config
|
||||
: profile.camoufox_config;
|
||||
setConfig(
|
||||
profileConfig || {
|
||||
geoip: true,
|
||||
os: getCurrentOS(),
|
||||
},
|
||||
);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!profile || !isAntiDetectBrowser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const browserName = profile.browser === "wayfern" ? "Wayfern" : "Camoufox";
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="flex h-[min(85vh,52rem)] max-w-3xl flex-col">
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle>
|
||||
{isRunning
|
||||
? t("camoufoxDialog.titleView", {
|
||||
name: profile.name,
|
||||
browser: browserName,
|
||||
})
|
||||
: t("camoufoxDialog.titleConfigure", {
|
||||
name: profile.name,
|
||||
browser: browserName,
|
||||
})}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="py-4">
|
||||
{profile.browser === "wayfern" ? (
|
||||
<WayfernConfigForm
|
||||
config={config as WayfernConfig}
|
||||
onConfigChange={updateConfig}
|
||||
forceAdvanced={true}
|
||||
readOnly={isRunning}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
limitedMode={!crossOsUnlocked}
|
||||
profileVersion={profile.version}
|
||||
profileBrowser="wayfern"
|
||||
/>
|
||||
) : (
|
||||
<SharedCamoufoxConfigForm
|
||||
config={config as CamoufoxConfig}
|
||||
onConfigChange={updateConfig}
|
||||
forceAdvanced={true}
|
||||
readOnly={isRunning}
|
||||
browserType="camoufox"
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
limitedMode={!crossOsUnlocked}
|
||||
profileVersion={profile.version}
|
||||
profileBrowser="camoufox"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="shrink-0 border-t pt-4">
|
||||
<RippleButton variant="outline" onClick={handleClose}>
|
||||
{isRunning ? t("common.buttons.close") : t("common.buttons.cancel")}
|
||||
</RippleButton>
|
||||
{!isRunning && (
|
||||
<LoadingButton
|
||||
isLoading={isSaving}
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuTriangleAlert } from "react-icons/lu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface CamoufoxDeprecationDialogProps {
|
||||
profiles: BrowserProfile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Warns users who still have Camoufox profiles that Camoufox support is ending.
|
||||
* Shown once per app session (this component mounts for the app lifetime), only
|
||||
* when at least one Camoufox profile exists. Not a toast — a blocking dialog so
|
||||
* the deprecation can't be missed.
|
||||
*/
|
||||
export function CamoufoxDeprecationDialog({
|
||||
profiles,
|
||||
}: CamoufoxDeprecationDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [shown, setShown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (shown) return;
|
||||
const hasCamoufox = profiles.some((p) => p.browser === "camoufox");
|
||||
if (hasCamoufox) {
|
||||
setIsOpen(true);
|
||||
setShown(true);
|
||||
}
|
||||
}, [profiles, shown]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<LuTriangleAlert className="size-5 text-warning" />
|
||||
{t("camoufoxDeprecation.title")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("camoufoxDeprecation.description")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
void openUrl(
|
||||
"https://github.com/zhom/donutbrowser/discussions/426",
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("common.buttons.learnMore")}
|
||||
</RippleButton>
|
||||
<RippleButton
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("camoufoxDeprecation.acknowledge")}
|
||||
</RippleButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -84,9 +84,7 @@ export function CookieCopyDialog({
|
||||
// dead-end state (source picked = target list empty = copy button disabled).
|
||||
const eligibleSourceProfiles = useMemo(() => {
|
||||
return profiles.filter(
|
||||
(p) =>
|
||||
!selectedProfiles.includes(p.id) &&
|
||||
(p.browser === "wayfern" || p.browser === "camoufox"),
|
||||
(p) => !selectedProfiles.includes(p.id) && p.browser === "wayfern",
|
||||
);
|
||||
}, [profiles, selectedProfiles]);
|
||||
|
||||
@@ -95,7 +93,7 @@ export function CookieCopyDialog({
|
||||
(p) =>
|
||||
selectedProfiles.includes(p.id) &&
|
||||
p.id !== sourceProfileId &&
|
||||
(p.browser === "wayfern" || p.browser === "camoufox"),
|
||||
p.browser === "wayfern",
|
||||
);
|
||||
}, [profiles, selectedProfiles, sourceProfileId]);
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ const getCurrentOS = (): WayfernOS => {
|
||||
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
type BrowserTypeString = "camoufox" | "wayfern";
|
||||
type BrowserTypeString = "wayfern";
|
||||
|
||||
interface CreateProfileDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -113,8 +113,8 @@ export function CreateProfileDialog({
|
||||
const proxyListboxIdAntiDetect = useId();
|
||||
const proxyListboxIdRegular = useId();
|
||||
const [profileName, setProfileName] = useState("");
|
||||
// Camoufox is deprecated: only Wayfern profiles can be created, so the dialog
|
||||
// opens straight into the Wayfern config step (no browser-selection screen).
|
||||
// Only Wayfern profiles can be created, so the dialog opens straight into
|
||||
// the Wayfern config step (no browser-selection screen).
|
||||
const [currentStep, setCurrentStep] = useState<
|
||||
"browser-selection" | "browser-config"
|
||||
>("browser-config");
|
||||
@@ -139,8 +139,7 @@ export function CreateProfileDialog({
|
||||
setCurrentStep("browser-config");
|
||||
};
|
||||
|
||||
// Reset the form fields without leaving the Wayfern config step — Camoufox is
|
||||
// deprecated, so there is no browser-selection screen to go back to.
|
||||
// Reset the form fields without leaving the Wayfern config step.
|
||||
const resetForm = () => {
|
||||
setSelectedBrowser("wayfern");
|
||||
setProfileName("");
|
||||
@@ -286,8 +285,7 @@ export function CreateProfileDialog({
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void loadSupportedBrowsers();
|
||||
// Load downloaded Wayfern versions up front so the availability gate is
|
||||
// accurate. Camoufox is deprecated and no longer creatable.
|
||||
// Load downloaded Wayfern versions up front so the availability gate is accurate.
|
||||
void loadDownloadedVersions("wayfern");
|
||||
// Load release types when a browser is selected
|
||||
if (selectedBrowser) {
|
||||
@@ -395,7 +393,7 @@ export function CreateProfileDialog({
|
||||
: undefined;
|
||||
try {
|
||||
if (activeTab === "anti-detect") {
|
||||
// Camoufox is deprecated — only Wayfern anti-detect profiles are created.
|
||||
// Only Wayfern anti-detect profiles are created.
|
||||
const bestWayfernVersion = getCreatableVersion("wayfern");
|
||||
if (!bestWayfernVersion) {
|
||||
console.error("No Wayfern version available");
|
||||
@@ -430,7 +428,7 @@ export function CreateProfileDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the best available version (stable preferred, nightly as fallback)
|
||||
// Use the latest available Wayfern version
|
||||
const bestVersion = getCreatableVersion(selectedBrowser);
|
||||
if (!bestVersion) {
|
||||
console.error("No version available");
|
||||
@@ -465,8 +463,7 @@ export function CreateProfileDialog({
|
||||
// Cancel any ongoing loading
|
||||
loadingBrowserRef.current = null;
|
||||
|
||||
// Reset all states. Stay on the Wayfern config step — Camoufox is
|
||||
// deprecated, so the browser-selection screen is gone.
|
||||
// Reset all states. Stay on the Wayfern config step.
|
||||
setProfileName("");
|
||||
setCurrentStep("browser-config");
|
||||
setActiveTab("anti-detect");
|
||||
@@ -535,10 +532,7 @@ export function CreateProfileDialog({
|
||||
{currentStep === "browser-selection"
|
||||
? t("createProfile.title")
|
||||
: t("createProfile.configureTitle", {
|
||||
browser:
|
||||
selectedBrowser === "wayfern"
|
||||
? t("createProfile.chromiumLabel")
|
||||
: t("createProfile.firefoxLabel"),
|
||||
browser: t("createProfile.chromiumLabel"),
|
||||
})}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -591,9 +585,6 @@ export function CreateProfileDialog({
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
{/* Camoufox is deprecated — no longer offered for new
|
||||
profiles. Only Wayfern can be created. */}
|
||||
|
||||
{!getCreatableVersion("wayfern") && (
|
||||
<p className="pt-2 text-center text-sm text-muted-foreground">
|
||||
{t("createProfile.browsersDownloading")}
|
||||
@@ -616,7 +607,6 @@ export function CreateProfileDialog({
|
||||
|
||||
<div className="space-y-3">
|
||||
{regularBrowsers.map((browser) => {
|
||||
if (browser.value === "camoufox") return null; // Skip camoufox as it's handled in anti-detect tab
|
||||
const IconComponent = getBrowserIcon(browser.value);
|
||||
return (
|
||||
<Button
|
||||
@@ -904,8 +894,7 @@ export function CreateProfileDialog({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Regular Browser Configuration (should not happen in
|
||||
// the anti-detect tab; Camoufox creation is removed).
|
||||
// Regular Browser Configuration (should not happen in the anti-detect tab).
|
||||
<div className="space-y-4">
|
||||
{selectedBrowser && (
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -13,7 +13,7 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaChrome, FaFirefox } from "react-icons/fa";
|
||||
import { FaChrome } from "react-icons/fa";
|
||||
import { GoPlus } from "react-icons/go";
|
||||
import {
|
||||
LuChevronDown,
|
||||
@@ -652,34 +652,19 @@ export function ExtensionManagementDialog({
|
||||
const renderCompatIcons = useCallback(
|
||||
(compat: string[]) => {
|
||||
const hasChromium = compat.includes("chromium");
|
||||
const hasFirefox = compat.includes("firefox");
|
||||
if (!hasChromium && !hasFirefox) return null;
|
||||
if (!hasChromium) return null;
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{hasChromium && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<FaChrome className="size-3.5 text-muted-foreground" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("extensions.compatibility.chromium")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasFirefox && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<FaFirefox className="size-3.5 text-muted-foreground" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("extensions.compatibility.firefox")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<FaChrome className="size-3.5 text-muted-foreground" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("extensions.compatibility.chromium")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -39,9 +39,7 @@ import { cn } from "@/lib/utils";
|
||||
import type { DetectedProfile, WayfernConfig } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
const getMappedBrowser = (browser: string): "camoufox" | "wayfern" => {
|
||||
if (["firefox", "firefox-developer", "zen"].includes(browser))
|
||||
return "camoufox";
|
||||
const getMappedBrowser = (_browser: string): "wayfern" => {
|
||||
return "wayfern";
|
||||
};
|
||||
|
||||
@@ -90,8 +88,7 @@ export function ImportProfileDialog({
|
||||
useBrowserSupport();
|
||||
const { storedProxies } = useProxyEvents();
|
||||
|
||||
// Firefox-based browsers map to the deprecated Camoufox and can no longer be
|
||||
// imported (the backend rejects them); only offer Chromium-family sources.
|
||||
// Only Chromium-family browsers can be imported as Wayfern profiles.
|
||||
const importableBrowsers = supportedBrowsers.filter(
|
||||
(browser) => getMappedBrowser(browser) === "wayfern",
|
||||
);
|
||||
@@ -189,8 +186,6 @@ export function ImportProfileDialog({
|
||||
browserType,
|
||||
newProfileName,
|
||||
proxyId: selectedProxyId ?? null,
|
||||
// Camoufox import is deprecated/blocked; only Wayfern configs are sent.
|
||||
camoufoxConfig: null,
|
||||
wayfernConfig: mappedBrowser === "wayfern" ? wayfernConfig : null,
|
||||
});
|
||||
|
||||
@@ -587,8 +582,6 @@ export function ImportProfileDialog({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Only Wayfern profiles are importable now (Camoufox/Firefox
|
||||
import is deprecated and blocked). */}
|
||||
<WayfernConfigForm
|
||||
config={wayfernConfig}
|
||||
onConfigChange={(key, value) => {
|
||||
|
||||
@@ -209,7 +209,7 @@ interface TableMeta {
|
||||
|
||||
// Overflow actions
|
||||
onAssignProfilesToGroup?: (profileIds: string[]) => void;
|
||||
onConfigureCamoufox?: (profile: BrowserProfile) => void;
|
||||
onConfigureWayfern?: (profile: BrowserProfile) => void;
|
||||
onCloneProfile?: (profile: BrowserProfile) => void;
|
||||
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
|
||||
onOpenCookieManagement?: (profile: BrowserProfile) => void;
|
||||
@@ -1127,7 +1127,7 @@ interface ProfilesDataTableProps {
|
||||
onCloneProfile: (profile: BrowserProfile) => void | Promise<void>;
|
||||
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
|
||||
onRenameProfile: (profileId: string, newName: string) => Promise<void>;
|
||||
onConfigureCamoufox: (profile: BrowserProfile) => void;
|
||||
onConfigureWayfern: (profile: BrowserProfile) => void;
|
||||
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
|
||||
onOpenCookieManagement?: (profile: BrowserProfile) => void;
|
||||
runningProfiles: Set<string>;
|
||||
@@ -1177,7 +1177,7 @@ export function ProfilesDataTable({
|
||||
onCloneProfile,
|
||||
onDeleteProfile,
|
||||
onRenameProfile,
|
||||
onConfigureCamoufox,
|
||||
onConfigureWayfern,
|
||||
onCopyCookiesToProfile,
|
||||
onOpenCookieManagement,
|
||||
runningProfiles,
|
||||
@@ -1952,7 +1952,7 @@ export function ProfilesDataTable({
|
||||
void onCloneProfile(profile);
|
||||
}
|
||||
: undefined,
|
||||
onConfigureCamoufox,
|
||||
onConfigureWayfern,
|
||||
onCopyCookiesToProfile,
|
||||
onOpenCookieManagement,
|
||||
|
||||
@@ -2030,7 +2030,7 @@ export function ProfilesDataTable({
|
||||
onLaunchProfile,
|
||||
onAssignProfilesToGroup,
|
||||
onCloneProfile,
|
||||
onConfigureCamoufox,
|
||||
onConfigureWayfern,
|
||||
onCopyCookiesToProfile,
|
||||
onOpenCookieManagement,
|
||||
syncStatuses,
|
||||
@@ -2086,10 +2086,7 @@ export function ProfilesDataTable({
|
||||
|
||||
// Cross-OS profiles: show OS icon when checkboxes aren't visible, show checkbox when they are
|
||||
if (isCrossOs && !meta.showCheckboxes && !isSelected) {
|
||||
const resolvedOs =
|
||||
profile.host_os ||
|
||||
profile.camoufox_config?.os ||
|
||||
profile.wayfern_config?.os;
|
||||
const resolvedOs = profile.host_os || profile.wayfern_config?.os;
|
||||
const osName = resolvedOs
|
||||
? getOSDisplayName(resolvedOs)
|
||||
: "another OS";
|
||||
@@ -2128,10 +2125,7 @@ export function ProfilesDataTable({
|
||||
|
||||
// Cross-OS profiles with checkboxes visible: show checkbox (selectable for bulk delete)
|
||||
if (isCrossOs && (meta.showCheckboxes || isSelected)) {
|
||||
const resolvedOs =
|
||||
profile.host_os ||
|
||||
profile.camoufox_config?.os ||
|
||||
profile.wayfern_config?.os;
|
||||
const resolvedOs = profile.host_os || profile.wayfern_config?.os;
|
||||
const osName = resolvedOs
|
||||
? getOSDisplayName(resolvedOs)
|
||||
: "another OS";
|
||||
@@ -3183,7 +3177,6 @@ export function ProfilesDataTable({
|
||||
? t("crossOs.viewOnly", {
|
||||
os: getOSDisplayName(
|
||||
row.original.host_os ||
|
||||
row.original.camoufox_config?.os ||
|
||||
row.original.wayfern_config?.os ||
|
||||
"",
|
||||
),
|
||||
@@ -3273,7 +3266,7 @@ export function ProfilesDataTable({
|
||||
}}
|
||||
onOpenProfileSyncDialog={onOpenProfileSyncDialog}
|
||||
onAssignProfilesToGroup={onAssignProfilesToGroup}
|
||||
onConfigureCamoufox={onConfigureCamoufox}
|
||||
onConfigureWayfern={onConfigureWayfern}
|
||||
onCopyCookiesToProfile={onCopyCookiesToProfile}
|
||||
onOpenCookieManagement={onOpenCookieManagement}
|
||||
onAssignExtensionGroup={onAssignExtensionGroup}
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
LuUsers,
|
||||
LuX,
|
||||
} from "react-icons/lu";
|
||||
import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form";
|
||||
import { SharedFingerprintConfigForm } from "@/components/shared-fingerprint-config-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ColorPicker,
|
||||
@@ -71,7 +71,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { WayfernConfigForm } from "@/components/wayfern-config-form";
|
||||
import { translateBackendError } from "@/lib/backend-errors";
|
||||
import { getProfileIcon } from "@/lib/browser-utils";
|
||||
import { formatRelativeTime } from "@/lib/flag-utils";
|
||||
@@ -79,7 +78,6 @@ import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
BrowserProfile,
|
||||
CamoufoxConfig,
|
||||
ProfileGroup,
|
||||
StoredProxy,
|
||||
VpnConfig,
|
||||
@@ -95,7 +93,7 @@ interface ProfileInfoDialogProps {
|
||||
onOpenTrafficDialog?: (profileId: string) => void;
|
||||
onOpenProfileSyncDialog?: (profile: BrowserProfile) => void;
|
||||
onAssignProfilesToGroup?: (profileIds: string[]) => void;
|
||||
onConfigureCamoufox?: (profile: BrowserProfile) => void;
|
||||
onConfigureWayfern?: (profile: BrowserProfile) => void;
|
||||
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
|
||||
onOpenCookieManagement?: (profile: BrowserProfile) => void;
|
||||
onAssignExtensionGroup?: (profileIds: string[]) => void;
|
||||
@@ -271,7 +269,7 @@ export function ProfileInfoDialog({
|
||||
onOpenTrafficDialog,
|
||||
onOpenProfileSyncDialog,
|
||||
onAssignProfilesToGroup,
|
||||
onConfigureCamoufox,
|
||||
onConfigureWayfern,
|
||||
onCopyCookiesToProfile,
|
||||
onOpenCookieManagement,
|
||||
onAssignExtensionGroup,
|
||||
@@ -340,8 +338,7 @@ export function ProfileInfoDialog({
|
||||
if (!profile) return null;
|
||||
|
||||
const ProfileIcon = getProfileIcon(profile);
|
||||
const isCamoufoxOrWayfern =
|
||||
profile.browser === "camoufox" || profile.browser === "wayfern";
|
||||
const isWayfern = profile.browser === "wayfern";
|
||||
const isDeleteDisabled = isRunning;
|
||||
|
||||
const proxyName = profile.proxy_id
|
||||
@@ -435,13 +432,13 @@ export function ProfileInfoDialog({
|
||||
icon: <LuFingerprint className="size-4" />,
|
||||
label: t("profiles.actions.changeFingerprint"),
|
||||
onClick: () => {
|
||||
handleAction(() => onConfigureCamoufox?.(profile));
|
||||
handleAction(() => onConfigureWayfern?.(profile));
|
||||
},
|
||||
// Viewing and editing fingerprints both require an active paid plan.
|
||||
disabled: isDisabled || !crossOsUnlocked,
|
||||
proBadge: !crossOsUnlocked,
|
||||
runningBadge: isRunning,
|
||||
hidden: !isCamoufoxOrWayfern || !onConfigureCamoufox,
|
||||
hidden: !isWayfern || !onConfigureWayfern,
|
||||
},
|
||||
{
|
||||
icon: <LuUsers className="size-4" />,
|
||||
@@ -463,9 +460,7 @@ export function ProfileInfoDialog({
|
||||
disabled: isDisabled,
|
||||
runningBadge: isRunning,
|
||||
hidden:
|
||||
!isCamoufoxOrWayfern ||
|
||||
profile.ephemeral === true ||
|
||||
!onCopyCookiesToProfile,
|
||||
!isWayfern || profile.ephemeral === true || !onCopyCookiesToProfile,
|
||||
},
|
||||
{
|
||||
id: "cookiesManage",
|
||||
@@ -477,9 +472,7 @@ export function ProfileInfoDialog({
|
||||
disabled: isDisabled,
|
||||
runningBadge: isRunning,
|
||||
hidden:
|
||||
!isCamoufoxOrWayfern ||
|
||||
profile.ephemeral === true ||
|
||||
!onOpenCookieManagement,
|
||||
!isWayfern || profile.ephemeral === true || !onOpenCookieManagement,
|
||||
},
|
||||
{
|
||||
icon: <LuSettings className="size-4" />,
|
||||
@@ -1772,9 +1765,8 @@ function CookiesSectionInline({
|
||||
// Inline password set / change / remove form. Replaces three separate
|
||||
// nested modal dialogs with one in-page form that branches on the current
|
||||
// `password_protected` state of the profile.
|
||||
// Inline fingerprint editor. Reuses SharedCamoufoxConfigForm (Camoufox/Firefox
|
||||
// engine) and WayfernConfigForm (Chromium engine) so the same field set as
|
||||
// the standalone dialog is available without opening a nested modal.
|
||||
// Inline fingerprint editor. Reuses SharedFingerprintConfigForm so the same
|
||||
// field set as the standalone dialog is available without opening a nested modal.
|
||||
function FingerprintSectionInline({
|
||||
profile,
|
||||
isDisabled,
|
||||
@@ -1788,9 +1780,6 @@ function FingerprintSectionInline({
|
||||
onSaved: () => void;
|
||||
t: (key: string, options?: Record<string, unknown>) => string;
|
||||
}) {
|
||||
const [camoufoxConfig, setCamoufoxConfig] = React.useState<CamoufoxConfig>(
|
||||
() => profile.camoufox_config ?? {},
|
||||
);
|
||||
const [wayfernConfig, setWayfernConfig] = React.useState<WayfernConfig>(
|
||||
() => profile.wayfern_config ?? {},
|
||||
);
|
||||
@@ -1798,19 +1787,15 @@ function FingerprintSectionInline({
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [success, setSuccess] = React.useState<string | null>(null);
|
||||
|
||||
// When the underlying profile changes (e.g. a different profile is opened
|
||||
// in the dialog) reset the local form state to match.
|
||||
React.useEffect(() => {
|
||||
setCamoufoxConfig(profile.camoufox_config ?? {});
|
||||
setWayfernConfig(profile.wayfern_config ?? {});
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
}, [profile.camoufox_config, profile.wayfern_config]);
|
||||
}, [profile.wayfern_config]);
|
||||
|
||||
const isCamoufox = profile.browser === "camoufox";
|
||||
const isWayfern = profile.browser === "wayfern";
|
||||
|
||||
if (!isCamoufox && !isWayfern) {
|
||||
if (!isWayfern) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
@@ -1824,9 +1809,6 @@ function FingerprintSectionInline({
|
||||
);
|
||||
}
|
||||
|
||||
// Viewing and editing fingerprints both require an active paid plan
|
||||
// (`crossOsUnlocked` is that paid flag here). Render a locked state instead of
|
||||
// the editor so free users can neither see nor change the fingerprint.
|
||||
if (!crossOsUnlocked) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 rounded-lg border p-6 text-center">
|
||||
@@ -1841,10 +1823,6 @@ function FingerprintSectionInline({
|
||||
);
|
||||
}
|
||||
|
||||
const onCamoufoxChange = (key: keyof CamoufoxConfig, value: unknown) => {
|
||||
setCamoufoxConfig((prev) => ({ ...prev, [key]: value }));
|
||||
setSuccess(null);
|
||||
};
|
||||
const onWayfernChange = (key: keyof WayfernConfig, value: unknown) => {
|
||||
setWayfernConfig((prev) => ({ ...prev, [key]: value }));
|
||||
setSuccess(null);
|
||||
@@ -1855,19 +1833,11 @@ function FingerprintSectionInline({
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
try {
|
||||
if (isCamoufox) {
|
||||
await invoke("update_camoufox_config", {
|
||||
profileId: profile.id,
|
||||
config: camoufoxConfig,
|
||||
});
|
||||
} else {
|
||||
await invoke("update_wayfern_config", {
|
||||
profileId: profile.id,
|
||||
config: wayfernConfig,
|
||||
});
|
||||
}
|
||||
await invoke("update_wayfern_config", {
|
||||
profileId: profile.id,
|
||||
config: wayfernConfig,
|
||||
});
|
||||
setSuccess(t("common.buttons.saved"));
|
||||
// Close the dialog once the fingerprint is saved.
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
setError(translateBackendError(t as never, e));
|
||||
@@ -1876,12 +1846,8 @@ function FingerprintSectionInline({
|
||||
}
|
||||
};
|
||||
|
||||
const initial = isCamoufox
|
||||
? JSON.stringify(profile.camoufox_config ?? {})
|
||||
: JSON.stringify(profile.wayfern_config ?? {});
|
||||
const current = isCamoufox
|
||||
? JSON.stringify(camoufoxConfig)
|
||||
: JSON.stringify(wayfernConfig);
|
||||
const initial = JSON.stringify(profile.wayfern_config ?? {});
|
||||
const current = JSON.stringify(wayfernConfig);
|
||||
const dirty = current !== initial;
|
||||
|
||||
return (
|
||||
@@ -1894,30 +1860,16 @@ function FingerprintSectionInline({
|
||||
{t("profileInfo.sectionDesc.fingerprint")}
|
||||
</p>
|
||||
|
||||
{isCamoufox && (
|
||||
<SharedCamoufoxConfigForm
|
||||
config={camoufoxConfig}
|
||||
onConfigChange={onCamoufoxChange}
|
||||
forceAdvanced={true}
|
||||
readOnly={isDisabled}
|
||||
browserType="camoufox"
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
limitedMode={false}
|
||||
profileVersion={profile.version}
|
||||
profileBrowser={profile.browser}
|
||||
/>
|
||||
)}
|
||||
{isWayfern && (
|
||||
<WayfernConfigForm
|
||||
config={wayfernConfig}
|
||||
onConfigChange={onWayfernChange}
|
||||
forceAdvanced={true}
|
||||
readOnly={isDisabled}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
profileVersion={profile.version}
|
||||
profileBrowser={profile.browser}
|
||||
/>
|
||||
)}
|
||||
<SharedFingerprintConfigForm
|
||||
config={wayfernConfig}
|
||||
onConfigChange={onWayfernChange}
|
||||
forceAdvanced={true}
|
||||
readOnly={isDisabled}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
limitedMode={false}
|
||||
profileVersion={profile.version}
|
||||
profileBrowser={profile.browser}
|
||||
/>
|
||||
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
{success && !error && <p className="text-xs text-success">{success}</p>}
|
||||
@@ -1939,7 +1891,6 @@ function FingerprintSectionInline({
|
||||
variant="ghost"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => {
|
||||
setCamoufoxConfig(profile.camoufox_config ?? {});
|
||||
setWayfernConfig(profile.wayfern_config ?? {});
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useId, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuCheck, LuChevronsUpDown, LuDownload } from "react-icons/lu";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { BrowserReleaseTypes } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface ReleaseTypeSelectorProps {
|
||||
selectedReleaseType: "stable" | "nightly" | null;
|
||||
onReleaseTypeSelect: (releaseType: "stable" | "nightly" | null) => void;
|
||||
availableReleaseTypes: BrowserReleaseTypes;
|
||||
isDownloading: boolean;
|
||||
onDownload: () => void;
|
||||
placeholder?: string;
|
||||
showDownloadButton?: boolean;
|
||||
downloadedVersions?: string[];
|
||||
}
|
||||
|
||||
export function ReleaseTypeSelector({
|
||||
selectedReleaseType,
|
||||
onReleaseTypeSelect,
|
||||
availableReleaseTypes,
|
||||
isDownloading,
|
||||
onDownload,
|
||||
placeholder,
|
||||
showDownloadButton = true,
|
||||
downloadedVersions = [],
|
||||
}: ReleaseTypeSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const listboxId = useId();
|
||||
const effectivePlaceholder =
|
||||
placeholder ?? t("releaseTypeSelector.placeholder");
|
||||
|
||||
const releaseOptions = [
|
||||
...(availableReleaseTypes.stable
|
||||
? [{ type: "stable" as const, version: availableReleaseTypes.stable }]
|
||||
: []),
|
||||
...(availableReleaseTypes.nightly
|
||||
? [{ type: "nightly" as const, version: availableReleaseTypes.nightly }]
|
||||
: []),
|
||||
];
|
||||
|
||||
// Only show dropdown if there are multiple release types available
|
||||
const showDropdown = releaseOptions.length > 1;
|
||||
|
||||
// If only one release type is available, auto-select it
|
||||
if (!showDropdown && releaseOptions.length === 1 && !selectedReleaseType) {
|
||||
setTimeout(() => {
|
||||
onReleaseTypeSelect(releaseOptions[0].type);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
const selectedDisplayText = selectedReleaseType
|
||||
? selectedReleaseType === "stable"
|
||||
? t("releaseTypeSelector.stable")
|
||||
: t("releaseTypeSelector.nightly")
|
||||
: effectivePlaceholder;
|
||||
|
||||
const selectedVersion =
|
||||
selectedReleaseType === "stable"
|
||||
? availableReleaseTypes.stable
|
||||
: selectedReleaseType === "nightly"
|
||||
? availableReleaseTypes.nightly
|
||||
: null;
|
||||
|
||||
const isVersionDownloaded =
|
||||
selectedVersion && downloadedVersions.includes(selectedVersion);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{showDropdown ? (
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal={true}>
|
||||
<PopoverTrigger asChild>
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={popoverOpen}
|
||||
aria-controls={listboxId}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{selectedDisplayText}
|
||||
<LuChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</RippleButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent id={listboxId} className="p-0">
|
||||
<Command>
|
||||
<CommandEmpty>
|
||||
{t("releaseTypeSelector.noReleaseTypes")}
|
||||
</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{releaseOptions.map((option) => {
|
||||
const isDownloaded = downloadedVersions.includes(
|
||||
option.version,
|
||||
);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.type}
|
||||
value={option.type}
|
||||
onSelect={(currentValue) => {
|
||||
const selectedType = currentValue as
|
||||
| "stable"
|
||||
| "nightly";
|
||||
onReleaseTypeSelect(
|
||||
selectedType === selectedReleaseType
|
||||
? null
|
||||
: selectedType,
|
||||
);
|
||||
setPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<LuCheck
|
||||
className={cn(
|
||||
"mr-2 size-4",
|
||||
selectedReleaseType === option.type
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="capitalize">{option.type}</span>
|
||||
{option.type === "nightly" && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t("releaseTypeSelector.nightly")}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{option.version}
|
||||
</Badge>
|
||||
{isDownloaded && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
{t("releaseTypeSelector.downloaded")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
// Show a simple display when only one release type is available
|
||||
releaseOptions.length === 1 && (
|
||||
<div className="flex items-center justify-center gap-2 rounded-md border bg-muted/50 p-3">
|
||||
<span className="text-sm font-medium capitalize">
|
||||
{releaseOptions[0].type}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{releaseOptions[0].version}
|
||||
</Badge>
|
||||
{downloadedVersions.includes(releaseOptions[0].version) && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
{t("releaseTypeSelector.downloaded")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{showDownloadButton &&
|
||||
selectedReleaseType &&
|
||||
selectedVersion &&
|
||||
!isVersionDownloaded && (
|
||||
<LoadingButton
|
||||
isLoading={isDownloading}
|
||||
onClick={() => {
|
||||
onDownload();
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<LuDownload className="mr-2 size-4" />
|
||||
{isDownloading
|
||||
? t("releaseTypeSelector.downloading")
|
||||
: t("releaseTypeSelector.downloadBrowser")}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
export { WayfernConfigForm as SharedFingerprintConfigForm } from "@/components/wayfern-config-form";
|
||||
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SharedFingerprintConfigForm } from "@/components/shared-fingerprint-config-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import type { BrowserProfile, WayfernConfig, WayfernOS } from "@/types";
|
||||
import { LoadingButton } from "./loading-button";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
const getCurrentOS = (): WayfernOS => {
|
||||
if (typeof navigator === "undefined") return "linux";
|
||||
const platform = navigator.platform.toLowerCase();
|
||||
if (platform.includes("win")) return "windows";
|
||||
if (platform.includes("mac")) return "macos";
|
||||
return "linux";
|
||||
};
|
||||
|
||||
interface WayfernConfigDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
profile: BrowserProfile | null;
|
||||
onSave: (profile: BrowserProfile, config: WayfernConfig) => Promise<void>;
|
||||
isRunning?: boolean;
|
||||
crossOsUnlocked?: boolean;
|
||||
}
|
||||
|
||||
export function WayfernConfigDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
profile,
|
||||
onSave,
|
||||
isRunning = false,
|
||||
crossOsUnlocked = false,
|
||||
}: WayfernConfigDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [config, setConfig] = useState<WayfernConfig>(() => ({
|
||||
geoip: true,
|
||||
os: getCurrentOS(),
|
||||
}));
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile?.browser === "wayfern") {
|
||||
setConfig(
|
||||
profile.wayfern_config || {
|
||||
geoip: true,
|
||||
os: getCurrentOS(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
const updateConfig = (key: keyof WayfernConfig, value: unknown) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!profile) return;
|
||||
|
||||
if (config.fingerprint) {
|
||||
try {
|
||||
JSON.parse(config.fingerprint);
|
||||
} catch (_error) {
|
||||
const { toast } = await import("sonner");
|
||||
toast.error(t("wayfernConfigDialog.invalidFingerprint"), {
|
||||
description: t("wayfernConfigDialog.invalidFingerprintDescription"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave(profile, config);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to save config:", error);
|
||||
const { toast } = await import("sonner");
|
||||
toast.error(t("wayfernConfigDialog.saveFailed"), {
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("wayfernConfigDialog.unknownError"),
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (profile?.browser === "wayfern") {
|
||||
setConfig(
|
||||
profile.wayfern_config || {
|
||||
geoip: true,
|
||||
os: getCurrentOS(),
|
||||
},
|
||||
);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (profile?.browser !== "wayfern") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="flex h-[min(85vh,52rem)] max-w-3xl flex-col">
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle>
|
||||
{isRunning
|
||||
? t("wayfernConfigDialog.titleView", {
|
||||
name: profile.name,
|
||||
browser: "Wayfern",
|
||||
})
|
||||
: t("wayfernConfigDialog.titleConfigure", {
|
||||
name: profile.name,
|
||||
browser: "Wayfern",
|
||||
})}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="py-4">
|
||||
<SharedFingerprintConfigForm
|
||||
config={config}
|
||||
onConfigChange={updateConfig}
|
||||
forceAdvanced={true}
|
||||
readOnly={isRunning}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
limitedMode={!crossOsUnlocked}
|
||||
profileVersion={profile.version}
|
||||
profileBrowser="wayfern"
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="shrink-0 border-t pt-4">
|
||||
<RippleButton variant="outline" onClick={handleClose}>
|
||||
{isRunning ? t("common.buttons.close") : t("common.buttons.cancel")}
|
||||
</RippleButton>
|
||||
{!isRunning && (
|
||||
<LoadingButton
|
||||
isLoading={isSaving}
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -17,13 +17,11 @@ import { Label } from "@/components/ui/label";
|
||||
interface WindowResizeWarningDialogProps {
|
||||
isOpen: boolean;
|
||||
onResult: (proceed: boolean) => void;
|
||||
browserType?: string;
|
||||
}
|
||||
|
||||
export function WindowResizeWarningDialog({
|
||||
isOpen,
|
||||
onResult,
|
||||
browserType,
|
||||
}: WindowResizeWarningDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
@@ -49,15 +47,8 @@ export function WindowResizeWarningDialog({
|
||||
onResult(false);
|
||||
};
|
||||
|
||||
const isCamoufox = browserType === "camoufox";
|
||||
|
||||
const title = isCamoufox
|
||||
? t("warnings.windowResizeCamoufoxTitle")
|
||||
: t("warnings.windowResizeTitle");
|
||||
|
||||
const description = isCamoufox
|
||||
? t("warnings.windowResizeCamoufoxDescription")
|
||||
: t("warnings.windowResizeDescription");
|
||||
const title = t("warnings.windowResizeTitle");
|
||||
const description = t("warnings.windowResizeDescription");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen}>
|
||||
|
||||
@@ -26,7 +26,6 @@ interface GithubRelease {
|
||||
|
||||
interface BrowserVersionInfo {
|
||||
version: string;
|
||||
is_prerelease: boolean;
|
||||
date: string;
|
||||
}
|
||||
|
||||
@@ -100,7 +99,7 @@ export function useBrowserDownload() {
|
||||
tag_name: versionInfo.version,
|
||||
assets: [],
|
||||
published_at: versionInfo.date,
|
||||
is_nightly: versionInfo.is_prerelease,
|
||||
is_nightly: false,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -146,7 +145,7 @@ export function useBrowserDownload() {
|
||||
tag_name: versionInfo.version,
|
||||
assets: [],
|
||||
published_at: versionInfo.date,
|
||||
is_nightly: versionInfo.is_prerelease,
|
||||
is_nightly: false,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -402,15 +401,8 @@ export function useBrowserDownload() {
|
||||
next.delete(progress.browser);
|
||||
return next;
|
||||
});
|
||||
// On completion, refresh the downloaded versions for this browser and also refresh camoufox,
|
||||
// since the Create dialog implicitly uses camoufox on the anti-detect tab
|
||||
try {
|
||||
await Promise.all([
|
||||
loadDownloadedVersions(progress.browser),
|
||||
progress.browser !== "camoufox"
|
||||
? loadDownloadedVersions("camoufox")
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
await loadDownloadedVersions(progress.browser);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
@@ -157,10 +157,7 @@ export function useBrowserState(
|
||||
if (!isClient) return "Loading...";
|
||||
|
||||
if (isCrossOsProfile(profile)) {
|
||||
const profileOs =
|
||||
profile.host_os ||
|
||||
profile.camoufox_config?.os ||
|
||||
profile.wayfern_config?.os;
|
||||
const profileOs = profile.host_os || profile.wayfern_config?.os;
|
||||
if (profileOs) {
|
||||
const osName = getOSDisplayName(profileOs);
|
||||
return `This profile was created on ${osName} and cannot be launched on a different operating system.`;
|
||||
|
||||
@@ -10,7 +10,6 @@ interface UpdateNotification {
|
||||
current_version: string;
|
||||
new_version: string;
|
||||
affected_profiles: string[];
|
||||
is_stable_update: boolean;
|
||||
timestamp: number;
|
||||
is_rolling_release: boolean;
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "Anti-Detect Browser",
|
||||
"description": "Choose a browser with anti-detection capabilities",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "Anti-Detect Browser"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "Powered by Wayfern",
|
||||
"firefoxLabel": "Firefox",
|
||||
"firefoxSubtitle": "Powered by Camoufox",
|
||||
"camoufoxWarning": "Firefox (Camoufox) is maintained by a third-party organization. For production use, please use Chromium.",
|
||||
"platformUnavailable": "{{browser}} is not available on your platform yet.",
|
||||
"passwordProtect": {
|
||||
"label": "Password protect this profile",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "Failed to import profile"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Camoufox Configuration",
|
||||
"fingerprint": {
|
||||
"title": "Fingerprint",
|
||||
"randomize": "Randomize on Launch",
|
||||
"randomizeDescription": "Generate a new fingerprint each time the browser is launched.",
|
||||
"osCpuPlaceholder": "e.g., Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "e.g., llvmpipe, or similar"
|
||||
},
|
||||
"os": {
|
||||
"title": "Operating System",
|
||||
"description": "The operating system to emulate for fingerprint generation.",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "Screen Size",
|
||||
"minWidth": "Min Width",
|
||||
"maxWidth": "Max Width",
|
||||
"minHeight": "Min Height",
|
||||
"maxHeight": "Max Height"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "Automatic (based on proxy)",
|
||||
"manual": "Manual",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "Blocking",
|
||||
"images": "Block Images",
|
||||
"webrtc": "Block WebRTC",
|
||||
"webgl": "Block WebGL"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Wayfern Configuration",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "Select a profile to copy cookies from",
|
||||
"running": "(running)",
|
||||
"targetProfiles": "Target Profiles ({{count}})",
|
||||
"noOtherTargets": "No other Wayfern/Camoufox profiles selected",
|
||||
"noOtherTargets": "No other Wayfern profiles selected",
|
||||
"selectSourceFirst": "Select a source profile first",
|
||||
"selectionStatus": "({{selected}} of {{total}} selected)",
|
||||
"searchPlaceholder": "Search domains or cookies...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "Server error. Please try again later.",
|
||||
"unknownError": "An unknown error occurred. Please try again.",
|
||||
"noProfilesForUrl": "No profiles available. Please create a profile first before opening URLs.",
|
||||
"updateCamoufoxConfigFailed": "Failed to update camoufox config: {{error}}",
|
||||
"updateWayfernConfigFailed": "Failed to update wayfern config: {{error}}",
|
||||
"createProfileFailed": "Failed to create profile: {{error}}",
|
||||
"launchBrowserFailed": "Failed to launch browser: {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "Failed to rename profile: {{error}}",
|
||||
"killBrowserFailed": "Failed to kill browser: {{error}}",
|
||||
"deleteSelectedProfilesFailed": "Failed to delete selected profiles: {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "Cookie copy only works with Wayfern and Camoufox profiles",
|
||||
"cookieCopyUnsupportedBrowser": "Cookie copy only works with Wayfern profiles",
|
||||
"updateSyncSettingsFailed": "Failed to update sync settings",
|
||||
"cloneProfileFailed": "Failed to clone profile: {{error}}",
|
||||
"loadSupportedBrowsersFailed": "Failed to load supported browsers",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "Failed to set profile password: {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Custom Window Dimensions",
|
||||
"windowResizeDescription": "Changing browser window dimensions may increase the chance of website detection that browser information is spoofed.",
|
||||
"windowResizeCamoufoxTitle": "Viewport Locked by Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox locks the viewport to the spoofed screen dimensions for anti-fingerprinting. Resizing the window may cause cropped or grey areas. This is expected behavior.",
|
||||
"dontShowAgain": "Don't show this again",
|
||||
"continue": "Continue",
|
||||
"cancel": "Cancel"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "Stop the profile before changing its password."
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "Fingerprint editing is only available for Camoufox and Wayfern profiles.",
|
||||
"notSupported": "Fingerprint editing is only available for Wayfern profiles.",
|
||||
"lockedTitle": "Viewing & editing the fingerprint is a Pro feature",
|
||||
"lockedDescription": "Fingerprint protection is included on every plan. Viewing and editing a profile's fingerprint values is what requires an active paid plan."
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "Extension Group",
|
||||
"compatibility": {
|
||||
"label": "Compatibility",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"both": "Chromium & Firefox"
|
||||
"chromium": "Chromium"
|
||||
},
|
||||
"selectedFile": "Selected file",
|
||||
"namePlaceholder": "Extension name",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ Sent: ",
|
||||
"tooltipReceived": "↓ Received: "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "View Fingerprint Settings - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configure Fingerprint Settings - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Invalid fingerprint configuration",
|
||||
"invalidFingerprintDescription": "The fingerprint configuration contains invalid JSON. Please check your advanced settings.",
|
||||
"saveFailed": "Failed to save configuration",
|
||||
"unknownError": "Unknown error occurred"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "Unknown",
|
||||
"locationToast": "Your proxy location is:",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPNs",
|
||||
"createByCountryHeading": "Create by country"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "No release types available.",
|
||||
"placeholder": "Select release type...",
|
||||
"stable": "Stable",
|
||||
"nightly": "Nightly",
|
||||
"downloaded": "Downloaded",
|
||||
"downloadBrowser": "Download Browser",
|
||||
"downloading": "Downloading..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "{{count}} selected",
|
||||
"clearSelection": "Clear selection"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "The selected proxy isn't working, so the profile wasn't created.",
|
||||
"proxyPaymentRequired": "The selected proxy requires payment (402) — its subscription may have expired — so the profile wasn't created.",
|
||||
"vpnNotWorking": "The selected VPN isn't working, so the profile wasn't created.",
|
||||
"camoufoxImportDeprecated": "Importing Firefox-based (Camoufox) profiles is no longer supported. Camoufox is being deprecated — please use Wayfern instead."
|
||||
"camoufoxImportDeprecated": "Importing Firefox-based profiles is no longer supported. Please use Wayfern instead."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "Profiles",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "Show Donut Browser",
|
||||
"quit": "Quit"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "Browser support ending soon",
|
||||
"endingSoonDescription": "Support for the following profiles will be removed on March 15, 2026: {{profiles}}. Please migrate to Wayfern profiles."
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "Browser automation paused",
|
||||
"description": "Your account was temporarily restricted from Pro browser features, usually from signing in on multiple devices at once. Sign out of other devices, then relaunch the profile to restore it."
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "Camoufox support is ending",
|
||||
"description": "Support for Camoufox profiles is ending on July 8, 2026. You have one or more Camoufox profiles. Please migrate them to Wayfern before then — after that date, Camoufox profiles may stop working.",
|
||||
"acknowledge": "Got it"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "View Fingerprint Settings - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configure Fingerprint Settings - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Invalid fingerprint configuration",
|
||||
"invalidFingerprintDescription": "The fingerprint configuration contains invalid JSON. Please check your advanced settings.",
|
||||
"saveFailed": "Failed to save configuration",
|
||||
"unknownError": "Unknown error occurred"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "No other Wayfern profiles selected"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "Navegador Anti-Detección",
|
||||
"description": "Elige un navegador con capacidades anti-detección",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "Navegador Anti-Detección"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "Impulsado por Wayfern",
|
||||
"firefoxLabel": "Firefox",
|
||||
"firefoxSubtitle": "Impulsado por Camoufox",
|
||||
"camoufoxWarning": "Firefox (Camoufox) está mantenido por una organización de terceros. Para uso en producción, utilice Chromium.",
|
||||
"platformUnavailable": "{{browser}} aún no está disponible en tu plataforma.",
|
||||
"passwordProtect": {
|
||||
"label": "Proteger este perfil con contraseña",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "Error al importar perfil"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Configuración de Camoufox",
|
||||
"fingerprint": {
|
||||
"title": "Huella Digital",
|
||||
"randomize": "Aleatorizar al Iniciar",
|
||||
"randomizeDescription": "Genera una nueva huella digital cada vez que se inicia el navegador.",
|
||||
"osCpuPlaceholder": "p. ej., Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "p. ej., llvmpipe, o similar"
|
||||
},
|
||||
"os": {
|
||||
"title": "Sistema Operativo",
|
||||
"description": "El sistema operativo a emular para la generación de huellas digitales.",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "Tamaño de Pantalla",
|
||||
"minWidth": "Ancho Mín",
|
||||
"maxWidth": "Ancho Máx",
|
||||
"minHeight": "Alto Mín",
|
||||
"maxHeight": "Alto Máx"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "Automático (basado en proxy)",
|
||||
"manual": "Manual",
|
||||
"disabled": "Deshabilitado"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "Bloqueo",
|
||||
"images": "Bloquear Imágenes",
|
||||
"webrtc": "Bloquear WebRTC",
|
||||
"webgl": "Bloquear WebGL"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Configuración de Wayfern",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "Selecciona un perfil del que copiar cookies",
|
||||
"running": "(en ejecución)",
|
||||
"targetProfiles": "Perfiles de destino ({{count}})",
|
||||
"noOtherTargets": "No hay otros perfiles Wayfern/Camoufox seleccionados",
|
||||
"noOtherTargets": "No hay otros perfiles Wayfern seleccionados",
|
||||
"selectSourceFirst": "Selecciona primero un perfil de origen",
|
||||
"selectionStatus": "({{selected}} de {{total}} seleccionadas)",
|
||||
"searchPlaceholder": "Buscar dominios o cookies...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "Error del servidor. Por favor intenta de nuevo más tarde.",
|
||||
"unknownError": "Ocurrió un error desconocido. Por favor intenta de nuevo.",
|
||||
"noProfilesForUrl": "No hay perfiles disponibles. Crea un perfil antes de abrir URLs.",
|
||||
"updateCamoufoxConfigFailed": "Error al actualizar la configuración de camoufox: {{error}}",
|
||||
"updateWayfernConfigFailed": "Error al actualizar la configuración de wayfern: {{error}}",
|
||||
"createProfileFailed": "Error al crear el perfil: {{error}}",
|
||||
"launchBrowserFailed": "Error al iniciar el navegador: {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "Error al renombrar el perfil: {{error}}",
|
||||
"killBrowserFailed": "Error al detener el navegador: {{error}}",
|
||||
"deleteSelectedProfilesFailed": "Error al eliminar los perfiles seleccionados: {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "La copia de cookies sólo funciona con perfiles Wayfern y Camoufox",
|
||||
"cookieCopyUnsupportedBrowser": "La copia de cookies solo funciona con perfiles Wayfern",
|
||||
"updateSyncSettingsFailed": "Error al actualizar los ajustes de sincronización",
|
||||
"cloneProfileFailed": "Error al clonar el perfil: {{error}}",
|
||||
"loadSupportedBrowsersFailed": "Error al cargar los navegadores compatibles",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "Error al establecer la contraseña del perfil: {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Dimensiones de ventana personalizadas",
|
||||
"windowResizeDescription": "Cambiar las dimensiones de la ventana del navegador puede aumentar la posibilidad de que los sitios web detecten que la información del navegador está falsificada.",
|
||||
"windowResizeCamoufoxTitle": "Viewport bloqueado por Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox bloquea el viewport a las dimensiones de pantalla falsificadas para anti-fingerprinting. Redimensionar la ventana puede causar áreas recortadas o grises. Este es el comportamiento esperado.",
|
||||
"dontShowAgain": "No mostrar esto de nuevo",
|
||||
"continue": "Continuar",
|
||||
"cancel": "Cancelar"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "Detén el perfil antes de cambiar su contraseña."
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "La edición de huellas digitales solo está disponible para perfiles Camoufox y Wayfern.",
|
||||
"notSupported": "La edición de huellas digitales solo está disponible para perfiles Wayfern.",
|
||||
"lockedTitle": "Ver y editar la huella digital es una función Pro",
|
||||
"lockedDescription": "La protección de huella digital está incluida en todos los planes. Ver y editar los valores de la huella digital de un perfil es lo que requiere un plan de pago activo."
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "Grupo de Extensiones",
|
||||
"compatibility": {
|
||||
"label": "Compatibilidad",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"both": "Chromium y Firefox"
|
||||
"chromium": "Chromium"
|
||||
},
|
||||
"selectedFile": "Archivo seleccionado",
|
||||
"namePlaceholder": "Nombre de la extensión",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ Enviado: ",
|
||||
"tooltipReceived": "↓ Recibido: "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "Ver configuración de huella - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurar huella - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Configuración de huella inválida",
|
||||
"invalidFingerprintDescription": "La configuración de huella contiene JSON inválido. Revisa la configuración avanzada.",
|
||||
"saveFailed": "Error al guardar la configuración",
|
||||
"unknownError": "Ocurrió un error desconocido"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "Desconocido",
|
||||
"locationToast": "La ubicación de tu proxy es:",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "Crear por país"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "No hay tipos de versión disponibles.",
|
||||
"placeholder": "Selecciona el tipo de versión...",
|
||||
"stable": "Estable",
|
||||
"nightly": "Nightly",
|
||||
"downloaded": "Descargado",
|
||||
"downloadBrowser": "Descargar navegador",
|
||||
"downloading": "Descargando..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "{{count}} seleccionados",
|
||||
"clearSelection": "Limpiar selección"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "El proxy seleccionado no funciona, por lo que no se creó el perfil.",
|
||||
"proxyPaymentRequired": "El proxy seleccionado requiere pago (402) —su suscripción puede haber vencido— por lo que no se creó el perfil.",
|
||||
"vpnNotWorking": "La VPN seleccionada no funciona, por lo que no se creó el perfil.",
|
||||
"camoufoxImportDeprecated": "La importación de perfiles basados en Firefox (Camoufox) ya no es compatible. Camoufox está en desuso; usa Wayfern en su lugar."
|
||||
"camoufoxImportDeprecated": "La importación de perfiles basados en Firefox ya no está soportada. Utilice Wayfern en su lugar."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "Perfiles",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "Mostrar Donut Browser",
|
||||
"quit": "Salir"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "El soporte del navegador finalizará pronto",
|
||||
"endingSoonDescription": "El soporte para los siguientes perfiles se eliminará el 15 de marzo de 2026: {{profiles}}. Migra a perfiles de Wayfern."
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "Automatización del navegador en pausa",
|
||||
"description": "Tu cuenta fue restringida temporalmente de las funciones Pro del navegador, normalmente por iniciar sesión en varios dispositivos a la vez. Cierra sesión en los demás dispositivos y vuelve a iniciar el perfil para restaurarla."
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "El soporte para Camoufox está terminando",
|
||||
"description": "El soporte para los perfiles de Camoufox terminará el 8 de julio de 2026. Tienes uno o más perfiles de Camoufox. Migra a Wayfern antes de esa fecha; después, los perfiles de Camoufox podrían dejar de funcionar.",
|
||||
"acknowledge": "Entendido"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Ver configuración de huella - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurar huella - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Configuración de huella inválida",
|
||||
"invalidFingerprintDescription": "La configuración de huella contiene JSON inválido. Revisa la configuración avanzada.",
|
||||
"saveFailed": "Error al guardar la configuración",
|
||||
"unknownError": "Ocurrió un error desconocido"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "No hay otros perfiles Wayfern seleccionados"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "Navigateur anti-détection",
|
||||
"description": "Choisissez un navigateur avec des capacités anti-détection",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "Navigateur anti-détection"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "Propulsé par Wayfern",
|
||||
"firefoxLabel": "Firefox",
|
||||
"firefoxSubtitle": "Propulsé par Camoufox",
|
||||
"camoufoxWarning": "Firefox (Camoufox) est maintenu par une organisation tierce. Pour une utilisation en production, veuillez utiliser Chromium.",
|
||||
"platformUnavailable": "{{browser}} n'est pas encore disponible sur votre plateforme.",
|
||||
"passwordProtect": {
|
||||
"label": "Protéger ce profil par mot de passe",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "Échec de l'importation du profil"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Configuration Camoufox",
|
||||
"fingerprint": {
|
||||
"title": "Empreinte digitale",
|
||||
"randomize": "Randomiser au lancement",
|
||||
"randomizeDescription": "Génère une nouvelle empreinte digitale à chaque lancement du navigateur.",
|
||||
"osCpuPlaceholder": "p. ex., Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "p. ex., llvmpipe, ou similaire"
|
||||
},
|
||||
"os": {
|
||||
"title": "Système d'exploitation",
|
||||
"description": "Le système d'exploitation à émuler pour la génération d'empreinte digitale.",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "Taille d'écran",
|
||||
"minWidth": "Largeur min",
|
||||
"maxWidth": "Largeur max",
|
||||
"minHeight": "Hauteur min",
|
||||
"maxHeight": "Hauteur max"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "Automatique (basé sur le proxy)",
|
||||
"manual": "Manuel",
|
||||
"disabled": "Désactivé"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "Blocage",
|
||||
"images": "Bloquer les images",
|
||||
"webrtc": "Bloquer WebRTC",
|
||||
"webgl": "Bloquer WebGL"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Configuration Wayfern",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "Sélectionnez un profil pour copier les cookies",
|
||||
"running": "(en cours)",
|
||||
"targetProfiles": "Profils cibles ({{count}})",
|
||||
"noOtherTargets": "Aucun autre profil Wayfern/Camoufox sélectionné",
|
||||
"noOtherTargets": "Aucun autre profil Wayfern sélectionné",
|
||||
"selectSourceFirst": "Sélectionnez d'abord un profil source",
|
||||
"selectionStatus": "({{selected}} sur {{total}} sélectionnés)",
|
||||
"searchPlaceholder": "Rechercher des domaines ou cookies...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "Erreur serveur. Veuillez réessayer plus tard.",
|
||||
"unknownError": "Une erreur inconnue s'est produite. Veuillez réessayer.",
|
||||
"noProfilesForUrl": "Aucun profil disponible. Veuillez créer un profil avant d’ouvrir des URL.",
|
||||
"updateCamoufoxConfigFailed": "Échec de la mise à jour de la configuration camoufox : {{error}}",
|
||||
"updateWayfernConfigFailed": "Échec de la mise à jour de la configuration wayfern : {{error}}",
|
||||
"createProfileFailed": "Échec de la création du profil : {{error}}",
|
||||
"launchBrowserFailed": "Échec du lancement du navigateur : {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "Échec du renommage du profil : {{error}}",
|
||||
"killBrowserFailed": "Échec de l’arrêt du navigateur : {{error}}",
|
||||
"deleteSelectedProfilesFailed": "Échec de la suppression des profils sélectionnés : {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "La copie de cookies ne fonctionne qu’avec les profils Wayfern et Camoufox",
|
||||
"cookieCopyUnsupportedBrowser": "La copie de cookies ne fonctionne qu'avec les profils Wayfern",
|
||||
"updateSyncSettingsFailed": "Échec de la mise à jour des paramètres de synchronisation",
|
||||
"cloneProfileFailed": "Échec du clonage du profil : {{error}}",
|
||||
"loadSupportedBrowsersFailed": "Échec du chargement des navigateurs pris en charge",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "Échec de la définition du mot de passe du profil : {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Dimensions de fenêtre personnalisées",
|
||||
"windowResizeDescription": "Modifier les dimensions de la fenêtre du navigateur peut augmenter les chances de détection par les sites web que les informations du navigateur sont falsifiées.",
|
||||
"windowResizeCamoufoxTitle": "Viewport verrouillé par Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox verrouille le viewport aux dimensions d'écran falsifiées pour l'anti-fingerprinting. Redimensionner la fenêtre peut causer des zones recadrées ou grises. C'est le comportement attendu.",
|
||||
"dontShowAgain": "Ne plus afficher",
|
||||
"continue": "Continuer",
|
||||
"cancel": "Annuler"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "Arrêtez le profil avant de modifier son mot de passe."
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "L’édition des empreintes n’est disponible que pour les profils Camoufox et Wayfern.",
|
||||
"notSupported": "L'édition des empreintes n'est disponible que pour les profils Wayfern.",
|
||||
"lockedTitle": "Afficher et modifier l'empreinte est une fonctionnalité Pro",
|
||||
"lockedDescription": "La protection contre le fingerprinting est incluse dans tous les forfaits. C'est l'affichage et la modification des valeurs de l'empreinte d'un profil qui nécessitent un forfait payant actif."
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "Groupe d'Extensions",
|
||||
"compatibility": {
|
||||
"label": "Compatibilité",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"both": "Chromium et Firefox"
|
||||
"chromium": "Chromium"
|
||||
},
|
||||
"selectedFile": "Fichier sélectionné",
|
||||
"namePlaceholder": "Nom de l'extension",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ Envoyé : ",
|
||||
"tooltipReceived": "↓ Reçu : "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "Voir les paramètres d'empreinte - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurer les paramètres d'empreinte - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Configuration d'empreinte invalide",
|
||||
"invalidFingerprintDescription": "La configuration d'empreinte contient du JSON invalide. Vérifiez vos paramètres avancés.",
|
||||
"saveFailed": "Échec de la sauvegarde de la configuration",
|
||||
"unknownError": "Une erreur inconnue s'est produite"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "Inconnu",
|
||||
"locationToast": "L'emplacement de votre proxy est :",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "Créer par pays"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "Aucun type de version disponible.",
|
||||
"placeholder": "Sélectionnez le type de version...",
|
||||
"stable": "Stable",
|
||||
"nightly": "Nightly",
|
||||
"downloaded": "Téléchargé",
|
||||
"downloadBrowser": "Télécharger le navigateur",
|
||||
"downloading": "Téléchargement..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "{{count}} sélectionné(s)",
|
||||
"clearSelection": "Effacer la sélection"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "Le proxy sélectionné ne fonctionne pas, le profil n'a donc pas été créé.",
|
||||
"proxyPaymentRequired": "Le proxy sélectionné requiert un paiement (402) — son abonnement a peut-être expiré — le profil n'a donc pas été créé.",
|
||||
"vpnNotWorking": "Le VPN sélectionné ne fonctionne pas, le profil n'a donc pas été créé.",
|
||||
"camoufoxImportDeprecated": "L'importation de profils basés sur Firefox (Camoufox) n'est plus prise en charge. Camoufox est en cours d'abandon — utilisez Wayfern à la place."
|
||||
"camoufoxImportDeprecated": "L'importation de profils basés sur Firefox n'est plus prise en charge. Utilisez Wayfern à la place."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "Profils",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "Afficher Donut Browser",
|
||||
"quit": "Quitter"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "La prise en charge du navigateur prend bientôt fin",
|
||||
"endingSoonDescription": "La prise en charge des profils suivants sera supprimée le 15 mars 2026 : {{profiles}}. Veuillez migrer vers des profils Wayfern."
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "Automatisation du navigateur en pause",
|
||||
"description": "Votre compte a été temporairement privé des fonctionnalités Pro du navigateur, généralement à cause d'une connexion sur plusieurs appareils à la fois. Déconnectez-vous des autres appareils, puis relancez le profil pour la rétablir."
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "La prise en charge de Camoufox prend fin",
|
||||
"description": "La prise en charge des profils Camoufox prendra fin le 8 juillet 2026. Vous avez un ou plusieurs profils Camoufox. Migrez-les vers Wayfern avant cette date — après quoi, les profils Camoufox pourraient cesser de fonctionner.",
|
||||
"acknowledge": "Compris"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Voir les paramètres d'empreinte - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurer les paramètres d'empreinte - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Configuration d'empreinte invalide",
|
||||
"invalidFingerprintDescription": "La configuration d'empreinte contient du JSON invalide. Vérifiez vos paramètres avancés.",
|
||||
"saveFailed": "Échec de la sauvegarde de la configuration",
|
||||
"unknownError": "Une erreur inconnue s'est produite"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "Aucun autre profil Wayfern sélectionné"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "アンチ検出ブラウザ",
|
||||
"description": "アンチ検出機能を持つブラウザを選択",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "アンチ検出ブラウザ"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "Wayfern搭載",
|
||||
"firefoxLabel": "Firefox",
|
||||
"firefoxSubtitle": "Camoufox搭載",
|
||||
"camoufoxWarning": "Firefox(Camoufox)はサードパーティの組織によって管理されています。本番環境での使用にはChromiumをご利用ください。",
|
||||
"platformUnavailable": "{{browser}} はまだお使いのプラットフォームで利用できません。",
|
||||
"passwordProtect": {
|
||||
"label": "このプロファイルをパスワードで保護",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "プロファイルのインポートに失敗しました"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Camoufox設定",
|
||||
"fingerprint": {
|
||||
"title": "フィンガープリント",
|
||||
"randomize": "起動時にランダム化",
|
||||
"randomizeDescription": "ブラウザ起動時に新しいフィンガープリントを生成します。",
|
||||
"osCpuPlaceholder": "例: Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "例: llvmpipe など"
|
||||
},
|
||||
"os": {
|
||||
"title": "オペレーティングシステム",
|
||||
"description": "フィンガープリント生成用にエミュレートするOS。",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "画面サイズ",
|
||||
"minWidth": "最小幅",
|
||||
"maxWidth": "最大幅",
|
||||
"minHeight": "最小高さ",
|
||||
"maxHeight": "最大高さ"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "自動(プロキシに基づく)",
|
||||
"manual": "手動",
|
||||
"disabled": "無効"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "ブロック",
|
||||
"images": "画像をブロック",
|
||||
"webrtc": "WebRTCをブロック",
|
||||
"webgl": "WebGLをブロック"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Wayfern設定",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "Cookie をコピーするプロファイルを選択",
|
||||
"running": "(実行中)",
|
||||
"targetProfiles": "対象プロファイル ({{count}})",
|
||||
"noOtherTargets": "他に Wayfern/Camoufox プロファイルが選択されていません",
|
||||
"noOtherTargets": "他に Wayfern プロファイルが選択されていません",
|
||||
"selectSourceFirst": "最初にソースプロファイルを選択してください",
|
||||
"selectionStatus": "({{selected}} / {{total}} 選択中)",
|
||||
"searchPlaceholder": "ドメインまたは Cookie を検索...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "サーバーエラー。後でもう一度お試しください。",
|
||||
"unknownError": "不明なエラーが発生しました。もう一度お試しください。",
|
||||
"noProfilesForUrl": "利用可能なプロファイルがありません。URLを開く前にプロファイルを作成してください。",
|
||||
"updateCamoufoxConfigFailed": "camoufox設定の更新に失敗しました: {{error}}",
|
||||
"updateWayfernConfigFailed": "wayfern設定の更新に失敗しました: {{error}}",
|
||||
"createProfileFailed": "プロファイルの作成に失敗しました: {{error}}",
|
||||
"launchBrowserFailed": "ブラウザの起動に失敗しました: {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "プロファイルの名前変更に失敗しました: {{error}}",
|
||||
"killBrowserFailed": "ブラウザの停止に失敗しました: {{error}}",
|
||||
"deleteSelectedProfilesFailed": "選択したプロファイルの削除に失敗しました: {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "Cookieのコピーは Wayfern および Camoufox プロファイルでのみ機能します",
|
||||
"cookieCopyUnsupportedBrowser": "Cookieのコピーは Wayfern プロファイルでのみ機能します",
|
||||
"updateSyncSettingsFailed": "同期設定の更新に失敗しました",
|
||||
"cloneProfileFailed": "プロファイルの複製に失敗しました: {{error}}",
|
||||
"loadSupportedBrowsersFailed": "サポートされているブラウザの読み込みに失敗しました",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "プロファイルのパスワード設定に失敗しました: {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "カスタムウィンドウサイズ",
|
||||
"windowResizeDescription": "ブラウザウィンドウのサイズを変更すると、ブラウザ情報が偽装されていることをウェブサイトに検出される可能性が高くなります。",
|
||||
"windowResizeCamoufoxTitle": "Camoufoxによりビューポートがロックされています",
|
||||
"windowResizeCamoufoxDescription": "Camoufoxはアンチフィンガープリントのためにビューポートを偽装された画面サイズにロックします。ウィンドウのサイズを変更すると、切り取られた領域やグレーの領域が表示される場合があります。これは想定された動作です。",
|
||||
"dontShowAgain": "今後表示しない",
|
||||
"continue": "続行",
|
||||
"cancel": "キャンセル"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "パスワードを変更する前にプロファイルを停止してください。"
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "フィンガープリント編集は Camoufox / Wayfern プロファイルでのみ利用できます。",
|
||||
"notSupported": "フィンガープリント編集は Wayfern プロファイルでのみ利用できます。",
|
||||
"lockedTitle": "フィンガープリントの表示と編集は Pro 機能です",
|
||||
"lockedDescription": "フィンガープリント保護はすべてのプランに含まれています。プロファイルのフィンガープリントの値を表示・編集するには、有効な有料プランが必要です。"
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "拡張機能グループ",
|
||||
"compatibility": {
|
||||
"label": "互換性",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"both": "Chromium & Firefox"
|
||||
"chromium": "Chromium"
|
||||
},
|
||||
"selectedFile": "選択されたファイル",
|
||||
"namePlaceholder": "拡張機能名",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ 送信: ",
|
||||
"tooltipReceived": "↓ 受信: "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "フィンガープリント設定を表示 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "フィンガープリント設定 - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "無効なフィンガープリント設定",
|
||||
"invalidFingerprintDescription": "フィンガープリント設定に無効な JSON が含まれています。詳細設定を確認してください。",
|
||||
"saveFailed": "設定の保存に失敗しました",
|
||||
"unknownError": "不明なエラーが発生しました"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "不明",
|
||||
"locationToast": "プロキシの場所:",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "国別に作成"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "利用可能なリリースタイプがありません。",
|
||||
"placeholder": "リリースタイプを選択...",
|
||||
"stable": "Stable",
|
||||
"nightly": "Nightly",
|
||||
"downloaded": "ダウンロード済み",
|
||||
"downloadBrowser": "ブラウザをダウンロード",
|
||||
"downloading": "ダウンロード中..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "{{count}} 件選択",
|
||||
"clearSelection": "選択を解除"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "選択したプロキシが機能していないため、プロファイルは作成されませんでした。",
|
||||
"proxyPaymentRequired": "選択したプロキシは支払いが必要です(402)。サブスクリプションが期限切れの可能性があります。そのため、プロファイルは作成されませんでした。",
|
||||
"vpnNotWorking": "選択したVPNが機能していないため、プロファイルは作成されませんでした。",
|
||||
"camoufoxImportDeprecated": "Firefox ベース(Camoufox)のプロファイルのインポートはサポートされなくなりました。Camoufox は廃止予定です。代わりに Wayfern をご利用ください。"
|
||||
"camoufoxImportDeprecated": "Firefox ベースのプロファイルのインポートはサポートされなくなりました。Wayfern をご利用ください。"
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "プロファイル",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "Donut Browser を表示",
|
||||
"quit": "終了"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "ブラウザのサポートが間もなく終了します",
|
||||
"endingSoonDescription": "以下のプロファイルのサポートは 2026 年 3 月 15 日に削除されます: {{profiles}}。Wayfern プロファイルに移行してください。"
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "ブラウザの自動化が一時停止しました",
|
||||
"description": "通常は複数のデバイスで同時にサインインしたことが原因で、アカウントのProブラウザ機能が一時的に制限されました。他のデバイスからサインアウトし、プロファイルを再起動すると復元されます。"
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "Camoufox のサポートが終了します",
|
||||
"description": "Camoufox プロファイルのサポートは 2026 年 7 月 8 日に終了します。Camoufox プロファイルが 1 つ以上あります。それまでに Wayfern へ移行してください。その後、Camoufox プロファイルは動作しなくなる可能性があります。",
|
||||
"acknowledge": "了解しました"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "フィンガープリント設定を表示 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "フィンガープリント設定 - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "無効なフィンガープリント設定",
|
||||
"invalidFingerprintDescription": "フィンガープリント設定に無効な JSON が含まれています。詳細設定を確認してください。",
|
||||
"saveFailed": "設定の保存に失敗しました",
|
||||
"unknownError": "不明なエラーが発生しました"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "他に Wayfern プロファイルが選択されていません"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "안티-디텍트 브라우저",
|
||||
"description": "안티-디텍트 기능을 갖춘 브라우저를 선택하세요",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "안티-디텍트 브라우저"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "크로미움",
|
||||
"chromiumSubtitle": "Wayfern 기반",
|
||||
"firefoxLabel": "파이어폭스",
|
||||
"firefoxSubtitle": "Camoufox 기반",
|
||||
"camoufoxWarning": "파이어폭스(Camoufox)는 제3자 조직에서 유지 관리합니다. 프로덕션 용도로는 크로미움을 사용하세요.",
|
||||
"platformUnavailable": "{{browser}}는 아직 이 플랫폼에서 사용할 수 없습니다.",
|
||||
"passwordProtect": {
|
||||
"label": "이 프로필을 비밀번호로 보호",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "프로필 가져오기 실패"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Camoufox 구성",
|
||||
"fingerprint": {
|
||||
"title": "핑거프린트",
|
||||
"randomize": "실행 시 무작위화",
|
||||
"randomizeDescription": "브라우저가 실행될 때마다 새 핑거프린트를 생성합니다.",
|
||||
"osCpuPlaceholder": "예: Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "예: llvmpipe 또는 유사"
|
||||
},
|
||||
"os": {
|
||||
"title": "운영 체제",
|
||||
"description": "핑거프린트 생성을 위해 에뮬레이트할 운영 체제입니다.",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "화면 크기",
|
||||
"minWidth": "최소 너비",
|
||||
"maxWidth": "최대 너비",
|
||||
"minHeight": "최소 높이",
|
||||
"maxHeight": "최대 높이"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "자동 (프록시 기반)",
|
||||
"manual": "수동",
|
||||
"disabled": "사용 안 함"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "차단",
|
||||
"images": "이미지 차단",
|
||||
"webrtc": "WebRTC 차단",
|
||||
"webgl": "WebGL 차단"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Wayfern 구성",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "쿠키를 복사할 원본 프로필 선택",
|
||||
"running": "(실행 중)",
|
||||
"targetProfiles": "대상 프로필 ({{count}})",
|
||||
"noOtherTargets": "선택된 다른 Wayfern/Camoufox 프로필이 없습니다",
|
||||
"noOtherTargets": "선택된 다른 Wayfern 프로필이 없습니다",
|
||||
"selectSourceFirst": "먼저 원본 프로필을 선택하세요",
|
||||
"selectionStatus": "({{total}}개 중 {{selected}}개 선택됨)",
|
||||
"searchPlaceholder": "도메인 또는 쿠키 검색...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "서버 오류. 나중에 다시 시도하세요.",
|
||||
"unknownError": "알 수 없는 오류가 발생했습니다. 다시 시도하세요.",
|
||||
"noProfilesForUrl": "사용할 수 있는 프로필이 없습니다. URL을 열기 전에 먼저 프로필을 생성하세요.",
|
||||
"updateCamoufoxConfigFailed": "Camoufox 구성 업데이트 실패: {{error}}",
|
||||
"updateWayfernConfigFailed": "Wayfern 구성 업데이트 실패: {{error}}",
|
||||
"createProfileFailed": "프로필 생성 실패: {{error}}",
|
||||
"launchBrowserFailed": "브라우저 실행 실패: {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "프로필 이름 변경 실패: {{error}}",
|
||||
"killBrowserFailed": "브라우저 종료 실패: {{error}}",
|
||||
"deleteSelectedProfilesFailed": "선택한 프로필 삭제 실패: {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "쿠키 복사는 Wayfern 및 Camoufox 프로필에서만 작동합니다",
|
||||
"cookieCopyUnsupportedBrowser": "쿠키 복사는 Wayfern 프로필에서만 작동합니다",
|
||||
"updateSyncSettingsFailed": "동기화 설정 업데이트 실패",
|
||||
"cloneProfileFailed": "프로필 복제 실패: {{error}}",
|
||||
"loadSupportedBrowsersFailed": "지원되는 브라우저 불러오기 실패",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "프로필 비밀번호 설정 실패: {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "사용자 지정 창 크기",
|
||||
"windowResizeDescription": "브라우저 창 크기를 변경하면 브라우저 정보가 스푸핑된 것으로 웹사이트에서 감지될 가능성이 높아질 수 있습니다.",
|
||||
"windowResizeCamoufoxTitle": "Camoufox에서 잠긴 뷰포트",
|
||||
"windowResizeCamoufoxDescription": "Camoufox는 안티-핑거프린팅을 위해 뷰포트를 스푸핑된 화면 크기로 잠급니다. 창 크기를 조정하면 잘리거나 회색 영역이 발생할 수 있습니다. 이는 예상된 동작입니다.",
|
||||
"dontShowAgain": "다시 표시하지 않음",
|
||||
"continue": "계속",
|
||||
"cancel": "취소"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "비밀번호를 변경하기 전에 프로필을 중지하세요."
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "핑거프린트 편집은 Camoufox 및 Wayfern 프로필에서만 사용할 수 있습니다.",
|
||||
"notSupported": "핑거프린트 편집은 Wayfern 프로필에서만 사용할 수 있습니다.",
|
||||
"lockedTitle": "핑거프린트 보기 및 편집은 Pro 기능입니다",
|
||||
"lockedDescription": "핑거프린트 보호는 모든 요금제에 포함되어 있습니다. 프로필의 핑거프린트 값을 보고 편집하려면 활성 유료 요금제가 필요합니다."
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "확장 프로그램 그룹",
|
||||
"compatibility": {
|
||||
"label": "호환성",
|
||||
"chromium": "크로미움",
|
||||
"firefox": "파이어폭스",
|
||||
"both": "크로미움 및 파이어폭스"
|
||||
"chromium": "크로미움"
|
||||
},
|
||||
"selectedFile": "선택된 파일",
|
||||
"namePlaceholder": "확장 프로그램 이름",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ 보냄: ",
|
||||
"tooltipReceived": "↓ 받음: "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "핑거프린트 설정 보기 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "핑거프린트 설정 구성 - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "잘못된 핑거프린트 구성",
|
||||
"invalidFingerprintDescription": "핑거프린트 구성에 잘못된 JSON이 포함되어 있습니다. 고급 설정을 확인하세요.",
|
||||
"saveFailed": "구성 저장 실패",
|
||||
"unknownError": "알 수 없는 오류가 발생했습니다"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "알 수 없음",
|
||||
"locationToast": "프록시 위치:",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "국가별 생성"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "사용 가능한 릴리스 유형이 없습니다.",
|
||||
"placeholder": "릴리스 유형 선택...",
|
||||
"stable": "안정",
|
||||
"nightly": "나이틀리",
|
||||
"downloaded": "다운로드됨",
|
||||
"downloadBrowser": "브라우저 다운로드",
|
||||
"downloading": "다운로드 중..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "{{count}}개 선택됨",
|
||||
"clearSelection": "선택 지우기"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "선택한 프록시가 작동하지 않아 프로필이 생성되지 않았습니다.",
|
||||
"proxyPaymentRequired": "선택한 프록시는 결제가 필요합니다(402). 구독이 만료되었을 수 있어 프로필이 생성되지 않았습니다.",
|
||||
"vpnNotWorking": "선택한 VPN이 작동하지 않아 프로필이 생성되지 않았습니다.",
|
||||
"camoufoxImportDeprecated": "Firefox 기반(Camoufox) 프로필 가져오기는 더 이상 지원되지 않습니다. Camoufox는 지원 종료 예정입니다. 대신 Wayfern을 사용하세요."
|
||||
"camoufoxImportDeprecated": "Firefox 기반 프로필 가져오기는 더 이상 지원되지 않습니다. Wayfern을 사용하세요."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "프로필",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "Donut Browser 표시",
|
||||
"quit": "종료"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "브라우저 지원이 곧 종료됩니다",
|
||||
"endingSoonDescription": "다음 프로필에 대한 지원이 2026년 3월 15일에 제거됩니다: {{profiles}}. Wayfern 프로필로 이전하세요."
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "브라우저 자동화가 일시 중지됨",
|
||||
"description": "보통 여러 기기에서 동시에 로그인하여 계정의 Pro 브라우저 기능이 일시적으로 제한되었습니다. 다른 기기에서 로그아웃한 후 프로필을 다시 실행하면 복원됩니다."
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "Camoufox 지원이 종료됩니다",
|
||||
"description": "Camoufox 프로필 지원이 2026년 7월 8일에 종료됩니다. Camoufox 프로필이 하나 이상 있습니다. 그 전에 Wayfern으로 이전하세요. 이후에는 Camoufox 프로필이 작동하지 않을 수 있습니다.",
|
||||
"acknowledge": "확인"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "핑거프린트 설정 보기 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "핑거프린트 설정 구성 - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "잘못된 핑거프린트 구성",
|
||||
"invalidFingerprintDescription": "핑거프린트 구성에 잘못된 JSON이 포함되어 있습니다. 고급 설정을 확인하세요.",
|
||||
"saveFailed": "구성 저장 실패",
|
||||
"unknownError": "알 수 없는 오류가 발생했습니다"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "선택된 다른 Wayfern 프로필이 없습니다"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "Navegador Anti-Detecção",
|
||||
"description": "Escolha um navegador com capacidades anti-detecção",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "Navegador Anti-Detecção"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "Desenvolvido com Wayfern",
|
||||
"firefoxLabel": "Firefox",
|
||||
"firefoxSubtitle": "Desenvolvido com Camoufox",
|
||||
"camoufoxWarning": "O Firefox (Camoufox) é mantido por uma organização terceira. Para uso em produção, utilize o Chromium.",
|
||||
"platformUnavailable": "{{browser}} ainda não está disponível para sua plataforma.",
|
||||
"passwordProtect": {
|
||||
"label": "Proteger este perfil com senha",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "Falha ao importar perfil"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Configuração do Camoufox",
|
||||
"fingerprint": {
|
||||
"title": "Impressão Digital",
|
||||
"randomize": "Aleatorizar ao Iniciar",
|
||||
"randomizeDescription": "Gera uma nova impressão digital cada vez que o navegador é iniciado.",
|
||||
"osCpuPlaceholder": "ex., Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "ex., llvmpipe ou similar"
|
||||
},
|
||||
"os": {
|
||||
"title": "Sistema Operacional",
|
||||
"description": "O sistema operacional a emular para geração de impressão digital.",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "Tamanho da Tela",
|
||||
"minWidth": "Largura Mín",
|
||||
"maxWidth": "Largura Máx",
|
||||
"minHeight": "Altura Mín",
|
||||
"maxHeight": "Altura Máx"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "Automático (baseado no proxy)",
|
||||
"manual": "Manual",
|
||||
"disabled": "Desativado"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "Bloqueio",
|
||||
"images": "Bloquear Imagens",
|
||||
"webrtc": "Bloquear WebRTC",
|
||||
"webgl": "Bloquear WebGL"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Configuração do Wayfern",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "Selecione um perfil para copiar os cookies",
|
||||
"running": "(em execução)",
|
||||
"targetProfiles": "Perfis de destino ({{count}})",
|
||||
"noOtherTargets": "Nenhum outro perfil Wayfern/Camoufox selecionado",
|
||||
"noOtherTargets": "Nenhum outro perfil Wayfern selecionado",
|
||||
"selectSourceFirst": "Selecione primeiro um perfil de origem",
|
||||
"selectionStatus": "({{selected}} de {{total}} selecionados)",
|
||||
"searchPlaceholder": "Buscar domínios ou cookies...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "Erro do servidor. Por favor, tente novamente mais tarde.",
|
||||
"unknownError": "Ocorreu um erro desconhecido. Por favor, tente novamente.",
|
||||
"noProfilesForUrl": "Sem perfis disponíveis. Crie um perfil primeiro antes de abrir URLs.",
|
||||
"updateCamoufoxConfigFailed": "Falha ao atualizar a configuração do camoufox: {{error}}",
|
||||
"updateWayfernConfigFailed": "Falha ao atualizar a configuração do wayfern: {{error}}",
|
||||
"createProfileFailed": "Falha ao criar o perfil: {{error}}",
|
||||
"launchBrowserFailed": "Falha ao iniciar o navegador: {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "Falha ao renomear o perfil: {{error}}",
|
||||
"killBrowserFailed": "Falha ao encerrar o navegador: {{error}}",
|
||||
"deleteSelectedProfilesFailed": "Falha ao excluir os perfis selecionados: {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "A cópia de cookies só funciona com perfis Wayfern e Camoufox",
|
||||
"cookieCopyUnsupportedBrowser": "A cópia de cookies só funciona com perfis Wayfern",
|
||||
"updateSyncSettingsFailed": "Falha ao atualizar as configurações de sincronização",
|
||||
"cloneProfileFailed": "Falha ao clonar o perfil: {{error}}",
|
||||
"loadSupportedBrowsersFailed": "Falha ao carregar os navegadores suportados",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "Falha ao definir a senha do perfil: {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Dimensões de janela personalizadas",
|
||||
"windowResizeDescription": "Alterar as dimensões da janela do navegador pode aumentar a chance de detecção pelos sites de que as informações do navegador estão falsificadas.",
|
||||
"windowResizeCamoufoxTitle": "Viewport bloqueado pelo Camoufox",
|
||||
"windowResizeCamoufoxDescription": "O Camoufox bloqueia o viewport nas dimensões de tela falsificadas para anti-fingerprinting. Redimensionar a janela pode causar áreas cortadas ou cinzas. Este é o comportamento esperado.",
|
||||
"dontShowAgain": "Não mostrar novamente",
|
||||
"continue": "Continuar",
|
||||
"cancel": "Cancelar"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "Pare o perfil antes de alterar a senha."
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "A edição de impressão digital só está disponível para perfis Camoufox e Wayfern.",
|
||||
"notSupported": "A edição de impressão digital só está disponível para perfis Wayfern.",
|
||||
"lockedTitle": "Visualizar e editar a impressão digital é um recurso Pro",
|
||||
"lockedDescription": "A proteção contra fingerprint está incluída em todos os planos. Visualizar e editar os valores da impressão digital de um perfil é o que requer um plano pago ativo."
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "Grupo de Extensões",
|
||||
"compatibility": {
|
||||
"label": "Compatibilidade",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"both": "Chromium e Firefox"
|
||||
"chromium": "Chromium"
|
||||
},
|
||||
"selectedFile": "Arquivo selecionado",
|
||||
"namePlaceholder": "Nome da extensão",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ Enviado: ",
|
||||
"tooltipReceived": "↓ Recebido: "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "Ver configurações de impressão digital - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurar impressão digital - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Configuração de impressão digital inválida",
|
||||
"invalidFingerprintDescription": "A configuração de impressão digital contém JSON inválido. Verifique suas configurações avançadas.",
|
||||
"saveFailed": "Falha ao salvar configuração",
|
||||
"unknownError": "Ocorreu um erro desconhecido"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "Desconhecido",
|
||||
"locationToast": "A localização do seu proxy é:",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPNs",
|
||||
"createByCountryHeading": "Criar por país"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "Nenhum tipo de versão disponível.",
|
||||
"placeholder": "Selecione o tipo de versão...",
|
||||
"stable": "Estável",
|
||||
"nightly": "Nightly",
|
||||
"downloaded": "Baixado",
|
||||
"downloadBrowser": "Baixar navegador",
|
||||
"downloading": "Baixando..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "{{count}} selecionado(s)",
|
||||
"clearSelection": "Limpar seleção"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "O proxy selecionado não está funcionando, então o perfil não foi criado.",
|
||||
"proxyPaymentRequired": "O proxy selecionado exige pagamento (402) — sua assinatura pode ter expirado — então o perfil não foi criado.",
|
||||
"vpnNotWorking": "A VPN selecionada não está funcionando, então o perfil não foi criado.",
|
||||
"camoufoxImportDeprecated": "A importação de perfis baseados em Firefox (Camoufox) não é mais suportada. O Camoufox está sendo descontinuado — use o Wayfern."
|
||||
"camoufoxImportDeprecated": "A importação de perfis baseados em Firefox não é mais suportada. Use o Wayfern."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "Perfis",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "Mostrar Donut Browser",
|
||||
"quit": "Sair"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "O suporte ao navegador terminará em breve",
|
||||
"endingSoonDescription": "O suporte aos seguintes perfis será removido em 15 de março de 2026: {{profiles}}. Migre para perfis Wayfern."
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "Automação do navegador pausada",
|
||||
"description": "Sua conta foi temporariamente restringida dos recursos Pro do navegador, geralmente por entrar em vários dispositivos ao mesmo tempo. Saia dos outros dispositivos e reinicie o perfil para restaurá-la."
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "O suporte ao Camoufox está terminando",
|
||||
"description": "O suporte aos perfis do Camoufox terminará em 8 de julho de 2026. Você tem um ou mais perfis do Camoufox. Migre-os para o Wayfern antes dessa data — depois disso, os perfis do Camoufox podem parar de funcionar.",
|
||||
"acknowledge": "Entendi"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Ver configurações de impressão digital - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Configurar impressão digital - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Configuração de impressão digital inválida",
|
||||
"invalidFingerprintDescription": "A configuração de impressão digital contém JSON inválido. Verifique suas configurações avançadas.",
|
||||
"saveFailed": "Falha ao salvar configuração",
|
||||
"unknownError": "Ocorreu um erro desconhecido"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "Nenhum outro perfil Wayfern selecionado"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "Антидетект браузер",
|
||||
"description": "Выберите браузер с возможностями защиты от обнаружения",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "Антидетект браузер"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "На базе Wayfern",
|
||||
"firefoxLabel": "Firefox",
|
||||
"firefoxSubtitle": "На базе Camoufox",
|
||||
"camoufoxWarning": "Firefox (Camoufox) поддерживается сторонней организацией. Для промышленного использования используйте Chromium.",
|
||||
"platformUnavailable": "{{browser}} пока недоступен на вашей платформе.",
|
||||
"passwordProtect": {
|
||||
"label": "Защитить этот профиль паролем",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "Ошибка импорта профиля"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Настройки Camoufox",
|
||||
"fingerprint": {
|
||||
"title": "Отпечаток",
|
||||
"randomize": "Случайный при запуске",
|
||||
"randomizeDescription": "Генерировать новый отпечаток при каждом запуске браузера.",
|
||||
"osCpuPlaceholder": "напр., Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "напр., llvmpipe или похожее"
|
||||
},
|
||||
"os": {
|
||||
"title": "Операционная система",
|
||||
"description": "Операционная система для эмуляции при генерации отпечатка.",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "Размер экрана",
|
||||
"minWidth": "Мин. ширина",
|
||||
"maxWidth": "Макс. ширина",
|
||||
"minHeight": "Мин. высота",
|
||||
"maxHeight": "Макс. высота"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "Автоматически (на основе прокси)",
|
||||
"manual": "Вручную",
|
||||
"disabled": "Выключено"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "Блокировка",
|
||||
"images": "Блокировать изображения",
|
||||
"webrtc": "Блокировать WebRTC",
|
||||
"webgl": "Блокировать WebGL"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Настройки Wayfern",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "Выберите профиль для копирования cookies",
|
||||
"running": "(запущен)",
|
||||
"targetProfiles": "Целевые профили ({{count}})",
|
||||
"noOtherTargets": "Других выбранных Wayfern/Camoufox профилей нет",
|
||||
"noOtherTargets": "Других выбранных Wayfern профилей нет",
|
||||
"selectSourceFirst": "Сначала выберите исходный профиль",
|
||||
"selectionStatus": "(выбрано {{selected}} из {{total}})",
|
||||
"searchPlaceholder": "Поиск доменов или cookies...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "Ошибка сервера. Попробуйте позже.",
|
||||
"unknownError": "Произошла неизвестная ошибка. Попробуйте снова.",
|
||||
"noProfilesForUrl": "Нет доступных профилей. Сначала создайте профиль, прежде чем открывать URL.",
|
||||
"updateCamoufoxConfigFailed": "Не удалось обновить настройки camoufox: {{error}}",
|
||||
"updateWayfernConfigFailed": "Не удалось обновить настройки wayfern: {{error}}",
|
||||
"createProfileFailed": "Не удалось создать профиль: {{error}}",
|
||||
"launchBrowserFailed": "Не удалось запустить браузер: {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "Не удалось переименовать профиль: {{error}}",
|
||||
"killBrowserFailed": "Не удалось остановить браузер: {{error}}",
|
||||
"deleteSelectedProfilesFailed": "Не удалось удалить выбранные профили: {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "Копирование cookies работает только с профилями Wayfern и Camoufox",
|
||||
"cookieCopyUnsupportedBrowser": "Копирование cookies работает только с профилями Wayfern",
|
||||
"updateSyncSettingsFailed": "Не удалось обновить параметры синхронизации",
|
||||
"cloneProfileFailed": "Не удалось клонировать профиль: {{error}}",
|
||||
"loadSupportedBrowsersFailed": "Не удалось загрузить поддерживаемые браузеры",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "Не удалось установить пароль профиля: {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Пользовательские размеры окна",
|
||||
"windowResizeDescription": "Изменение размеров окна браузера может повысить вероятность обнаружения сайтами того, что информация браузера подменена.",
|
||||
"windowResizeCamoufoxTitle": "Viewport заблокирован Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox блокирует viewport на подменённых размерах экрана для защиты от фингерпринтинга. Изменение размера окна может вызвать обрезанные или серые области. Это ожидаемое поведение.",
|
||||
"dontShowAgain": "Больше не показывать",
|
||||
"continue": "Продолжить",
|
||||
"cancel": "Отмена"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "Остановите профиль перед сменой пароля."
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "Редактирование отпечатков доступно только для профилей Camoufox и Wayfern.",
|
||||
"notSupported": "Редактирование отпечатков доступно только для профилей Wayfern.",
|
||||
"lockedTitle": "Просмотр и редактирование отпечатка — функция Pro",
|
||||
"lockedDescription": "Защита от отпечатков включена во все планы. Активный платный план требуется именно для просмотра и редактирования значений отпечатка профиля."
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "Группа расширений",
|
||||
"compatibility": {
|
||||
"label": "Совместимость",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"both": "Chromium и Firefox"
|
||||
"chromium": "Chromium"
|
||||
},
|
||||
"selectedFile": "Выбранный файл",
|
||||
"namePlaceholder": "Название расширения",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ Отправлено: ",
|
||||
"tooltipReceived": "↓ Получено: "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "Просмотр настроек отпечатка - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Настроить отпечаток - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Неверная конфигурация отпечатка",
|
||||
"invalidFingerprintDescription": "Конфигурация отпечатка содержит недопустимый JSON. Проверьте расширенные настройки.",
|
||||
"saveFailed": "Не удалось сохранить конфигурацию",
|
||||
"unknownError": "Произошла неизвестная ошибка"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "Неизвестно",
|
||||
"locationToast": "Местоположение вашего прокси:",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "Создать по стране"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "Нет доступных типов выпусков.",
|
||||
"placeholder": "Выберите тип выпуска...",
|
||||
"stable": "Стабильный",
|
||||
"nightly": "Nightly",
|
||||
"downloaded": "Загружено",
|
||||
"downloadBrowser": "Загрузить браузер",
|
||||
"downloading": "Загрузка..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "Выбрано: {{count}}",
|
||||
"clearSelection": "Очистить выбор"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "Выбранный прокси не работает, поэтому профиль не создан.",
|
||||
"proxyPaymentRequired": "Выбранный прокси требует оплаты (402) — возможно, его подписка истекла — поэтому профиль не создан.",
|
||||
"vpnNotWorking": "Выбранный VPN не работает, поэтому профиль не создан.",
|
||||
"camoufoxImportDeprecated": "Импорт профилей на основе Firefox (Camoufox) больше не поддерживается. Camoufox выводится из эксплуатации — используйте Wayfern."
|
||||
"camoufoxImportDeprecated": "Импорт профилей на основе Firefox больше не поддерживается. Используйте Wayfern."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "Профили",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "Показать Donut Browser",
|
||||
"quit": "Выход"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "Поддержка браузера скоро завершится",
|
||||
"endingSoonDescription": "Поддержка следующих профилей будет прекращена 15 марта 2026 года: {{profiles}}. Перейдите на профили Wayfern."
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "Автоматизация браузера приостановлена",
|
||||
"description": "Доступ вашей учётной записи к Pro-функциям браузера временно ограничен — обычно из-за входа сразу на нескольких устройствах. Выйдите из аккаунта на других устройствах и перезапустите профиль, чтобы восстановить доступ."
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "Поддержка Camoufox прекращается",
|
||||
"description": "Поддержка профилей Camoufox прекращается 8 июля 2026 года. У вас есть один или несколько профилей Camoufox. Перенесите их на Wayfern до этой даты — после неё профили Camoufox могут перестать работать.",
|
||||
"acknowledge": "Понятно"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Просмотр настроек отпечатка - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Настроить отпечаток - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Неверная конфигурация отпечатка",
|
||||
"invalidFingerprintDescription": "Конфигурация отпечатка содержит недопустимый JSON. Проверьте расширенные настройки.",
|
||||
"saveFailed": "Не удалось сохранить конфигурацию",
|
||||
"unknownError": "Произошла неизвестная ошибка"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "Других выбранных Wayfern профилей нет"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "Trình duyệt chống phát hiện",
|
||||
"description": "Chọn trình duyệt có khả năng chống phát hiện",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "Trình duyệt chống phát hiện"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "Dựa trên Wayfern",
|
||||
"firefoxLabel": "Firefox",
|
||||
"firefoxSubtitle": "Dựa trên Camoufox",
|
||||
"camoufoxWarning": "Firefox (Camoufox) được duy trì bởi bên thứ ba. Đối với sử dụng trong môi trường sản xuất, vui lòng dùng Chromium.",
|
||||
"platformUnavailable": "{{browser}} chưa khả dụng trên nền tảng của bạn.",
|
||||
"passwordProtect": {
|
||||
"label": "Bảo vệ hồ sơ bằng mật khẩu",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "Nhập profile thất bại"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Cấu hình Camoufox",
|
||||
"fingerprint": {
|
||||
"title": "Vân tay",
|
||||
"randomize": "Tạo ngẫu nhiên khi khởi chạy",
|
||||
"randomizeDescription": "Tạo vân tay mới mỗi khi khởi chạy trình duyệt.",
|
||||
"osCpuPlaceholder": "ví dụ: Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "ví dụ: llvmpipe, hoặc tương tự"
|
||||
},
|
||||
"os": {
|
||||
"title": "Hệ điều hành",
|
||||
"description": "Hệ điều hành mô phỏng để tạo vân tay.",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "Kích thước màn hình",
|
||||
"minWidth": "Chiều rộng tối thiểu",
|
||||
"maxWidth": "Chiều rộng tối đa",
|
||||
"minHeight": "Chiều cao tối thiểu",
|
||||
"maxHeight": "Chiều cao tối đa"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "Tự động (dựa trên proxy)",
|
||||
"manual": "Thủ công",
|
||||
"disabled": "Tắt"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "Chặn",
|
||||
"images": "Chặn hình ảnh",
|
||||
"webrtc": "Chặn WebRTC",
|
||||
"webgl": "Chặn WebGL"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Cấu hình Wayfern",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "Chọn profile để sao chép cookie từ",
|
||||
"running": "(đang chạy)",
|
||||
"targetProfiles": "Profile đích ({{count}})",
|
||||
"noOtherTargets": "Chưa chọn profile Wayfern/Camoufox nào khác",
|
||||
"noOtherTargets": "Chưa chọn profile Wayfern nào khác",
|
||||
"selectSourceFirst": "Hãy chọn profile nguồn trước",
|
||||
"selectionStatus": "(đã chọn {{selected}}/{{total}})",
|
||||
"searchPlaceholder": "Tìm kiếm tên miền hoặc cookie...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "Lỗi máy chủ. Vui lòng thử lại sau.",
|
||||
"unknownError": "Đã xảy ra lỗi không xác định. Vui lòng thử lại.",
|
||||
"noProfilesForUrl": "Không có profile nào. Vui lòng tạo profile trước khi mở URL.",
|
||||
"updateCamoufoxConfigFailed": "Cập nhật cấu hình camoufox thất bại: {{error}}",
|
||||
"updateWayfernConfigFailed": "Cập nhật cấu hình wayfern thất bại: {{error}}",
|
||||
"createProfileFailed": "Tạo profile thất bại: {{error}}",
|
||||
"launchBrowserFailed": "Khởi chạy trình duyệt thất bại: {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "Đổi tên profile thất bại: {{error}}",
|
||||
"killBrowserFailed": "Dừng trình duyệt thất bại: {{error}}",
|
||||
"deleteSelectedProfilesFailed": "Xóa các profile đã chọn thất bại: {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "Sao chép cookie chỉ hoạt động với profile Wayfern và Camoufox",
|
||||
"cookieCopyUnsupportedBrowser": "Sao chép cookie chỉ hoạt động với profile Wayfern",
|
||||
"updateSyncSettingsFailed": "Cập nhật cài đặt đồng bộ thất bại",
|
||||
"cloneProfileFailed": "Nhân bản profile thất bại: {{error}}",
|
||||
"loadSupportedBrowsersFailed": "Tải danh sách trình duyệt hỗ trợ thất bại",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "Đặt mật khẩu profile thất bại: {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Kích thước cửa sổ tùy chỉnh",
|
||||
"windowResizeDescription": "Thay đổi kích thước cửa sổ trình duyệt có thể tăng khả năng bị trang web phát hiện thông tin trình duyệt bị giả mạo.",
|
||||
"windowResizeCamoufoxTitle": "Cửa sổ xem bị khóa bởi Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox khóa cửa sổ xem theo kích thước màn hình giả mạo để chống vân tay. Thay đổi kích thước cửa sổ có thể gây ra vùng bị cắt xén hoặc xám. Đây là hành vi bình thường.",
|
||||
"dontShowAgain": "Không hiển thị lại",
|
||||
"continue": "Tiếp tục",
|
||||
"cancel": "Hủy"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "Dừng profile trước khi thay đổi mật khẩu."
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "Chỉnh sửa vân tay chỉ khả dụng cho profile Camoufox và Wayfern.",
|
||||
"notSupported": "Chỉnh sửa vân tay chỉ khả dụng cho profile Wayfern.",
|
||||
"lockedTitle": "Xem và chỉnh sửa vân tay là tính năng Pro",
|
||||
"lockedDescription": "Bảo vệ vân tay được bao gồm trong mọi gói. Việc xem và chỉnh sửa các giá trị vân tay của profile mới là phần yêu cầu gói trả phí đang hoạt động."
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "Nhóm tiện ích",
|
||||
"compatibility": {
|
||||
"label": "Tương thích",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"both": "Chromium và Firefox"
|
||||
"chromium": "Chromium"
|
||||
},
|
||||
"selectedFile": "Tệp đã chọn",
|
||||
"namePlaceholder": "Tên tiện ích",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ Đã gửi: ",
|
||||
"tooltipReceived": "↓ Đã nhận: "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "Xem cài đặt vân tay - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Cấu hình cài đặt vân tay - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Cấu hình vân tay không hợp lệ",
|
||||
"invalidFingerprintDescription": "Cấu hình vân tay chứa JSON không hợp lệ. Vui lòng kiểm tra cài đặt nâng cao.",
|
||||
"saveFailed": "Lưu cấu hình thất bại",
|
||||
"unknownError": "Đã xảy ra lỗi không xác định"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "Không xác định",
|
||||
"locationToast": "Vị trí proxy của bạn là:",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "Tạo theo quốc gia"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "Không có loại phát hành nào.",
|
||||
"placeholder": "Chọn loại phát hành...",
|
||||
"stable": "Ổn định",
|
||||
"nightly": "Nightly",
|
||||
"downloaded": "Đã tải xuống",
|
||||
"downloadBrowser": "Tải trình duyệt",
|
||||
"downloading": "Đang tải xuống..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "Đã chọn {{count}}",
|
||||
"clearSelection": "Xóa lựa chọn"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "Proxy đã chọn không hoạt động, nên profile chưa được tạo.",
|
||||
"proxyPaymentRequired": "Proxy đã chọn yêu cầu thanh toán (402) — gói đăng ký của nó có thể đã hết hạn — nên profile chưa được tạo.",
|
||||
"vpnNotWorking": "VPN đã chọn không hoạt động, nên profile chưa được tạo.",
|
||||
"camoufoxImportDeprecated": "Không còn hỗ trợ nhập profile dựa trên Firefox (Camoufox). Camoufox đang bị ngừng hỗ trợ — vui lòng dùng Wayfern."
|
||||
"camoufoxImportDeprecated": "Không còn hỗ trợ nhập profile dựa trên Firefox. Vui lòng dùng Wayfern."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "Profile",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "Hiển thị Donut Browser",
|
||||
"quit": "Thoát"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "Hỗ trợ trình duyệt sắp kết thúc",
|
||||
"endingSoonDescription": "Hỗ trợ cho các profile sau sẽ bị gỡ bỏ vào ngày 15 tháng 3 năm 2026: {{profiles}}. Vui lòng chuyển sang profile Wayfern."
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "Tự động hóa trình duyệt đã tạm dừng",
|
||||
"description": "Tài khoản của bạn tạm thời bị hạn chế các tính năng Pro của trình duyệt, thường do đăng nhập trên nhiều thiết bị cùng lúc. Hãy đăng xuất khỏi các thiết bị khác rồi khởi chạy lại profile để khôi phục."
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "Hỗ trợ Camoufox sắp kết thúc",
|
||||
"description": "Hỗ trợ cho các profile Camoufox sẽ kết thúc vào ngày 8 tháng 7 năm 2026. Bạn có một hoặc nhiều profile Camoufox. Hãy chuyển chúng sang Wayfern trước thời điểm đó — sau ngày này, các profile Camoufox có thể ngừng hoạt động.",
|
||||
"acknowledge": "Đã hiểu"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "Xem cài đặt vân tay - {{name}} ({{browser}})",
|
||||
"titleConfigure": "Cấu hình cài đặt vân tay - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "Cấu hình vân tay không hợp lệ",
|
||||
"invalidFingerprintDescription": "Cấu hình vân tay chứa JSON không hợp lệ. Vui lòng kiểm tra cài đặt nâng cao.",
|
||||
"saveFailed": "Lưu cấu hình thất bại",
|
||||
"unknownError": "Đã xảy ra lỗi không xác định"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "Chưa chọn profile Wayfern nào khác"
|
||||
}
|
||||
}
|
||||
|
||||
+15
-76
@@ -340,7 +340,6 @@
|
||||
"title": "防检测浏览器",
|
||||
"description": "选择具有防检测功能的浏览器",
|
||||
"chromium": "Wayfern",
|
||||
"firefox": "Camoufox",
|
||||
"badge": "防检测浏览器"
|
||||
},
|
||||
"regular": {
|
||||
@@ -376,9 +375,6 @@
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "由 Wayfern 驱动",
|
||||
"firefoxLabel": "Firefox",
|
||||
"firefoxSubtitle": "由 Camoufox 驱动",
|
||||
"camoufoxWarning": "Firefox(Camoufox)由第三方组织维护。在生产环境中,请使用 Chromium。",
|
||||
"platformUnavailable": "{{browser}} 在您的平台上尚不可用。",
|
||||
"passwordProtect": {
|
||||
"label": "为此配置文件设置密码保护",
|
||||
@@ -749,42 +745,6 @@
|
||||
"error": "导入配置文件失败"
|
||||
},
|
||||
"config": {
|
||||
"camoufox": {
|
||||
"title": "Camoufox 配置",
|
||||
"fingerprint": {
|
||||
"title": "指纹",
|
||||
"randomize": "启动时随机化",
|
||||
"randomizeDescription": "每次启动浏览器时生成新的指纹。",
|
||||
"osCpuPlaceholder": "例如: Intel Mac OS X 10.15",
|
||||
"webglRendererPlaceholder": "例如: llvmpipe 或类似"
|
||||
},
|
||||
"os": {
|
||||
"title": "操作系统",
|
||||
"description": "用于生成指纹的模拟操作系统。",
|
||||
"windows": "Windows",
|
||||
"macos": "macOS",
|
||||
"linux": "Linux"
|
||||
},
|
||||
"screen": {
|
||||
"title": "屏幕尺寸",
|
||||
"minWidth": "最小宽度",
|
||||
"maxWidth": "最大宽度",
|
||||
"minHeight": "最小高度",
|
||||
"maxHeight": "最大高度"
|
||||
},
|
||||
"geoip": {
|
||||
"title": "GeoIP",
|
||||
"auto": "自动(基于代理)",
|
||||
"manual": "手动",
|
||||
"disabled": "禁用"
|
||||
},
|
||||
"blocking": {
|
||||
"title": "阻止",
|
||||
"images": "阻止图片",
|
||||
"webrtc": "阻止 WebRTC",
|
||||
"webgl": "阻止 WebGL"
|
||||
}
|
||||
},
|
||||
"wayfern": {
|
||||
"title": "Wayfern 配置",
|
||||
"fingerprint": {
|
||||
@@ -839,7 +799,7 @@
|
||||
"sourcePlaceholder": "选择要复制 Cookie 的配置文件",
|
||||
"running": "(运行中)",
|
||||
"targetProfiles": "目标配置文件 ({{count}})",
|
||||
"noOtherTargets": "未选择其他 Wayfern/Camoufox 配置文件",
|
||||
"noOtherTargets": "未选择其他 Wayfern 配置文件",
|
||||
"selectSourceFirst": "请先选择源配置文件",
|
||||
"selectionStatus": "(已选择 {{selected}} / {{total}})",
|
||||
"searchPlaceholder": "搜索域名或 Cookie...",
|
||||
@@ -968,7 +928,6 @@
|
||||
"serverError": "服务器错误。请稍后重试。",
|
||||
"unknownError": "发生未知错误。请重试。",
|
||||
"noProfilesForUrl": "没有可用的配置文件。请先创建配置文件再打开 URL。",
|
||||
"updateCamoufoxConfigFailed": "更新 camoufox 配置失败: {{error}}",
|
||||
"updateWayfernConfigFailed": "更新 wayfern 配置失败: {{error}}",
|
||||
"createProfileFailed": "创建配置文件失败: {{error}}",
|
||||
"launchBrowserFailed": "启动浏览器失败: {{error}}",
|
||||
@@ -977,7 +936,7 @@
|
||||
"renameProfileFailed": "重命名配置文件失败: {{error}}",
|
||||
"killBrowserFailed": "终止浏览器失败: {{error}}",
|
||||
"deleteSelectedProfilesFailed": "删除所选配置文件失败: {{error}}",
|
||||
"cookieCopyUnsupportedBrowser": "Cookie 复制仅适用于 Wayfern 和 Camoufox 配置文件",
|
||||
"cookieCopyUnsupportedBrowser": "Cookie 复制仅适用于 Wayfern 配置文件",
|
||||
"updateSyncSettingsFailed": "更新同步设置失败",
|
||||
"cloneProfileFailed": "克隆配置文件失败: {{error}}",
|
||||
"loadSupportedBrowsersFailed": "加载受支持的浏览器失败",
|
||||
@@ -994,7 +953,6 @@
|
||||
"setProfilePasswordFailed": "设置配置文件密码失败: {{error}}"
|
||||
},
|
||||
"browser": {
|
||||
"camoufox": "Camoufox",
|
||||
"wayfern": "Wayfern"
|
||||
},
|
||||
"fingerprint": {
|
||||
@@ -1105,8 +1063,6 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "自定义窗口尺寸",
|
||||
"windowResizeDescription": "更改浏览器窗口尺寸可能会增加网站检测到浏览器信息被伪装的概率。",
|
||||
"windowResizeCamoufoxTitle": "视口已被 Camoufox 锁定",
|
||||
"windowResizeCamoufoxDescription": "Camoufox 将视口锁定为伪装的屏幕尺寸以防止指纹识别。调整窗口大小可能会导致内容被裁剪或出现灰色区域。这是预期行为。",
|
||||
"dontShowAgain": "不再显示",
|
||||
"continue": "继续",
|
||||
"cancel": "取消"
|
||||
@@ -1237,7 +1193,7 @@
|
||||
"cannotWhileRunning": "更改密码前请先停止此配置文件。"
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "指纹编辑仅适用于 Camoufox 和 Wayfern 配置文件。",
|
||||
"notSupported": "指纹编辑仅适用于 Wayfern 配置文件。",
|
||||
"lockedTitle": "查看和编辑指纹是 Pro 功能",
|
||||
"lockedDescription": "所有方案都包含指纹保护。查看和编辑配置文件的指纹数值才需要有效的付费方案。"
|
||||
},
|
||||
@@ -1267,9 +1223,7 @@
|
||||
"extensionGroup": "扩展程序组",
|
||||
"compatibility": {
|
||||
"label": "兼容性",
|
||||
"chromium": "Chromium",
|
||||
"firefox": "Firefox",
|
||||
"both": "Chromium 和 Firefox"
|
||||
"chromium": "Chromium"
|
||||
},
|
||||
"selectedFile": "已选文件",
|
||||
"namePlaceholder": "扩展程序名称",
|
||||
@@ -1656,14 +1610,6 @@
|
||||
"tooltipSent": "↑ 已发送: ",
|
||||
"tooltipReceived": "↓ 已接收: "
|
||||
},
|
||||
"camoufoxDialog": {
|
||||
"titleView": "查看指纹设置 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "配置指纹设置 - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "无效的指纹配置",
|
||||
"invalidFingerprintDescription": "指纹配置包含无效的 JSON。请检查高级设置。",
|
||||
"saveFailed": "保存配置失败",
|
||||
"unknownError": "发生未知错误"
|
||||
},
|
||||
"proxyCheck": {
|
||||
"unknownLocation": "未知",
|
||||
"locationToast": "你的代理位置:",
|
||||
@@ -1703,15 +1649,6 @@
|
||||
"vpnsHeading": "VPN",
|
||||
"createByCountryHeading": "按国家创建"
|
||||
},
|
||||
"releaseTypeSelector": {
|
||||
"noReleaseTypes": "没有可用的发布类型。",
|
||||
"placeholder": "选择发布类型...",
|
||||
"stable": "Stable",
|
||||
"nightly": "Nightly",
|
||||
"downloaded": "已下载",
|
||||
"downloadBrowser": "下载浏览器",
|
||||
"downloading": "正在下载..."
|
||||
},
|
||||
"dataTableActionBar": {
|
||||
"selected": "已选择 {{count}}",
|
||||
"clearSelection": "清除选择"
|
||||
@@ -1861,7 +1798,7 @@
|
||||
"proxyNotWorking": "所选代理无法使用,因此未创建配置文件。",
|
||||
"proxyPaymentRequired": "所选代理需要付费(402),其订阅可能已过期,因此未创建配置文件。",
|
||||
"vpnNotWorking": "所选 VPN 无法使用,因此未创建配置文件。",
|
||||
"camoufoxImportDeprecated": "不再支持导入基于 Firefox 的 (Camoufox) 配置文件。Camoufox 即将停用——请改用 Wayfern。"
|
||||
"camoufoxImportDeprecated": "不再支持导入基于 Firefox 的配置文件。请改用 Wayfern。"
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "配置文件",
|
||||
@@ -1988,10 +1925,6 @@
|
||||
"show": "显示 Donut Browser",
|
||||
"quit": "退出"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "浏览器支持即将结束",
|
||||
"endingSoonDescription": "以下配置文件的支持将于 2026 年 3 月 15 日移除:{{profiles}}。请迁移到 Wayfern 配置文件。"
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
@@ -2073,9 +2006,15 @@
|
||||
"title": "浏览器自动化已暂停",
|
||||
"description": "您的账户暂时被限制使用 Pro 浏览器功能,通常是因为同时在多台设备上登录。请退出其他设备的登录,然后重新启动配置文件即可恢复。"
|
||||
},
|
||||
"camoufoxDeprecation": {
|
||||
"title": "Camoufox 支持即将结束",
|
||||
"description": "Camoufox 配置文件的支持将于 2026 年 7 月 8 日结束。您有一个或多个 Camoufox 配置文件。请在此之前迁移到 Wayfern——之后 Camoufox 配置文件可能会停止工作。",
|
||||
"acknowledge": "知道了"
|
||||
"wayfernConfigDialog": {
|
||||
"titleView": "查看指纹设置 - {{name}} ({{browser}})",
|
||||
"titleConfigure": "配置指纹设置 - {{name}} ({{browser}})",
|
||||
"invalidFingerprint": "无效的指纹配置",
|
||||
"invalidFingerprintDescription": "指纹配置包含无效的 JSON。请检查高级设置。",
|
||||
"saveFailed": "保存配置失败",
|
||||
"unknownError": "发生未知错误"
|
||||
},
|
||||
"cookieCopy": {
|
||||
"noOtherTargets": "未选择其他 Wayfern 配置文件"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
* Centralized helpers for browser name mapping, icons, etc.
|
||||
*/
|
||||
|
||||
import {
|
||||
FaChrome,
|
||||
FaExclamationTriangle,
|
||||
FaFire,
|
||||
FaFirefox,
|
||||
} from "react-icons/fa";
|
||||
import { FaChrome, FaExclamationTriangle, FaFire } from "react-icons/fa";
|
||||
import { LuLock } from "react-icons/lu";
|
||||
|
||||
/**
|
||||
@@ -16,7 +11,6 @@ import { LuLock } from "react-icons/lu";
|
||||
*/
|
||||
export function getBrowserDisplayName(browserType: string): string {
|
||||
const browserNames: Record<string, string> = {
|
||||
camoufox: "Camoufox",
|
||||
wayfern: "Wayfern",
|
||||
};
|
||||
|
||||
@@ -30,12 +24,9 @@ export function getBrowserDisplayName(browserType: string): string {
|
||||
*/
|
||||
export function getBrowserIcon(browserType: string) {
|
||||
switch (browserType) {
|
||||
case "camoufox":
|
||||
return FaFirefox; // Firefox-based anti-detect browser
|
||||
case "wayfern":
|
||||
return FaChrome; // Chromium-based anti-detect browser
|
||||
return FaChrome;
|
||||
default:
|
||||
// All other browsers get a warning icon
|
||||
return FaExclamationTriangle;
|
||||
}
|
||||
}
|
||||
@@ -66,13 +57,9 @@ export const getCurrentOS = () => {
|
||||
|
||||
export function isCrossOsProfile(profile: {
|
||||
host_os?: string;
|
||||
camoufox_config?: { os?: string };
|
||||
wayfern_config?: { os?: string };
|
||||
}): boolean {
|
||||
const profileOs =
|
||||
profile.host_os ||
|
||||
profile.camoufox_config?.os ||
|
||||
profile.wayfern_config?.os;
|
||||
const profileOs = profile.host_os || profile.wayfern_config?.os;
|
||||
if (!profileOs) return false;
|
||||
return profileOs !== getCurrentOS();
|
||||
}
|
||||
|
||||
+1
-189
@@ -21,8 +21,7 @@ export interface BrowserProfile {
|
||||
launch_hook?: string;
|
||||
process_id?: number;
|
||||
last_launch?: number;
|
||||
release_type: string; // "stable" or "nightly"
|
||||
camoufox_config?: CamoufoxConfig; // Camoufox configuration
|
||||
release_type: string;
|
||||
wayfern_config?: WayfernConfig; // Wayfern configuration
|
||||
group_id?: string; // Reference to profile group
|
||||
tags?: string[];
|
||||
@@ -200,7 +199,6 @@ export interface DetectedProfile {
|
||||
|
||||
export interface BrowserReleaseTypes {
|
||||
stable?: string;
|
||||
nightly?: string;
|
||||
}
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
@@ -223,192 +221,6 @@ export interface AppUpdateProgress {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type CamoufoxOS = "windows" | "macos" | "linux";
|
||||
|
||||
export interface CamoufoxConfig {
|
||||
proxy?: string;
|
||||
screen_max_width?: number;
|
||||
screen_max_height?: number;
|
||||
screen_min_width?: number;
|
||||
screen_min_height?: number;
|
||||
geoip?: string | boolean;
|
||||
block_images?: boolean;
|
||||
block_webrtc?: boolean;
|
||||
block_webgl?: boolean;
|
||||
executable_path?: string;
|
||||
fingerprint?: string; // JSON string of the complete fingerprint config
|
||||
randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch
|
||||
os?: CamoufoxOS; // Operating system for fingerprint generation
|
||||
}
|
||||
|
||||
// Extended interface for the advanced fingerprint configuration
|
||||
export interface CamoufoxFingerprintConfig {
|
||||
// Browser behavior
|
||||
allowAddonNewTab?: boolean;
|
||||
|
||||
// Navigator properties
|
||||
"navigator.userAgent"?: string;
|
||||
"navigator.appVersion"?: string;
|
||||
"navigator.platform"?: string;
|
||||
"navigator.oscpu"?: string;
|
||||
"navigator.appCodeName"?: string;
|
||||
"navigator.appName"?: string;
|
||||
"navigator.product"?: string;
|
||||
"navigator.productSub"?: string;
|
||||
"navigator.buildID"?: string;
|
||||
"navigator.language"?: string;
|
||||
"navigator.languages"?: string[];
|
||||
"navigator.doNotTrack"?: string;
|
||||
"navigator.hardwareConcurrency"?: number;
|
||||
"navigator.maxTouchPoints"?: number;
|
||||
"navigator.cookieEnabled"?: boolean;
|
||||
"navigator.globalPrivacyControl"?: boolean;
|
||||
"navigator.onLine"?: boolean;
|
||||
|
||||
// Screen properties
|
||||
"screen.height"?: number;
|
||||
"screen.width"?: number;
|
||||
"screen.availHeight"?: number;
|
||||
"screen.availWidth"?: number;
|
||||
"screen.availTop"?: number;
|
||||
"screen.availLeft"?: number;
|
||||
"screen.colorDepth"?: number;
|
||||
"screen.pixelDepth"?: number;
|
||||
"screen.pageXOffset"?: number;
|
||||
"screen.pageYOffset"?: number;
|
||||
|
||||
// Window properties
|
||||
"window.outerHeight"?: number;
|
||||
"window.outerWidth"?: number;
|
||||
"window.innerHeight"?: number;
|
||||
"window.innerWidth"?: number;
|
||||
"window.screenX"?: number;
|
||||
"window.screenY"?: number;
|
||||
"window.scrollMinX"?: number;
|
||||
"window.scrollMinY"?: number;
|
||||
"window.scrollMaxX"?: number;
|
||||
"window.scrollMaxY"?: number;
|
||||
"window.devicePixelRatio"?: number;
|
||||
"window.history.length"?: number;
|
||||
|
||||
// Document properties
|
||||
"document.body.clientWidth"?: number;
|
||||
"document.body.clientHeight"?: number;
|
||||
"document.body.clientTop"?: number;
|
||||
"document.body.clientLeft"?: number;
|
||||
|
||||
// Locale and geolocation
|
||||
"locale:language"?: string;
|
||||
"locale:region"?: string;
|
||||
"locale:script"?: string;
|
||||
"locale:all"?: string;
|
||||
"geolocation:latitude"?: number;
|
||||
"geolocation:longitude"?: number;
|
||||
"geolocation:accuracy"?: number;
|
||||
timezone?: string;
|
||||
|
||||
// Headers
|
||||
"headers.Accept-Language"?: string;
|
||||
"headers.User-Agent"?: string;
|
||||
"headers.Accept-Encoding"?: string;
|
||||
|
||||
// WebRTC
|
||||
"webrtc:ipv4"?: string;
|
||||
"webrtc:ipv6"?: string;
|
||||
"webrtc:localipv4"?: string;
|
||||
"webrtc:localipv6"?: string;
|
||||
|
||||
// Battery
|
||||
"battery:charging"?: boolean;
|
||||
"battery:chargingTime"?: number;
|
||||
"battery:dischargingTime"?: number;
|
||||
"battery:level"?: number;
|
||||
|
||||
// Fonts
|
||||
fonts?: string[];
|
||||
"fonts:spacing_seed"?: number;
|
||||
|
||||
// Audio
|
||||
"AudioContext:sampleRate"?: number;
|
||||
"AudioContext:outputLatency"?: number;
|
||||
"AudioContext:maxChannelCount"?: number;
|
||||
|
||||
// Media devices
|
||||
"mediaDevices:micros"?: number;
|
||||
"mediaDevices:webcams"?: number;
|
||||
"mediaDevices:speakers"?: number;
|
||||
"mediaDevices:enabled"?: boolean;
|
||||
|
||||
// WebGL
|
||||
"webGl:renderer"?: string;
|
||||
"webGl:vendor"?: string;
|
||||
"webGl:supportedExtensions"?: string[];
|
||||
"webGl2:supportedExtensions"?: string[];
|
||||
"webGl:contextAttributes"?: {
|
||||
alpha?: boolean;
|
||||
antialias?: boolean;
|
||||
depth?: boolean;
|
||||
failIfMajorPerformanceCaveat?: boolean;
|
||||
powerPreference?: string;
|
||||
premultipliedAlpha?: boolean;
|
||||
preserveDrawingBuffer?: boolean;
|
||||
stencil?: boolean;
|
||||
};
|
||||
"webGl2:contextAttributes"?: {
|
||||
alpha?: boolean;
|
||||
antialias?: boolean;
|
||||
depth?: boolean;
|
||||
failIfMajorPerformanceCaveat?: boolean;
|
||||
powerPreference?: string;
|
||||
premultipliedAlpha?: boolean;
|
||||
preserveDrawingBuffer?: boolean;
|
||||
stencil?: boolean;
|
||||
};
|
||||
"webGl:parameters"?: Record<string, unknown>;
|
||||
"webGl2:parameters"?: Record<string, unknown>;
|
||||
"webGl:shaderPrecisionFormats"?: Record<string, unknown>;
|
||||
"webGl2:shaderPrecisionFormats"?: Record<string, unknown>;
|
||||
|
||||
// Canvas
|
||||
"canvas:aaOffset"?: number;
|
||||
"canvas:aaCapOffset"?: boolean;
|
||||
|
||||
// Voices
|
||||
voices?: {
|
||||
isLocalService?: boolean;
|
||||
isDefault?: boolean;
|
||||
voiceURI?: string;
|
||||
name?: string;
|
||||
lang?: string;
|
||||
}[];
|
||||
"voices:blockIfNotDefined"?: boolean;
|
||||
"voices:fakeCompletion"?: boolean;
|
||||
"voices:fakeCompletion:charsPerSecond"?: number;
|
||||
|
||||
// Other properties
|
||||
humanize?: boolean;
|
||||
"humanize:maxTime"?: number;
|
||||
"humanize:minTime"?: number;
|
||||
showcursor?: boolean;
|
||||
allowMainWorld?: boolean;
|
||||
forceScopeAccess?: boolean;
|
||||
enableRemoteSubframes?: boolean;
|
||||
disableTheming?: boolean;
|
||||
memorysaver?: boolean;
|
||||
addons?: string[];
|
||||
certificatePaths?: string[];
|
||||
certificates?: string[];
|
||||
debug?: boolean;
|
||||
pdfViewerEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CamoufoxLaunchResult {
|
||||
id: string;
|
||||
processId?: number;
|
||||
profilePath?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export type WayfernOS = "windows" | "macos" | "linux" | "android" | "ios";
|
||||
|
||||
export interface WayfernConfig {
|
||||
|
||||
+1
-7
@@ -33,11 +33,5 @@
|
||||
".next/dev/types/**/*.ts",
|
||||
"dist/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"nodecar",
|
||||
"src-tauri/target",
|
||||
"donut-sync",
|
||||
"camoufox-js"
|
||||
]
|
||||
"exclude": ["node_modules", "nodecar", "src-tauri/target", "donut-sync"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user