Skip to content

Commit

Permalink
feat: expose detectors and separate detect and collect functions (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
Le0Developer authored Nov 5, 2023
1 parent 66e0f2a commit bfc36e8
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 117 deletions.
74 changes: 74 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
AbstractDetectorDict,
AbstractSourceDict,
BotdError,
BotDetectionResult,
BotKind,
Component,
ComponentDict,
DetectionDict,
State,
} from './types'

export function detect<T extends ComponentDict, K extends AbstractDetectorDict<T>>(
components: T,
detectors: K,
): [DetectionDict<K>, BotDetectionResult] {
const detections = {} as DetectionDict<K>
let finalDetection: BotDetectionResult = {
bot: false,
}

for (const detectorName in detectors) {
const detector = detectors[detectorName as keyof typeof detectors]
const detectorRes = detector(components)

let detection: BotDetectionResult = { bot: false }

if (typeof detectorRes === 'string') {
detection = { bot: true, botKind: detectorRes }
} else if (detectorRes) {
detection = { bot: true, botKind: BotKind.Unknown }
}

detections[detectorName as keyof typeof detectors] = detection

if (detection.bot) {
finalDetection = detection
}
}

return [detections, finalDetection]
}

export async function collect<T extends AbstractSourceDict>(sources: T): Promise<ComponentDict<T>> {
const components = {} as ComponentDict<T>
const sourcesKeys = Object.keys(sources) as (keyof typeof sources)[]

await Promise.all(
sourcesKeys.map(async (sourceKey) => {
const res = sources[sourceKey]

try {
components[sourceKey] = ({
value: await res(),
state: State.Success,
} as Component<any>) as any
} catch (error) {
if (error instanceof BotdError) {
components[sourceKey] = {
state: error.state,
error: `${error.name}: ${error.message}`,
}
} else {
components[sourceKey] = {
state: State.UnexpectedBehaviour,
error: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
}
}
}
}),
)

return components
}
82 changes: 4 additions & 78 deletions src/detector.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { BotDetectionResult, BotDetectorInterface, ComponentDict, DetectionDict } from './types'
import { collect, detect } from './api'
import { detectors } from './detectors'
import { sources } from './sources'
import {
BotdError,
BotDetectionResult,
BotDetectorInterface,
BotKind,
Component,
ComponentDict,
DetectionDict,
State,
} from './types'

/**
* Class representing a bot detector.
Expand All @@ -30,16 +22,6 @@ export default class BotDetector implements BotDetectorInterface {
return this.detections
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
protected getSources() {
return sources
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
protected getDetectors() {
return detectors
}

/**
* @inheritdoc
*/
Expand All @@ -48,73 +30,17 @@ export default class BotDetector implements BotDetectorInterface {
throw new Error("BotDetector.detect can't be called before BotDetector.collect")
}

const components = this.components
const detectors = this.getDetectors()

const detections = {} as DetectionDict
let finalDetection: BotDetectionResult = {
bot: false,
}

for (const detectorName in detectors) {
const detector = detectors[detectorName as keyof typeof detectors]
const detectorRes = detector(components)

let detection: BotDetectionResult = { bot: false }

if (typeof detectorRes === 'string') {
detection = { bot: true, botKind: detectorRes }
} else if (detectorRes) {
detection = { bot: true, botKind: BotKind.Unknown }
}

detections[detectorName as keyof typeof detectors] = detection

if (detection.bot) {
finalDetection = detection
}
}
const [detections, finalDetection] = detect(this.components, detectors)

this.detections = detections

return finalDetection
}

/**
* @inheritdoc
*/
public async collect(): Promise<ComponentDict> {
const sources = this.getSources()
const components = {} as ComponentDict

const sourcesKeys = Object.keys(sources) as (keyof typeof sources)[]

await Promise.all(
sourcesKeys.map(async (sourceKey) => {
const res = sources[sourceKey]

try {
components[sourceKey] = ({
value: await res(),
state: State.Success,
} as Component<any>) as any
} catch (error) {
if (error instanceof BotdError) {
components[sourceKey] = {
state: error.state,
error: `${error.name}: ${error.message}`,
}
} else {
components[sourceKey] = {
state: State.UnexpectedBehaviour,
error: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
}
}
}
}),
)

this.components = components
this.components = await collect(sources)
return this.components
}
}
26 changes: 16 additions & 10 deletions src/detectors/eval_length.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { arrayIncludes } from '../utils/ponyfills'
import { BrowserEngineKind, BrowserKind, ComponentDict, DetectorResponse, State } from '../types'
import { getBrowserEngineKind, getBrowserKind } from '../utils/browser'

export function detectEvalLengthInconsistency({ evalLength }: ComponentDict): DetectorResponse {
if (evalLength.state !== State.Success) return
export function detectEvalLengthInconsistency({
evalLength,
browserKind,
browserEngineKind,
}: ComponentDict): DetectorResponse {
if (
evalLength.state !== State.Success ||
browserKind.state !== State.Success ||
browserEngineKind.state !== State.Success
)
return

const length = evalLength.value
const browser = getBrowserKind()
const browserEngine = getBrowserEngineKind()
if (browserEngine == BrowserEngineKind.Unknown) {
if (browserEngineKind.value === BrowserEngineKind.Unknown)

Check failure on line 17 in src/detectors/eval_length.ts

View workflow job for this annotation

GitHub Actions / build

Delete `⏎···`
return false
}
return (
(length === 37 && !arrayIncludes([BrowserEngineKind.Webkit, BrowserEngineKind.Gecko], browserEngine)) ||
(length === 39 && !arrayIncludes([BrowserKind.IE], browser)) ||
(length === 33 && !arrayIncludes([BrowserEngineKind.Chromium], browserEngine))
(length === 37 && !arrayIncludes([BrowserEngineKind.Webkit, BrowserEngineKind.Gecko], browserEngineKind.value)) ||
(length === 39 && !arrayIncludes([BrowserKind.IE], browserKind.value)) ||
(length === 33 && !arrayIncludes([BrowserEngineKind.Chromium], browserEngineKind.value))
)
}
9 changes: 5 additions & 4 deletions src/detectors/notification_permissions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { BotKind, BrowserKind, ComponentDict, DetectorResponse, State } from '../types'
import { getBrowserKind } from '../utils/browser'

export function detectNotificationPermissions({ notificationPermissions }: ComponentDict): DetectorResponse {
const browserKind = getBrowserKind()
if (browserKind !== BrowserKind.Chrome) return false
export function detectNotificationPermissions({
notificationPermissions,
browserKind,
}: ComponentDict): DetectorResponse {
if (browserKind.state !== State.Success || browserKind.value !== BrowserKind.Chrome) return false

if (notificationPermissions.state === State.Success && notificationPermissions.value) {
return BotKind.HeadlessChrome
Expand Down
25 changes: 19 additions & 6 deletions src/detectors/plugins_inconsistency.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { BotKind, BrowserEngineKind, BrowserKind, ComponentDict, DetectorResponse, State } from '../types'
import { getBrowserEngineKind, getBrowserKind, isAndroid } from '../utils/browser'

export function detectPluginsLengthInconsistency({ pluginsLength }: ComponentDict): DetectorResponse {
if (pluginsLength.state !== State.Success) return
const browserKind = getBrowserKind()
const browserEngineKind = getBrowserEngineKind()
if (browserKind !== BrowserKind.Chrome || isAndroid() || browserEngineKind !== BrowserEngineKind.Chromium) return
export function detectPluginsLengthInconsistency({
pluginsLength,
android,
browserKind,
browserEngineKind,
}: ComponentDict): DetectorResponse {
if (
pluginsLength.state !== State.Success ||
android.state !== State.Success ||
browserKind.state !== State.Success ||
browserEngineKind.state !== State.Success
)
return
if (
browserKind.value !== BrowserKind.Chrome ||
android.value ||
browserEngineKind.value !== BrowserEngineKind.Chromium
)
return
if (pluginsLength.value === 0) return BotKind.HeadlessChrome
}
14 changes: 6 additions & 8 deletions src/detectors/product_sub.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { BotKind, BrowserKind, ComponentDict, DetectorResponse, State } from '../types'
import { getBrowserKind } from '../utils/browser'

export function detectProductSub({ productSub }: ComponentDict): DetectorResponse {
if (productSub.state !== State.Success) return false
const browserKind = getBrowserKind()
export function detectProductSub({ productSub, browserKind }: ComponentDict): DetectorResponse {
if (productSub.state !== State.Success || browserKind.state !== State.Success) return false
if (
(browserKind === BrowserKind.Chrome ||
browserKind === BrowserKind.Safari ||
browserKind === BrowserKind.Opera ||
browserKind === BrowserKind.WeChat) &&
(browserKind.value === BrowserKind.Chrome ||
browserKind.value === BrowserKind.Safari ||
browserKind.value === BrowserKind.Opera ||
browserKind.value === BrowserKind.WeChat) &&
productSub.value !== '20030107'
)
return BotKind.Unknown
Expand Down
7 changes: 3 additions & 4 deletions src/detectors/rtt.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { BotKind, ComponentDict, DetectorResponse, State } from '../types'
import { isAndroid } from '../utils/browser'

export function detectRTT({ rtt }: ComponentDict): DetectorResponse {
if (rtt.state !== State.Success) return
export function detectRTT({ rtt, android }: ComponentDict): DetectorResponse {
if (rtt.state !== State.Success || android.state !== State.Success) return
// Rtt is 0 on android webview
if (isAndroid()) return
if (android.value) return
if (rtt.value === 0) return BotKind.HeadlessChrome
}
7 changes: 3 additions & 4 deletions src/detectors/window_size.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { BotKind, ComponentDict, DetectorResponse, State } from '../types'
import { getDocumentFocus } from '../utils/browser'

export function detectWindowSize({ windowSize }: ComponentDict): DetectorResponse {
if (windowSize.state !== State.Success) return false
export function detectWindowSize({ windowSize, documentFocus }: ComponentDict): DetectorResponse {
if (windowSize.state !== State.Success || documentFocus.state !== State.Success) return false
const { outerWidth, outerHeight } = windowSize.value
// When a page is opened in a new tab without focusing it right away, the window outer size is 0x0
if (!getDocumentFocus()) return
if (!documentFocus.value) return
if (outerWidth === 0 && outerHeight === 0) return BotKind.HeadlessChrome
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { version } from '../package.json'
import BotDetector from './detector'
import { sources, WindowSizePayload, ProcessPayload, DistinctivePropertiesPayload } from './sources'
import { detectors } from './detectors'
import { BotdError, BotDetectorInterface, BotKind, BotDetectionResult } from './types'
import { collect, detect } from './api'

/**
* Sends an unpersonalized AJAX request to collect installation statistics
Expand Down Expand Up @@ -37,6 +39,9 @@ export default { load }
/** Not documented, out of Semantic Versioning, usage is at your own risk */
export {
sources,
detectors,
collect,
detect,
BotdError,
WindowSizePayload,
ProcessPayload,
Expand Down
5 changes: 5 additions & 0 deletions src/sources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ import getWebGL from './webgl'
import getWindowExternal from './window_external'
import getWindowSize, { WindowSizePayload } from './window_size'
import checkDistinctiveProperties, { DistinctivePropertiesPayload } from './distinctive_properties'
import { getBrowserEngineKind, getBrowserKind, getDocumentFocus, isAndroid } from '../utils/browser'

export const sources = {
android: isAndroid,
browserKind: getBrowserKind,
browserEngineKind: getBrowserEngineKind,
documentFocus: getDocumentFocus,
userAgent: getUserAgent,
appVersion: getAppVersion,
rtt: getRTT,
Expand Down
9 changes: 6 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ export type DefaultDetectorDict = typeof detectors
*/
export type SourceResponse<T> = T extends (...args: any[]) => any ? Awaited<ReturnType<T>> : T

export type AbstractDetector = (...args: any[]) => DetectorResponse
export type AbstractDetector<T> = (components: T) => DetectorResponse

export type AbstractSourceDict = Record<string, SourceResponse<any>>

export type AbstractDetectorDict = Record<string, AbstractDetector>
export type AbstractDetectorDict<T> = Record<string, AbstractDetector<T>>

export type AbstractComponentDict = Record<string, Component<any>>

Expand All @@ -100,7 +100,10 @@ export type AbstractDetectionsDict = Record<string, BotDetectionResult>
/**
* Represents a dictionary of detectors detection.
*/
export type DetectionDict<T extends AbstractDetectorDict = DefaultDetectorDict> = Record<keyof T, BotDetectionResult>
export type DetectionDict<T extends AbstractDetectorDict<any> = DefaultDetectorDict> = Record<
keyof T,
BotDetectionResult
>

/**
* Dictionary of components.
Expand Down

0 comments on commit bfc36e8

Please sign in to comment.