@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
JavaScript
;
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