diff --git a/src/bot.test.ts b/src/bot.test.ts deleted file mode 100644 index f7f90b9..0000000 --- a/src/bot.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { BotDetectionService, BotReason } from './bot' - -beforeEach(() => { - Object.assign(globalThis, { - window: {}, // this make isServerSide() return false - navigator: undefined, - chrome: undefined, - }) -}) - -const botDetectionService = new BotDetectionService() - -const userAgentChrome = 'Innocent Chrome 998 Mozilla IE Trident All Good Windows 95' -const userAgentSafari = 'Innocent Safari 21 Apple Cupertino 991 Next' - -test('serverSide script is not a bot', () => { - Object.assign(globalThis, { - window: undefined, - }) - expect(botDetectionService.getBotReason()).toBeNull() - expect(botDetectionService.isCDP()).toBe(false) - expect(botDetectionService.isBot()).toBe(false) - expect(botDetectionService.isBotOrCDP()).toBe(false) -}) - -test('innocent chrome is not a bot', () => { - Object.assign(globalThis, { - navigator: { - userAgent: userAgentChrome, - } as Navigator, - chrome: {}, - }) - expect(botDetectionService.getBotReason()).toBeNull() -}) - -test('innocent safari is not a bot', () => { - Object.assign(globalThis, { - navigator: { - userAgent: userAgentSafari, - } as Navigator, - }) - expect(botDetectionService.getBotReason()).toBeNull() -}) - -test('no navigator means bot', () => { - expect(botDetectionService.getBotReason()).toBe(BotReason.NoNavigator) - expect(botDetectionService.isBot()).toBe(true) - expect(botDetectionService.isBotOrCDP()).toBe(true) -}) - -test('no userAgent means bot', () => { - globalThis.navigator = {} as Navigator - expect(botDetectionService.getBotReason()).toBe(BotReason.NoUserAgent) -}) - -test('"bot" in userAgent means bot', () => { - globalThis.navigator = { userAgent: 'KlaviyoBot' } as Navigator - expect(botDetectionService.getBotReason()).toBe(BotReason.UserAgent) -}) - -test('"headless" in userAgent means bot', () => { - globalThis.navigator = { userAgent: 'HeadlessChrome' } as Navigator - expect(botDetectionService.getBotReason()).toBe(BotReason.UserAgent) -}) - -test('"electron" in userAgent means bot', () => { - globalThis.navigator = { userAgent: 'I am Electron 99' } as Navigator - expect(botDetectionService.getBotReason()).toBe(BotReason.UserAgent) -}) - -test('"phantom" in userAgent means bot', () => { - globalThis.navigator = { userAgent: 'Phantom Menace' } as Navigator - expect(botDetectionService.getBotReason()).toBe(BotReason.UserAgent) -}) - -test('"slimer" in userAgent means bot', () => { - globalThis.navigator = { userAgent: 'Slimer than Slime' } as Navigator - expect(botDetectionService.getBotReason()).toBe(BotReason.UserAgent) -}) - -test('navigator.webdriver means bot', () => { - globalThis.navigator = { - userAgent: userAgentSafari, - webdriver: true, - } as Navigator - expect(botDetectionService.getBotReason()).toBe(BotReason.WebDriver) -}) - -// test('0 plugins means bot', () => { -// globalThis.navigator = { -// userAgent: userAgentSafari, -// plugins: [] as any, -// } as Navigator -// expect(botDetectionService.isBot()).toBe(BotReason.ZeroPlugins) -// }) - -test('"" languages means bot', () => { - globalThis.navigator = { - userAgent: userAgentSafari, - languages: '' as any, - } as Navigator - expect(botDetectionService.getBotReason()).toBe(BotReason.EmptyLanguages) -}) - -// test('Chrome without chrome means bot', () => { -// globalThis.navigator = { -// userAgent: userAgentChrome, -// } as Navigator -// expect(botDetectionService.isBot()).toBe(BotReason.ChromeWithoutChrome) -// }) - -// This test helps with coverage, while not really testing anything useful -test('cdp in jest looks like a bot, because it does error serialization', () => { - Object.assign(globalThis, { - window: globalThis, - }) - - // console.log('isBot', botDetectionService.isBot()) - const isCDP = botDetectionService.isCDP() - // expect(isCDP).toBe(botDetectionService.isBotOrCDP()) - expect(isCDP).toBeDefined() - - // const runInIDE = process.argv.some( - // a => a === '--runTestsByPath' || a.includes('IDEA') || a.includes('processChild.js'), - // ) - // console.log({ runInIDE, argv: process.argv }) - // When running in IntelliJ it is detected as CDP (involves Error.stack serialization somehow), while in Jest CLI it is not - // const expectedIsCDP = runInIDE - // expect(isCDP).toBe(expectedIsCDP) -}) diff --git a/src/bot.ts b/src/bot.ts deleted file mode 100644 index 8eee626..0000000 --- a/src/bot.ts +++ /dev/null @@ -1,139 +0,0 @@ -// Relevant material: -// https://deviceandbrowserinfo.com/learning_zone/articles/detecting-headless-chrome-puppeteer-2024 - -import { isServerSide } from '@naturalcycles/js-lib' - -export interface BotDetectionServiceCfg { - /** - * Defaults to false. - * If true - the instance will memoize (remember) the results of the detection - * and won't re-run it. - */ - memoizeResults?: boolean -} - -/** - * Service to detect bots and CDP (Chrome DevTools Protocol). - * - * @experimental - */ -export class BotDetectionService { - constructor(public cfg: BotDetectionServiceCfg = {}) {} - - // memoized results - private botReason: BotReason | null | undefined - private cdp: boolean | undefined - - isBotOrCDP(): boolean { - return !!this.getBotReason() || this.isCDP() - } - - isBot(): boolean { - return !!this.getBotReason() - } - - /** - * Returns null if it's not a Bot, - * otherwise a truthy BotReason. - */ - getBotReason(): BotReason | null { - if (this.cfg.memoizeResults && this.botReason !== undefined) { - return this.botReason - } - - this.botReason = this.detectBotReason() - return this.botReason - } - - private detectBotReason(): BotReason | null { - // SSR - not a bot - if (isServerSide()) return null - const { navigator } = globalThis - if (!navigator) return BotReason.NoNavigator - const { userAgent } = navigator - if (!userAgent) return BotReason.NoUserAgent - - if (/bot|headless|electron|phantom|slimer/i.test(userAgent)) { - return BotReason.UserAgent - } - - if (navigator.webdriver) { - return BotReason.WebDriver - } - - // Kirill: commented out, as it's no longer seems reliable, - // e.g generates false positives with latest Android clients (e.g. Chrome 129) - // if (navigator.plugins?.length === 0) { - // return BotReason.ZeroPlugins // Headless Chrome - // } - - if ((navigator.languages as any) === '') { - return BotReason.EmptyLanguages // Headless Chrome - } - - // isChrome is true if the browser is Chrome, Chromium or Opera - // this is "the chrome test" from https://intoli.com/blog/not-possible-to-block-chrome-headless/ - // this property is for some reason not present by default in headless chrome - // Kirill: criterium removed due to false positives with Android - // if (userAgent.includes('Chrome') && !(globalThis as any).chrome) { - // return BotReason.ChromeWithoutChrome // Headless Chrome - // } - - return null - } - - /** - * CDP stands for Chrome DevTools Protocol. - * This function tests if the current environment is a CDP environment. - * If it's true - it's one of: - * - * 1. Bot, automated with CDP, e.g Puppeteer, Playwright or such. - * 2. Developer with Chrome DevTools open. - * - * 2 is certainly not a bot, but unfortunately we can't distinguish between the two. - * That's why this function is not part of `isBot()`, because it can give "false positive" with DevTools. - * - * Based on: https://deviceandbrowserinfo.com/learning_zone/articles/detecting-headless-chrome-puppeteer-2024 - */ - isCDP(): boolean { - if (this.cfg.memoizeResults && this.cdp !== undefined) { - return this.cdp - } - - this.cdp = this.detectCDP() - return this.cdp - } - - private detectCDP(): boolean { - if (isServerSide()) return false - let cdpCheck1 = false - try { - /* eslint-disable */ - // biome-ignore lint/suspicious/useErrorMessage: ok - const e = new window.Error() - window.Object.defineProperty(e, 'stack', { - configurable: false, - enumerable: false, - // biome-ignore lint/complexity/useArrowFunction: ok - get: function () { - cdpCheck1 = true - return '' - }, - }) - // This is part of the detection and shouldn't be deleted - window.console.debug(e) - /* eslint-enable */ - } catch {} - return cdpCheck1 - } -} - -export enum BotReason { - NoNavigator = 1, - NoUserAgent = 2, - UserAgent = 3, - WebDriver = 4, - // ZeroPlugins = 5, - EmptyLanguages = 6, - // ChromeWithoutChrome = 7, -} diff --git a/src/index.ts b/src/index.ts index 03461af..aa7a77a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ export * from './admin/adminService' export * from './analytics/analytics' -export * from './bot' export * from './i18n/fetchTranslationLoader' export * from './i18n/translation.service' export * from './image/imageFitter'