fakebrowser
Version:
🤖 Fake fingerprints to bypass anti-bot systems. Simulate mouse and keyboard operations to make behavior like a real person.
355 lines (312 loc) • 13.3 kB
JavaScript
// noinspection JSUnusedLocalSymbols
;
const {PuppeteerExtraPlugin} = require('puppeteer-extra-plugin');
const withUtils = require('../_utils/withUtils');
const withWorkerUtils = require('../_utils/withWorkerUtils');
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',
},
}
;
/**
* Mock the `chrome.runtime` object if not available (e.g. when running headless) and on a secure site.
*/
class Plugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts);
}
get name() {
return 'evasions/chrome.runtime';
}
get defaults() {
return {runOnInsecureOrigins: false}; // Override for testing
}
async onPageCreated(page) {
await withUtils(this, page).evaluateOnNewDocument(
this.mainFunction,
{
opts: this.opts,
STATIC_DATA,
},
);
}
mainFunction = (utils, {opts, STATIC_DATA}) => {
const _Object = utils.cache.Object;
const _Reflect = utils.cache.Reflect;
if (!window.chrome) {
// Use the exact property descriptor found in headful Chrome
// fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
// FIXME: order of 'chrome' in window, cannot by modified
// when defining this property, 'chrome' becomes the most bottom of the property list
// inspired by creepjs
_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
let isNotSecure = false;
try {
isNotSecure = !window.top.location.protocol.startsWith('https');
} catch (ex) {
try {
isNotSecure = !window.location.protocol.startsWith('https');
} catch (ignore) {
}
// console.warn(ex);
}
if (existsAlready || (isNotSecure && !opts.runOnInsecureOrigins)) {
return; // Nothing to do here
}
_Object.defineProperty(window.chrome, 'runtime', {
configurable: true,
enumerable: true,
value: {
// 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
id: undefined,
// These two require more sophisticated mocks
connect: null,
sendMessage: null,
},
writable: true,
});
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 = {
get: (target, property, receiver) => {
// I use Object.create as a prototype native function to disguise our js function
// However, Object.create contains two call parameters
// So we have to hook length and return 0
if (property === 'name') {
return 'sendMessage';
} else if (property === 'length') {
return 0;
}
return _Reflect.get(target, property, receiver);
},
apply: function (target, thisArg, 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',
_Object.create, // We just need a native function
{},
sendMessageHandler,
);
/**
* Mock `chrome.runtime.connect`
*
* @see https://developer.chrome.com/apps/runtime#method-connect
*/
const connectHandler = {
get: (target, property, receiver) => {
if (property === 'name') {
return 'connect';
} else if (property === 'length') {
return 0;
}
return _Reflect.get(target, property, receiver);
},
apply: function (target, thisArg, 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: exteensionId 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',
_Object.create,
{},
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;
}
};
}
module.exports = function (pluginConfig) {
return new Plugin(pluginConfig);
};