generated from kgod/ai-review-template
提交
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from playwright_stealth.stealth import Stealth, ALL_EVASIONS_DISABLED_KWARGS
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
|
||||
# straight from: https://github.com/kennethreitz/requests/blob/master/src/requests/structures.py
|
||||
class CaseInsensitiveDict(MutableMapping):
|
||||
"""A case-insensitive ``dict``-like object.
|
||||
|
||||
Implements all methods and operations of
|
||||
``MutableMapping`` as well as dict's ``copy``. Also
|
||||
provides ``lower_items``.
|
||||
|
||||
All keys are expected to be strings. The structure remembers the
|
||||
case of the last key to be set, and ``iter(instance)``,
|
||||
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
|
||||
will contain case-sensitive keys. However, querying and contains
|
||||
testing is case insensitive::
|
||||
|
||||
cid = CaseInsensitiveDict()
|
||||
cid['Accept'] = 'application/json'
|
||||
cid['aCCEPT'] == 'application/json' # True
|
||||
list(cid) == ['Accept'] # True
|
||||
|
||||
For example, ``headers['content-encoding']`` will return the
|
||||
value of a ``'Content-Encoding'`` response header, regardless
|
||||
of how the header name was originally stored.
|
||||
|
||||
If the constructor, ``.update``, or equality comparison
|
||||
operations are given keys that have equal ``.lower()``s, the
|
||||
behavior is undefined.
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, **kwargs):
|
||||
self._store = {}
|
||||
if data is None:
|
||||
data = {}
|
||||
self.update(data, **kwargs)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# Use the lowercased key for lookups, but store the actual
|
||||
# key alongside the value.
|
||||
self._store[key.lower()] = (key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._store[key.lower()][1]
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._store[key.lower()]
|
||||
|
||||
def __iter__(self):
|
||||
return (casedkey for casedkey, mappedvalue in self._store.values())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._store)
|
||||
|
||||
def lower_items(self):
|
||||
"""Like iteritems(), but with all lowercase keys."""
|
||||
return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
|
||||
|
||||
def __eq__(self, other):
|
||||
from collections.abc import Mapping
|
||||
|
||||
if isinstance(other, Mapping):
|
||||
other = CaseInsensitiveDict(other)
|
||||
else:
|
||||
return NotImplemented
|
||||
# Compare insensitively
|
||||
return dict(self.lower_items()) == dict(other.lower_items())
|
||||
|
||||
# Copy is required
|
||||
def copy(self):
|
||||
return CaseInsensitiveDict(self._store.values())
|
||||
|
||||
def __repr__(self):
|
||||
return str(dict(self.items()))
|
||||
|
||||
def items(self):
|
||||
pass
|
||||
@@ -0,0 +1,43 @@
|
||||
from playwright import async_api, sync_api
|
||||
|
||||
|
||||
class AsyncWrappingContextManager:
|
||||
def __init__(self, stealth: "Stealth", manager: async_api.PlaywrightContextManager):
|
||||
if isinstance(manager, sync_api.PlaywrightContextManager):
|
||||
raise TypeError("You need to call 'use_sync' instead of 'use_async' for a sync Playwright context")
|
||||
self.stealth = stealth
|
||||
self.manager = manager
|
||||
|
||||
async def __aenter__(
|
||||
self,
|
||||
) -> async_api.Playwright:
|
||||
context = await self.manager.__aenter__()
|
||||
self.stealth.hook_playwright_context(context)
|
||||
return context
|
||||
|
||||
async def start(self):
|
||||
return await self.__aenter__()
|
||||
|
||||
async def __aexit__(self, *args) -> None:
|
||||
await self.manager.__aexit__(*args)
|
||||
|
||||
|
||||
class SyncWrappingContextManager:
|
||||
def __init__(self, stealth: "Stealth", manager: sync_api.PlaywrightContextManager):
|
||||
if isinstance(manager, async_api.PlaywrightContextManager):
|
||||
raise TypeError("You need to call 'use_async' instead of 'use_sync' for an async Playwright context")
|
||||
self.stealth = stealth
|
||||
self.manager = manager
|
||||
|
||||
def __enter__(
|
||||
self,
|
||||
) -> sync_api.Playwright:
|
||||
context = self.manager.__enter__()
|
||||
self.stealth.hook_playwright_context(context)
|
||||
return context
|
||||
|
||||
def start(self):
|
||||
return self.__enter__()
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
self.manager.__exit__(*args)
|
||||
@@ -0,0 +1,68 @@
|
||||
log("loading chrome.app.js");
|
||||
|
||||
if (!window.chrome) {
|
||||
// Use the exact property descriptor found in headful Chrome
|
||||
// fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
|
||||
Object.defineProperty(window, "chrome", {
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: false, // note!
|
||||
value: {} // We'll extend that later
|
||||
});
|
||||
}
|
||||
|
||||
// app in window.chrome means we're running headful and don't need to mock anything
|
||||
if (!("app" in window.chrome)) {
|
||||
const makeError = {
|
||||
ErrorInInvocation: (fn) => {
|
||||
const err = new TypeError(`Error in invocation of app.${fn}()`);
|
||||
return utils.stripErrorWithAnchor(err, `at ${fn} (eval at <anonymous>`);
|
||||
}
|
||||
};
|
||||
|
||||
const APP_STATIC_DATA = JSON.parse(
|
||||
`
|
||||
{
|
||||
"isInstalled": false,
|
||||
"InstallState": {
|
||||
"DISABLED": "disabled",
|
||||
"INSTALLED": "installed",
|
||||
"NOT_INSTALLED": "not_installed"
|
||||
},
|
||||
"RunningState": {
|
||||
"CANNOT_RUN": "cannot_run",
|
||||
"READY_TO_RUN": "ready_to_run",
|
||||
"RUNNING": "running"
|
||||
}
|
||||
}
|
||||
`.trim()
|
||||
);
|
||||
|
||||
window.chrome.app = {
|
||||
...APP_STATIC_DATA,
|
||||
|
||||
get isInstalled() {
|
||||
return false;
|
||||
},
|
||||
|
||||
getDetails: function getDetails() {
|
||||
if (arguments.length) {
|
||||
throw makeError.ErrorInInvocation(`getDetails`);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getIsInstalled: function getDetails() {
|
||||
if (arguments.length) {
|
||||
throw makeError.ErrorInInvocation(`getIsInstalled`);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
runningState: function getDetails() {
|
||||
if (arguments.length) {
|
||||
throw makeError.ErrorInInvocation(`runningState`);
|
||||
}
|
||||
return "cannot_run";
|
||||
}
|
||||
};
|
||||
utils.patchToStringNested(window.chrome.app);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
log("loading chrome.csi.js");
|
||||
|
||||
if (!window.chrome) {
|
||||
// Use the exact property descriptor found in headful Chrome
|
||||
// fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
|
||||
Object.defineProperty(window, "chrome", {
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: false, // note!
|
||||
value: {} // We'll extend that later
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we're running headful and don't need to mock anything
|
||||
// Check that the Navigation Timing API v1 is available, we need that
|
||||
if (!("csi" in window.chrome) && window.performance?.timing) {
|
||||
const { csi_timing } = window.performance;
|
||||
|
||||
log("loading chrome.csi.js");
|
||||
window.chrome.csi = function() {
|
||||
return {
|
||||
onloadT: csi_timing?.domContentLoadedEventEnd,
|
||||
startE: csi_timing?.navigationStart,
|
||||
pageT: Date.now() - csi_timing?.navigationStart,
|
||||
tran: 15 // transition? seems constant
|
||||
};
|
||||
};
|
||||
utils.patchToString(window.chrome.csi);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
log("loading chrome.hairline.js");
|
||||
// inspired by: https://intoli.com/blog/making-chrome-headless-undetectable/
|
||||
const elementDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype,
|
||||
"offsetHeight");
|
||||
|
||||
utils.replaceProperty(HTMLDivElement.prototype, "offsetHeight", {
|
||||
get: function() {
|
||||
// hmmm not sure about this
|
||||
if (this.id === "modernizr") {
|
||||
return 1;
|
||||
}
|
||||
return elementDescriptor.get.apply(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
log("loading chrome.load.times.js");
|
||||
|
||||
if (!window.chrome) {
|
||||
// Use the exact property descriptor found in headful Chrome
|
||||
// fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
|
||||
Object.defineProperty(window, "chrome", {
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: false, // note!
|
||||
value: {} // We'll extend that later
|
||||
});
|
||||
}
|
||||
|
||||
// That means we're running headful and don't need to mock anything
|
||||
if ("loadTimes" in window.chrome) {
|
||||
warn("skipping chrome loadtimes update, running in headful mode");
|
||||
}
|
||||
|
||||
// Check that the Navigation Timing API v1 + v2 is available, we need that
|
||||
if (window.performance?.timing || window.PerformancePaintTiming) {
|
||||
const { performance } = window;
|
||||
|
||||
// Some stuff is not available on about:blank as it requires a navigation to occur,
|
||||
// let's harden the code to not fail then:
|
||||
const ntEntryFallback = {
|
||||
nextHopProtocol: "h2",
|
||||
type: "other"
|
||||
};
|
||||
|
||||
// The API exposes some funky info regarding the connection
|
||||
const protocolInfo = {
|
||||
get connectionInfo() {
|
||||
const ntEntry = performance.getEntriesByType("navigation")[0] ||
|
||||
ntEntryFallback;
|
||||
return ntEntry.nextHopProtocol;
|
||||
},
|
||||
get npnNegotiatedProtocol() {
|
||||
// NPN is deprecated in favor of ALPN, but this implementation returns the
|
||||
// HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.
|
||||
const ntEntry = performance.getEntriesByType("navigation")[0] ||
|
||||
ntEntryFallback;
|
||||
return ["h2", "hq"].includes(ntEntry.nextHopProtocol) ?
|
||||
ntEntry.nextHopProtocol :
|
||||
"unknown";
|
||||
},
|
||||
get navigationType() {
|
||||
const ntEntry = performance.getEntriesByType("navigation")[0] ||
|
||||
ntEntryFallback;
|
||||
return ntEntry.type;
|
||||
},
|
||||
get wasAlternateProtocolAvailable() {
|
||||
// The Alternate-Protocol header is deprecated in favor of Alt-Svc
|
||||
// (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this
|
||||
// should always return false.
|
||||
return false;
|
||||
},
|
||||
get wasFetchedViaSpdy() {
|
||||
// SPDY is deprecated in favor of HTTP/2, but this implementation returns
|
||||
// true for HTTP/2 or HTTP2+QUIC/39 as well.
|
||||
const ntEntry = performance.getEntriesByType("navigation")[0] ||
|
||||
ntEntryFallback;
|
||||
return ["h2", "hq"].includes(ntEntry.nextHopProtocol);
|
||||
},
|
||||
get wasNpnNegotiated() {
|
||||
// NPN is deprecated in favor of ALPN, but this implementation returns true
|
||||
// for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.
|
||||
const ntEntry = performance.getEntriesByType("navigation")[0] ||
|
||||
ntEntryFallback;
|
||||
return ["h2", "hq"].includes(ntEntry.nextHopProtocol);
|
||||
}
|
||||
};
|
||||
|
||||
const { timing } = window.performance;
|
||||
|
||||
// Truncate number to specific number of decimals, most of the `loadTimes` stuff has 3
|
||||
function toFixed(num, fixed) {
|
||||
const re = new RegExp("^-?\\d+(?:.\\d{0," + (fixed || -1) + "})?");
|
||||
return num.toString().match(re)[0];
|
||||
}
|
||||
|
||||
const timingInfo = {
|
||||
get firstPaintAfterLoadTime() {
|
||||
// This was never actually implemented and always returns 0.
|
||||
return 0;
|
||||
},
|
||||
get requestTime() {
|
||||
return timing.navigationStart / 1000;
|
||||
},
|
||||
get startLoadTime() {
|
||||
return timing.navigationStart / 1000;
|
||||
},
|
||||
get commitLoadTime() {
|
||||
return timing.responseStart / 1000;
|
||||
},
|
||||
get finishDocumentLoadTime() {
|
||||
return timing.domContentLoadedEventEnd / 1000;
|
||||
},
|
||||
get finishLoadTime() {
|
||||
return timing.loadEventEnd / 1000;
|
||||
},
|
||||
get firstPaintTime() {
|
||||
const fpEntry = performance.getEntriesByType("paint")[0] || {
|
||||
startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occured (`about:blank`)
|
||||
};
|
||||
return toFixed((fpEntry.startTime + performance.timeOrigin) / 1000, 3);
|
||||
}
|
||||
};
|
||||
|
||||
window.chrome.loadTimes = function() {
|
||||
return {
|
||||
...protocolInfo,
|
||||
...timingInfo
|
||||
};
|
||||
};
|
||||
utils.patchToString(window.chrome.loadTimes);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
log("loading chrome.runtime.js");
|
||||
|
||||
const STATIC_DATA = {
|
||||
OnInstalledReason: {
|
||||
CHROME_UPDATE: "chrome_update",
|
||||
INSTALL: "install",
|
||||
SHARED_MODULE_UPDATE: "shared_module_update",
|
||||
UPDATE: "update"
|
||||
},
|
||||
OnRestartRequiredReason: {
|
||||
APP_UPDATE: "app_update",
|
||||
OS_UPDATE: "os_update",
|
||||
PERIODIC: "periodic"
|
||||
},
|
||||
PlatformArch: {
|
||||
ARM: "arm",
|
||||
ARM64: "arm64",
|
||||
MIPS: "mips",
|
||||
MIPS64: "mips64",
|
||||
X86_32: "x86-32",
|
||||
X86_64: "x86-64"
|
||||
},
|
||||
PlatformNaclArch: {
|
||||
ARM: "arm",
|
||||
MIPS: "mips",
|
||||
MIPS64: "mips64",
|
||||
X86_32: "x86-32",
|
||||
X86_64: "x86-64"
|
||||
},
|
||||
PlatformOs: {
|
||||
ANDROID: "android",
|
||||
CROS: "cros",
|
||||
LINUX: "linux",
|
||||
MAC: "mac",
|
||||
OPENBSD: "openbsd",
|
||||
WIN: "win"
|
||||
},
|
||||
RequestUpdateCheckStatus: {
|
||||
NO_UPDATE: "no_update",
|
||||
THROTTLED: "throttled",
|
||||
UPDATE_AVAILABLE: "update_available"
|
||||
}
|
||||
};
|
||||
|
||||
if (!window.chrome) {
|
||||
// Use the exact property descriptor found in headful Chrome
|
||||
// fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
|
||||
Object.defineProperty(window, "chrome", {
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: false, // note!
|
||||
value: {} // We'll extend that later
|
||||
});
|
||||
}
|
||||
|
||||
// That means we're running headful and don't need to mock anything
|
||||
const existsAlready = "runtime" in window.chrome;
|
||||
// `chrome.runtime` is only exposed on secure origins
|
||||
const isNotSecure = !window.location.protocol.startsWith("https");
|
||||
if (!existsAlready && !isNotSecure) {
|
||||
window.chrome.runtime = {
|
||||
// There's a bunch of static data in that property which doesn't seem to change,
|
||||
// we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)`
|
||||
...STATIC_DATA,
|
||||
// `chrome.runtime.id` is extension related and returns undefined in Chrome
|
||||
get id() {
|
||||
return undefined;
|
||||
},
|
||||
// These two require more sophisticated mocks
|
||||
connect: null,
|
||||
sendMessage: null
|
||||
};
|
||||
|
||||
const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({
|
||||
NoMatchingSignature: new TypeError(preamble + `No matching signature.`),
|
||||
MustSpecifyExtensionID: new TypeError(
|
||||
preamble +
|
||||
`${method} called from a webpage must specify an Extension ID (string) for its first argument.`
|
||||
),
|
||||
InvalidExtensionID: new TypeError(
|
||||
preamble + `Invalid extension id: '${extensionId}'`)
|
||||
});
|
||||
|
||||
// Valid Extension IDs are 32 characters in length and use the letter `a` to `p`:
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90
|
||||
const isValidExtensionID = (str) => str.length === 32 &&
|
||||
str.toLowerCase().match(/^[a-p]+$/);
|
||||
|
||||
/** Mock `chrome.runtime.sendMessage` */
|
||||
const sendMessageHandler = {
|
||||
apply: function(target, ctx, args) {
|
||||
const [extensionId, options, responseCallback] = args || [];
|
||||
|
||||
// Define custom errors
|
||||
const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): `;
|
||||
const Errors = makeCustomRuntimeErrors(errorPreamble,
|
||||
`chrome.runtime.sendMessage()`, extensionId);
|
||||
|
||||
// Check if the call signature looks ok
|
||||
const noArguments = args.length === 0;
|
||||
const tooManyArguments = args.length > 4;
|
||||
const incorrectOptions = options && typeof options !== "object";
|
||||
const incorrectResponseCallback = responseCallback &&
|
||||
typeof responseCallback !== "function";
|
||||
if (noArguments || tooManyArguments || incorrectOptions ||
|
||||
incorrectResponseCallback) {
|
||||
throw Errors.NoMatchingSignature;
|
||||
}
|
||||
|
||||
// At least 2 arguments are required before we even validate the extension ID
|
||||
if (args.length < 2) {
|
||||
throw Errors.MustSpecifyExtensionID;
|
||||
}
|
||||
|
||||
// Now let's make sure we got a string as extension ID
|
||||
if (typeof extensionId !== "string") {
|
||||
throw Errors.NoMatchingSignature;
|
||||
}
|
||||
|
||||
if (!isValidExtensionID(extensionId)) {
|
||||
throw Errors.InvalidExtensionID;
|
||||
}
|
||||
|
||||
return undefined; // Normal behavior
|
||||
}
|
||||
};
|
||||
utils.mockWithProxy(window.chrome.runtime, "sendMessage",
|
||||
function sendMessage() {
|
||||
}, sendMessageHandler);
|
||||
|
||||
/**
|
||||
* Mock `chrome.runtime.connect`
|
||||
*
|
||||
* @see https://developer.chrome.com/apps/runtime#method-connect
|
||||
*/
|
||||
const connectHandler = {
|
||||
apply: function(target, ctx, args) {
|
||||
const [extensionId, connectInfo] = args || [];
|
||||
|
||||
// Define custom errors
|
||||
const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): `;
|
||||
const Errors = makeCustomRuntimeErrors(errorPreamble,
|
||||
`chrome.runtime.connect()`, extensionId);
|
||||
|
||||
// Behavior differs a bit from sendMessage:
|
||||
const noArguments = args.length === 0;
|
||||
const emptyStringArgument = args.length === 1 && extensionId === "";
|
||||
if (noArguments || emptyStringArgument) {
|
||||
throw Errors.MustSpecifyExtensionID;
|
||||
}
|
||||
|
||||
const tooManyArguments = args.length > 2;
|
||||
const incorrectConnectInfoType = connectInfo && typeof connectInfo !==
|
||||
"object";
|
||||
|
||||
if (tooManyArguments || incorrectConnectInfoType) {
|
||||
throw Errors.NoMatchingSignature;
|
||||
}
|
||||
|
||||
const extensionIdIsString = typeof extensionId === "string";
|
||||
if (extensionIdIsString && extensionId === "") {
|
||||
throw Errors.MustSpecifyExtensionID;
|
||||
}
|
||||
if (extensionIdIsString && !isValidExtensionID(extensionId)) {
|
||||
throw Errors.InvalidExtensionID;
|
||||
}
|
||||
|
||||
// There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate
|
||||
const validateConnectInfo = (ci) => {
|
||||
// More than a first param connectInfo as been provided
|
||||
if (args.length > 1) {
|
||||
throw Errors.NoMatchingSignature;
|
||||
}
|
||||
// An empty connectInfo has been provided
|
||||
if (Object.keys(ci).length === 0) {
|
||||
throw Errors.MustSpecifyExtensionID;
|
||||
}
|
||||
// Loop over all connectInfo props an check them
|
||||
Object.entries(ci).forEach(([k, v]) => {
|
||||
const isExpected = ["name", "includeTlsChannelId"].includes(k);
|
||||
if (!isExpected) {
|
||||
throw new TypeError(errorPreamble + `Unexpected property: '${k}'.`);
|
||||
}
|
||||
const MismatchError = (propName, expected, found) =>
|
||||
TypeError(
|
||||
errorPreamble +
|
||||
`Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.`
|
||||
);
|
||||
if (k === "name" && typeof v !== "string") {
|
||||
throw MismatchError(k, "string", typeof v);
|
||||
}
|
||||
if (k === "includeTlsChannelId" && typeof v !== "boolean") {
|
||||
throw MismatchError(k, "boolean", typeof v);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (typeof extensionId === "object") {
|
||||
validateConnectInfo(extensionId);
|
||||
throw Errors.MustSpecifyExtensionID;
|
||||
}
|
||||
|
||||
// Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well
|
||||
return utils.patchToStringNested(makeConnectResponse());
|
||||
}
|
||||
};
|
||||
utils.mockWithProxy(window.chrome.runtime, "connect", function connect() {
|
||||
}, connectHandler);
|
||||
|
||||
function makeConnectResponse() {
|
||||
const onSomething = () => ({
|
||||
addListener: function addListener() {
|
||||
},
|
||||
dispatch: function dispatch() {
|
||||
},
|
||||
hasListener: function hasListener() {
|
||||
},
|
||||
hasListeners: function hasListeners() {
|
||||
return false;
|
||||
},
|
||||
removeListener: function removeListener() {
|
||||
}
|
||||
});
|
||||
|
||||
const response = {
|
||||
name: "",
|
||||
sender: undefined,
|
||||
disconnect: function disconnect() {
|
||||
},
|
||||
onDisconnect: onSomething(),
|
||||
onMessage: onSomething(),
|
||||
postMessage: function postMessage() {
|
||||
if (!arguments.length) {
|
||||
throw new TypeError(`Insufficient number of arguments.`);
|
||||
}
|
||||
throw new Error(`Attempting to use a disconnected port object`);
|
||||
}
|
||||
};
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
log("loading error.prototype.js");
|
||||
|
||||
Object.defineProperty(Error.prototype, "name", {configurable: false, enumerable: false})
|
||||
@@ -0,0 +1,95 @@
|
||||
log("loading iframe.contentWindow.js");
|
||||
|
||||
try {
|
||||
// Adds a contentWindow proxy to the provided iframe element
|
||||
const addContentWindowProxy = (iframe) => {
|
||||
const contentWindowProxy = {
|
||||
get(target, key) {
|
||||
// Now to the interesting part:
|
||||
// We actually make this thing behave like a regular iframe window,
|
||||
// by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)
|
||||
// That makes it possible for these assertions to be correct:
|
||||
// iframe.contentWindow.self === window.top // must be false
|
||||
if (key === "self") {
|
||||
return this;
|
||||
}
|
||||
// iframe.contentWindow.frameElement === iframe // must be true
|
||||
if (key === "frameElement") {
|
||||
return iframe;
|
||||
}
|
||||
return Reflect.get(target, key);
|
||||
}
|
||||
};
|
||||
|
||||
if (!iframe.contentWindow) {
|
||||
const proxy = new Proxy(window, contentWindowProxy);
|
||||
Object.defineProperty(iframe, "contentWindow", {
|
||||
get() {
|
||||
return proxy;
|
||||
},
|
||||
set(newValue) {
|
||||
return newValue; // contentWindow is immutable
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handles iframe element creation, augments `srcdoc` property so we can intercept further
|
||||
const handleIframeCreation = (target, thisArg, args) => {
|
||||
const iframe = target.apply(thisArg, args);
|
||||
|
||||
// We need to keep the originals around
|
||||
const _iframe = iframe;
|
||||
const _srcdoc = _iframe.srcdoc;
|
||||
|
||||
// Add hook for the srcdoc property
|
||||
// We need to be very surgical here to not break other iframes by accident
|
||||
Object.defineProperty(iframe, "srcdoc", {
|
||||
configurable: true, // Important, so we can reset this later
|
||||
get: function() {
|
||||
return _iframe.srcdoc;
|
||||
},
|
||||
set: function(newValue) {
|
||||
addContentWindowProxy(this);
|
||||
// Reset property, the hook is only needed once
|
||||
Object.defineProperty(iframe, "srcdoc", {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: _srcdoc
|
||||
});
|
||||
_iframe.srcdoc = newValue;
|
||||
}
|
||||
});
|
||||
return iframe;
|
||||
};
|
||||
|
||||
// Adds a hook to intercept iframe creation events
|
||||
const addIframeCreationSniffer = () => {
|
||||
/* global document */
|
||||
const createElementHandler = {
|
||||
// Make toString() native
|
||||
get(target, key) {
|
||||
return Reflect.get(target, key);
|
||||
},
|
||||
apply: function(target, thisArg, args) {
|
||||
const isIframe = args && args.length && `${args[0]}`.toLowerCase() ===
|
||||
"iframe";
|
||||
if (!isIframe) {
|
||||
// Everything as usual
|
||||
return target.apply(thisArg, args);
|
||||
} else {
|
||||
return handleIframeCreation(target, thisArg, args);
|
||||
}
|
||||
}
|
||||
};
|
||||
// All this just due to iframes with srcdoc bug
|
||||
utils.replaceWithProxy(document, "createElement", createElementHandler);
|
||||
};
|
||||
|
||||
// Let's go
|
||||
addIframeCreationSniffer();
|
||||
} catch (err) {
|
||||
// console.warn(err)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
log("loading media.codec.js");
|
||||
/**
|
||||
* Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.
|
||||
*
|
||||
* @example
|
||||
* video/webm; codecs="vp8, vorbis"
|
||||
* video/mp4; codecs="avc1.42E01E"
|
||||
* audio/x-m4a;
|
||||
* audio/ogg; codecs="vorbis"
|
||||
* @param {String} arg
|
||||
*/
|
||||
const parseInput = (arg) => {
|
||||
const [mime, codecStr] = arg.trim().split(";");
|
||||
let codecs = [];
|
||||
if (codecStr && codecStr.includes("codecs=\"")) {
|
||||
codecs = codecStr.trim().
|
||||
replace(`codecs="`, "").
|
||||
replace(`"`, "").
|
||||
trim().
|
||||
split(",").
|
||||
filter((x) => !!x).
|
||||
map((x) => x.trim());
|
||||
}
|
||||
return {
|
||||
mime,
|
||||
codecStr,
|
||||
codecs
|
||||
};
|
||||
};
|
||||
|
||||
const canPlayType = {
|
||||
// Intercept certain requests
|
||||
apply: function(target, ctx, args) {
|
||||
if (!args || !args.length) {
|
||||
return target.apply(ctx, args);
|
||||
}
|
||||
const { mime, codecs } = parseInput(args[0]);
|
||||
// This specific mp4 codec is missing in Chromium
|
||||
if (mime === "video/mp4") {
|
||||
if (codecs.includes("avc1.42E01E")) {
|
||||
return "probably";
|
||||
}
|
||||
}
|
||||
// This mimetype is only supported if no codecs are specified
|
||||
if (mime === "audio/x-m4a" && !codecs.length) {
|
||||
return "maybe";
|
||||
}
|
||||
|
||||
// This mimetype is only supported if no codecs are specified
|
||||
if (mime === "audio/aac" && !codecs.length) {
|
||||
return "probably";
|
||||
}
|
||||
// Everything else as usual
|
||||
return target.apply(ctx, args);
|
||||
}
|
||||
};
|
||||
|
||||
/* global HTMLMediaElement */
|
||||
utils.replaceWithProxy(HTMLMediaElement.prototype, "canPlayType", canPlayType);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
log("loading navigator.hardwareConcurrency");
|
||||
|
||||
utils.replaceProperty(Object.getPrototypeOf(navigator), "hardwareConcurrency", {
|
||||
get() {
|
||||
return 4;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
log("loading navigator.languages.js");
|
||||
log(navigator.languages, opts.navigator_languages_override);
|
||||
if (utils.arrayEqual(navigator.languages, opts.navigator_languages_override)) {
|
||||
log("not patching navigator.languages, assuming CLI args were used instead");
|
||||
} else {
|
||||
utils.replaceProperty(Object.getPrototypeOf(navigator), "languages", {
|
||||
get: () => opts.navigator_languages_override
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
log("loading navigator.permissions.js");
|
||||
|
||||
const handler = {
|
||||
apply: function(target, ctx, args) {
|
||||
const param = (args || [])[0];
|
||||
|
||||
if (param && param.name && param.name === "notifications") {
|
||||
const result = { state: Notification.permission };
|
||||
Object.setPrototypeOf(result, PermissionStatus.prototype);
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
return utils.cache.Reflect.apply(...arguments);
|
||||
}
|
||||
};
|
||||
|
||||
utils.replaceWithProxy(
|
||||
window.navigator.permissions.__proto__, // eslint-disable-line no-proto
|
||||
"query",
|
||||
handler
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
log(`loading navigator.platform.js with opt: ${opts.navigator_platform}`);
|
||||
|
||||
if (opts.navigator_platform && navigator.platform !== opts.navigator_platform) {
|
||||
utils.replaceProperty(Object.getPrototypeOf(navigator), "platform", {
|
||||
get: () => opts.navigator_platform
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
log("loading navigator.plugins.js");
|
||||
|
||||
const data = {
|
||||
mimeTypes: [
|
||||
{
|
||||
type: "application/pdf",
|
||||
suffixes: "pdf",
|
||||
description: "",
|
||||
__pluginName: "Chrome PDF Viewer"
|
||||
},
|
||||
{
|
||||
type: "application/x-google-chrome-pdf",
|
||||
suffixes: "pdf",
|
||||
description: "Portable Document Format",
|
||||
__pluginName: "Chrome PDF Plugin"
|
||||
},
|
||||
{
|
||||
type: "application/x-nacl",
|
||||
suffixes: "",
|
||||
description: "Native Client Executable",
|
||||
__pluginName: "Native Client"
|
||||
},
|
||||
{
|
||||
type: "application/x-pnacl",
|
||||
suffixes: "",
|
||||
description: "Portable Native Client Executable",
|
||||
__pluginName: "Native Client"
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
name: "Chrome PDF Plugin",
|
||||
filename: "internal-pdf-viewer",
|
||||
description: "Portable Document Format",
|
||||
__mimeTypes: ["application/x-google-chrome-pdf"]
|
||||
},
|
||||
{
|
||||
name: "Chrome PDF Viewer",
|
||||
filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai",
|
||||
description: "",
|
||||
__mimeTypes: ["application/pdf"]
|
||||
},
|
||||
{
|
||||
name: "Native Client",
|
||||
filename: "internal-nacl-plugin",
|
||||
description: "",
|
||||
__mimeTypes: ["application/x-nacl", "application/x-pnacl"]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// That means we're running headful
|
||||
const hasPlugins = "plugins" in navigator && navigator.plugins.length;
|
||||
if (!hasPlugins) {
|
||||
const mimeTypes = generateMagicArray(data.mimeTypes, MimeTypeArray.prototype,
|
||||
MimeType.prototype, "type");
|
||||
const plugins = generateMagicArray(data.plugins, PluginArray.prototype,
|
||||
Plugin.prototype, "name");
|
||||
|
||||
// Plugin and MimeType cross-reference each other, let's do that now
|
||||
// Note: We're looping through `data.plugins` here, not the generated `plugins`
|
||||
for (const pluginData of data.plugins) {
|
||||
pluginData.__mimeTypes.forEach((type, index) => {
|
||||
plugins[pluginData.name][index] = mimeTypes[type];
|
||||
plugins[type] = mimeTypes[type];
|
||||
Object.defineProperty(mimeTypes[type], "enabledPlugin", {
|
||||
value: JSON.parse(JSON.stringify(plugins[pluginData.name])),
|
||||
writable: false,
|
||||
enumerable: false, // Important: `JSON.stringify(navigator.plugins)`
|
||||
configurable: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const patchNavigator = (name, value) =>
|
||||
utils.replaceProperty(Object.getPrototypeOf(navigator), name, {
|
||||
get() {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
patchNavigator("mimeTypes", mimeTypes);
|
||||
patchNavigator("plugins", plugins);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
log("loading navigator.userAgent.js");
|
||||
// replace Headless references in default useragent
|
||||
const current_ua = navigator.userAgent;
|
||||
utils.replaceProperty(Object.getPrototypeOf(navigator), "userAgent", {
|
||||
get: () => opts.navigator_user_agent ||
|
||||
current_ua.replace("HeadlessChrome/", "Chrome/")
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
log("loading navigator.userAgentData.js");
|
||||
|
||||
const originalUserAgentData = navigator.userAgentData;
|
||||
|
||||
if (originalUserAgentData) {
|
||||
/**
|
||||
* Helper to replace HeadlessChrome with Google Chrome in brand lists
|
||||
* @param {Array<{brand: string, version: string}>} list - Array of UADataBrand objects
|
||||
*/
|
||||
const filterBrandList = (list) =>
|
||||
list.map((b) => (b.brand === "HeadlessChrome" ? { brand: "Google Chrome", version: b.version } : b));
|
||||
|
||||
// Get the NavigatorUAData prototype
|
||||
const NavigatorUADataProto = Object.getPrototypeOf(originalUserAgentData);
|
||||
|
||||
// Cache original methods before patching
|
||||
const originalGetHighEntropyValues = NavigatorUADataProto.getHighEntropyValues;
|
||||
const originalToJSON = NavigatorUADataProto.toJSON;
|
||||
const originalBrandsDescriptor = Object.getOwnPropertyDescriptor(NavigatorUADataProto, "brands");
|
||||
|
||||
let cachedFilteredBrands = null;
|
||||
// Helper to get filtered brands (cached for identity checks)
|
||||
const getFilteredBrands = () => {
|
||||
if (cachedFilteredBrands === null) {
|
||||
const originalBrands = originalBrandsDescriptor.get.call(originalUserAgentData);
|
||||
cachedFilteredBrands = filterBrandList(originalBrands);
|
||||
}
|
||||
return cachedFilteredBrands;
|
||||
};
|
||||
|
||||
// Patch getHighEntropyValues to filter HeadlessChrome from results
|
||||
utils.replaceProperty(NavigatorUADataProto, "getHighEntropyValues", {
|
||||
value: function (hints) {
|
||||
return originalGetHighEntropyValues.call(this, hints).then((data) => {
|
||||
const newData = { ...data };
|
||||
if (newData.brands) {
|
||||
newData.brands = filterBrandList(newData.brands);
|
||||
}
|
||||
if (newData.fullVersionList) {
|
||||
newData.fullVersionList = filterBrandList(newData.fullVersionList);
|
||||
}
|
||||
return newData;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Patch toJSON to filter HeadlessChrome
|
||||
utils.replaceProperty(NavigatorUADataProto, "toJSON", {
|
||||
value: function () {
|
||||
const data = originalToJSON.call(this);
|
||||
return {
|
||||
brands: filterBrandList(data.brands),
|
||||
mobile: data.mobile,
|
||||
platform: data.platform,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Patch brands getter to return filtered array (same instance each call, like real Chrome)
|
||||
utils.replaceProperty(NavigatorUADataProto, "brands", {
|
||||
get: function () {
|
||||
// Return cached filtered brands, computing and freezing on first access
|
||||
return getFilteredBrands();
|
||||
},
|
||||
enumerable: originalBrandsDescriptor.enumerable,
|
||||
configurable: originalBrandsDescriptor.configurable,
|
||||
});
|
||||
|
||||
utils.replaceProperty(NavigatorUADataProto, "userAgentData", {
|
||||
get: () => originalUserAgentData,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
log("loading navigator.vendor.js");
|
||||
|
||||
utils.replaceProperty(Object.getPrototypeOf(navigator), "vendor", {
|
||||
get: () => opts.navigator_vendor || "Google Inc."
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
log("loading navigator.webdriver.js");
|
||||
// this is close to the most accurate way to emulate this: https://stackoverflow.com/a/69533548
|
||||
// no point "giving ourselves away" if we don't need to mock this values
|
||||
// techniques exist to detect Object.defineProperty etc., so if we can avoid it we do
|
||||
// if args include --disable-blink-features=AutomationControlled, we do not need to mock this
|
||||
if (navigator.webdriver) {
|
||||
utils.replaceProperty(Object.getPrototypeOf(navigator), "webdriver", {
|
||||
get: new Proxy(
|
||||
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator),
|
||||
"webdriver").get, {
|
||||
apply: (target, thisArg, args) => {
|
||||
// emulate getter call validation
|
||||
Reflect.apply(target, thisArg, args);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
log("not patching navigator.webdriver, assuming CLI args were used instead");
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
log("loading webgl.vendor.js");
|
||||
|
||||
const getParameterProxyHandler = {
|
||||
apply: function (target, ctx, args) {
|
||||
const param = (args || [])[0];
|
||||
// UNMASKED_VENDOR_WEBGL
|
||||
if (param === 37445) {
|
||||
return opts.webgl_vendor || "Intel Inc."; // default in headless: Google Inc.
|
||||
}
|
||||
// UNMASKED_RENDERER_WEBGL
|
||||
if (param === 37446) {
|
||||
return opts.webgl_renderer || "Intel Iris OpenGL Engine"; // default in headless: Google SwiftShader
|
||||
}
|
||||
return utils.cache.Reflect.apply(target, ctx, args);
|
||||
},
|
||||
};
|
||||
|
||||
// There's more than one WebGL rendering context
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext#Browser_compatibility
|
||||
// To find out the original values here: Object.getOwnPropertyDescriptors(WebGLRenderingContext.prototype.getParameter)
|
||||
const addProxy = (obj, propName) => {
|
||||
utils.replaceWithProxy(obj, propName, getParameterProxyHandler);
|
||||
};
|
||||
// For whatever weird reason loops don't play nice with Object.defineProperty, here's the next best thing:
|
||||
addProxy(WebGLRenderingContext.prototype, "getParameter");
|
||||
addProxy(WebGL2RenderingContext.prototype, "getParameter");
|
||||
@@ -0,0 +1,130 @@
|
||||
generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
|
||||
item: utils.createProxy(proto.item, {
|
||||
apply(target, ctx, args) {
|
||||
if (!args.length) {
|
||||
throw new TypeError(
|
||||
`Failed to execute 'item' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.`
|
||||
);
|
||||
}
|
||||
// Special behavior alert:
|
||||
// - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup
|
||||
// - If anything else than an integer (including as string) is provided it will return the first entry
|
||||
const isInteger = args[0] && Number.isInteger(Number(args[0])); // Cast potential string to number first, then check for integer
|
||||
// Note: Vanilla never returns `undefined`
|
||||
return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null;
|
||||
}
|
||||
}),
|
||||
/** Returns the MimeType object with the specified name. */
|
||||
namedItem: utils.createProxy(proto.namedItem, {
|
||||
apply(target, ctx, args) {
|
||||
if (!args.length) {
|
||||
throw new TypeError(
|
||||
`Failed to execute 'namedItem' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.`
|
||||
);
|
||||
}
|
||||
return dataArray.find((mt) => mt[itemMainProp] === args[0]) || null; // Not `undefined`!
|
||||
}
|
||||
}),
|
||||
/** Does nothing and shall return nothing */
|
||||
refresh: proto.refresh
|
||||
? utils.createProxy(proto.refresh, {
|
||||
apply(target, ctx, args) {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
});
|
||||
|
||||
function generateMagicArray(
|
||||
dataArray = [],
|
||||
proto = MimeTypeArray.prototype,
|
||||
itemProto = MimeType.prototype,
|
||||
itemMainProp = "type"
|
||||
) {
|
||||
// Quick helper to set props with the same descriptors vanilla is using
|
||||
const defineProp = (obj, prop, value) =>
|
||||
Object.defineProperty(obj, prop, {
|
||||
value,
|
||||
writable: false,
|
||||
enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)`
|
||||
configurable: false
|
||||
});
|
||||
|
||||
// Loop over our fake data and construct items
|
||||
const makeItem = (data) => {
|
||||
const item = {};
|
||||
for (const prop of Object.keys(data)) {
|
||||
if (prop.startsWith("__")) {
|
||||
continue;
|
||||
}
|
||||
defineProp(item, prop, data[prop]);
|
||||
}
|
||||
// navigator.plugins[i].length should always be 1
|
||||
if (itemProto === Plugin.prototype) {
|
||||
defineProp(item, "length", 1);
|
||||
}
|
||||
// We need to spoof a specific `MimeType` or `Plugin` object
|
||||
return Object.create(itemProto, Object.getOwnPropertyDescriptors(item));
|
||||
};
|
||||
|
||||
const magicArray = [];
|
||||
|
||||
// Loop through our fake data and use that to create convincing entities
|
||||
dataArray.forEach((data) => {
|
||||
magicArray.push(makeItem(data));
|
||||
});
|
||||
|
||||
// Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards
|
||||
magicArray.forEach((entry) => {
|
||||
defineProp(magicArray, entry[itemMainProp], entry);
|
||||
});
|
||||
|
||||
// This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)`
|
||||
const magicArrayObj = Object.create(proto, {
|
||||
...Object.getOwnPropertyDescriptors(magicArray),
|
||||
|
||||
// There's one ugly quirk we unfortunately need to take care of:
|
||||
// The `MimeTypeArray` prototype has an enumerable `length` property,
|
||||
// but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`.
|
||||
// To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap.
|
||||
length: {
|
||||
value: magicArray.length,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`
|
||||
}
|
||||
});
|
||||
|
||||
// Generate our functional function mocks :-)
|
||||
const functionMocks = generateFunctionMocks(proto, itemMainProp, magicArray);
|
||||
|
||||
// Override custom object with proxy
|
||||
return new Proxy(magicArrayObj, {
|
||||
get(target, key = "") {
|
||||
// Redirect function calls to our custom proxied versions mocking the vanilla behavior
|
||||
if (key === "item") {
|
||||
return functionMocks.item;
|
||||
}
|
||||
if (key === "namedItem") {
|
||||
return functionMocks.namedItem;
|
||||
}
|
||||
if (proto === PluginArray.prototype && key === "refresh") {
|
||||
return functionMocks.refresh;
|
||||
}
|
||||
// Everything else can pass through as normal
|
||||
return utils.cache.Reflect.get(...arguments);
|
||||
},
|
||||
ownKeys(target) {
|
||||
// There are a couple of quirks where the original property demonstrates "magical" behavior that makes no sense
|
||||
// This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length`
|
||||
// My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly
|
||||
// For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing
|
||||
// Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing
|
||||
const keys = [];
|
||||
const typeProps = magicArray.map((mt) => mt[itemMainProp]);
|
||||
typeProps.forEach((_, i) => keys.push(`${i}`));
|
||||
typeProps.forEach((propName) => keys.push(propName));
|
||||
return keys;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* A set of shared utility functions specifically to modify native browser APIs without leaving traces.
|
||||
*/
|
||||
const utils = {};
|
||||
|
||||
/**
|
||||
* Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw.
|
||||
* The presence of a JS Proxy can be revealed as it shows up in error stack traces.
|
||||
*
|
||||
* @param {object} handler - The JS Proxy handler to wrap
|
||||
*/
|
||||
utils.stripProxyFromErrors = (handler = {}) => {
|
||||
const handler_name = (Math.random() + 1).toString(36).substring(2);
|
||||
window[handler_name] = {}
|
||||
// We wrap each trap in the handler in a try/catch and modify the error stack if they throw
|
||||
const traps = Object.getOwnPropertyNames(handler);
|
||||
traps.forEach((trap) => {
|
||||
window[handler_name][trap] = function () {
|
||||
try {
|
||||
// Forward the call to the defined proxy handler
|
||||
return handler[trap].apply(this, arguments || []);
|
||||
} catch (err) {
|
||||
// Stack traces differ per browser, we only support chromium based ones currently
|
||||
if (!err || !err.stack || !err.stack.includes(`at `)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// When something throws within one of our traps the Proxy will show up in error stacks
|
||||
// An earlier implementation of this code would simply strip lines with a blacklist,
|
||||
// but it makes sense to be more surgical here and only remove lines related to our Proxy.
|
||||
// We try to use a known "anchor" line for that and strip it with everything above it.
|
||||
// If the anchor line cannot be found for some reason we fall back to our blacklist approach.
|
||||
|
||||
const stripWithBlacklist = (stack) => {
|
||||
const blacklist = [
|
||||
`at Reflect.${trap} `, // e.g. Reflect.get or Reflect.apply
|
||||
`at Object.${trap} `, // e.g. Object.get or Object.apply
|
||||
`at window.<computed>.<computed> [as ${trap}] `, // caused by this very wrapper :-)
|
||||
];
|
||||
return (
|
||||
err.stack
|
||||
.split("\n")
|
||||
// Always remove the first (file) line in the stack (guaranteed to be our proxy)
|
||||
.filter((line, index) => index !== 1)
|
||||
// Check if the line starts with one of our blacklisted strings
|
||||
.filter((line) => !blacklist.some((bl) => line.trim().startsWith(bl)))
|
||||
.join("\n")
|
||||
);
|
||||
};
|
||||
|
||||
const stripWithAnchor = (stack) => {
|
||||
const stackArr = stack.split("\n");
|
||||
const anchor = `at window.<computed>.<computed> [as ${trap}] `; // Known first Proxy line in chromium
|
||||
const anchorIndex = stackArr.findIndex((line) => line.trim().startsWith(anchor));
|
||||
if (anchorIndex === -1) {
|
||||
return false; // 404, anchor not found
|
||||
}
|
||||
// Strip everything from the top until we reach the anchor line
|
||||
// Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
|
||||
stackArr.splice(1, anchorIndex);
|
||||
return stackArr.join("\n");
|
||||
};
|
||||
|
||||
// Try using the anchor method, fallback to blacklist if necessary
|
||||
err.stack = stripWithAnchor(err.stack) || stripWithBlacklist(err.stack);
|
||||
|
||||
throw err; // Re-throw our now sanitized error
|
||||
}
|
||||
};
|
||||
});
|
||||
return window[handler_name];
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip error lines from stack traces until (and including) a known line the stack.
|
||||
*
|
||||
* @param {object} err - The error to sanitize
|
||||
* @param {string} anchor - The string the anchor line starts with
|
||||
*/
|
||||
utils.stripErrorWithAnchor = (err, anchor) => {
|
||||
const stackArr = err.stack.split("\n");
|
||||
const anchorIndex = stackArr.findIndex((line) => line.trim().startsWith(anchor));
|
||||
if (anchorIndex === -1) {
|
||||
return err; // 404, anchor not found
|
||||
}
|
||||
// Strip everything from the top until we reach the anchor line (remove anchor line as well)
|
||||
// Note: We're keeping the 1st line (zero index) as it's unrelated (e.g. `TypeError`)
|
||||
stackArr.splice(1, anchorIndex);
|
||||
err.stack = stackArr.join("\n");
|
||||
return err;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace the property of an object in a stealthy way.
|
||||
*
|
||||
* Note: You also want to work on the prototype of an object most often,
|
||||
* as you'd otherwise leave traces (e.g. showing up in Object.getOwnPropertyNames(obj)).
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
|
||||
*
|
||||
* @example
|
||||
* replaceProperty(WebGLRenderingContext.prototype, 'getParameter', { value: "alice" })
|
||||
* // or
|
||||
* replaceProperty(Object.getPrototypeOf(navigator), 'languages', { get: () => ['en-US', 'en'] })
|
||||
*
|
||||
* @param {object} obj - The object which has the property to replace
|
||||
* @param {string} propName - The property name to replace
|
||||
* @param {object} descriptorOverrides - e.g. { value: "alice" }
|
||||
*/
|
||||
utils.replaceProperty = (obj, propName, descriptorOverrides = {}) => {
|
||||
return Object.defineProperty(obj, propName, {
|
||||
// Copy over the existing descriptors (writable, enumerable, configurable, etc)
|
||||
...(Object.getOwnPropertyDescriptor(obj, propName) || {}),
|
||||
// Add our overrides (e.g. value, get())
|
||||
...descriptorOverrides,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Preload a cache of function copies and data.
|
||||
*
|
||||
* For a determined enough observer it would be possible to overwrite and sniff usage of functions
|
||||
* we use in our internal Proxies, to combat that we use a cached copy of those functions.
|
||||
*
|
||||
* This is evaluated once per execution context (e.g. window)
|
||||
*/
|
||||
utils.preloadCache = () => {
|
||||
if (utils.cache) {
|
||||
return;
|
||||
}
|
||||
utils.cache = {
|
||||
// Used in our proxies
|
||||
Reflect: {
|
||||
get: Reflect.get.bind(Reflect),
|
||||
apply: Reflect.apply.bind(Reflect),
|
||||
},
|
||||
// Used in `makeNativeString`
|
||||
nativeToStringStr: Function.toString.toString(), // => `function toString() { [native code] }`
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to generate a cross-browser `toString` result representing native code.
|
||||
*
|
||||
* There's small differences: Chromium uses a single line, whereas FF & Webkit uses multiline strings.
|
||||
* To future-proof this we use an existing native toString result as the basis.
|
||||
*
|
||||
* The only advantage we have over the other team is that our JS runs first, hence we cache the result
|
||||
* of the native toString result once, so they cannot spoof it afterwards and reveal that we're using it.
|
||||
*
|
||||
* Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before,
|
||||
* by executing `utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups).
|
||||
*
|
||||
* @example
|
||||
* makeNativeString('foobar') // => `function foobar() { [native code] }`
|
||||
*
|
||||
* @param {string} [name] - Optional function name
|
||||
*/
|
||||
utils.makeNativeString = (name = "") => {
|
||||
// Cache (per-window) the original native toString or use that if available
|
||||
utils.preloadCache();
|
||||
return utils.cache.nativeToStringStr.replace("toString", name || "");
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to modify the `toString()` result of the provided object.
|
||||
*
|
||||
* Note: Use `utils.redirectToString` instead when possible.
|
||||
*
|
||||
* There's a quirk in JS Proxies that will cause the `toString()` result to differ from the vanilla Object.
|
||||
* If no string is provided we will generate a `[native code]` thing based on the name of the property object.
|
||||
*
|
||||
* @example
|
||||
* patchToString(WebGLRenderingContext.prototype.getParameter, 'function getParameter() { [native code] }')
|
||||
*
|
||||
* @param {object} obj - The object for which to modify the `toString()` representation
|
||||
* @param {string} str - Optional string used as a return value
|
||||
*/
|
||||
utils.patchToString = (obj, str = "") => {
|
||||
utils.preloadCache();
|
||||
|
||||
const toStringProxy = new Proxy(Function.prototype.toString, {
|
||||
apply: function (target, ctx) {
|
||||
// This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
|
||||
if (ctx === Function.prototype.toString) {
|
||||
return utils.makeNativeString("toString");
|
||||
}
|
||||
// `toString` targeted at our proxied Object detected
|
||||
if (ctx === obj) {
|
||||
// We either return the optional string verbatim or derive the most desired result automatically
|
||||
return str || utils.makeNativeString(obj.name);
|
||||
}
|
||||
// Check if the toString protype of the context is the same as the global prototype,
|
||||
// if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
|
||||
const hasSameProto = Object.getPrototypeOf(Function.prototype.toString).isPrototypeOf(ctx.toString); // eslint-disable-line no-prototype-builtins
|
||||
if (!hasSameProto) {
|
||||
// Pass the call on to the local Function.prototype.toString instead
|
||||
return ctx.toString();
|
||||
}
|
||||
return target.call(ctx);
|
||||
},
|
||||
});
|
||||
utils.replaceProperty(Function.prototype, "toString", {
|
||||
value: toStringProxy,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Make all nested functions of an object native.
|
||||
*
|
||||
* @param {object} obj
|
||||
*/
|
||||
utils.patchToStringNested = (obj = {}) => {
|
||||
return utils.execRecursively(obj, ["function"], utils.patchToString);
|
||||
};
|
||||
|
||||
/**
|
||||
* Redirect toString requests from one object to another.
|
||||
*
|
||||
* @param {object} proxyObj - The object that toString will be called on
|
||||
* @param {object} originalObj - The object which toString result we wan to return
|
||||
*/
|
||||
utils.redirectToString = (proxyObj, originalObj) => {
|
||||
utils.preloadCache();
|
||||
|
||||
const toStringProxy = new Proxy(Function.prototype.toString, {
|
||||
apply: function (target, ctx) {
|
||||
// This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""`
|
||||
if (ctx === Function.prototype.toString) {
|
||||
return utils.makeNativeString("toString");
|
||||
}
|
||||
|
||||
// `toString` targeted at our proxied Object detected
|
||||
if (ctx === proxyObj) {
|
||||
const fallback = () =>
|
||||
originalObj && originalObj.name
|
||||
? utils.makeNativeString(originalObj.name)
|
||||
: utils.makeNativeString(proxyObj.name);
|
||||
|
||||
// Return the toString representation of our original object if possible
|
||||
return originalObj + "" || fallback();
|
||||
}
|
||||
|
||||
// Check if the toString protype of the context is the same as the global prototype,
|
||||
// if not indicates that we are doing a check across different windows., e.g. the iframeWithdirect` test case
|
||||
const hasSameProto = Object.getPrototypeOf(Function.prototype.toString).isPrototypeOf(ctx.toString); // eslint-disable-line no-prototype-builtins
|
||||
if (!hasSameProto) {
|
||||
// Pass the call on to the local Function.prototype.toString instead
|
||||
return ctx.toString();
|
||||
}
|
||||
|
||||
return target.call(ctx);
|
||||
},
|
||||
});
|
||||
utils.replaceProperty(Function.prototype, "toString", {
|
||||
value: toStringProxy,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* All-in-one method to replace a property with a JS Proxy using the provided Proxy handler with traps.
|
||||
*
|
||||
* Will stealthify these aspects (strip error stack traces, redirect toString, etc).
|
||||
* Note: This is meant to modify native Browser APIs and works best with prototype objects.
|
||||
*
|
||||
* @example
|
||||
* replaceWithProxy(WebGLRenderingContext.prototype, 'getParameter', proxyHandler)
|
||||
*
|
||||
* @param {object} obj - The object which has the property to replace
|
||||
* @param {string} propName - The name of the property to replace
|
||||
* @param {object} handler - The JS Proxy handler to use
|
||||
*/
|
||||
utils.replaceWithProxy = (obj, propName, handler) => {
|
||||
utils.preloadCache();
|
||||
const originalObj = obj[propName];
|
||||
const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler));
|
||||
|
||||
utils.replaceProperty(obj, propName, { value: proxyObj });
|
||||
utils.redirectToString(proxyObj, originalObj);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps.
|
||||
*
|
||||
* Will stealthify these aspects (strip error stack traces, redirect toString, etc).
|
||||
*
|
||||
* @example
|
||||
* mockWithProxy(chrome.runtime, 'sendMessage', function sendMessage() {}, proxyHandler)
|
||||
*
|
||||
* @param {object} obj - The object which has the property to replace
|
||||
* @param {string} propName - The name of the property to replace or create
|
||||
* @param {object} pseudoTarget - The JS Proxy target to use as a basis
|
||||
* @param {object} handler - The JS Proxy handler to use
|
||||
*/
|
||||
utils.mockWithProxy = (obj, propName, pseudoTarget, handler) => {
|
||||
utils.preloadCache();
|
||||
const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler));
|
||||
|
||||
utils.replaceProperty(obj, propName, { value: proxyObj });
|
||||
utils.patchToString(proxyObj);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* All-in-one method to create a new JS Proxy with stealth tweaks.
|
||||
*
|
||||
* This is meant to be used whenever we need a JS Proxy but don't want to replace or mock an existing known property.
|
||||
*
|
||||
* Will stealthify certain aspects of the Proxy (strip error stack traces, redirect toString, etc).
|
||||
*
|
||||
* @example
|
||||
* createProxy(navigator.mimeTypes.__proto__.namedItem, proxyHandler) // => Proxy
|
||||
*
|
||||
* @param {object} pseudoTarget - The JS Proxy target to use as a basis
|
||||
* @param {object} handler - The JS Proxy handler to use
|
||||
*/
|
||||
utils.createProxy = (pseudoTarget, handler) => {
|
||||
utils.preloadCache();
|
||||
const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler));
|
||||
utils.patchToString(proxyObj);
|
||||
return proxyObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to split a full path to an Object into the first part and property.
|
||||
*
|
||||
* @example
|
||||
* splitObjPath(`HTMLMediaElement.prototype.canPlayType`)
|
||||
* // => {objName: "HTMLMediaElement.prototype", propName: "canPlayType"}
|
||||
*
|
||||
* @param {string} objPath - The full path to an object as dot notation string
|
||||
*/
|
||||
utils.splitObjPath = (objPath) => ({
|
||||
// Remove last dot entry (property) ==> `HTMLMediaElement.prototype`
|
||||
objName: objPath.split(".").slice(0, -1).join("."),
|
||||
// Extract last dot entry ==> `canPlayType`
|
||||
propName: objPath.split(".").slice(-1)[0],
|
||||
});
|
||||
|
||||
/**
|
||||
* Convenience method to replace a property with a JS Proxy using the provided objPath.
|
||||
*
|
||||
* Supports a full path (dot notation) to the object as string here, in case that makes it easier.
|
||||
*
|
||||
* @example
|
||||
* replaceObjPathWithProxy('WebGLRenderingContext.prototype.getParameter', proxyHandler)
|
||||
*
|
||||
* @param {string} objPath - The full path to an object (dot notation string) to replace
|
||||
* @param {object} handler - The JS Proxy handler to use
|
||||
*/
|
||||
utils.replaceObjPathWithProxy = (objPath, handler) => {
|
||||
const { objName, propName } = utils.splitObjPath(objPath);
|
||||
const obj = eval(objName); // eslint-disable-line no-eval
|
||||
return utils.replaceWithProxy(obj, propName, handler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Traverse nested properties of an object recursively and apply the given function on a whitelist of value types.
|
||||
*
|
||||
* @param {object} obj
|
||||
* @param {array} typeFilter - e.g. `['function']`
|
||||
* @param {Function} fn - e.g. `utils.patchToString`
|
||||
*/
|
||||
utils.execRecursively = (obj = {}, typeFilter = [], fn) => {
|
||||
function recurse(obj) {
|
||||
for (const key in obj) {
|
||||
if (obj[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (obj[key] && typeof obj[key] === "object") {
|
||||
recurse(obj[key]);
|
||||
} else {
|
||||
if (obj[key] && typeFilter.includes(typeof obj[key])) {
|
||||
fn.call(this, obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recurse(obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Everything we run through e.g. `page.evaluate` runs in the browser context, not the NodeJS one.
|
||||
* That means we cannot just use reference variables and functions from outside code, we need to pass everything as a parameter.
|
||||
*
|
||||
* Unfortunately, the data we can pass is only allowed to be of primitive types, regular functions don't survive the built-in serialization process.
|
||||
* This utility function will take an object with functions and stringify them, so we can pass them down unharmed as strings.
|
||||
*
|
||||
* We use this to pass down our utility functions as well as any other functions (to be able to split up code better).
|
||||
*
|
||||
* @see utils.materializeFns
|
||||
*
|
||||
* @param {object} fnObj - An object containing functions as properties
|
||||
*/
|
||||
utils.stringifyFns = (fnObj = { hello: () => "world" }) => {
|
||||
// Object.fromEntries() polyfill (in 6 lines) - supported only in Node v12+, modern browsers are fine
|
||||
// https://github.com/feross/fromentries
|
||||
return (Object.fromEntries || fromEntries)(
|
||||
Object.entries(fnObj)
|
||||
.filter(([key, value]) => typeof value === "function")
|
||||
.map(([key, value]) => [key, value.toString()]) // eslint-disable-line no-eval
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to reverse the process of `utils.stringifyFns`.
|
||||
* Will materialize an object with stringified functions (supports classic and fat arrow functions).
|
||||
*
|
||||
* @param {object} fnStrObj - An object containing stringified functions as properties
|
||||
*/
|
||||
utils.materializeFns = (fnStrObj = { hello: "() => 'world'" }) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(fnStrObj).map(([key, value]) => {
|
||||
if (value.startsWith("function")) {
|
||||
// some trickery is necessary to make oldschool functions work :-)
|
||||
return [key, eval(`() => ${value}`)()]; // eslint-disable-line no-eval
|
||||
} else {
|
||||
// arrow functions just work
|
||||
return [key, eval(value)]; // eslint-disable-line no-eval
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
utils.arrayEqual = (arr1, arr2) => arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
|
||||
|
||||
const log = (...args) => opts.script_logging && console.log("[playwright-stealth]:", ...args);
|
||||
const warn = (...args) => opts.script_logging && console.warn("[playwright-stealth]:", ...args);
|
||||
|
||||
log(JSON.stringify(opts));
|
||||
@@ -0,0 +1,587 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import inspect
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Union, Any, Tuple, Optional
|
||||
|
||||
from playwright import async_api, sync_api
|
||||
from playwright_stealth.case_insensitive_dict import CaseInsensitiveDict
|
||||
from playwright_stealth.context_managers import (
|
||||
AsyncWrappingContextManager,
|
||||
SyncWrappingContextManager,
|
||||
)
|
||||
|
||||
|
||||
def from_file(name) -> str:
|
||||
return (Path(__file__).parent / "js" / name).read_text()
|
||||
|
||||
|
||||
SCRIPTS: Dict[str, str] = {
|
||||
"generate_magic_arrays": from_file("generate.magic.arrays.js"),
|
||||
"utils": from_file("utils.js"),
|
||||
"chrome_app": from_file("evasions/chrome.app.js"),
|
||||
"chrome_csi": from_file("evasions/chrome.csi.js"),
|
||||
"chrome_hairline": from_file("evasions/chrome.hairline.js"),
|
||||
"chrome_load_times": from_file("evasions/chrome.load.times.js"),
|
||||
"chrome_runtime": from_file("evasions/chrome.runtime.js"),
|
||||
"iframe_content_window": from_file("evasions/iframe.contentWindow.js"),
|
||||
"media_codecs": from_file("evasions/media.codecs.js"),
|
||||
"navigator_hardware_concurrency": from_file("evasions/navigator.hardwareConcurrency.js"),
|
||||
"navigator_languages": from_file("evasions/navigator.languages.js"),
|
||||
"navigator_permissions": from_file("evasions/navigator.permissions.js"),
|
||||
"navigator_platform": from_file("evasions/navigator.platform.js"),
|
||||
"navigator_plugins": from_file("evasions/navigator.plugins.js"),
|
||||
"navigator_user_agent": from_file("evasions/navigator.userAgent.js"),
|
||||
"navigator_user_agent_data": from_file("evasions/navigator.userAgentData.js"),
|
||||
"navigator_vendor": from_file("evasions/navigator.vendor.js"),
|
||||
"navigator_webdriver": from_file("evasions/navigator.webdriver.js"),
|
||||
"error_prototype": from_file("evasions/error.prototype.js"),
|
||||
"webgl_vendor": from_file("evasions/webgl.vendor.js"),
|
||||
}
|
||||
|
||||
|
||||
class Stealth:
|
||||
"""
|
||||
Playwright stealth configuration that applies stealth strategies to Playwright.
|
||||
The stealth strategies are contained in ./js package and are basic javascript scripts that are executed
|
||||
on every page.goto() called.
|
||||
Note:
|
||||
All init scripts are combined by playwright into one script and then executed this means
|
||||
the scripts should not have conflicting constants/variables etc. !
|
||||
This also means scripts can be extended by overriding enabled_scripts generator:
|
||||
```
|
||||
@property
|
||||
def enabled_scripts():
|
||||
yield 'console.log("first script")'
|
||||
yield from super().enabled_scripts()
|
||||
yield 'console.log("last script")'
|
||||
```
|
||||
"""
|
||||
|
||||
_USER_AGENT_OVERRIDE_PIGGYBACK_KEY = "_stealth_user_agent"
|
||||
_SEC_CH_UA_OVERRIDE_PIGGYBACK_KEY = "_stealth_sec_ch_ua"
|
||||
_STEALTH_APPLIED_KEY = "_playwright_stealth_applied"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
chrome_app: bool = True,
|
||||
chrome_csi: bool = True,
|
||||
chrome_load_times: bool = True,
|
||||
chrome_runtime: bool = False,
|
||||
hairline: bool = True,
|
||||
iframe_content_window: bool = True,
|
||||
media_codecs: bool = True,
|
||||
navigator_hardware_concurrency: bool = True,
|
||||
navigator_languages: bool = True,
|
||||
navigator_permissions: bool = True,
|
||||
navigator_platform: bool = True,
|
||||
navigator_plugins: bool = True,
|
||||
navigator_user_agent: bool = True,
|
||||
navigator_user_agent_data: bool = True,
|
||||
navigator_vendor: bool = True,
|
||||
navigator_webdriver: bool = True,
|
||||
error_prototype: bool = True,
|
||||
sec_ch_ua: bool = True,
|
||||
webgl_vendor: bool = True,
|
||||
navigator_languages_override: Tuple[str, str] = ("en-US", "en"),
|
||||
navigator_platform_override: str = "Win32",
|
||||
navigator_user_agent_override: Optional[str] = None,
|
||||
navigator_vendor_override: str = None,
|
||||
sec_ch_ua_override: Optional[str] = None,
|
||||
webgl_renderer_override: str = None,
|
||||
webgl_vendor_override: str = None,
|
||||
init_scripts_only: bool = False,
|
||||
script_logging: bool = False,
|
||||
):
|
||||
# scripts to load
|
||||
self.chrome_app: bool = chrome_app
|
||||
self.chrome_csi: bool = chrome_csi
|
||||
self.chrome_load_times: bool = chrome_load_times
|
||||
self.chrome_runtime: bool = chrome_runtime
|
||||
self.hairline: bool = hairline
|
||||
self.iframe_content_window: bool = iframe_content_window
|
||||
self.media_codecs: bool = media_codecs
|
||||
self.navigator_hardware_concurrency: int = navigator_hardware_concurrency
|
||||
self.navigator_languages: bool = navigator_languages
|
||||
self.navigator_permissions: bool = navigator_permissions
|
||||
self.navigator_platform: bool = navigator_platform
|
||||
self.navigator_plugins: bool = navigator_plugins
|
||||
self.navigator_user_agent: bool = navigator_user_agent
|
||||
self.navigator_user_agent_data: bool = navigator_user_agent_data
|
||||
self.navigator_vendor: bool = navigator_vendor
|
||||
self.navigator_webdriver: bool = navigator_webdriver
|
||||
self.error_prototype: bool = error_prototype
|
||||
self.sec_ch_ua: bool = sec_ch_ua
|
||||
self.webgl_vendor: bool = webgl_vendor
|
||||
|
||||
# warn if an override was provided for a disabled option
|
||||
self._check_for_disabled_options_overridden(locals())
|
||||
# evasion options
|
||||
self.navigator_languages_override: Tuple[str, str] = navigator_languages_override or ("en-US", "en")
|
||||
self.navigator_platform_override: Optional[str] = navigator_platform_override
|
||||
self.navigator_user_agent_override: Optional[str] = navigator_user_agent_override
|
||||
self.navigator_vendor_override: str = navigator_vendor_override or None
|
||||
if sec_ch_ua_override is None and self.navigator_user_agent_override is not None:
|
||||
# we can get sec_ch_ua override for "free" here if we can parse the Chrome version string
|
||||
self.sec_ch_ua_override = self._get_greased_chrome_sec_ua_ch(self.navigator_user_agent_override)
|
||||
else:
|
||||
self.sec_ch_ua_override: Optional[str] = sec_ch_ua_override
|
||||
self.webgl_renderer_override: str = webgl_renderer_override or "Intel Iris OpenGL Engine"
|
||||
self.webgl_vendor_override: str = webgl_vendor_override or "Intel Inc."
|
||||
# other options
|
||||
self.init_scripts_only: bool = init_scripts_only
|
||||
self.script_logging = script_logging
|
||||
|
||||
@property
|
||||
def script_payload(self) -> str:
|
||||
"""
|
||||
Generates an immediately invoked function expression for all enabled scripts
|
||||
Returns: string of enabled scripts in IIFE
|
||||
"""
|
||||
scripts_block = "\n".join(self.enabled_scripts)
|
||||
if len(scripts_block) == 0:
|
||||
return ""
|
||||
return "(() => {\n" + scripts_block + "\n})();"
|
||||
|
||||
@property
|
||||
def options_payload(self) -> str:
|
||||
opts = {
|
||||
"navigator_hardware_concurrency": self.navigator_hardware_concurrency,
|
||||
"navigator_languages_override": self.navigator_languages_override,
|
||||
"navigator_platform": self.navigator_platform_override,
|
||||
"navigator_user_agent": self.navigator_user_agent_override,
|
||||
"navigator_vendor": self.navigator_vendor_override,
|
||||
"webgl_renderer": self.webgl_renderer_override,
|
||||
"webgl_vendor": self.webgl_vendor_override,
|
||||
"script_logging": self.script_logging,
|
||||
}
|
||||
return f"const opts = {json.dumps(opts)};"
|
||||
|
||||
@property
|
||||
def enabled_scripts(self):
|
||||
evasion_script_block = "\n".join(self._evasion_scripts)
|
||||
if len(evasion_script_block) == 0:
|
||||
return ""
|
||||
|
||||
yield self.options_payload
|
||||
yield SCRIPTS["utils"]
|
||||
yield SCRIPTS["generate_magic_arrays"]
|
||||
yield evasion_script_block
|
||||
|
||||
@property
|
||||
def _evasion_scripts(self) -> str:
|
||||
if self.chrome_app:
|
||||
yield SCRIPTS["chrome_app"]
|
||||
if self.chrome_csi:
|
||||
yield SCRIPTS["chrome_csi"]
|
||||
if self.hairline:
|
||||
yield SCRIPTS["chrome_hairline"]
|
||||
if self.chrome_load_times:
|
||||
yield SCRIPTS["chrome_load_times"]
|
||||
if self.chrome_runtime:
|
||||
yield SCRIPTS["chrome_runtime"]
|
||||
if self.iframe_content_window:
|
||||
yield SCRIPTS["iframe_content_window"]
|
||||
if self.media_codecs:
|
||||
yield SCRIPTS["media_codecs"]
|
||||
if self.navigator_languages:
|
||||
yield SCRIPTS["navigator_languages"]
|
||||
if self.navigator_permissions:
|
||||
yield SCRIPTS["navigator_permissions"]
|
||||
if self.navigator_platform:
|
||||
yield SCRIPTS["navigator_platform"]
|
||||
if self.navigator_plugins:
|
||||
yield SCRIPTS["navigator_plugins"]
|
||||
if self.navigator_user_agent:
|
||||
yield SCRIPTS["navigator_user_agent"]
|
||||
if self.navigator_user_agent_data:
|
||||
yield SCRIPTS["navigator_user_agent_data"]
|
||||
if self.navigator_vendor:
|
||||
yield SCRIPTS["navigator_vendor"]
|
||||
if self.navigator_webdriver:
|
||||
yield SCRIPTS["navigator_webdriver"]
|
||||
if self.error_prototype:
|
||||
yield SCRIPTS["error_prototype"]
|
||||
if self.webgl_vendor:
|
||||
yield SCRIPTS["webgl_vendor"]
|
||||
|
||||
def warn_if_stealth_applied(self, obj: Any) -> bool:
|
||||
if hasattr(obj, self._STEALTH_APPLIED_KEY):
|
||||
warnings.warn(
|
||||
"Stealth has already been applied to this page or context. Skipping duplicate application.",
|
||||
category=UserWarning,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def use_async(self, ctx: async_api.PlaywrightContextManager) -> AsyncWrappingContextManager:
|
||||
"""
|
||||
Instruments the playwright context manager.
|
||||
Any browser connected to or any page created with any method from
|
||||
the patched context should have stealth evasions applied automatically.
|
||||
|
||||
async with Stealth().use_async(async_playwright()) as p:
|
||||
...
|
||||
"""
|
||||
return AsyncWrappingContextManager(self, ctx)
|
||||
|
||||
def use_sync(self, ctx: sync_api.PlaywrightContextManager) -> SyncWrappingContextManager:
|
||||
"""
|
||||
Instruments the playwright context manager.
|
||||
Any browser connected to or any page created with any method from
|
||||
the patched context should have stealth evasions applied automatically.
|
||||
|
||||
with Stealth().use_sync(sync_playwright()) as p:
|
||||
...
|
||||
"""
|
||||
return SyncWrappingContextManager(self, ctx)
|
||||
|
||||
async def apply_stealth_async(self, page_or_context: Union[async_api.Page, async_api.BrowserContext]) -> None:
|
||||
if len(self.script_payload) > 0 and not self.warn_if_stealth_applied(page_or_context):
|
||||
await page_or_context.add_init_script(self.script_payload)
|
||||
setattr(page_or_context, self._STEALTH_APPLIED_KEY, True)
|
||||
|
||||
def apply_stealth_sync(self, page_or_context: Union[sync_api.Page, sync_api.BrowserContext]) -> None:
|
||||
if len(self.script_payload) > 0 and not self.warn_if_stealth_applied(page_or_context):
|
||||
page_or_context.add_init_script(self.script_payload)
|
||||
setattr(page_or_context, self._STEALTH_APPLIED_KEY, True)
|
||||
|
||||
def hook_playwright_context(self, ctx: Union[async_api.Playwright, sync_api.Playwright]) -> None:
|
||||
"""
|
||||
Given a Playwright context object, hooks all the browser type object methods that return a Browser object.
|
||||
Can be used with sync and async methods contexts
|
||||
"""
|
||||
browser_class_name = sync_api.Browser.__name__
|
||||
for browser_type in (ctx.chromium, ctx.firefox, ctx.webkit):
|
||||
chromium_mode = browser_type.name == "chromium"
|
||||
for name, hooked_method in inspect.getmembers(browser_type, predicate=inspect.ismethod):
|
||||
# todo: ctx.browser.launch_persistent_context
|
||||
if hooked_method.__annotations__.get("return") == browser_class_name:
|
||||
hooked_method = self._generate_hooked_method_that_returns_browser(hooked_method, chromium_mode)
|
||||
setattr(browser_type, name, hooked_method)
|
||||
|
||||
def _kwargs_with_patched_cli_arg(
|
||||
self, method: Callable, packed_kwargs: Dict[str, Any], chromium_mode: bool
|
||||
) -> Dict[str, Any]:
|
||||
signature = inspect.signature(method).parameters
|
||||
args_parameter = signature.get("args")
|
||||
|
||||
# deep just in case
|
||||
new_kwargs = deepcopy(packed_kwargs)
|
||||
if args_parameter is not None:
|
||||
if chromium_mode and not self.init_scripts_only:
|
||||
new_cli_args = new_kwargs.get("args", args_parameter.default)
|
||||
if self.navigator_webdriver:
|
||||
new_cli_args = self._patch_blink_features_cli_args(new_cli_args or [])
|
||||
if self.navigator_languages:
|
||||
languages_cli_flag = f"--accept-lang={','.join(self.navigator_languages_override)}"
|
||||
new_cli_args = self._patch_cli_arg(new_cli_args or [], languages_cli_flag)
|
||||
new_kwargs["args"] = new_cli_args
|
||||
return new_kwargs
|
||||
|
||||
def _generate_hooked_method_that_returns_browser(self, method: Callable, chromium_mode: bool):
|
||||
async def async_hooked_method(*args, **kwargs) -> async_api.Browser:
|
||||
browser = await method(
|
||||
*args,
|
||||
**self._kwargs_with_patched_cli_arg(method, kwargs, chromium_mode),
|
||||
)
|
||||
self._reassign_new_page_new_context(browser)
|
||||
return browser
|
||||
|
||||
def sync_hooked_method(*args, **kwargs) -> sync_api.Browser:
|
||||
browser = method(
|
||||
*args,
|
||||
**self._kwargs_with_patched_cli_arg(method, kwargs, chromium_mode),
|
||||
)
|
||||
self._reassign_new_page_new_context(browser)
|
||||
return browser
|
||||
|
||||
if inspect.iscoroutinefunction(method):
|
||||
return async_hooked_method
|
||||
return sync_hooked_method
|
||||
|
||||
def _generate_hooked_new_context(self, new_context_method: Callable, new_page_method: Callable) -> Callable:
|
||||
async def hooked_new_context_async(*args, **kwargs):
|
||||
context = await new_context_method(
|
||||
*args,
|
||||
**(await self._kwargs_new_page_context_with_patches_async(new_page_method, kwargs)),
|
||||
)
|
||||
await self.apply_stealth_async(context)
|
||||
return context
|
||||
|
||||
def hooked_browser_method_sync(*args, **kwargs):
|
||||
context = new_context_method(
|
||||
*args,
|
||||
**(self._kwargs_new_page_context_with_patches_sync(new_page_method, kwargs)),
|
||||
)
|
||||
self.apply_stealth_sync(context)
|
||||
return context
|
||||
|
||||
if inspect.iscoroutinefunction(new_context_method):
|
||||
return hooked_new_context_async
|
||||
return hooked_browser_method_sync
|
||||
|
||||
def _generate_hooked_new_page(self, new_page_method: Callable, patch_kwargs: bool) -> Callable:
|
||||
"""
|
||||
Returns a hooked method (async or sync) for new_page.
|
||||
*args and **kwargs even though these methods may not take any number of arguments,
|
||||
we want to preserve accurate stack traces when caller passes args improperly.
|
||||
|
||||
If patch_kwargs is true, we patch kwargs the caller passes to enhance evasions. This only applies
|
||||
to Browser.new_page, so we pass false when passing in a BrowserContext.new_page method
|
||||
"""
|
||||
|
||||
async def hooked_new_page_async(*args, **kwargs):
|
||||
kwargs = (
|
||||
await self._kwargs_new_page_context_with_patches_async(new_page_method, kwargs)
|
||||
if patch_kwargs
|
||||
else kwargs
|
||||
)
|
||||
page = await new_page_method(*args, **kwargs)
|
||||
await self.apply_stealth_async(page)
|
||||
return page
|
||||
|
||||
def hooked_new_page_sync(*args, **kwargs):
|
||||
kwargs = (
|
||||
self._kwargs_new_page_context_with_patches_sync(new_page_method, kwargs) if patch_kwargs else kwargs
|
||||
)
|
||||
page = new_page_method(*args, **kwargs)
|
||||
self.apply_stealth_sync(page)
|
||||
return page
|
||||
|
||||
if inspect.iscoroutinefunction(new_page_method):
|
||||
return hooked_new_page_async
|
||||
return hooked_new_page_sync
|
||||
|
||||
async def _kwargs_new_page_context_with_patches_async(
|
||||
self, unpatched_new_page: Callable, packed_kwargs: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
This returns kwargs with arguments added based on enabled evasions, while respecting any kwargs the caller
|
||||
has passed in. If enabled and overrides aren't set for either navigator_user_agent or sec_ch_ua,
|
||||
and we're dealing with a Chromium browser, we create a temporary page to get up-to-date UA information.
|
||||
|
||||
This function is suitable for patching kwargs for new_context and new_page, though in either case, an unpatched
|
||||
new_page method must be provided.
|
||||
Args:
|
||||
unpatched_new_page: new_page
|
||||
packed_kwargs: kwargs the caller has passed in
|
||||
Returns:
|
||||
patched kwargs
|
||||
"""
|
||||
browser_or_context = unpatched_new_page.__self__
|
||||
if isinstance(browser_or_context, async_api.BrowserContext):
|
||||
browser_instance = browser_or_context.browser
|
||||
else:
|
||||
browser_instance = browser_or_context
|
||||
is_chromium = browser_instance.browser_type.name == "chromium"
|
||||
|
||||
async def get_user_agent_and_sec_ch_ua_async() -> Tuple[str, str]:
|
||||
temp_page: Optional[async_api.Page]
|
||||
stealth_user_agent = getattr(browser_instance, self._USER_AGENT_OVERRIDE_PIGGYBACK_KEY, None)
|
||||
sec_ch_ua = getattr(browser_instance, self._SEC_CH_UA_OVERRIDE_PIGGYBACK_KEY, None)
|
||||
if stealth_user_agent is None or sec_ch_ua is None:
|
||||
temp_page = await unpatched_new_page()
|
||||
stealth_user_agent = (await temp_page.evaluate("navigator.userAgent")).replace(
|
||||
"HeadlessChrome", "Chrome"
|
||||
)
|
||||
await temp_page.close(reason="playwright_stealth internal temp utility page")
|
||||
sec_ch_ua = self._get_greased_chrome_sec_ua_ch(stealth_user_agent)
|
||||
setattr(browser_instance, self._SEC_CH_UA_OVERRIDE_PIGGYBACK_KEY, sec_ch_ua)
|
||||
setattr(
|
||||
browser_instance,
|
||||
self._USER_AGENT_OVERRIDE_PIGGYBACK_KEY,
|
||||
stealth_user_agent,
|
||||
)
|
||||
return stealth_user_agent, sec_ch_ua
|
||||
|
||||
new_kwargs = deepcopy(packed_kwargs)
|
||||
if self.navigator_user_agent and packed_kwargs.get("user_agent") is None:
|
||||
resolved_user_agent_override = self.navigator_user_agent_override
|
||||
if resolved_user_agent_override is None and is_chromium:
|
||||
(
|
||||
resolved_user_agent_override,
|
||||
_,
|
||||
) = await get_user_agent_and_sec_ch_ua_async()
|
||||
new_kwargs["user_agent"] = resolved_user_agent_override
|
||||
extra_http_headers = packed_kwargs.get("extra_http_headers", {})
|
||||
if self.sec_ch_ua and CaseInsensitiveDict(extra_http_headers).get("sec-ch-ua") is None:
|
||||
resolved_sec_ch_ua_override = self.sec_ch_ua_override
|
||||
if resolved_sec_ch_ua_override is None and is_chromium:
|
||||
(
|
||||
_,
|
||||
resolved_sec_ch_ua_override,
|
||||
) = await get_user_agent_and_sec_ch_ua_async()
|
||||
if resolved_sec_ch_ua_override is not None:
|
||||
extra_http_headers["sec-ch-ua"] = resolved_sec_ch_ua_override
|
||||
new_kwargs["extra_http_headers"] = extra_http_headers
|
||||
|
||||
return new_kwargs
|
||||
|
||||
def _kwargs_new_page_context_with_patches_sync(
|
||||
self, unpatched_new_page: Callable, packed_kwargs: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""see self._kwargs_new_page_context_with_patches_async for docs."""
|
||||
browser_or_context = unpatched_new_page.__self__
|
||||
if isinstance(browser_or_context, sync_api.BrowserContext):
|
||||
browser_instance = browser_or_context.browser
|
||||
else:
|
||||
browser_instance = browser_or_context
|
||||
is_chromium = browser_instance.browser_type.name == "chromium"
|
||||
|
||||
def get_user_agent_and_sec_ch_ua_sync() -> Tuple[str, str]:
|
||||
temp_page: Optional[sync_api.Page]
|
||||
stealth_user_agent = getattr(browser_instance, self._USER_AGENT_OVERRIDE_PIGGYBACK_KEY, None)
|
||||
sec_ch_ua = getattr(browser_instance, self._SEC_CH_UA_OVERRIDE_PIGGYBACK_KEY, None)
|
||||
if stealth_user_agent is None or sec_ch_ua is None:
|
||||
temp_page = unpatched_new_page()
|
||||
stealth_user_agent = temp_page.evaluate("navigator.userAgent").replace("HeadlessChrome", "Chrome")
|
||||
sec_ch_ua = self._get_greased_chrome_sec_ua_ch(stealth_user_agent)
|
||||
temp_page.close(reason="playwright_stealth internal temp utility page")
|
||||
setattr(browser_instance, self._SEC_CH_UA_OVERRIDE_PIGGYBACK_KEY, sec_ch_ua)
|
||||
setattr(
|
||||
browser_instance,
|
||||
self._USER_AGENT_OVERRIDE_PIGGYBACK_KEY,
|
||||
stealth_user_agent,
|
||||
)
|
||||
return stealth_user_agent, sec_ch_ua
|
||||
|
||||
new_kwargs = deepcopy(packed_kwargs)
|
||||
if self.navigator_user_agent and packed_kwargs.get("user_agent") is None:
|
||||
resolved_user_agent_override = self.navigator_user_agent_override
|
||||
if resolved_user_agent_override is None and is_chromium:
|
||||
resolved_user_agent_override, _ = get_user_agent_and_sec_ch_ua_sync()
|
||||
new_kwargs["user_agent"] = resolved_user_agent_override
|
||||
extra_http_headers = packed_kwargs.get("extra_http_headers", {})
|
||||
if self.sec_ch_ua and CaseInsensitiveDict(extra_http_headers).get("sec-ch-ua") is None:
|
||||
resolved_sec_ch_ua_override = self.sec_ch_ua_override
|
||||
if resolved_sec_ch_ua_override is None and is_chromium:
|
||||
_, resolved_sec_ch_ua_override = get_user_agent_and_sec_ch_ua_sync()
|
||||
if resolved_sec_ch_ua_override is not None:
|
||||
extra_http_headers["sec-ch-ua"] = resolved_sec_ch_ua_override
|
||||
new_kwargs["extra_http_headers"] = extra_http_headers
|
||||
|
||||
return new_kwargs
|
||||
|
||||
def _reassign_new_page_new_context(self, browser: Union[async_api.Browser, sync_api.Browser]) -> None:
|
||||
if isinstance(browser, (async_api.Browser, sync_api.Browser)):
|
||||
browser.new_context = self._generate_hooked_new_context(browser.new_context, browser.new_page)
|
||||
browser.new_page = self._generate_hooked_new_page(browser.new_page, patch_kwargs=True)
|
||||
else:
|
||||
raise TypeError(f"unexpected type from function (bug): returned {browser}")
|
||||
|
||||
@staticmethod
|
||||
def _get_greased_chrome_sec_ua_ch(user_agent: str) -> Optional[str]:
|
||||
"""
|
||||
From the major version in user_agent, generate a Sec-CH-UA header value. An example of the data in this
|
||||
header can be generated from navigator.userAgentData.brands (requires secure context). We could query that
|
||||
ourselves, but since it requires a secure context, there's no performant way to do that, so instead we
|
||||
re-implement the greasing algorithm from Chrome.
|
||||
|
||||
See Also:
|
||||
https://wicg.github.io/ua-client-hints/#grease
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA
|
||||
https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc
|
||||
Args:
|
||||
user_agent: Chrome UA
|
||||
|
||||
Returns:
|
||||
greased Sec-CH-UA header value, None if the Chrome version cannot be parsed
|
||||
"""
|
||||
greased_versions = [8, 99, 24]
|
||||
greasy_chars = " ():-./;=?_"
|
||||
greasy_brand = f"Not{random.choice(greasy_chars)}A{random.choice(greasy_chars)}Brand"
|
||||
version = re.search(r"Chrome/(\d+)[\d.]+", user_agent, re.IGNORECASE)
|
||||
if len(version.groups()) == 0:
|
||||
return None
|
||||
major_version = version.group(1)
|
||||
brands = [
|
||||
("Chromium", major_version),
|
||||
("Chrome", major_version),
|
||||
(greasy_brand, random.choice(greased_versions)),
|
||||
]
|
||||
random.shuffle(brands)
|
||||
return ", ".join(f'"{brand}";v="{version}"' for brand, version in brands)
|
||||
|
||||
@staticmethod
|
||||
def _patch_blink_features_cli_args(existing_args: Optional[List[str]]) -> List[str]:
|
||||
"""Patches CLI args list to disable AutomationControlled blink feature, while preserving other args"""
|
||||
new_args = []
|
||||
disable_blink_features_prefix = "--disable-blink-features="
|
||||
automation_controlled_feature_name = "AutomationControlled"
|
||||
for arg in existing_args or []:
|
||||
stripped_arg = arg.strip()
|
||||
if stripped_arg.startswith(disable_blink_features_prefix):
|
||||
if automation_controlled_feature_name not in stripped_arg:
|
||||
stripped_arg += f",{automation_controlled_feature_name}"
|
||||
new_args.append(stripped_arg)
|
||||
else:
|
||||
new_args.append(arg)
|
||||
else: # no break
|
||||
# the user has specified no extra blink features disabled,
|
||||
# so no need to be careful how we modify the command line
|
||||
new_args.append(f"{disable_blink_features_prefix}{automation_controlled_feature_name}")
|
||||
return new_args
|
||||
|
||||
@staticmethod
|
||||
def _patch_cli_arg(existing_args: List[str], flag: str) -> List[str]:
|
||||
"""Patches CLI args list with any arg, warns if the user passed their own value in themselves"""
|
||||
new_args = []
|
||||
switch_name = re.search("(.*)=?", flag).group(1)
|
||||
for arg in existing_args:
|
||||
stripped_arg = arg.strip()
|
||||
if stripped_arg.startswith(switch_name):
|
||||
warnings.warn(
|
||||
"playwright-stealth is trying to modify a flag you have set yourself already."
|
||||
f"Either disable the mitigation or don't specify this flag manually {flag=}"
|
||||
f"to avoid this warning. playwright-stealth has overridden your flag",
|
||||
category=UserWarning,
|
||||
)
|
||||
new_args.append(flag)
|
||||
break
|
||||
else:
|
||||
new_args.append(arg)
|
||||
else: # no break
|
||||
# none of the existing switches overlap with the one we're trying to set
|
||||
new_args.append(flag)
|
||||
return new_args
|
||||
|
||||
@staticmethod
|
||||
def _check_for_disabled_options_overridden(packed_kwargs: Dict[str, Any]) -> None:
|
||||
for key in ALL_EVASIONS_DISABLED_KWARGS.keys():
|
||||
if not packed_kwargs.get(key) and packed_kwargs.get(f"{key}_override") is not None:
|
||||
warnings.warn(
|
||||
f"{key} is False, but an override ({key}_override) was provided, "
|
||||
f"which is probably not what you intended to do",
|
||||
stacklevel=3,
|
||||
category=UserWarning,
|
||||
)
|
||||
|
||||
|
||||
ALL_EVASIONS_DISABLED_KWARGS = {
|
||||
"chrome_app": False,
|
||||
"chrome_csi": False,
|
||||
"chrome_load_times": False,
|
||||
"chrome_runtime": False,
|
||||
"hairline": False,
|
||||
"iframe_content_window": False,
|
||||
"media_codecs": False,
|
||||
"navigator_hardware_concurrency": False,
|
||||
"navigator_languages": False,
|
||||
"navigator_permissions": False,
|
||||
"navigator_platform": False,
|
||||
"navigator_plugins": False,
|
||||
"navigator_user_agent": False,
|
||||
"navigator_user_agent_data": False,
|
||||
"navigator_vendor": False,
|
||||
"navigator_webdriver": False,
|
||||
"error_prototype": False,
|
||||
"sec_ch_ua": False,
|
||||
"webgl_vendor": False,
|
||||
}
|
||||
Reference in New Issue
Block a user