Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 23 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
push:
tags:
- "*"
# The font bundle ships as its own release, in a separate tag namespace so
# it does not appear among the browser downloads. Those tags must not
# trigger a browser build (see scripts/fetch-fonts.py).
- "!font-bundle-*"

jobs:
build:
Expand Down Expand Up @@ -86,7 +90,25 @@ jobs:
sudo apt-get update
# ccache: the mozconfig enables --with-ccache, but the runner image no
# longer ships it, so configure fails with "Cannot find ccache".
sudo apt-get install -y msitools p7zip-full aria2 ccache
# fontconfig: scripts/verify-fonts.py resolves every reportable family
# through fc-list/fc-match, so the bundle is checked with the same tool
# the browser uses rather than assumed correct.
sudo apt-get install -y msitools p7zip-full aria2 ccache fontconfig

- name: Fetch the font bundle
# The bundle is a release asset, not repo content (~2.16 GB extracted,
# 843 MB as .tar.xz -- see scripts/fetch-fonts.py). Without this the
# package would ship with NO fonts while pythonlib/camoufox/fonts.json
# still reports hundreds of families, which is a reverse leak in the
# shipped browser. After the dependency install, so the download uses
# aria2c's parallel connections rather than the curl fallback.
run: make fonts-extract

- name: Verify the font bundle matches the manifest
# Full run, not --quick: this is the release path, and the one invariant
# that cannot be recovered after publishing is a family fonts.json
# reports that the packaged fontconfig cannot resolve.
run: python3 scripts/verify-fonts.py

- name: Fetch source
env:
Expand Down
69 changes: 61 additions & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,13 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
# -e pythonlib because the settled-decisions tests import camoufox.* to
# assert against it. It is a pure-Python install; no browser involved.
- run: pip install -r ci/requirements.txt pytest -e pythonlib
- run: |
pip install -r ci/requirements.txt pytest -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
python3 scripts/pin-fpgen-model.py

- name: Synthesized input goes through one chokepoint
run: python3 scripts/check-input-dispatch.py
Expand Down Expand Up @@ -278,7 +284,13 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: pip install -r ci/requirements.txt pytest -e pythonlib
- run: |
pip install -r ci/requirements.txt pytest -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
python3 scripts/pin-fpgen-model.py
- name: Run
# pythonlib resolves the published release through the GitHub API
# (pkgman.py honours GITHUB_TOKEN). Unauthenticated, a runner shares the
Expand Down Expand Up @@ -470,7 +482,11 @@ jobs:

# Fail here, once, rather than in five browser jobs with five
# different confusing errors.
for required in camoufox-bin properties.json camoufox.cfg fonts/linux fontconfig/linux; do
# fonts/ holds the bundle's group directories (L, M, W, LM, ... --
# bundle/fonts/groups.json), not a copy per OS; groups.json is what
# utils._generate_fontconfig reads to decide which of them an identity
# may see, so its absence is the failure that matters.
for required in camoufox-bin properties.json camoufox.cfg fonts/groups.json fonts/LMW fontconfig/linux; do
[ -e "$src/$required" ] || { echo "::error::artifact is missing $required"; exit 1; }
done

Expand Down Expand Up @@ -569,7 +585,13 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: pip install -r ci/requirements.txt -e pythonlib
- run: |
pip install -r ci/requirements.txt -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
python3 scripts/pin-fpgen-model.py

- name: Download
# Same GitHub API path as the pythonlib job above, and the same anonymous
Expand Down Expand Up @@ -686,7 +708,13 @@ jobs:
- uses: ./.github/actions/prepare-browser
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: pip install -e pythonlib
- run: |
pip install -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
python3 scripts/pin-fpgen-model.py
# Fonts and properties.json are staged into the artifact by the build job;
# `make stage-fonts` here would find no source tree and do nothing.
- run: xvfb-run -a python3 -m ci.run_patch_guards --binary "$CAMOUFOX_BINARY"
Expand Down Expand Up @@ -730,6 +758,13 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- run: |
cd build-tester && npm install && pip install -r requirements.txt
# build-tester pulls pythonlib -- and so fpgen -- through its own
# requirements.txt (`-e ../pythonlib`) rather than `pip install -e
# pythonlib`, so the pin the other jobs get by matching that line has
# to be spelled out here. Without it this job was still downloading
# fpgen's model itself: TLS verification off, no checksum, and only
# ever the April-2025 release (observed in run 36050915401).
python3 "$GITHUB_WORKSPACE/scripts/pin-fpgen-model.py"
- run: xvfb-run -a python3 -m ci.run_build_tester --binary "$CAMOUFOX_BINARY"
- uses: actions/upload-artifact@v4
if: always()
Expand Down Expand Up @@ -767,7 +802,13 @@ jobs:
- uses: ./.github/actions/prepare-browser
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: pip install -e pythonlib
- run: |
pip install -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
python3 scripts/pin-fpgen-model.py
- name: Run
# Launches browsers, kills them, and proves nothing survived -- the
# failure a long-running scraper hits after six hours and no Playwright
Expand Down Expand Up @@ -824,7 +865,13 @@ jobs:
- uses: ./.github/actions/prepare-browser
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: pip install -e pythonlib
- run: |
pip install -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
python3 scripts/pin-fpgen-model.py
- run: xvfb-run -a python3 -m ci.run_native --subset growth --binary "$CAMOUFOX_BINARY"
- uses: actions/upload-artifact@v4
if: always()
Expand All @@ -850,7 +897,13 @@ jobs:
- uses: ./.github/actions/prepare-browser
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: pip install -e pythonlib
- run: |
pip install -e pythonlib
# fpgen downloads its model on first import with TLS verification
# OFF and no checksum, and its release picker can only ever reach the
# April-2025 model. Install the pinned one first: see
# scripts/pin-fpgen-model.py.
python3 scripts/pin-fpgen-model.py
- name: Run
# Exits 0 with a SKIP result if sundial itself is unreachable -- the
# browser was never measured, so neither a pass nor a failure would be
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ pythonlib/test*
jsonvv/test*
/.vscode
/bundle/fonts/extra

# The font bundle is a release asset, not repo content: ~2.1 GB extracted,
# ~770 MB as .tar.xz, and an xz archive cannot be delta-compressed, so tracking
# it would append the whole thing to history on every font change. It is pinned
# by scripts/data/font-bundle.json (asset name + size + sha256) and fetched with
# `make fetch-fonts`. Only the compressed archive is kept; bundle/fonts/ is
# extracted on demand.
/bundle/fonts/
/bundle/fonts-bundle-*.tar.xz
/bundle/*.tar.xz.part
/bundle/langpacks/
pythonlib/*.png
scripts/*.png
scripts/test*
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ Low-level equivalents: `make patch ./patches/x.patch`, `make unpatch ./patches/x

- **`patches/`** — the diffs applied to Firefox source. This is where browser behavior is changed.
- **`additions/`** — whole files copied *into* the source tree (not diffs) by `scripts/copy-additions.sh`:
- `additions/camoucfg/` — the C++ config layer. `MaskConfig.hpp` reads the spoofing config (from `CAMOU_CONFIG` env var / `camoufox.cfg`) that the patches consult at the C++ level; `MouseTrajectories.hpp` is the human-cursor algorithm.
- `additions/camoucfg/` — the C++ config layer. `MaskConfig.hpp` reads the spoofing config (from `CAMOU_CONFIG` env var / `camoufox.cfg`) that the patches consult at the C++ level. (The human-cursor algorithm used to live here too; it is now `additions/juggler/input/CursorTrajectory.js` and the vendored Cursory beside it.)
- `additions/juggler/` — Camoufox's patched **Juggler** (Firefox's Playwright automation protocol, the Firefox analog of CDP). This is where Playwright is made undetectable — the page agent runs in an isolated scope so injected automation JS is not visible to the page.
- **`settings/`** — `camoufox.cfg`, `chrome.css`, `properties.json`, `camoucfg.jvv`, prefs/policies. Copied into the source's `lw/` dir by `copy-additions.sh`. Edit the built config with `make edit-cfg`.
- **`scripts/`** — `patch.py` (the patcher, LibreWolf-derived), `developer.py` (the `make edits` UI), `package.py`, `copy-additions.sh`, `install-deps.sh`.
- **`pythonlib/`** — the `camoufox` PyPI package: the Playwright-compatible Python interface that generates + injects fingerprints via BrowserForge and launches the binary. `fingerprint-presets-v150.json` holds real scraped fingerprints. This is the user-facing API; the browser binary is the backend.
- **`pythonlib/`** — the `camoufox` PyPI package: the Playwright-compatible Python interface that generates + injects fingerprints via [fpgen](https://github.com/scrapfly/fingerprint-generator) and launches the binary. `fingerprint-presets-v150.json` holds real scraped fingerprints; `coherence.py` checks the assembled identity (the pools are sampled independently, so an impossible machine can be built from individually plausible parts), and `scripts/clean-fingerprint-data.py` applies the same rules to the shipped data files. This is the user-facing API; the browser binary is the backend.
- **`jsonvv/`** — JSON-with-validation format library used for `camoucfg.jvv` (config schema).
- **`legacy/launcher/`** — Go launcher binary.
- **`assets/`** — `base.mozconfig` and other build inputs.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ See [`build-tester/README.md`](build-tester/README.md) for full details.

### service-tester

Tests the **full stack** — the binary and the Python package together — using only the public `AsyncNewContext` API. Fingerprints are generated entirely by camoufox/browserforge with no manual injection. Real proxies are required; the WebRTC IP and timezone are auto-derived from each proxy's exit IP. This is a black-box trust test: if it fails, the fix belongs in the Python package, not in the test.
Tests the **full stack** — the binary and the Python package together — using only the public `AsyncNewContext` API. Fingerprints are generated entirely by camoufox/fpgen with no manual injection. Real proxies are required; the WebRTC IP and timezone are auto-derived from each proxy's exit IP. This is a black-box trust test: if it fails, the fix belongs in the Python package, not in the test.

**Run this when you change:** `pythonlib/` (fingerprint generation, `AsyncNewContext`, `NewContext`), proxy handling, or any behaviour that affects how the Python package interacts with the binary.

Expand Down
36 changes: 29 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ debs := python3 python3-dev python3-pip p7zip-full golang-go msitools wget aria2
rpms := python3 python3-devel p7zip golang msitools wget aria2 sqlite-devel
pacman := python python-pip p7zip go msitools wget aria2 sqlite

.PHONY: help fetch setup setup-minimal clean set-target distclean build package \
.PHONY: help fetch fetch-fonts fonts-extract fonts-check fonts-clean setup setup-minimal clean set-target distclean build package \
build-launcher check-arch revert edits run bootstrap mozbootstrap dir \
package-linux package-macos package-windows vcredist_arch patch unpatch \
workspace check-arg edit-cfg ff-dbg tests update-ubo-assets generate-assets-car \
Expand Down Expand Up @@ -81,6 +81,23 @@ ff-dbg: setup
revert:
cd $(cf_source_dir) && git reset --hard unpatched

# The font bundle is a release asset, not repo content (see
# scripts/fetch-fonts.py for why). fetch-fonts downloads and verifies the
# archive; fonts-extract unpacks it to bundle/fonts/, which every font tool and
# `make package-*` needs. Both are no-ops once satisfied, so they are cheap to
# depend on.
fetch-fonts:
python3 scripts/fetch-fonts.py

fonts-extract:
python3 scripts/fetch-fonts.py --extract

fonts-check:
python3 scripts/fetch-fonts.py --check

fonts-clean:
python3 scripts/fetch-fonts.py --clean

dir:
@if [ ! -d $(cf_source_dir) ]; then \
make setup; \
Expand Down Expand Up @@ -154,7 +171,7 @@ check-arch:
build-launcher: check-arch
cd legacy/launcher && bash build.sh $(arch) $(os)

package-linux:
package-linux: fonts-extract
python3 scripts/package.py linux \
--includes \
settings/chrome.css \
Expand All @@ -166,7 +183,7 @@ package-linux:
--arch $(arch) \
--fonts windows macos linux

package-macos:
package-macos: fonts-extract
python3 scripts/package.py macos \
--includes \
settings/chrome.css \
Expand All @@ -175,9 +192,9 @@ package-macos:
--version $(version) \
--release $(release) \
--arch $(arch) \
--fonts windows linux
--fonts windows macos linux

package-windows:
package-windows: fonts-extract
python3 scripts/package.py windows \
--includes \
settings/chrome.css \
Expand All @@ -187,7 +204,7 @@ package-windows:
--version $(version) \
--release $(release) \
--arch $(arch) \
--fonts macos linux
--fonts windows macos linux

run-launcher:
rm -rf $(cf_source_dir)/obj-x86_64-pc-linux-gnu/dist/bin/launch;
Expand Down Expand Up @@ -254,7 +271,12 @@ tests:
# Lets tests/patches/*.py run against an unpackaged build. Not needed by `run`
# or `tests`, which launch without the Python wrapper and so fall back to the
# system fontconfig.
stage-fonts:
#
# Depends on fonts-extract because the bundle is a release asset: a fresh
# checkout has no bundle/fonts/ to stage from. That is a no-op once the tree is
# unpacked (fetch-fonts.py stamps it with the archive's sha256), so this stays
# cheap enough to run before every launch.
stage-fonts: fonts-extract
bash scripts/stage-fonts.sh $(version) $(release)

unbusy:
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,11 @@ Anti-bot systems also run client-side scripts to monitor your behavior. For exam

<video src="https://github.com/user-attachments/assets/6d33d6af-3537-4603-bf24-6bd3f4f8f455" width="200px" autoplay loop muted></video>

Camoufox tries its best with its human-like mouse movement algorithm. The natural motion algorithm was originally from [riflosnake's HumanCursor](https://github.com/riflosnake/HumanCursor) and has been rewritten in C++ and modified for more distance-aware trajectories.
Camoufox does not draw its cursor paths. With `humanize=True` it uses [**Cursory**](https://github.com/Vinyzu/cursory) by [Vinyzu](https://github.com/Vinyzu), which holds 2357 mouse movements recorded from real people: it picks a recording whose direction, distance and wander suit the move being made, morphs it onto the requested start and end points, and replays it with that recording's own timing — pauses, overshoots and all.

That last part matters as much as the shape. Camoufox previously walked a Bézier curve through two random knots and emitted a point every 10ms. Both halves of that are tells: an analytic curve sampled at a fixed rate has velocity and jerk profiles that separate cleanly from a hand's, and the acceleration came entirely from one easing function, so every movement Camoufox ever made sped up and slowed down the same way. A replayed recording has neither property.

Camoufox ships [cursory-js](https://github.com/JWriter20/cursory-js), a TypeScript port of Cursory, vendored into Juggler at `additions/juggler/input/cursory/`. It reproduces the Python original bit for bit, so a path can be reproduced against `pip install cursory`. **Cursory is LGPLv3-or-later, not MPL-2.0 like the rest of Camoufox**; its licence and full provenance are in `additions/juggler/input/cursory/NOTICE`.

However, this isn't perfect. It may still be detected with sophisticated enough analysis. (WIP for the future)

Expand Down Expand Up @@ -801,7 +805,8 @@ Debloating & references:

Web scraping & testing:

- [riflosnake/HumanCursor](https://github.com/riflosnake/HumanCursor): Original human-like cursor movement algorithm, ported to C++
- [Vinyzu/cursory](https://github.com/Vinyzu/cursory): The recorded human mouse trajectories behind `humanize=True`, vendored via [cursory-js](https://github.com/JWriter20/cursory-js) (LGPLv3-or-later — see `additions/juggler/input/cursory/NOTICE`)
- [riflosnake/HumanCursor](https://github.com/riflosnake/HumanCursor): The Bézier cursor algorithm Camoufox used before Cursory
- [CreepJS](https://github.com/abrahamjuliot/creepjs), [Browserleaks](https://browserleaks.com), [BrowserScan](https://www.browserscan.net/) - Valuable leak testing sites

UI theming:
Expand Down
11 changes: 7 additions & 4 deletions additions/browser/base/content/aboutDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

"use strict";

// Services = object with smart getters for common XPCOM services
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
// Services is a chrome global; importing it declared an extra page-visible
// name, and resource://gre/modules/Services.jsm no longer exists in Firefox
// 152, so that line threw. chrome://browser/content/ is contentaccessible, so
// the declarations here must match stock's (guard:
// tests/patches/contentaccessible-parity.py).
var { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);

async function init(aEvent) {
Expand Down
Loading