UNPKG

@headwall/trusted-network-providers

Version:
387 lines (343 loc) 11.4 kB
/** * index.js */ const ipaddr = require('ipaddr.js'); // Constants for IP address versions const IP_VERSION_V4 = 'ipv4'; const IP_VERSION_V6 = 'ipv6'; const defaultProviders = [ require('./providers/private.js'), require('./providers/googlebot.js'), require('./providers/google-workspace.js'), require('./providers/google-services.js'), require('./providers/stripe-api.js'), require('./providers/stripe-webhooks.js'), require('./providers/opayo.js'), require('./providers/paypal.js'), require('./providers/outlook.js'), require('./providers/cloudflare.js'), require('./providers/ezoic.js'), require('./providers/ship-hero.js'), require('./providers/bunnynet.js'), require('./providers/semrush.js'), require('./providers/ahrefsbot.js'), require('./providers/facebookbot.js'), require('./providers/brevo.js'), require('./providers/get-terms.js'), require('./providers/labrika.js'), // require('./providers/mailgun.js'), // require('./providers/gtmetrix.js'), // require('./providers/seobility.js'), // Unreliable ]; const parsedAddresses = {}; /** * @typedef {Object} Provider * @property {string} name - The display name of the provider * @property {string[]} [testAddresses] - Sample IP addresses for testing * @property {Function|Function[]} [reload] - Function(s) to reload provider data * @property {Object} ipv4 - IPv4 configuration * @property {string[]} ipv4.addresses - Individual IPv4 addresses * @property {string[]} ipv4.ranges - IPv4 CIDR ranges * @property {Object} ipv6 - IPv6 configuration * @property {string[]} ipv6.addresses - Individual IPv6 addresses * @property {string[]} ipv6.ranges - IPv6 CIDR ranges */ const self = { providers: [], isDiagnosticsEnabled: false, /** * Adds a new provider to the trusted network list. * Providers must have a unique name and will be checked in order during IP lookups. * * @param {Provider} provider - The provider configuration object * @returns {void} * * @example * trustedProviders.addProvider({ * name: 'My CDN', * ipv4: { * addresses: ['1.2.3.4'], * ranges: ['10.0.0.0/8'] * }, * ipv6: { * addresses: [], * ranges: ['2001:db8::/32'] * } * }); */ addProvider: (provider) => { if (provider && typeof provider.name !== 'undefined' && !self.hasProvider(provider.name)) { if (self.isDiagnosticsEnabled) { console.log(`➕ Add provider: ${provider.name}`); } self.providers.push(provider); } }, /** * Removes a provider from the trusted network list by name. * * @param {string} providerName - The name of the provider to remove * @returns {void} * * @example * trustedProviders.deleteProvider('My CDN'); */ deleteProvider: (providerName) => { if (self.hasProvider(providerName)) { const providerIndex = self.providers.findIndex((testProvider) => testProvider.name === providerName); if (providerIndex >= 0) { self.providers.splice(providerIndex, 1); } } }, /** * Returns an array of all registered providers. * * @returns {Provider[]} Array of provider objects * * @example * const providers = trustedProviders.getAllProviders(); * console.log(`Loaded ${providers.length} providers`); */ getAllProviders: () => { return self.providers; }, /** * Checks if a provider with the given name is already registered. * * @param {string} providerName - The name of the provider to check * @returns {boolean} True if the provider exists, false otherwise * * @example * if (trustedProviders.hasProvider('Cloudflare')) { * console.log('Cloudflare provider is loaded'); * } */ hasProvider: (providerName) => { let isFound = false; if (providerName) { self.providers.forEach((testProvider) => { isFound |= testProvider.name === providerName; }); } return isFound; }, /** * Loads all built-in providers (Googlebot, Stripe, Cloudflare, etc.). * This is typically called once during application initialization. * * @returns {void} * * @example * const trustedProviders = require('@headwall/trusted-network-providers'); * trustedProviders.loadDefaultProviders(); * await trustedProviders.reloadAll(); */ loadDefaultProviders: () => { defaultProviders.forEach((defaultProvider) => { if (!self.hasProvider(defaultProvider.name)) { self.addProvider(defaultProvider); } }); }, /** * Reloads data for all providers that support dynamic updates. * This fetches fresh IP ranges from external sources (APIs, DNS, bundled assets). * Should be called periodically (e.g., daily) to keep provider data current. * * @returns {Promise<void[]>} Promise that resolves when all providers have reloaded * * @example * // Initial load * trustedProviders.loadDefaultProviders(); * await trustedProviders.reloadAll(); * * // Periodic update (once per day) * setInterval(async () => { * try { * await trustedProviders.reloadAll(); * console.log('Provider data updated'); * } catch (error) { * console.error('Failed to reload providers:', error); * } * }, 24 * 60 * 60 * 1000); */ reloadAll: () => { const reloadRequests = []; self.providers.forEach((provider) => { if (typeof provider.reload === 'function') { if (self.isDiagnosticsEnabled) { console.log(`🔃 Reload: ${provider.name}`); } const reloadPromises = provider.reload(); if (Array.isArray(reloadPromises)) { // console.log( `Array of promises: ${provider.name}`); reloadPromises.forEach((promise) => { reloadRequests.push(promise); }); } else { // console.log( `Single promise: ${provider.name}`); reloadRequests.push(reloadPromises); } } }); return Promise.all(reloadRequests); }, /** * Identifies which trusted provider (if any) an IP address belongs to. * Returns the provider name on match, or null if the IP is not trusted. * * This function performs a linear search through providers in registration order. * First match wins, so provider order matters if ranges overlap. * * @param {string} ipAddress - The IP address to check (IPv4 or IPv6) * @returns {string|null} The name of the trusted provider, or null if not found * * @example * const provider = trustedProviders.getTrustedProvider('66.249.66.1'); * if (provider === 'Googlebot') { * console.log('Request is from Googlebot'); * } * * @example * // Express.js middleware example * app.use((req, res, next) => { * const provider = trustedProviders.getTrustedProvider(req.ip); * if (provider) { * req.trustedProvider = provider; * } * next(); * }); */ getTrustedProvider: (ipAddress) => { let trustedSource = null; let parsedIp = null; try { parsedIp = ipaddr.parse(ipAddress); } catch { console.error(`Failed to parse IP: ${ipAddress}`); parsedIp = null; } if (parsedIp) { const ipAddressVersion = parsedIp.kind(); const providerCount = self.providers.length; let providerIndex = 0; while (providerIndex < providerCount) { const provider = self.providers[providerIndex]; let testPool = null; if (ipAddressVersion === IP_VERSION_V4) { testPool = provider.ipv4; } else if (ipAddressVersion === IP_VERSION_V6) { testPool = provider.ipv6; } else { // ... } try { if (testPool) { const addressCount = testPool.addresses.length; for (let addressIndex = 0; addressIndex < addressCount; ++addressIndex) { if (testPool.addresses[addressIndex] === ipAddress) { trustedSource = provider.name; break; } } const rangeCount = testPool.ranges.length; for (let rangeIndex = 0; rangeIndex < rangeCount; ++rangeIndex) { const testRange = testPool.ranges[rangeIndex]; if (!parsedAddresses[testRange]) { parsedAddresses[testRange] = ipaddr.parseCIDR(testRange); } if (parsedIp.match(parsedAddresses[testRange])) { trustedSource = provider.name; break; } } } } catch (error) { console.error(`ERROR: Failed to find trusted source of ${ipAddress}`); console.error(error); } if (trustedSource) { break; } else { ++providerIndex; } } } return trustedSource; }, /** * Checks if an IP address belongs to any trusted provider. * This is a convenience wrapper around getTrustedProvider(). * * @param {string} ipAddress - The IP address to check (IPv4 or IPv6) * @returns {boolean} True if the IP belongs to a trusted provider, false otherwise * * @example * if (trustedProviders.isTrusted('34.237.253.141')) { * console.log('IP is from a trusted source'); * } */ isTrusted: (ipAddress) => { return self.getTrustedProvider(ipAddress) !== null; }, /** * Runs tests against all registered providers using their configured test addresses. * Outputs results to console with ✅ for passing tests and ❌ for failures. * Useful for verifying provider configuration after loading or reloading. * * @returns {Promise<void>} Promise that resolves when all tests complete * * @example * trustedProviders.loadDefaultProviders(); * await trustedProviders.reloadAll(); * await trustedProviders.runTests(); */ runTests: () => { return new Promise((resolve) => { const tests = [ { ip: '192.42.116.182', provider: null }, { ip: '123.123.123.123', provider: null }, ]; let failedProviderIndex = 0; self.getAllProviders().forEach((testProvider) => { if (!Array.isArray(testProvider.testAddresses)) { if (failedProviderIndex === 0) { console.log(); } console.log(`🔷 No tests for ${testProvider.name}`); ++failedProviderIndex; } else { testProvider.testAddresses.forEach((testAddress) => { tests.push({ ip: testAddress, provider: testProvider.name, }); }); } }); console.log(); tests.forEach((test) => { let testProviderName = test.provider; if (!testProviderName) { testProviderName = '_wild_'; } const provider = self.getTrustedProvider(test.ip); let foundProviderName = provider; if (!foundProviderName) { foundProviderName = '_wild_'; } if (provider !== test.provider) { console.log(`❌${test.ip} => ${foundProviderName} (should be ${testProviderName})`); } else { console.log(`✅${test.ip} => ${foundProviderName}`); } }); console.log(); console.log('🏁 Finished tests'); console.log(); resolve(); }); }, }; module.exports = self;