mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-05 08:00:21 +00:00
fix: stock animations and speech by default; drop config keys that freeze live values
Three behaviours a page could detect, changed in one breaking release:
- **Animations run on stock timing.** no-css-animations.patch finished every
finite animation at once by default, and any page could read it:
`el.animate(frames, 1000).effect.getComputedTiming().duration` was 0, and a
500ms transition reported 0. Measured on v152.0.4-beta.31. The speedup is
now an opt-in, `instantAnimations: True`, which raises a LeakWarning.
disableInstantAnimations is gone.
- **speak() on a spoofed voice works like a real voice.** It fired `error`
after 3ms unless voices:fakeCompletion was set, and then start and end in
the same tick. It now starts and ends after the text's duration at ~150
words per minute. Both voices:fakeCompletion keys are gone, and so is a
debug line printed to stderr on every call.
- **Keys removed:**
- battery:* and window.scrollMinX/Y: Firefox keeps getBattery() and
scrollMin* chrome-only, so no page could read them.
- window.scrollMaxX/Y, screen.pageXOffset/pageYOffset,
window.history.length and document.body.client*: each pinned a live value
to a constant, so scrolling, navigating or re-laying out never changed it.
fpgen.yml mapped pageYOffset, so about 15% of identities froze
window.scrollY at a non-zero value.
- The body keys' role as an undocumented alias for window.innerWidth/Height
in browser-init and in the launcher.
- MaskConfig::GetInt32Rect, which only the body keys used.
New guards, both of which fail on v152.0.4-beta.31:
tests/patches/animation-timing.py and tests/patches/spoofed-voice-speaks.py.
The decisions are recorded as animations-run-on-stock-timing and
spoofed-voices-speak. Every patch applies cleanly to a fresh tree, and the
result builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
676fb3f7c1
commit
fb21b2e35a
@@ -359,7 +359,7 @@ Camoufox is a Firefox fork engineered for web scraping and AI agents. It is head
|
||||
* **Optimized for automation**
|
||||
* Human-like mouse movement 🖱️
|
||||
* Blocks & circumvents ads 🛡️
|
||||
* No CSS animations 💨
|
||||
* Optional instant animations (`instantAnimations`), so Playwright never waits on one 💨
|
||||
|
||||
- Debloated & optimized for memory efficiency ⚡
|
||||
- [PyPI package](https://pypi.org/project/camoufox/) for updates & auto fingerprint injection 📦
|
||||
@@ -427,7 +427,6 @@ Below is a list of patches and features implemented in Camoufox.
|
||||
- Network headers (Accept-Languages and User-Agent) are spoofed to match the navigator properties
|
||||
- WebRTC IP spoofing at the protocol level
|
||||
- Geolocation, timezone, and locale spoofing
|
||||
- Battery API spoofing
|
||||
- etc.
|
||||
|
||||
### Stealth patches
|
||||
@@ -460,7 +459,7 @@ Below is a list of patches and features implemented in Camoufox.
|
||||
- Patches from LibreWolf & Ghostery to help remove telemetry & bloat
|
||||
- Debloat config from PeskyFox, LibreWolf, and others
|
||||
- Speed & network optimizations from FastFox
|
||||
- Removed all CSS animations
|
||||
- Animations run on stock timing; `instantAnimations: True` finishes them at once, at the cost of being detectable
|
||||
- Minimalistic theming
|
||||
- etc.
|
||||
|
||||
|
||||
@@ -212,18 +212,6 @@ inline std::optional<std::array<uint32_t, 4>> GetRect(
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::optional<std::array<int32_t, 4>> GetInt32Rect(
|
||||
const std::string& left, const std::string& top, const std::string& width,
|
||||
const std::string& height) {
|
||||
if (auto optValue = GetRect(left, top, width, height)) {
|
||||
std::array<int32_t, 4> result;
|
||||
std::transform(optValue->begin(), optValue->end(), result.begin(),
|
||||
[](const auto& val) { return static_cast<int32_t>(val); });
|
||||
return result;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Helpers for WebGL
|
||||
|
||||
inline std::optional<nlohmann::json> GetNested(const std::string& domain,
|
||||
|
||||
@@ -159,6 +159,32 @@ rules:
|
||||
# Measurement
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
- id: animations-run-on-stock-timing
|
||||
title: Animations run on stock timing unless the caller opts into instantAnimations
|
||||
check: automated
|
||||
evidence:
|
||||
- "measured on v152.0.4-beta.31: el.animate(frames, 1000).effect.getComputedTiming().duration returned 0, a 500ms transition 0"
|
||||
- "the finished promise still resolved after ~1000ms, so the only saving was Playwright's stability wait"
|
||||
rationale: >-
|
||||
Instant animations were the default so Playwright never waited on one,
|
||||
but any page reads the zero duration back in one line. Reporting the
|
||||
real duration while not rendering the animation does not hide it
|
||||
either: getComputedStyle or getBoundingClientRect sampled mid-animation
|
||||
returns the end state. So the saving costs stealth, and it is the
|
||||
caller's choice: `instantAnimations: True` turns it on and warns.
|
||||
|
||||
- id: spoofed-voices-speak
|
||||
title: speak() on a spoofed voice starts, then ends after the text's duration
|
||||
check: automated
|
||||
evidence:
|
||||
- "measured on v152.0.4-beta.31: speak() on a spoofed voice fired `error` after 3ms"
|
||||
- "voices:fakeCompletion, the opt-out, fired start and end in the same tick"
|
||||
rationale: >-
|
||||
A spoofed voice has no engine behind it, and a real voice never errors
|
||||
on a plain utterance or ends the instant it starts. So the browser
|
||||
speaks it silently for as long as the text takes at ~150 words per
|
||||
minute. The two config keys that chose between the two tells are gone.
|
||||
|
||||
- id: no-glyph-spacing-noise
|
||||
title: Text is shaped exactly as stock Firefox shapes it; there is no spacing seed
|
||||
check: automated
|
||||
|
||||
@@ -542,6 +542,19 @@ def test_a_sandbox_held_over_a_page_window_is_nuked_not_just_dropped():
|
||||
)
|
||||
|
||||
|
||||
def test_instant_animations_are_an_opt_in():
|
||||
patch = (REPO_ROOT / "patches" / "no-css-animations.patch").read_text(encoding="utf-8")
|
||||
assert 'MaskConfig::GetBool("instantAnimations")' in patch, explain("animations-run-on-stock-timing")
|
||||
assert "disableInstantAnimations" not in patch, explain("animations-run-on-stock-timing")
|
||||
|
||||
|
||||
def test_spoofed_voices_complete_without_a_config_switch():
|
||||
patch = (REPO_ROOT / "patches" / "voice-spoofing.patch").read_text(encoding="utf-8")
|
||||
assert "fakeCompletion" not in patch and "DispatchError(0, 0)" not in patch, (
|
||||
explain("spoofed-voices-speak")
|
||||
)
|
||||
|
||||
|
||||
def test_no_glyph_spacing_seed_anywhere():
|
||||
"""No config key, no setter, no shaper hook."""
|
||||
declared = {
|
||||
|
||||
+11
-11
@@ -1,10 +1,10 @@
|
||||
diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js
|
||||
index 1cfa4b8497..bdf8d7202e 100644
|
||||
index 0aa4baea8e..3c964b7ad5 100644
|
||||
--- a/browser/base/content/browser-init.js
|
||||
+++ b/browser/base/content/browser-init.js
|
||||
@@ -3,6 +3,16 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
@@ -44,6 +44,16 @@ var gSerialDeviceObserver = {
|
||||
},
|
||||
};
|
||||
|
||||
+const { AddonManager } = ChromeUtils.importESModule(
|
||||
+ "resource://gre/modules/AddonManager.sys.mjs",
|
||||
@@ -19,7 +19,7 @@ index 1cfa4b8497..bdf8d7202e 100644
|
||||
let _resolveDelayedStartup;
|
||||
var delayedStartupPromise = new Promise(resolve => {
|
||||
_resolveDelayedStartup = resolve;
|
||||
@@ -72,7 +82,7 @@ var gBrowserInit = {
|
||||
@@ -157,7 +167,7 @@ var gBrowserInit = {
|
||||
updateBookmarkToolbarVisibility();
|
||||
|
||||
// Set a sane starting width/height for all resolutions on new profiles.
|
||||
@@ -28,7 +28,7 @@ index 1cfa4b8497..bdf8d7202e 100644
|
||||
// When the fingerprinting resistance is enabled, making sure that we don't
|
||||
// have a maximum window to interfere with generating rounded window dimensions.
|
||||
document.documentElement.setAttribute("sizemode", "normal");
|
||||
@@ -310,6 +320,22 @@ var gBrowserInit = {
|
||||
@@ -385,6 +395,22 @@ var gBrowserInit = {
|
||||
// Update UI if browser is under remote control.
|
||||
gRemoteControl.updateVisualCue();
|
||||
|
||||
@@ -51,7 +51,7 @@ index 1cfa4b8497..bdf8d7202e 100644
|
||||
// If we are given a tab to swap in, take care of it before first paint to
|
||||
// avoid an about:blank flash.
|
||||
let tabToAdopt = this.getTabToAdopt();
|
||||
@@ -345,6 +371,33 @@ var gBrowserInit = {
|
||||
@@ -448,6 +474,33 @@ var gBrowserInit = {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,8 @@ index 1cfa4b8497..bdf8d7202e 100644
|
||||
// Wait until chrome is painted before executing code not critical to making the window visible
|
||||
this._boundDelayedStartup = this._delayedStartup.bind(this);
|
||||
window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
|
||||
@@ -366,9 +417,177 @@ var gBrowserInit = {
|
||||
)?.removeAttribute("key");
|
||||
@@ -475,9 +528,177 @@ var gBrowserInit = {
|
||||
}
|
||||
}
|
||||
|
||||
+ // Set default size
|
||||
@@ -132,8 +132,8 @@ index 1cfa4b8497..bdf8d7202e 100644
|
||||
+ browser.style.setProperty('box-sizing', 'content-box');
|
||||
+
|
||||
+ // Hijack the inner window size
|
||||
+ let innerWidth = ChromeUtils.camouGetInt("window.innerWidth") || ChromeUtils.camouGetInt("document.body.clientWidth");
|
||||
+ let innerHeight = ChromeUtils.camouGetInt("window.innerHeight") || ChromeUtils.camouGetInt("document.body.clientHeight");
|
||||
+ let innerWidth = ChromeUtils.camouGetInt("window.innerWidth");
|
||||
+ let innerHeight = ChromeUtils.camouGetInt("window.innerHeight");
|
||||
+
|
||||
+ if (innerWidth || innerHeight) {
|
||||
+ let win_inner_style = document.createElement('style');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/browser/app/moz.build b/browser/app/moz.build
|
||||
index a23bb7812a..58942ccb13 100644
|
||||
index 28a9650680..9251ac4547 100644
|
||||
--- a/browser/app/moz.build
|
||||
+++ b/browser/app/moz.build
|
||||
@@ -182,6 +182,9 @@ for icon in ("firefox", "document", "newwindow", "newtab", "pbmode", "document_p
|
||||
@@ -186,6 +186,9 @@ for icon in ("firefox", "document", "newwindow", "newtab", "pbmode", "document_p
|
||||
icon,
|
||||
)
|
||||
|
||||
@@ -12,43 +12,11 @@ index a23bb7812a..58942ccb13 100644
|
||||
if CONFIG["MOZ_ASAN"] or CONFIG["MOZ_DEBUG"]:
|
||||
WINCONSOLE = True
|
||||
else:
|
||||
diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp
|
||||
index 101590d8c8..f004930b20 100644
|
||||
--- a/dom/base/Element.cpp
|
||||
+++ b/dom/base/Element.cpp
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
#include "mozilla/dom/Element.h"
|
||||
|
||||
+#include "MaskConfig.hpp"
|
||||
+
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -1006,6 +1008,18 @@ nsRect Element::GetClientAreaRect() {
|
||||
Document* doc = OwnerDoc();
|
||||
nsPresContext* presContext = doc->GetPresContext();
|
||||
|
||||
+ if (doc->GetBodyElement() == this) {
|
||||
+ if (auto conf = MaskConfig::GetInt32Rect(
|
||||
+ "document.body.clientLeft", "document.body.clientTop",
|
||||
+ "document.body.clientWidth", "document.body.clientHeight")) {
|
||||
+ if (conf.has_value()) {
|
||||
+ auto values = conf.value();
|
||||
+ return nsRect(values[0] * 60, values[1] * 60, values[2] * 60,
|
||||
+ values[3] * 60);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
// We can avoid a layout flush if this is the scrolling element of the
|
||||
// document, we have overlay scrollbars, and we aren't embedded in another
|
||||
// document
|
||||
diff --git a/dom/base/moz.build b/dom/base/moz.build
|
||||
index cd9090cda3..37a053579e 100644
|
||||
index e25ac44451..13839534ad 100644
|
||||
--- a/dom/base/moz.build
|
||||
+++ b/dom/base/moz.build
|
||||
@@ -634,3 +634,6 @@ GeneratedFile(
|
||||
@@ -647,3 +647,6 @@ GeneratedFile(
|
||||
"/servo/components/style/properties/counted_unknown_properties.py",
|
||||
],
|
||||
)
|
||||
@@ -57,10 +25,10 @@ index cd9090cda3..37a053579e 100644
|
||||
+LOCAL_INCLUDES += ["/camoucfg"]
|
||||
\ No newline at end of file
|
||||
diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp
|
||||
index 330a707789..241f87780b 100644
|
||||
index 4bc6688f9a..693779ca08 100644
|
||||
--- a/dom/base/nsGlobalWindowInner.cpp
|
||||
+++ b/dom/base/nsGlobalWindowInner.cpp
|
||||
@@ -5,6 +5,7 @@
|
||||
@@ -3,6 +3,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsGlobalWindowInner.h"
|
||||
@@ -68,7 +36,7 @@ index 330a707789..241f87780b 100644
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
@@ -3439,6 +3440,8 @@ void nsGlobalWindowInner::SetName(const nsAString& aName,
|
||||
@@ -3704,6 +3705,8 @@ void nsGlobalWindowInner::SetName(const nsAString& aName,
|
||||
}
|
||||
|
||||
double nsGlobalWindowInner::GetInnerWidth(ErrorResult& aError) {
|
||||
@@ -77,7 +45,7 @@ index 330a707789..241f87780b 100644
|
||||
FORWARD_TO_OUTER_OR_THROW(GetInnerWidthOuter, (aError), aError, 0);
|
||||
}
|
||||
|
||||
@@ -3450,6 +3453,8 @@ nsresult nsGlobalWindowInner::GetInnerWidth(double* aWidth) {
|
||||
@@ -3715,6 +3718,8 @@ nsresult nsGlobalWindowInner::GetInnerWidth(double* aWidth) {
|
||||
}
|
||||
|
||||
double nsGlobalWindowInner::GetInnerHeight(ErrorResult& aError) {
|
||||
@@ -86,7 +54,7 @@ index 330a707789..241f87780b 100644
|
||||
// We ignore aCallerType; we only have that argument because some other things
|
||||
// called by GetReplaceableWindowCoord need it. If this ever changes, fix
|
||||
// nsresult nsGlobalWindowInner::GetInnerHeight(double* aInnerWidth)
|
||||
@@ -3466,12 +3471,18 @@ nsresult nsGlobalWindowInner::GetInnerHeight(double* aHeight) {
|
||||
@@ -3731,6 +3736,12 @@ nsresult nsGlobalWindowInner::GetInnerHeight(double* aHeight) {
|
||||
|
||||
int32_t nsGlobalWindowInner::GetOuterWidth(CallerType aCallerType,
|
||||
ErrorResult& aError) {
|
||||
@@ -99,13 +67,7 @@ index 330a707789..241f87780b 100644
|
||||
FORWARD_TO_OUTER_OR_THROW(GetOuterWidthOuter, (aCallerType, aError), aError,
|
||||
0);
|
||||
}
|
||||
|
||||
int32_t nsGlobalWindowInner::GetOuterHeight(CallerType aCallerType,
|
||||
ErrorResult& aError) {
|
||||
FORWARD_TO_OUTER_OR_THROW(GetOuterHeightOuter, (aCallerType, aError), aError,
|
||||
0);
|
||||
}
|
||||
@@ -3486,11 +3497,13 @@ double nsGlobalWindowInner::ScreenEdgeSlopY() const {
|
||||
@@ -3751,11 +3762,13 @@ double nsGlobalWindowInner::ScreenEdgeSlopY() const {
|
||||
|
||||
int32_t nsGlobalWindowInner::GetScreenX(CallerType aCallerType,
|
||||
ErrorResult& aError) {
|
||||
@@ -119,7 +81,7 @@ index 330a707789..241f87780b 100644
|
||||
FORWARD_TO_OUTER_OR_THROW(GetScreenYOuter, (aCallerType, aError), aError, 0);
|
||||
}
|
||||
|
||||
@@ -3524,6 +3537,8 @@ static nsPresContext* GetPresContextForRatio(Document* aDoc) {
|
||||
@@ -3789,6 +3802,8 @@ static nsPresContext* GetPresContextForRatio(Document* aDoc) {
|
||||
double nsGlobalWindowInner::GetDevicePixelRatio(CallerType aCallerType,
|
||||
ErrorResult& aError) {
|
||||
ENSURE_ACTIVE_DOCUMENT(aError, 0.0);
|
||||
@@ -128,80 +90,20 @@ index 330a707789..241f87780b 100644
|
||||
|
||||
RefPtr<nsPresContext> presContext = GetPresContextForRatio(mDoc);
|
||||
if (NS_WARN_IF(!presContext)) {
|
||||
@@ -3594,26 +3609,38 @@ already_AddRefed<MediaQueryList> nsGlobalWindowInner::MatchMedia(
|
||||
}
|
||||
|
||||
int32_t nsGlobalWindowInner::GetScrollMinX(ErrorResult& aError) {
|
||||
+ if (auto value = MaskConfig::GetInt32("window.scrollMinX"))
|
||||
+ return value.value();
|
||||
FORWARD_TO_OUTER_OR_THROW(GetScrollBoundaryOuter, (eSideLeft), aError, 0);
|
||||
}
|
||||
|
||||
int32_t nsGlobalWindowInner::GetScrollMinY(ErrorResult& aError) {
|
||||
+ if (auto value = MaskConfig::GetInt32("window.scrollMinY"))
|
||||
+ return value.value();
|
||||
FORWARD_TO_OUTER_OR_THROW(GetScrollBoundaryOuter, (eSideTop), aError, 0);
|
||||
}
|
||||
|
||||
int32_t nsGlobalWindowInner::GetScrollMaxX(ErrorResult& aError) {
|
||||
+ if (auto value = MaskConfig::GetInt32("window.scrollMaxX"))
|
||||
+ return value.value();
|
||||
FORWARD_TO_OUTER_OR_THROW(GetScrollBoundaryOuter, (eSideRight), aError, 0);
|
||||
}
|
||||
|
||||
int32_t nsGlobalWindowInner::GetScrollMaxY(ErrorResult& aError) {
|
||||
+ if (auto value = MaskConfig::GetInt32("window.scrollMaxY"))
|
||||
+ return value.value();
|
||||
FORWARD_TO_OUTER_OR_THROW(GetScrollBoundaryOuter, (eSideBottom), aError, 0);
|
||||
}
|
||||
|
||||
double nsGlobalWindowInner::GetScrollX(ErrorResult& aError) {
|
||||
+ if (auto value = MaskConfig::GetDouble("screen.pageXOffset"))
|
||||
+ return value.value();
|
||||
FORWARD_TO_OUTER_OR_THROW(GetScrollXOuter, (), aError, 0);
|
||||
}
|
||||
|
||||
double nsGlobalWindowInner::GetScrollY(ErrorResult& aError) {
|
||||
+ if (auto value = MaskConfig::GetDouble("screen.pageYOffset"))
|
||||
+ return value.value();
|
||||
FORWARD_TO_OUTER_OR_THROW(GetScrollYOuter, (), aError, 0);
|
||||
}
|
||||
|
||||
diff --git a/dom/base/nsHistory.cpp b/dom/base/nsHistory.cpp
|
||||
index e2fd8e6389..e5ddf4e08c 100644
|
||||
--- a/dom/base/nsHistory.cpp
|
||||
+++ b/dom/base/nsHistory.cpp
|
||||
@@ -5,6 +5,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsHistory.h"
|
||||
+#include "MaskConfig.hpp"
|
||||
|
||||
#include "jsapi.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
@@ -64,6 +65,8 @@ JSObject* nsHistory::WrapObject(JSContext* aCx,
|
||||
}
|
||||
|
||||
uint32_t nsHistory::GetLength(ErrorResult& aRv) const {
|
||||
+ if (auto value = MaskConfig::GetUint32("window.history.length"))
|
||||
+ return value.value();
|
||||
nsCOMPtr<nsPIDOMWindowInner> win(do_QueryReferent(mInnerWindow));
|
||||
if (!win || !win->HasActiveDocument()) {
|
||||
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
||||
diff --git a/dom/base/nsScreen.cpp b/dom/base/nsScreen.cpp
|
||||
index 306ab35772..7f7728a6cc 100644
|
||||
index de9a6a4d11..78849001c5 100644
|
||||
--- a/dom/base/nsScreen.cpp
|
||||
+++ b/dom/base/nsScreen.cpp
|
||||
@@ -6,6 +6,8 @@
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "nsScreen.h"
|
||||
|
||||
+#include "MaskConfig.hpp"
|
||||
+
|
||||
#include "mozilla/GeckoBindings.h"
|
||||
#include "mozilla/dom/BrowsingContextBinding.h"
|
||||
#include "mozilla/dom/Document.h"
|
||||
#include "mozilla/dom/DocumentInlines.h"
|
||||
@@ -40,6 +42,10 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(nsScreen, DOMEventTargetHelper,
|
||||
@@ -44,6 +46,10 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(nsScreen, DOMEventTargetHelper,
|
||||
mScreenOrientation)
|
||||
|
||||
int32_t nsScreen::PixelDepth() {
|
||||
@@ -212,7 +114,7 @@ index 306ab35772..7f7728a6cc 100644
|
||||
// Return 24 to prevent fingerprinting.
|
||||
if (ShouldResistFingerprinting(RFPTarget::ScreenPixelDepth)) {
|
||||
return 24;
|
||||
@@ -89,6 +95,12 @@ CSSIntRect nsScreen::GetRect() {
|
||||
@@ -99,6 +105,12 @@ CSSIntRect nsScreen::GetRect() {
|
||||
}
|
||||
|
||||
CSSIntRect nsScreen::GetAvailRect() {
|
||||
@@ -225,79 +127,19 @@ index 306ab35772..7f7728a6cc 100644
|
||||
// Return window inner rect to prevent fingerprinting.
|
||||
if (ShouldResistFingerprinting(RFPTarget::ScreenAvailRect)) {
|
||||
return GetTopWindowInnerRectForRFP();
|
||||
diff --git a/dom/battery/BatteryManager.cpp b/dom/battery/BatteryManager.cpp
|
||||
index 6322093fd9..264cc91bd1 100644
|
||||
--- a/dom/battery/BatteryManager.cpp
|
||||
+++ b/dom/battery/BatteryManager.cpp
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
+#include "MaskConfig.hpp"
|
||||
#include "Constants.h"
|
||||
#include "mozilla/DOMEventTargetHelper.h"
|
||||
#include "mozilla/Hal.h"
|
||||
@@ -53,6 +54,9 @@ JSObject* BatteryManager::WrapObject(JSContext* aCx,
|
||||
|
||||
bool BatteryManager::Charging() const {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
+ if (auto value = MaskConfig::GetBool("battery:charging"); value.has_value())
|
||||
+ return value.value();
|
||||
+
|
||||
// For testing, unable to report the battery status information
|
||||
if (Preferences::GetBool("dom.battery.test.default", false)) {
|
||||
return true;
|
||||
@@ -69,6 +73,8 @@ bool BatteryManager::Charging() const {
|
||||
|
||||
double BatteryManager::DischargingTime() const {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
+ if (auto value = MaskConfig::GetDouble("battery:dischargingTime"))
|
||||
+ return value.value();
|
||||
// For testing, unable to report the battery status information
|
||||
if (Preferences::GetBool("dom.battery.test.default", false)) {
|
||||
return std::numeric_limits<double>::infinity();
|
||||
@@ -86,6 +92,8 @@ double BatteryManager::DischargingTime() const {
|
||||
|
||||
double BatteryManager::ChargingTime() const {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
+ if (auto value = MaskConfig::GetDouble("battery:chargingTime"))
|
||||
+ return value.value();
|
||||
// For testing, unable to report the battery status information
|
||||
if (Preferences::GetBool("dom.battery.test.default", false)) {
|
||||
return 0.0;
|
||||
@@ -103,6 +111,7 @@ double BatteryManager::ChargingTime() const {
|
||||
|
||||
double BatteryManager::Level() const {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
+ if (auto value = MaskConfig::GetDouble("battery:level")) return value.value();
|
||||
// For testing, unable to report the battery status information
|
||||
if (Preferences::GetBool("dom.battery.test.default")) {
|
||||
return 1.0;
|
||||
diff --git a/dom/battery/moz.build b/dom/battery/moz.build
|
||||
index 3a90c93c01..91d673039b 100644
|
||||
--- a/dom/battery/moz.build
|
||||
+++ b/dom/battery/moz.build
|
||||
@@ -21,3 +21,6 @@ FINAL_LIBRARY = "xul"
|
||||
|
||||
MOCHITEST_CHROME_MANIFESTS += ["test/chrome.toml"]
|
||||
MOCHITEST_MANIFESTS += ["test/mochitest.toml"]
|
||||
+
|
||||
+# DOM Mask
|
||||
+LOCAL_INCLUDES += ["/camoucfg"]
|
||||
\ No newline at end of file
|
||||
diff --git a/dom/workers/WorkerNavigator.cpp b/dom/workers/WorkerNavigator.cpp
|
||||
index 622724b529..c9a65ff013 100644
|
||||
index e9ebd3e01b..c6f64371f2 100644
|
||||
--- a/dom/workers/WorkerNavigator.cpp
|
||||
+++ b/dom/workers/WorkerNavigator.cpp
|
||||
@@ -5,6 +5,7 @@
|
||||
@@ -3,6 +3,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "mozilla/dom/WorkerNavigator.h"
|
||||
+#include "MaskConfig.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -103,6 +104,9 @@ JSObject* WorkerNavigator::WrapObject(JSContext* aCx,
|
||||
#include "ErrorList.h"
|
||||
#include "MainThreadUtils.h"
|
||||
@@ -106,6 +107,9 @@ JSObject* WorkerNavigator::WrapObject(JSContext* aCx,
|
||||
}
|
||||
|
||||
bool WorkerNavigator::GlobalPrivacyControl() const {
|
||||
@@ -307,7 +149,7 @@ index 622724b529..c9a65ff013 100644
|
||||
bool gpcStatus = StaticPrefs::privacy_globalprivacycontrol_enabled();
|
||||
if (!gpcStatus) {
|
||||
JSObject* jso = GetWrapper();
|
||||
@@ -125,6 +129,8 @@ void WorkerNavigator::SetLanguages(const nsTArray<nsString>& aLanguages) {
|
||||
@@ -128,6 +132,8 @@ void WorkerNavigator::SetLanguages(const nsTArray<nsString>& aLanguages) {
|
||||
void WorkerNavigator::GetAppVersion(nsString& aAppVersion,
|
||||
CallerType aCallerType,
|
||||
ErrorResult& aRv) const {
|
||||
@@ -316,7 +158,7 @@ index 622724b529..c9a65ff013 100644
|
||||
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
|
||||
MOZ_ASSERT(workerPrivate);
|
||||
|
||||
@@ -147,6 +153,8 @@ void WorkerNavigator::GetAppVersion(nsString& aAppVersion,
|
||||
@@ -150,6 +156,8 @@ void WorkerNavigator::GetAppVersion(nsString& aAppVersion,
|
||||
|
||||
void WorkerNavigator::GetPlatform(nsString& aPlatform, CallerType aCallerType,
|
||||
ErrorResult& aRv) const {
|
||||
@@ -325,7 +167,7 @@ index 622724b529..c9a65ff013 100644
|
||||
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
|
||||
MOZ_ASSERT(workerPrivate);
|
||||
|
||||
@@ -207,6 +215,8 @@ class GetUserAgentRunnable final : public WorkerMainThreadRunnable {
|
||||
@@ -210,6 +218,8 @@ class GetUserAgentRunnable final : public WorkerMainThreadRunnable {
|
||||
|
||||
void WorkerNavigator::GetUserAgent(nsString& aUserAgent, CallerType aCallerType,
|
||||
ErrorResult& aRv) const {
|
||||
@@ -334,7 +176,7 @@ index 622724b529..c9a65ff013 100644
|
||||
WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
|
||||
MOZ_ASSERT(workerPrivate);
|
||||
|
||||
@@ -218,6 +228,8 @@ void WorkerNavigator::GetUserAgent(nsString& aUserAgent, CallerType aCallerType,
|
||||
@@ -221,6 +231,8 @@ void WorkerNavigator::GetUserAgent(nsString& aUserAgent, CallerType aCallerType,
|
||||
}
|
||||
|
||||
uint64_t WorkerNavigator::HardwareConcurrency() const {
|
||||
@@ -344,10 +186,10 @@ index 622724b529..c9a65ff013 100644
|
||||
MOZ_ASSERT(rts);
|
||||
|
||||
diff --git a/dom/workers/moz.build b/dom/workers/moz.build
|
||||
index 2f2948a729..b8ad10403f 100644
|
||||
index f5ec1301a7..294a1c7a03 100644
|
||||
--- a/dom/workers/moz.build
|
||||
+++ b/dom/workers/moz.build
|
||||
@@ -114,3 +114,6 @@ MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"]
|
||||
@@ -110,3 +110,6 @@ MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"]
|
||||
XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ["test/browser.toml"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/dom/animation/AnimationEffect.cpp b/dom/animation/AnimationEffect.cpp
|
||||
index e92fdc9268..5f3fa86940 100644
|
||||
index e92fdc9268..9c04f00200 100644
|
||||
--- a/dom/animation/AnimationEffect.cpp
|
||||
+++ b/dom/animation/AnimationEffect.cpp
|
||||
@@ -11,6 +11,7 @@
|
||||
@@ -10,7 +10,7 @@ index e92fdc9268..5f3fa86940 100644
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
@@ -121,10 +122,19 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
|
||||
@@ -121,10 +122,20 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
|
||||
// Always return the same object to benefit from return-value optimization.
|
||||
ComputedTiming result;
|
||||
|
||||
@@ -19,19 +19,20 @@ index e92fdc9268..5f3fa86940 100644
|
||||
MOZ_ASSERT(aTiming.Duration().ref() >= zeroDuration,
|
||||
"Iteration duration should be positive");
|
||||
- result.mDuration = aTiming.Duration().ref();
|
||||
+ // Camoufox: finite CSS animations complete instantly so Playwright doesn't
|
||||
+ // wait on them. `disableInstantAnimations` restores normal animation timing.
|
||||
+ if (MaskConfig::GetBool("disableInstantAnimations") ||
|
||||
+ result.mActiveDuration == StickyTimeDuration::Forever()) {
|
||||
+ result.mDuration = aTiming.Duration().ref();
|
||||
+ } else {
|
||||
+ // Camoufox: `instantAnimations` completes finite animations at once so
|
||||
+ // Playwright never waits on them. Off by default: a page reads the zero
|
||||
+ // duration back through getComputedTiming().
|
||||
+ if (MaskConfig::GetBool("instantAnimations") &&
|
||||
+ result.mActiveDuration != StickyTimeDuration::Forever()) {
|
||||
+ result.mDuration = zeroDuration;
|
||||
+ result.mActiveDuration = zeroDuration;
|
||||
+ } else {
|
||||
+ result.mDuration = aTiming.Duration().ref();
|
||||
+ }
|
||||
}
|
||||
|
||||
MOZ_ASSERT(aTiming.Iterations() >= 0.0 && !std::isnan(aTiming.Iterations()),
|
||||
@@ -137,7 +147,6 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
|
||||
@@ -137,7 +148,6 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
|
||||
"ValidateIterationStart");
|
||||
result.mIterationStart = aTiming.IterationStart();
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ be listed there.
|
||||
| `audio-context-spoofing.patch` | `AudioContext:outputLatency` |
|
||||
| `audio-fingerprint-manager.patch` | `audio:seed` |
|
||||
| `chromeutil.patch` | `debug` |
|
||||
| `fingerprint-injection.patch` | `navigator.*`, `screen.*`, `window.*`, `battery:*` |
|
||||
| `fingerprint-injection.patch` | `navigator.*`, `screen.*`, `window.*` |
|
||||
| `font-hijacker.patch` | `navigator.platform` |
|
||||
| `font-system-fonts-css2.patch` | `navigator.platform`, `window.devicePixelRatio` |
|
||||
| `force-default-pointer.patch` | `navigator.maxTouchPoints` |
|
||||
@@ -36,7 +36,7 @@ be listed there.
|
||||
| `media-device-spoofing.patch` | `mediaDevices:*` |
|
||||
| `navigator-spoofing.patch` | `navigator.*`, `timezone` |
|
||||
| `network-patches.patch` | `headers.*`, `navigator.userAgent` |
|
||||
| `no-css-animations.patch` | `disableInstantAnimations` |
|
||||
| `no-css-animations.patch` | `instantAnimations` |
|
||||
| `screen-spoofing.patch` | `screen.width`, `screen.height` |
|
||||
| `system-ui-font-spoofing.patch` | `navigator.platform` |
|
||||
| `timezone-spoofing.patch` | `timezone` |
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
diff --git a/dom/media/webspeech/synth/moz.build b/dom/media/webspeech/synth/moz.build
|
||||
index ca6ffacb21..fb661fd69e 100644
|
||||
--- a/dom/media/webspeech/synth/moz.build
|
||||
+++ b/dom/media/webspeech/synth/moz.build
|
||||
@@ -63,2 +63,5 @@
|
||||
@@ -62,3 +62,6 @@ LOCAL_INCLUDES += [
|
||||
"/dom/base",
|
||||
"ipc",
|
||||
]
|
||||
+
|
||||
+# DOM Mask
|
||||
+LOCAL_INCLUDES += ['/camoucfg']
|
||||
|
||||
diff --git a/dom/media/webspeech/synth/nsSynthVoiceRegistry.cpp b/dom/media/webspeech/synth/nsSynthVoiceRegistry.cpp
|
||||
index e5a1353d6b..4a4a5b080b 100644
|
||||
index 3648239545..48e195a684 100644
|
||||
--- a/dom/media/webspeech/synth/nsSynthVoiceRegistry.cpp
|
||||
+++ b/dom/media/webspeech/synth/nsSynthVoiceRegistry.cpp
|
||||
@@ -26,7 +26,8 @@
|
||||
@@ -23,7 +23,9 @@
|
||||
#include "nsServiceManagerUtils.h"
|
||||
#include "nsSpeechTask.h"
|
||||
#include "nsString.h"
|
||||
+#include "nsThreadUtils.h"
|
||||
|
||||
+#include "MaskConfig.hpp"
|
||||
using mozilla::intl::LocaleService;
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
@@ -169,6 +171,20 @@
|
||||
#undef LOG
|
||||
@@ -165,6 +167,20 @@ nsSynthVoiceRegistry* nsSynthVoiceRegistry::GetInstance() {
|
||||
// Start up all speech synth services.
|
||||
NS_CreateServicesFromCategory(NS_SPEECH_SYNTH_STARTED, nullptr,
|
||||
NS_SPEECH_SYNTH_STARTED);
|
||||
@@ -42,7 +44,7 @@ index e5a1353d6b..4a4a5b080b 100644
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +320,21 @@ nsSynthVoiceRegistry::AddVoice(nsISpeechService* aService,
|
||||
@@ -301,6 +317,21 @@ nsSynthVoiceRegistry::AddVoice(nsISpeechService* aService,
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ index e5a1353d6b..4a4a5b080b 100644
|
||||
return AddVoiceImpl(aService, aUri, aName, aLang, aLocalService,
|
||||
aQueuesUtterances);
|
||||
}
|
||||
@@ -779,6 +796,35 @@ void nsSynthVoiceRegistry::SpeakImpl(VoiceData* aVoice, nsSpeechTask* aTask,
|
||||
@@ -775,6 +806,30 @@ void nsSynthVoiceRegistry::SpeakImpl(VoiceData* aVoice, nsSpeechTask* aTask,
|
||||
NS_ConvertUTF16toUTF8(aText).get(),
|
||||
NS_ConvertUTF16toUTF8(aVoice->mUri).get(), aRate, aPitch));
|
||||
|
||||
@@ -72,26 +74,21 @@ index e5a1353d6b..4a4a5b080b 100644
|
||||
+ if (auto voices = MaskConfig::MVoices()) {
|
||||
+ for (const auto& [lang, name, uri, isDefault, isLocal] : voices.value()) {
|
||||
+ if (NS_ConvertUTF8toUTF16(uri).Equals(aVoice->mUri)) {
|
||||
+ printf_stderr("Tried to speak a fake voice: %s",
|
||||
+ NS_ConvertUTF16toUTF8(aVoice->mUri).get());
|
||||
+ // A spoofed voice has no engine behind it. Speak it silently for as
|
||||
+ // long as the text takes at ~150 words per minute, so start and end
|
||||
+ // arrive when they would from a real voice.
|
||||
+ aTask->Init();
|
||||
+ // If fake completion is disabled, throw an error
|
||||
+ if (!MaskConfig::GetBool("voices:fakeCompletion")) {
|
||||
+ aTask->DispatchError(0, 0);
|
||||
+ return;
|
||||
+ }
|
||||
+ float charsPerSecond;
|
||||
+ if (auto value =
|
||||
+ MaskConfig::GetDouble("voices:fakeCompletion:charsPerSecond")) {
|
||||
+ charsPerSecond = value.value();
|
||||
+ } else {
|
||||
+ charsPerSecond = 12.5f;
|
||||
+ }
|
||||
+ // Return a fake success with a speach rate of 150wpm
|
||||
+ aTask->DispatchStart();
|
||||
+ float fakeElapsedTime =
|
||||
+ static_cast<float>(aText.Length()) / (charsPerSecond * aRate);
|
||||
+ aTask->DispatchEnd(fakeElapsedTime, aText.Length());
|
||||
+ const float elapsed =
|
||||
+ static_cast<float>(aText.Length()) / (12.5f * aRate);
|
||||
+ RefPtr<nsSpeechTask> task = aTask;
|
||||
+ const uint32_t length = aText.Length();
|
||||
+ NS_DelayedDispatchToCurrentThread(
|
||||
+ NS_NewRunnableFunction("CamouFakeSpeechEnd",
|
||||
+ [task, elapsed, length]() {
|
||||
+ (void)task->DispatchEnd(elapsed, length);
|
||||
+ }),
|
||||
+ static_cast<uint32_t>(elapsed * 1000));
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
|
||||
@@ -59,7 +59,6 @@ window:
|
||||
# bottom edge land (see BROWSER_CHROME_HEIGHT in coherence.py).
|
||||
screenX: window.screenX
|
||||
screenY: window.screenY
|
||||
pageYOffset: screen.pageYOffset
|
||||
# devicePixelRatio is not mapped: any value but 1 is a spoofing tell unless
|
||||
# the whole geometry is scaled with it.
|
||||
|
||||
|
||||
@@ -575,6 +575,8 @@ def warn_manual_config(config: Dict[str, Any]) -> None:
|
||||
# CSS pointer media queries and the TouchEvent interfaces.
|
||||
if is_domain_set(config, 'navigator.maxTouchPoints'):
|
||||
LeakWarning.warn('max_touch_points', False)
|
||||
if config.get('instantAnimations'):
|
||||
LeakWarning.warn('instant_animations', False)
|
||||
# Manual screen/window setting
|
||||
if is_domain_set(config, 'screen.', 'window.', 'document.body.'):
|
||||
LeakWarning.warn('viewport', False)
|
||||
@@ -585,8 +587,6 @@ _WINDOW_DIM_KEYS = (
|
||||
'window.outerHeight',
|
||||
'window.innerWidth',
|
||||
'window.innerHeight',
|
||||
'document.body.clientWidth',
|
||||
'document.body.clientHeight',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ no_region: >-
|
||||
Because you did not pass in a locale region, Camoufox will generate one for you.
|
||||
This can cause suspicion if your IP does not match your locale region.
|
||||
|
||||
instant_animations: >-
|
||||
instantAnimations makes every finite animation finish at once, so Playwright
|
||||
never waits on one. A page can see it: getComputedTiming() reports a duration
|
||||
of 0 where stock Firefox reports the real one.
|
||||
|
||||
block_webgl: >-
|
||||
Disabling WebGL is not recommended. Many WAFs will check if WebGL is enabled.
|
||||
|
||||
|
||||
@@ -194,3 +194,10 @@ def test_config_overrides_reach_the_config_and_the_init_script():
|
||||
context = fp.generate_context_fingerprint(os="linux", config_overrides={"audio:seed": 7})
|
||||
assert context["config"]["audio:seed"] == 7
|
||||
assert "setAudioFingerprintSeed(7)" in context["init_script"]
|
||||
|
||||
|
||||
def test_instant_animations_warn_that_they_are_detectable():
|
||||
from camoufox._warnings import LeakWarning
|
||||
|
||||
with pytest.warns(LeakWarning, match="getComputedTiming"):
|
||||
launch(config={"instantAnimations": True}, i_know_what_im_doing=False)
|
||||
|
||||
@@ -22,7 +22,6 @@ def _opts(config_blob: str):
|
||||
[
|
||||
('{"window.outerWidth": 360}', True),
|
||||
('{"window.innerHeight": 740}', True),
|
||||
('{"document.body.clientWidth": 360}', True),
|
||||
('{"screen.width": 360}', False),
|
||||
('{"navigator.userAgent": "x"}', False),
|
||||
("{}", False),
|
||||
|
||||
@@ -17,33 +17,18 @@
|
||||
{ "property": "screen.width", "type": "uint" },
|
||||
{ "property": "screen.colorDepth", "type": "uint" },
|
||||
{ "property": "screen.pixelDepth", "type": "uint" },
|
||||
{ "property": "screen.pageXOffset", "type": "double" },
|
||||
{ "property": "screen.pageYOffset", "type": "double" },
|
||||
{ "property": "window.scrollMinX", "type": "int" },
|
||||
{ "property": "window.scrollMinY", "type": "int" },
|
||||
{ "property": "window.scrollMaxX", "type": "int" },
|
||||
{ "property": "window.scrollMaxY", "type": "int" },
|
||||
{ "property": "window.outerHeight", "type": "uint" },
|
||||
{ "property": "window.outerWidth", "type": "uint" },
|
||||
{ "property": "window.innerHeight", "type": "uint" },
|
||||
{ "property": "window.innerWidth", "type": "uint" },
|
||||
{ "property": "window.screenX", "type": "int" },
|
||||
{ "property": "window.screenY", "type": "int" },
|
||||
{ "property": "window.history.length", "type": "uint" },
|
||||
{ "property": "window.devicePixelRatio", "type": "double" },
|
||||
{ "property": "document.body.clientWidth", "type": "uint" },
|
||||
{ "property": "document.body.clientHeight", "type": "uint" },
|
||||
{ "property": "document.body.clientTop", "type": "uint" },
|
||||
{ "property": "document.body.clientLeft", "type": "uint" },
|
||||
{ "property": "headers.User-Agent", "type": "str" },
|
||||
{ "property": "headers.Accept-Language", "type": "str" },
|
||||
{ "property": "headers.Accept-Encoding", "type": "str" },
|
||||
{ "property": "webrtc:ipv4", "type": "str" },
|
||||
{ "property": "webrtc:ipv6", "type": "str" },
|
||||
{ "property": "battery:charging", "type": "bool" },
|
||||
{ "property": "battery:chargingTime", "type": "double" },
|
||||
{ "property": "battery:dischargingTime", "type": "double" },
|
||||
{ "property": "battery:level", "type": "double" },
|
||||
{ "property": "fonts", "type": "array" },
|
||||
{ "property": "audio:seed", "type": "uint" },
|
||||
{ "property": "geolocation:latitude", "type": "double" },
|
||||
@@ -75,8 +60,6 @@
|
||||
{ "property": "webGl2:contextAttributes", "type": "dict" },
|
||||
{ "property": "voices", "type": "array" },
|
||||
{ "property": "voices:blockIfNotDefined", "type": "bool" },
|
||||
{ "property": "voices:fakeCompletion", "type": "bool" },
|
||||
{ "property": "voices:fakeCompletion:charsPerSecond", "type": "double" },
|
||||
{ "property": "mediaDevices:micros", "type": "uint" },
|
||||
{ "property": "mediaDevices:webcams", "type": "uint" },
|
||||
{ "property": "mediaDevices:speakers", "type": "uint" },
|
||||
@@ -94,7 +77,7 @@
|
||||
{ "property": "forceScopeAccess", "type": "bool" },
|
||||
|
||||
{ "property": "disableTheming", "type": "bool" },
|
||||
{ "property": "disableInstantAnimations", "type": "bool" },
|
||||
{ "property": "instantAnimations", "type": "bool" },
|
||||
{ "property": "addons", "type": "array" },
|
||||
{ "property": "certificatePaths", "type": "array" },
|
||||
{ "property": "certificates", "type": "array" },
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Verify animations run on stock timing unless `instantAnimations` is set.
|
||||
|
||||
no-css-animations.patch can finish every finite animation at once, so Playwright
|
||||
never waits on one. That used to be the default, and a page could read it back in
|
||||
one line: `el.animate(frames, 1000).effect.getComputedTiming().duration` was 0
|
||||
where stock Firefox reports 1000, and a CSS transition reported 0 as well. It is
|
||||
now an opt-in.
|
||||
|
||||
What PASS means:
|
||||
* by default, a 1000ms Web Animation and a 500ms CSS transition report
|
||||
their real durations;
|
||||
* with config {"instantAnimations": True}, both report 0.
|
||||
|
||||
python tests/patches/animation-timing.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from helpers import resolve_binary # noqa: E402
|
||||
|
||||
PROBE = """() => {
|
||||
const animation = document.body.animate([{opacity: 0}, {opacity: 1}], 1000);
|
||||
const el = document.createElement('div');
|
||||
document.body.append(el);
|
||||
el.style.transition = 'opacity 500ms';
|
||||
el.style.opacity = '0';
|
||||
el.offsetWidth;
|
||||
el.style.opacity = '1';
|
||||
return {
|
||||
animation: animation.effect.getComputedTiming().duration,
|
||||
transition: el.getAnimations().map(a => a.effect.getComputedTiming().duration),
|
||||
};
|
||||
}"""
|
||||
|
||||
|
||||
async def probe(config):
|
||||
from camoufox.async_api import AsyncCamoufox
|
||||
|
||||
async with AsyncCamoufox(headless=True, os="linux", config=config,
|
||||
i_know_what_im_doing=True,
|
||||
executable_path=str(resolve_binary())) as browser:
|
||||
page = await browser.new_page()
|
||||
await page.set_content("<body></body>")
|
||||
return await page.evaluate(PROBE)
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
passed = True
|
||||
for config, expected in (({}, {"animation": 1000, "transition": [500]}),
|
||||
({"instantAnimations": True}, {"animation": 0, "transition": [0]})):
|
||||
label = "instantAnimations" if config else "default"
|
||||
got = await probe(dict(config)) # the launcher fills in the dict it is given
|
||||
if got == expected:
|
||||
print(f" PASS {label}: {got}")
|
||||
else:
|
||||
passed = False
|
||||
print(f" FAIL {label}: got {got}, expected {expected}")
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Verify speechSynthesis.speak() on a spoofed voice behaves like a real voice.
|
||||
|
||||
A spoofed voice has no speech engine behind it. voice-spoofing.patch used to fire
|
||||
an `error` event for it unless `voices:fakeCompletion` was set, and even then it
|
||||
fired `start` and `end` in the same instant. A real voice never errors on a plain
|
||||
utterance, and its `end` arrives after the text has been spoken. Both were tells.
|
||||
|
||||
What PASS means:
|
||||
* the page sees spoofed voices at all (so the check is not vacuous);
|
||||
* speaking 25 characters on one fires `start`, then `end`, and no `error`;
|
||||
* `end` arrives about as long after `start` as the text takes to say at
|
||||
~150 words per minute (2s here), not in the same tick.
|
||||
|
||||
python tests/patches/spoofed-voice-speaks.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from helpers import resolve_binary # noqa: E402
|
||||
|
||||
TEXT = "a" * 25 # 25 chars at 12.5 chars/s = 2.0s
|
||||
PROBE = """async (text) => {
|
||||
let voices = speechSynthesis.getVoices();
|
||||
if (!voices.length) {
|
||||
await new Promise(r => { speechSynthesis.onvoiceschanged = r; setTimeout(r, 3000); });
|
||||
voices = speechSynthesis.getVoices();
|
||||
}
|
||||
if (!voices.length) return {voices: 0};
|
||||
const u = new SpeechSynthesisUtterance(text);
|
||||
u.voice = voices[0];
|
||||
const events = [];
|
||||
const t0 = performance.now();
|
||||
const done = new Promise(resolve => {
|
||||
for (const type of ['start', 'end', 'error']) {
|
||||
u.addEventListener(type, () => {
|
||||
events.push([type, Math.round(performance.now() - t0)]);
|
||||
if (type !== 'start') resolve();
|
||||
});
|
||||
}
|
||||
setTimeout(resolve, 10000);
|
||||
});
|
||||
speechSynthesis.speak(u);
|
||||
await done;
|
||||
return {voices: voices.length, events};
|
||||
}"""
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
from camoufox.async_api import AsyncCamoufox
|
||||
|
||||
async with AsyncCamoufox(headless=True, os="windows",
|
||||
executable_path=str(resolve_binary())) as browser:
|
||||
page = await browser.new_page()
|
||||
await page.goto("about:blank")
|
||||
got = await page.evaluate(PROBE, TEXT)
|
||||
print(f" {got}")
|
||||
|
||||
if not got["voices"]:
|
||||
print(" FAIL: the page sees no voices, so nothing was tested")
|
||||
return 1
|
||||
kinds = [kind for kind, _ in got["events"]]
|
||||
if kinds != ["start", "end"]:
|
||||
print(f" FAIL: expected start then end, got {kinds}")
|
||||
return 1
|
||||
spoken = got["events"][1][1] - got["events"][0][1]
|
||||
if not 1500 <= spoken <= 4000:
|
||||
print(f" FAIL: end came {spoken}ms after start, expected about 2000ms")
|
||||
return 1
|
||||
print(f" PASS: start, then end {spoken}ms later, no error")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
Reference in New Issue
Block a user