gigafibre-fsm/services/targo-hub/lib/cloud-ips.js
louispaulb 0f65c02d83 feat(fsm): platform build — comms UI, F→ERPNext sync/billing, roster, campaigns, network, reports
Accumulated work on the dispatch/legacy-writeback branch:
- Communications UI: CommunicationsPage, ConversationFullPage, DepartmentBoard,
  PipelineBoard, ReaderStack, Orchestrator/NewTicket/ServiceStatus/Outbox dialogs;
  hub gmail.js, ticket-collab.js, outbox.js, coupon-triage.js, client-diag.js.
- Billing/sync mirror (F→ERPNext): legacy-payments.js, legacy-sync.js,
  sync-orchestrator.js, supplier-invoices.js, municipality.js + incremental
  migration scripts; LegacySyncPage, SupplierInvoices + negative-billing /
  terminated-active reports.
- Roster/campaigns/network/voice: roster + roster-assistant, campaigns, giftbit,
  olt-snmp, traccar, twilio, vision, tech-absence-sms, ai/agent/config/helpers,
  legacy-dispatch-sync; ops PlanificationPage, RapportsPage, Settings, Tickets,
  ClientDetail updates.
- docs/ PLATFORM_GUIDE + UI_AND_OPTIMIZATION; .gitignore __pycache__.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:12:12 -04:00

84 lines
5.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 }