// ==UserScript== // @name Azar IP Scanner // @namespace https://github.com/VeltrixJS/azar-ip-sniffer // @version 4.0 // @description IP scanner for Azar with tracking + reliable geolocation fallback // @author VeltrixJS // @match https://azarlive.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=azarlive.com // @grant unsafeWindow // @grant GM_xmlhttpRequest // @connect ipwho.is // @connect freeipapi.com // @connect api.techniknews.net // @connect geo.kamero.ai // @connect geo.wp-statistics.com // @connect nominatim.openstreetmap.org // @connect script.google.com // @connect script.googleusercontent.com // @connect api.ipify.org // @updateURL https://raw.githubusercontent.com/VeltrixJS/Azarlive-IP-Scanner/main/ip-scanner.js // @downloadURL https://raw.githubusercontent.com/VeltrixJS/Azarlive-IP-Scanner/main/ip-scanner.js // ==/UserScript== (function () { 'use strict'; const W = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; // ===== TRACKING ===== const TRACK_URL = 'https://script.google.com/macros/s/AKfycby_nr6TjTSZ57kf_yCNMG1GbsF_QeTzJEnHrkhdhqxWw7b2XiZ2kkQyANFtAB3mmSGR7A/exec'; function getFingerprint() { try { let fp = localStorage.getItem('_azid'); if (fp) return fp; const raw = [ navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, screen.colorDepth, new Date().getTimezoneOffset(), navigator.hardwareConcurrency || 0, navigator.platform || '', navigator.maxTouchPoints || 0 ].join('|'); let h = 0; for (let i = 0; i < raw.length; i++) { h = ((h << 5) - h) + raw.charCodeAt(i); h = h & h; } fp = Math.abs(h).toString(36) + Date.now().toString(36); localStorage.setItem('_azid', fp); return fp; } catch (e) { return 'na'; } } function trackLoad() { try { const send = (ip) => { const payload = { fingerprint: getFingerprint(), version: '9.0', ip: ip || 'unknown', ua: navigator.userAgent, ref: document.referrer || 'direct', screen: screen.width + 'x' + screen.height, lang: navigator.language }; if (typeof GM_xmlhttpRequest === 'function') { GM_xmlhttpRequest({ method: 'POST', url: TRACK_URL, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, onload: function () {}, onerror: function () {} }); } }; fetch('https://api.ipify.org?format=json', { cache: 'no-store' }) .then(r => r.json()).then(j => send(j.ip)).catch(() => send('unknown')); } catch (e) {} } trackLoad(); // ===== ÉTAT GLOBAL ===== let history = [], cache = new Map(), popupWindow = null, historyVisible = false; let container = null, miniBtn = null; // ===== RATE LIMITING + DÉDUPLICATION ===== const RATE = { windowMs: 60000, // fenêtre glissante : 1 minute maxCalls: 60, // max 60 lookups API / minute (marge large vs quotas) timestamps: [], // horodatages des appels récents inFlight: new Set() // IPs en cours de traitement (évite doublons simultanés) }; function rateLimitOk() { const now = Date.now(); RATE.timestamps = RATE.timestamps.filter(t => now - t < RATE.windowMs); if (RATE.timestamps.length >= RATE.maxCalls) return false; RATE.timestamps.push(now); return true; } // ===== CONSTANTES UI ===== const COLORS = { green: '#51f59b', dark: '#121212', white: '#fff', grey: '#1c1c1c', borderColor: '#222', yellow: '#ffd93d', red: '#ff4d4d' }; const btn = `padding:8px;border:none;background:${COLORS.green};color:${COLORS.dark};border-radius:6px;cursor:pointer;font-weight:600;transition:all 0.2s;`; const card = `display:flex;flex-direction:column;background-color:${COLORS.grey};border-left:4px solid ${COLORS.green};padding:15px;margin-bottom:12px;border-radius:8px;color:${COLORS.white};`; const esc = s => String(s ?? '').replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); // ===== FILTRE IP ===== function isPublicIP(ip) { if (!ip) return false; if (/^\d+\.\d+\.\d+\.\d+$/.test(ip)) { const p = ip.split('.').map(Number); if (p.some(n => isNaN(n) || n < 0 || n > 255)) return false; if (p[0] === 0 || p[0] === 10 || p[0] === 127) return false; if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return false; if (p[0] === 169 && p[1] === 254) return false; if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return false; if (p[0] === 192 && p[1] === 0 && p[2] === 0) return false; if (p[0] === 192 && p[1] === 0 && p[2] === 2) return false; if (p[0] === 192 && p[1] === 168) return false; if (p[0] === 198 && (p[1] === 18 || p[1] === 19)) return false; if (p[0] === 198 && p[1] === 51 && p[2] === 100) return false; if (p[0] === 203 && p[1] === 0 && p[2] === 113) return false; if (p[0] >= 224) return false; return true; } const low = ip.toLowerCase(); if (low === '::1' || low === '::') return false; if (low.startsWith('fe80:') || low.startsWith('fe80::')) return false; if (low.startsWith('fc') || low.startsWith('fd')) return false; if (low.startsWith('ff')) return false; return true; } function isIPv6(ip) { return ip && ip.includes(':'); } // ===== HOOK WebRTC ===== W.oRTCPeerConnection = W.oRTCPeerConnection || W.RTCPeerConnection; W.RTCPeerConnection = function (...a) { const pc = new W.oRTCPeerConnection(...a); pc.oaddIceCandidate = pc.addIceCandidate; pc.addIceCandidate = async function (c, ...r) { try { const cand = c?.candidate || c?.candidate?.candidate || ''; const f = (typeof cand === 'string' ? cand : '').split(' '); const ip = f[4]; const typ = f[7]; if (ip && ip !== '0.0.0.0' && !ip.endsWith('.local') && (typ === 'srflx' || typ === 'prflx' || typ === 'host')) { if (isPublicIP(ip)) onIP(ip); } } catch (e) {} return pc.oaddIceCandidate(c, ...r); }; return pc; }; W.RTCPeerConnection.prototype = W.oRTCPeerConnection.prototype; // ===== APIs ===== const APIS = [ // 1. ipwho.is — 1000 req/jour { name: 'ipwho.is', url: ip => `https://ipwho.is/${ip}`, parse: d => ({ city: d.city, region: d.region, postal: d.postal, country: d.country, countryCode: d.country_code, isp: d.connection?.isp || d.connection?.org, lat: d.latitude, lon: d.longitude, timezone: d.timezone?.id, vpn: d.security?.proxy || d.security?.vpn, hosting: d.security?.hosting, flag: d.flag?.emoji, asn: d.connection?.asn }) }, // 2. freeipapi.com — 60 req/min { name: 'freeipapi.com', url: ip => `https://freeipapi.com/api/json/${ip}`, parse: d => ({ city: d.cityName, region: d.regionName, postal: d.zipCode, country: d.countryName, countryCode: d.countryCode, isp: d.org, lat: d.latitude, lon: d.longitude, vpn: d.isProxy }) }, // 3. techniknews — gratuit, HTTPS { name: 'techniknews', url: ip => `https://api.techniknews.net/ipgeo/${ip}`, parse: d => ({ city: d.city, region: d.regionName, postal: d.zip, country: d.country, countryCode: d.countryCode, isp: d.isp, lat: d.lat, lon: d.lon, timezone: d.timezone }) } ]; // ===== GÉOMÉTRIE ===== function haversine(lat1, lon1, lat2, lon2) { const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function median(arr) { if (!arr.length) return 0; const s = [...arr].sort((a, b) => a - b); const m = Math.floor(s.length / 2); return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; } function geometricMedian(points, maxIter = 30, eps = 1e-5) { if (!points.length) return null; if (points.length === 1) return { lat: points[0].lat, lon: points[0].lon }; let lat = points.reduce((s, p) => s + p.lat, 0) / points.length; let lon = points.reduce((s, p) => s + p.lon, 0) / points.length; for (let iter = 0; iter < maxIter; iter++) { let numLat = 0, numLon = 0, den = 0; for (const p of points) { const d = Math.max(haversine(lat, lon, p.lat, p.lon), eps); const w = 1 / d; numLat += p.lat * w; numLon += p.lon * w; den += w; } const newLat = numLat / den, newLon = numLon / den; if (haversine(lat, lon, newLat, newLon) < 0.001) { lat = newLat; lon = newLon; break; } lat = newLat; lon = newLon; } return { lat, lon }; } function fuseCoordinates(results, rttMs) { const pts = results .filter(r => r.lat && r.lon && !isNaN(r.lat) && !isNaN(r.lon)) .map(r => ({ lat: Number(r.lat), lon: Number(r.lon) })); if (!pts.length) return null; if (pts.length === 1) return { lat: pts[0].lat, lon: pts[0].lon, confidence: 'low', spread: 0, sources: 1, discarded: 0 }; let filtered = pts; if (rttMs && rttMs > 0) { const maxKm = rttMs * 100 * 3 + 500; const tmpMed = geometricMedian(pts); if (tmpMed) { filtered = pts.filter(p => haversine(tmpMed.lat, tmpMed.lon, p.lat, p.lon) <= maxKm); if (filtered.length < 2) filtered = pts; } } const center = geometricMedian(filtered); let cleaned = filtered.filter(p => haversine(center.lat, center.lon, p.lat, p.lon) <= 500); if (cleaned.length < 2) cleaned = filtered; const finalCenter = geometricMedian(cleaned); const distances = cleaned.map(p => haversine(finalCenter.lat, finalCenter.lon, p.lat, p.lon)); const spread = median(distances); let confidence = 'low'; if (cleaned.length >= 3 && spread < 30) confidence = 'high'; else if (cleaned.length >= 2 && spread < 150) confidence = 'medium'; return { lat: finalCenter.lat, lon: finalCenter.lon, confidence, spread: Math.round(spread), sources: cleaned.length, discarded: pts.length - cleaned.length }; } // ===== CORE ===== async function onIP(ip) { if (!isPublicIP(ip)) return; // 1) Cache : on applique direct sans toucher aux APIs if (cache.has(ip)) { const cur = history[0]; if (!cur || cur.ip !== ip) { history.unshift({ ...cache.get(ip), ip, timestamp: Date.now(), time: new Date().toLocaleTimeString(), loading: false }); if (history.length > 10) history = history.slice(0, 10); } else { updateEntry(ip, cache.get(ip)); } render(); if (historyVisible) renderHistory(); updatePopup(); return; } // 2) Déduplication : si cette IP est déjà en cours de traitement, on ignore if (RATE.inFlight.has(ip)) return; // 3) Rate limiting global if (!rateLimitOk()) { console.warn('[IP Scanner] Rate limit atteint (' + RATE.maxCalls + '/min), skip:', ip); return; } RATE.inFlight.add(ip); try { // Création de l'entrée (placeholder) const cur = history[0]; if (!cur || cur.ip !== ip) { history.unshift({ ip, timestamp: Date.now(), time: new Date().toLocaleTimeString(), loading: true }); if (history.length > 10) history = history.slice(0, 10); render(); if (historyVisible) renderHistory(); } // Ping en parallèle ping(ip).then(p => { const i = history.findIndex(h => h.ip === ip); if (i >= 0) { history[i].ping = p; if (history[i]._allResults) { const fused = fuseCoordinates(history[i]._allResults, p); if (fused) { history[i].lat = fused.lat; history[i].lon = fused.lon; history[i]._confidence = fused.confidence; history[i]._spread = fused.spread; history[i]._sources = fused.sources; } } render(); if (historyVisible) renderHistory(); updatePopup(); } }); let firstApplied = false; const result = await raceAPIs(ip, (partial) => { if (!firstApplied) { firstApplied = true; updateEntry(ip, partial); } }, null); if (result) { cache.set(ip, result); updateEntry(ip, result); } } finally { RATE.inFlight.delete(ip); } } async function raceAPIs(ip, onFirst, rttMs) { const isV6 = isIPv6(ip); const promises = APIS.map(async (a) => { // Skip certaines APIs sur IPv6 if (isV6 && a.name === 'techniknews') return null; try { const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 4000); const r = await fetch(a.url(ip), { headers: { Accept: 'application/json' }, signal: ctl.signal }); clearTimeout(t); if (!r.ok) return null; const d = await r.json(); if (d && !d.error && d.success !== false && !d.message) { const parsed = a.parse(d); onFirst(parsed); return parsed; } } catch (e) {} return null; }); const results = (await Promise.all(promises)).filter(Boolean); if (!results.length) return { city: 'Unknown', region: '', country: 'Unknown', isp: 'N/A' }; const withCoords = results.filter(r => r.lat && r.lon); const m = { ...(withCoords[0] || results[0]) }; for (const r of results) { for (const k of ['city', 'region', 'postal', 'country', 'countryCode', 'isp', 'timezone', 'flag']) if (!m[k] && r[k]) m[k] = r[k]; if (r.vpn) m.vpn = true; if (r.hosting) m.hosting = true; if (r.asn && !m.asn) m.asn = r.asn; } const fused = fuseCoordinates(results, rttMs); if (fused) { m.lat = fused.lat; m.lon = fused.lon; m._confidence = fused.confidence; m._spread = fused.spread; m._sources = fused.sources; m._discarded = fused.discarded; } m._allResults = withCoords.map(r => ({ lat: r.lat, lon: r.lon })); if (m.lat && m.lon) { try { const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 3500); const r = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${m.lat}&lon=${m.lon}&zoom=16&addressdetails=1`, { signal: ctl.signal }); clearTimeout(t); if (r.ok) { const a = (await r.json()).address || {}; m.city = a.city || a.town || a.village || a.municipality || m.city; m.suburb = a.suburb || a.neighbourhood || a.quarter || a.road || ''; m.region = a.state || m.region; m.country = a.country || m.country; m.postal = a.postcode || m.postal; } } catch (e) {} } return m; } function updateEntry(ip, data) { const i = history.findIndex(h => h.ip === ip); if (i < 0) return; const prevAll = history[i]._allResults; history[i] = { ...history[i], ...data, loading: false }; if (prevAll && !history[i]._allResults) history[i]._allResults = prevAll; render(); if (historyVisible) renderHistory(); updatePopup(); } function ping(ip) { if (!isPublicIP(ip)) return Promise.resolve(null); return new Promise(res => { const img = new Image(), s = performance.now(); const t = setTimeout(() => res(null), 2500); img.onload = img.onerror = () => { clearTimeout(t); res(Math.round(performance.now() - s)); }; img.src = `https://${ip}/favicon.ico?_=${Date.now()}`; }); } // ===== RENDER ===== function render() { const el = document.getElementById('ip-addresses'); if (!el) return; if (!history.length) { el.innerHTML = `
${esc(e.ip)}