mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-05-30 16:10:06 +02:00
add dual-stack check
This commit is contained in:
@@ -8,9 +8,10 @@ const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
|||||||
|
|
||||||
// ASN org-name patterns that strongly suggest a VPN service
|
// ASN org-name patterns that strongly suggest a VPN service
|
||||||
const VPN_PATTERNS = [
|
const VPN_PATTERNS = [
|
||||||
/\bvpn\b/i, /nordvpn/i, /expressvpn/i, /mullvad/i, /surfshark/i,
|
/\bvpn\b/i, /nordvpn/i, /expressvpn/i, /mullvad/i, /31173\s+services/i,
|
||||||
/protonvpn/i, /cyberghost/i, /ipvanish/i, /purevpn/i, /tunnelbear/i,
|
/owl\s+limited/i, /surfshark/i, /protonvpn/i, /cyberghost/i, /ipvanish/i,
|
||||||
/private.?internet.?access/i, /\bpia\b/i, /hide\.?my\.?ip/i, /hidemyass/i,
|
/purevpn/i, /tunnelbear/i, /private.?internet.?access/i, /\bpia\b/i,
|
||||||
|
/hide\.?my\.?ip/i, /hidemyass/i, /windscribe/i, /perfect.?privacy/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
// ASN org-name patterns that suggest cloud/datacenter/hosting (but not necessarily VPN)
|
// ASN org-name patterns that suggest cloud/datacenter/hosting (but not necessarily VPN)
|
||||||
@@ -58,7 +59,10 @@ router.get('/:ip', async (req, res, next) => {
|
|||||||
|
|
||||||
// Tor exit-node check (async DNS, ~100–300 ms)
|
// Tor exit-node check (async DNS, ~100–300 ms)
|
||||||
const tor = await isTorExit(ip);
|
const tor = await isTorExit(ip);
|
||||||
if (tor) flags.push({ id: 'tor', label: 'Tor Exit Node', color: 'red' });
|
if (tor) {
|
||||||
|
flags.length = 0; // Tor supersedes all network-type flags — showing "Residential" alongside Tor is misleading
|
||||||
|
flags.push({ id: 'tor', label: 'Tor Exit Node', color: 'red' });
|
||||||
|
}
|
||||||
|
|
||||||
logger.info({ ip, flags: flags.map(f => f.id) }, 'Privacy check complete');
|
logger.info({ ip, flags: flags.map(f => f.id) }, 'Privacy check complete');
|
||||||
res.json({ success: true, ip, flags });
|
res.json({ success: true, ip, flags });
|
||||||
|
|||||||
+44
-18
@@ -31,9 +31,21 @@ export const page = {
|
|||||||
<span id="ipv6-address" class="font-mono text-blue-300 text-sm break-all"></span>
|
<span id="ipv6-address" class="font-mono text-blue-300 text-sm break-all"></span>
|
||||||
<button id="copy-ipv6-btn" class="copy-btn hidden">copy</button>
|
<button id="copy-ipv6-btn" class="copy-btn hidden">copy</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Privacy / risk flags -->
|
<!-- Privacy / risk flags — dual-stack: always show both rows -->
|
||||||
<div id="privacy-flags" class="hidden mt-3 flex flex-wrap gap-1.5"></div>
|
<div id="privacy-checks" class="mt-3 space-y-1.5">
|
||||||
<div id="privacy-loader" class="loader mt-3" style="width:16px;height:16px;border-width:2px"></div>
|
<div class="flex items-center gap-2 min-h-[22px]">
|
||||||
|
<span class="text-xs font-bold text-gray-500 uppercase tracking-wider w-10 shrink-0">IPv4</span>
|
||||||
|
<div id="privacy-flags-v4" class="flex flex-wrap gap-1.5 items-center">
|
||||||
|
<div id="privacy-loader-v4" class="loader" style="width:12px;height:12px;border-width:2px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 min-h-[22px]">
|
||||||
|
<span class="text-xs font-bold text-blue-400/60 uppercase tracking-wider w-10 shrink-0">IPv6</span>
|
||||||
|
<div id="privacy-flags-v6" class="flex flex-wrap gap-1.5 items-center">
|
||||||
|
<span class="text-xs text-gray-600 italic">detecting…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="glass-card rounded-lg p-5 space-y-4">
|
<div class="glass-card rounded-lg p-5 space-y-4">
|
||||||
@@ -476,17 +488,26 @@ export const page = {
|
|||||||
async function checkIPv6() {
|
async function checkIPv6() {
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
const timer = setTimeout(() => ctrl.abort(), 4000);
|
const timer = setTimeout(() => ctrl.abort(), 4000);
|
||||||
|
const v6Flags = document.getElementById('privacy-flags-v6');
|
||||||
try {
|
try {
|
||||||
const r = await fetch('https://ipv6.icanhazip.com', { signal: ctrl.signal });
|
const r = await fetch('https://ipv6.icanhazip.com', { signal: ctrl.signal });
|
||||||
const ip6 = r.ok ? (await r.text()).trim() : null;
|
const ip6 = r.ok ? (await r.text()).trim() : null;
|
||||||
if (ip6 && ip6.includes(':')) {
|
if (ip6 && ip6.includes(':') && ip6 !== currentIp) {
|
||||||
|
// Separate IPv6 address — show it and run its own privacy check
|
||||||
document.getElementById('ipv6-address').textContent = ip6;
|
document.getElementById('ipv6-address').textContent = ip6;
|
||||||
const row = document.getElementById('ipv6-row');
|
document.getElementById('ipv6-row').classList.remove('hidden');
|
||||||
row.classList.remove('hidden');
|
|
||||||
copyIpv6Btn.classList.remove('hidden');
|
copyIpv6Btn.classList.remove('hidden');
|
||||||
|
if (v6Flags) v6Flags.innerHTML = '<div id="privacy-loader-v6" class="loader" style="width:12px;height:12px;border-width:2px"></div>';
|
||||||
|
fetchPrivacyFlags(ip6, 6);
|
||||||
|
} else {
|
||||||
|
// No IPv6, timed out, or same exit as IPv4 (VPN tunnels both)
|
||||||
|
if (v6Flags) v6Flags.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (v6Flags) v6Flags.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
} catch { /* no IPv6 or timeout — show nothing */ }
|
|
||||||
finally { clearTimeout(timer); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Privacy / risk flags ──────────────────────────────────────
|
// ── Privacy / risk flags ──────────────────────────────────────
|
||||||
@@ -497,19 +518,25 @@ export const page = {
|
|||||||
red: 'bg-red-900/40 text-red-300 border border-red-700/50',
|
red: 'bg-red-900/40 text-red-300 border border-red-700/50',
|
||||||
};
|
};
|
||||||
|
|
||||||
async function fetchPrivacyFlags(ip) {
|
async function fetchPrivacyFlags(ip, version = 4) {
|
||||||
const loader = document.getElementById('privacy-loader');
|
const container = document.getElementById(`privacy-flags-v${version}`);
|
||||||
const container = document.getElementById('privacy-flags');
|
const loader = document.getElementById(`privacy-loader-v${version}`);
|
||||||
|
if (!container) return;
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`);
|
const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (!data.flags?.length) return;
|
loader?.remove();
|
||||||
|
if (!data.flags?.length) {
|
||||||
|
container.innerHTML = '<span class="text-xs text-gray-600">—</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
container.innerHTML = data.flags.map(f =>
|
container.innerHTML = data.flags.map(f =>
|
||||||
`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold ${FLAG_COLORS[f.color] || FLAG_COLORS.green}">${f.label}</span>`
|
`<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold ${FLAG_COLORS[f.color] || FLAG_COLORS.green}">${f.label}</span>`
|
||||||
).join('');
|
).join('');
|
||||||
container.classList.remove('hidden');
|
} catch {
|
||||||
} catch { /* silent fail */ }
|
loader?.remove();
|
||||||
finally { loader?.classList.add('hidden'); }
|
container.innerHTML = '<span class="text-xs text-gray-600">N/A</span>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Browser fingerprint ───────────────────────────────────────
|
// ── Browser fingerprint ───────────────────────────────────────
|
||||||
@@ -572,7 +599,7 @@ export const page = {
|
|||||||
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
|
if (!r.ok) throw new Error(`${r.statusText} (${r.status})`);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
currentIp = data.ip;
|
currentIp = data.ip;
|
||||||
fetchPrivacyFlags(data.ip);
|
fetchPrivacyFlags(data.ip, 4);
|
||||||
|
|
||||||
ipAddressSpan.textContent = data.ip;
|
ipAddressSpan.textContent = data.ip;
|
||||||
ipAddressLink.classList.remove('hidden');
|
ipAddressLink.classList.remove('hidden');
|
||||||
@@ -941,8 +968,7 @@ export const page = {
|
|||||||
|
|
||||||
// ── Bootstrap ────────────────────────────────────────────────
|
// ── Bootstrap ────────────────────────────────────────────────
|
||||||
populateBrowserFingerprint();
|
populateBrowserFingerprint();
|
||||||
checkIPv6();
|
fetchIpInfo().then(() => checkIPv6());
|
||||||
fetchIpInfo();
|
|
||||||
|
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
const ipParam = params.get('ip');
|
const ipParam = params.get('ip');
|
||||||
|
|||||||
Reference in New Issue
Block a user