mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-05 00:00:20 +00:00
e425dddcfdd764957dee5e2f58d95ceedbcd257d
718
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e425dddcfd |
chore(python): remove dead helpers and a stale dependency
None of these had a caller: - pkgman: is_supported_path, extract_zip, cleanup and set_version, left over from the single-directory install. cleanup() would have deleted every installed browser version. - multiversion.get_cached_repo_names, CONSTRAINTS.as_range, fingerprints._load_os_voices, utils._clean_locals, and unused imports. Also: - The "Apify Fingerprints" row in `camoufox version`, which has read "?" since fpgen replaced BrowserForge. - lxml is no longer a dependency; nothing imports it. - The geoip extra now names maxminddb, the module geolocation.py actually imports, rather than getting it transitively through geoip2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
be9f38b08e |
chore: remove build tooling nothing uses
- The developer UI (scripts/developer.py, `make edits`). It depended on easygui, which no requirements file declares, and every action it offered is a Makefile target: patch, unpatch, workspace, revert, diff. Its two helpers in scripts/_mixin.py (is_bootstrap_patch, patch) had no other callers. - legacy/, the Go launcher deprecated in 2024-11. Nothing built or shipped it. Its Makefile targets and scripts/run-pw.py go with it, and so does Go from every dependency list and workflow. - jsonvv/ and settings/camoucfg.jvv. Nothing read the .jvv schema: config is validated against settings/properties.json, and the two had already drifted. The jsonvv package stays on PyPI. - Scripts with no caller: bootstrap.py, moztree, setup-wasi-linux.sh, package-helper.sh, install-local-build.sh, mozfetch.sh (copied into lw/ but never packaged), examples/. - The pre-ESM Juggler copies JugglerFrameParent.jsm and JugglerFrameChild.jsm, and hidden-scrollbars.css. Juggler loads the .sys.mjs actors and deliberately no stylesheet, but jar.mn still packaged all three. - patches/librewolf/*.opt, which list_patches() never picks up; the roverfox second pass in patch.py, whose directory no longer exists; the unread --no-settings-pane option. - The CAMOUFOX_PASSWD secret passed to `make fetch` and closedsrc_rev in upstream.sh, which nothing reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c769df8ea8 | Update README wording | ||
|
|
6daae88dbc |
Stock-Firefox parity for native identities: input, fonts, locale, WebGL, WebRTC, media, timing, launcher (#779)
* feat(humanize): replay recorded human mouse movements (Cursory) humanize=True used to walk a Bezier curve through two random knots and emit a point every 10 ms. Both halves are tells: an analytic curve sampled at a fixed rate has velocity and jerk profiles that separate cleanly from a hand's, and every movement accelerated through the same easing function. Juggler now picks one of Cursory's 2357 recorded human movements whose direction, distance and wander suit the move, morphs it onto the requested endpoints and replays it with the recording's own timing. The generator is cursory-js (a bit-exact TypeScript port of Vinyzu/cursory) vendored under additions/juggler/input/cursory/; it is LGPLv3-or-later, not MPL-2.0, and ships its LICENSE and NOTICE inside juggler.jar. MouseTrajectories.hpp and ChromeUtils.camouGetMouseTrajectory are removed. sendTrajectoryAcked takes per-step pauses, drops points on the pixel the last dispatch left the cursor on (a zero-displacement move is never acked), and the humanize guards are updated for the new path shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(patches): shared helpers for binary resolution, a private Xvfb and Marionette resolve_binary() honours the runner's CAMOUFOX_EXECUTABLE_PATH before falling back to a Linux objdir (search-service-init and touchscreen-digitizer ignored it and ran the newest objdir, which after a macOS cross build is an arm64 Mach-O), hidden_display() gives a guard its own Xvfb so nothing ever opens on the user's display, and a minimal chrome-context Marionette client lets guards inspect browser UI state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(juggler): synthesized input carries what real mouse and keyboard input carries - pointerType was "" for every Playwright mouse event: juggler dispatched with MOZ_SOURCE_UNKNOWN. It now passes MOZ_SOURCE_MOUSE (#776). - keyboard.type() never pressed Shift: a shifted character now arrives bracketed by ShiftLeft keydown/keyup (location 1) with shiftKey set. - After the pointer was parked off content, pointerover/enter re-entered with buttons=1 and pressure 0.5; the tracked position is now forgotten on park. - A Windows identity gets contextmenu after mouseup with buttons=0, as Windows does; GTK/macOS keep it on press. - Wheel events are sent as line deltas (DOMMouseScroll.detail 3 per notch instead of the pixel count). - The browser rect is measured after the APZ flush await, so a chrome height change during the wait cannot put a y==0 dispatch one row above content. - ci/run_sundial.py moves and clicks the mouse so input vectors have data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(juggler): evaluate() no longer grants user activation Upstream Playwright runs every evaluate() as handling user input and notifies a user-gesture activation. Init scripts go through that path at load, so every page started with navigator.userActivation.hasBeenActive === true, autoplay allowed and popups permitted before any input. Activation now only comes from juggler's trusted input events, as in a stock browser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(juggler): stop hiding scrollbars in headless The headless agent sheet set scrollbar-width: none !important, which a page reads back from getComputedStyle and from overflow:scroll gutters. Scrollbar appearance is left to the platform look-and-feel (the launcher sets ui.useOverlayScrollbars per claimed OS). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ui): no visible automation cues in the browser window - Every Playwright context was a public container, so the URL bar showed a "JUGGLER <id>" label and a container colour. Contexts are now non-public identities (tabbrowser renders public identities only); startup cleanup still removes persisted leftovers. - showcursor defaulted to true, drawing a red dot that followed the mouse. It is now opt-in. tests/patches/visible-automation-cues.py checks both on a private Xvfb. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(fonts): web fonts, local(), per-character fallback and native bundles - FontFace / @font-face were answered from the font allowlist by the FontFace's own family name, so every url() web font failed with NS_ERROR_FAILURE and never rendered, local() of an allowed font failed, and a miss rejected with an XPCOM code instead of NetworkError (#759). Stock FontFace/FontFaceImpl are restored; local() is filtered by the RESOLVED family in gfxUserFontSet. - GlobalFontFallback forced the cmap scan, which skips families whose charmap is not loaded yet, so any character outside Gecko's script-based common-fallback table rendered as the primary family's .notdef (U+1E9E on macOS). The platform fallback chooses again, and its choice is held to the mask. - For a native macOS/Windows identity the bundled font sets are not activated: a bundled face of a family the system also has (Papyrus, Helvetica) won the lookup with different metrics. On Windows the enumerator still keeps Twemoji Mozilla, the emoji font stock Firefox ships (flag emoji drew nothing). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(fonts): CSS2 system fonts and system-ui follow the claimed OS - The host's own OS gets no system-ui override (macOS resolved system-ui to Helvetica instead of -apple-system). - CSS2 system font keywords use per-keyword faces and sizes; a Linux identity reports the Ubuntu desktop font; Windows form controls (-moz-button/field/list) answer "MS Shell Dlg 2" as Windows does, not Segoe UI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(fonts): per-OS font model and fontconfig parity fonts.json is now generated from the bundle by scripts/gen-fonts-json.py (fc-scan + aliases + scan-time families, intersected with the per-OS manifest in scripts/data/font-manifests.json) so every reportable family is renderable; scripts/verify-fonts.py checks that invariant, the generics and the reject globs. font-groups.json lets the draw keep co-shipped families together. Linux fontconfig: stock metric aliases (Arial -> Liberation Sans, ...), 49-sansserif, urw-base35 and the non-Latin rule files in stock conf.d order, generics resolving like a stock Ubuntu (Noto Sans / Noto Serif / DejaVu Sans Mono / Z003), hintslight so advances are not pinned to whole pixels, and weak <prefer> lists instead of strongly-bound generic pins so lang can promote a script face. Windows fontconfig: GDI substitution aliases, MS Shell Dlg 2, cursive/fantasy generics, duplicate-face rejects and Sitka / Segoe UI Variable optical-size families. NOTE: generated against a ~3.9 GB target font bundle that is not part of this change (one file is over GitHub's 100 MB limit); see docs/FONTS.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(locale): localize browser strings with the spoofed locale; stop rewriting explicit locales - With locale="fr-FR", Intl/number/date went French but input.validationMessage and XML parse errors stayed English, a mix no real Firefox produces. Official language packs are now baked in as packaged locales (scripts/fetch-langpacks.py, scripts/inject-locales.py, called by package.py, fetched on demand) and the launcher selects the UI locale through intl.locale.requested. A langpack add-on cannot do this: the parent pre-creates those string bundles first. - locale-spoofing.patch overrode Language/Script/Region on every intl::Locale, so new Intl.DisplayNames(['en'],{type:'region'}).of('DE') returned the spoofed region's name and Intl.Locale('ja-Jpan-JP').minimize() returned the spoofed tag. Only the OS/default locale is spoofed now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(media): enumerate, capture and label the identity's media devices coherently The fake media engine now enumerates the identity's microphones, cameras and speakers (labels and group ids from new mediaDevices:*Labels/*Groups config keys), MediaManager uses it whenever mediaDevices:enabled, and stock exposure rules apply: before a grant one device per input kind, no outputs, no labels; after a grant OS-style labels, distinct deviceIds, shared groupIds. So enumerateDevices(), getUserMedia() tracks and getSettings() ids agree, and a claimed camera captures instead of throwing NotFoundError. Fixes the content-process crash on an identity with a camera and no microphone (InsertElementAt on an empty array). docs/MEDIA-DEVICES.md; guard tests/patches/media-devices-coherence.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(navigator): globalPrivacyControl agrees between window and workers (#760) The main-thread Navigator getter ignored the config key that WorkerNavigator::GlobalPrivacyControl honours, so a page read false in the window and true in a worker. Both read the key the same way now; the launcher also mirrors it into privacy.globalprivacycontrol.enabled so the Sec-GPC header agrees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(timezone): apply the launch-level timezone from the first read (#773) The timezone config key was only applied lazily from a navigator getter, so Intl and Date reported the host zone until a page happened to touch navigator. It is now applied eagerly in every process (nsJSContext::EnsureStatics) and per realm when a new inner window is created, entering that window's realm rather than whichever one triggered the navigation. window.setTimezone() still takes precedence per context. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(screen): the CSS color media feature follows the spoofed colorDepth screen.colorDepth was spoofed at the WebIDL level only, so on a 10-bit panel a 24-bit identity reported 24 with (color: 10), a pair Gecko cannot produce. Gecko_MediaFeatures_GetColorDepth now resolves the depth in the same order as nsScreen::PixelDepth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(webgl): pass live state through instead of answering it from the table getParameter answered everything from the sampled table, so state a page had just set read back wrong (lineWidth(5) read 1, VIEWPORT/SCISSOR_BOX stayed 300x150 on a 64x64 canvas), extension parameters were null (anisotropy, draw buffers), COMPRESSED_TEXTURE_FORMATS was null instead of [], and getContextAttributes() ignored the attributes requested ({antialias:false} still reported 4 samples). Identity and limits still come from the table; live state and context attributes are real. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(webrtc): ICE gathering completes behind a proxy (#774) With Playwright's per-context proxy and media.peerconnection.ice.proxy_only_if_behind_proxy, ICE failed before gathering started and iceGatheringState stayed "new" forever, where stock Firefox completes with host candidates. When that happens around the fabricated candidates the new -> gathering -> complete state walk is replayed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(patches): refresh window-setter-seal.patch offsets Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(windows): embed Firefox's application manifest in camoufox.exe config/rules.mk embeds <program>.manifest and browser/app only ships firefox.exe.manifest, so --with-app-name=camoufox produced an exe with no manifest. Without the Windows 10 supportedOS GUID the process and its children run as a pre-Windows-10 application and Gecko's Windows-10-gated paths switch off (MediaCapabilities.decodingInfo powerEfficient false for H.264/VP9 where stock is true). The new patch adds a byte-for-byte copy as camoufox.exe.manifest; the old rename hunk in windows-theming-bug-modified.patch is dropped. Guard: tests/patches/windows-exe-manifest.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(settings): stock values for page-observable prefs; launcher prefs at startup Page-observable defaults that no stock Firefox has, restored: - the forced built-in dark theme (it also removed the 1 px nav-bar separator, and was applied ~1 s after startup, resizing the viewport) and ui.systemUsesDarkTheme (prefers-color-scheme disagreed with the desktop); - focus rings off, autoplay allowed, popup blocker off; - gfx.color_management.mode=0 (Playwright's test pref: ICC-tagged images were drawn unconverted, readable from a canvas pixel); - ui.use_standins_for_native_colors (non-native system colours); - GMP updates off (Widevine/OpenH264 never available); - storage.estimate() quota derived from the raw disk instead of the stock cap. The HardwareAcceleration:false enterprise policy is removed: it locked software WebRender with no hardware video decoding on every OS (guard tests/patches/hardware-acceleration-policy.py). The minimal-theme chrome.css is emptied: its ~55 px chrome made outerHeight - innerHeight impossible. Playwright's non-persistent launch writes no user.js, so launcher prefs only arrived through juggler after startup and anything Gecko reads while starting raced (on Windows the UI locale lost 3 of 4 launches). camoufox.cfg now applies the launcher's CAMOU_PREFS_1..N env chunks as default prefs at startup (guard tests/patches/startup-prefs.py). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(branding): chrome://branding assets match stock Firefox chrome://branding/content/ is content-accessible. The wordmark SVGs had different intrinsic sizes (336x48 / 172x48 vs 300x67) and document.ico, document_pdf.svg and the private-browsing about logos were missing, all measurable from a page with an <img>. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(pythonlib): identity draws that match real machines and stay stable Launcher-side fixes found by comparing camoufox against stock Firefox 152.0.4 on Linux, Windows 11 and macOS hosts: - DNT / GPC: BrowserForge draws doNotTrack "1" on most Firefox samples, but a stock Firefox 152 reports "unspecified" and globalPrivacyControl false; the stock defaults are used unless the caller sets them, and both are applied as prefs so the API, the worker and the DNT / Sec-GPC headers agree (#760). - Timezone and geolocation: the timezone is passed to the browser, and a configured position sets permissions.default.geo so permissions.query agrees with the auto-grant (#769, #773). - hardwareConcurrency: the reported count is the fingerprint's and the browser is pinned to that many cores (cpu_affinity.py, Linux/Windows), so worker timing agrees with it; otherwise the host count snapped into the core counts real machines ship with (never 2, Firefox's resistFingerprinting value). - Fonts: the OS base is always present in full, OS-version variants are drawn all-or-nothing, co-shipped groups stay together, Cascadia is never claimed off Windows, a native macOS/Windows identity claims only the real OS base, and gfx.font_rendering.fallback.async is off on Linux so per-character fallback does not depend on cmap-load timing. - Speech voices: a per-OS installed-voice model (voice-manifests.json) with the voiceURI formats each backend really produces (voice-uris.json); no default voice where stock has none. - WebGL: extensions a release Firefox never exposes are filtered, but OVR_multiview2 stays for Windows D3D11 renderers, which expose it. - Media devices: a seeded draw of common per-OS devices with OS-style labels. - Windows scrollbars follow the drawn Windows version (overlay on 11). - Glyph-advance perturbation (fonts:spacing_seed) defaults to off: it moved every measureText width off the value the same font gives on a real machine. - Launcher prefs are also exported as CAMOU_PREFS_1..N so camoufox.cfg applies them at startup, and the browser UI locale follows the spoofed locale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(pythonlib): per-identity salt for seeded draws; core pinning under concurrency Found in review of the previous commits: - identity_seed() hashed only the UA, platform, screen size and core count. Those take a handful of values per OS, so over 500 launches the seed took 12-30 distinct values and every install drew its fonts, voices, GPU, media devices and canvas/audio noise seeds from that same short list. The seed now mixes in identity_salt(): derived from what the caller pinned the identity with (a Fingerprint, a preset dict, a config naming the UA) so relaunching that identity reproduces every draw, and random otherwise. A pinned preset now reproduces its noise seeds too; seeds the caller sets are kept. - Concurrent AsyncNewBrowser launches on one driver interleaved pin/restore: one browser inherited the other's mask and the driver could stay pinned. pin -> launch -> restore is serialized per driver. - Every pinned browser landed on cores 0..N-1; pins now take N adjacent cores from a random start. - A pinnable host with 1-3 cores reported 1, 2 or 3 (2 is the resistFingerprinting value); the table floor of 4 applies as on other hosts. - launch_options() callers that launch the browser themselves (launch_server, direct use) kept the drawn core count although nothing pins the browser; only Camoufox/AsyncCamoufox pass pin_cpu_cores=True now, everyone else reports the host's snapped count. - PLAUSIBLE_CORE_COUNTS gains 18, 22, 28 and 32, all recorded in the -v150 corpus. - The Windows voice list was drawn before the locale was resolved, so an fr-FR identity got en-US voices; it is drawn after locale/geoip now. - macOS "Alex" gets its com.apple.speech.synthesis.voice identifier. - CAMOU_PREFS env chunks are ASCII-only JSON (Windows getenv goes through the ANSI code page). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(fonts): canvas accepts CSS2 system-font keywords; local() works on macOS - GetSpoofedSystemFontForRFP's per-OS branches returned before marking the result a system font. ComputeSystemFont copies that flag into FontFamilyList::is_system_font, and without it the canvas font setter could not serialize the value: ctx.font = 'caption' (or icon, menu, message-box, small-caption, status-bar) was silently ignored and read back '10px sans-serif' where stock reads back the keyword. - CoreTextFontList::LookupLocalFont builds a CTFontEntry with no family name, and local() sources are held to the spoofed font list by the resolved family, so on macOS every local() face (Helvetica, Menlo, Arial...) failed with NetworkError, installed and allowed or not. The entry now carries the family CoreText resolved. A blocked lookup's entry is released instead of leaked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(webgl): only device limits come from the spoofed table getParameter still answered ~100 state pnames from the table, so state the page had just changed read back wrong: UNPACK_FLIP_Y_WEBGL / PREMULTIPLY_ALPHA / COLORSPACE_CONVERSION after pixelStorei, FRAGMENT_SHADER_DERIVATIVE_HINT after hint(), DRAW_BUFFERi after drawBuffers(), RED/ALPHA/DEPTH/STENCIL_BITS and IMPLEMENTATION_COLOR_READ_* for the bound framebuffer, and COMPRESSED_TEXTURE_ FORMATS after enabling an extension. UNMASKED_VENDOR/RENDERER_WEBGL came back without the extension enabled, where stock returns null with INVALID_ENUM. The table now answers only the MAX_*/ALIASED_*/SUBPIXEL_BITS limits, WebGL 2 limits on WebGL 2 contexts only, and extension limits (anisotropy, draw buffers, OVR multiview) only once that extension is enabled; everything else is the real context's answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(webrtc): fabricate candidates only where a real gather would have them With webrtc:ipv4/ipv6 set (every geoip launch), new RTCPeerConnection() with no iceServers produced a srflx carrying the spoofed IP, and after end-of-candidates a second host set with a different mDNS name. getStats() exposed that srflx as id 'camou-srflx' and rewrote every candidate address, including .local host names and the remote peer's candidates. A srflx is now fabricated only when the page configured an ICE server, host candidates only when none reached the page (sharing the real UDP host's port otherwise), the synthetic stats id has the shape real candidate ids have (8 hex digits, fixed per connection), and only this side's non-mDNS addresses are rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(media): honour mediaDevices:enabled=false; page fake:true behaves as stock - MaskConfig::GetBool returns std::optional<bool>, and the checks tested its presence: "mediaDevices:enabled": false still enabled the fake devices. - media.navigator.permission.fake=true was page-readable: a page's own getUserMedia({video: true, fake: true}) prompted and never resolved, where stock resolves at once with its generic fake device. The pref is off again; the identity's devices count as real hardware in the capturing checks instead (prompt, sharing indicator, post-grant labels), and a page's fake:true request gets stock's generic devices rather than the identity's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(storage): per-context values are session state, and misses are cached - Values lived on the user pref branch, which a persistent profile writes to prefs.js: relaunching with a different timezone (or navigator values) kept reporting the previous session's in the page, iframes and workers. They now live on the default branch, which is never saved, and reads ignore user values an older build left behind. - A read of an unset key did a synchronous IPC to the parent every time, and in a launch without per-context values every read is unset: navigator.hardwareConcurrency, screen.* and (color) media queries measured ~20x slower than stock. A miss is now cached per key until a pref change or a local put clears it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(timezone): no per-realm override for the process-wide zone; cache DateTimeInfo With a launch-level timezone every new document and worker got a per-realm override of the zone the process already reported. Setting one releases all JIT code in the runtime (hot code after adding an iframe ran ~4x slower), and the realm rebuilt its DateTimeInfo on every call (getHours() ~40x slower than stock). The override is applied only when the zone differs from the one JS::SetTimeZoneOverride applied process-wide, and a realm keeps its DateTimeInfo until its override changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(juggler): wheel scrolls in native notches; Shift leads the key it modifies - A wheel notch now reaches the page as its own 3-line event carrying one native tick (new WHEEL_EVENT_NATIVE_NOTCHES option in patches/wheel-native-ticks.patch), so wheelDelta is -120 per notch as with a physical wheel; it was -396, and a multi-notch scroll arrived as one event. Several notches are spaced a few tens of ms apart. - Auto-Shift pressed Shift 0 ms before the character's keydown and released it 0 ms after its keyup; it now leads and trails by a drawn human-scale delay, and a failing keydown no longer leaves Shift latched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(settings): stock cookie partitioning, preconnect, popup notification; about dialog CSS - network.cookie.cookieBehavior 4 (Playwright's) -> Firefox's default 5. With 4 a cross-site iframe (captcha and anti-bot widgets are exactly that) sees document.hasStorageAccess() true and its first-party cookies and localStorage, where stock partitions them. Playwright set 4 so storageState need not carry thirdPartyCookie^ permissions. - network.http.speculative-parallel-limit 0 turned <link rel=preconnect> into a no-op, visible in Resource Timing. - privacy.popups.showBrowserMessage false: stock shows a notification bar for a blocked popup, which shrinks the viewport and fires resize. - chrome://branding/content/aboutDialog.css is page-loadable and was empty; it is the official branding's now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(patches): stock-parity probes for the leaks found in review One launch (plus two persistent relaunches) checks page-observable invariants stock Firefox 152 holds: canvas CSS2 system-font keywords, WebGL state readback and UNMASKED_RENDERER without the extension, no srflx without ICE servers and no 'camou' stats id, getUserMedia({fake: true}), cross-site storage partitioning, wheel notches, per-read cost of (color)/hardwareConcurrency and of local Date getters under a launch timezone, and a persistent profile's timezone after relaunch (page and worker). Run against the build before these fixes it fails on every one of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(humanize): judge cadence by distinct values and spread, not a share of the gap count The page clock is clamped to 1 ms and Cursory's recorded steps mostly sit between 12 and 20 ms, so the number of distinct gap values cannot grow with the number of gaps. Requiring len(gaps) // 4 made the guard fail on visibly uneven runs whenever event delivery was steady (3 of 4 runs once the per-read sync IPC jitter was gone). A fixed 10 ms cadence yields about three values within a few ms of each other, which the new rule (>= 6 values, >= 20 ms spread) still fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(juggler): restore the popup, wheel and history contracts The Playwright suite went red on this branch, and five of its six shards ran out their 40-minute budget before reporting, so ~470 sync tests were never run at all. Three causes, all ours: - window.open() from page.evaluate() returned null. The popup blocker being ON (Firefox's default) and evaluate() no longer granting user activation are each defensible alone; together they block every gesture-less popup. ~35 tests, each burning 30s x 4 attempts x 2 worlds, which is what exhausted the shards. The blocker goes back to Playwright's and geckodriver's value. Reading it costs a detector a popup window the user sees, so it is not a check an anti-bot script in the page runs -- unlike navigator.userActivation.hasBeenActive, which is one property read, and which is why the activation half stays. - mouse.wheel(0, 100) delivered deltaY 114 (or 132, depending on the host's font metrics) in deltaMode 1. Quantising into native wheel notches is what a physical wheel does, but it changes the number the caller asked for, so it now rides behind humanize= with the rest of the humanized input. Default is the exact requested delta in deltaMode 0. - page.go_back() did nothing after history.pushState(). canGoBack is the BACK BUTTON's answer: under browser.navigation.requireUserInteraction it reports false when every entry behind this one was pushed without the user touching the page, which is now every entry, because evaluate() grants no activation. goBack() itself does not skip those entries and neither does history.back(), so ask canGoBackIgnoringUserInteraction, as Marionette does. Two keyboard tests are skiplisted rather than fixed: auto-Shift means typing "!" emits the Shift a US keyboard requires, and upstream asserts the character's three events with shiftKey false throughout. The character's own key/code/keyCode are unchanged; what upstream asserts is the absence of a Shift no real typist could omit. Full suite against the fixed build: 2223 passed, 6 failed -- the two keyboard tests above, and four client-certificate tests that fail only on this machine (Node/OpenSSL rejects the fixture server) and pass in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(pythonlib): stop overriding the corpus on core counts; pinning is opt-in Two findings from auditing the sweep's fixes against one bar: a difference is a leak only if a page's JavaScript can actually read it. hardwareConcurrency 2 was excluded from PLAUSIBLE_CORE_COUNTS because "2 is what Firefox reports under resistFingerprinting". That has not been true for years: RuntimeService::ClampedHardwareConcurrency hardcodes 4, and 8 on macOS, both of which are already in the table. The exclusion protected against nothing and cost every genuinely dual-core machine -- 20% of the macOS presets in the recorded corpus, 4.2% of Linux draws. It also made the small-host tail worse: a 3-core host reported 4, which cannot be pinned, so a page measured 3 while being told 4. At 2 the pin succeeds. pin_cpu_cores now defaults to False. What it buys is defence against a page timing N parallel workers; what it costs is a browser-wide CPU cap, a per-driver launch lock, and nothing at all on macOS. Unpinned, the host's own snapped count is reported, so reported and measurable still agree -- the identity just loses one drawn value. Callers who want the draw kept can still ask for it. The WebGL sampler keeps rejecting software rasterisers, and its docstring now says so: it described the opposite of what the code does. llvmpipe as the presented GPU is a live check on a string every fingerprint script reads, which is worth ~1.5% of corpus fidelity. 251 pythonlib tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(pythonlib): keep 2 out of the core table -- an Apple M1 is never dual-core Reverts the PLAUSIBLE_CORE_COUNTS half of |
||
|
|
5e59b70bdd |
Update README
Added id="sponsors" & Bytefulfont-bundle-v1 |
||
|
|
52d6746a4a |
ci: a repo-wide test pipeline, and the one check that gates merge on it (#772)
* ci: a repo-wide test pipeline, and the suites Camoufox was missing
Nothing checked a pull request before this. `build.yml` runs on tags and takes
about forty minutes, and `lint.yml` ran a single static script, so a change
could reach main having had no browser suite run against it at all.
This adds one pipeline, driven identically from a pull request, a push to main,
and -- through `workflow_call` -- any caller that needs to test a specific
browser version, so there is exactly one definition of "the tests pass".
resolve ──┬─ static ────────── tribal rules, skiplist, self-tests
├─ pythonlib ─────── the package's own tests
└─ build ──┬─ playwright upstream × 6 shards (conformance)
├─ playwright vendored (regression)
├─ native ───────────── leaks, contexts
├─ patch guards ─────── one per spoofing patch
├─ build-tester ─────── 8 fingerprint profiles
└─ sundial ──────────── stealth grade (off, see below)
│
summary ──► one comment on the PR
Two Playwright suites, because they answer different questions. `tests/` is a
frozen ~v1.55-era fork carrying roughly 1800 lines of Camoufox adaptations, so
every test in it has a known prior outcome: that is the regression check. The
upstream suite is fetched fresh at the tag `ci/versions.py` resolves and runs
unmodified, which is the conformance check -- `ci/pw_camoufox_plugin.py` adapts
the environment around it rather than editing it, hooking BrowserType at the
_impl layer so upstream can refactor its fixtures freely.
`native-tests/` covers what neither can ask about: that resource cost does not
scale with launch count (the shape an FD or socket leak actually has), that two
contexts in one browser get different fingerprints while two pages in one
context get the same one (get this wrong and per-context injection silently
degrades to process-global, which passes every single-context test there is),
and that decisions already made stay made -- `ci/tribal-rules.yml` lists them
with the issue or PR that settled each.
Cost is tiered so a two-second lint failure never reaches a build, and a
driver-only pull request never builds at all: it fetches the published release
and tests against the build users are actually running, a minute instead of
seventy. Merges gate on one required check, `All tests passed`, so the
branch-protection list does not need editing every time a suite is added or
resharded; `ci/branch-protection.json` holds the settings so they are reviewable
rather than lore.
**The stealth check ships disabled** (`ci/sundial.yml: enabled: false`). It
drives a private detection suite, and the deployment it talks to predates that
suite's score mode; an older one ignores `?score=1` and posts the entire report
-- every vector's id, name, brief, source and value -- to whatever collector
asked. Receiving that on a public runner and discarding it afterwards is not the
same guarantee as never being sent it, so while the flag is false the job is not
scheduled, no credential enters a runner, and `run_sundial.py` refuses a hand-run
too. When it is enabled, `redact()` publishes a grade and counts against a
runtime whitelist and refuses anything that is not already aggregated.
Also included: the fixes these suites exposed on a clean runner -- build-tester
hashing canvas pixels rather than a prefix of the data URL, the virtdisplay
cleanup when Xvfb has already died, a juggler sandbox released on frame destroy
rather than only on navigation, and the pythonlib geometry and version-floor
corrections. `lint.yml` is removed because the static job absorbed its one check.
Verified locally: ci/tests 68 passed, tribal rules 24 passed, pythonlib 209
passed, input-dispatch clean, `ci.versions` resolves 152.0.4/beta.31 against
playwright v1.61.0, and `ci.summarize` folds a run to "all suites passed".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1UY3f8gm2jA1J23C3ew9s
* ci: make result files survive the trip from job to summary
The first full run failed, and the summary could not say why: five suites came
back "required, but produced no result", including two whose jobs had passed.
Three separate plumbing bugs, none of them in a test.
**Hidden files.** `actions/upload-artifact@v4` excludes dotfiles unless told
otherwise, and every result we write lives under `.ci-work`. The jobs whose
`path:` was a list containing a glob uploaded nothing at all -- the Playwright
suites and the leak suite each wrote their evidence and then had it silently
dropped:
evidence -> .../.ci-work/results/playwright_vendored.json (fail, 1203 tests)
##[warning]No files were found with the provided path: .ci-work/results/
.ci-work/junit-*.xml. No artifacts will be uploaded.
**Common root.** Where a list did upload, the second entry moved
upload-artifact's common root from `.ci-work/results/` up to `.ci-work/`, so the
JSON arrived at `results/build_tester.json` instead of the artifact root. The
summary merges every `results-*` into one directory and `load_all()` globs a
single level, so the file was there and invisible. build_tester passed and was
reported missing.
Every `results-*` artifact now uploads exactly `.ci-work/results/`, with
diagnostics (junit XML, the build-tester graded tree) split into their own
`diagnostics-*` artifacts that the summary's `results-*` pattern ignores.
`include-hidden-files: true` everywhere that touches `.ci-work`.
**A required name nothing writes.** `static` was in the required list, but it is
a job, not a suite -- no runner writes a result by that name, so summarize
reported it missing on every run including a wholly green one. The suites that
job runs are the pipeline self-tests, which write no result, and native_rules,
which is required by name. The job is already covered: the gate fails on any job
that is not success.
Three guards, each verified by reintroducing the bug it catches:
- results-* artifacts upload exactly one path, so nothing nests
- anything touching .ci-work sets include-hidden-files
- every required name is one some runner can actually write
This changes no test. The real failures the first run found -- 6 in the vendored
suite, plus upstream shards 1 and 5 and the leak suite -- were masked by the
above and should now be reported rather than swallowed.
ci/tests 71 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1UY3f8gm2jA1J23C3ew9s
* test: two failures that were the tests' fault, not the browser's
**The leak check waited on the wrong set of processes.**
`test_a_single_launch_leaves_nothing` failed with Gecko's GPU probe still alive:
1 process(es) this test started are still alive: glxtest(2887, now ppid=1)
`settle()` polled `children(recursive=True)`, but `survivors()` judges the
sampled PID set -- deliberately, so that a process reparented to init cannot
hide a leak. Those two sets differ exactly when a process outlives its parent:
it stops being our child, `settle()` sees nothing left and returns at once, and
anything still winding down is reported as leaked. `glxtest` does this on every
launch; it is spawned by Gecko, its parent exits first, and it needs a moment.
So settle on the set the assertion actually uses. This is a grace period, not an
exemption -- a process that is still there when the timeout expires fails the
test exactly as before, and no name is special-cased.
**Playwright renamed a protocol method the tracing tests spelled out.**
`Page.waitForEventInfo` is `Page.__waitInfo__` in newer versions, so two tracing
assertions failed on a name, not on behaviour. The suite is pinned to a range
(`playwright<1.63`), not a version, so hard-coding either spelling is wrong.
Normalised in `get_trace_actions()`, next to the comment about the last time
Playwright moved this data -- the tests care which actions ran and in what
order, not what Playwright calls them this month.
Neither of these was Camoufox misbehaving.
Still failing, and genuinely about the browser or by design -- triaged next:
navigation popup load state, locator handler visibility, clock pause off by 1ms,
websocket close reason, and the three upstream ones (request headers, worker
locale, screencast viewport) which all look like deliberate spoofing divergence
and probably belong in the skiplist with a stated reason.
ci/tests 71 passed, tribal rules 24 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1UY3f8gm2jA1J23C3ew9s
* fix: the failures the new pipeline found, and the flake that hid them
Eleven gates were red on PR #9. Each one is now either a fixed defect or an
entry that says why the test cannot apply here -- nothing is silenced.
One real browser bug, found by the conformance suite:
The compositor-backed screencast added in
|
||
|
|
571e416ac1 |
Update README
Layer3 |
||
|
|
041ceb0af1 |
Update README
September updates |
||
|
|
b6aa72ea5a | Update REAMDE | ||
|
|
1b4f43ace3 | Update README | ||
|
|
eb5dc3bc5b |
chore(release): v152.0.4-beta.31, pythonlib 0.5.6
Browser: beta.31, cut from the integration merge in
v152.0.4-beta.31
|
||
|
|
35b45c145c |
Merge pull request #756 from JWriter20/integration/triage-2026-09-05
Integration branch: eleven open PRs built and verified together on FF152 |
||
|
|
1ae5baef6c |
test(touchscreen): replace the reconstructed reference with the real capture
The reference values were reconstructed, because the recording sat on the
Windows reference box and was not reachable when the guard was written. It is
now read from that machine, so the table is the measured article.
The reconstruction had the right values but the wrong *set*. Of the sixteen
static signals it listed, fourteen matched the capture exactly. The other two
were invented -- `'ontouchstart' in document` and `... in documentElement`,
both false, both harmless -- and they stood in for two real signals that were
missed entirely:
document.createTouch false. Gated by TouchEvent::LegacyAPIEnabled, the same
gate as ontouchstart, so a real touchscreen laptop exposes TouchEvent while
createTouch stays absent.
window.PointerEvent true, and not gated on a digitizer at all. Pinned so it
cannot start varying.
Both were verified against this build before being written down: it reports
false and true respectively, so the fingerprint matches the device on all
sixteen signals, not merely on the fourteen that were guessed correctly. The
two invented signals are kept as informational output, since nothing measured
backs them.
Captured with tests/assets/touch-reference.html, added here so the recording
can be reproduced on another device: it renders the sixteen static signals plus
an input-event log as JSON. The source device was a Dell XPS 15 9510 on
Firefox 152.0 (Windows NT 10.0, Win64, Win32, 1382x864 @ dpr 2.5), whose
capture also carried 284 input events that this guard does not assert on.
Still fails on a binary without the fix -- now 12/16 rather than 13/16, since
the recorded set no longer includes the two invented always-false signals.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2RfR387Mh1JhZ9LZvkptP
|
||
|
|
92d79a8c8e |
chore: drop three stale .bak files
None is referenced by anything that builds or tests the tree:
* additions/juggler/TargetRegistry.js.bak -- copy-additions.sh does
`cp -r ../additions/* .`, so this 48K copy was landing in every source tree.
It does not reach omni.ja, so it never shipped to users, but it has no reason
to be in the build either.
* patches/ghostery/Disable-Onboarding-Messages.patch.bak
* patches/librewolf/urlbarprovider-interventions.patch.bak -- neither is picked
up by list_patches(), which globs "*.patch", so both have been inert since
they landed.
scripts/check-input-dispatch.py skips `path.name.endswith(".bak")`, but that
condition is already unreachable: it filters the output of rglob("*.js"), which
cannot yield a name ending in .bak. Left in place rather than widen this commit
into someone else's guard. Verified: the script still reports the same 16 files
scanned, and patch discovery still finds 56 patches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2RfR387Mh1JhZ9LZvkptP
|
||
|
|
e459dc5e1e |
fix(patches): express the no-search-engines stub in search-config v2 shape
SearchService.#init() threw on every launch, on a clean profile, in beta.28 through beta.30: "missing field `recordType` at line 1 column 114". The browser ended up with no search service at all, which also takes out the urlbar's heuristic result. Reported as daijro/camoufox#737. The issue blames a stale bundled search-config-overrides-v2 dump. It is not that. The reporter retracted that diagnosis after measuring, and both bundled dumps are fine as shipped -- the overrides dump is never even parsed, because setSearchConfig throws on the line before setConfigOverrides is reached. The failing document is this patch's own stub. It returned a search-config *v1* record (appliesTo/webExtension), but the selector is the Rust SearchEngineSelector, which deserializes v2: #[serde(tag = "recordType", rename_all = "camelCase")] enum JSONSearchConfigurationRecords { ... } recordType is that enum's tag, so a record lacking it aborts the whole document. Note the #[serde(other)] Unknown variant: an unrecognised recordType is tolerated, a missing one is fatal. Serialising the old stub gives exactly 116 characters with '}' at column 114 -- precisely where serde reports the missing field, confirming the stub is the document that fails. Return v2 records instead: one engine, plus defaultEngines and engineOrders. An empty configuration is not an option, since getEngineConfiguration() rejects [] with "Failed to get engine data from Remote Settings" and SearchSettings refuses to write without an engine. So the "no search engines" stance is kept by making the single engine inert, with a loopback search URL that cannot leave the machine even if something does submit a search. Required fields were checked against configuration_types.rs in this tree rather than assumed. Verified on linux x86_64: 0 recordType errors, "Completed #init", search.json.mozlz4 written, and the only engine present is the inert "None" that settings/distribution/policies.json already defines as the default. tests/patches/search-service-init.py guards it. This shipped broken through three releases because the failure is a console error on a browser that otherwise starts fine, so the test asserts the observable consequences -- settings written to disk, and the engine list -- rather than only the absence of the error string. It fails on stock beta.30 and passes here. The issue's further claim that this also empties the urlbar drop-down (history, autofill) is untested here; the reporter flagged it as an inference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W2RfR387Mh1JhZ9LZvkptP |
||
|
|
a80abb452a |
feat(patches): report a touchscreen digitizer, not a phone
navigator.maxTouchPoints could already be spoofed, but nothing moved with it, so a spoofed digitizer contradicted itself in two places a script reads in one line: (any-pointer: coarse) stayed false, and window.TouchEvent and window.Touch were absent entirely. Restore the aID branch in force-default-pointer.patch so the coarse bit joins the *any-pointer* set, and only when maxTouchPoints > 0. The primary pointer stays Fine|Hover: a touchscreen laptop still drives its trackpad, and reporting (pointer: coarse) would claim a phone while the accompanying desktop UA said otherwise. The host LookAndFeel value is still not consulted -- the capability set must not vary with the machine the browser runs on. Expose the touch interfaces by moving TouchEvent::PrefEnabled only, never LegacyAPIEnabled. dom.w3c_touch_events.legacy_apis.enabled is false everywhere but Android, so a real Windows touchscreen laptop exposes TouchEvent and Touch while 'ontouchstart' in window is false. Matching that shape matters more than exposing the whole touch API: a build that switches touch on wholesale is more detectable than one that does nothing. Rename mobile-fingerprint-spoofing.patch to touchscreen-fingerprint-spoofing .patch, since the rationale is the ordinary Windows touchscreen laptop rather than a phone, and carry the new TouchEvent.cpp hunk there beside the existing Navigator.cpp one. The rename moves it after navigator-spoofing.patch in basename order, so its Navigator.cpp hunk now lands with an offset; verified to still apply cleanly with no rejects. Warn at launch whenever navigator.maxTouchPoints is set, separately from the blanket navigator warning, because the knock-on effects reach past navigator into the CSS pointer media queries and the TouchEvent interfaces. tests/patches/touchscreen-digitizer.py checks all 16 signals and asserts that maxTouchPoints=0 still looks like a machine with no digitizer. It fails on a binary built without this change (13/16) and passes on one built with it. The reference values it carries are RECONSTRUCTED, not captured: the recording from the Dell XPS 15 9510 was not reachable from the build host, so eight values come from the specification and eight from Gecko's own gating logic. Each is marked in the table. Check them against the real capture when the reference machine is available; the capture wins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W2RfR387Mh1JhZ9LZvkptP |
||
|
|
0e1f9a816d |
fix(python): bound headful geometry again after the get_screen_cons flip
PR #315 corrects get_screen_cons()'s inverted guard (`headless is False` ->
`headless is True`), which is right on its own. But the call site passes
`headless or has_display(env)`, folding two separate questions into one
boolean, so with the corrected guard a headful run on a real display now
reads as headless and the display bound is skipped:
headless=False, has_display=True -> arg=True -> None (want Screen)
headless=False, has_display=False -> arg=False -> Screen (want None)
That drops the monitor bound for every ordinary headful launch, which is
the constraint
|
||
|
|
0169975638 |
fix(config): declare media:spoof_codecs, and guard the whole class
PR #562 added a `media:spoof_codecs` read on the C++ side -- MaskConfig::GetBool("media:spoof_codecs") in MP4Decoder and MatroskaDecoder -- but never declared the key in settings/. Since validate_config() drops any key it does not recognise, the documented usage was inert: AsyncCamoufox(config={"media:spoof_codecs": True}) -> "Skipping unknown patch media:spoof_codecs : True" The key never reached the browser, so the feature could not be turned on through the supported path at all. Declared in both properties.json and camoucfg.jvv (bool, beside mediaDevices:enabled). The new test is the general form rather than a check for this one key: it scans patches/ and additions/ for MaskConfig::Get*/Has*("key") reads and fails when a key is not declared in settings/properties.json. A patch and its schema entry are two halves of one change, and shipping only one half is a mistake this project has now made in both directions -- canvas:seed (#721) and navigator.maxTouchPoints (#696) were declared but unconsumed; this one was consumed but undeclared. Across the tree the scan finds 63 reads against 109 declared keys, and media:spoof_codecs was the only gap. Note the runtime reads properties.json from the *installed browser bundle*, not the repo, so this fix only takes effect for a build packaged after it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQgHHGRXNp29jr4xQjK7iv (cherry picked from commit 375b0fca4529a722220022c7993c030b83439db1) |
||
|
|
17fe73a084 |
fix(patches): retarget the media-codec include hunk to the FF152 order
PR #562's `media-codec-spoofing.patch` was written when MP4Decoder.cpp included H264.h and VPXDecoder.h adjacently, with AOMDecoder.h behind `#ifdef MOZ_AV1`. FF152 sorts that block alphabetically and hoists AOMDecoder.h out of the ifdef, so the `#include "MaskConfig.hpp"` hunk found no context and the patch failed to apply -- taking every patch after it in the sequence down with it. Only the include hunk was stale; the IsSupportedType() body hunk applied cleanly. Retargeted onto the PlatformDecoderModule.h line, matching where the same patch already places the include in MatroskaDecoder.cpp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQgHHGRXNp29jr4xQjK7iv (cherry picked from commit 5debb846630bf6e16b85d5e26f96e897d16cabc3) |
||
|
|
4132dd50a6 |
refactor(juggler): make the input-dispatch deadlock structurally unreachable
Four deadlocks shipped between 2026-04 and 2026-09 -- exact-edge coordinates (9270618), humanized trajectory points that bypassed the endpoint's guard ( |
||
|
|
6e994b9b16 |
fix(juggler): don't dispatch mouse events on the chrome/content boundary row (#751, #752)
`sendOne()` converts a relative coordinate to an absolute one as
`eventY + boundingBox.top`, so a relative y of 0 dispatches at absolute
y == boundingBox.top exactly -- the content area's first row. The chrome
above the content is a fractional number of CSS pixels tall, and the
widget rounds the coordinate to a whole device row before hit-testing it.
Wherever round(top) < top that rounded row still belongs to chrome, so
the event fires as an exit event rather than eMouseMove, no
juggler-mouse-event-hit-renderer ack is produced, and -- because dispatch
is serialized on activateAndRun()'s process-global chain -- that one
missing ack wedges every later input event in the process forever. Same
deadlock as the far-edge (#225) and no-op-move cases, from the near edge.
The chrome height is a deterministic function of the spoofed OS, so this
is not flaky, it is per-fingerprint. Measured on v152.0.4-beta.30,
headless Linux, `page.mouse.move(31, 0)` from an interior point:
os boundingBox.top rounds to result
windows 51.4 51 (above) hangs, 5/5
macos 53.1 53 (above) hangs, 5/5
linux 56.5 57 (below) completes, 8/8
Relative x == 0 is unaffected because boundingBox.left is a whole 0.
The far edges were fixed by treating them as out-of-viewport; 0 cannot
be. It is a legitimate in-viewport coordinate, and the out-of-viewport
branch returns silently for mousedown/mouseup -- so widening the bounds
check would convert the hang into a click that reports success and fires
nothing, which is what #752 describes. Snap the dispatched coordinate to
the first whole pixel inside the browser element instead: it stays within
content pixel 0 and lands clear of the boundary.
humanize is what makes this common in the field rather than the cause of
it. Every PageHandler starts at _lastTrackedPos = {x: 0, y: 0}, so a
session's first humanized move always departs from the top-left corner
and the curve rides the y==0 row. On a stock build a first humanized
click hung on 5 of 20 cold pages; all five had dispatched a point at
y==0, and the 15 that completed had dispatched none. With the fix, 0 of
20 hung and all 8 y==0 points dispatched across those runs were acked.
Add tests/patches/near-edge-mouse-deadlock.py, which covers all three
spoofed OSes so it does not depend on which offset a given host's chrome
produces, and both the direct and humanized dispatch paths. It fails on
stock at the first move and passes with the fix. humanize-edge-deadlock,
humanize-mouse-trajectory, noop-mousemove-deadlock and trusted-events all
still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQgHHGRXNp29jr4xQjK7iv
(cherry picked from commit
|
||
|
|
97fcc48171 |
fix(fonts): exempt chrome docs from system-ui spoof
Refs #695
(cherry picked from commit
|
||
|
|
55b056f873 |
fix(fonts): exempt chrome docs from CSS2 system-font spoof
Chrome CSS is `font: message-box`, so the browser UI was handed "Segoe UI"
on Linux and "-apple-system" on Windows -- families that aren't installed,
leaving the toolbar in the default serif font.
The GTK and cocoa LookAndFeel hooks are dropped: with the ComputeSystemFont
guard they are only reachable when navigator.platform is unspoofed (no-op)
or the document is chrome (harmful).
Refs #695
(cherry picked from commit
|
||
|
|
70a22d8120 |
fix(fonts): apply font allowlist at lookup time, not via whitelist pref
font.system.whitelist makes ApplyWhitelist() delete non-listed families
from the process-wide font list, and with the shared font list that
pruning happens in the parent process -- the one that paints the browser
chrome. On Windows the titlebar buttons lost Segoe Fluent Icons and drew
tofu boxes for U+E921/E922/E8BB.
Filter on FontVisibilityProvider::IsChrome() instead, in the three
content-reachable paths: FindAndAddFamiliesLocked, GlobalFontFallback and
gfxUserFontSet's src: local().
Fixes #695
(cherry picked from commit
|
||
|
|
0969a94db9 |
camoucfg: add cached font allowlist helpers
The gfx font lookup paths need to consult the spoofed "fonts" list on
every family resolution, so it cannot be re-parsed from CAMOU_CONFIG per
call.
(cherry picked from commit
|
||
|
|
03ac03c35a |
Add media:spoof_codecs config option to prevent codec fingerprinting
canPlayType() and isTypeSupported() leak which system codec libraries
(FFmpeg, VideoToolbox, GStreamer) are installed by checking
PDMFactory::Supports(). This patch bypasses those checks in
MP4Decoder::IsSupportedType() and MatroskaDecoder::IsSupportedType()
when the media:spoof_codecs config key is set.
Gated behind MaskConfig::GetBool("media:spoof_codecs").value_or(false)
so default behaviour is unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
53618449d2 |
Spoof navigator.maxTouchPoints from config
navigator.maxTouchPoints was already a recognized config property
(settings/properties.json declares it) but nothing read it, so it always
reported 0 on a desktop build regardless of the config. Under a
Firefox-for-Android user agent that 0 is a mobile-detection tell: a real
phone reports 5, and Firefox's RFP path only ever collapses the value to 0
(MaxTouchPointsCollapse) — there is no path to a phone value without an
explicit override.
Add the standard MaskConfig early-return at the top of
Navigator::MaxTouchPoints, matching how other navigator/screen properties
are spoofed. Navigator.cpp already includes MaskConfig.hpp via
navigator-spoofing.patch, so no extra include or schema change is needed.
Build-verified: applies cleanly against firefox-152.0.4 and compiles for
linux/arm64. Tested on the built binary — config value 5 -> 5, 1 -> 1,
no config -> 0 (unchanged desktop default).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
fff2c730be |
fix(pythonlib): derive navigator.appVersion from the preset's user agent
from_preset() set userAgent, platform and oscpu from the captured device
but never appVersion. Firefox reports appVersion as "5.0 (<OS tokens>)",
so leaving it unset let the host's own value through — and a page reading
two properties saw them disagree.
Measured on 152.0.4-beta.29, macOS host, os="linux", fingerprint_preset:
navigator.platform Linux x86_64
navigator.appVersion 5.0 (Macintosh) <- the host
The value is derived from the user agent rather than from the platform,
because 20 of the 65 bundled Linux presets carry a distro token
("X11; Ubuntu") that a platform lookup would flatten to "X11" — a smaller
mismatch than the host leaking, but the same kind. Firefox builds
appVersion from the same OS tokens as the UA, minus the architecture and
the Gecko revision, with Windows collapsed to its family name; checked
against 800 browserforge fingerprints, the derivation is exact on every
one, including Android and the Ubuntu variant.
A preset that ships its own appVersion keeps it, and a user agent the
rule cannot parse leaves the key unset rather than inventing a value.
(cherry picked from commit
|
||
|
|
1ffa32dfce |
feat(windows): add run_tests.ps1 and fix Windows compat in service-tester
- Add service-tester/run_tests.ps1: PowerShell equivalent of run_tests.sh,
mirrors all three binary modes (local | fetched | both), auto-detects
Windows build at obj-*-windows-msvc/dist/bin/camoufox.exe, handles
Windows venv paths (.venv\Scripts\python.exe), and sets UTF-8 console
encoding so Unicode box-drawing characters render correctly
- Fix service-tester/_bundle.py: use esbuild.cmd on Windows instead of
the Unix shell script esbuild (WinError 193 otherwise)
- Fix service-tester/run_tests.py: reconfigure stdout/stderr to UTF-8 on
win32 to prevent UnicodeEncodeError from box-drawing characters in
console output (CP1252 default does not support U+2500)
Tested on Windows 11 (build 26200) — all three -Binary scenarios
(local/fetched/both) behave correctly at the script level. Browser launch
is currently blocked by a missing mozglue.manifest in the Windows zip
package (SxS resolution fails on clean installs); documented in PR.
(cherry picked from commit
|
||
|
|
9519d2dba3 |
wip: add Windows service test script (run_tests.ps1)
(cherry picked from commit
|
||
|
|
1934b3532d |
Enable TLS verification for public IP lookups
public_ip() called requests.get with verify=False and wrapped it in a
context manager that silenced urllib3's InsecureRequestWarning, so the
disabled verification produced no output either.
These requests are routed through the user's proxy, which is the exact
position an attacker occupies. A forged response controls the value
public_ip() returns, and that value is used to spoof the WebRTC IP --
so the leak the function exists to prevent becomes attacker-selectable.
validate_ip() bounds this to a well-formed address, but the address is
still theirs to choose.
Set verify=True and drop the warning suppression. requests raises
SSLError, a subclass of RequestException, which the existing loop
already catches -- a host with a bad certificate is now skipped in
favour of the next one in URLS instead of being trusted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
b2d842177a |
Verify sha256 of downloaded release assets before extracting
check_asset() already reads the asset's digest from the GitHub API and
stores it as installed_sha256, and AvailableVersion carries a sha256
field through to version.json. Nothing compared either against the
bytes that were downloaded: every sha256 equality check in the package
compares metadata to metadata when selecting an installed version, and
hashlib appeared only in utils.py to key a config cache.
So the archive that gets extracted over the install directory, and then
chmod 755'd and executed, was accepted on transport security alone. The
digest needed to catch a substituted or truncated asset was already in
hand and unused.
Add verify_sha256() and call it between download and extraction on both
install paths -- install_versioned() for the CLI and InstallWorker for
the GUI. It hashes in 1 MiB blocks so a multi-hundred-megabyte asset
does not have to be held in memory, and rewinds the buffer afterwards
so unzip() still reads from the start.
When no digest is published the install proceeds with a warning rather
than failing: some sources publish no digest, and refusing to install
from them would be a regression, not a fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
6b8b08646d |
fix(addons): re-download addons with a missing manifest
maybe_download_addons() treated an addon as already downloaded whenever its
directory existed. A download that fails partway leaves an empty directory
behind, which is then trusted on every later launch, so confirm_paths()
raises InvalidAddonPath: manifest.json is missing and never recovers. Gate
the check on manifest.json presence and rmtree the partial directory on
failure. Closes #308.
(cherry picked from commit
|
||
|
|
e36e0fe3e1 |
fix: Return None if headless
(cherry picked from commit
|
||
|
|
e41e4d6c80 |
Merge pull request #750 from JWriter20/fix/seal-fingerprint-setters
fix(fingerprint): seal the window.setXxx() setters before page script (#749) |
||
|
|
d75ebdf41a | README: Update table | ||
|
|
7911f4a977 |
fix(fingerprint): seal the window.setXxx() setters before page script (#749)
Camoufox applies a per-context fingerprint through fifteen helpers on window
-- setNavigatorPlatform(), setTimezone(), setWebGLRenderer() and the rest.
Each removed itself from window when called, which only ever covered the
setters a given fingerprint happened to set:
* a value the config left alone never called its setter, so setTimezone (no
timezone configured) and setWebRTCIPv6 (never emitted by any code path)
stayed on window for every context;
* a launch that registers no init script at all left all fifteen. That is
Camoufox() followed by browser.new_page(), the documented default, where
the fingerprint comes from CAMOU_CONFIG in C++ and nothing ever touches a
setter;
* so did launching the binary as a plain browser, with no juggler running.
Page script could not only see them but call them: window.setNavigator-
HardwareConcurrency(999) from a page moved navigator.hardwareConcurrency to
999. Fifteen window properties no other Firefox build has is a sharper
fingerprint than any of the values they were hiding.
A window is now created sealed and juggler opens it for exactly as long as
init scripts are running:
window created -> sealed -> unseal -> init scripts apply the fingerprint
-> seal -> page script runs, setters gone
Sealed by default so the failure is closed: a path that does not go through
juggler never offers the setters, rather than offering them permanently.
Per *window*, not per browsing context. A context-wide flag -- which is what
the existing self-destruct effectively was -- fires on the initial about:blank
and leaves every later navigation unable to apply anything, so both contexts
come up with the launch-level fingerprint instead of their own. The regression
test covers that case.
Deleting the names is not on its own enough: they are handed out lazily by the
DOM resolve hook, so a deleted name comes straight back. The Func= guards
consult the seal, which is what makes the deletion stick; the delete clears
what the init script's `typeof w.setX === "function"` probe already
materialised.
build-tester scored this A throughout because its checklist omitted
setNavigatorUserAgent and setWebRTCIPv6 -- the two that leaked on every
context -- while asserting the absence of setCanvasSeed, which has never
existed anywhere in the tree and so passed vacuously.
Measured on Linux x86_64, beta.30 vs the shipped beta.29:
path before after
Camoufox() + new_context() 15 0
context with an init script 2 0
page.add_init_script() after new_page 15 0
binary launched with no juggler 15 0
Fingerprints still apply as requested in every case. tests/patches 7/7,
build-tester 962/962.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018i4WFSwSGLNH2d7wVVKFjQ
|
||
|
|
e66c69a918 |
Merge pull request #748 from JWriter20/release/pythonlib-0.5.6b1
chore(release): mark pythonlib 0.5.6 as a pre-release (0.5.6b1) |
||
|
|
8cb7914328 |
feat(python): warn when a supplied binary predates the Playwright in use
A managed install below the version floor is upgraded by pkgman, but
executable_path deliberately bypasses that -- the caller supplied the binary,
so we neither replace it nor download another. That left one pairing nothing
checked: an old build driven by Playwright >= 1.61, which sends viewport fields
the older Juggler schema rejects. The user saw a bare
Protocol error (Browser.setDefaultViewport)
with nothing naming the cause.
Warn rather than raise, because the pairing is not always fatal. Camoufox
defaults to no_viewport when it spoofs window dimensions (sync_api), and
Playwright then never sends Browser.setDefaultViewport -- so the default path
works fine on an old build. Measured against a real beta.29 binary on
Playwright 1.62:
default path WORKS
new_context(viewport=...) BREAKS
new_context(no_viewport=False) BREAKS
new_context(viewport=..., is_mobile=False) BREAKS
Refusing to launch would break the setups in the first row. A build with no
version.json beside it -- an unpackaged objdir build -- tells us nothing, so it
is left alone rather than nagged about.
Verified end to end: warns on the real beta.29 build under Playwright 1.62,
silent on beta.30.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
b68a4fb940 |
chore(release): mark pythonlib 0.5.6 as a pre-release (0.5.6b1)
Pairs the library with the browser: v152.0.4-beta.30 is published as a GitHub
pre-release, so the library that requires it should be one too.
PEP 440 puts 0.5.6b1 after 0.5.5 and before 0.5.6, and pip skips pre-releases
by default -- so `pip install camoufox` still resolves 0.5.5, and only
`pip install --pre camoufox` or an explicit pin picks this up. That is what
makes the conditional browser floor safe to exercise in the wild: the users who
opt in are the ones who get moved to beta.30.
Verified against the live release: with Playwright 1.61 installed the effective
floor resolves to beta.30 and the fetcher selects the real published asset
(camoufox-152.0.4-beta.30-lin.x86_64.zip). 172 tests pass, the 3.8 vermin gate
holds, and the package builds as camoufox-0.5.6b1.
Known wrinkle, not introduced here: _parse_semver() does int("6b1"), fails, and
substitutes 0, so 0.5.6b1 parses to (0, 5, 0). The browser constraint still
resolves correctly because repos.yml's only entry is min 0.5.0 / max 1, but a
future entry gating on a patch version would silently miss a pre-release
install. Worth making that parser PEP 440-aware separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5d06ec1629 |
Merge pull request #747 from JWriter20/fix/rust-host-triplet-rustc-198
fix(build): resolve the rust host triplet under rustc 1.98v152.0.4-beta.30 |
||
|
|
cb0d7a6893 |
fix(build): resolve the rust host triplet under rustc 1.98
The v152.0.4-beta.30 tag build died in configure on every target, before
compiling a file:
checking for rust host triplet...
ERROR: Don't know how to translate x86_64-pc-linux-gnu for rustc
Nothing in the release caused it -- beta.29..beta.30 touches no build-system
file at all. The GitHub runner image moved from rustc 1.97.1 to 1.98.0, and
1.98 added five OpenEmbedded targets, one being x86_64-oe-linux-gnu. That is
enough to break the host lookup in build/moz.configure/rust.configure, whose
narrowing steps only return when they reduce to exactly one candidate:
1.97.1 raw_os "linux-gnu" -> [x86_64-unknown-linux-gnu] -> resolved
1.98.0 raw_os "linux-gnu" -> [x86_64-oe-linux-gnu, x86_64-unknown-...] -> 2, falls through
exact alias -> no rust target is spelled x86_64-pc-linux-gnu
vendor == "pc" -> 0 (candidates are "oe" and "unknown")
gentoo alias -> 0
-> None -> die
It takes out every target, since all of them cross-compile from the Linux
runner and it is the HOST that fails to resolve.
Add a final fallback preferring the vendor-neutral "unknown" spelling. Rust
writes its canonical Linux targets that way and the distro-flavoured ones
("gentoo", "oe") are the exceptions, so when an autoconf vendor like "pc"
matches nothing, the canonical target is the right answer.
Verified by running ./mach configure against the real 152.0.4 tree:
rustc 1.98.0, without this patch -> ERROR, exit 1 (reproduces CI exactly)
rustc 1.98.0, with this patch -> x86_64-unknown-linux-gnu, exit 0
rustc 1.96.1, with this patch -> x86_64-unknown-linux-gnu, exit 0
Ported from JWriter20/camoufox ef5f54d, which carries the same fix under that
fork's restructured layout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
de38301af1 |
Merge pull request #746 from JWriter20/release/v152.0.4-beta.30
release: v152.0.4-beta.30 + pythonlib 0.5.6, with a Playwright-keyed browser floor |
||
|
|
fc3392e427 |
fix(python): resolve the bundle from executable_path, not the managed install
get_env_vars() and _generate_fontconfig() read the bundled fontconfig and fonts through get_path(), i.e. the managed install, even when the caller supplied their own binary. _load_properties() already honours executable_path for properties.json; these two did not. Before the floor could reject anything this silently mixed one build's fonts into another build's launch. Once the floor is live it becomes fatal: every launch raises UnsupportedVersion while the caller is holding a perfectly good binary, because resolving the bundle drags in the managed install and that is what gets version-checked. Thread executable_path through both, matching _load_properties. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b1fe7227fa |
fix(python): key the browser floor on Playwright instead of a flat minimum
The incompatibility is two-dimensional -- it needs both a Playwright >= 1.61
and a browser < beta.30 -- but MIN_VERSION only knows about the browser. To
stay safe a flat floor has to assume the worst Playwright, which means:
* every 0.5.6 user re-downloads the browser, including the majority on
<1.61 who are in no danger;
* installs pinned to an older build lose the pin, and prerelease/alpha users
are moved off their channel, since every alpha sorts below beta.30;
* the library cannot run at all until the matching browser release is
published, making the PyPI-after-release ordering load-bearing.
Key it on the resolved Playwright instead. Measured: 1.60 works on beta.29 and
beta.30; 1.61 and 1.62 fail on beta.29 and pass on beta.30.
playwright <1.61 -> floor alpha.1 -> every install kept
playwright >=1.61 -> floor beta.30 -> below-beta.30 installs upgraded
version unreadable -> floor alpha.1 -> kept; a spurious forced re-download is
worse than leaving a working install
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
df35ae79d2 |
chore(release): v152.0.4-beta.30, pythonlib 0.5.6
Raises the browser floor to beta.30 because 0.5.6 permits Playwright >= 1.61, which sends viewport isMobile/screenSize in Browser.setDefaultViewport. Only beta.30's Protocol.js schema accepts those; on beta.29 every new_context() fails with "Protocol error (Browser.setDefaultViewport)". Measured: 1.60 works on both builds, 1.61 and 1.62 fail on beta.29 and pass on beta.30. The floor means pythonlib 0.5.6 cannot run until the beta.30 release assets are published -- it must not reach PyPI first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ce87cf7dab |
fix(python): report an unsatisfiable version floor instead of recursing
camoufox_path() ended in `return camoufox_path()` after a fetch. When the
newest published build is still below CONSTRAINTS.MIN_VERSION, install() is a
no-op ("already installed") and that tail recursed ~1000 times -- each
iteration firing another GitHub API call, which exhausts the unauthenticated
rate limit (60/hr) long before the RecursionError lands.
That is precisely the state a library published ahead of its browser release
puts every user in, and it is reachable now that the floor is raised. It also
hits permanently for anyone using a repos.yml source that does not carry the
required build.
Re-check after the fetch instead, and raise UnsupportedVersion naming the
required minimum.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
da67775257 |
fix(python): reach the fetch path when the installed build is below the floor
Raising CONSTRAINTS.MIN_VERSION is how this library has always forced a browser upgrade (beta.12 -> beta.15 -> beta.17 -> beta.18 -> beta.19); the floor only became 'alpha.1' incidentally, in an unrelated PR. That left the branch dead, and it had rotted: camoufox_path() probed INSTALL_DIR/version.json, which only the pre-multiversion flat layout ever wrote. With a versioned install below the floor it raised FileNotFoundError instead of falling through to a fetch, so raising the floor would have crashed every existing user rather than upgrading them. Treat a missing root version.json as "no legacy install here" so the caller falls through to CamoufoxFetcher().install() as intended. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
850a5751e6 |
Merge pull request #745 from JWriter20/fix/738-main-world-init-scripts
fix(juggler): let init scripts reach the page world with "mw:" (#738) |
||
|
|
dc201ddee8 |
test(playwright): run the vendored suite in the page's own world
The suite in tests/async is upstream Playwright's conformance suite, so it asserts upstream semantics: tests read globals their own page scripts defined and pass element handles into evaluate(). Under Camoufox's isolated world about 59 of them fail on "X is not defined" for a global the page really did set. The "mw:" prefix cannot stand in -- it refuses handles by design (Runtime.js) and much of this suite needs them -- so this adds a disableWorldIsolation config key that makes the default world the page's own, and turns it on for this suite only. The flag gives up the property this fork exists for: automation JS becomes visible to the page again. It is a conformance-suite mode, not a scraping mode. Camoufox's isolation keeps its own coverage in tests/patches/isolated-evaluate.py, which must go on passing without the flag. Measured on beta.30 with Playwright 1.62: 73 failed/1023 passed -> 14 failed/1082 passed, with no test failing that was not already failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |