/** * Public HTTP(S) only — private / loopback / metadata hosts are blocked. */ function isBlockedHostname(hostname) { if (!hostname) return true; const h = String(hostname).toLowerCase().replace(/^\[|\]$/g, ''); if ( h === 'localhost' || h.endsWith('.localhost') || h === '0.0.0.0' || h === '::' || h === '::1' || h === 'metadata.google.internal' || h.endsWith('.internal') ) { return true; } const v4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); if (v4) { const a = Number(v4[1]); const b = Number(v4[2]); if (a === 0 || a === 10 || a === 127) return true; if (a === 169 && b === 254) return true; if (a === 172 && b >= 16 && b <= 31) return true; if (a === 192 && b === 168) return true; if (a === 100 && b >= 64 && b <= 127) return true; } if (h.includes(':')) { if (h === '::1' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true; } return false; } function assertHttpUrl(raw) { let url; try { url = new URL(String(raw)); } catch (_) { throw new Error('invalid URL'); } if (url.protocol !== 'https:' && url.protocol !== 'http:') { throw new Error('only http(s) URLs are allowed'); } return url; } function assertPublicHttpUrl(raw) { const url = assertHttpUrl(raw); if (isBlockedHostname(url.hostname)) { throw new Error('private, loopback, and metadata hosts are blocked'); } return url; } module.exports = { isBlockedHostname, assertHttpUrl, assertPublicHttpUrl };