camoufox-cli 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of camoufox-cli might be problematic. Click here for more details.
- camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0/.claude-plugin/marketplace.json +20 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/Makefile +2 -2
- camoufox_cli-0.3.0/PKG-INFO → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0/README.md +40 -23
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/package.json +1 -1
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/browser.ts +28 -1
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/cli.ts +13 -7
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/daemon.ts +2 -1
- camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0/js/src/identity.ts +206 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/server.ts +2 -2
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/pyproject.toml +1 -1
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/skills/camoufox-cli/SKILL.md +20 -1
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/skills/camoufox-cli/references/commands.md +8 -1
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/__main__.py +2 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/browser.py +27 -1
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/cli.py +15 -6
- camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0/src/camoufox_cli/identity.py +215 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/server.py +2 -2
- camoufox_cli-0.5.0/.gitignore +30 -0
- camoufox_cli-0.5.0/LICENSE +21 -0
- camoufox_cli-0.5.0/Makefile +30 -0
- camoufox_cli-0.5.0/PKG-INFO +275 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0}/README.md +67 -1
- camoufox_cli-0.5.0/js/.gitignore +2 -0
- camoufox_cli-0.5.0/js/LICENSE +21 -0
- camoufox_cli-0.5.0/js/package-lock.json +2719 -0
- camoufox_cli-0.5.0/js/package.json +47 -0
- camoufox_cli-0.5.0/js/pnpm-lock.yaml +1599 -0
- camoufox_cli-0.5.0/js/scripts/postinstall.js +11 -0
- camoufox_cli-0.5.0/js/src/browser.ts +192 -0
- camoufox_cli-0.5.0/js/src/cli.ts +505 -0
- camoufox_cli-0.5.0/js/src/commands.ts +352 -0
- camoufox_cli-0.5.0/js/src/config.ts +140 -0
- camoufox_cli-0.5.0/js/src/daemon.ts +27 -0
- camoufox_cli-0.5.0/js/src/identity.ts +206 -0
- camoufox_cli-0.5.0/js/src/protocol.ts +32 -0
- camoufox_cli-0.5.0/js/src/proxy.ts +40 -0
- camoufox_cli-0.5.0/js/src/refs.ts +65 -0
- camoufox_cli-0.5.0/js/src/server.ts +131 -0
- camoufox_cli-0.5.0/js/tests/browser.test.ts +61 -0
- camoufox_cli-0.5.0/js/tests/cli.test.ts +455 -0
- camoufox_cli-0.5.0/js/tests/commands.test.ts +194 -0
- camoufox_cli-0.5.0/js/tests/e2e.test.ts +216 -0
- camoufox_cli-0.5.0/js/tests/protocol.test.ts +98 -0
- camoufox_cli-0.5.0/js/tests/proxy.test.ts +55 -0
- camoufox_cli-0.5.0/js/tests/refs.test.ts +150 -0
- camoufox_cli-0.5.0/js/tests/server.test.ts +193 -0
- camoufox_cli-0.5.0/js/tsconfig.json +14 -0
- camoufox_cli-0.5.0/js/vitest.config.ts +7 -0
- camoufox_cli-0.5.0/pyproject.toml +46 -0
- camoufox_cli-0.5.0/skills/camoufox-cli/SKILL.md +351 -0
- camoufox_cli-0.5.0/skills/camoufox-cli/references/commands.md +143 -0
- camoufox_cli-0.5.0/skills/camoufox-cli/references/snapshot-refs.md +221 -0
- camoufox_cli-0.5.0/src/camoufox_cli/__init__.py +0 -0
- camoufox_cli-0.5.0/src/camoufox_cli/__main__.py +38 -0
- camoufox_cli-0.5.0/src/camoufox_cli/browser.py +190 -0
- camoufox_cli-0.5.0/src/camoufox_cli/cli.py +518 -0
- camoufox_cli-0.5.0/src/camoufox_cli/commands.py +390 -0
- camoufox_cli-0.5.0/src/camoufox_cli/config.py +130 -0
- camoufox_cli-0.5.0/src/camoufox_cli/identity.py +215 -0
- camoufox_cli-0.5.0/src/camoufox_cli/protocol.py +24 -0
- camoufox_cli-0.5.0/src/camoufox_cli/proxy.py +30 -0
- camoufox_cli-0.5.0/src/camoufox_cli/refs.py +83 -0
- camoufox_cli-0.5.0/src/camoufox_cli/server.py +145 -0
- camoufox_cli-0.5.0/tests/__init__.py +0 -0
- camoufox_cli-0.5.0/tests/fixture.html +22 -0
- camoufox_cli-0.5.0/tests/test_cli.py +392 -0
- camoufox_cli-0.5.0/tests/test_commands.py +556 -0
- camoufox_cli-0.5.0/tests/test_e2e.py +217 -0
- camoufox_cli-0.5.0/tests/test_protocol.py +63 -0
- camoufox_cli-0.5.0/tests/test_proxy.py +37 -0
- camoufox_cli-0.5.0/tests/test_refs.py +112 -0
- camoufox_cli-0.3.0/.claude/settings.local.json +0 -20
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/.gitignore +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/LICENSE +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/.gitignore +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/LICENSE +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/package-lock.json +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/pnpm-lock.yaml +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/scripts/postinstall.js +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/commands.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/protocol.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/proxy.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/refs.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tests/browser.test.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tests/cli.test.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tests/commands.test.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tests/e2e.test.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tests/protocol.test.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tests/proxy.test.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tests/refs.test.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tests/server.test.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/tsconfig.json +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/vitest.config.ts +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/skills/camoufox-cli/references/snapshot-refs.md +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/__init__.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/commands.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/protocol.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/proxy.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/src/camoufox_cli/refs.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/tests/__init__.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/tests/fixture.html +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/tests/test_cli.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/tests/test_commands.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/tests/test_e2e.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/tests/test_protocol.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/tests/test_proxy.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/tests/test_refs.py +0 -0
- {camoufox_cli-0.3.0 → camoufox_cli-0.5.0}/.claude-plugin/marketplace.json +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "camoufox-cli",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Benn Huang"
|
|
5
|
+
},
|
|
6
|
+
"metadata": {
|
|
7
|
+
"description": "Anti-detect browser automation CLI & Skills for AI agents, powered by Camoufox",
|
|
8
|
+
"version": "0.1.0"
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "camoufox-cli",
|
|
13
|
+
"description": "Anti-detect browser automation skill with fingerprint spoofing for AI agents",
|
|
14
|
+
"source": "./",
|
|
15
|
+
"skills": [
|
|
16
|
+
"./skills/camoufox-cli"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -14,13 +14,13 @@ test-e2e:
|
|
|
14
14
|
cd js && npx vitest run tests/e2e.test.ts
|
|
15
15
|
|
|
16
16
|
build: clean
|
|
17
|
-
|
|
17
|
+
uv build
|
|
18
18
|
cd js && npm run build
|
|
19
19
|
|
|
20
20
|
publish: publish-pip publish-npm
|
|
21
21
|
|
|
22
22
|
publish-pip: build
|
|
23
|
-
twine upload dist/*
|
|
23
|
+
uvx twine upload dist/*
|
|
24
24
|
|
|
25
25
|
publish-npm: build
|
|
26
26
|
cd js && npm publish --access public
|
camoufox_cli-0.3.0/PKG-INFO → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0/README.md
RENAMED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: camoufox-cli
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: Anti-detect browser CLI & Skills for AI agents, powered by Camoufox
|
|
5
|
-
Project-URL: Homepage, https://github.com/Bin-Huang/camoufox-cli
|
|
6
|
-
Project-URL: Repository, https://github.com/Bin-Huang/camoufox-cli
|
|
7
|
-
Project-URL: Issues, https://github.com/Bin-Huang/camoufox-cli/issues
|
|
8
|
-
Author: Benn Huang
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: ai-agent,anti-detect,automation,browser,camoufox,headless
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
16
|
-
Classifier: Topic :: Software Development :: Testing
|
|
17
|
-
Requires-Python: >=3.10
|
|
18
|
-
Requires-Dist: camoufox[geoip]
|
|
19
|
-
Requires-Dist: pillow
|
|
20
|
-
Requires-Dist: playwright
|
|
21
|
-
Description-Content-Type: text/markdown
|
|
22
|
-
|
|
23
1
|
# camoufox-cli
|
|
24
2
|
|
|
25
3
|
Anti-detect browser CLI & Skills for AI agents, powered by [Camoufox](https://github.com/daijro/camoufox).
|
|
@@ -27,6 +5,7 @@ Anti-detect browser CLI & Skills for AI agents, powered by [Camoufox](https://gi
|
|
|
27
5
|
### Highlights
|
|
28
6
|
|
|
29
7
|
- C++-level fingerprint spoofing via Camoufox (canvas, WebGL, audio, screen metrics, fonts)
|
|
8
|
+
- Persistent identities: stable fingerprint, OS, and locale across launches with `--persistent`
|
|
30
9
|
- Accessibility-tree snapshots with `@ref` element targeting
|
|
31
10
|
- Session isolation with cookie import/export
|
|
32
11
|
- Shell commands, no code generation
|
|
@@ -173,11 +152,49 @@ camoufox-cli cookies export file.json # Export cookies
|
|
|
173
152
|
--headed Show browser window (default: headless)
|
|
174
153
|
--timeout <seconds> Daemon idle timeout (default: 1800)
|
|
175
154
|
--json Output as JSON
|
|
176
|
-
--persistent [path]
|
|
155
|
+
--persistent [path] Persistent identity (see below); default path: ~/.camoufox-cli/profiles/<session>
|
|
177
156
|
--proxy <url> Proxy server (http:// or https://; auth: http://user:pass@host:port)
|
|
178
157
|
--no-geoip Disable automatic GeoIP spoofing (auto-enabled with --proxy)
|
|
158
|
+
--locale <tag> Force browser locale (e.g. "en-US" or "en-US,zh-CN")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Persistent Identity
|
|
162
|
+
|
|
163
|
+
Without `--persistent`, every launch gets a fresh random fingerprint. With `--persistent`, a `camoufox-cli.json` file is written next to the browser's user data. Fingerprint, OS, and canvas/font seeds are generated on the first launch and frozen afterwards; locale and proxy-derived timezone/geolocation are also saved but track whatever you pass on the command line. Every subsequent launch with the same path reloads the identity, so sites see the same device on every visit.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# First launch: generates + stores identity
|
|
167
|
+
camoufox-cli --persistent ~/.camoufox-cli/profiles/alice open https://example.com
|
|
168
|
+
|
|
169
|
+
# Every subsequent launch: same fingerprint, same cookies, same device
|
|
170
|
+
camoufox-cli --persistent ~/.camoufox-cli/profiles/alice open https://example.com
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Frozen** — generated on the first launch, permanent until the directory is deleted:
|
|
174
|
+
|
|
175
|
+
- Fingerprint (UA, canvas, WebGL, fonts, navigator properties)
|
|
176
|
+
- OS (defaults to host OS)
|
|
177
|
+
- Canvas/font noise seeds
|
|
178
|
+
|
|
179
|
+
**Persisted** — stored in the file, refreshed whenever you pass the flag:
|
|
180
|
+
|
|
181
|
+
- `--locale`: sets the stored locale; pass it again to change it, omit it to keep the stored value.
|
|
182
|
+
- Timezone / geolocation: re-derived from the current proxy's IP whenever `--proxy` is active (and `--no-geoip` isn't set), then written back. Without a proxy, the last stored values stay in effect.
|
|
183
|
+
|
|
184
|
+
**Per-launch** — never stored; pass on every launch for it to apply:
|
|
185
|
+
|
|
186
|
+
- `--proxy`: proxies rotate or expire; re-specify each launch to avoid silently running through a stale one.
|
|
187
|
+
- `--no-geoip`: follows the current `--proxy`.
|
|
188
|
+
|
|
189
|
+
**Multiple identities in parallel.** Each persistent path is a distinct identity. Combine with `--session` for concurrent daemons:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
camoufox-cli --session a --persistent ~/.camoufox-cli/profiles/alice open https://...
|
|
193
|
+
camoufox-cli --session b --persistent ~/.camoufox-cli/profiles/bob open https://...
|
|
179
194
|
```
|
|
180
195
|
|
|
196
|
+
**Resetting an identity.** Just delete the directory (`rm -rf ~/.camoufox-cli/profiles/alice`). The next launch generates a fresh one.
|
|
197
|
+
|
|
181
198
|
## Architecture
|
|
182
199
|
|
|
183
200
|
```
|
{camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/browser.ts
RENAMED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/** Browser manager: launches and manages Camoufox instance. */
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
4
5
|
import { Camoufox, launchOptions } from "camoufox-js";
|
|
5
6
|
import { firefox, type Browser, type BrowserContext, type Page } from "playwright-core";
|
|
7
|
+
import { loadOrCreate, toLaunchOptions } from "./identity.js";
|
|
6
8
|
import { parseProxySettings } from "./proxy.js";
|
|
7
9
|
import { RefRegistry } from "./refs.js";
|
|
8
10
|
|
|
11
|
+
const MAX_HISTORY = 200;
|
|
12
|
+
|
|
9
13
|
function ensureBrowserInstalled(): void {
|
|
10
14
|
try {
|
|
11
15
|
execFileSync("npx", ["camoufox-js", "path"], { stdio: "pipe" });
|
|
@@ -24,13 +28,15 @@ export class BrowserManager {
|
|
|
24
28
|
private persistent: string | null;
|
|
25
29
|
private proxy: string | null;
|
|
26
30
|
private geoip: boolean;
|
|
31
|
+
private locale: string | null;
|
|
27
32
|
private history: string[] = [];
|
|
28
33
|
private historyIndex = -1;
|
|
29
34
|
|
|
30
|
-
constructor(persistent: string | null = null, proxy: string | null = null, geoip: boolean = true) {
|
|
35
|
+
constructor(persistent: string | null = null, proxy: string | null = null, geoip: boolean = true, locale: string | null = null) {
|
|
31
36
|
this.persistent = persistent;
|
|
32
37
|
this.proxy = proxy;
|
|
33
38
|
this.geoip = geoip;
|
|
39
|
+
this.locale = locale;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
async launch(headless: boolean = true): Promise<void> {
|
|
@@ -50,11 +56,29 @@ export class BrowserManager {
|
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
if (this.persistent) {
|
|
59
|
+
// Persistent identity: freeze fingerprint/OS on first launch; reload
|
|
60
|
+
// it on subsequent launches. CLI-passed locale / proxy-derived geo
|
|
61
|
+
// overwrite the stored values so the file tracks current intent.
|
|
62
|
+
mkdirSync(this.persistent, { recursive: true });
|
|
63
|
+
const identity = await loadOrCreate(
|
|
64
|
+
this.persistent,
|
|
65
|
+
this.locale,
|
|
66
|
+
this.proxy,
|
|
67
|
+
this.geoip,
|
|
68
|
+
);
|
|
69
|
+
Object.assign(launchOpts, toLaunchOptions(identity));
|
|
53
70
|
const opts = await launchOptions(launchOpts);
|
|
54
71
|
this.context = await firefox.launchPersistentContext(this.persistent, opts);
|
|
55
72
|
const pages = this.context.pages();
|
|
56
73
|
this.page = pages[0] || await this.context.newPage();
|
|
57
74
|
} else {
|
|
75
|
+
if (this.locale) {
|
|
76
|
+
// Non-persistent path: locale is a one-shot override, no identity file.
|
|
77
|
+
const locales = this.locale.split(",").map((s) => s.trim()).filter(Boolean);
|
|
78
|
+
if (locales.length > 0) {
|
|
79
|
+
launchOpts.locale = locales.length > 1 ? locales : locales[0];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
58
82
|
this.browser = await Camoufox(launchOpts) as Browser;
|
|
59
83
|
this.page = await this.browser.newPage();
|
|
60
84
|
this.context = this.page.context();
|
|
@@ -125,6 +149,9 @@ export class BrowserManager {
|
|
|
125
149
|
pushHistory(url: string): void {
|
|
126
150
|
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
127
151
|
this.history.push(url);
|
|
152
|
+
if (this.history.length > MAX_HISTORY) {
|
|
153
|
+
this.history = this.history.slice(-MAX_HISTORY);
|
|
154
|
+
}
|
|
128
155
|
this.historyIndex = this.history.length - 1;
|
|
129
156
|
}
|
|
130
157
|
|
{camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/cli.ts
RENAMED
|
@@ -30,7 +30,7 @@ function sendCommand(sockPath: string, command: Record<string, unknown>): Promis
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function spawnDaemon(session: string, headed: boolean, timeout: number, persistent: string | null, proxy: string | null = null, geoip: boolean = true): Promise<void> {
|
|
33
|
+
function spawnDaemon(session: string, headed: boolean, timeout: number, persistent: string | null, proxy: string | null = null, geoip: boolean = true, locale: string | null = null): Promise<void> {
|
|
34
34
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
35
35
|
const daemonPath = path.join(__dirname, "daemon.js");
|
|
36
36
|
|
|
@@ -39,6 +39,7 @@ function spawnDaemon(session: string, headed: boolean, timeout: number, persiste
|
|
|
39
39
|
if (persistent) args.push("--persistent", persistent);
|
|
40
40
|
if (proxy) args.push("--proxy", proxy);
|
|
41
41
|
if (!geoip) args.push("--no-geoip");
|
|
42
|
+
if (locale) args.push("--locale", locale);
|
|
42
43
|
|
|
43
44
|
spawn("node", [daemonPath, ...args], {
|
|
44
45
|
detached: true,
|
|
@@ -58,7 +59,7 @@ function spawnDaemon(session: string, headed: boolean, timeout: number, persiste
|
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
async function ensureDaemon(session: string, headed: boolean, timeout: number, persistent: string | null, proxy: string | null = null, geoip: boolean = true): Promise<void> {
|
|
62
|
+
async function ensureDaemon(session: string, headed: boolean, timeout: number, persistent: string | null, proxy: string | null = null, geoip: boolean = true, locale: string | null = null): Promise<void> {
|
|
62
63
|
const sockPath = getSocketPath(session);
|
|
63
64
|
if (fs.existsSync(sockPath)) {
|
|
64
65
|
// Verify daemon is alive
|
|
@@ -73,7 +74,7 @@ async function ensureDaemon(session: string, headed: boolean, timeout: number, p
|
|
|
73
74
|
try { fs.unlinkSync(sockPath); } catch {}
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
|
-
await spawnDaemon(session, headed, timeout, persistent, proxy, geoip);
|
|
77
|
+
await spawnDaemon(session, headed, timeout, persistent, proxy, geoip, locale);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export function listSessions(): string[] {
|
|
@@ -100,10 +101,11 @@ export interface Flags {
|
|
|
100
101
|
persistent: string | null;
|
|
101
102
|
proxy: string | null;
|
|
102
103
|
geoip: boolean;
|
|
104
|
+
locale: string | null;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
export function parseArgs(argv: string[]): { flags: Flags; command: Record<string, unknown> } {
|
|
106
|
-
const flags: Flags = { session: "default", headed: false, timeout: 1800, json: false, persistent: null, proxy: null, geoip: true };
|
|
108
|
+
const flags: Flags = { session: "default", headed: false, timeout: 1800, json: false, persistent: null, proxy: null, geoip: true, locale: null };
|
|
107
109
|
const rest: string[] = [];
|
|
108
110
|
|
|
109
111
|
let i = 0;
|
|
@@ -137,6 +139,9 @@ export function parseArgs(argv: string[]): { flags: Flags; command: Record<strin
|
|
|
137
139
|
case "--no-geoip":
|
|
138
140
|
flags.geoip = false;
|
|
139
141
|
break;
|
|
142
|
+
case "--locale":
|
|
143
|
+
flags.locale = argv[++i] ?? (process.stderr.write("Error: --locale requires a value\n"), process.exit(1), "");
|
|
144
|
+
break;
|
|
140
145
|
default:
|
|
141
146
|
rest.push(argv[i]);
|
|
142
147
|
}
|
|
@@ -401,7 +406,7 @@ async function main() {
|
|
|
401
406
|
}
|
|
402
407
|
|
|
403
408
|
// Ensure daemon is running
|
|
404
|
-
await ensureDaemon(flags.session, flags.headed, flags.timeout, flags.persistent, flags.proxy, flags.geoip);
|
|
409
|
+
await ensureDaemon(flags.session, flags.headed, flags.timeout, flags.persistent, flags.proxy, flags.geoip, flags.locale);
|
|
405
410
|
|
|
406
411
|
const sockPath = getSocketPath(flags.session);
|
|
407
412
|
|
|
@@ -472,9 +477,10 @@ Flags:
|
|
|
472
477
|
--headed Show browser window
|
|
473
478
|
--timeout <secs> Daemon idle timeout (default: 1800)
|
|
474
479
|
--json Output as JSON
|
|
475
|
-
--persistent [path]
|
|
480
|
+
--persistent [path] Persistent identity — freeze fingerprint/OS/locale + store cookies/state (default: ~/.camoufox-cli/profiles/<session>)
|
|
476
481
|
--proxy <url> Proxy server (e.g. http://host:port or https://host:443)
|
|
477
|
-
--no-geoip Disable automatic GeoIP spoofing (auto-enabled with --proxy)
|
|
482
|
+
--no-geoip Disable automatic GeoIP spoofing (auto-enabled with --proxy)
|
|
483
|
+
--locale <tag> Force browser locale (e.g. "en-US" or "en-US,zh-CN")`;
|
|
478
484
|
|
|
479
485
|
const isDirectRun = (() => {
|
|
480
486
|
try {
|
{camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/daemon.ts
RENAMED
|
@@ -16,8 +16,9 @@ const timeout = parseInt(getArg("--timeout", "1800"), 10);
|
|
|
16
16
|
const persistent = args.includes("--persistent") ? getArg("--persistent", "") || null : null;
|
|
17
17
|
const proxy = args.includes("--proxy") ? getArg("--proxy", "") || null : null;
|
|
18
18
|
const geoip = !args.includes("--no-geoip");
|
|
19
|
+
const locale = args.includes("--locale") ? getArg("--locale", "") || null : null;
|
|
19
20
|
|
|
20
|
-
const server = new DaemonServer({ session, headless, timeout, persistent, proxy, geoip });
|
|
21
|
+
const server = new DaemonServer({ session, headless, timeout, persistent, proxy, geoip, locale });
|
|
21
22
|
|
|
22
23
|
process.stderr.write(`[camoufox-cli] Starting daemon session=${session} headless=${headless}\n`);
|
|
23
24
|
server.start().catch((err) => {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/** Persistent identity: freeze fingerprint/OS into a persistent dir.
|
|
2
|
+
*
|
|
3
|
+
* When a user launches with `--persistent <dir>`, a `camoufox-cli.json` file
|
|
4
|
+
* is written on first launch capturing the generated fingerprint, OS, locale,
|
|
5
|
+
* and derived timezone/geolocation. Subsequent launches reload it so the browser
|
|
6
|
+
* reports the same device identity to every site.
|
|
7
|
+
*
|
|
8
|
+
* Fingerprint/OS/canvas+font seeds are frozen for the lifetime of the identity.
|
|
9
|
+
* User-controllable fields (locale; proxy-derived timezone/geolocation) are
|
|
10
|
+
* updated to match the command line whenever it's explicitly passed — so the
|
|
11
|
+
* stored identity always reflects the most recent intent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import type { Fingerprint } from "fingerprint-generator";
|
|
17
|
+
import { generateFingerprint } from "camoufox-js/dist/fingerprints.js";
|
|
18
|
+
import { publicIP, validIPv4, validIPv6 } from "camoufox-js/dist/ip.js";
|
|
19
|
+
import { getGeolocation } from "camoufox-js/dist/locale.js";
|
|
20
|
+
import { parseProxySettings } from "./proxy.js";
|
|
21
|
+
|
|
22
|
+
const IDENTITY_FILENAME = "camoufox-cli.json";
|
|
23
|
+
const IDENTITY_VERSION = 1;
|
|
24
|
+
|
|
25
|
+
export type HostOS = "windows" | "macos" | "linux";
|
|
26
|
+
|
|
27
|
+
export interface Identity {
|
|
28
|
+
version: number;
|
|
29
|
+
created_at: string;
|
|
30
|
+
os: HostOS;
|
|
31
|
+
locale: string | null;
|
|
32
|
+
fingerprint: Fingerprint;
|
|
33
|
+
config: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hostOS(): HostOS {
|
|
37
|
+
if (process.platform === "win32") return "windows";
|
|
38
|
+
if (process.platform === "darwin") return "macos";
|
|
39
|
+
return "linux";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function identityPath(persistentDir: string): string {
|
|
43
|
+
return join(persistentDir, IDENTITY_FILENAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return the identity for this persistent directory.
|
|
48
|
+
*
|
|
49
|
+
* On first launch, a fresh identity is generated and written:
|
|
50
|
+
* - fingerprint: fingerprint-generator, firefox, host OS
|
|
51
|
+
* - canvas/font seeds: random, stored so future launches reproduce them
|
|
52
|
+
* - timezone/geolocation: derived via GeoIP if proxy is set and geoip=true
|
|
53
|
+
* - locale: recorded if passed on this first launch, else null
|
|
54
|
+
*
|
|
55
|
+
* On subsequent launches, `<persistentDir>/camoufox-cli.json` is loaded.
|
|
56
|
+
* Fields the user explicitly passes on the command line overwrite the stored
|
|
57
|
+
* values (`--locale`; `--proxy` + geoip re-derives timezone/geolocation).
|
|
58
|
+
* Fingerprint, OS, and canvas/font seeds are never touched after first launch.
|
|
59
|
+
*/
|
|
60
|
+
export async function loadOrCreate(
|
|
61
|
+
persistentDir: string,
|
|
62
|
+
locale: string | null,
|
|
63
|
+
proxy: string | null,
|
|
64
|
+
geoip: boolean,
|
|
65
|
+
): Promise<Identity> {
|
|
66
|
+
const path = identityPath(persistentDir);
|
|
67
|
+
if (existsSync(path)) {
|
|
68
|
+
const identity = JSON.parse(readFileSync(path, "utf8")) as Identity;
|
|
69
|
+
const changed = await applyCliOverrides(identity, locale, proxy, geoip);
|
|
70
|
+
if (changed) writeFileSync(path, JSON.stringify(identity, null, 2));
|
|
71
|
+
return identity;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const os_ = hostOS();
|
|
75
|
+
const fp = generateFingerprint(undefined, { operatingSystems: [os_] });
|
|
76
|
+
|
|
77
|
+
const config: Record<string, unknown> = {
|
|
78
|
+
"canvas:aaOffset": Math.floor(Math.random() * 101) - 50,
|
|
79
|
+
"canvas:aaCapOffset": Math.random() < 0.5,
|
|
80
|
+
"fonts:spacing_seed": Math.floor(Math.random() * 0x1_0000_0000),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (proxy && geoip) {
|
|
84
|
+
mergeGeo(config, await geolocateProxy(proxy));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const identity: Identity = {
|
|
88
|
+
version: IDENTITY_VERSION,
|
|
89
|
+
created_at: new Date().toISOString(),
|
|
90
|
+
os: os_,
|
|
91
|
+
locale,
|
|
92
|
+
fingerprint: fp,
|
|
93
|
+
config,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
mkdirSync(persistentDir, { recursive: true });
|
|
97
|
+
writeFileSync(path, JSON.stringify(identity, null, 2));
|
|
98
|
+
return identity;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Mutate identity with CLI-passed values. Return true if anything changed. */
|
|
102
|
+
async function applyCliOverrides(
|
|
103
|
+
identity: Identity,
|
|
104
|
+
locale: string | null,
|
|
105
|
+
proxy: string | null,
|
|
106
|
+
geoip: boolean,
|
|
107
|
+
): Promise<boolean> {
|
|
108
|
+
let changed = false;
|
|
109
|
+
|
|
110
|
+
if (locale !== null && identity.locale !== locale) {
|
|
111
|
+
identity.locale = locale;
|
|
112
|
+
changed = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (proxy && geoip) {
|
|
116
|
+
if (!identity.config) identity.config = {};
|
|
117
|
+
const derived = await geolocateProxy(proxy);
|
|
118
|
+
if (mergeGeo(identity.config, derived)) changed = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return changed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Merge proxy-derived geo into config. Return true if anything changed. */
|
|
125
|
+
function mergeGeo(config: Record<string, unknown>, derived: GeoInfo | null): boolean {
|
|
126
|
+
if (!derived) return false;
|
|
127
|
+
let changed = false;
|
|
128
|
+
if (derived.timezone && config["timezone"] !== derived.timezone) {
|
|
129
|
+
config["timezone"] = derived.timezone;
|
|
130
|
+
changed = true;
|
|
131
|
+
}
|
|
132
|
+
if (derived.latitude !== undefined && derived.longitude !== undefined) {
|
|
133
|
+
if (config["geolocation:latitude"] !== derived.latitude) {
|
|
134
|
+
config["geolocation:latitude"] = derived.latitude;
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
if (config["geolocation:longitude"] !== derived.longitude) {
|
|
138
|
+
config["geolocation:longitude"] = derived.longitude;
|
|
139
|
+
changed = true;
|
|
140
|
+
}
|
|
141
|
+
if (derived.accuracy !== undefined && config["geolocation:accuracy"] !== derived.accuracy) {
|
|
142
|
+
config["geolocation:accuracy"] = derived.accuracy;
|
|
143
|
+
changed = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return changed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Translate identity into kwargs for Camoufox / launchOptions.
|
|
151
|
+
*
|
|
152
|
+
* Returns fingerprint/os/config (always) and locale (when set). Does NOT
|
|
153
|
+
* set persistent_context / user_data_dir — the caller handles those.
|
|
154
|
+
*/
|
|
155
|
+
export function toLaunchOptions(identity: Identity): Record<string, unknown> {
|
|
156
|
+
const opts: Record<string, unknown> = {
|
|
157
|
+
fingerprint: identity.fingerprint,
|
|
158
|
+
os: identity.os,
|
|
159
|
+
config: { ...(identity.config || {}) },
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (identity.locale) {
|
|
163
|
+
const parts = identity.locale.split(",").map((s) => s.trim()).filter(Boolean);
|
|
164
|
+
if (parts.length > 0) {
|
|
165
|
+
opts.locale = parts.length > 1 ? parts : parts[0];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return opts;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface GeoInfo {
|
|
173
|
+
timezone?: string;
|
|
174
|
+
latitude?: number;
|
|
175
|
+
longitude?: number;
|
|
176
|
+
accuracy?: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Return {timezone, latitude, longitude, accuracy?} from the proxy's public
|
|
180
|
+
* IP, or null if anything fails. */
|
|
181
|
+
async function geolocateProxy(proxyUrl: string): Promise<GeoInfo | null> {
|
|
182
|
+
try {
|
|
183
|
+
const ip = await publicIP(proxyUrlWithAuth(proxyUrl));
|
|
184
|
+
if (!validIPv4(ip) && !validIPv6(ip)) return null;
|
|
185
|
+
const geo = await getGeolocation(ip);
|
|
186
|
+
const out: GeoInfo = {
|
|
187
|
+
timezone: geo.timezone,
|
|
188
|
+
latitude: geo.latitude,
|
|
189
|
+
longitude: geo.longitude,
|
|
190
|
+
};
|
|
191
|
+
if (geo.accuracy !== undefined) out.accuracy = geo.accuracy;
|
|
192
|
+
return out;
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Rebuild proxy URL as scheme://user:pass@host:port for publicIP(). */
|
|
199
|
+
function proxyUrlWithAuth(proxyUrl: string): string {
|
|
200
|
+
const settings = parseProxySettings(proxyUrl).proxy;
|
|
201
|
+
if (!settings.username) return settings.server;
|
|
202
|
+
const url = new URL(settings.server);
|
|
203
|
+
url.username = encodeURIComponent(settings.username);
|
|
204
|
+
url.password = encodeURIComponent(settings.password ?? "");
|
|
205
|
+
return url.href;
|
|
206
|
+
}
|
{camoufox_cli-0.3.0 → camoufox_cli-0.5.0/.claude/worktrees/hungry-mclaren-0aa1c0}/js/src/server.ts
RENAMED
|
@@ -17,13 +17,13 @@ export class DaemonServer {
|
|
|
17
17
|
private lastActivity = Date.now();
|
|
18
18
|
private watchdogTimer: ReturnType<typeof setInterval> | null = null;
|
|
19
19
|
|
|
20
|
-
constructor(opts: { session?: string; headless?: boolean; timeout?: number; persistent?: string | null; proxy?: string | null; geoip?: boolean }) {
|
|
20
|
+
constructor(opts: { session?: string; headless?: boolean; timeout?: number; persistent?: string | null; proxy?: string | null; geoip?: boolean; locale?: string | null }) {
|
|
21
21
|
this.session = opts.session ?? "default";
|
|
22
22
|
this.headless = opts.headless ?? true;
|
|
23
23
|
this.timeout = opts.timeout ?? 1800;
|
|
24
24
|
this.socketPath = `/tmp/camoufox-cli-${this.session}.sock`;
|
|
25
25
|
this.pidPath = `/tmp/camoufox-cli-${this.session}.pid`;
|
|
26
|
-
this.manager = new BrowserManager(opts.persistent ?? null, opts.proxy ?? null, opts.geoip ?? true);
|
|
26
|
+
this.manager = new BrowserManager(opts.persistent ?? null, opts.proxy ?? null, opts.geoip ?? true, opts.locale ?? null);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async start(): Promise<void> {
|
|
@@ -163,6 +163,8 @@ camoufox-cli cookies import auth.json
|
|
|
163
163
|
camoufox-cli reload
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
For long-lived accounts where the site also verifies device stability (not just the cookie), combine this with `--persistent` so the fingerprint stays fixed alongside the cookies — see the Persistent Identity section below.
|
|
167
|
+
|
|
166
168
|
### Multiple Tabs
|
|
167
169
|
|
|
168
170
|
```bash
|
|
@@ -313,9 +315,26 @@ camoufox-cli snapshot -i
|
|
|
313
315
|
--headed Show browser window (default: headless)
|
|
314
316
|
--timeout <seconds> Daemon idle timeout (default: 1800)
|
|
315
317
|
--json Output as JSON instead of human-readable
|
|
316
|
-
--persistent [path]
|
|
318
|
+
--persistent [path] Persistent identity — reuse the same fingerprint + cookies across launches
|
|
319
|
+
(default path: ~/.camoufox-cli/profiles/<session>)
|
|
317
320
|
--proxy <url> Proxy server (http:// or https://; auth: http://user:pass@host:port)
|
|
318
321
|
--no-geoip Disable automatic GeoIP spoofing (auto-enabled with --proxy)
|
|
322
|
+
--locale <tag> Force browser locale (e.g. "en-US" or "en-US,zh-CN")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Persistent Identity
|
|
326
|
+
|
|
327
|
+
By default, every launch gets a fresh random fingerprint. Add `--persistent [path]` to reuse the same fingerprint + cookies across launches — fingerprint/OS/canvas+font seeds are frozen on first launch (delete the directory to reset); `--locale` and proxy-derived timezone/geolocation are stored but refreshed whenever you pass the flag; `--proxy` / `--no-geoip` are never stored, so pass them every launch.
|
|
328
|
+
|
|
329
|
+
**Use it when** the same device should see the same fingerprint across visits (account-bound tasks, parallel independent identities, or when `cookies import/export` alone isn't enough because the site also checks device stability). **Skip it** for one-off scraping or quick debugging.
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Parallel identities, each with its own fingerprint + cookies
|
|
333
|
+
camoufox-cli --session a --persistent ~/.camoufox-cli/profiles/alice open https://app.example.com
|
|
334
|
+
camoufox-cli --session b --persistent ~/.camoufox-cli/profiles/bob open https://app.example.com
|
|
335
|
+
|
|
336
|
+
# Reset an identity: just remove the directory
|
|
337
|
+
rm -rf ~/.camoufox-cli/profiles/alice
|
|
319
338
|
```
|
|
320
339
|
|
|
321
340
|
## Documentation
|
|
@@ -120,6 +120,13 @@ camoufox-cli --session <name> ... # Isolated browser session
|
|
|
120
120
|
camoufox-cli --headed ... # Show browser window (not headless)
|
|
121
121
|
camoufox-cli --json ... # JSON output for parsing
|
|
122
122
|
camoufox-cli --timeout <seconds> ... # Daemon idle timeout (default: 1800)
|
|
123
|
-
camoufox-cli --persistent [path] ... #
|
|
123
|
+
camoufox-cli --persistent [path] ... # Persistent identity — reuse the same fingerprint + cookies
|
|
124
|
+
# across launches (default: ~/.camoufox-cli/profiles/<session>)
|
|
124
125
|
camoufox-cli --proxy <url> ... # Proxy server (e.g. http://host:port or http://user:pass@host:port)
|
|
126
|
+
camoufox-cli --no-geoip ... # Disable automatic GeoIP spoofing (auto-enabled with --proxy)
|
|
127
|
+
camoufox-cli --locale <tag> ... # Force browser locale (e.g. "en-US" or "en-US,zh-CN")
|
|
125
128
|
```
|
|
129
|
+
|
|
130
|
+
### `--persistent` in detail
|
|
131
|
+
|
|
132
|
+
Stores fingerprint, OS, canvas/font seeds, locale, and proxy-derived timezone/geolocation in `<path>/camoufox-cli.json` and reloads it on every launch with the same path. Fingerprint/OS/seeds are frozen — delete the directory to reset. `--locale` overwrites the stored locale when passed; `--proxy` + GeoIP re-derives timezone/geolocation each launch and writes back. `--proxy` and `--no-geoip` themselves are never stored. See the README for the full mental model.
|
|
@@ -15,6 +15,7 @@ def main():
|
|
|
15
15
|
parser.add_argument("--persistent", default=None, help="Path for persistent browser profile")
|
|
16
16
|
parser.add_argument("--proxy", default=None, help="Proxy server URL")
|
|
17
17
|
parser.add_argument("--no-geoip", dest="geoip", action="store_false", default=True, help="Disable automatic GeoIP spoofing when using a proxy")
|
|
18
|
+
parser.add_argument("--locale", default=None, help="Force browser locale (e.g. 'en-US' or 'en-US,zh-CN')")
|
|
18
19
|
args = parser.parse_args()
|
|
19
20
|
|
|
20
21
|
headless = not args.headed
|
|
@@ -26,6 +27,7 @@ def main():
|
|
|
26
27
|
persistent=args.persistent,
|
|
27
28
|
proxy=args.proxy,
|
|
28
29
|
geoip=args.geoip,
|
|
30
|
+
locale=args.locale,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
print(f"[camoufox-cli] Starting daemon session={args.session} headless={headless}", file=sys.stderr)
|
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
|
+
import os
|
|
6
7
|
|
|
7
8
|
from camoufox.sync_api import Camoufox
|
|
8
9
|
from playwright.sync_api import BrowserContext, Page
|
|
9
10
|
|
|
11
|
+
from .identity import load_or_create, to_launch_kwargs
|
|
10
12
|
from .proxy import parse_proxy_settings
|
|
11
13
|
from .refs import RefRegistry
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
_MAX_HISTORY = 200
|
|
17
|
+
|
|
18
|
+
|
|
14
19
|
def _ensure_browser_installed() -> None:
|
|
15
20
|
"""Check that the Camoufox browser binary is installed, raise if not."""
|
|
16
21
|
try:
|
|
@@ -23,7 +28,7 @@ def _ensure_browser_installed() -> None:
|
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
class BrowserManager:
|
|
26
|
-
def __init__(self, persistent: str | None = None, proxy: str | None = None, geoip: bool = True):
|
|
31
|
+
def __init__(self, persistent: str | None = None, proxy: str | None = None, geoip: bool = True, locale: str | None = None):
|
|
27
32
|
self._camoufox: Camoufox | None = None
|
|
28
33
|
self._context: BrowserContext | None = None
|
|
29
34
|
self._page: Page | None = None
|
|
@@ -32,6 +37,7 @@ class BrowserManager:
|
|
|
32
37
|
self._persistent = persistent
|
|
33
38
|
self._proxy = proxy
|
|
34
39
|
self._geoip = geoip
|
|
40
|
+
self._locale = locale
|
|
35
41
|
# Camoufox spoofs history API for anti-fingerprinting,
|
|
36
42
|
# so we track navigation history ourselves.
|
|
37
43
|
self._history: list[str] = []
|
|
@@ -51,9 +57,26 @@ class BrowserManager:
|
|
|
51
57
|
kwargs["proxy"] = proxy_settings
|
|
52
58
|
if self._geoip:
|
|
53
59
|
kwargs["geoip"] = True
|
|
60
|
+
|
|
54
61
|
if self._persistent:
|
|
62
|
+
# Persistent identity: freeze fingerprint/OS on first launch; reload
|
|
63
|
+
# it on subsequent launches. CLI-passed locale / proxy-derived geo
|
|
64
|
+
# overwrite the stored values so the file tracks current intent.
|
|
65
|
+
os.makedirs(self._persistent, exist_ok=True)
|
|
66
|
+
identity = load_or_create(
|
|
67
|
+
self._persistent,
|
|
68
|
+
locale=self._locale,
|
|
69
|
+
proxy=self._proxy,
|
|
70
|
+
geoip=self._geoip,
|
|
71
|
+
)
|
|
72
|
+
kwargs.update(to_launch_kwargs(identity))
|
|
55
73
|
kwargs["persistent_context"] = True
|
|
56
74
|
kwargs["user_data_dir"] = self._persistent
|
|
75
|
+
elif self._locale:
|
|
76
|
+
# Non-persistent path: locale is a one-shot override, no identity file.
|
|
77
|
+
locales = [s.strip() for s in self._locale.split(",") if s.strip()]
|
|
78
|
+
if locales:
|
|
79
|
+
kwargs["locale"] = locales if len(locales) > 1 else locales[0]
|
|
57
80
|
|
|
58
81
|
self._camoufox = Camoufox(**kwargs)
|
|
59
82
|
result = self._camoufox.__enter__()
|
|
@@ -127,6 +150,9 @@ class BrowserManager:
|
|
|
127
150
|
# Truncate forward history when navigating to a new page
|
|
128
151
|
self._history = self._history[:self._history_index + 1]
|
|
129
152
|
self._history.append(url)
|
|
153
|
+
# Cap history to avoid unbounded growth in long-lived daemons.
|
|
154
|
+
if len(self._history) > _MAX_HISTORY:
|
|
155
|
+
self._history = self._history[-_MAX_HISTORY:]
|
|
130
156
|
self._history_index = len(self._history) - 1
|
|
131
157
|
|
|
132
158
|
def go_back(self) -> str | None:
|