UNPKG

smart-browser-detection

Version:

Smart browser detection library with anti-spoofing capabilities, multi-method detection, and mobile browser support. Outperforms UA-Parser-JS with superior accuracy and spoofing resistance.

920 lines (825 loc) 27.9 kB
/** * Smart Browser Detection Library * Advanced browser detection with anti-spoofing capabilities * * @version 1.0.0 * @author Smart Browser Detection Team * @license MIT */ // Type definitions export interface BrowserDetectionResult { browser: string; browserVersion: string; engine: string; engineVersion: string; platform: string; os: string; osVersion: string; isMobile: boolean; isTablet: boolean; isDesktop: boolean; confidence: number; detectionMethods: string[]; userAgent: string; vendor: string; timestamp: number; } export interface DetectionMethodResult { browser: string; confidence: number; method: string; } export interface EngineInfo { engine: string; version: string; } export interface OSInfo { os: string; osVersion: string; } export interface CacheStats { size: number; keys: string[]; } export interface RegexPatterns { chrome: RegExp; firefox: RegExp; safari: RegExp; edge: RegExp; opera: RegExp; ie: RegExp; android: RegExp; ios: RegExp; windows: RegExp; macos: RegExp; } export type DetectionMethod = 'apiDetection' | 'vendorDetection' | 'userAgentDetection' | 'cssDetection'; export type BrowserType = 'Chrome' | 'Firefox' | 'Safari' | 'Edge' | 'Opera' | 'Internet Explorer' | 'Unknown'; export type PlatformType = 'Desktop' | 'Mobile' | 'Tablet'; export type OSType = 'Windows' | 'macOS' | 'Linux' | 'Android' | 'iOS' | 'Unknown'; export type EngineType = 'Blink' | 'Gecko' | 'WebKit' | 'Trident' | 'Unknown'; // Browser-specific API interfaces interface ChromeAPI { runtime?: { onConnect?: EventTarget; }; webstore?: { install?: (url: string, callback?: (error?: Error) => void) => void; }; } interface SafariAPI { pushNotification?: { permission?: string; requestPermission?: () => Promise<string>; }; } interface OperaAPI { version?: string; } interface UserAgentData { brands?: Array<{ brand: string; version: string }>; mobile?: boolean; platform?: string; } // Extended Window interface declare global { interface Window { chrome?: ChromeAPI; InstallTrigger?: boolean; safari?: SafariAPI; opera?: OperaAPI; } interface Navigator { userAgentData?: UserAgentData; } interface Document { documentMode?: number; } interface CSSStyleDeclaration { webkitTransform: string; webkitAppearance: string; } } /** * Smart Browser Detection Class * Provides multi-method browser detection with confidence scoring */ export class SmartBrowserDetection { private cache: Map<string, BrowserDetectionResult>; private detectionMethods: DetectionMethod[]; private regexPatterns: RegexPatterns; constructor() { this.cache = new Map(); this.detectionMethods = [ 'apiDetection', 'vendorDetection', 'userAgentDetection', 'cssDetection' ]; // Performance optimization: pre-compile regex patterns this.regexPatterns = { chrome: /(?:chrome|crios)\/(\d+\.\d+(?:\.\d+)?)/i, firefox: /(?:firefox|fxios)\/(\d+\.\d+(?:\.\d+)?)/i, safari: /(?:version|safari)\/(\d+\.\d+(?:\.\d+)?)/i, edge: /(?:edg|edge)\/(\d+\.\d+(?:\.\d+)?)/i, opera: /(?:opera|opr)\/(\d+\.\d+(?:\.\d+)?)/i, ie: /(?:msie\s(\d+\.\d+)|rv:(\d+\.\d+))/i, android: /android\s(\d+\.\d+(?:\.\d+)?)/i, ios: /os\s(\d+_\d+)/i, windows: /windows nt (10\.0|6\.3|6\.2|6\.1)/i, macos: /mac os x 10_(\d+)/i }; } /** * Main detection function that combines multiple methods * @returns Browser detection result with confidence scoring */ public detectBrowser(): BrowserDetectionResult { const cacheKey = 'browser_detection'; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey)!; } const result: BrowserDetectionResult = { browser: 'Unknown', browserVersion: 'Unknown', engine: 'Unknown', engineVersion: 'Unknown', platform: 'Unknown', os: 'Unknown', osVersion: 'Unknown', isMobile: false, isTablet: false, isDesktop: false, confidence: 0, detectionMethods: [], userAgent: navigator.userAgent, vendor: navigator.vendor, timestamp: Date.now() }; // Collect all method results const methodResults: DetectionMethodResult[] = []; // Try each detection method for (const method of this.detectionMethods) { try { let methodResult: DetectionMethodResult | null = null; switch (method) { case 'apiDetection': methodResult = this.apiDetection(); break; case 'vendorDetection': methodResult = this.vendorDetection(); break; case 'userAgentDetection': methodResult = this.userAgentDetection(); break; case 'cssDetection': methodResult = this.cssDetection(); break; } if (methodResult && methodResult.browser !== 'Unknown') { methodResults.push(methodResult); } } catch (error) { // Silent fail for detection methods to ensure robustness console.warn(`Detection method ${method} failed:`, error); } } // Smart result selection based on method priority and confidence if (methodResults.length > 0) { const finalResult = this.selectBestResult(methodResults); Object.assign(result, finalResult); } // Determine device type result.isMobile = this.detectMobile(); result.isTablet = this.detectTablet(); result.isDesktop = !result.isMobile && !result.isTablet; // Add engine information const engineInfo = this.getEngineInfo(result.browser as BrowserType); result.engine = engineInfo.engine; result.engineVersion = engineInfo.version; // Add OS information const osInfo = this.getOSInfo(); result.os = osInfo.os; result.osVersion = osInfo.osVersion; // Add browser version result.browserVersion = this.getBrowserVersion(result.browser as BrowserType); this.cache.set(cacheKey, result); return result; } /** * Smart result selection algorithm with priority handling * @param methodResults - Array of detection method results * @returns Best detection result */ private selectBestResult(methodResults: DetectionMethodResult[]): Partial<BrowserDetectionResult> { // Count detections per browser const browserCounts: Record<string, number> = {}; const browserConfidences: Record<string, number> = {}; const browserResults: Record<string, DetectionMethodResult> = {}; methodResults.forEach((result) => { const browser = result.browser; browserCounts[browser] = (browserCounts[browser] || 0) + 1; browserConfidences[browser] = Math.max( browserConfidences[browser] || 0, result.confidence ); browserResults[browser] = result; }); // Find browser with highest count and confidence let bestBrowser = 'Unknown'; let bestScore = 0; Object.keys(browserCounts).forEach((browser) => { const count = browserCounts[browser] || 0; const confidence = browserConfidences[browser] || 0; const score = count * confidence; // Special case: If Edge is detected by at least 2 methods, prioritize it if (browser === 'Edge' && count >= 2) { bestBrowser = browser; bestScore = score; return; } // Special case: Chrome with high confidence from API detection if (browser === 'Chrome' && confidence >= 0.9) { bestBrowser = browser; bestScore = score; return; } if (score > bestScore) { bestBrowser = browser; bestScore = score; } }); // Get the best result for the selected browser const bestResult = browserResults[bestBrowser]; return { browser: bestBrowser, confidence: bestResult ? bestResult.confidence : 0, detectionMethods: methodResults .filter((r) => r.browser === bestBrowser) .map((r) => r.method) }; } /** * API-based detection using browser-specific APIs * @returns API detection result */ private apiDetection(): DetectionMethodResult { const result: DetectionMethodResult = { browser: 'Unknown', confidence: 0, method: 'apiDetection' }; // Firefox-specific APIs (check first to avoid false positives) if (window.InstallTrigger !== undefined) { result.browser = 'Firefox'; result.confidence = 0.95; } // Chrome-specific APIs else if (window.chrome && window.chrome.runtime && window.chrome.runtime.onConnect) { result.browser = 'Chrome'; result.confidence = 0.95; } // Safari-specific APIs else if (window.safari && window.safari.pushNotification) { result.browser = 'Safari'; result.confidence = 0.95; } // Edge-specific APIs (with fallback for mobile) else if (navigator.userAgentData && navigator.userAgentData.brands) { const brands = navigator.userAgentData.brands; const hasEdge = brands.some((brand) => brand.brand === 'Microsoft Edge'); if (hasEdge) { result.browser = 'Edge'; result.confidence = 0.95; } } // Opera detection else if (window.opera && window.opera.version) { result.browser = 'Opera'; result.confidence = 0.9; } // Internet Explorer detection else if (document.documentMode) { result.browser = 'Internet Explorer'; result.confidence = 0.95; } return result; } /** * Vendor-based detection using navigator.vendor and other vendor info * @returns Vendor detection result */ private vendorDetection(): DetectionMethodResult { const result: DetectionMethodResult = { browser: 'Unknown', confidence: 0, method: 'vendorDetection' }; const vendor = navigator.vendor.toLowerCase(); const ua = navigator.userAgent.toLowerCase(); // Edge detection: UA contains 'edg' takes precedence if (ua.includes('edg')) { result.browser = 'Edge'; result.confidence = 0.9; return result; } if (vendor.includes('google')) { result.browser = 'Chrome'; result.confidence = 0.9; } else if (vendor.includes('apple')) { result.browser = 'Safari'; result.confidence = 0.9; } else if (vendor.includes('mozilla')) { result.browser = 'Firefox'; result.confidence = 0.8; } else if (vendor === '') { // Edge often has an empty vendor string if (ua.includes('edg') || ua.includes('edge')) { result.browser = 'Edge'; result.confidence = 0.8; } // Edge mobile on Android might have empty vendor else if (ua.includes('android') && ua.includes('chrome') && ua.includes('safari')) { result.browser = 'Edge'; result.confidence = 0.7; } } return result; } /** * Enhanced user agent detection with better parsing * @returns User agent detection result */ private userAgentDetection(): DetectionMethodResult { const result: DetectionMethodResult = { browser: 'Unknown', confidence: 0, method: 'userAgentDetection' }; const ua = navigator.userAgent.toLowerCase(); // Firefox detection (prioritize over Safari to avoid false positives) if (ua.includes('firefox') || ua.includes('fxios')) { result.browser = 'Firefox'; result.confidence = 0.85; return result; } // Edge detection (prioritize 'edg' over 'chrome') if (ua.includes('edg')) { result.browser = 'Edge'; result.confidence = 0.85; return result; } // Opera detection if (ua.includes('opr') || ua.includes('opera')) { result.browser = 'Opera'; result.confidence = 0.85; return result; } // Internet Explorer detection if (ua.includes('msie') || ua.includes('trident')) { result.browser = 'Internet Explorer'; result.confidence = 0.95; return result; } // Chrome detection (including mobile Chrome) if (ua.includes('chrome') && !ua.includes('edg') && !ua.includes('opr')) { result.browser = 'Chrome'; result.confidence = 0.8; if (ua.includes('crios')) { result.browser = 'Chrome'; result.confidence = 0.95; } return result; } // Special case: Chrome mobile emulation (UA shows Safari but vendor shows Google) if (ua.includes('safari') && navigator.vendor.toLowerCase().includes('google')) { result.browser = 'Chrome'; result.confidence = 0.85; return result; } // Edge detection (multiple patterns) if (ua.includes('edge')) { result.browser = 'Edge'; result.confidence = 0.85; return result; } // Edge mobile on Android (might look like Chrome) if (ua.includes('android') && ua.includes('chrome') && ua.includes('safari') && !ua.includes('firefox') && navigator.vendor === '') { result.browser = 'Edge'; result.confidence = 0.8; return result; } // Safari detection (last resort, after all other browsers) if (ua.includes('safari') && !ua.includes('chrome')) { result.browser = 'Safari'; result.confidence = 0.7; return result; } return result; } /** * CSS-based detection using browser-specific CSS features * @returns CSS detection result */ private cssDetection(): DetectionMethodResult { const result: DetectionMethodResult = { browser: 'Unknown', confidence: 0, method: 'cssDetection' }; try { // Create a test element const testElement = document.createElement('div'); testElement.style.cssText = 'position:absolute;top:-9999px;left:-9999px;'; document.body.appendChild(testElement); // Chrome-specific CSS testElement.style.webkitTransform = 'translateZ(0)'; if (testElement.style.webkitTransform !== '') { if (window.chrome && window.chrome.webstore) { result.browser = 'Chrome'; result.confidence = 0.7; } } // Safari-specific CSS testElement.style.webkitAppearance = 'none'; if (testElement.style.webkitAppearance !== '') { if (window.safari && window.safari.pushNotification) { result.browser = 'Safari'; result.confidence = 0.7; } } // Edge-specific CSS (Edge supports webkit prefixes but has unique behavior) if (navigator.userAgent.toLowerCase().includes('edg') || navigator.userAgent.toLowerCase().includes('edge')) { result.browser = 'Edge'; result.confidence = 0.7; } // Edge mobile on Android pattern else if (navigator.userAgent.toLowerCase().includes('android') && navigator.userAgent.toLowerCase().includes('chrome') && navigator.userAgent.toLowerCase().includes('safari') && navigator.vendor === '') { result.browser = 'Edge'; result.confidence = 0.6; } // Clean up if (testElement.parentNode) { testElement.parentNode.removeChild(testElement); } } catch { // Silent fail for CSS detection } return result; } /** * Detect if running on mobile device * @returns True if mobile device */ public detectMobile(): boolean { const ua = navigator.userAgent.toLowerCase(); const mobileKeywords = [ 'mobile', 'android', 'iphone', 'ipod', 'blackberry', 'windows phone', 'opera mini', 'iemobile' ]; const hasMobileKeyword = mobileKeywords.some(keyword => ua.includes(keyword)); const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isSmallScreen = window.innerWidth <= 768; return hasMobileKeyword || (hasTouch && isSmallScreen); } /** * Detect if running on tablet device * @returns True if tablet device */ public detectTablet(): boolean { const ua = navigator.userAgent.toLowerCase(); const tabletKeywords = ['ipad', 'tablet', 'playbook', 'silk']; const hasTabletKeyword = tabletKeywords.some(keyword => ua.includes(keyword)); const isTabletSize = window.innerWidth > 768 && window.innerWidth <= 1024; const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0; return hasTabletKeyword || (hasTouch && isTabletSize); } /** * Get browser version from user agent * @param browser - Browser name * @returns Browser version */ public getBrowserVersion(browser: BrowserType): string { // Try to get version from navigator.userAgentData first (modern browsers) if (navigator.userAgentData && navigator.userAgentData.brands) { const brands = navigator.userAgentData.brands; for (const brand of brands) { if (brand.brand.toLowerCase() === browser.toLowerCase()) { return brand.version; } // Handle Edge case if (browser === 'Edge' && brand.brand === 'Microsoft Edge') { return brand.version; } // Handle Chrome case if (browser === 'Chrome' && brand.brand === 'Google Chrome') { return brand.version; } } } const ua = navigator.userAgent; let version = 'Unknown'; if (browser === 'Chrome') { // Try multiple patterns for Chrome let match = ua.match(this.regexPatterns.chrome); if (!match) { // Fallback for Chrome mobile match = ua.match(/crios\/(\d+\.\d+(?:\.\d+)?)/i); } if (!match) { // Fallback for Chrome desktop match = ua.match(/chrome\/(\d+\.\d+(?:\.\d+)?)/i); } version = match && match[1] ? match[1] : 'Unknown'; } else if (browser === 'Firefox') { // Try multiple patterns for Firefox let match = ua.match(this.regexPatterns.firefox); if (!match) { // Fallback for Firefox mobile match = ua.match(/fxios\/(\d+\.\d+(?:\.\d+)?)/i); } if (!match) { // Fallback for Firefox desktop match = ua.match(/firefox\/(\d+\.\d+(?:\.\d+)?)/i); } version = match && match[1] ? match[1] : 'Unknown'; } else if (browser === 'Safari') { // Try multiple patterns for Safari let match = ua.match(this.regexPatterns.safari); if (!match) { // Fallback for Safari version match = ua.match(/version\/(\d+\.\d+(?:\.\d+)?)/i); } if (!match) { // Fallback for Safari mobile match = ua.match(/mobile\/\w+\s(\d+\.\d+(?:\.\d+)?)/i); } version = match && match[1] ? match[1] : 'Unknown'; } else if (browser === 'Edge') { // Try multiple patterns for Edge let match = ua.match(this.regexPatterns.edge); if (!match) { // Fallback for Edge legacy match = ua.match(/edge\/(\d+\.\d+(?:\.\d+)?)/i); } if (!match) { // Fallback for Edge Chromium match = ua.match(/edg\/(\d+\.\d+(?:\.\d+)?)/i); } version = match && match[1] ? match[1] : 'Unknown'; } else if (browser === 'Opera') { // Try multiple patterns for Opera let match = ua.match(this.regexPatterns.opera); if (!match) { // Fallback for Opera mobile match = ua.match(/opr\/(\d+\.\d+(?:\.\d+)?)/i); } if (!match) { // Fallback for Opera desktop match = ua.match(/opera\/(\d+\.\d+(?:\.\d+)?)/i); } version = match && match[1] ? match[1] : 'Unknown'; } else if (browser === 'Internet Explorer') { // Try multiple patterns for IE let match = ua.match(this.regexPatterns.ie); if (!match) { // Fallback for IE 11 match = ua.match(/rv:(\d+\.\d+(?:\.\d+)?)/i); } if (!match) { // Fallback for older IE match = ua.match(/msie\s(\d+\.\d+(?:\.\d+)?)/i); } version = match && (match[1] || match[2]) ? (match[1] || match[2] || 'Unknown') : 'Unknown'; } // Additional fallback: try to extract any version number if still unknown if (version === 'Unknown' && browser !== 'Unknown') { const genericMatch = ua.match(/(\d+\.\d+(?:\.\d+)?)/); if (genericMatch && genericMatch[1]) { version = genericMatch[1]; } } return version; } /** * Get engine information for browser * @param browser - Browser name * @returns Engine information */ public getEngineInfo(browser: BrowserType): EngineInfo { const browserVersion = this.getBrowserVersion(browser); const engineMap: Record<BrowserType, EngineInfo> = { 'Chrome': { engine: 'Blink', version: browserVersion }, 'Edge': { engine: 'Blink', version: browserVersion }, 'Firefox': { engine: 'Gecko', version: browserVersion }, 'Safari': { engine: 'WebKit', version: browserVersion }, 'Opera': { engine: 'Blink', version: browserVersion }, 'Internet Explorer': { engine: 'Trident', version: browserVersion }, 'Unknown': { engine: 'Unknown', version: 'Unknown' } }; const result = engineMap[browser] || { engine: 'Unknown', version: 'Unknown' }; // If engine version is still unknown, try to extract from user agent if (result.version === 'Unknown' && browser !== 'Unknown') { const ua = navigator.userAgent; // Try to extract engine version from user agent if (result.engine === 'Blink') { const blinkMatch = ua.match(/blink\/(\d+\.\d+(?:\.\d+)?)/i); if (blinkMatch && blinkMatch[1]) { result.version = blinkMatch[1]; } } else if (result.engine === 'Gecko') { const geckoMatch = ua.match(/gecko\/(\d+\.\d+(?:\.\d+)?)/i); if (geckoMatch && geckoMatch[1]) { result.version = geckoMatch[1]; } } else if (result.engine === 'WebKit') { const webkitMatch = ua.match(/webkit\/(\d+\.\d+(?:\.\d+)?)/i); if (webkitMatch && webkitMatch[1]) { result.version = webkitMatch[1]; } } else if (result.engine === 'Trident') { const tridentMatch = ua.match(/trident\/(\d+\.\d+(?:\.\d+)?)/i); if (tridentMatch && tridentMatch[1]) { result.version = tridentMatch[1]; } } } return result; } /** * Get OS information * @returns OS information */ public getOSInfo(): OSInfo { const ua = navigator.userAgent.toLowerCase(); let os: OSType = 'Unknown'; let osVersion = 'Unknown'; if (/win/i.test(ua) || /windows/i.test(ua)) { os = 'Windows'; const match = ua.match(this.regexPatterns.windows); if (match) { if (match[1] === '10.0') { osVersion = 'Windows 10/11'; } else if (match[1] === '6.3') { osVersion = 'Windows 8.1'; } else if (match[1] === '6.2') { osVersion = 'Windows 8'; } else if (match[1] === '6.1') { osVersion = 'Windows 7'; } } else { // Fallback for Windows without specific version osVersion = 'Windows'; } } else if (/mac/i.test(ua) || /macintosh/i.test(ua)) { os = 'macOS'; const match = ua.match(this.regexPatterns.macos); if (match && match[1]) { const version = parseInt(match[1]); if (version >= 15) { osVersion = 'macOS Big Sur+'; } else if (version >= 14) { osVersion = 'macOS Mojave'; } else if (version >= 13) { osVersion = 'macOS High Sierra'; } else if (version >= 12) { osVersion = 'macOS Sierra'; } else if (version >= 11) { osVersion = 'macOS El Capitan'; } else if (version >= 10) { osVersion = 'macOS Yosemite'; } else { osVersion = `macOS ${version}`; } } else { // Fallback for macOS without specific version osVersion = 'macOS'; } } else if (/linux/i.test(ua) || /x11/i.test(ua)) { os = 'Linux'; // Try to detect specific Linux distributions if (ua.includes('ubuntu')) { osVersion = 'Ubuntu'; } else if (ua.includes('fedora')) { osVersion = 'Fedora'; } else if (ua.includes('debian')) { osVersion = 'Debian'; } else if (ua.includes('centos')) { osVersion = 'CentOS'; } else if (ua.includes('redhat')) { osVersion = 'Red Hat'; } else { osVersion = 'Linux'; } } else if (/android/i.test(ua)) { os = 'Android'; const match = ua.match(this.regexPatterns.android); if (match && match[1]) { const version = parseFloat(match[1]); if (version >= 14) { osVersion = `Android ${match[1]} (Android 14+)`; } else if (version >= 13) { osVersion = `Android ${match[1]} (Android 13)`; } else if (version >= 12) { osVersion = `Android ${match[1]} (Android 12)`; } else if (version >= 11) { osVersion = `Android ${match[1]} (Android 11)`; } else if (version >= 10) { osVersion = `Android ${match[1]} (Android 10)`; } else { osVersion = `Android ${match[1]}`; } } else { osVersion = 'Android'; } } else if (/iphone|ipad|ipod/i.test(ua)) { os = 'iOS'; const match = ua.match(this.regexPatterns.ios); if (match && match[1]) { const version = match[1].replace(/_/g, '.'); const versionNum = parseFloat(version); if (versionNum >= 17) { osVersion = `iOS ${version} (iOS 17+)`; } else if (versionNum >= 16) { osVersion = `iOS ${version} (iOS 16)`; } else if (versionNum >= 15) { osVersion = `iOS ${version} (iOS 15)`; } else if (versionNum >= 14) { osVersion = `iOS ${version} (iOS 14)`; } else { osVersion = `iOS ${version}`; } } else { osVersion = 'iOS'; } } return { os, osVersion }; } /** * Get platform type * @returns Platform type */ public getPlatform(): PlatformType { if (this.detectMobile()) return 'Mobile'; if (this.detectTablet()) return 'Tablet'; return 'Desktop'; } /** * Get complete browser information * @returns Complete browser information */ public getCompleteInfo(): BrowserDetectionResult { const detection = this.detectBrowser(); const version = this.getBrowserVersion(detection.browser as BrowserType); const { os, osVersion } = this.getOSInfo(); const platform = this.getPlatform(); const engineInfo = this.getEngineInfo(detection.browser as BrowserType); return { browser: detection.browser, browserVersion: version, engine: engineInfo.engine, engineVersion: engineInfo.version, platform: platform, os: os, osVersion: osVersion, isMobile: detection.isMobile, isTablet: detection.isTablet, isDesktop: detection.isDesktop, confidence: detection.confidence, detectionMethods: detection.detectionMethods, userAgent: navigator.userAgent, vendor: navigator.vendor, timestamp: detection.timestamp }; } /** * Clear detection cache */ public clearCache(): void { this.cache.clear(); } /** * Get cache statistics * @returns Cache statistics */ public getCacheStats(): CacheStats { return { size: this.cache.size, keys: Array.from(this.cache.keys()) }; } } // Export for different module systems if (typeof window !== 'undefined') { (window as Window & { SmartBrowserDetection?: typeof SmartBrowserDetection }).SmartBrowserDetection = SmartBrowserDetection; } // ES6 module export (default) if (typeof module !== 'undefined' && module.exports) { module.exports = SmartBrowserDetection; } export default SmartBrowserDetection;