// lib/cloud-ips.js — détecte si une IP appartient à un fournisseur cloud/datacenter (AWS, GCP…). // Sert à BLOQUER les ouvertures de liens-cadeaux depuis des IP de datacenter (bots/proxies) au wrapper /g/. // Charge les plages PUBLIÉES (AWS ip-ranges.json, GCP cloud.json) en mémoire + rafraîchit 1×/jour. // Fail-open : tant que les plages ne sont pas chargées (ou en cas d'erreur), isCloudIp renvoie {cloud:false} → on NE bloque pas par défaut. const https = require('https') let V4 = [] // [{base:int, bits, provider}] let V6 = [] // [{base:BigInt, bits, provider}] let _loadedAt = 0 // ── Parsing IP ── function ip4ToInt (ip) { const p = String(ip).split('.'); if (p.length !== 4) return null; let n = 0; for (const o of p) { const x = Number(o); if (!Number.isInteger(x) || x < 0 || x > 255) return null; n = (n * 256) + x } return n >>> 0 } function ip6ToBig (ip) { ip = String(ip).split('%')[0].toLowerCase() if (ip.indexOf(':') < 0) return null let head, tail if (ip.indexOf('::') >= 0) { const parts = ip.split('::'); head = parts[0] ? parts[0].split(':') : []; tail = parts[1] ? parts[1].split(':') : [] } else { head = ip.split(':'); tail = [] } const expand = (arr) => { const out = []; for (const g of arr) { if (g.indexOf('.') >= 0) { const q = g.split('.').map(Number); if (q.length === 4 && q.every(x => x >= 0 && x <= 255)) { out.push((((q[0] << 8) | q[1]) >>> 0).toString(16)); out.push((((q[2] << 8) | q[3]) >>> 0).toString(16)) } } else if (g !== '') out.push(g) } return out } head = expand(head); tail = expand(tail) const mid = 8 - (head.length + tail.length); if (mid < 0) return null const groups = head.concat(Array(mid).fill('0'), tail) let big = 0n for (const g of groups) { const v = parseInt(g || '0', 16); if (isNaN(v)) return null; big = (big << 16n) | BigInt(v) } return big } function parseCidr (cidr, provider) { const [addr, bitsRaw] = String(cidr).split('/'); const bits = parseInt(bitsRaw, 10) if (addr.indexOf(':') >= 0) { const base = ip6ToBig(addr); if (base == null || isNaN(bits)) return; V6.push({ base, bits, provider }) } else { const base = ip4ToInt(addr); if (base == null || isNaN(bits)) return; V4.push({ base, bits, provider }) } } // ── Détection ── function isCloudIp (ip) { if (!ip) return { cloud: false } ip = String(ip).trim().replace(/^::ffff:/i, '') // IPv4-mapped IPv6 → IPv4 if (ip.indexOf(':') < 0) { const n = ip4ToInt(ip); if (n == null) return { cloud: false } for (const r of V4) { const mask = r.bits === 0 ? 0 : (0xFFFFFFFF << (32 - r.bits)) >>> 0; if ((n & mask) >>> 0 === (r.base & mask) >>> 0) return { cloud: true, provider: r.provider } } return { cloud: false } } const big = ip6ToBig(ip); if (big == null) return { cloud: false } for (const r of V6) { const sh = 128n - BigInt(r.bits); if ((big >> sh) === (r.base >> sh)) return { cloud: true, provider: r.provider } } return { cloud: false } } // IP(s) candidates d'une requête derrière Traefik : toute la chaîne X-Forwarded-For + l'adresse socket. function clientIps (req) { const out = [] const xff = req.headers['x-forwarded-for'] if (xff) for (const p of String(xff).split(',')) { const ip = p.trim(); if (ip) out.push(ip) } const xr = req.headers['x-real-ip']; if (xr) out.push(String(xr).trim()) const ra = req.socket && req.socket.remoteAddress; if (ra) out.push(String(ra).trim()) return out.filter((v, i, a) => v && a.indexOf(v) === i) } // ── Chargement des plages publiées ── function fetchJson (url) { return new Promise((resolve) => { https.get(url, { headers: { 'User-Agent': 'targo-hub/cloud-ips' }, timeout: 20000 }, (r) => { if (r.statusCode !== 200) { r.resume(); return resolve(null) } let b = ''; r.on('data', c => { b += c }); r.on('end', () => { try { resolve(JSON.parse(b)) } catch (e) { resolve(null) } }) }).on('error', () => resolve(null)).on('timeout', function () { this.destroy(); resolve(null) }) }) } async function load () { const v4 = [], v6 = [] const push = (cidr, prov) => { const [a, bRaw] = String(cidr).split('/'); const bits = parseInt(bRaw, 10); if (isNaN(bits)) return; if (a.indexOf(':') >= 0) { const base = ip6ToBig(a); if (base != null) v6.push({ base, bits, provider: prov }) } else { const base = ip4ToInt(a); if (base != null) v4.push({ base, bits, provider: prov }) } } const aws = await fetchJson('https://ip-ranges.amazonaws.com/ip-ranges.json') if (aws) { for (const p of (aws.prefixes || [])) push(p.ip_prefix, 'AWS'); for (const p of (aws.ipv6_prefixes || [])) push(p.ipv6_prefix, 'AWS') } const gcp = await fetchJson('https://www.gstatic.com/ipranges/cloud.json') if (gcp) { for (const p of (gcp.prefixes || [])) { if (p.ipv4Prefix) push(p.ipv4Prefix, 'GCP'); if (p.ipv6Prefix) push(p.ipv6Prefix, 'GCP') } } // Plages supplémentaires (datacenters fréquents) — ajout manuel léger. for (const c of EXTRA_V4) push(c, 'DC') if (v4.length + v6.length > 100) { V4 = v4; V6 = v6; _loadedAt = Date.now() } // ne remplace que si le chargement a vraiment ramené des plages return { v4: v4.length, v6: v6.length, loaded: !!(v4.length + v6.length) } } // DigitalOcean / OVH / Hetzner / Oracle (échantillon ; étendre au besoin). const EXTRA_V4 = ['159.203.0.0/16', '167.71.0.0/16', '134.209.0.0/16', '157.245.0.0/16', '144.217.0.0/16', '51.222.0.0/16', '5.9.0.0/16', '88.198.0.0/16', '129.213.0.0/16'] let _initPromise = null function init () { if (!_initPromise) { _initPromise = load().catch(() => ({ loaded: false })); const t = setInterval(() => { load().catch(() => {}) }, 24 * 3600 * 1000); if (t && t.unref) t.unref() } return _initPromise } function stats () { return { v4: V4.length, v6: V6.length, loadedAt: _loadedAt } } module.exports = { init, isCloudIp, clientIps, stats, ip4ToInt, ip6ToBig }