Skip to content

Commit

Permalink
feat: botDetectionService.isBot to give BotReason enum instead of pla…
Browse files Browse the repository at this point in the history
…in boolean
  • Loading branch information
kirillgroshkov committed Oct 15, 2024
1 parent 48fcd39 commit 957cfdc
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 29 deletions.
34 changes: 20 additions & 14 deletions src/bot.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { botDetectionService } from './bot'
import { botDetectionService, BotReason } from './bot'

beforeEach(() => {
Object.assign(globalThis, {
Expand All @@ -15,7 +15,7 @@ test('serverSide script is not a bot', () => {
Object.assign(globalThis, {
window: undefined,
})
expect(botDetectionService.isBot()).toBe(false)
expect(botDetectionService.isBot()).toBeUndefined()
expect(botDetectionService.isCDP()).toBe(false)
expect(botDetectionService.isBotOrCDP()).toBe(false)
})
Expand All @@ -27,7 +27,7 @@ test('innocent chrome is not a bot', () => {
} as Navigator,
chrome: {},
})
expect(botDetectionService.isBot()).toBe(false)
expect(botDetectionService.isBot()).toBeUndefined()
})

test('innocent safari is not a bot', () => {
Expand All @@ -36,63 +36,67 @@ test('innocent safari is not a bot', () => {
userAgent: userAgentSafari,
} as Navigator,
})
expect(botDetectionService.isBot()).toBe(false)
expect(botDetectionService.isBot()).toBeUndefined()
})

test('no navigator means bot', () => {
expect(botDetectionService.isBot()).toBe(BotReason.NoNavigator)
})

test('no userAgent means bot', () => {
globalThis.navigator = {} as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.NoUserAgent)
})

test('"headless" in userAgent means bot', () => {
globalThis.navigator = { userAgent: 'HeadlessChrome' } as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.UserAgent)
})

test('"electron" in userAgent means bot', () => {
globalThis.navigator = { userAgent: 'I am Electron 99' } as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.UserAgent)
})

test('"phantom" in userAgent means bot', () => {
globalThis.navigator = { userAgent: 'Phantom Menace' } as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.UserAgent)
})

test('"slimer" in userAgent means bot', () => {
globalThis.navigator = { userAgent: 'Slimer than Slime' } as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.UserAgent)
})

test('navigator.webdriver means bot', () => {
globalThis.navigator = {
userAgent: userAgentSafari,
webdriver: true,
} as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.WebDriver)
})

test('0 plugins means bot', () => {
globalThis.navigator = {
userAgent: userAgentSafari,
plugins: [] as any,
} as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.ZeroPlugins)
})

test('"" languages means bot', () => {
globalThis.navigator = {
userAgent: userAgentSafari,
languages: '' as any,
} as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.EmptyLanguages)
})

test('Chrome without chrome means bot', () => {
globalThis.navigator = {
userAgent: userAgentChrome,
} as Navigator
expect(botDetectionService.isBot()).toBe(true)
expect(botDetectionService.isBot()).toBe(BotReason.ChromeWithoutChrome)
})

// This test helps with coverage, while not really testing anything useful
Expand All @@ -101,8 +105,10 @@ test('cdp in jest looks like a bot, because it does error serialization', () =>
window: globalThis,
})

// console.log('isBot', botDetectionService.isBot())
const isCDP = botDetectionService.isCDP()
expect(isCDP).toBe(botDetectionService.isBotOrCDP())
// expect(isCDP).toBe(botDetectionService.isBotOrCDP())
expect(isCDP).toBeDefined()

// const runInIDE = process.argv.some(
// a => a === '--runTestsByPath' || a.includes('IDEA') || a.includes('processChild.js'),
Expand Down
42 changes: 27 additions & 15 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,44 @@ import { isServerSide } from '@naturalcycles/js-lib'
*/
class BotDetectionService {
isBotOrCDP(): boolean {
return this.isBot() || this.isCDP()
return !!this.isBot() || this.isCDP()
}

isBot(): boolean {
/**
* Returns undefined if it's not a Bot,
* otherwise a truthy BotReason.
*/
isBot(): BotReason | undefined {
// SSR - not a bot
if (isServerSide()) return false
if (isServerSide()) return
const { navigator } = globalThis
if (!navigator) return false
if (!navigator) return BotReason.NoNavigator
const { userAgent } = navigator
if (!userAgent) return true
if (!userAgent) return BotReason.NoUserAgent

if (/headless/i.test(userAgent)) return true
if (/electron/i.test(userAgent)) return true
if (/phantom/i.test(userAgent)) return true
if (/slimer/i.test(userAgent)) return true
if (/headless/i.test(userAgent)) return BotReason.UserAgent
if (/electron/i.test(userAgent)) return BotReason.UserAgent
if (/phantom/i.test(userAgent)) return BotReason.UserAgent
if (/slimer/i.test(userAgent)) return BotReason.UserAgent

if (navigator.webdriver) {
return true
return BotReason.WebDriver
}

if (navigator.plugins?.length === 0) {
return true // Headless Chrome
return BotReason.ZeroPlugins // Headless Chrome
}

if ((navigator.languages as any) === '') {
return true // Headless Chrome
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
if (userAgent.includes('Chrome') && !(globalThis as any).chrome) {
return true // Headless Chrome
return BotReason.ChromeWithoutChrome // Headless Chrome
}

return false
}

/**
Expand Down Expand Up @@ -86,3 +88,13 @@ class BotDetectionService {
}

export const botDetectionService = new BotDetectionService()

export enum BotReason {
NoNavigator = 1,
NoUserAgent = 2,
UserAgent = 3,
WebDriver = 4,
ZeroPlugins = 5,
EmptyLanguages = 6,
ChromeWithoutChrome = 7,
}

0 comments on commit 957cfdc

Please sign in to comment.