// ==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) => ``;
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) => ``;
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) => '
' + (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
' +
'
' +
'
' + 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',
'' + esc(licenseTier.charAt(0).toUpperCase() + licenseTier.slice(1)) + ' ' +
'' +
'Licensed to:
'
)}
${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);
})();