UNPKG

@usehall/bot-gate

Version:

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

270 lines (219 loc) 7.09 kB
import { defineComponent, onMounted } from 'vue'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; 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$1 = path.dirname(fileURLToPath(import.meta.url)); let userAgentPatterns = null; function loadUserAgentPatterns() { if (!userAgentPatterns) { try { const dataPath = path.join(__dirname$1, '../../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 = path.dirname(fileURLToPath(import.meta.url)); 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, `../../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; } const BotGate = defineComponent({ name: 'BotGate', props: { userAgent: { type: String, required: true }, ipAddress: { type: String, required: true }, display: { type: String, required: true, validator: value => ['show', 'hide'].includes(value) }, role: { type: String, required: true, validator: value => ['bot', 'user'].includes(value) }, bots: { type: Array, default: null } }, setup(props, { slots }) { onMounted(() => { warnIfClientSide(); }); const shouldShow = () => { const validation = validateBotGateProps(props.userAgent, props.ipAddress, props.display, props.role); if (!validation.isValid) { return false; } const isBot = isValidBot(props.userAgent, props.ipAddress, props.bots); return (props.display === 'show') ? (props.role === 'bot' ? isBot : !isBot) : (props.role === 'bot' ? !isBot : isBot); }; return () => { const show = shouldShow(); return show ? slots.default?.() : null; }; } }); export { BotGate }; //# sourceMappingURL=index.js.map