UNPKG

@usehall/bot-gate

Version:

React/Vue/Svelte components for conditionally rendering content based on bot detection with IP validation

225 lines (215 loc) 7.06 kB
'use strict'; var React = require('react'); var fs = require('fs'); var path = require('path'); var url = require('url'); var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; function isServerSide() { return typeof window === 'undefined' && typeof process !== 'undefined' && process.versions && process.versions.node; } function warnIfClientSide() { if (!isServerSide()) { console.warn('bot-gate: This library is designed for server-side use only. Bot detection requires access to request headers and IP addresses that are not available in the browser.'); } } function sanitizeIpAddress(ipAddress) { if (!ipAddress || typeof ipAddress !== 'string') return null; return ipAddress.trim().split(',')[0].trim().replace(/^::ffff:/, ''); } const __dirname$2 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))); let userAgentPatterns = null; function loadUserAgentPatterns() { if (!userAgentPatterns) { try { const dataPath = path.join(__dirname$2, '../../data/user-agents.json'); const data = fs.readFileSync(dataPath, 'utf8'); userAgentPatterns = JSON.parse(data); } catch (error) { console.warn('Failed to load user agent patterns:', error.message); userAgentPatterns = { bots: [] }; } } return userAgentPatterns; } function detectBot(userAgent) { if (!userAgent || typeof userAgent !== 'string') { return null; } const patterns = loadUserAgentPatterns(); const normalizedUA = userAgent.toLowerCase(); for (const bot of patterns.bots) { for (const pattern of bot.patterns) { const regex = new RegExp(pattern.toLowerCase(), 'i'); if (regex.test(normalizedUA)) { return { name: bot.name, type: bot.type, detected: true, pattern: pattern }; } } } return null; } const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))); const ipRangeCache = new Map(); function loadBotIpRanges(botName) { if (!ipRangeCache.has(botName)) { try { // Handle applebot-extended using the same IP ranges as applebot let actualBotName = botName.toLowerCase(); if (actualBotName === 'applebot-extended') { actualBotName = 'applebot'; } const dataPath = path.join(__dirname$1, `../../data/bots/${actualBotName}.json`); const data = fs.readFileSync(dataPath, 'utf8'); const rawData = JSON.parse(data); // Official API format: { creationTime, prefixes: [{ ipv4Prefix: "..." }] } const ranges = rawData.prefixes ? rawData.prefixes.map(prefix => prefix.ipv4Prefix || prefix.ipv6Prefix).filter(Boolean) : []; ipRangeCache.set(botName, { ranges }); } catch (error) { console.warn(`Failed to load IP ranges for ${botName}:`, error.message); ipRangeCache.set(botName, { ranges: [] }); } } return ipRangeCache.get(botName); } function ipToInt(ip) { return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0; } function cidrToRange(cidr) { const [ip, prefixLength] = cidr.split('/'); const ipInt = ipToInt(ip); const mask = -1 << 32 - parseInt(prefixLength, 10) >>> 0; const networkInt = ipInt & mask; const broadcastInt = networkInt | ~mask >>> 0; return { start: networkInt, end: broadcastInt }; } function isIpInRange(ip, range) { const ipInt = ipToInt(ip); if (range.includes('/')) { const { start, end } = cidrToRange(range); return ipInt >= start && ipInt <= end; } if (range.includes('-')) { const [startIp, endIp] = range.split('-'); const startInt = ipToInt(startIp.trim()); const endInt = ipToInt(endIp.trim()); return ipInt >= startInt && ipInt <= endInt; } return ipInt === ipToInt(range); } function isValidIpAddress(ip) { const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; return ipv4Regex.test(ip); } function validateBotByName(botName, ipAddress) { if (!botName || !ipAddress) { return false; } if (!isValidIpAddress(ipAddress)) { return false; } const botData = loadBotIpRanges(botName); if (!botData.ranges || botData.ranges.length === 0) { return false; } return botData.ranges.some(range => isIpInRange(ipAddress, range)); } function validateBot(userAgent, ipAddress) { if (!userAgent || !ipAddress) { return false; } const botInfo = detectBot(userAgent); if (!botInfo) { return false; } return validateBotByName(botInfo.name, ipAddress); } function validateBotGateProps(userAgent, ipAddress, display, role) { if (!userAgent || !ipAddress) { console.warn('BotGate: userAgent and ipAddress are required props'); return { isValid: false, error: 'missing_required_props' }; } if (!display) { console.warn('BotGate: display prop is required ("show" or "hide")'); return { isValid: false, error: 'missing_display' }; } if (!role) { console.warn('BotGate: role prop is required ("bot" or "user")'); return { isValid: false, error: 'missing_role' }; } if (!['show', 'hide'].includes(display)) { console.warn('BotGate: display must be either "show" or "hide"'); return { isValid: false, error: 'invalid_display' }; } if (!['bot', 'user'].includes(role)) { console.warn('BotGate: role must be either "bot" or "user"'); return { isValid: false, error: 'invalid_role' }; } return { isValid: true }; } function isValidBot(userAgent, ipAddress, bots = null) { const cleanIp = sanitizeIpAddress(ipAddress); const isValid = validateBot(userAgent, cleanIp); if (!isValid) { return false; } // If specific bots are requested, check if this bot is in the list if (bots && Array.isArray(bots) && bots.length > 0) { const botInfo = detectBot(userAgent); return botInfo ? bots.includes(botInfo.name) : false; } return true; } function BotGate({ userAgent, ipAddress, display, role, bots = null, children }) { React.useEffect(() => { warnIfClientSide(); }, []); const validation = validateBotGateProps(userAgent, ipAddress, display, role); if (!validation.isValid) { return null; } const isBot = isValidBot(userAgent, ipAddress, bots); const shouldShow = display === 'show' ? role === 'bot' ? isBot : !isBot : role === 'bot' ? !isBot : isBot; return shouldShow ? children : null; } exports.BotGate = BotGate; //# sourceMappingURL=index.cjs.map