diff --git a/README.md b/README.md
index 9a66f60..a8b28b4 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ Camoufox aims to be a minimalistic browser for robust fingerprint injection & an
- Spoof properties of device, viewport, screen, WebGL, battery API, location, etc. ✅
- Font spoofing & anti-fingerprinting ✅
- WebRTC IP spoofing ✅
+- Human-like mouse movement 🖱️
- Blocks & circumvents ads 🛡️
- Debloated & optimized for memory and speed ⚡
- [PyPi package](https://pypi.org/project/camoufox/) for updates & auto fingerprint injection 📦
@@ -68,7 +69,7 @@ Camoufox is built on top of Firefox/Juggler instead of Chromium because:
- Continue research on potential leaks
- Remote hosting Camoufox as a Playwright server
- Integrate into [hrequests](https://github.com/daijro/hrequests)
-- Human-like typing & mouse movement
+- Human-like typing & ~~mouse movement~~ ✔️
- WebGL fingerprint spoofing through ANGLE rendering
- Create integration tests
- Chromium port (long term)
@@ -130,6 +131,34 @@ Camoufox will automatically add the following default fonts associated your spoo
+
+
+Cursor movement
+
+
+### Human-like Cursor movement
+
+Camoufox supports use human-like cursor movement. The natural motion algorithm was originally from [rifosnake's HumanCursor](https://github.com/riflosnake/HumanCursor), but has been rewritten in C++ and modified for more distance-aware trajectories.
+
+### Demo
+
+
+
+### Properties
+
+| Property | Supported | Description |
+| ---------------- | --------- | ------------------------------------------------------------------- |
+| humanize | ✅ | Enable/disable human-like cursor movement. Defaults to False. |
+| humanize:maxTime | ✅ | Maximum time in seconds for the cursor movement. Defaults to `1.5`. |
+| showcursor | ✅ | Show/hide the cursor. Defaults to True. |
+
+**Notes:**
+
+- Trajectories are capped at taking 1.5 seconds to complete.
+- The cursor highlighter is **not** ran in the page context. It will not be visible to the page. You don't have to worry about it leaking.
+
+
+
Fonts
@@ -483,7 +512,7 @@ Camoufox performs well against every major WAF I've tested. (Original test sites
| **Font Fingerprinting** | ✔️ |
| ‣ [Browserleaks Fonts](https://browserleaks.net/fonts) | ✔️ Rotates all metrics. |
| ‣ [CreepJS TextMetrics](https://abrahamjuliot.github.io/creepjs/tests/fonts.html) | ✔️ Rotates all metrics. |
-| [**Incolumitas**](https://bot.incolumitas.com/) | ✔️ 0.8-r1.0 |
+| [**Incolumitas**](https://bot.incolumitas.com/) | ✔️ 0.8-1.0 |
| [**SannySoft**](https://bot.sannysoft.com/) | ✔️ |
| [**Fingerprint.com**](https://fingerprint.com/products/bot-detection/) | ✔️ |
| [**IpHey**](https://iphey.com/) | ✔️ |
@@ -741,4 +770,5 @@ Patches can be edited, created, removed, and managed through here.
- [Jamir-boop/minimalisticfox](https://github.com/Jamir-boop/minimalisticfox) - Inspired Camoufox's minimalistic theming
- [nicoth-in/Dark-Space-Theme](https://github.com/nicoth-in/Dark-Space-Theme) - Camoufox's dark theme
- [Playwright](https://github.com/microsoft/playwright/tree/main/browser_patches/firefox), [Puppeteer/Juggler](https://github.com/puppeteer/juggler) - Original Juggler implementation
-- [CreepJS](https://github.com/abrahamjuliot/creepjs), [Browserleaks](https://browserleaks.com) - Valuable leak testing sites
+- [CreepJS](https://github.com/abrahamjuliot/creepjs), [Browserleaks](https://browserleaks.com), [BrowserScan](https://www.browserscan.net/) - Valuable leak testing sites
+- [riflosnake/HumanCursor](https://github.com/riflosnake/HumanCursor) - Original human-like cursor movement algorithm
diff --git a/additions/camoucfg/MouseTrajectories.hpp b/additions/camoucfg/MouseTrajectories.hpp
new file mode 100644
index 0000000..9a69669
--- /dev/null
+++ b/additions/camoucfg/MouseTrajectories.hpp
@@ -0,0 +1,230 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include "MaskConfig.hpp"
+
+/**
+ * Human-like mouse movement generator. Ported from:
+ * https://github.com/riflosnake/HumanCursor/blob/main/humancursor/utilities/human_curve_generator.py
+ * Modified to use a more human-like easing function.
+ */
+
+class BezierCalculator {
+ public:
+ static long long factorial(int n) {
+ if (n < 0) return -1; // Indicate error
+ long long result = 1;
+ for (int i = 2; i <= n; i++) result *= i;
+ return result;
+ }
+
+ static double binomial(int n, int k) {
+ return static_cast(factorial(n)) /
+ (factorial(k) * factorial(n - k));
+ }
+
+ static double bernsteinPolynomialPoint(double x, int i, int n) {
+ return binomial(n, i) * std::pow(x, i) * std::pow(1 - x, n - i);
+ }
+
+ static std::vector bernsteinPolynomial(
+ const std::vector>& points, double t) {
+ int n = static_cast(points.size()) - 1;
+ double x = 0.0;
+ double y = 0.0;
+ for (int i = 0; i <= n; i++) {
+ double bern = bernsteinPolynomialPoint(t, i, n);
+ x += points[i].first * bern;
+ y += points[i].second * bern;
+ }
+ return {x, y};
+ }
+
+ static std::vector> calculatePointsInCurve(
+ int nPoints, const std::vector>& points) {
+ std::vector> curvePoints;
+ for (int i = 0; i < nPoints; i++) {
+ double t = static_cast(i) / (nPoints - 1);
+ curvePoints.push_back(bernsteinPolynomial(points, t));
+ }
+ return curvePoints;
+ }
+};
+
+class HumanizeMouseTrajectory {
+ public:
+ HumanizeMouseTrajectory(const std::pair& fromPoint,
+ const std::pair& toPoint)
+ : fromPoint(fromPoint), toPoint(toPoint) {
+ generateCurve();
+ }
+
+ std::vector getPoints() const {
+ std::vector flatPoints;
+ flatPoints.reserve(points.size() * 2);
+
+ for (const auto& point : points) {
+ flatPoints.push_back(static_cast(std::round(point[0])));
+ flatPoints.push_back(static_cast(std::round(point[1])));
+ }
+
+ return flatPoints;
+ }
+
+ private:
+ std::pair fromPoint;
+ std::pair toPoint;
+ std::vector> points;
+
+ void generateCurve() {
+ double leftBoundary = std::min(fromPoint.first, toPoint.first) - 80.0;
+ double rightBoundary = std::max(fromPoint.first, toPoint.first) + 80.0;
+ double downBoundary = std::min(fromPoint.second, toPoint.second) - 80.0;
+ double upBoundary = std::max(fromPoint.second, toPoint.second) + 80.0;
+
+ std::vector> internalKnots =
+ generateInternalKnots(leftBoundary, rightBoundary, downBoundary,
+ upBoundary, 2);
+
+ std::vector> curvePoints =
+ generatePoints(internalKnots);
+ curvePoints = distortPoints(curvePoints, 1.0, 1.0, 0.5);
+ points = tweenPoints(curvePoints);
+ }
+
+ double easeOutQuad(double n) const {
+ assert(n >= 0.0 && n <= 1.0 && "Argument must be between 0.0 and 1.0.");
+ return -n * (n - 2);
+ }
+
+ std::vector> generateInternalKnots(
+ double lBoundary, double rBoundary, double dBoundary, double uBoundary,
+ int knotsCount) const {
+ assert(isNumeric(lBoundary) && isNumeric(rBoundary) &&
+ isNumeric(dBoundary) && isNumeric(uBoundary) &&
+ "Boundaries must be numeric values");
+ assert(knotsCount >= 0 && "knotsCount must be non-negative");
+ assert(lBoundary <= rBoundary &&
+ "Left boundary must be less than or equal to right boundary");
+ assert(dBoundary <= uBoundary &&
+ "Down boundary must be less than or equal to upper boundary");
+
+ std::vector knotsX =
+ randomChoiceDoubles(lBoundary, rBoundary, knotsCount);
+ std::vector knotsY =
+ randomChoiceDoubles(dBoundary, uBoundary, knotsCount);
+
+ std::vector> knots;
+ for (int i = 0; i < knotsCount; i++) {
+ knots.emplace_back(knotsX[i], knotsY[i]);
+ }
+ return knots;
+ }
+
+ std::vector randomChoiceDoubles(double min, double max,
+ int size) const {
+ std::vector choices;
+ std::uniform_real_distribution dist(min, max);
+ for (int i = 0; i < size; i++) {
+ choices.push_back(dist(randomEngine));
+ }
+ return choices;
+ }
+
+ std::vector> generatePoints(
+ const std::vector>& knots) const {
+ assert(isListOfPoints(knots) && "Knots must be a valid list of points");
+ int midPtsCnt = static_cast(
+ std::max({std::abs(fromPoint.first - toPoint.first),
+ std::abs(fromPoint.second - toPoint.second), 2.0}));
+ std::vector> controlPoints = knots;
+ controlPoints.insert(controlPoints.begin(), fromPoint);
+ controlPoints.push_back(toPoint);
+ return BezierCalculator::calculatePointsInCurve(midPtsCnt, controlPoints);
+ }
+
+ std::vector> distortPoints(
+ const std::vector>& points, double distortionMean,
+ double distortionStDev, double distortionFrequency) const {
+ assert(isNumeric(distortionMean) && isNumeric(distortionStDev) &&
+ isNumeric(distortionFrequency) && "Distortions must be numeric");
+ assert(isListOfPoints(points) && "Points must be a valid list of points");
+ assert(0.0 <= distortionFrequency && distortionFrequency <= 1.0 &&
+ "distortion_frequency must be in range [0,1]");
+
+ std::vector> distorted;
+ distorted.push_back(points.front());
+
+ std::normal_distribution normalDist(distortionMean,
+ distortionStDev);
+ std::uniform_real_distribution uniformDist(0.0, 1.0);
+
+ for (size_t i = 1; i < points.size() - 1; i++) {
+ double x = points[i][0];
+ double y = points[i][1];
+ double delta = 0.0;
+ if (uniformDist(randomEngine) < distortionFrequency) {
+ delta = std::round(normalDist(randomEngine));
+ }
+ distorted.push_back({x, y + delta});
+ }
+ distorted.push_back(points.back());
+ return distorted;
+ }
+
+ int32_t getMaxTime() const {
+ if (auto maxTime = MaskConfig::GetDouble("humanize:maxTime")) {
+ return static_cast(maxTime.value() * 100);
+ }
+ return 150;
+ }
+
+ std::vector> tweenPoints(
+ const std::vector>& points) const {
+ assert(isListOfPoints(points) && "List of points not valid");
+
+ double totalLength = 0.0;
+ for (size_t i = 1; i < points.size(); ++i) {
+ double dx = points[i][0] - points[i - 1][0];
+ double dy = points[i][1] - points[i - 1][1];
+ totalLength += std::sqrt(dx * dx + dy * dy);
+ }
+
+ // Uses a power scale to keep the speed consistent
+ int targetPoints = std::min(
+ getMaxTime(),
+ std::max(2, static_cast(std::pow(totalLength, 0.25) * 20)));
+
+ std::vector> res;
+ for (int i = 0; i < targetPoints; i++) {
+ double t = static_cast(i) / (targetPoints - 1);
+ double easedT = easeOutQuad(t);
+ int index = static_cast(easedT * (points.size() - 1));
+ res.push_back(points[index]);
+ }
+ return res;
+ }
+
+ bool isNumeric(double val) const { return !std::isnan(val); }
+
+ bool isListOfPoints(
+ const std::vector>& points) const {
+ for (const auto& p : points) {
+ if (!isNumeric(p.first) || !isNumeric(p.second)) return false;
+ }
+ return true;
+ }
+
+ bool isListOfPoints(const std::vector>& points) const {
+ for (const auto& p : points) {
+ if (p.size() != 2 || !isNumeric(p[0]) || !isNumeric(p[1])) return false;
+ }
+ return true;
+ }
+
+ mutable std::default_random_engine randomEngine{
+ static_cast(std::time(nullptr))};
+};
\ No newline at end of file
diff --git a/additions/camoucfg/moz.build b/additions/camoucfg/moz.build
index c7f06bf..3fe87d2 100644
--- a/additions/camoucfg/moz.build
+++ b/additions/camoucfg/moz.build
@@ -9,10 +9,10 @@ with Files("**"):
EXPORTS += [
"MaskConfig.hpp",
+ "MouseTrajectories.hpp",
]
LOCAL_INCLUDES += [
- "/dom/base",
"/camoucfg",
]
diff --git a/additions/juggler/protocol/PageHandler.js b/additions/juggler/protocol/PageHandler.js
index bab151b..3c3cd7b 100644
--- a/additions/juggler/protocol/PageHandler.js
+++ b/additions/juggler/protocol/PageHandler.js
@@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
- * 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/. */
+* 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/. */
"use strict";
@@ -81,6 +81,7 @@ class PageHandler {
this._isDragging = false;
this._lastMousePosition = { x: 0, y: 0 };
+ this._lastTrackedPos = { x: 0, y: 0 };
this._reportedFrameIds = new Set();
this._networkEventsForUnreportedFrameIds = new Map();
@@ -500,13 +501,11 @@ class PageHandler {
await helper.awaitTopic('apz-repaints-flushed');
const watcher = new EventWatcher(this._pageEventSink, types, this._pendingEventWatchers);
- const promises = [];
- for (const type of types) {
- // This dispatches to the renderer synchronously.
+ const sendMouseEvent = async (eventType, eventX, eventY) => {
const jugglerEventId = win.windowUtils.jugglerSendMouseEvent(
- type,
- x + boundingBox.left,
- y + boundingBox.top,
+ eventType,
+ eventX + boundingBox.left,
+ eventY + boundingBox.top,
button,
clickCount,
modifiers,
@@ -519,9 +518,26 @@ class PageHandler {
win.windowUtils.DEFAULT_MOUSE_POINTER_ID /* pointerIdentifier */,
false /* disablePointerEvent */
);
- promises.push(watcher.ensureEvent(type, eventObject => eventObject.jugglerEventId === jugglerEventId));
+ await watcher.ensureEvent(eventType, eventObject => eventObject.jugglerEventId === jugglerEventId);
+ };
+ for (const type of types) {
+ if (type === 'mousemove' && ChromeUtils.camouGetBool('humanize', false)) {
+ let trajectory = ChromeUtils.camouGetMouseTrajectory(this._lastTrackedPos.x, this._lastTrackedPos.y, x, y);
+ for (let i = 2; i < trajectory.length - 2; i += 2) {
+ let currentX = trajectory[i];
+ let currentY = trajectory[i + 1];
+ // Skip movement that is out of bounds
+ if (currentX < 0 || currentY < 0 || currentX > boundingBox.width || currentY > boundingBox.height) {
+ continue;
+ }
+ await sendMouseEvent(type, currentX, currentY);
+ await new Promise(resolve => setTimeout(resolve, 10));
+ }
+ } else {
+ // Call the function for the current event
+ await sendMouseEvent(type, x, y);
+ }
}
- await Promise.all(promises);
await watcher.dispose();
};
@@ -579,6 +595,7 @@ class PageHandler {
const watcher = new EventWatcher(this._pageEventSink, ['dragstart', 'juggler-drag-finalized'], this._pendingEventWatchers);
await sendEvents(['mousemove']);
+ this._lastTrackedPos = { x, y };
// The order of events after 'mousemove' is sent:
// 1. [dragstart] - might or might NOT be emitted
diff --git a/patches/chromeutil.patch b/patches/chromeutil.patch
index b689e49..5ab6fd6 100644
--- a/patches/chromeutil.patch
+++ b/patches/chromeutil.patch
@@ -1,16 +1,17 @@
diff --git a/dom/base/ChromeUtils.cpp b/dom/base/ChromeUtils.cpp
-index 6833d2227f..0b2ea99615 100644
+index 6833d2227f..9f88bd3d34 100644
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
-@@ -5,6 +5,7 @@
+@@ -5,6 +5,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ChromeUtils.h"
+#include "MaskConfig.hpp"
++#include "MouseTrajectories.hpp"
#include "JSOracleParent.h"
#include "js/CallAndConstruct.h" // JS::Call
-@@ -2068,6 +2069,24 @@ bool ChromeUtils::IsDarkBackground(GlobalObject&, Element& aElement) {
+@@ -2068,6 +2070,24 @@ bool ChromeUtils::IsDarkBackground(GlobalObject&, Element& aElement) {
return nsNativeTheme::IsDarkBackground(f);
}
@@ -35,7 +36,7 @@ index 6833d2227f..0b2ea99615 100644
double ChromeUtils::DateNow(GlobalObject&) { return JS_Now() / 1000.0; }
/* static */
-@@ -2094,6 +2113,39 @@ void ChromeUtils::GetAllPossibleUtilityActorNames(GlobalObject& aGlobal,
+@@ -2094,6 +2114,62 @@ void ChromeUtils::GetAllPossibleUtilityActorNames(GlobalObject& aGlobal,
}
}
@@ -61,6 +62,17 @@ index 6833d2227f..0b2ea99615 100644
+}
+
+/* static */
++bool ChromeUtils::CamouGetBool(GlobalObject& aGlobal,
++ const nsAString& aVarName,
++ bool aDefaultValue) {
++ NS_ConvertUTF16toUTF8 utf8VarName(aVarName);
++ if (auto value = MaskConfig::GetBool(utf8VarName.get())) {
++ return value.value();
++ }
++ return aDefaultValue;
++}
++
++/* static */
+void ChromeUtils::CamouGetString(GlobalObject& aGlobal,
+ const nsAString& aVarName,
+ nsAString& aRetVal) {
@@ -71,12 +83,24 @@ index 6833d2227f..0b2ea99615 100644
+ aRetVal.Truncate();
+ }
+}
++
++/* static */
++void ChromeUtils::CamouGetMouseTrajectory(GlobalObject& aGlobal, long aFromX,
++ long aFromY, long aToX, long aToY,
++ nsTArray& aPoints) {
++ HumanizeMouseTrajectory trajectory(std::make_pair(aFromX, aFromY),
++ std::make_pair(aToX, aToY));
++ std::vector flattenedPoints = trajectory.getPoints();
++
++ aPoints.Clear();
++ aPoints.AppendElements(flattenedPoints.data(), flattenedPoints.size());
++}
+
/* static */
bool ChromeUtils::ShouldResistFingerprinting(
GlobalObject& aGlobal, JSRFPTarget aTarget,
diff --git a/dom/base/ChromeUtils.h b/dom/base/ChromeUtils.h
-index 0150c59670..e3d203ed1a 100644
+index 0150c59670..3a244a80e4 100644
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -301,6 +301,10 @@ class ChromeUtils {
@@ -90,7 +114,7 @@ index 0150c59670..e3d203ed1a 100644
static double DateNow(GlobalObject&);
static void EnsureJSOracleStarted(GlobalObject&);
-@@ -310,6 +314,14 @@ class ChromeUtils {
+@@ -310,6 +314,21 @@ class ChromeUtils {
static void GetAllPossibleUtilityActorNames(GlobalObject& aGlobal,
nsTArray& aNames);
@@ -99,14 +123,21 @@ index 0150c59670..e3d203ed1a 100644
+ static double CamouGetDouble(GlobalObject& aGlobal, const nsAString& aVarName,
+ double aDefaultValue);
+
++ static bool CamouGetBool(GlobalObject& aGlobal, const nsAString& aVarName,
++ bool aDefaultValue);
++
+ static void CamouGetString(GlobalObject& aGlobal, const nsAString& aVarName,
+ nsAString& aRetVal);
++
++ static void CamouGetMouseTrajectory(GlobalObject& aGlobal, long aFromX,
++ long aFromY, long aToX, long aToY,
++ nsTArray& aPoints);
+
static bool ShouldResistFingerprinting(
GlobalObject& aGlobal, JSRFPTarget aTarget,
const Nullable& aOverriddenFingerprintingSettings);
diff --git a/dom/chrome-webidl/ChromeUtils.webidl b/dom/chrome-webidl/ChromeUtils.webidl
-index bf196f039d..04e0cdcabd 100644
+index bf196f039d..e134d816b3 100644
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -746,6 +746,13 @@ partial namespace ChromeUtils {
@@ -123,7 +154,7 @@ index bf196f039d..04e0cdcabd 100644
/**
* Starts the JSOracle process for ORB JavaScript validation, if it hasn't started already.
*/
-@@ -757,6 +764,21 @@ partial namespace ChromeUtils {
+@@ -757,6 +764,31 @@ partial namespace ChromeUtils {
[ChromeOnly]
readonly attribute unsigned long aliveUtilityProcesses;
@@ -138,9 +169,19 @@ index bf196f039d..04e0cdcabd 100644
+ double camouGetDouble(DOMString varName, double defaultValue);
+
+ /**
++ * Get a bool value from Camoufox MaskConfig.
++ */
++ boolean camouGetBool(DOMString varName, boolean defaultValue);
++
++ /**
+ * Get a string value from Camoufox MaskConfig.
+ */
+ DOMString camouGetString(DOMString varName);
++
++ /**
++ * Calculate a human-like mouse trajectory between two points.
++ */
++ sequence camouGetMouseTrajectory(long fromX, long fromY, long toX, long toY);
+
/**
* Get a list of all possible Utility process Actor Names ; mostly useful to
diff --git a/patches/cursor-highlighter.patch b/patches/cursor-highlighter.patch
new file mode 100644
index 0000000..de761d7
--- /dev/null
+++ b/patches/cursor-highlighter.patch
@@ -0,0 +1,36 @@
+diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js
+index 63c9c39741..cc7fa914c0 100644
+--- a/browser/base/content/browser-init.js
++++ b/browser/base/content/browser-init.js
+@@ -292,6 +292,31 @@ var gBrowserInit = {
+ }
+ }
+
++ if (ChromeUtils.camouGetBool("showcursor", true)) {
++ let cursorFollower = document.createElement("div");
++ cursorFollower.id = "cursor-highlighter";
++ cursorFollower.style.cssText = `
++ position: fixed;
++ width: 10px;
++ height: 10px;
++ background-color: rgba(255,105,105,0.8);
++ border-radius: 50%;
++ pointer-events: none;
++ z-index: 2147483647;
++ transform: translate(-50%, -50%);
++ box-shadow:
++ 0 0 0 5px rgba(255,105,105,0.5),
++ 0 0 0 10px rgba(255,105,105,0.3),
++ 0 0 0 15px rgba(255,105,105,0.1);
++ `;
++ document.documentElement.appendChild(cursorFollower);
++
++ window.addEventListener('mousemove', e => {
++ cursorFollower.style.left = `${e.clientX}px`;
++ cursorFollower.style.top = `${e.clientY}px`;
++ });
++ }
++
+ // 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);
diff --git a/settings/properties.json b/settings/properties.json
index 3bfefe7..2982a6d 100644
--- a/settings/properties.json
+++ b/settings/properties.json
@@ -61,5 +61,8 @@
{ "property": "timezone", "type": "str" },
{ "property": "locale:language", "type": "str" },
{ "property": "locale:region", "type": "str" },
- { "property": "locale:script", "type": "str" }
+ { "property": "locale:script", "type": "str" },
+ { "property": "humanize", "type": "bool" },
+ { "property": "humanize:maxTime", "type": "double" },
+ { "property": "showcursor", "type": "bool" }
]