diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22621e88..a3fb6da0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ jobs: release: runs-on: ubuntu-20.04 timeout-minutes: 15 - environment: main + environment: dev steps: - name: Cancel Previous Runs diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 00000000..0bced46f --- /dev/null +++ b/src/api.ts @@ -0,0 +1,74 @@ +import { + AbstractDetectorDict, + AbstractSourceDict, + BotdError, + BotDetectionResult, + BotKind, + Component, + ComponentDict, + DetectionDict, + State, +} from './types' + +export function detect>( + components: T, + detectors: K, +): [DetectionDict, BotDetectionResult] { + 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 + } + } + + return [detections, finalDetection] +} + +export async function collect(sources: T): Promise> { + 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) 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 +} diff --git a/src/detector.ts b/src/detector.ts index 6d5b476e..7d0793c9 100644 --- a/src/detector.ts +++ b/src/detector.ts @@ -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. @@ -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 */ @@ -48,35 +30,9 @@ 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 } @@ -84,37 +40,7 @@ export default class BotDetector implements BotDetectorInterface { * @inheritdoc */ public async collect(): Promise { - 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) 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 } } diff --git a/src/detectors/eval_length.ts b/src/detectors/eval_length.ts index 741aa652..bdae192b 100644 --- a/src/detectors/eval_length.ts +++ b/src/detectors/eval_length.ts @@ -1,18 +1,23 @@ 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) { - return false - } + if (browserEngineKind.value === BrowserEngineKind.Unknown) 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)) ) } diff --git a/src/detectors/notification_permissions.ts b/src/detectors/notification_permissions.ts index d6a6d2de..dbf63416 100644 --- a/src/detectors/notification_permissions.ts +++ b/src/detectors/notification_permissions.ts @@ -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 diff --git a/src/detectors/plugins_inconsistency.ts b/src/detectors/plugins_inconsistency.ts index 8249e1cf..69d5eebb 100644 --- a/src/detectors/plugins_inconsistency.ts +++ b/src/detectors/plugins_inconsistency.ts @@ -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 } diff --git a/src/detectors/product_sub.ts b/src/detectors/product_sub.ts index 238cc0dd..c7d9c079 100644 --- a/src/detectors/product_sub.ts +++ b/src/detectors/product_sub.ts @@ -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 diff --git a/src/detectors/rtt.ts b/src/detectors/rtt.ts index 6195cbe2..9237658a 100644 --- a/src/detectors/rtt.ts +++ b/src/detectors/rtt.ts @@ -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 } diff --git a/src/detectors/window_size.ts b/src/detectors/window_size.ts index 2a08d155..80b87a17 100644 --- a/src/detectors/window_size.ts +++ b/src/detectors/window_size.ts @@ -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 } diff --git a/src/index.ts b/src/index.ts index 7960f33a..25340970 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 @@ -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, diff --git a/src/sources/index.ts b/src/sources/index.ts index 8db41ca0..1dddac88 100644 --- a/src/sources/index.ts +++ b/src/sources/index.ts @@ -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, diff --git a/src/types.ts b/src/types.ts index 537ba3d9..28e767de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,11 +87,11 @@ export type DefaultDetectorDict = typeof detectors */ export type SourceResponse = T extends (...args: any[]) => any ? Awaited> : T -export type AbstractDetector = (...args: any[]) => DetectorResponse +export type AbstractDetector = (components: T) => DetectorResponse export type AbstractSourceDict = Record> -export type AbstractDetectorDict = Record +export type AbstractDetectorDict = Record> export type AbstractComponentDict = Record> @@ -100,7 +100,10 @@ export type AbstractDetectionsDict = Record /** * Represents a dictionary of detectors detection. */ -export type DetectionDict = Record +export type DetectionDict = DefaultDetectorDict> = Record< + keyof T, + BotDetectionResult +> /** * Dictionary of components. diff --git a/src/utils/browser.ts b/src/utils/browser.ts index aefb336d..fc78635f 100644 --- a/src/utils/browser.ts +++ b/src/utils/browser.ts @@ -52,7 +52,11 @@ export function getBrowserEngineKind(): BrowserEngineKind { export function getBrowserKind(): BrowserKind { const userAgent = navigator.userAgent?.toLowerCase() - if (strIncludes(userAgent, 'wechat')) { + if (strIncludes(userAgent, 'safari')) { + return BrowserKind.Safari + } else if (strIncludes(userAgent, 'trident') || strIncludes(userAgent, 'msie')) { + return BrowserKind.IE + } else if (strIncludes(userAgent, 'wechat')) { return BrowserKind.WeChat } else if (strIncludes(userAgent, 'firefox')) { return BrowserKind.Firefox @@ -60,10 +64,6 @@ export function getBrowserKind(): BrowserKind { return BrowserKind.Opera } else if (strIncludes(userAgent, 'chrome')) { return BrowserKind.Chrome - } else if (strIncludes(userAgent, 'safari')) { - return BrowserKind.Safari - } else if (strIncludes(userAgent, 'trident') || strIncludes(userAgent, 'msie')) { - return BrowserKind.IE } else { return BrowserKind.Unknown }