From db871a3ce7b6e7080488867a92a78cbe88df8020 Mon Sep 17 00:00:00 2001 From: kkoooqq <> Date: Tue, 9 Nov 2021 18:11:35 -0500 Subject: [PATCH] fix: touch screen devices forged user operation --- package.json | 2 +- src/core/DeviceDescriptor.ts | 10 +- src/core/FakeBrowser.ts | 107 ++++++++------- src/core/FakeUserAction.ts | 254 ++++++++++++++++++++++++++--------- src/core/PptrPatcher.ts | 2 +- src/core/TouchScreen.ts | 92 +++++++++++++ 6 files changed, 348 insertions(+), 119 deletions(-) create mode 100644 src/core/TouchScreen.ts diff --git a/package.json b/package.json index 2e4bf14..605f186 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fakebrowser", - "version": "0.0.52", + "version": "0.0.53", "description": "🤖 Fake fingerprints to bypass anti-bot systems. Simulate mouse and keyboard operations to make behavior like a real person.", "repository": { "type": "git", diff --git a/src/core/DeviceDescriptor.ts b/src/core/DeviceDescriptor.ts index dedffb1..b34a356 100644 --- a/src/core/DeviceDescriptor.ts +++ b/src/core/DeviceDescriptor.ts @@ -209,7 +209,7 @@ export interface DeviceDescriptor { "state"?: string, "exType"?: string, "msg"?: string, - }> + }>, } export type ChromeUACHHeaders = { @@ -261,7 +261,7 @@ export default class DeviceDescriptorHelper { * Check device descriptor legal based on attributes * @param e */ - static checkLegal(e: DeviceDescriptor) { + static checkLegal(e: DeviceDescriptor): boolean { if (!e) { throw new Error('DeviceDescriptor empty') } @@ -291,6 +291,10 @@ export default class DeviceDescriptorHelper { if (e.navigator.maxTouchPoints != 0) { throw new Error('Desktop browsers cannot have touchscreens') } + } else { + if (e.navigator.maxTouchPoints === 0) { + throw new Error('Mobile devices must have touch screen') + } } if (e.window.innerHeight > e.screen.availHeight @@ -344,6 +348,8 @@ export default class DeviceDescriptorHelper { if (!e.permissions || Object.keys(e.permissions).length === 0) { throw new Error('permissions cannot be empty') } + + return true } /** diff --git a/src/core/FakeBrowser.ts b/src/core/FakeBrowser.ts index 0cd6f6b..f0fb88b 100644 --- a/src/core/FakeBrowser.ts +++ b/src/core/FakeBrowser.ts @@ -15,6 +15,7 @@ import {UserAgentHelper} from "./UserAgentHelper"; import {FakeUserAction} from "./FakeUserAction"; import {BrowserLauncher} from "./BrowserLauncher"; import {BrowserBuilder} from "./BrowserBuilder"; +import {Touchscreen} from "./TouchScreen"; export const kDefaultWindowsDD = require(path.resolve(__dirname, '../../device-hub/Windows.json')) @@ -139,51 +140,30 @@ export class FakeBrowser { defaultLaunchArgs: kDefaultLaunchArgs, } - private readonly _driverParams: DriverParameters - private readonly _vanillaBrowser: Browser - private readonly _pptrExtra: PuppeteerExtra - private readonly _bindingTime: number - private readonly _uuid: string - private readonly _userAction: FakeUserAction + readonly driverParams: DriverParameters + readonly vanillaBrowser: Browser + readonly pptrExtra: PuppeteerExtra + /** + * Browser instance launch time or connection time + */ + readonly bindingTime: number + readonly uuid: string + readonly isMobileBrowser: boolean + readonly userAction: FakeUserAction + + // friend to FakeUserAction private _zombie: boolean // private readonly _workerUrls: string[] - get driverParams(): DriverParameters { - return this._driverParams - } - get launchParams(): LaunchParameters { - assert((this._driverParams as LaunchParameters).launchOptions) - return this._driverParams as LaunchParameters + assert((this.driverParams as LaunchParameters).launchOptions) + return this.driverParams as LaunchParameters } get connectParams(): ConnectParameters { - assert((this._driverParams as ConnectParameters).connectOptions) - return this._driverParams as ConnectParameters - } - - get vanillaBrowser(): Browser { - return this._vanillaBrowser - } - - get pptrExtra(): PuppeteerExtra { - return this._pptrExtra - } - - /** - * Browser instance launch time or connection time - */ - get bindingTime(): number { - return this._bindingTime - } - - get uuid(): string { - return this._uuid - } - - get userAction(): FakeUserAction { - return this._userAction + assert((this.driverParams as ConnectParameters).connectOptions) + return this.driverParams as ConnectParameters } private async beforeShutdown() { @@ -201,7 +181,7 @@ export class FakeBrowser { } async getActivePage(): Promise { - const result = await PptrToolkit.getActivePage(this._vanillaBrowser) + const result = await PptrToolkit.getActivePage(this.vanillaBrowser) return result } @@ -212,16 +192,24 @@ export class FakeBrowser { bindingTime: number, uuid: string, ) { - this._driverParams = params - this._vanillaBrowser = vanillaBrowser - this._pptrExtra = pptrExtra - this._bindingTime = bindingTime - this._uuid = uuid + this.driverParams = params + this.vanillaBrowser = vanillaBrowser + this.pptrExtra = pptrExtra + this.bindingTime = bindingTime + + assert( + params.deviceDesc + && params.deviceDesc.navigator + && params.deviceDesc.navigator.userAgent + ) + + this.isMobileBrowser = UserAgentHelper.isMobile(params.deviceDesc.navigator.userAgent) + this.uuid = uuid + this.userAction = new FakeUserAction(this) + this._zombie = false // this._workerUrls = [] - this._userAction = new FakeUserAction(this) - vanillaBrowser.on('targetcreated', this.onTargetCreated.bind(this)) vanillaBrowser.on('disconnected', this.onDisconnected.bind(this)) } @@ -272,18 +260,18 @@ export class FakeBrowser { // console.log('inject page') let cdpSession: CDPSession | null = null - const fakeDD = this._driverParams.fakeDeviceDesc + const fakeDD = this.driverParams.fakeDeviceDesc assert(fakeDD) // if there is an account password that proxy needs to log in if ( - this._driverParams.proxy && - this._driverParams.proxy.username && - this._driverParams.proxy.password + this.driverParams.proxy && + this.driverParams.proxy.username && + this.driverParams.proxy.password ) { await page.authenticate({ - username: this._driverParams.proxy.username, - password: this._driverParams.proxy.password, + username: this.driverParams.proxy.username, + password: this.driverParams.proxy.password, }); } @@ -291,7 +279,22 @@ export class FakeBrowser { try { await page['_client'].send('ServiceWorker.setForceUpdateOnPageLoad', {forceUpdateOnPageLoad: true}) } catch (ex: any) { - console.warn('ServiceWorker.setForceUpdateOnPageLoad exception', ex) + console.warn('CDP ServiceWorker.setForceUpdateOnPageLoad exception', ex) + } + + // touch + if (this.isMobileBrowser) { + try { + await page['_client'].send('Emulation.setEmitTouchEventsForMouse', { + enabled: true, + }) + } catch (ex: any) { + console.warn('CDP Emulation.setEmitTouchEventsForMouse exception', ex) + } + + Object.defineProperty(page, '_patchTouchscreen', { + value: new Touchscreen(page['_client'], page.keyboard) + }) } // intercept worker diff --git a/src/core/FakeUserAction.ts b/src/core/FakeUserAction.ts index 9e888b0..e90dafa 100644 --- a/src/core/FakeUserAction.ts +++ b/src/core/FakeUserAction.ts @@ -1,21 +1,18 @@ -// noinspection JSUnusedGlobalSymbols,JSUnusedLocalSymbols +// noinspection JSUnusedGlobalSymbols,JSUnusedLocalSymbols,PointlessArithmeticExpressionJS import {strict as assert} from 'assert'; -import {ElementHandle, KeyInput, Page} from "puppeteer"; +import {BoundingBox, ElementHandle, KeyInput, Page, Point} from "puppeteer"; import {helper} from "./helper"; import {FakeBrowser} from "./FakeBrowser"; import {PptrToolkit} from "./PptrToolkit"; - -export interface IMousePosition { - x: number, - y: number, -} +import {FakeDeviceDescriptor} from "./DeviceDescriptor"; +import {Touchscreen} from "./TouchScreen"; export class FakeUserAction { - private _mouseCurrPos: IMousePosition + private _mouseCurrPos: Point // WeakRef needs node >= 14.6.0 // private _fakeBrowser: WeakRef | null @@ -34,12 +31,12 @@ export class FakeUserAction { * @param maxPoints * @param cpDelta */ - static mouseMovementTrack( - startPos: IMousePosition, - endPos: IMousePosition, + private static mouseMovementTrack( + startPos: Point, + endPos: Point, maxPoints = 30, cpDelta = 1, - ): IMousePosition[] { + ): Point[] { // reference: https://github.com/mtsee/Bezier/blob/master/src/bezier.js let nums = [] @@ -94,9 +91,9 @@ export class FakeUserAction { * @param page * @param options */ - static async simMouseMove(page: Page, options: { - startPos: IMousePosition, - endPos: IMousePosition, + private static async simMouseMove(page: Page, options: { + startPos: Point, + endPos: Point, maxPoints?: number, timestamp?: number, cpDelta?: number, @@ -126,6 +123,7 @@ export class FakeUserAction { return null } + // WeakRef: // const fb: FakeBrowser | undefined = this._fakeBrowser.deref() const fb: FakeBrowser | undefined = this._fakeBrowser if (!fb) { @@ -137,7 +135,7 @@ export class FakeUserAction { } async simMouseMoveTo( - endPos: IMousePosition, + endPos: Point, maxPoints?: number, timestamp?: number, cpDelta?: number, @@ -147,39 +145,66 @@ export class FakeUserAction { return false } + if (fb.isMobileBrowser) { + // We don't need to simulate mouse slide. + await helper.sleepRd(300, 800) + return true + } + // Get the current page of the browser const currPage = await fb.getActivePage() assert(currPage) + // first move to a close position, then finally move to the target position + const closeToEndPos: Point = { + x: endPos.x + helper.rd(5, 30, true), + y: endPos.y + helper.rd(5, 20, true), + } + await FakeUserAction.simMouseMove(currPage, { startPos: this._mouseCurrPos, - endPos, + endPos: closeToEndPos, maxPoints, timestamp, cpDelta }) + // The last pos must correction + await currPage.mouse.move( + endPos.x, + endPos.y, + {steps: helper.rd(5, 13)} + ) + this._mouseCurrPos = endPos return true } async simRandomMouseMove(): Promise { - // 1/6 - // 1/4 1/4 - // 1/6 - const fb = this.fakeBrowser if (!fb) { return false } + if (fb.isMobileBrowser) { + // We don't need to simulate mouse slide. + await helper.sleepRd(300, 800) + return true + } + const fakeDD = fb.driverParams.fakeDeviceDesc assert(fakeDD) const innerWidth = fakeDD.window.innerWidth const innerHeight = fakeDD.window.innerHeight + // ----------------- + // | 1/6 | + // | 1/4 1/4 | + // | 1/6 | + // ----------------- + const startX = innerWidth / 4 const startY = innerHeight / 6 const endX = innerWidth * 3 / 4 @@ -202,19 +227,24 @@ export class FakeUserAction { const currPage = await fb.getActivePage() assert(currPage) - await currPage.mouse.down() - await helper.sleepRd(50, 80) - await currPage.mouse.up() + if (fb.isMobileBrowser) { + // We can't use mouse obj, we have to use touchscreen + await currPage.touchscreen.tap(this._mouseCurrPos.x, this._mouseCurrPos.y) + } else { + await currPage.mouse.down() + await helper.sleepRd(50, 80) + await currPage.mouse.up() + } if (options && options.pauseAfterMouseUp) { - await helper.sleepRd(300, 1000) + await helper.sleepRd(200, 1000) } return true } async simMoveToAndClick( - endPos: IMousePosition, + endPos: Point, options = { pauseAfterMouseUp: true } @@ -227,8 +257,16 @@ export class FakeUserAction { const currPage = await fb.getActivePage() assert(currPage) - await this.simMouseMoveTo(endPos) - await currPage.mouse.move(endPos.x + helper.rd(-10, 10), endPos.y, {steps: helper.rd(8, 20)}) + if (!fb.isMobileBrowser) { + await this.simMouseMoveTo(endPos) + await currPage.mouse.move( + endPos.x + helper.rd(-10, 10), + endPos.y, + {steps: helper.rd(8, 20)} + ) + } + + this._mouseCurrPos = endPos await helper.sleepRd(100, 300) return this.simClick(options) @@ -251,6 +289,48 @@ export class FakeUserAction { const currPage = await fb.getActivePage() assert(currPage) + let box: BoundingBox | null + + if (fb.isMobileBrowser) { + box = await FakeUserAction.adjustElementPositionWithTouchscreen(eh, currPage, fakeDD) + } else { + box = await FakeUserAction.adjustElementPositionWithMouse(eh, currPage, fakeDD) + } + + if (box) { + // The position of each element click should not be the center of the element + // size of the clicked element must larger than 10 x 10 + let endPos: Point = { + x: box.x + box.width / 2 + helper.rd(0, 5, true), + y: box.y + box.height / 2 + helper.rd(0, 5, true), + } + + await this.simMouseMoveTo(endPos) + + // Pause + await helper.sleepRd(100, 300) + + // click + if (await this.simClick(options)) { + if (options && options.pauseAfterMouseUp) { + // Pause + await helper.sleepRd(500, 1500) + } + + return true + } else { + return false + } + } + + return false + } + + private static async adjustElementPositionWithMouse( + eh: ElementHandle, + currPage: Page, + fakeDD: FakeDeviceDescriptor + ): Promise { let box = null for (; ;) { box = await PptrToolkit.boundingBox(eh) @@ -262,31 +342,51 @@ export class FakeUserAction { let deltaY: number = 0 let viewportAdjust = false + + // If the top of the node is less than 0 if (box.y <= 0) { - // If the top of the node is less than 0 - deltaY = Math.min(-box.y + 30, helper.rd(100, 400)) - await currPage.mouse.wheel({deltaY: -deltaY}) + // deltaY always positive + + // --------------------- + // 30px | + // [ ] | + // .. Distance to be moved + // .. | + // .. | + // ---------------------body top + + deltaY = Math.min( + -(box.y - 30) - 0, + helper.rd(150, 400) + ) + + deltaY = -deltaY viewportAdjust = true } else if (box.y + box.height >= fakeDD.window.innerHeight) { // If the bottom is beyond - deltaY = Math.min(box.y + box.height - fakeDD.window.innerHeight + 30, helper.rd(100, 400)) - await currPage.mouse.wheel({deltaY: deltaY}) + + deltaY = Math.min( + box.y + box.height + 30 - fakeDD.window.innerHeight, + helper.rd(150, 400) + ) + viewportAdjust = true } // if (box.x <= 0) { // // If the top of the button is less than 0 // deltaX = Math.min(-box.x + 30, sh.rd(100, 400)) - // await currPage.mouse.wheel({deltaX: -deltaX}) + // deltaX = -deltaX // viewportAdjust = true // } else if (box.x + box.width >= fakeDD.window.innerWidth) { // // If the bottom is beyond // deltaX = Math.min(box.x + box.width - fakeDD.window.innerWidth + 30, sh.rd(100, 400)) - // await currPage.mouse.wheel({deltaX: deltaX}) // viewportAdjust = true // } if (viewportAdjust) { + // await currPage.mouse.wheel({deltaX}) + await currPage.mouse.wheel({deltaY}) await helper.sleepRd(100, 500) } else { break @@ -296,44 +396,71 @@ export class FakeUserAction { } } - if (box) { - // button cannot be smaller than 25 pixels - let endPos: IMousePosition = { - x: helper.rd(box.x + box.width / 2 - box.width / 3, box.x + box.width / 2 + box.width / 3), - y: helper.rd(box.y, box.y + box.height) - } + return box; + } - await this.simMouseMoveTo(endPos) + private static async adjustElementPositionWithTouchscreen( + eh: ElementHandle, + currPage: Page, + fakeDD: FakeDeviceDescriptor + ): Promise { + let box = null + for (; ;) { + box = await PptrToolkit.boundingBox(eh) - // The last click must be hit - endPos = { - x: box.x + box.width / 2, - y: box.y + box.height / 2, - } + if (box) { + // @ts-ignore + let deltaX: number = 0 + let deltaY: number = 0 - await currPage.mouse.move( - endPos.x + helper.rd(-10, 10), - endPos.y, - {steps: 13} - ) + let viewportAdjust = false + if (box.y <= 0) { + deltaY = Math.min(-box.y + 30, helper.rd(100, 400)) + deltaY = -deltaY + viewportAdjust = true + } else if (box.y + box.height >= fakeDD.window.innerHeight) { + deltaY = Math.min(box.y + box.height - fakeDD.window.innerHeight + 30, helper.rd(100, 400)) + viewportAdjust = true + } - // Pause - await helper.sleepRd(100, 300) + if (viewportAdjust) { + // noinspection TypeScriptValidateTypes + const _patchTouchscreenDesc = Object.getOwnPropertyDescriptor(currPage, '_patchTouchscreen') + assert(_patchTouchscreenDesc) + + const touchscreen: Touchscreen = _patchTouchscreenDesc.value + assert(touchscreen) + + // if deltaY is negative, drop down, otherwise drop up + const startX: number = fakeDD.window.innerWidth / 2 + helper.rd(0, fakeDD.window.innerWidth / 6) + const endX: number = fakeDD.window.innerWidth / 2 + helper.rd(0, fakeDD.window.innerWidth / 6) + let startY: number + let endY: number + + if (deltaY < 0) { + startY = helper.rd(0, fakeDD.window.innerHeight - (-deltaY)) + endY = startY + deltaY + } else { + startY = helper.rd(deltaY, fakeDD.window.innerHeight) + endY = startY - deltaY + } + + await touchscreen.drag({ + x: startX, y: startY + }, { + x: endX, y: endY + }) - // click - if (await this.simClick(options)) { - if (options && options.pauseAfterMouseUp) { - // Pause - await helper.sleepRd(500, 1500) + await helper.sleepRd(100, 500) + } else { + break } - - return true } else { - return false + break } } - return false + return box; } async simKeyboardPress( @@ -387,6 +514,7 @@ export class FakeUserAction { const needsShiftKey = '~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?' + // TODO: check if shiftKey, alt, ctrl can be fired in mobile browsers for (let ch of text) { let needsShift = false if (needsShiftKey.includes(ch)) { diff --git a/src/core/PptrPatcher.ts b/src/core/PptrPatcher.ts index a8515ad..62be755 100644 --- a/src/core/PptrPatcher.ts +++ b/src/core/PptrPatcher.ts @@ -22,7 +22,6 @@ export class PptrPatcher { await this.patchTaskEnv(uuid, pptr, params) await this.patchUserActionLayer(uuid, pptr, params) - await this.pathChrome(uuid, pptr, params) await this.patchWindowHistoryLength(uuid, pptr, params) await this.patchWindowMatchMedia(uuid, pptr, params) @@ -523,6 +522,7 @@ tmpVarNames.forEach(e => { ;({body, base64Encoded} = await client.send('Fetch.getResponseBody', {requestId})) jsContent = base64Encoded ? Buffer.from(body, 'base64').toString('utf-8') : body } else { + // TODO: get through proxy const jsResp = await axios.get(request.url, {headers: request.headers}) jsContent = jsResp.data diff --git a/src/core/TouchScreen.ts b/src/core/TouchScreen.ts new file mode 100644 index 0000000..ca56085 --- /dev/null +++ b/src/core/TouchScreen.ts @@ -0,0 +1,92 @@ +// noinspection JSUnusedGlobalSymbols + +import {CDPSession, Keyboard, MouseButton, MouseOptions, Point} from "puppeteer"; + +export class Touchscreen { + private _client: CDPSession; + private _keyboard: Keyboard; + private _x: number = 0 + private _y: number = 0 + private _button: MouseButton | 'none' = 'none'; + + constructor(client: CDPSession, keyboard: Keyboard) { + this._client = client; + this._keyboard = keyboard; + } + + async move( + x: number, + y: number, + options: { steps?: number } = {} + ) { + const {steps = 1} = options; + const fromX = this._x, fromY = this._y; + this._x = x; + this._y = y; + + for (let i = 1; i <= steps; i++) { + await this._client.send('Input.emulateTouchFromMouseEvent', { + type: 'mouseMoved', + button: this._button, + x: fromX + (this._x - fromX) * (i / steps), + y: fromY + (this._y - fromY) * (i / steps), + modifiers: this._keyboard._modifiers + }); + } + } + + async tap( + x: number, + y: number, + options: MouseOptions & { delay?: number } = {} + ) { + const {delay = null} = options; + if (delay !== null) { + await this.move(x, y); + await this.down(options); + await new Promise((f) => setTimeout(f, delay)); + await this.up(options); + } else { + await this.move(x, y); + await this.down(options); + await this.up(options); + } + } + + async down(options: MouseOptions = {}) { + const {button = 'left', clickCount = 1} = options; + this._button = button; + + await this._client.send('Input.emulateTouchFromMouseEvent', { + type: 'mousePressed', + button, + x: this._x, + y: this._y, + modifiers: this._keyboard._modifiers, + clickCount + }); + } + + async up(options: MouseOptions = {}) { + const {button = 'left', clickCount = 1} = options; + this._button = 'none'; + + await this._client.send('Input.emulateTouchFromMouseEvent', { + type: 'mouseReleased', + button, + x: this._x, + y: this._y, + modifiers: this._keyboard._modifiers, + clickCount + }); + } + + async drag(start: Point, target: Point) { + await this.move(start.x, start.y); + await this.down(); + await this.move(target.x, target.y, { + steps: Math.min(Math.abs(start.x - target.x), Math.abs(start.y - target.y)) / 1.5 + }); + await this.up(); + } +}