// ==UserScript== // @name GeoChecker v5.0 // @namespace GeoChecker // @version 5.0.12 // @description Extract peer IPs via WebRTC with real-time connection stats, threat intel, mini-map, glassmorphism UI, connection quality scoring, network path visualizer, mDNS/IPv6 leak detection, codec fingerprinting, DTLS info, media device enumeration, screen sharing detection, NAT type analysis, peer fingerprinting, session timeline, advanced OSINT, and native Tampermonkey menu integration. // @author w0wzahh // @license All Rights Reserved // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_xmlhttpRequest // @grant unsafeWindow // @downloadURL https://raw.githubusercontent.com/w0wzahh/GeoChecker/main/geochecker-webrtc.user.js // @updateURL https://raw.githubusercontent.com/w0wzahh/GeoChecker/main/geochecker-webrtc.user.js // @run-at document-start // ==/UserScript== (function () { 'use strict'; const CONFIG = { apis: [ { url: ip => `https://get.geojs.io/v1/ip/geo/${encodeURIComponent(ip)}.json`, name: 'geojs' }, { url: ip => `https://free.freeipapi.com/api/json/${encodeURIComponent(ip)}`, name: 'freeipapi' }, { url: ip => `https://api.ipapi.is?q=${encodeURIComponent(ip)}`, name: 'ipapiis' } ], accent: '#00f0c8', accent2: '#58a6ff', danger: '#ff4757', warn: '#ff9f43', ok: '#3fb950', muted: '#8b9aad', bg: 'rgba(11,15,20,.88)', card: 'rgba(14,20,27,.75)', border: 'rgba(27,40,56,.6)', glass: 'backdrop-filter:blur(20px) saturate(180%);-webkit-backdrop-filter:blur(20px) saturate(180%);', weatherApi: (lat, lon) => `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`, osint: { shodan: ip => `https://www.shodan.io/search?query=${encodeURIComponent(ip)}`, abuseipdb: ip => `https://www.abuseipdb.com/check/${encodeURIComponent(ip)}`, virustotal: ip => `https://www.virustotal.com/gui/ip-address/${encodeURIComponent(ip)}/detection`, ipinfo: ip => `https://ipinfo.io/${encodeURIComponent(ip)}`, greynoise: ip => `https://viz.greynoise.io/ip/${encodeURIComponent(ip)}`, censys: ip => `https://search.censys.io/hosts/${encodeURIComponent(ip)}`, ipvoid: ip => `https://www.ipvoid.com/scan/${encodeURIComponent(ip)}`, threatfox: ip => `https://threatfox.abuse.ch/browse.php?search=ioc%3A${encodeURIComponent(ip)}`, alienvault: ip => `https://otx.alienvault.com/indicator/ip/${encodeURIComponent(ip)}`, talos: ip => `https://www.talosintelligence.com/reputation_center/lookup?search=${encodeURIComponent(ip)}`, torexit: ip => `https://check.torproject.org/torbulkexitlist?ip=${encodeURIComponent(ip)}` }, riskCountries: new Set(['KP','IR','SY','CU','RU','BY','MM','AF','IQ','LY','SO','SD','VE']), maxBitrateHistory: 30, statsPollInterval: 1500, hookInterval: 2000, passiveScanInterval: 3000 }; // License key system (Gumroad) const LICENSE_KEY = '__gc_license_key_v1'; const LICENSE_CACHE_KEY = '__gc_license_cache_v1'; const LICENSE_PRODUCT_PERMALINK = 'geochecker-pro'; // update when Gumroad product is created const LICENSE_VERIFY_URL = 'https://api.gumroad.com/v2/licenses/verify'; const LICENSE_CACHE_HOURS = 24; let licenseTier = 'free'; // 'free' | 'pro' | 'lifetime' let licenseEmail = ''; async function verifyLicenseKey(key) { if (!key || typeof key !== 'string') return { valid: false, tier: 'free' }; try { // Check cached result first const cached = localStorage.getItem(LICENSE_CACHE_KEY); if (cached) { const c = JSON.parse(cached); if (c.key === key && c.ts && (Date.now() - c.ts) < LICENSE_CACHE_HOURS * 3600000) { licenseTier = c.tier; licenseEmail = c.email || ''; return { valid: true, tier: c.tier }; } } // Call Gumroad API const res = await fetch(LICENSE_VERIFY_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ product_permalink: LICENSE_PRODUCT_PERMALINK, license_key: key.trim(), increment_uses_count: 'false' }) }); if (!res.ok) return { valid: false, tier: 'free' }; const data = await res.json(); if (!data.success) return { valid: false, tier: 'free' }; const purchase = data.purchase; if (purchase.refunded || purchase.chargebacked) return { valid: false, tier: 'free' }; const tier = purchase.subscription_ended_at ? 'lifetime' : 'pro'; licenseTier = tier; licenseEmail = purchase.email || ''; localStorage.setItem(LICENSE_CACHE_KEY, JSON.stringify({ key, tier, email: licenseEmail, ts: Date.now() })); return { valid: true, tier }; } catch (e) { return { valid: false, tier: 'free' }; } } function isPro() { return licenseTier === 'pro' || licenseTier === 'lifetime'; } function isLifetime() { return licenseTier === 'lifetime'; } async function loadLicense() { const key = localStorage.getItem(LICENSE_KEY); if (key) await verifyLicenseKey(key); } const UPDATE_CHECK_URL = 'https://raw.githubusercontent.com/w0wzahh/GeoChecker/main/geochecker-webrtc.user.js'; const UPDATE_CHECK_INTERVAL = 30 * 60 * 1000; // 30 minutes let updateCheckTimer = null; let updateAvailable = false; const EU_COUNTRIES = new Set(['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IE','IT','LV','LT','LU','MT','NL','PL','PT','RO','SK','SI','ES','SE']); const CALLING_CODES = {AF:'+93',AL:'+355',DZ:'+213',AS:'+1-684',AD:'+376',AO:'+244',AR:'+54',AM:'+374',AU:'+61',AT:'+43',AZ:'+994',BS:'+1-242',BH:'+973',BD:'+880',BY:'+375',BE:'+32',BZ:'+501',BJ:'+229',BT:'+975',BO:'+591',BA:'+387',BW:'+267',BR:'+55',BN:'+673',BG:'+359',BF:'+226',BI:'+257',KH:'+855',CM:'+237',CA:'+1',CV:'+238',CF:'+236',TD:'+235',CL:'+56',CN:'+86',CO:'+57',KM:'+269',CG:'+242',CR:'+506',HR:'+385',CU:'+53',CY:'+357',CZ:'+420',DK:'+45',DJ:'+253',DO:'+1-809',EC:'+593',EG:'+20',SV:'+503',GQ:'+240',ER:'+291',EE:'+372',ET:'+251',FJ:'+679',FI:'+358',FR:'+33',GA:'+241',GM:'+220',GE:'+995',DE:'+49',GH:'+233',GR:'+30',GT:'+502',GN:'+224',GW:'+245',GY:'+592',HT:'+509',HN:'+504',HU:'+36',IS:'+354',IN:'+91',ID:'+62',IR:'+98',IQ:'+964',IE:'+353',IL:'+972',IT:'+39',JM:'+1-876',JP:'+81',JO:'+962',KZ:'+7',KE:'+254',KI:'+686',KW:'+965',KG:'+996',LA:'+856',LV:'+371',LB:'+961',LS:'+266',LR:'+231',LY:'+218',LI:'+423',LT:'+370',LU:'+352',MG:'+261',MW:'+265',MY:'+60',MV:'+960',ML:'+223',MT:'+356',MH:'+692',MR:'+222',MU:'+230',MX:'+52',MD:'+373',MC:'+377',MN:'+976',ME:'+382',MA:'+212',MZ:'+258',MM:'+95',NA:'+264',NR:'+674',NP:'+977',NL:'+31',NZ:'+64',NI:'+505',NE:'+227',NG:'+234',KP:'+850',NO:'+47',OM:'+968',PK:'+92',PW:'+680',PA:'+507',PG:'+675',PY:'+595',PE:'+51',PH:'+63',PL:'+48',PT:'+351',QA:'+974',RO:'+40',RU:'+7',RW:'+250',KN:'+1-869',LC:'+1-758',VC:'+1-784',WS:'+685',SM:'+378',ST:'+239',SA:'+966',SN:'+221',RS:'+381',SC:'+248',SL:'+232',SG:'+65',SK:'+421',SI:'+386',SB:'+677',SO:'+252',ZA:'+27',KR:'+82',SS:'+211',ES:'+34',LK:'+94',SD:'+249',SR:'+597',SE:'+46',CH:'+41',SY:'+963',TW:'+886',TJ:'+992',TZ:'+255',TH:'+66',TL:'+670',TG:'+228',TO:'+676',TT:'+1-868',TN:'+216',TR:'+90',TM:'+993',TV:'+688',UG:'+256',UA:'+380',AE:'+971',GB:'+44',US:'+1',UY:'+598',UZ:'+998',VU:'+678',VE:'+58',VN:'+84',YE:'+967',ZM:'+260',ZW:'+263'}; const $ = (id) => document.getElementById(id); function esc(str) { if (str == null) return ''; return String(str).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } function isPrivateIP(ip) { if (!ip) return true; if (ip.startsWith('0.') || ip.startsWith('127.') || ip === '::1') return true; if (ip.startsWith('10.') || ip.startsWith('192.168.')) return true; if (ip.startsWith('172.')) { const s = parseInt(ip.split('.')[1], 10); if (s >= 16 && s <= 31) return true; } if (ip.startsWith('169.254.')) return true; if (ip.startsWith('fc') || ip.startsWith('fd')) return true; if (ip === '::ffff:127.0.0.1') return true; if (ip.startsWith('fe80:')) return true; if (ip.startsWith('::ffff:') && ip.startsWith('::ffff:127.')) return true; return false; } function isValidIP(ip) { if (!ip || typeof ip !== 'string') return false; const ipv4 = /^(?:(?:25[0-5]|2[0-4]\d|1?\d{1,2})\.){3}(?:25[0-5]|2[0-4]\d|1?\d{1,2})$/; if (ipv4.test(ip)) return !isPrivateIP(ip); const ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/; if (ipv6.test(ip)) return !isPrivateIP(ip); return false; } function detectBrowserFamily() { const ua = navigator.userAgent; if (/Chrome\/\d+/.test(ua)) return 'chromium'; if (/Firefox\/\d+/.test(ua)) return 'firefox'; if (/Safari\/\d+/.test(ua)) return 'safari'; return 'unknown'; } const browserFamily = detectBrowserFamily(); const isChromiumBrowser = browserFamily === 'chromium'; const supportsIncomingBitrate = isChromiumBrowser || browserFamily === 'safari'; function extractIP(candidate) { if (!candidate) return null; const parts = candidate.split(' '); if (parts.length < 5) return null; const typIdx = parts.findIndex(p => p === 'typ'); if (typIdx === -1) return null; const typ = parts[typIdx + 1]; if (typ !== 'srflx' && typ !== 'prflx') return null; const ip = parts[4]; return isValidIP(ip) ? ip : null; } function extractmDNS(candidate) { if (!candidate) return null; const parts = candidate.split(' '); if (parts.length < 5) return null; const typIdx = parts.findIndex(p => p === 'typ'); if (typIdx === -1) return null; const typ = parts[typIdx + 1]; if (typ !== 'host') return null; const addr = parts[4]; if (addr && addr.endsWith('.local')) return addr; return null; } function extractIPv6Public(candidate) { if (!candidate) return null; const parts = candidate.split(' '); if (parts.length < 5) return null; const typIdx = parts.findIndex(p => p === 'typ'); if (typIdx === -1) return null; const typ = parts[typIdx + 1]; if (typ !== 'srflx' && typ !== 'prflx') return null; const addr = parts[4]; if (!addr || !addr.includes(':')) return null; const ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/; if (!ipv6.test(addr)) return null; return isPrivateIP(addr) ? null : addr; } function flagEmoji(cc) { if (!cc) return ''; try { return String.fromCodePoint(...[...cc.toUpperCase()].map(c => 0x1F1E6 + c.charCodeAt(0) - 65)); } catch { return ''; } } function countryName(cc) { if (!cc) return ''; try { return new Intl.DisplayNames(['en'], { type: 'region' }).of(cc.toUpperCase()); } catch { return cc; } } function fmtOffset(off) { if (off == null) return '—'; if (typeof off === 'string') { const m = off.match(/([+-]?)(\d{1,2}):(\d{2})(?::(\d{2}))?/); if (m) { const sign = m[1] === '-' ? -1 : 1; const h = parseInt(m[2], 10); const mn = parseInt(m[3], 10); const s = m[4] ? parseInt(m[4], 10) : 0; const totalSec = sign * (h * 3600 + mn * 60 + s); const h2 = Math.floor(Math.abs(totalSec) / 3600); const m2 = Math.floor((Math.abs(totalSec) % 3600) / 60); const sign2 = totalSec >= 0 ? '+' : '-'; return `${sign2}${h2.toString().padStart(2,'0')}:${m2.toString().padStart(2,'0')}`; } const n = Number(off.replace(/[^0-9.-]/g, '')); if (!isNaN(n)) off = n; else return '—'; } let s = Number(off); if (isNaN(s)) return '—'; if (Math.abs(s) < 20) s = s * 3600; const h = Math.floor(Math.abs(s) / 3600); const m = Math.floor((Math.abs(s) % 3600) / 60); const sign = s >= 0 ? '+' : '-'; return `${sign}${h.toString().padStart(2,'0')}:${m.toString().padStart(2,'0')}`; } function badgeCls(v) { if (v === true) return 'background:rgba(248,81,73,.12);border-color:rgba(248,81,73,.35);color:#ff4757;'; if (v === false) return 'background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.35);color:#3fb950;'; return 'background:rgba(139,148,158,.1);border-color:#1b2838;color:#7a8a9a;'; } function ipType(ip) { if (!ip) return 'unknown'; return ip.includes(':') ? 'IPv6' : 'IPv4'; } function threatScore(proxy, hosting, mobile, tor) { let score = 0; if (proxy === true) score += 70; if (hosting === true) score += 35; if (mobile === true) score += 5; if (tor === true) score += 60; return Math.min(100, score); } function threatLabel(score) { if (score >= 70) return { text: 'High Threat', cls: 'danger' }; if (score >= 35) return { text: 'Medium Threat', cls: 'warn' }; if (score > 0) return { text: 'Low Threat', cls: 'clean' }; return { text: 'Clean', cls: 'clean' }; } function isEU(cc) { return EU_COUNTRIES.has((cc || '').toUpperCase()); } function getCallingCode(cc) { return CALLING_CODES[(cc || '').toUpperCase()] || ''; } 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 Math.round(R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))); } function fmtBytes(b) { if (b == null) return '—'; if (b > 1e9) return (b/1e9).toFixed(2) + ' GB'; if (b > 1e6) return (b/1e6).toFixed(2) + ' MB'; if (b > 1e3) return (b/1e3).toFixed(1) + ' KB'; return b + ' B'; } function fmtBitrate(bps) { if (bps == null) return '—'; if (bps > 1e6) return (bps/1e6).toFixed(2) + ' Mbps'; if (bps > 1e3) return (bps/1e3).toFixed(1) + ' Kbps'; return bps + ' bps'; } function fmtMs(ms) { if (ms == null) return '—'; return Math.round(ms) + ' ms'; } function fmtPercent(v) { if (v == null) return '—'; return (v * 100).toFixed(1) + '%'; } function h(tag, attrs = {}, ...children) { const el = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) { if (k === 'className') el.className = v; else if (k === 'dataset' && typeof v === 'object') Object.assign(el.dataset, v); else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v); else if (k.startsWith('on') && typeof v === 'function') { const evt = k.slice(2).toLowerCase(); el.addEventListener(evt, v); } else if (v != null) { el.setAttribute(k, String(v)); } } for (const child of children) { if (child == null) continue; if (child instanceof Node) el.appendChild(child); else el.appendChild(document.createTextNode(String(child))); } return el; } const seenIPs = new Set(); let lastIP = null; let fetching = false; let renderDebounceTimer = null; let currentData = null; let streamerMode = false; let activeTab = 'overview'; let compactMode = false; let myLocation = null; let rtcStats = null; let peerConnections = []; let statsInterval = null; let weatherData = null; let peerHistory = []; let showHistory = false; const HISTORY_KEY = '__gc_peer_history_v3'; const MAX_HISTORY = 50; let themeSetting = 'dark'; let autoHideEnabled = false; let autoHideMs = 30000; let autoHideTimer = null; let notificationsEnabled = false; let bitrateHistory = []; let eventLog = []; let helpVisible = false; // v5.0 state variables let mDNSLeaks = []; let ipv6Leaks = []; let signalingState = null; let iceGatheringState = null; let codecInfo = null; let mediaDevices = null; let screenSharing = false; let natType = null; let natDetecting = false; let peerFingerprint = null; let autoRefreshTimer = null; let sessionTimeline = []; let soundEnabled = true; let autoCopyEnabled = false; let sessionStart = null; let sessionTimerInterval = null; let totalStats = { peers: 0, time: 0, threats: 0, sessions: 0 }; let hookIntervalId = null; let passiveScanIntervalId = null; const STATS_KEY = '__gc_stats_v2'; const SETTINGS_KEY = '__gc_settings_v2'; function loadSettings() { try { const raw = localStorage.getItem(SETTINGS_KEY); if (raw) { const s = JSON.parse(raw); if (s.sound != null) soundEnabled = s.sound; if (s.autoCopy != null) autoCopyEnabled = s.autoCopy; if (s.theme != null) themeSetting = s.theme; if (s.autoHide != null) autoHideEnabled = s.autoHide; if (s.autoHideMs != null) autoHideMs = s.autoHideMs; if (s.notifications != null) notificationsEnabled = s.notifications; if (s.compact != null) compactMode = s.compact; if (s.streamer != null) streamerMode = s.streamer; } } catch (e) {} } function saveSettings() { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify({ sound: soundEnabled, autoCopy: autoCopyEnabled, theme: themeSetting, autoHide: autoHideEnabled, autoHideMs: autoHideMs, notifications: notificationsEnabled, compact: compactMode, streamer: streamerMode })); } catch (e) {} } function loadStats() { try { const raw = localStorage.getItem(STATS_KEY); if (raw) { const s = JSON.parse(raw); if (s && typeof s === 'object') totalStats = { peers: s.peers || 0, time: s.time || 0, threats: s.threats || 0, sessions: s.sessions || 0 }; } } catch (e) {} } function saveStats() { try { localStorage.setItem(STATS_KEY, JSON.stringify(totalStats)); } catch (e) {} } const toastQueue = []; let toastContainer = null; function ensureToastContainer() { if (toastContainer) return; toastContainer = h('div', { id: 'gc-toasts' }); document.body.appendChild(toastContainer); } function showToast(message, type = 'info', duration = 3000) { ensureToastContainer(); const colors = { info: { bg: 'rgba(88,166,255,.15)', border: 'rgba(88,166,255,.3)', color: '#58a6ff' }, success: { bg: 'rgba(0,240,200,.12)', border: 'rgba(0,240,200,.25)', color: '#00f0c8' }, warn: { bg: 'rgba(255,159,67,.12)', border: 'rgba(255,159,67,.25)', color: '#ff9f43' }, danger: { bg: 'rgba(248,81,73,.12)', border: 'rgba(248,81,73,.25)', color: '#ff4757' } }; const c = colors[type] || colors.info; const el = h('div', { className: 'gc-toast', style: { background: c.bg, borderColor: c.border, color: c.color } }, message); toastContainer.appendChild(el); requestAnimationFrame(() => el.classList.add('gc-toast-show')); setTimeout(() => { el.classList.remove('gc-toast-show'); el.addEventListener('transitionend', () => el.remove(), { once: true }); setTimeout(() => { if (el.parentNode) el.remove(); }, 300); }, duration); } function compareVersions(a, b) { const pa = String(a).split('.').map(Number); const pb = String(b).split('.').map(Number); const len = Math.max(pa.length, pb.length); for (let i = 0; i < len; i++) { const na = pa[i] || 0; const nb = pb[i] || 0; if (na > nb) return 1; if (na < nb) return -1; } return 0; } function checkForUpdates(manual = false) { if (typeof GM_xmlhttpRequest === 'undefined') { if (manual) showToast('Update check unavailable', 'warn'); return; } try { GM_xmlhttpRequest({ method: 'GET', url: UPDATE_CHECK_URL + '?t=' + Date.now(), nocache: true, onload: (res) => { if (res.status !== 200) { if (manual) showToast('Update check failed (HTTP ' + res.status + ')', 'warn'); return; } const text = res.responseText || ''; const match = text.match(/\/\/\s*@version\s+(.+)/); if (!match) { if (manual) showToast('Update check failed (no version)', 'warn'); return; } const remoteVersion = match[1].trim(); const currentVersion = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) ? GM_info.script.version : '5.0'; if (compareVersions(remoteVersion, currentVersion) > 0) { if (!updateAvailable || manual) { updateAvailable = true; const msg = h('div', {}, h('div', {}, 'GeoChecker v' + remoteVersion + ' is available.'), h('a', { href: UPDATE_CHECK_URL, target: '_blank', style: 'color:#00f0c8;text-decoration:underline;cursor:pointer;' }, 'Click to update') ); showToast(msg, 'success', 10000); } } else { if (manual) showToast('GeoChecker is up to date (v' + currentVersion + ')', 'info'); updateAvailable = false; } }, onerror: () => { if (manual) showToast('Update check failed (network)', 'warn'); } }); } catch (e) { if (manual) showToast('Update check error', 'warn'); } } function applyTheme(theme) { const box = $('gc-box'); if (!box) return; if (theme === 'light') { box.style.setProperty('--gc-bg', 'rgba(245,247,250,.92)'); box.style.setProperty('--gc-card', 'rgba(255,255,255,.85)'); box.style.setProperty('--gc-text', '#1a202c'); box.style.setProperty('--gc-muted', '#5a6a7a'); box.style.setProperty('--gc-border', 'rgba(200,210,225,.6)'); box.classList.add('light'); } else { box.style.removeProperty('--gc-bg'); box.style.removeProperty('--gc-card'); box.style.removeProperty('--gc-text'); box.style.removeProperty('--gc-muted'); box.style.removeProperty('--gc-border'); box.classList.remove('light'); } } async function requestNotificationPermission() { if (!('Notification' in window)) return false; if (Notification.permission === 'granted') return true; if (Notification.permission === 'denied') return false; try { const p = await Notification.requestPermission(); return p === 'granted'; } catch (e) { return false; } } function notifyPeer(ip, country) { if (!isPro() || !notificationsEnabled || document.visibilityState === 'visible') return; const body = `IP: ${ip}${country ? ' — ' + country : ''}`; try { if (typeof GM_notification !== 'undefined') { GM_notification({ title: 'GeoChecker — New Peer', text: body, image: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22%3E%3Crect width=%22128%22 height=%22128%22 rx=%2228%22 fill=%22%230f172a%22/%3E%3Cpath d=%22M64 28 C48 28 36 40 36 54 C36 72 64 100 64 100 C64 100 92 72 92 54 C92 40 80 28 64 28 Z%22 fill=%22%2300f0c8%22/%3E%3Ccircle cx=%2264%22 cy=%2254%22 r=%2212%22 fill=%22%230f172a%22/%3E%3Ccircle cx=%2264%22 cy=%2254%22 r=%225%22 fill=%22%2300f0c8%22/%3E%3C/svg%3E', timeout: 5000 }); } else { new Notification('GeoChecker — New Peer', { body: body, icon: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22%3E%3Crect width=%22128%22 height=%22128%22 rx=%2228%22 fill=%22%230f172a%22/%3E%3Cpath d=%22M64 28 C48 28 36 40 36 54 C36 72 64 100 64 100 C64 100 92 72 92 54 C92 40 80 28 64 28 Z%22 fill=%22%2300f0c8%22/%3E%3Ccircle cx=%2264%22 cy=%2254%22 r=%2212%22 fill=%22%230f172a%22/%3E%3Ccircle cx=%2264%22 cy=%2254%22 r=%225%22 fill=%22%2300f0c8%22/%3E%3C/svg%3E' }); } } catch (e) {} } function resetAutoHide() { if (!autoHideEnabled) return; if (autoHideTimer) clearTimeout(autoHideTimer); const box = $('gc-box'); if (box && box.style.display === 'none') box.style.display = ''; autoHideTimer = setTimeout(() => { const box = $('gc-box'); if (box && !lastIP) box.style.display = 'none'; }, autoHideMs); } function logEvent(type, detail) { eventLog.unshift({ time: Date.now(), type, detail }); if (eventLog.length > 100) eventLog.length = 100; } function exportHistoryCSV() { if (!peerHistory.length) return; const headers = ['Timestamp','IP','Country Code','Country','City','Region','ISP','Threat Score','Threat Label']; const rows = peerHistory.map(h => [ new Date(h.timestamp).toISOString(), h.ip, h.cc, h.country, h.city, h.region, h.isp, h.tscore, h.tlabel ].map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')); const csv = [headers.join(','), ...rows].join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `geochecker-history-${new Date().toISOString().slice(0,10)}.csv`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); showToast('History exported as CSV', 'success'); } function playBeep() { if (!soundEnabled) return; try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(880, ctx.currentTime); gain.gain.setValueAtTime(0.08, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.15); } catch (e) {} } function startSessionTimer() { if (sessionTimerInterval) clearInterval(sessionTimerInterval); sessionStart = Date.now(); sessionTimerInterval = setInterval(() => { const elapsed = Date.now() - sessionStart; const timerEl = document.getElementById('gc-session-timer'); const statusTimer = document.getElementById('gc-timer'); const txt = fmtDuration(elapsed); if (timerEl) timerEl.textContent = txt; if (statusTimer) { statusTimer.textContent = 'Timer: ' + txt; statusTimer.classList.remove('muted'); } }, 1000); } function stopSessionTimer() { if (sessionTimerInterval) { clearInterval(sessionTimerInterval); sessionTimerInterval = null; } if (sessionStart) { totalStats.time += Date.now() - sessionStart; sessionStart = null; saveStats(); } } function fmtDuration(ms) { const s = Math.floor(ms / 1000); const m = Math.floor(s / 60); const h = Math.floor(m / 60); if (h > 0) return `${h}:${String(m % 60).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; return `${m}:${String(s % 60).padStart(2, '0')}`; } function saveHistory() { try { localStorage.setItem(HISTORY_KEY, JSON.stringify(peerHistory)); } catch (e) {} } function loadHistory() { try { const raw = localStorage.getItem(HISTORY_KEY); if (raw) peerHistory = JSON.parse(raw); if (!Array.isArray(peerHistory)) peerHistory = []; } catch (e) { peerHistory = []; } } function addToHistory(data) { if (!data || !data.ip) return; const idx = peerHistory.findIndex(h => h.ip === data.ip); const entry = { ip: data.ip, timestamp: Date.now(), cc: (data.country_code || data.countryCode || data.country || '').toUpperCase(), city: data.city || data.cityName || '', region: data.region || data.regionName || data.region_name || data.state || data.state_prov || '', country: data.country_name || data.country || 'Unknown', isp: data.isp || data.org || data.organization_name || '', tlabel: threatLabel(threatScore(data.proxy ?? data.is_proxy ?? data.is_vpn ?? null, data.hosting ?? data.is_datacenter ?? null, data.mobile ?? data.cellular ?? data.is_mobile ?? null, data.is_tor ?? null)).text, tscore: threatScore(data.proxy ?? data.is_proxy ?? data.is_vpn ?? null, data.hosting ?? data.is_datacenter ?? null, data.mobile ?? data.cellular ?? data.is_mobile ?? null, data.is_tor ?? null), data: JSON.parse(JSON.stringify(data)) }; if (idx !== -1) peerHistory.splice(idx, 1); peerHistory.unshift(entry); if (peerHistory.length > MAX_HISTORY) peerHistory.length = MAX_HISTORY; saveHistory(); } function renderHistory() { const body = $('gc-body'); if (!body) return; if (!peerHistory.length) { body.innerHTML = `
No peer history yet.
IPs will be logged automatically as you connect.
`; return; } const items = peerHistory.map((h, i) => { const f = flagEmoji(h.cc); const time = new Date(h.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const ago = Math.floor((Date.now() - h.timestamp) / 60000); const agoText = ago < 1 ? 'just now' : ago < 60 ? ago + 'm ago' : Math.floor(ago / 60) + 'h ago'; const loc = [h.city, h.region, h.country].filter(Boolean).join(', ') || '—'; return `
${esc(f)} ${esc(h.ip)} ${esc(h.cc) || '—'}
${esc(loc)} ${esc(h.isp) || '—'} ${esc(agoText)} · ${esc(time)}
`; }).join(''); body.innerHTML = `
← Back ${peerHistory.length} peer${peerHistory.length > 1 ? 's' : ''}
${items}
`; function updateToolbarFromState() { const box = $('gc-box'); if (!box) return; const btns = box.querySelectorAll('.gc-tbar-btn'); btns.forEach(b => b.classList.toggle('on', !showHistory && b.dataset.tab === activeTab)); } body.querySelectorAll('.gc-history-item').forEach(el => { el.addEventListener('click', e => { if (e.target.classList.contains('gc-history-load')) return; const idx = parseInt(el.dataset.idx, 10); const entry = peerHistory[idx]; if (entry && entry.data) { showHistory = false; updateToolbarFromState(); currentData = entry.data; render(currentData, 'history'); } }); }); body.querySelectorAll('.gc-history-load').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); const idx = parseInt(btn.dataset.idx, 10); const entry = peerHistory[idx]; if (entry && entry.data) { showHistory = false; updateToolbarFromState(); currentData = entry.data; render(currentData, 'history'); } }); }); const backBtn = $('gc-history-back'); if (backBtn) backBtn.addEventListener('click', () => { showHistory = false; updateToolbarFromState(); if (currentData) render(currentData, currentData.__source || 'merged'); else renderEmpty(); }); const clearBtn = $('gc-history-clear'); if (clearBtn) clearBtn.addEventListener('click', () => { if (confirm('Clear all peer history?')) { peerHistory = []; saveHistory(); renderHistory(); } }); } function renderEmpty() { const body = $('gc-body'); if (body) body.innerHTML = `
Waiting for peer IP...
Standard WebRTC (Ome.tv, Emerald, ChatHub): auto-captures.
`; } function cleanupPeerConnections() { const before = peerConnections.length; peerConnections = peerConnections.filter(pc => { const state = pc.connectionState || pc.iceConnectionState; return state !== 'closed' && state !== 'failed'; }); if (peerConnections.length === 0 && before > 0) { if (statsInterval) { clearInterval(statsInterval); statsInterval = null; } prevStatsMap.clear(); } } function savePos(l, t, w, h) { try { localStorage.setItem('gc_pos', JSON.stringify({l,t,w,h})); } catch(e){} } function loadPos() { try { return JSON.parse(localStorage.getItem('gc_pos')||'null'); } catch(e){ return null; } } async function fetchMyLoc() { try { const r = await fetch('https://get.geojs.io/v1/ip/geo.json', {cache:'no-store'}); if (r.ok) myLocation = await r.json(); } catch(e) {} } fetchMyLoc(); function injectStyles() { if (document.getElementById('gc-styles')) return; if (!document.getElementById('gc-fonts')) { const link = document.createElement('link'); link.id = 'gc-fonts'; link.rel = 'stylesheet'; link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap'; document.head.appendChild(link); } const s = document.createElement('style'); s.id = 'gc-styles'; s.textContent = ` #gc-box{position:fixed;top:14px;right:14px;width:420px;min-width:260px;max-width:800px;height:640px;min-height:320px;max-height:90vh;overflow:hidden;background:rgba(11,15,20,.88);border:1px solid rgba(27,40,56,.45);color:#c8d6e0;border-radius:20px;padding:0;font-family:'Inter',system-ui,-apple-system,sans-serif;font-size:12px;z-index:2147483647;box-shadow:0 28px 90px rgba(0,0,0,.7),0 0 0 1px rgba(0,240,200,.04) inset;backdrop-filter:blur(28px) saturate(150%);-webkit-backdrop-filter:blur(28px) saturate(150%);line-height:1.5;transition:width .35s cubic-bezier(.4,0,.2,1),height .35s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;container-type:inline-size;container-name:gcbox;} #gc-box.compact{width:280px !important;height:460px !important;max-height:90vh;font-size:10px;} #gc-box.compact #gc-hdr{padding:10px 12px;} #gc-box.compact #gc-hdr .gc-icon{width:20px;height:20px;} #gc-box.compact #gc-hdr .gc-title{font-size:11px;} #gc-box.compact #gc-toolbar{padding:4px 10px 0;gap:2px;} #gc-box.compact .gc-tbar-btn{min-width:36px;height:28px;font-size:9px;padding:0 6px;} #gc-box.compact #gc-body{padding:10px 12px 12px;max-height:340px;} #gc-box.compact .gc-top{margin-bottom:8px;} #gc-box.compact .gc-top .gc-big-ip{font-size:16px;} #gc-box.compact .gc-top .gc-sub{gap:4px;margin-top:3px;} #gc-box.compact .gc-top .gc-flag{font-size:14px;} #gc-box.compact .gc-top .gc-loc{font-size:10px;} #gc-box.compact .gc-badge{padding:2px 5px;font-size:9px;} #gc-box.compact .gc-sec{margin-bottom:8px;border-radius:10px;} #gc-box.compact .gc-sec-hd{padding:6px 10px;font-size:9px;} #gc-box.compact .gc-row{padding:6px 10px;} #gc-box.compact .gc-row .gc-lbl{font-size:10px;} #gc-box.compact .gc-row .gc-val{font-size:11px;} #gc-box.compact .gc-actions{gap:4px;margin-top:8px;} #gc-box.compact .gc-actions button{padding:6px 0;font-size:10px;border-radius:6px;} #gc-box.compact .gc-stat-grid{gap:4px;} #gc-box.compact .gc-stat-card{padding:6px 8px;border-radius:6px;} #gc-box.compact .gc-stat-val{font-size:11px;} #gc-box.compact .gc-stat-lbl{font-size:8px;} #gc-box.compact .gc-map-wrap{height:110px;margin-top:6px;} #gc-box.compact .gc-meter{height:3px;margin-top:4px;} #gc-box.compact .gc-meter-label{font-size:9px;} #gc-box.compact #gc-status{padding:6px 10px;} #gc-box.compact .gc-compact-summary{display:flex;flex-direction:column;gap:0;} #gc-box.compact .gc-compact-row{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid rgba(27,40,56,.3);} #gc-box.compact .gc-compact-row:last-child{border-bottom:none;} #gc-box.compact .gc-compact-label{color:${CONFIG.muted};text-transform:uppercase;letter-spacing:.04em;font-size:9px;} #gc-box.compact .gc-compact-val{color:#fff;font-weight:600;font-size:10px;} .gc-streamer-hide{filter:blur(5px);user-select:none;pointer-events:auto;transition:filter .15s ease;cursor:help;} .gc-streamer-hide:hover{filter:blur(0);} #gc-box.gc-peek .gc-streamer-hide{filter:blur(0);} #gc-hdr{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;border-bottom:1px solid rgba(27,40,56,.35);cursor:move;user-select:none;background:rgba(0,0,0,.08);} #gc-hdr .gc-logo{display:flex;align-items:center;gap:10px;} #gc-hdr .gc-icon{width:28px;height:28px;border-radius:8px;flex-shrink:0;box-shadow:0 0 14px rgba(0,240,200,.2);animation:gcPulse 3s infinite;} #gc-hdr .gc-title{color:${CONFIG.accent};font-size:15px;font-weight:700;letter-spacing:-.02em;} #gc-hdr .gc-ver{font-size:9px;color:${CONFIG.muted};background:rgba(0,240,200,.08);padding:2px 6px;border-radius:4px;margin-left:5px;} #gc-hdr .gc-close{cursor:pointer;color:${CONFIG.muted};font-size:20px;line-height:1;padding:4px 8px;border-radius:8px;transition:all .2s;} #gc-hdr .gc-close:hover{color:${CONFIG.danger};background:rgba(248,81,73,.08);} #gc-toolbar{display:flex;gap:3px;padding:6px 14px 0;border-bottom:1px solid rgba(27,40,56,.3);overflow-x:auto;scrollbar-width:none;} #gc-toolbar::-webkit-scrollbar{display:none;} .gc-tbar-btn{min-width:44px;height:34px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;border-radius:8px;border:1px solid transparent;background:transparent;color:${CONFIG.muted};cursor:pointer;transition:all .2s;flex-shrink:0;padding:0 10px;} .gc-tbar-btn:hover{color:#c8d6e0;background:rgba(255,255,255,.04);} .gc-tbar-btn.on{color:${CONFIG.accent};background:rgba(0,240,200,.08);border-color:rgba(0,240,200,.15);box-shadow:0 1px 0 rgba(0,240,200,.1) inset;} .gc-tbar-btn.on::after{content:'';position:absolute;bottom:-7px;left:50%;transform:translateX(-50%);width:16px;height:2px;background:${CONFIG.accent};border-radius:1px;} .gc-tbar-btn{position:relative;} #gc-body{padding:16px 18px 18px;overflow-y:auto;flex:1;min-height:0;scrollbar-width:thin;scrollbar-color:rgba(27,40,56,.5) transparent;transition:opacity .2s ease;} #gc-body::-webkit-scrollbar{width:5px;} #gc-body::-webkit-scrollbar-track{background:transparent;} #gc-body::-webkit-scrollbar-thumb{background:rgba(27,40,56,.6);border-radius:999px;} #gc-status{display:flex;justify-content:space-between;align-items:center;padding:8px 14px;border-top:1px solid rgba(27,40,56,.3);background:rgba(0,0,0,.08);font-size:10px;color:${CONFIG.muted};} #gc-status .gc-stat-left{display:flex;align-items:center;gap:8px;} #gc-status .gc-stat-pill{background:rgba(14,20,27,.5);padding:2px 8px;border-radius:999px;border:1px solid rgba(27,40,56,.4);font-family:'JetBrains Mono',monospace;font-size:10px;color:${CONFIG.accent};} #gc-status .gc-stat-pill.muted{color:${CONFIG.muted};} .gc-top{margin-bottom:14px;} .gc-top .gc-big-ip{font-family:'JetBrains Mono',monospace;font-size:24px;color:#fff;font-weight:600;letter-spacing:-.02em;word-break:break-all;} .gc-top .gc-sub{display:flex;align-items:center;gap:6px;margin-top:6px;flex-wrap:wrap;} .gc-top .gc-flag{font-size:22px;} .gc-top .gc-loc{color:${CONFIG.muted};font-size:12px;} .gc-top .gc-ip-type{font-size:9px;color:${CONFIG.muted};background:rgba(88,166,255,.1);padding:1px 5px;border-radius:4px;border:1px solid rgba(88,166,255,.15);} .gc-badge{display:inline-flex;align-items:center;font-size:10px;font-weight:700;padding:3px 8px;border-radius:6px;border:1px solid;text-transform:uppercase;letter-spacing:.04em;} .gc-badge.cc{background:rgba(0,240,200,.08);border-color:rgba(0,240,200,.25);color:${CONFIG.accent};} .gc-badge.clean{background:rgba(63,185,80,.08);border-color:rgba(63,185,80,.25);color:${CONFIG.ok};} .gc-badge.warn{background:rgba(255,159,67,.08);border-color:rgba(255,159,67,.25);color:${CONFIG.warn};} .gc-badge.danger{background:rgba(248,81,73,.08);border-color:rgba(248,81,73,.25);color:${CONFIG.danger};} .gc-sec{margin-bottom:12px;background:rgba(14,20,27,.6);border:1px solid rgba(27,40,56,.4);border-radius:14px;overflow:hidden;box-shadow:0 4px 12px rgba(0,0,0,.2),0 1px 0 rgba(255,255,255,.02) inset;animation:gcFadeIn .4s ease both;} .gc-sec:nth-child(2){animation-delay:.06s;} .gc-sec:nth-child(3){animation-delay:.12s;} .gc-sec:nth-child(4){animation-delay:.18s;} .gc-sec:nth-child(5){animation-delay:.24s;} .gc-sec-hd{padding:10px 14px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:${CONFIG.muted};border-bottom:1px solid rgba(27,40,56,.35);background:rgba(0,0,0,.1);display:flex;align-items:center;gap:6px;} .gc-row{display:flex;justify-content:space-between;align-items:center;padding:9px 14px;border-bottom:1px solid rgba(27,40,56,.3);transition:background .15s ease;} .gc-row:last-child{border-bottom:none;} .gc-row:hover{background:rgba(255,255,255,.025);} .gc-row .gc-lbl{font-size:11px;color:${CONFIG.muted};text-transform:uppercase;letter-spacing:.04em;display:flex;align-items:center;gap:5px;} .gc-row .gc-val{font-size:12px;color:#fff;text-align:right;word-break:break-word;max-width:60%;display:flex;align-items:center;gap:5px;} .gc-row .gc-val.mono{font-family:'JetBrains Mono',monospace;} .gc-row .gc-copy-btn{opacity:0;font-size:10px;color:${CONFIG.muted};cursor:pointer;padding:2px 4px;border-radius:3px;transition:all .15s;} .gc-row:hover .gc-copy-btn{opacity:1;} .gc-copy-btn:hover{color:${CONFIG.accent};background:rgba(0,240,200,.08);} .gc-meter{height:4px;background:rgba(27,40,56,.6);border-radius:999px;overflow:hidden;margin-top:8px;} .gc-meter-fill{height:100%;border-radius:999px;transition:width .6s ease;} .gc-meter-label{display:flex;justify-content:space-between;font-size:10px;color:${CONFIG.muted};margin-top:4px;} .gc-map-wrap{width:100%;height:170px;border-radius:12px;overflow:hidden;border:1px solid rgba(27,40,56,.5);margin-top:10px;} .gc-map-wrap iframe{width:100%;height:100%;border:none;} .gc-actions{display:flex;gap:8px;margin-top:12px;} .gc-actions button{flex:1;background:rgba(14,20,27,.6);border:1px solid rgba(27,40,56,.5);color:#c8d6e0;padding:9px 0;border-radius:10px;font-size:11px;font-weight:600;cursor:pointer;text-align:center;transition:all .2s ease;} .gc-actions button:hover{border-color:rgba(0,240,200,.3);color:${CONFIG.accent};transform:translateY(-1px);background:rgba(0,240,200,.04);} .gc-actions button.primary{background:linear-gradient(135deg,${CONFIG.accent},${CONFIG.accent2});color:#031411;border:none;font-weight:700;} .gc-actions button.primary:hover{filter:brightness(1.15);} .gc-stat-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:8px;} .gc-stat-card{background:rgba(14,20,27,.5);border:1px solid rgba(27,40,56,.4);border-radius:10px;padding:10px 12px;text-align:center;} .gc-stat-val{font-family:'JetBrains Mono',monospace;font-size:14px;color:#fff;font-weight:600;} .gc-stat-lbl{font-size:9px;color:${CONFIG.muted};text-transform:uppercase;letter-spacing:.06em;margin-top:3px;} .gc-wait{text-align:center;color:${CONFIG.muted};padding:24px 14px;font-size:12px;line-height:1.6;} .gc-wait strong{color:${CONFIG.accent};font-weight:600;} @keyframes gcFadeIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}} @keyframes gcPulse{0%{box-shadow:0 0 0 0 rgba(0,240,200,.35);}70%{box-shadow:0 0 0 10px rgba(0,240,200,0);}100%{box-shadow:0 0 0 0 rgba(0,240,200,0);}} .gc-icon{animation:gcPulse 3s infinite;} .gc-kb:focus{outline:none;} .gc-history-empty{text-align:center;color:${CONFIG.muted};padding:30px 10px;font-size:12px;line-height:1.6;} .gc-history-empty strong{color:${CONFIG.accent};font-weight:600;} .gc-history-hd{display:flex;justify-content:space-between;align-items:center;padding:0 0 8px;border-bottom:1px solid rgba(27,40,56,.4);margin-bottom:6px;} .gc-history-back{color:${CONFIG.accent};cursor:pointer;font-size:11px;font-weight:600;transition:opacity .15s;} .gc-history-back:hover{opacity:.75;} .gc-history-count{font-size:10px;color:${CONFIG.muted};} .gc-history-list{display:flex;flex-direction:column;gap:0;} .gc-history-item{padding:8px 10px;border-bottom:1px solid rgba(27,40,56,.3);cursor:pointer;transition:background .15s ease;position:relative;} .gc-history-item:last-child{border-bottom:none;} .gc-history-item:hover{background:rgba(255,255,255,.03);} .gc-history-main{display:flex;align-items:center;gap:6px;} .gc-history-flag{font-size:16px;} .gc-history-ip{font-family:'JetBrains Mono',monospace;font-size:12px;color:#fff;font-weight:600;} .gc-history-ip.danger{color:${CONFIG.danger};} .gc-history-ip.warn{color:${CONFIG.warn};} .gc-history-cc{font-size:9px;color:${CONFIG.muted};background:rgba(27,40,56,.4);padding:1px 4px;border-radius:4px;} .gc-history-meta{display:flex;align-items:center;gap:6px;margin-top:3px;flex-wrap:wrap;} .gc-history-loc{font-size:10px;color:${CONFIG.muted};} .gc-history-isp{font-size:10px;color:${CONFIG.muted};} .gc-history-time{font-size:9px;color:rgba(160,174,192,.5);margin-left:auto;} .gc-history-load{position:absolute;right:8px;top:50%;transform:translateY(-50%);font-size:9px;font-weight:600;color:${CONFIG.accent};background:rgba(0,240,200,.08);border:1px solid rgba(0,240,200,.2);border-radius:4px;padding:2px 6px;cursor:pointer;opacity:0;transition:opacity .15s;} .gc-history-item:hover .gc-history-load{opacity:1;} .gc-history-load:hover{background:rgba(0,240,200,.15);} .gc-history-clear{width:100%;margin-top:8px;padding:6px 0;font-size:10px;color:${CONFIG.danger};background:rgba(248,81,73,.06);border:1px solid rgba(248,81,73,.2);border-radius:6px;cursor:pointer;transition:background .15s;} .gc-history-clear:hover{background:rgba(248,81,73,.12);} #gc-box.compact .gc-history-item{padding:6px 8px;} #gc-box.compact .gc-history-main{gap:4px;} #gc-box.compact .gc-history-ip{font-size:11px;} #gc-box.compact .gc-history-flag{font-size:14px;} #gc-box.compact .gc-history-meta{gap:4px;} #gc-box.compact .gc-history-time{display:none;} #gc-box.compact .gc-history-load{padding:1px 4px;right:6px;} .gc-osint-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:8px;} .gc-osint-btn{display:flex;align-items:center;justify-content:center;gap:5px;background:rgba(14,20,27,.6);border:1px solid rgba(27,40,56,.5);color:#c8d6e0;padding:7px 0;border-radius:8px;font-size:10px;font-weight:600;cursor:pointer;text-decoration:none;transition:all .2s ease;} .gc-osint-btn:hover{border-color:rgba(0,240,200,.3);color:${CONFIG.accent};background:rgba(0,240,200,.04);transform:translateY(-1px);} .gc-settings-row{display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid rgba(27,40,56,.3);} .gc-settings-row:last-child{border-bottom:none;} .gc-settings-label{font-size:11px;color:#c8d6e0;} .gc-settings-desc{font-size:10px;color:${CONFIG.muted};margin-top:2px;} .gc-toggle{width:34px;height:18px;border-radius:999px;background:rgba(27,40,56,.6);position:relative;cursor:pointer;transition:background .2s;} .gc-toggle.on{background:${CONFIG.accent};} .gc-toggle::after{content:'';position:absolute;left:2px;top:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:transform .2s;} .gc-toggle.on::after{transform:translateX(16px);} .gc-stat-big{font-family:'JetBrains Mono',monospace;font-size:24px;color:#fff;font-weight:700;} .gc-stat-sub{font-size:10px;color:${CONFIG.muted};text-transform:uppercase;letter-spacing:.06em;margin-top:2px;} .gc-stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px;} .gc-stats-card{background:rgba(14,20,27,.5);border:1px solid rgba(27,40,56,.4);border-radius:10px;padding:12px;text-align:center;} #gc-box.compact .gc-osint-btn{padding:5px 0;font-size:9px;} #gc-box.compact .gc-stat-big{font-size:18px;} #gc-box.compact .gc-stats-card{padding:8px;} #gc-toasts{position:fixed;top:14px;right:444px;z-index:2147483648;display:flex;flex-direction:column;gap:6px;} #gc-box.compact ~ #gc-toasts{right:304px;} .gc-toast{padding:8px 14px;border-radius:10px;border:1px solid;font-size:11px;font-weight:600;opacity:0;transform:translateX(10px);transition:opacity .25s ease,transform .25s ease;max-width:260px;backdrop-filter:blur(12px);} .gc-toast-show{opacity:1;transform:translateX(0);} #gc-box.light{background:rgba(245,247,250,.92);color:#1a202c;border-color:rgba(200,210,225,.6);} #gc-box.light .gc-sec{background:rgba(255,255,255,.6);border-color:rgba(200,210,225,.4);} #gc-box.light .gc-row:hover{background:rgba(0,0,0,.02);} #gc-box.light .gc-stat-card{background:rgba(255,255,255,.5);} .gc-sparkline{display:flex;align-items:flex-end;gap:1px;height:28px;margin-top:6px;} .gc-spark-bar{width:3px;border-radius:1px;background:${CONFIG.accent};opacity:.7;transition:height .3s ease;} .gc-path-wrap{display:flex;align-items:center;gap:6px;margin-top:8px;padding:8px 10px;background:rgba(14,20,27,.5);border:1px solid rgba(27,40,56,.4);border-radius:10px;} .gc-path-node{width:10px;height:10px;border-radius:50%;background:${CONFIG.muted};} .gc-path-node.on{background:${CONFIG.ok};} .gc-path-node.warn{background:${CONFIG.warn};} .gc-path-node.danger{background:${CONFIG.danger};} .gc-path-line{flex:1;height:2px;background:rgba(27,40,56,.5);position:relative;} .gc-path-line::after{content:'';position:absolute;left:0;top:0;height:100%;background:${CONFIG.ok};transition:width .6s ease;} .gc-path-label{font-size:9px;color:${CONFIG.muted};text-transform:uppercase;letter-spacing:.04em;} .gc-quality-ring{width:48px;height:48px;border-radius:50%;position:relative;display:flex;align-items:center;justify-content:center;} .gc-quality-ring svg{position:absolute;top:0;left:0;width:100%;height:100%;transform:rotate(-90deg);} .gc-quality-val{font-family:'JetBrains Mono',monospace;font-size:14px;font-weight:700;color:#fff;} .gc-quality-label{font-size:9px;color:${CONFIG.muted};text-transform:uppercase;letter-spacing:.04em;margin-top:2px;} .gc-osint-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:8px;} .gc-osint-btn{display:flex;align-items:center;justify-content:center;gap:5px;background:rgba(14,20,27,.6);border:1px solid rgba(27,40,56,.5);color:#c8d6e0;padding:7px 0;border-radius:8px;font-size:10px;font-weight:600;cursor:pointer;text-decoration:none;transition:all .2s ease;} .gc-osint-btn:hover{border-color:rgba(0,240,200,.3);color:${CONFIG.accent};background:rgba(0,240,200,.04);transform:translateY(-1px);} .gc-help-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.55);z-index:2147483649;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(16px) saturate(140%);-webkit-backdrop-filter:blur(16px) saturate(140%);animation:gcHelpOverlayIn .25s ease both;} .gc-help-box{background:rgba(11,15,20,.92);border:1px solid rgba(27,40,56,.45);border-radius:20px;padding:26px;max-width:380px;width:90%;box-shadow:0 28px 90px rgba(0,0,0,.7),0 0 0 1px rgba(0,240,200,.04) inset;backdrop-filter:blur(28px) saturate(150%);-webkit-backdrop-filter:blur(28px) saturate(150%);animation:gcHelpBoxIn .35s cubic-bezier(.4,0,.2,1) both;color:#fff;} .gc-help-box h3{color:${CONFIG.accent};font-size:15px;margin-bottom:6px;font-weight:700;letter-spacing:-.02em;} .gc-help-sub{color:${CONFIG.muted};font-size:11px;margin-bottom:16px;} .gc-help-row{display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:1px solid rgba(27,40,56,.25);font-size:12px;animation:gcHelpRowIn .3s ease both;} .gc-help-row:nth-child(3){animation-delay:.05s;} .gc-help-row:nth-child(4){animation-delay:.1s;} .gc-help-row:nth-child(5){animation-delay:.15s;} .gc-help-row:last-child{border-bottom:none;} .gc-help-key{background:rgba(14,20,27,.75);border:1px solid rgba(0,240,200,.2);padding:5px 12px;border-radius:8px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;color:${CONFIG.accent};box-shadow:0 0 12px rgba(0,240,200,.08);transition:all .2s ease;} .gc-help-close{margin-top:20px;width:100%;padding:10px 0;background:linear-gradient(135deg,${CONFIG.accent},${CONFIG.accent2});color:#031411;border:none;border-radius:10px;font-size:12px;font-weight:700;cursor:pointer;transition:all .2s ease;animation:gcHelpRowIn .3s ease .2s both;} .gc-help-close:hover{filter:brightness(1.15);transform:translateY(-1px);box-shadow:0 4px 16px rgba(0,240,200,.25);} .gc-help-close:active{transform:translateY(0);} @keyframes gcHelpOverlayIn{from{opacity:0;}to{opacity:1;}} @keyframes gcHelpBoxIn{from{opacity:0;transform:scale(.92) translateY(10px);}to{opacity:1;transform:scale(1) translateY(0);}} @keyframes gcHelpRowIn{from{opacity:0;transform:translateX(-6px);}to{opacity:1;transform:translateX(0);}} .gc-risk-flag{display:inline-flex;align-items:center;gap:4px;font-size:9px;font-weight:700;padding:2px 6px;border-radius:4px;background:rgba(248,81,73,.1);border:1px solid rgba(248,81,73,.2);color:${CONFIG.danger};text-transform:uppercase;letter-spacing:.04em;} .gc-net-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:8px;} .gc-net-card{background:rgba(14,20,27,.5);border:1px solid rgba(27,40,56,.4);border-radius:8px;padding:8px;text-align:center;} .gc-net-val{font-family:'JetBrains Mono',monospace;font-size:12px;color:#fff;font-weight:600;} .gc-net-lbl{font-size:8px;color:${CONFIG.muted};text-transform:uppercase;letter-spacing:.06em;margin-top:2px;} .gc-timeline-row{display:flex;flex-direction:column;gap:2px;padding:8px 10px;border-bottom:1px solid rgba(27,40,56,.3);font-size:11px;} .gc-timeline-row:last-child{border-bottom:none;} .gc-timeline-time{font-family:'JetBrains Mono',monospace;font-size:9px;color:${CONFIG.muted};} .gc-timeline-event{color:#fff;font-weight:600;} .gc-timeline-details{font-size:9px;color:${CONFIG.muted};word-break:break-all;} #gc-box.compact .gc-timeline-row{padding:6px 8px;} .gc-resize-handle{position:absolute;bottom:0;right:0;width:18px;height:18px;cursor:se-resize;z-index:10;} .gc-resize-handle::after{content:'';position:absolute;bottom:4px;right:4px;width:8px;height:8px;border-right:2px solid rgba(160,174,192,.4);border-bottom:2px solid rgba(160,174,192,.4);border-radius:0 0 2px 0;} .gc-resize-handle:hover::after{border-color:${CONFIG.accent};} @container gcbox (max-width: 340px) { .gc-net-grid{grid-template-columns:repeat(2,1fr);} .gc-osint-grid{grid-template-columns:repeat(2,1fr);} .gc-stat-grid{grid-template-columns:1fr;} .gc-stats-grid{grid-template-columns:1fr;} .gc-row .gc-val{max-width:55%;font-size:11px;} .gc-row .gc-lbl{font-size:10px;} .gc-top .gc-big-ip{font-size:18px;} } @container gcbox (max-width: 280px) { .gc-net-grid{grid-template-columns:1fr;} .gc-osint-grid{grid-template-columns:1fr;} } @container gcbox (min-width: 560px) { .gc-osint-grid{grid-template-columns:repeat(4,1fr);} .gc-net-grid{grid-template-columns:repeat(3,1fr);} .gc-stat-grid{grid-template-columns:repeat(3,1fr);} .gc-stats-grid{grid-template-columns:repeat(3,1fr);} .gc-row .gc-val{max-width:65%;} } @container gcbox (min-width: 700px) { .gc-osint-grid{grid-template-columns:repeat(5,1fr);} .gc-stat-grid{grid-template-columns:repeat(4,1fr);} .gc-stats-grid{grid-template-columns:repeat(4,1fr);} } `; document.head.appendChild(s); } function createUI() { if ($('gc-box')) return; injectStyles(); const box = document.createElement('div'); box.id = 'gc-box'; const saved = loadPos(); if (saved) { box.style.left = saved.l + 'px'; box.style.top = saved.t + 'px'; box.style.right = 'auto'; if (saved.w) box.style.width = saved.w + 'px'; if (saved.h) box.style.height = saved.h + 'px'; } box.innerHTML = `
×
${isPro() ? '' : ''} ${isPro() ? '' : ''}
Waiting for peer IP...
Standard WebRTC (Ome.tv, Emerald, ChatHub): auto-captures.
Timer: 0:00 Peers: 0
Sound On
`; function appendBox(cb) { if (document.body) { document.body.appendChild(box); cb(); } else { const obs = new MutationObserver(() => { if (document.body) { document.body.appendChild(box); obs.disconnect(); cb(); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); } } appendBox(() => { $('gc-x').onclick = () => { savePos(box.offsetLeft, box.offsetTop, box.offsetWidth, box.offsetHeight); box.remove(); }; const hdr = $('gc-hdr'); let drag = false, sx, sy; hdr.addEventListener('mousedown', e => { drag = true; sx = e.clientX - box.offsetLeft; sy = e.clientY - box.offsetTop; box.style.transition='none'; }); document.addEventListener('mousemove', e => { if (!drag) return; box.style.left = (e.clientX - sx) + 'px'; box.style.top = (e.clientY - sy) + 'px'; box.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (drag) { drag = false; box.style.transition='width .35s cubic-bezier(.4,0,.2,1),height .35s cubic-bezier(.4,0,.2,1)'; savePos(box.offsetLeft, box.offsetTop, box.offsetWidth, box.offsetHeight); } }); const resizer = document.createElement('div'); resizer.className = 'gc-resize-handle'; box.appendChild(resizer); let resizing = false, rsx, rsy, rsw, rsh; resizer.addEventListener('mousedown', e => { resizing = true; rsx = e.clientX; rsy = e.clientY; rsw = box.offsetWidth; rsh = box.offsetHeight; box.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (!resizing) return; const newW = Math.min(Math.max(rsw + e.clientX - rsx, 260), 800); const newH = Math.min(Math.max(rsh + e.clientY - rsy, 320), window.innerHeight * 0.9); box.style.width = newW + 'px'; box.style.height = newH + 'px'; }); document.addEventListener('mouseup', () => { if (resizing) { resizing = false; box.style.transition='width .35s cubic-bezier(.4,0,.2,1),height .35s cubic-bezier(.4,0,.2,1)'; savePos(box.offsetLeft, box.offsetTop, box.offsetWidth, box.offsetHeight); } }); const toolbarBtns = box.querySelectorAll('.gc-tbar-btn'); toolbarBtns.forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab; if (tab === 'history') { showHistory = !showHistory; toolbarBtns.forEach(b => b.classList.toggle('on', showHistory && b.dataset.tab === 'history')); if (showHistory) renderHistory(); else if (currentData) render(currentData, currentData.__source || 'merged'); else renderEmpty(); return; } if (tab === 'help') { toggleHelp(); return; } showHistory = false; toolbarBtns.forEach(b => b.classList.toggle('on', b.dataset.tab === tab)); activeTab = tab; if (currentData) render(currentData, currentData.__source || 'merged'); }); }); loadHistory(); loadSettings(); loadLicense(); loadStats(); applyTheme(themeSetting); box.classList.toggle('compact', compactMode); document.addEventListener('keydown', e => { if (e.ctrlKey && e.shiftKey && e.key === 'G') { e.preventDefault(); box.style.display = box.style.display === 'none' ? '' : 'none'; } if (e.key === 'Shift' && streamerMode) { box.classList.add('gc-peek'); } if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) { const target = e.target; if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return; e.preventDefault(); toggleHelp(); } if (e.key === 'Escape' && helpVisible) { helpVisible = false; const existing = document.getElementById('gc-help-overlay'); if (existing) existing.remove(); } }); document.addEventListener('keyup', e => { if (e.key === 'Shift') { box.classList.remove('gc-peek'); } }); }); } function toggleHelp() { helpVisible = !helpVisible; const existing = document.getElementById('gc-help-overlay'); if (existing) { existing.remove(); return; } if (!helpVisible) return; const overlay = h('div', { id: 'gc-help-overlay', className: 'gc-help-overlay', onclick: (e) => { if (e.target === overlay) { helpVisible = false; overlay.remove(); } } }, h('div', { className: 'gc-help-box' }, h('h3', {}, 'Keyboard Shortcuts'), h('div', { className: 'gc-help-sub' }, 'Press these keys anywhere on the page'), h('div', { className: 'gc-help-row' }, h('span', {}, 'Toggle overlay'), h('span', { className: 'gc-help-key' }, 'Ctrl + Shift + G')), h('div', { className: 'gc-help-row' }, h('span', {}, 'Show help'), h('span', { className: 'gc-help-key' }, '?')), h('div', { className: 'gc-help-row' }, h('span', {}, 'Peek streamer mode'), h('span', { className: 'gc-help-key' }, 'Hold Shift')), h('div', { className: 'gc-help-row' }, h('span', {}, 'Close overlay'), h('span', { className: 'gc-help-key' }, 'X button')), h('button', { className: 'gc-help-close', onclick: () => { helpVisible = false; overlay.remove(); } }, 'Got it') ) ); document.body.appendChild(overlay); } function riskLabel(proxy, hosting, mobile, tor) { if (tor === true) return { text: 'High Risk', cls: 'danger' }; if (proxy === true) return { text: 'High Risk', cls: 'danger' }; if (hosting === true) return { text: 'Medium Risk', cls: 'warn' }; if (mobile === true) return { text: 'Low Risk', cls: 'clean' }; return { text: 'Clean', cls: 'clean' }; } function connType(hosting, mobile) { if (mobile === true) return 'Mobile / Wireless'; if (hosting === true) return 'Hosting / Datacenter'; return 'Residential'; } function riskCountry(cc) { return CONFIG.riskCountries.has((cc || '').toUpperCase()); } function connectionQualityScore(stats) { let score = 100; if (stats.rtt != null) { if (stats.rtt > 300) score -= 40; else if (stats.rtt > 150) score -= 20; else if (stats.rtt > 80) score -= 10; } if (stats.jitter != null) { if (stats.jitter > 100) score -= 25; else if (stats.jitter > 50) score -= 15; else if (stats.jitter > 20) score -= 5; } if (stats.fractionLoss != null) { if (stats.fractionLoss > 0.05) score -= 30; else if (stats.fractionLoss > 0.02) score -= 15; else if (stats.fractionLoss > 0.005) score -= 5; } if (stats.framesDropped != null && stats.framesReceived != null) { const ratio = stats.framesDropped / (stats.framesDropped + stats.framesReceived); if (ratio > 0.1) score -= 20; else if (ratio > 0.05) score -= 10; } return Math.max(0, Math.min(100, Math.round(score))); } function qualityLabel(score) { if (score >= 80) return { text: 'Excellent', cls: 'clean', color: '#3fb950' }; if (score >= 60) return { text: 'Good', cls: 'clean', color: '#58a6ff' }; if (score >= 40) return { text: 'Fair', cls: 'warn', color: '#ff9f43' }; return { text: 'Poor', cls: 'danger', color: '#ff4757' }; } function renderDebounced(data, sourceName) { if (renderDebounceTimer) clearTimeout(renderDebounceTimer); renderDebounceTimer = setTimeout(() => { render(data, sourceName); }, 300); } function render(data, sourceName) { const body = $('gc-body'); if (!body) return; currentData = data; data.__source = sourceName; if (data && data.ip) { addToHistory(data); notifyPeer(data.ip, countryName((data.country_code || data.countryCode || data.country || '').toUpperCase())); resetAutoHide(); logEvent('peer_found', { ip: data.ip, cc: data.country_code || data.countryCode || '' }); } if (data.location && typeof data.location === 'object') { const loc = data.location; if (!data.city && loc.city) data.city = loc.city; if (!data.region && loc.state) data.region = loc.state; if (!data.state && loc.state) data.state = loc.state; if (!data.country && loc.country) data.country = loc.country; if (!data.country_code && loc.country_code) data.country_code = loc.country_code; if (!data.latitude && loc.latitude != null) data.latitude = loc.latitude; if (!data.longitude && loc.longitude != null) data.longitude = loc.longitude; if (!data.continent && loc.continent) data.continent = loc.continent; } const cc = (data.country_code || data.countryCode || data.country || '').toUpperCase(); const f = flagEmoji(cc); const cn = countryName(cc) || data.country_name || data.country || 'Unknown'; const lat = parseFloat(data.latitude ?? data.lat); const lon = parseFloat(data.longitude ?? data.lon); const hasCoords = !isNaN(lat) && !isNaN(lon) && isFinite(lat) && isFinite(lon); const org = data.organization_name || data.org || data.organization || data.connection?.organization || data.isp || 'N/A'; const isp = data.isp || data.connection?.isp || data.connection?.org || data.org || data.organization_name || 'N/A'; const tzRaw = data.timezone ?? data.location?.timezone ?? data.time_zone?.name ?? data.time_zone?.id ?? data.timeZone ?? (Array.isArray(data.timeZones) ? data.timeZones[0] : null); const tz = (typeof tzRaw === 'string' ? tzRaw : tzRaw?.id ?? tzRaw?.name) || 'N/A'; const off = data.offset ?? data.timezone?.offset ?? data.time_zone?.offset ?? data.timezone_offset ?? data.timeZoneOffset ?? data.utc_offset ?? data.location?.utcoffset ?? null; const curRaw = data.currency ?? data.location?.currency_code ?? (Array.isArray(data.currencies) ? data.currencies[0] : null); const cur = (curRaw && typeof curRaw === 'object' ? curRaw.code : curRaw) || 'N/A'; const continent = data.continent || data.continent_code || data.continent_name || data.location?.continent || ''; const postal = data.postal || data.zip || data.zipcode || data.postal_code || data.location?.zip || data.zipCode || ''; const sec = data.security || {}; const proxy = data.proxy ?? data.is_proxy ?? data.is_vpn ?? sec.proxy ?? sec.vpn ?? null; const hosting = data.hosting ?? data.is_datacenter ?? sec.hosting ?? null; const mobile = data.mobile ?? data.cellular ?? data.is_mobile ?? sec.mobile ?? sec.anonymous ?? null; const tor = data.is_tor ?? sec.tor ?? null; const risk = riskLabel(proxy, hosting, mobile, tor); const conn = connType(hosting, mobile); const tscore = threatScore(proxy, hosting, mobile, tor); const tlabel = threatLabel(tscore); const ipDisplay = streamerMode ? '***.***.***.***' : data.ip; const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const iptype = ipType(data.ip); const calling = getCallingCode(cc); const eu = isEU(cc); let dist = ''; if (hasCoords && myLocation) { const myLat = parseFloat(myLocation.latitude); const myLon = parseFloat(myLocation.longitude); if (!isNaN(myLat) && !isNaN(myLon)) dist = haversine(myLat, myLon, lat, lon) + ' km'; } const isRiskCountry = riskCountry(cc); const qScore = connectionQualityScore(rtcStats || {}); const qLabel = qualityLabel(qScore); const copyBtn = (text) => `copy`; const row = (lbl, val, mono) => { const txt = typeof val === 'string' ? val : String(val); const isHtml = typeof val === 'string' && val.includes('<') && val.includes('>'); const display = isHtml ? val : esc(val); return `
${esc(lbl)}${display}${copyBtn(txt)}
`; }; const section = (title, rows) => `
${esc(title)}
${rows}
`; const meter = (score) => { const color = score >= 70 ? '#ff4757' : score >= 35 ? '#ff9f43' : score > 0 ? '#3fb950' : '#3fb950'; const width = Math.max(score, 2); return `
Threat Level${score}/100
`; }; const statCard = (val, lbl) => `
${esc(val)}
${esc(lbl)}
`; const sens = (val) => { const str = val == null ? '' : String(val); return streamerMode ? `${esc(str)}` : esc(str); }; let html = ''; if (activeTab === 'overview') { if (compactMode) { const locationText = [data.city, data.region || data.regionName || data.region_name || data.state || data.state_prov].filter(Boolean).map(esc).join(', '); html = `
${sens(ipDisplay)}${esc(iptype)}
${esc(cc) || '—'} ${esc(f)} ${esc(tlabel.text)} ${sens(locationText)}
Connection${esc(conn)}
Peers${seenIPs.size}
Threat${esc(tlabel.text)} (${esc(tscore)}/100)
ISP${sens(isp)}
Proxy/VPN${proxy === true ? 'Detected' : proxy === false ? 'Clean' : 'N/A'}
`; } else { const weatherHtml = weatherData ? statCard(weatherData.temperature + '°C', weatherData.weather) : ''; html = `
${sens(ipDisplay)}${esc(iptype)}
${esc(cc) || '—'} ${esc(tlabel.text)} ${esc(f)} ${isRiskCountry ? 'High-Risk Country' : ''} ${sens([data.city, data.region || data.regionName || data.region_name || data.state || data.state_prov, cn].filter(Boolean).map(esc).join(', '))}
${section('Connection Quality', '
' + '
' + qScore + '
' + '
' + qLabel.text + '
Quality Score
' + '
' + (bitrateHistory.length ? '
' + bitrateHistory.map(b => '
').join('') + '
Bitrate History' + fmtBitrate(bitrateHistory[bitrateHistory.length - 1]) + '
' : '') )} ${section('Threat Analysis', meter(tscore) + row('Risk Score', tlabel.text) + row('Connection', conn) + row('Proxy/VPN', proxy === true ? 'Detected' : proxy === false ? 'Clean' : 'N/A') + row('Hosting', hosting === true ? 'Yes' : hosting === false ? 'No' : 'N/A') + row('Tor', tor === true ? 'Detected' : tor === false ? 'Clean' : 'N/A') + (isRiskCountry ? row('Country Risk', 'High-Risk Jurisdiction') : ''))} ${section('Quick Stats', statCard(seenIPs.size, 'Peers') + statCard(cur, 'Currency') + statCard(calling || '—', 'Calling Code') + statCard(eu ? 'Yes' : 'No', 'EU/GDPR') )} ${weatherHtml ? section('Weather', weatherHtml) : ''}
${isPro() ? '' : ''}
`; } } if (activeTab === 'location') { html = ` ${section('Address', row('Full Address', sens([data.city, data.region || data.regionName || data.region_name || data.state || data.state_prov, postal, cn].filter(Boolean).map(esc).join(' '))) + row('Country', esc(cn) + (cc ? ' (' + esc(cc) + ')' : '')) + row('Region', sens(data.region || data.regionName || data.region_name || data.state || data.state_prov || 'N/A')) + row('City', sens(data.city || data.cityName || 'N/A')) + row('Postal', sens(postal || 'N/A')) + row('Continent', continent || 'N/A'))} ${section('Coordinates', row('Latitude', sens(lat ?? 'N/A'), true) + row('Longitude', sens(lon ?? 'N/A'), true) + row('Distance from you', sens(dist || 'N/A')))}
`; } if (activeTab === 'network') { const s = rtcStats || {}; const pathNodes = [ { label: 'You', on: true }, { label: s.localType === 'relay' || s.remoteType === 'relay' ? 'TURN Relay' : 'Direct', on: s.localType !== 'relay' && s.remoteType !== 'relay', warn: s.localType === 'relay' || s.remoteType === 'relay' }, { label: 'Peer', on: s.state === 'connected' || s.state === 'completed' } ]; const pathHtml = '
' + pathNodes.map((n, i) => '
' + esc(n.label) + '
' + (i < pathNodes.length - 1 ? '
' : '')).join('') + '
'; html = ` ${section('Network Details', row('Timezone', tz) + row('UTC Offset', fmtOffset(off)) + row('Currency', cur) + row('ISP / Provider', sens(isp)) + row('Organisation', sens(org)) + row('ASN', sens(data.asn ? (String(data.asn).startsWith('AS') ? data.asn : 'AS' + data.asn) : data.as || 'N/A'), true))} ${isPro() ? section('Connection Path', pathHtml) : ''} ${section('WebRTC Metrics', '
' + '
' + fmtBitrate(s.inboundBitrate) + '
Bitrate
' + '
' + fmtMs(s.rtt) + '
RTT
' + '
' + fmtPercent(s.fractionLoss) + '
Packet Loss
' + '
' + fmtMs(s.jitter) + '
Jitter
' + '
' + (s.frameWidth || '—') + 'x' + (s.frameHeight || '—') + '
Resolution
' + '
' + (s.framesPerSecond || '—') + '
FPS
' + '
' + row('Local Candidate', s.localType || '—') + row('Remote Candidate', s.remoteType || '—') + row('Connection State', s.state || '—') + row('Signaling State', signalingState || '—') + row('ICE Gathering', iceGatheringState || '—') + row('Frames Dropped', s.framesDropped != null ? s.framesDropped : '—') + (isChromiumBrowser ? row('Quality Limitation', s.qualityLimitation || '—') : '') + row('NACK / FIR / PLI', [s.nackCount, s.firCount, s.pliCount].map(v => v != null ? v : '—').join(' / ')) + row('Audio Level', s.audioLevel != null ? (s.audioLevel * 100).toFixed(1) + '%' : '—') + (supportsIncomingBitrate ? row('Available Incoming BW', s.availableIncomingBitrate != null ? fmtBitrate(s.availableIncomingBitrate) : 'N/A') : '') + row('Available Outgoing BW', s.availableOutgoingBitrate != null ? fmtBitrate(s.availableOutgoingBitrate) : '—') + row('ICE Packets Sent', s.packetsSent != null ? s.packetsSent : '—') + row('ICE Packets Received', s.packetsReceived != null ? s.packetsReceived : '—') + row('ICE Requests Sent', s.iceRequestsSent != null ? s.iceRequestsSent : '—') + row('ICE Responses Received', s.iceResponsesReceived != null ? s.iceResponsesReceived : '—') + (isPro() ? row('DTLS State', s.dtlsState || '—') : '') + (isPro() ? row('DTLS Role', s.dtlsRole || '—') : '') + (isPro() ? row('SRTP Cipher', s.srtpCipher || '—') : '') + (isPro() ? row('TLS Version', s.tlsVersion || '—') : '') + (isPro() ? row('ICE Role', s.iceRole || '—') : '') + (isPro() ? row('ICE Transport State', s.iceState || '—') : '') + (isPro() ? row('Codecs', s.codecs && s.codecs.length ? s.codecs.map(c => c.mimeType).join(', ') : '—') : '') + (isPro() ? row('NAT Type', natType || 'Detecting...') : '') + (isPro() ? row('Screen Sharing', screenSharing ? 'Detected' : 'Not detected') : '') + (isPro() ? row('Peer Fingerprint', peerFingerprint || '—') : '') )} `; } if (activeTab === 'security') { html = `
${sens(ipDisplay)}
${esc(tlabel.text)}Score ${esc(tscore)}/100
${section('Threat Meter', meter(tscore))} ${section('Security Flags', row('Proxy / VPN', proxy === true ? 'Detected' : proxy === false ? 'Clean' : 'N/A') + row('Hosting / DC', hosting === true ? 'Yes' : hosting === false ? 'No' : 'N/A') + row('Mobile Network', mobile === true ? 'Yes' : mobile === false ? 'No' : 'N/A') + row('Tor Exit Node', tor === true ? 'Detected' : tor === false ? 'Clean' : 'N/A') + row('IP Version', iptype) )} ${isPro() ? section('OSINT Lookup', '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' ) : ''} ${isPro() && mDNSLeaks.length ? section('mDNS Leaks', mDNSLeaks.map(h => row('Hostname', h)).join('')) : ''} ${isPro() && ipv6Leaks.length ? section('IPv6 Leaks', ipv6Leaks.map(ip => row('Address', ip)).join('')) : ''} ${isRiskCountry ? section('Country Risk', row('Jurisdiction', 'High-Risk Country — Exercise Caution')) : ''} ${section('Connection Info', row('Type', conn) + row('Last Seen', now) + (isPro() ? row('NAT Type', natType || 'Detecting...') : '') + (isPro() ? row('Screen Sharing', screenSharing ? 'Detected' : 'Not detected') : ''))} `; } if (activeTab === 'map') { if (hasCoords) { const bbox = `${encodeURIComponent((lon - 0.05).toFixed(4))}%2C${encodeURIComponent((lat - 0.05).toFixed(4))}%2C${encodeURIComponent((lon + 0.05).toFixed(4))}%2C${encodeURIComponent((lat + 0.05).toFixed(4))}`; const marker = `${encodeURIComponent(lat.toFixed(4))}%2C${encodeURIComponent(lon.toFixed(4))}`; html = `
`; } else { html = `
No coordinates available for map preview.
`; } } if (activeTab === 'stats') { const avgThreat = peerHistory.length ? (peerHistory.reduce((a, h) => a + h.tscore, 0) / peerHistory.length).toFixed(1) : '0'; const topCountry = peerHistory.length ? Object.entries(peerHistory.reduce((acc, h) => { acc[h.cc] = (acc[h.cc] || 0) + 1; return acc; }, {})).sort((a, b) => b[1] - a[1])[0] : null; html = ` ${section('Session', row('Current Timer', `${fmtDuration(sessionStart ? Date.now() - sessionStart : 0)}`, true) + row('Current Peer', data.ip || '—', true) + row('Current Threat', tlabel.text) )} ${section('All-Time Stats', '
' + '
' + esc(totalStats.peers) + '
Total Peers
' + '
' + esc(fmtDuration(totalStats.time)) + '
Total Time
' + '
' + esc(totalStats.threats) + '
Threats Found
' + '
' + esc(totalStats.sessions) + '
Sessions
' + '
' )} ${section('History Insights', row('Peers Logged', peerHistory.length) + row('Avg Threat Score', avgThreat + '/100') + row('Top Country', (topCountry ? (flagEmoji(topCountry[0]) + ' ' + topCountry[0] + ' (' + topCountry[1] + ')') : '—')) + row('Today\'s Peers', peerHistory.filter(h => h.timestamp > Date.now() - 86400000).length) )} ${section('Local Media Devices', (mediaDevices && mediaDevices.length ? mediaDevices.map(d => row(d.kind === 'videoinput' ? 'Camera' : d.kind === 'audioinput' ? 'Microphone' : 'Speaker', d.label || 'Unknown')).join('') : row('Devices', 'No permission granted or no devices')) )} ${section('Peer Fingerprint', row('Fingerprint', peerFingerprint || '—'))} `; } if (activeTab === 'timeline') { html = ` ${section('Session Timeline', (sessionTimeline.length ? sessionTimeline.slice().reverse().map(t => `
${esc(t.time)}${esc(t.event)}${t.details ? '' + (typeof t.details === 'string' ? esc(t.details) : esc(JSON.stringify(t.details))) + '' : ''}
`).join('') : '
No events yet. Start a video call to see the timeline.
') )} `; } if (activeTab === 'settings') { html = ` ${section('Preferences', '
Sound Notification
Play a beep when a new peer is found
' + '
Auto-Copy IP
Automatically copy peer IP to clipboard
' + '
Compact Mode
Shrink the widget for minimal screen use
' + '
Streamer Mode
Blur sensitive info — hover or hold Shift to peek
' + '
Light Theme
Switch between dark and light UI
' + '
Auto-Hide
Hide widget when no peer is found
' + (isPro() ? '
Desktop Notifications
Notify when a new peer appears
' : '') )} ${section('Data', '
History Size
Max ' + esc(MAX_HISTORY) + ' peers stored
' + esc(peerHistory.length) + '/' + esc(MAX_HISTORY) + '
' + (isPro() ? '
Export History
Download peer history as CSV
' : '') + '
Clear All Data
Wipe history, stats, and settings
' )} ${section('License', '
Tier
Current plan
' + esc(licenseTier.charAt(0).toUpperCase() + licenseTier.slice(1)) + '
' + '
' + '' )} ${section('About', row('Version', 'v5.0') + row('APIs', 'geojs.io, freeipapi.com, ipapi.is') + '
GeoChecker is an open-source WebRTC IP geolocation tool. No API key required. Works on any video chat site.
' )} `; } body.innerHTML = html; body.querySelectorAll('.gc-copy-btn').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); try { await navigator.clipboard.writeText(btn.dataset.copy); } catch (e) { const ta = document.createElement('textarea'); ta.value = btn.dataset.copy; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = 'copy', 800); }); }); const copyBtnEl = $('gc-copy'); if (copyBtnEl) { copyBtnEl.addEventListener('click', async () => { const lines = [ 'IP: ' + data.ip, 'Type: ' + iptype, 'Country: ' + cn + ' (' + cc + ')', 'Region: ' + (data.region || data.regionName || data.region_name || data.state || data.state_prov || 'N/A'), 'City: ' + (data.city || data.cityName || 'N/A'), 'Postal: ' + (postal || 'N/A'), 'Lat: ' + (lat ?? 'N/A'), 'Lon: ' + (lon ?? 'N/A'), 'Timezone: ' + tz, 'ISP: ' + isp, 'Org: ' + org, 'ASN: ' + (data.asn ? (String(data.asn).startsWith('AS') ? data.asn : 'AS' + data.asn) : data.as || 'N/A'), 'Threat: ' + tlabel.text + ' (' + tscore + '/100)', 'Proxy: ' + (proxy === true ? 'Yes' : proxy === false ? 'No' : 'N/A'), 'Hosting: ' + (hosting === true ? 'Yes' : hosting === false ? 'No' : 'N/A') ]; try { await navigator.clipboard.writeText(lines.join('\n')); } catch (e) { const ta = document.createElement('textarea'); ta.value = lines.join('\n'); ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } copyBtnEl.textContent = 'Copied!'; setTimeout(() => copyBtnEl.textContent = 'Copy Info', 1200); }); } const exportBtn = $('gc-export'); if (exportBtn) { exportBtn.addEventListener('click', async () => { const payload = { ...data, __threatScore: tscore, __threatLabel: tlabel.text, __rtcStats: rtcStats, __timestamp: Date.now() }; try { await navigator.clipboard.writeText(JSON.stringify(payload, null, 2)); } catch (e) { const ta = document.createElement('textarea'); ta.value = JSON.stringify(payload, null, 2); ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } exportBtn.textContent = 'Exported!'; setTimeout(() => exportBtn.textContent = 'Export JSON', 1200); }); } const mapBtn = $('gc-map'); if (mapBtn && hasCoords) { mapBtn.addEventListener('click', () => window.open(`https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=14/${lat}/${lon}`, '_blank')); } const mapFull = $('gc-map-full'); if (mapFull && hasCoords) { mapFull.addEventListener('click', () => window.open(`https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=14/${lat}/${lon}`, '_blank')); } if (hasCoords && !weatherData) { fetch(CONFIG.weatherApi(lat, lon), {cache:'no-store'}) .then(r => r.ok ? r.json() : null) .then(d => { if (d?.current_weather) { weatherData = d.current_weather; if (currentData) render(currentData, currentData.__source || 'merged'); } }) .catch(() => {}); } // OSINT lookup buttons const osintBtns = { 'gc-osint-shodan': CONFIG.osint.shodan(data.ip), 'gc-osint-abuseipdb': CONFIG.osint.abuseipdb(data.ip), 'gc-osint-virustotal': CONFIG.osint.virustotal(data.ip), 'gc-osint-ipinfo': CONFIG.osint.ipinfo(data.ip), 'gc-osint-greynoise': CONFIG.osint.greynoise(data.ip), 'gc-osint-censys': CONFIG.osint.censys(data.ip), 'gc-osint-ipvoid': CONFIG.osint.ipvoid(data.ip), 'gc-osint-threatfox': CONFIG.osint.threatfox(data.ip), 'gc-osint-alienvault': CONFIG.osint.alienvault(data.ip), 'gc-osint-talos': CONFIG.osint.talos(data.ip), 'gc-osint-torexit': CONFIG.osint.torexit(data.ip) }; for (const [id, url] of Object.entries(osintBtns)) { const btn = $(id); if (btn) btn.addEventListener('click', () => window.open(url, '_blank')); } // Settings toggles const soundToggle = $('gc-toggle-sound'); if (soundToggle) { soundToggle.addEventListener('click', () => { soundEnabled = !soundEnabled; soundToggle.classList.toggle('on', soundEnabled); const soundInd = $('gc-sound-indicator'); if (soundInd) soundInd.textContent = soundEnabled ? 'Sound On' : 'Sound Off'; saveSettings(); }); } const autoCopyToggle = $('gc-toggle-autocopy'); if (autoCopyToggle) { autoCopyToggle.addEventListener('click', () => { autoCopyEnabled = !autoCopyEnabled; autoCopyToggle.classList.toggle('on', autoCopyEnabled); saveSettings(); }); } const compactToggle = $('gc-toggle-compact'); if (compactToggle) { compactToggle.addEventListener('click', () => { compactMode = !compactMode; compactToggle.classList.toggle('on', compactMode); const box = $('gc-box'); if (box) box.classList.toggle('compact', compactMode); saveSettings(); }); } const streamerToggle = $('gc-toggle-streamer'); if (streamerToggle) { streamerToggle.addEventListener('click', () => { streamerMode = !streamerMode; streamerToggle.classList.toggle('on', streamerMode); if (currentData) render(currentData, currentData.__source || 'merged'); saveSettings(); }); } const themeToggle = $('gc-toggle-theme'); if (themeToggle) { themeToggle.addEventListener('click', () => { themeSetting = themeSetting === 'light' ? 'dark' : 'light'; themeToggle.classList.toggle('on', themeSetting === 'light'); applyTheme(themeSetting); saveSettings(); }); } const autoHideToggle = $('gc-toggle-autohide'); if (autoHideToggle) { autoHideToggle.addEventListener('click', () => { autoHideEnabled = !autoHideEnabled; autoHideToggle.classList.toggle('on', autoHideEnabled); saveSettings(); resetAutoHide(); }); } const notifyToggle = $('gc-toggle-notify'); if (notifyToggle) { notifyToggle.addEventListener('click', async () => { if (!notificationsEnabled) { const granted = await requestNotificationPermission(); if (!granted) { showToast('Notification permission denied', 'warn'); return; } } notificationsEnabled = !notificationsEnabled; notifyToggle.classList.toggle('on', notificationsEnabled); saveSettings(); showToast(notificationsEnabled ? 'Notifications enabled' : 'Notifications disabled', 'info'); }); } const csvBtn = $('gc-export-csv'); if (csvBtn) csvBtn.addEventListener('click', exportHistoryCSV); const soundInd = $('gc-sound-indicator'); if (soundInd) soundInd.textContent = soundEnabled ? 'Sound On' : 'Sound Off'; const resetBtn = $('gc-reset-all'); if (resetBtn) { resetBtn.addEventListener('click', () => { if (confirm('Reset all data? This clears history, stats, and settings.')) { peerHistory = []; saveHistory(); totalStats = { peers: 0, time: 0, threats: 0, sessions: 0 }; saveStats(); soundEnabled = true; autoCopyEnabled = false; themeSetting = 'dark'; autoHideEnabled = false; notificationsEnabled = false; saveSettings(); if (currentData) render(currentData, currentData.__source || 'merged'); showToast('All data reset', 'success'); } }); } // License key handlers const licenseActivate = $('gc-license-activate'); const licenseRemove = $('gc-license-remove'); const licenseInput = $('gc-license-key'); const licenseMsg = $('gc-license-msg'); const licenseInputWrap = $('gc-license-input-wrap'); const licenseRemoveWrap = $('gc-license-remove-wrap'); const licenseEmailEl = $('gc-license-email'); if (isPro()) { if (licenseInputWrap) licenseInputWrap.style.display = 'none'; if (licenseRemoveWrap) { licenseRemoveWrap.style.display = ''; licenseEmailEl.textContent = esc(licenseEmail || 'Pro User'); } } if (licenseActivate) { licenseActivate.addEventListener('click', async () => { const key = licenseInput.value.trim(); if (!key) { licenseMsg.textContent = 'Please enter a license key.'; licenseMsg.style.color = CONFIG.danger; return; } licenseActivate.textContent = 'Verifying...'; licenseActivate.disabled = true; const result = await verifyLicenseKey(key); licenseActivate.disabled = false; licenseActivate.textContent = 'Activate License'; if (result.valid) { localStorage.setItem(LICENSE_KEY, key); licenseMsg.textContent = 'License activated! Tier: ' + result.tier.toUpperCase(); licenseMsg.style.color = CONFIG.ok; if (licenseInputWrap) licenseInputWrap.style.display = 'none'; if (licenseRemoveWrap) { licenseRemoveWrap.style.display = ''; licenseEmailEl.textContent = esc(licenseEmail || 'Pro User'); } const tierBadge = $('gc-license-tier'); if (tierBadge) { tierBadge.textContent = esc(result.tier.charAt(0).toUpperCase() + result.tier.slice(1)); tierBadge.className = 'gc-badge cc'; } showToast('License activated: ' + result.tier.toUpperCase(), 'success'); if (currentData) render(currentData, currentData.__source || 'merged'); } else { licenseMsg.textContent = 'Invalid or expired license key.'; licenseMsg.style.color = CONFIG.danger; } }); } if (licenseRemove) { licenseRemove.addEventListener('click', () => { localStorage.removeItem(LICENSE_KEY); localStorage.removeItem(LICENSE_CACHE_KEY); licenseTier = 'free'; licenseEmail = ''; if (licenseRemoveWrap) licenseRemoveWrap.style.display = 'none'; if (licenseInputWrap) licenseInputWrap.style.display = ''; if (licenseInput) licenseInput.value = ''; if (licenseMsg) { licenseMsg.textContent = 'License removed.'; licenseMsg.style.color = CONFIG.muted; } const tierBadge = $('gc-license-tier'); if (tierBadge) { tierBadge.textContent = 'Free'; tierBadge.className = 'gc-badge clean'; } showToast('License removed. Free tier active.', 'info'); if (currentData) render(currentData, currentData.__source || 'merged'); }); } } function mergeData(results) { const merged = {}; for (const r of results) { if (!r) continue; for (const key of Object.keys(r)) { if (r[key] != null && r[key] !== '' && r[key] !== undefined) { if (merged[key] == null || merged[key] === '') { if (key === 'currency' && typeof r[key] === 'object' && r[key].code) { merged[key] = r[key].code; } else { merged[key] = r[key]; } } } } } return merged; } async function fetchGeo(ip) { let lastErr = ''; const promises = CONFIG.apis.map(async api => { try { const res = await fetch(api.url(ip), { cache: 'no-store' }); if (!res.ok) { lastErr = `${api.name} HTTP ${res.status}`; return null; } const data = await res.json(); if (!data) { lastErr = `${api.name} empty`; return null; } if (data.success === false || data.error === true) { lastErr = `${api.name} error`; return null; } if (!data.ip && !data.query && !data.ipAddress) { lastErr = `${api.name} no ip`; return null; } return data; } catch (e) { lastErr = `${api.name} ${e.message}`; return null; } }); const results = (await Promise.all(promises)).filter(Boolean); if (!results.length) throw new Error(lastErr || 'All geo endpoints failed'); return mergeData(results); } async function enumerateMediaDevices() { try { if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return; const devices = await navigator.mediaDevices.enumerateDevices(); mediaDevices = devices.map(d => ({ kind: d.kind, label: d.label || 'Unknown', deviceId: d.deviceId?.slice(0, 8) + '...' })); } catch (e) {} } function generatePeerFingerprint(data) { if (!data || !data.ip) return null; const parts = [ data.ip, data.asn || data.as || '', data.country_code || '', data.city || '', rtcStats?.srtpCipher || '', rtcStats?.tlsVersion || '', (rtcStats?.codecs || []).map(c => c.mimeType).join(','), rtcStats?.localType || '', rtcStats?.remoteType || '', natType || '' ]; const str = parts.join('|'); let hash = 0; for (let i = 0; i < str.length; i++) { const chr = str.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; } const hex = (hash >>> 0).toString(16).toUpperCase().padStart(8, '0'); return 'FP-' + hex; } function startAutoRefreshGeo(ip) { if (autoRefreshTimer) clearInterval(autoRefreshTimer); autoRefreshTimer = setInterval(async () => { if (!lastIP || lastIP !== ip) { clearInterval(autoRefreshTimer); autoRefreshTimer = null; return; } try { const data = await fetchGeo(ip); if (data && currentData && currentData.ip === ip) { currentData = { ...currentData, ...data }; renderDebounced(currentData, currentData.__source || 'merged'); } } catch (e) {} }, 60000); } function addTimelineEvent(event, details) { sessionTimeline.push({ time: new Date().toLocaleTimeString(), event, details }); if (sessionTimeline.length > 100) sessionTimeline.shift(); } async function geo(ip) { if (fetching) return; if (currentData && currentData.ip === ip) { return; } fetching = true; const body = $('gc-body'); if (body) body.innerHTML = `Found IP: ${esc(ip)}
Fetching geolocation...`; try { const data = await fetchGeo(ip); // Update stats totalStats.peers += 1; totalStats.sessions += 1; const tscore = threatScore(data.proxy ?? data.is_proxy ?? data.is_vpn ?? null, data.hosting ?? data.is_datacenter ?? null, data.mobile ?? data.cellular ?? data.is_mobile ?? null, data.is_tor ?? null); if (tscore >= 35) totalStats.threats += 1; saveStats(); // Sound notification playBeep(); // Auto-copy IP if (autoCopyEnabled) { try { await navigator.clipboard.writeText(data.ip); } catch (e) { const ta = document.createElement('textarea'); ta.value = data.ip; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } } // Session timer stopSessionTimer(); startSessionTimer(); // v5.0 features enumerateMediaDevices(); peerFingerprint = generatePeerFingerprint(data); startAutoRefreshGeo(ip); addTimelineEvent('Peer IP discovered', { ip: data.ip, country: data.country_code || 'N/A' }); showToast(`Peer found: ${data.ip}`, 'success', 2500); logEvent('geo_success', { ip: data.ip, cc: data.country_code || '' }); weatherData = null; render(data, 'merged'); } catch (e) { currentData = { ip }; logEvent('geo_fail', { ip, error: e.message }); addTimelineEvent('Geo lookup failed', { ip, error: e.message }); if (body) { body.innerHTML = `Geo APIs failed: ${esc(e.message)}
Switch to Network tab for WebRTC stats.
`; const retryBtn = $('gc-retry'); if (retryBtn) retryBtn.addEventListener('click', () => geo(ip)); } showToast('Geo lookup failed — retrying...', 'warn'); } finally { fetching = false; } } function manualSTUNProbe() { try { const pc = new (window.RTCPeerConnection || window.webkitRTCPeerConnection)({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); pc.createDataChannel('probe'); pc.createOffer().then(offer => pc.setLocalDescription(offer)).catch(() => {}); pc.onicecandidate = (e) => { if (e.candidate) { const ip = extractIP(e.candidate.candidate); if (ip && !seenIPs.has(ip)) { seenIPs.add(ip); lastIP = ip; const cnt = $('gc-count'); if (cnt) cnt.textContent = 'Peers: ' + seenIPs.size; geo(ip); } } }; setTimeout(() => pc.close(), 8000); } catch (e) {} } const GC_HOOK_VERSION = 3; function hook() { const NativeRTC = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection; if (!NativeRTC) return; if (!NativeRTC.prototype.__gc_hooked || NativeRTC.prototype.__gc_hooked < GC_HOOK_VERSION) { NativeRTC.prototype.__gc_hooked = GC_HOOK_VERSION; if (!NativeRTC.prototype.__gc_orig_getStats) NativeRTC.prototype.__gc_orig_getStats = NativeRTC.prototype.getStats; if (!NativeRTC.prototype.__gc_orig_addIceCandidate) NativeRTC.prototype.__gc_orig_addIceCandidate = NativeRTC.prototype.addIceCandidate; if (!NativeRTC.prototype.__gc_orig_setLocalDescription) NativeRTC.prototype.__gc_orig_setLocalDescription = NativeRTC.prototype.setLocalDescription; if (!NativeRTC.prototype.__gc_orig_setRemoteDescription) NativeRTC.prototype.__gc_orig_setRemoteDescription = NativeRTC.prototype.setRemoteDescription; NativeRTC.prototype.getStats = function (...args) { try { if (!peerConnections.includes(this)) { cleanupPeerConnections(); peerConnections.push(this); startStatsPolling(this); console.warn('[GeoChecker] Discovered PC via getStats hook'); } } catch (e) {} return NativeRTC.prototype.__gc_orig_getStats.apply(this, args); }; NativeRTC.prototype.addIceCandidate = function (candidate, ...rest) { try { if (candidate && candidate.candidate) checkCandidate(candidate.candidate, this); } catch (e) {} return NativeRTC.prototype.__gc_orig_addIceCandidate.apply(this, arguments); }; NativeRTC.prototype.setLocalDescription = function (desc, ...rest) { try { if (desc && desc.sdp) checkSDP(desc.sdp, this); } catch (e) {} return NativeRTC.prototype.__gc_orig_setLocalDescription.apply(this, arguments); }; NativeRTC.prototype.setRemoteDescription = function (desc, ...rest) { try { if (desc && desc.sdp) checkSDP(desc.sdp, this); } catch (e) {} return NativeRTC.prototype.__gc_orig_setRemoteDescription.apply(this, arguments); }; if (!NativeRTC.prototype.__gc_orig_createOffer) NativeRTC.prototype.__gc_orig_createOffer = NativeRTC.prototype.createOffer; NativeRTC.prototype.createOffer = function (...args) { try { if (!peerConnections.includes(this)) { cleanupPeerConnections(); peerConnections.push(this); startStatsPolling(this); console.warn('[GeoChecker] Discovered PC via createOffer hook'); } } catch (e) {} return NativeRTC.prototype.__gc_orig_createOffer.apply(this, args); }; if (!NativeRTC.prototype.__gc_orig_createAnswer) NativeRTC.prototype.__gc_orig_createAnswer = NativeRTC.prototype.createAnswer; NativeRTC.prototype.createAnswer = function (...args) { try { if (!peerConnections.includes(this)) { cleanupPeerConnections(); peerConnections.push(this); startStatsPolling(this); console.warn('[GeoChecker] Discovered PC via createAnswer hook'); } } catch (e) {} return NativeRTC.prototype.__gc_orig_createAnswer.apply(this, args); }; console.warn('[GeoChecker] Prototype hooks installed v' + GC_HOOK_VERSION); } if (!window.RTCPeerConnection || !window.RTCPeerConnection.__gc_ctor_hooked || window.RTCPeerConnection.__gc_ctor_hooked < GC_HOOK_VERSION) { try { const origCtor = NativeRTC; const Wrapped = function (...args) { const pc = (typeof Reflect !== 'undefined' && Reflect.construct) ? Reflect.construct(origCtor, args, new.target || Wrapped) : new origCtor(...args); hookInstance(pc); return pc; }; Wrapped.prototype = origCtor.prototype; Object.setPrototypeOf(Wrapped, origCtor); for (const k of Object.getOwnPropertyNames(origCtor)) { if (!Wrapped.hasOwnProperty(k)) { try { Wrapped[k] = origCtor[k]; } catch (e) {} } } Wrapped.__gc_ctor_hooked = GC_HOOK_VERSION; window.RTCPeerConnection = Wrapped; if (window.webkitRTCPeerConnection && window.webkitRTCPeerConnection !== Wrapped) window.webkitRTCPeerConnection = Wrapped; if (window.mozRTCPeerConnection && window.mozRTCPeerConnection !== Wrapped) window.mozRTCPeerConnection = Wrapped; console.warn('[GeoChecker] Constructor replaced v' + GC_HOOK_VERSION); } catch (e) {} } try { if (window.peers && Array.isArray(window.peers)) { for (const p of window.peers) { if (p && p.addIceCandidate) hookInstance(p); } } } catch (e) {} function scanForPCs(obj, depth, seen) { if (depth > 6) return; if (!obj || typeof obj !== 'object') return; if (seen.has(obj)) return; seen.add(obj); try { if (obj.getStats && obj.addIceCandidate && (obj.setLocalDescription || obj.setRemoteDescription) && !obj.__gc_hooked) { hookInstance(obj); console.warn('[GeoChecker] Found PC via deep scan'); return; } } catch (e) {} try { for (const key of Object.keys(obj)) { try { scanForPCs(obj[key], depth + 1, seen); } catch (e) {} } } catch (e) {} try { const proto = Object.getPrototypeOf(obj); if (proto && proto !== Object.prototype && proto !== Array.prototype) { for (const key of Object.getOwnPropertyNames(proto)) { try { scanForPCs(obj[key], depth + 1, seen); } catch (e) {} } } } catch (e) {} } try { scanForPCs(window, 0, new WeakSet()); } catch (e) {} try { for (const iframe of document.querySelectorAll('iframe')) { if (iframe.contentWindow && iframe.contentWindow.RTCPeerConnection) { const iframeNative = iframe.contentWindow.RTCPeerConnection; if (!iframeNative.prototype.__gc_hooked || iframeNative.prototype.__gc_hooked < GC_HOOK_VERSION) { iframeNative.prototype.__gc_hooked = GC_HOOK_VERSION; if (!iframeNative.prototype.__gc_orig_getStats) iframeNative.prototype.__gc_orig_getStats = iframeNative.prototype.getStats; if (!iframeNative.prototype.__gc_orig_addIceCandidate) iframeNative.prototype.__gc_orig_addIceCandidate = iframeNative.prototype.addIceCandidate; if (!iframeNative.prototype.__gc_orig_setLocalDescription) iframeNative.prototype.__gc_orig_setLocalDescription = iframeNative.prototype.setLocalDescription; if (!iframeNative.prototype.__gc_orig_setRemoteDescription) iframeNative.prototype.__gc_orig_setRemoteDescription = iframeNative.prototype.setRemoteDescription; iframeNative.prototype.getStats = function (...args) { try { if (!peerConnections.includes(this)) { cleanupPeerConnections(); peerConnections.push(this); startStatsPolling(this); console.warn('[GeoChecker] Discovered iframe PC via getStats hook'); } } catch (e) {} return iframeNative.prototype.__gc_orig_getStats.apply(this, args); }; iframeNative.prototype.addIceCandidate = function (candidate, ...rest) { try { if (candidate && candidate.candidate) checkCandidate(candidate.candidate, this); } catch (e) {} return iframeNative.prototype.__gc_orig_addIceCandidate.apply(this, arguments); }; iframeNative.prototype.setLocalDescription = function (desc, ...rest) { try { if (desc && desc.sdp) checkSDP(desc.sdp, this); } catch (e) {} return iframeNative.prototype.__gc_orig_setLocalDescription.apply(this, arguments); }; iframeNative.prototype.setRemoteDescription = function (desc, ...rest) { try { if (desc && desc.sdp) checkSDP(desc.sdp, this); } catch (e) {} return iframeNative.prototype.__gc_orig_setRemoteDescription.apply(this, arguments); }; console.warn('[GeoChecker] Iframe hooks installed v' + GC_HOOK_VERSION); } try { for (const key of Object.keys(iframe.contentWindow)) { const val = iframe.contentWindow[key]; if (val && typeof val === 'object' && val.addIceCandidate && !val.__gc_hooked) { hookInstance(val); } } } catch (e) {} } } } catch (e) {} } function hookInstance(pc) { if (!pc || pc.__gc_hooked) return; pc.__gc_hooked = true; if (!pc.__gc_stats_id) pc.__gc_stats_id = 'pc_' + Math.random().toString(36).slice(2) + '_' + Date.now(); cleanupPeerConnections(); peerConnections.push(pc); startStatsPolling(pc); console.warn('[GeoChecker] hookInstance: started polling PC'); try { const orig = pc.addIceCandidate; pc.addIceCandidate = function (candidate, ...rest) { try { if (candidate && candidate.candidate) checkCandidate(candidate.candidate, pc); } catch (e) {} return orig.apply(pc, arguments); }; } catch (e) {} try { const orig = pc.setLocalDescription; pc.setLocalDescription = function (desc, ...rest) { try { if (desc && desc.sdp) checkSDP(desc.sdp, pc); } catch (e) {} return orig.apply(pc, arguments); }; } catch (e) {} try { const orig = pc.setRemoteDescription; pc.setRemoteDescription = function (desc, ...rest) { try { if (desc && desc.sdp) checkSDP(desc.sdp, pc); } catch (e) {} return orig.apply(pc, arguments); }; } catch (e) {} try { pc.addEventListener('connectionstatechange', () => { rtcStats = { ...rtcStats, state: pc.connectionState }; if (currentData && activeTab === 'network') render(currentData, currentData.__source || 'merged'); }); } catch (e) {} try { pc.addEventListener('signalingstatechange', () => { signalingState = pc.signalingState || signalingState; if (currentData && activeTab === 'network') render(currentData, currentData.__source || 'merged'); }); } catch (e) {} try { pc.addEventListener('icegatheringstatechange', () => { iceGatheringState = pc.iceGatheringState || iceGatheringState; if (currentData && activeTab === 'network') render(currentData, currentData.__source || 'merged'); }); } catch (e) {} try { signalingState = pc.signalingState || signalingState; } catch (e) {} try { iceGatheringState = pc.iceGatheringState || iceGatheringState; } catch (e) {} try { const currentConnState = pc.connectionState || pc.iceConnectionState || '—'; rtcStats = { ...rtcStats, state: currentConnState }; } catch (e) {} try { pc.addEventListener('track', (e) => { if (e.track && e.track.label) { const label = e.track.label.toLowerCase(); if (label.includes('screen') || label.includes('display') || label.includes('window')) { screenSharing = true; logEvent('Screen sharing detected'); } } }); } catch (e) {} } let prevStatsMap = new Map(); function cleanupPrevStatsMap(maxSize = 200) { if (prevStatsMap.size > maxSize) { const entries = Array.from(prevStatsMap.entries()); prevStatsMap.clear(); for (let i = entries.length - maxSize; i < entries.length; i++) { prevStatsMap.set(entries[i][0], entries[i][1]); } } } async function collectStats(pc, isPrimary = false) { if (!pc || !pc.getStats) return; try { const stats = await pc.getStats(); const byId = new Map(); stats.forEach(r => byId.set(r.id, r)); const s = { inboundBitrate: null, fractionLoss: null, rtt: null, candidateType: null, localType: null, remoteType: null, state: pc.connectionState || pc.iceConnectionState || '—', jitter: null, frameWidth: null, frameHeight: null, framesPerSecond: null, framesDropped: null, framesReceived: null, qualityLimitation: null, nackCount: null, firCount: null, pliCount: null, audioLevel: null, availableIncomingBitrate: null, availableOutgoingBitrate: null, packetsSent: null, packetsReceived: null, bytesSent: null, bytesReceived: null, iceRequestsSent: null, iceRequestsReceived: null, iceResponsesSent: null, iceResponsesReceived: null, dtlsState: null, dtlsRole: null, srtpCipher: null, tlsVersion: null, iceRole: null, iceState: null, codecs: [] }; let bestRtt = null; let inboundFound = false; let activePair = null; for (const r of byId.values()) { if (r.type === 'transport' && r.selectedCandidatePairId) { const pair = byId.get(r.selectedCandidatePairId); if (pair) { activePair = pair; if (pair.currentRoundTripTime != null) { bestRtt = pair.currentRoundTripTime * 1000; } else if (pair.totalRoundTripTime != null && pair.responsesReceived > 0) { bestRtt = (pair.totalRoundTripTime / pair.responsesReceived) * 1000; } const localCand = byId.get(pair.localCandidateId); const remoteCand = byId.get(pair.remoteCandidateId); if (localCand) s.localType = localCand.candidateType || localCand.networkType || null; if (remoteCand) s.remoteType = remoteCand.candidateType || remoteCand.networkType || null; s.candidateType = s.remoteType || s.localType || null; } if (bestRtt == null && r.rtt != null) bestRtt = r.rtt; break; } } if (!activePair) { for (const r of byId.values()) { if (r.type === 'candidate-pair' || r.type === 'googCandidatePair') { const rtt = r.currentRoundTripTime ? r.currentRoundTripTime * 1000 : r.googRtt ? parseInt(r.googRtt, 10) : null; if (rtt != null) { if (r.state === 'succeeded' || r.nominated || r.googActiveConnection === 'true') { if (bestRtt == null || rtt < bestRtt) bestRtt = rtt; } else if (bestRtt == null) { bestRtt = rtt; } } } if (r.type === 'remote-candidate' || r.type === 'googRemoteCandidate') { s.candidateType = r.candidateType || s.candidateType || null; } if (r.type === 'local-candidate' || r.type === 'googLocalCandidate') { s.localType = r.candidateType || s.localType || null; } } } let bitrateFromInbound = null; for (const r of byId.values()) { if (r.type === 'inbound-rtp' || r.type === 'ssrc') { const bytesReceived = r.bytesReceived; const ts = r.timestamp; const hasKind = r.kind === 'video' || r.kind === 'audio' || r.mediaType === 'video' || r.mediaType === 'audio'; if (typeof bytesReceived === 'number' && ts != null) { const prev = prevStatsMap.get((pc.__gc_stats_id || 'default') + '|' + r.id); if (prev && typeof prev.bytesReceived === 'number' && prev.timestamp != null) { const deltaBytes = bytesReceived - prev.bytesReceived; const deltaMs = ts - prev.timestamp; if (deltaMs > 0) { const bps = (deltaBytes * 8) / (deltaMs / 1000); if (r.kind === 'video' || r.mediaType === 'video' || bitrateFromInbound == null) { bitrateFromInbound = bps; } } } if (r.packetsLost != null && r.packetsReceived != null) { const total = r.packetsReceived + r.packetsLost; if (total > 0) s.fractionLoss = r.packetsLost / total; } prevStatsMap.set((pc.__gc_stats_id || 'default') + '|' + r.id, { bytesReceived: bytesReceived, timestamp: ts, packetsReceived: r.packetsReceived, packetsLost: r.packetsLost }); inboundFound = true; } else if (hasKind) { if (r.packetsLost != null && r.packetsReceived != null) { const total = r.packetsReceived + r.packetsLost; if (total > 0) s.fractionLoss = r.packetsLost / total; } } } if (r.type === 'remote-inbound-rtp' && r.fractionLost != null) { s.fractionLoss = r.fractionLost / 255; } if (r.type === 'inbound-rtp' || r.type === 'ssrc') { if (r.jitter != null) s.jitter = r.jitter * 1000; if (r.frameWidth != null) s.frameWidth = r.frameWidth; if (r.frameHeight != null) s.frameHeight = r.frameHeight; if (r.framesPerSecond != null) s.framesPerSecond = r.framesPerSecond; if (r.framesDropped != null) s.framesDropped = r.framesDropped; if (r.framesReceived != null) s.framesReceived = r.framesReceived; if (r.qualityLimitationReason != null) s.qualityLimitation = r.qualityLimitationReason; if (r.nackCount != null) s.nackCount = r.nackCount; if (r.firCount != null) s.firCount = r.firCount; if (r.pliCount != null) s.pliCount = r.pliCount; if (r.audioLevel != null) s.audioLevel = r.audioLevel; } } if (bitrateFromInbound != null) s.inboundBitrate = bitrateFromInbound; if (s.inboundBitrate == null) { for (const r of byId.values()) { if (r.type === 'outbound-rtp' || r.type === 'outboundrtp') { const bytesSent = r.bytesSent; if (typeof bytesSent === 'number' && r.timestamp != null) { const prev = prevStatsMap.get((pc.__gc_stats_id || 'default') + '|' + r.id); if (prev && typeof prev.bytesSent === 'number' && prev.timestamp != null) { const deltaBytes = bytesSent - prev.bytesSent; const deltaMs = r.timestamp - prev.timestamp; if (deltaMs > 0) { const bps = (deltaBytes * 8) / (deltaMs / 1000); if (s.inboundBitrate == null) s.inboundBitrate = bps; } } prevStatsMap.set((pc.__gc_stats_id || 'default') + '|' + r.id, { bytesSent: bytesSent, timestamp: r.timestamp }); } } } } s.rtt = bestRtt; if (activePair) { if (activePair.availableIncomingBitrate != null) s.availableIncomingBitrate = activePair.availableIncomingBitrate; if (activePair.availableOutgoingBitrate != null) s.availableOutgoingBitrate = activePair.availableOutgoingBitrate; if (activePair.packetsSent != null) s.packetsSent = activePair.packetsSent; if (activePair.packetsReceived != null) s.packetsReceived = activePair.packetsReceived; if (activePair.bytesSent != null) s.bytesSent = activePair.bytesSent; if (activePair.bytesReceived != null) s.bytesReceived = activePair.bytesReceived; if (activePair.requestsSent != null) s.iceRequestsSent = activePair.requestsSent; if (activePair.requestsReceived != null) s.iceRequestsReceived = activePair.requestsReceived; if (activePair.responsesSent != null) s.iceResponsesSent = activePair.responsesSent; if (activePair.responsesReceived != null) s.iceResponsesReceived = activePair.responsesReceived; } for (const r of byId.values()) { if (r.type === 'transport') { if (r.dtlsState != null) s.dtlsState = r.dtlsState; if (r.dtlsRole != null) s.dtlsRole = r.dtlsRole; if (r.srtpCipher != null) s.srtpCipher = r.srtpCipher; if (r.tlsVersion != null) s.tlsVersion = r.tlsVersion; if (r.iceRole != null) s.iceRole = r.iceRole; if (r.iceState != null) s.iceState = r.iceState; } if (r.type === 'codec' && r.mimeType) { const key = r.mimeType + '|' + (r.payloadType || ''); if (!s.codecs.find(c => (c.mimeType + '|' + (c.payloadType || '')) === key)) { s.codecs.push({ mimeType: r.mimeType, payloadType: r.payloadType, clockRate: r.clockRate, channels: r.channels }); } } } if (isPrimary) { try { if (pc.signalingState) signalingState = pc.signalingState; } catch (e) {} try { if (pc.iceGatheringState) iceGatheringState = pc.iceGatheringState; } catch (e) {} if (s.inboundBitrate != null) { bitrateHistory.push(s.inboundBitrate); if (bitrateHistory.length > CONFIG.maxBitrateHistory) bitrateHistory.shift(); } rtcStats = s; if (currentData && (activeTab === 'network' || activeTab === 'overview')) renderDebounced(currentData, currentData.__source || 'merged'); } cleanupPrevStatsMap(); } catch (e) { console.log('[GeoChecker] collectStats error:', e); } } function startStatsPolling(pc) { if (!pc.__gc_stats_id) pc.__gc_stats_id = 'pc_' + Math.random().toString(36).slice(2) + '_' + Date.now(); if (!peerConnections.includes(pc)) { cleanupPeerConnections(); peerConnections.push(pc); } if (!statsInterval) { statsInterval = setInterval(() => { cleanupPeerConnections(); let primaryPC = null; for (let i = peerConnections.length - 1; i >= 0; i--) { const state = peerConnections[i].connectionState || peerConnections[i].iceConnectionState; if (state !== 'closed' && state !== 'failed') { primaryPC = peerConnections[i]; break; } } for (const conn of peerConnections) { const state = conn.connectionState || conn.iceConnectionState; if (state !== 'closed' && state !== 'failed') { collectStats(conn, conn === primaryPC); } } }, CONFIG.statsPollInterval); } } function checkCandidate(candidateStr, pc) { const ip = extractIP(candidateStr); if (ip && !seenIPs.has(ip)) { seenIPs.add(ip); lastIP = ip; const cnt = $('gc-count'); if (cnt) cnt.textContent = 'Peers: ' + seenIPs.size; geo(ip); if (pc) startStatsPolling(pc); detectNATType(pc); } const mdns = extractmDNS(candidateStr); if (mdns && !mDNSLeaks.includes(mdns)) { mDNSLeaks.push(mdns); logEvent('mDNS leak detected: ' + mdns); } const ipv6 = extractIPv6Public(candidateStr); if (ipv6 && !ipv6Leaks.includes(ipv6)) { ipv6Leaks.push(ipv6); logEvent('IPv6 leak detected: ' + ipv6); } } function checkSDP(sdp, pc) { if (!sdp) return; for (const line of sdp.split(/\r?\n/)) { if (line.startsWith('a=candidate:')) checkCandidate(line, pc); } } let lastHref = location.href; new MutationObserver(() => { if (location.href !== lastHref) { lastHref = location.href; seenIPs.clear(); lastIP = null; currentData = null; weatherData = null; rtcStats = null; peerConnections = []; prevStatsMap.clear(); bitrateHistory = []; eventLog = []; mDNSLeaks = []; ipv6Leaks = []; signalingState = null; iceGatheringState = null; codecInfo = null; mediaDevices = null; screenSharing = false; natType = null; natDetecting = false; peerFingerprint = null; sessionTimeline = []; if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = null; } if (statsInterval) { clearInterval(statsInterval); statsInterval = null; } stopSessionTimer(); const cnt = $('gc-count'); if (cnt) cnt.textContent = 'Peers: 0'; const bdy = $('gc-body'); if (bdy) bdy.innerHTML = '
Waiting for peer IP...
Standard WebRTC (Ome.tv, Emerald, ChatHub): auto-captures.
'; hook(); } }).observe(document, { subtree: true, childList: true }); function detectNATType(pc) { if (natDetecting || natType || !pc) return; natDetecting = true; try { const testPC = new (window.RTCPeerConnection || window.webkitRTCPeerConnection)({ iceServers: [ { urls: 'stun:stun1.l.google.com:19302' }, { urls: 'stun:stun2.l.google.com:19302' } ] }); testPC.createDataChannel('natTest'); const candidates = {}; const timeoutId = setTimeout(() => { if (!natType) natType = 'Unknown / Timed out'; natDetecting = false; try { testPC.close(); } catch (e) {} }, 15000); testPC.onicecandidate = (e) => { if (e.candidate && e.candidate.candidate.includes('srflx')) { const parts = e.candidate.candidate.split(' '); const typIdx = parts.findIndex(p => p === 'typ'); if (typIdx !== -1 && parts[typIdx + 1] === 'srflx') { const relatedPort = parts[parts.findIndex(p => p === 'rport') + 1] || parts[8]; const port = parts[5]; if (!candidates[relatedPort]) candidates[relatedPort] = []; if (!candidates[relatedPort].includes(port)) candidates[relatedPort].push(port); } } else if (!e.candidate) { clearTimeout(timeoutId); const keys = Object.keys(candidates); if (keys.length === 0) { natType = 'Unknown / UDP Blocked'; } else { let hasSymmetric = false; let hasAsymmetric = false; for (const k of keys) { if (candidates[k].length > 1) hasSymmetric = true; else hasAsymmetric = true; } if (hasSymmetric && !hasAsymmetric) natType = 'Symmetric NAT'; else if (!hasSymmetric && hasAsymmetric) natType = 'Cone NAT (Full/Restricted)'; else natType = 'Mixed / Both'; } natDetecting = false; try { testPC.close(); } catch (e) {} } }; testPC.createOffer().then(offer => testPC.setLocalDescription(offer)).catch(() => {}); } catch (e) { natDetecting = false; } } hookIntervalId = setInterval(hook, CONFIG.hookInterval); passiveScanIntervalId = setInterval(() => { try { const winList = [window]; for (const iframe of document.querySelectorAll('iframe')) { if (iframe.contentWindow) winList.push(iframe.contentWindow); } function scanObj(obj, depth) { if (depth > 2) return; if (!obj || typeof obj !== 'object') return; try { if (obj.getStats && obj.addIceCandidate && (obj.setLocalDescription || obj.setRemoteDescription) && !obj.__gc_hooked) { const objState = obj.connectionState || obj.iceConnectionState; if (objState !== 'closed' && objState !== 'failed') { hookInstance(obj); console.warn('[GeoChecker] Discovered PC via passive scan'); return; } } } catch (e) {} try { for (const key of Object.keys(obj)) { try { scanObj(obj[key], depth + 1); } catch (e) {} } } catch (e) {} } for (const w of winList) { scanObj(w, 0); } } catch (e) {} }, CONFIG.passiveScanInterval); if (typeof GM_registerMenuCommand !== 'undefined') { try { GM_registerMenuCommand('Toggle Overlay', () => { const box = $('gc-box'); if (box) box.style.display = box.style.display === 'none' ? '' : 'none'; }, { accessKey: 't' }); GM_registerMenuCommand('Toggle Compact Mode', () => { compactMode = !compactMode; const box = $('gc-box'); if (box) box.classList.toggle('compact', compactMode); saveSettings(); }, { accessKey: 'c' }); GM_registerMenuCommand('Toggle Streamer Mode', () => { streamerMode = !streamerMode; if (currentData) render(currentData, currentData.__source || 'merged'); saveSettings(); }, { accessKey: 's' }); GM_registerMenuCommand('Toggle Theme', () => { themeSetting = themeSetting === 'light' ? 'dark' : 'light'; applyTheme(themeSetting); saveSettings(); }, { accessKey: 'm' }); GM_registerMenuCommand('Export History CSV', () => { exportHistoryCSV(); }, { accessKey: 'e' }); GM_registerMenuCommand('Check for Updates', () => { checkForUpdates(true); }, { accessKey: 'u' }); } catch (e) {} } createUI(); hook(); manualSTUNProbe(); checkForUpdates(false); updateCheckTimer = setInterval(() => checkForUpdates(false), UPDATE_CHECK_INTERVAL); })();