mirror of
https://github.com/MrUnknownDE/utools.git
synced 2026-04-19 22:23:44 +02:00
fix: add asn-cache on filesystem
This commit is contained in:
@@ -1,28 +1,52 @@
|
|||||||
// backend/routes/asnLookup.js
|
// backend/routes/asnLookup.js
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
const pino = require('pino');
|
const pino = require('pino');
|
||||||
const Sentry = require('@sentry/node');
|
const Sentry = require('@sentry/node');
|
||||||
|
|
||||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// ─── In-Memory Cache (24h TTL) ───────────────────────────────────────────────
|
// ─── Filesystem Cache (24h TTL) ───────────────────────────────────────────────
|
||||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
const cache = new Map(); // key → { data, expiresAt }
|
const CACHE_DIR = process.env.ASN_CACHE_DIR || path.join(__dirname, '..', 'data', 'asn-cache');
|
||||||
|
|
||||||
|
// Ensure cache directory exists
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn({ error: e.message }, 'Could not create ASN cache directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheFilePath(key) {
|
||||||
|
// Sanitize key to safe filename
|
||||||
|
return path.join(CACHE_DIR, key.replace(/[^a-zA-Z0-9_:-]/g, '_') + '.json');
|
||||||
|
}
|
||||||
|
|
||||||
function getCached(key) {
|
function getCached(key) {
|
||||||
const entry = cache.get(key);
|
try {
|
||||||
if (!entry) return null;
|
const file = cacheFilePath(key);
|
||||||
|
const raw = fs.readFileSync(file, 'utf8');
|
||||||
|
const entry = JSON.parse(raw);
|
||||||
if (Date.now() > entry.expiresAt) {
|
if (Date.now() > entry.expiresAt) {
|
||||||
cache.delete(key);
|
fs.unlinkSync(file);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return entry.data;
|
return entry.data;
|
||||||
|
} catch {
|
||||||
|
return null; // File doesn't exist or parse failed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCache(key, data) {
|
function setCache(key, data) {
|
||||||
cache.set(key, { data, expiresAt: Date.now() + CACHE_TTL_MS });
|
try {
|
||||||
|
const entry = { data, expiresAt: Date.now() + CACHE_TTL_MS };
|
||||||
|
fs.writeFileSync(cacheFilePath(key), JSON.stringify(entry), 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn({ key, error: e.message }, 'ASN cache write failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── HTTP Helper ──────────────────────────────────────────────────────────────
|
// ─── HTTP Helper ──────────────────────────────────────────────────────────────
|
||||||
@@ -41,22 +65,18 @@ function fetchJson(url) {
|
|||||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||||
return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
||||||
}
|
}
|
||||||
try {
|
try { resolve(JSON.parse(raw)); }
|
||||||
resolve(JSON.parse(raw));
|
catch (e) { reject(new Error(`JSON parse error: ${e.message}`)); }
|
||||||
} catch (e) {
|
|
||||||
reject(new Error(`JSON parse error from ${url}: ${e.message}`));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
req.on('error', reject);
|
req.on('error', reject);
|
||||||
req.on('timeout', () => { req.destroy(); reject(new Error(`Timeout fetching ${url}`)); });
|
req.on('timeout', () => { req.destroy(); reject(new Error(`Timeout: ${url}`)); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── ASN Validation ───────────────────────────────────────────────────────────
|
// ─── ASN Validation ───────────────────────────────────────────────────────────
|
||||||
function parseAsn(raw) {
|
function parseAsn(raw) {
|
||||||
if (!raw || typeof raw !== 'string') return null;
|
if (!raw || typeof raw !== 'string') return null;
|
||||||
// Accept "15169", "AS15169", "as15169"
|
|
||||||
const cleaned = raw.trim().toUpperCase().replace(/^AS/, '');
|
const cleaned = raw.trim().toUpperCase().replace(/^AS/, '');
|
||||||
const n = parseInt(cleaned, 10);
|
const n = parseInt(cleaned, 10);
|
||||||
if (isNaN(n) || n < 1 || n > 4294967295 || String(n) !== cleaned) return null;
|
if (isNaN(n) || n < 1 || n > 4294967295 || String(n) !== cleaned) return null;
|
||||||
@@ -65,12 +85,11 @@ function parseAsn(raw) {
|
|||||||
|
|
||||||
// ─── RIPE Stat Fetchers ───────────────────────────────────────────────────────
|
// ─── RIPE Stat Fetchers ───────────────────────────────────────────────────────
|
||||||
async function fetchOverview(asn) {
|
async function fetchOverview(asn) {
|
||||||
const cacheKey = `overview:${asn}`;
|
const key = `overview:${asn}`;
|
||||||
const cached = getCached(cacheKey);
|
const cached = getCached(key);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const url = `https://stat.ripe.net/data/as-overview/data.json?resource=AS${asn}`;
|
const json = await fetchJson(`https://stat.ripe.net/data/as-overview/data.json?resource=AS${asn}`);
|
||||||
const json = await fetchJson(url);
|
|
||||||
const d = json?.data;
|
const d = json?.data;
|
||||||
const result = {
|
const result = {
|
||||||
asn,
|
asn,
|
||||||
@@ -79,17 +98,16 @@ async function fetchOverview(asn) {
|
|||||||
type: d?.type || null,
|
type: d?.type || null,
|
||||||
block: d?.block || null,
|
block: d?.block || null,
|
||||||
};
|
};
|
||||||
setCache(cacheKey, result);
|
setCache(key, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNeighbours(asn) {
|
async function fetchNeighbours(asn) {
|
||||||
const cacheKey = `neighbours:${asn}`;
|
const key = `neighbours:${asn}`;
|
||||||
const cached = getCached(cacheKey);
|
const cached = getCached(key);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const url = `https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS${asn}`;
|
const json = await fetchJson(`https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS${asn}`);
|
||||||
const json = await fetchJson(url);
|
|
||||||
const neighbours = (json?.data?.neighbours || []).map(n => ({
|
const neighbours = (json?.data?.neighbours || []).map(n => ({
|
||||||
asn: n.asn,
|
asn: n.asn,
|
||||||
type: n.type, // 'left' = upstream, 'right' = downstream
|
type: n.type, // 'left' = upstream, 'right' = downstream
|
||||||
@@ -97,32 +115,30 @@ async function fetchNeighbours(asn) {
|
|||||||
v4_peers: n.v4_peers || 0,
|
v4_peers: n.v4_peers || 0,
|
||||||
v6_peers: n.v6_peers || 0,
|
v6_peers: n.v6_peers || 0,
|
||||||
}));
|
}));
|
||||||
setCache(cacheKey, neighbours);
|
setCache(key, neighbours);
|
||||||
return neighbours;
|
return neighbours;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPrefixes(asn) {
|
async function fetchPrefixes(asn) {
|
||||||
const cacheKey = `prefixes:${asn}`;
|
const key = `prefixes:${asn}`;
|
||||||
const cached = getCached(cacheKey);
|
const cached = getCached(key);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const url = `https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}`;
|
const json = await fetchJson(`https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS${asn}`);
|
||||||
const json = await fetchJson(url);
|
|
||||||
const prefixes = (json?.data?.prefixes || []).map(p => p.prefix);
|
const prefixes = (json?.data?.prefixes || []).map(p => p.prefix);
|
||||||
setCache(cacheKey, prefixes);
|
setCache(key, prefixes);
|
||||||
return prefixes;
|
return prefixes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPeeringDb(asn) {
|
async function fetchPeeringDb(asn) {
|
||||||
const cacheKey = `peeringdb:${asn}`;
|
const key = `peeringdb:${asn}`;
|
||||||
const cached = getCached(cacheKey);
|
const cached = getCached(key);
|
||||||
if (cached) return cached;
|
if (cached !== null) return cached;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `https://www.peeringdb.com/api/net?asn=${asn}&depth=2`;
|
const json = await fetchJson(`https://www.peeringdb.com/api/net?asn=${asn}&depth=2`);
|
||||||
const json = await fetchJson(url);
|
|
||||||
const net = json?.data?.[0];
|
const net = json?.data?.[0];
|
||||||
if (!net) { setCache(cacheKey, null); return null; }
|
if (!net) { setCache(key, null); return null; }
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
peeringPolicy: net.policy_general || null,
|
peeringPolicy: net.policy_general || null,
|
||||||
@@ -133,9 +149,9 @@ async function fetchPeeringDb(asn) {
|
|||||||
speed: ix.speed,
|
speed: ix.speed,
|
||||||
ipv4: ix.ipaddr4 || null,
|
ipv4: ix.ipaddr4 || null,
|
||||||
ipv6: ix.ipaddr6 || null,
|
ipv6: ix.ipaddr6 || null,
|
||||||
})).slice(0, 20), // max 20 IXPs
|
})).slice(0, 20),
|
||||||
};
|
};
|
||||||
setCache(cacheKey, result);
|
setCache(key, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn({ asn, error: e.message }, 'PeeringDB fetch failed');
|
logger.warn({ asn, error: e.message }, 'PeeringDB fetch failed');
|
||||||
@@ -143,6 +159,16 @@ async function fetchPeeringDb(asn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Resolve names for a list of ASNs ────────────────────────────────────────
|
||||||
|
async function resolveNames(asnList) {
|
||||||
|
const results = await Promise.allSettled(asnList.map(a => fetchOverview(a)));
|
||||||
|
const map = {};
|
||||||
|
results.forEach((r, i) => {
|
||||||
|
map[asnList[i]] = r.status === 'fulfilled' ? (r.value.name || null) : null;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Route ────────────────────────────────────────────────────────────────────
|
// ─── Route ────────────────────────────────────────────────────────────────────
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
const rawAsn = req.query.asn;
|
const rawAsn = req.query.asn;
|
||||||
@@ -150,13 +176,16 @@ router.get('/', async (req, res, next) => {
|
|||||||
|
|
||||||
const asn = parseAsn(String(rawAsn || ''));
|
const asn = parseAsn(String(rawAsn || ''));
|
||||||
if (!asn) {
|
if (!asn) {
|
||||||
return res.status(400).json({ success: false, error: 'Invalid ASN. Please provide a number between 1 and 4294967295, e.g. ?asn=15169' });
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid ASN. Please provide a number between 1 and 4294967295, e.g. ?asn=15169'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info({ requestIp, asn }, 'ASN lookup request');
|
logger.info({ requestIp, asn }, 'ASN lookup request');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Level 1 + Level 2: overview + direct neighbours + prefixes + PeeringDB (parallel)
|
// Level 1 + Level 2: fetch all base data in parallel
|
||||||
const [overview, neighbours, prefixes, peeringdb] = await Promise.all([
|
const [overview, neighbours, prefixes, peeringdb] = await Promise.all([
|
||||||
fetchOverview(asn),
|
fetchOverview(asn),
|
||||||
fetchNeighbours(asn),
|
fetchNeighbours(asn),
|
||||||
@@ -164,67 +193,49 @@ router.get('/', async (req, res, next) => {
|
|||||||
fetchPeeringDb(asn),
|
fetchPeeringDb(asn),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Split neighbours into upstream (left) and downstream (right)
|
// Split neighbours
|
||||||
const upstreams = neighbours
|
const upstreams = neighbours.filter(n => n.type === 'left').sort((a, b) => b.power - a.power).slice(0, 10);
|
||||||
.filter(n => n.type === 'left')
|
const downstreams = neighbours.filter(n => n.type === 'right').sort((a, b) => b.power - a.power).slice(0, 10);
|
||||||
.sort((a, b) => b.power - a.power)
|
|
||||||
.slice(0, 10); // Top 10 upstreams for Level 2
|
|
||||||
|
|
||||||
const downstreams = neighbours
|
// Resolve names for ALL Level 2 nodes (both upstreams and downstreams)
|
||||||
.filter(n => n.type === 'right')
|
const level2Asns = [...new Set([...upstreams, ...downstreams].map(n => n.asn))];
|
||||||
.sort((a, b) => b.power - a.power)
|
const level2Names = await resolveNames(level2Asns);
|
||||||
.slice(0, 10); // Top 10 downstreams for Level 2
|
|
||||||
|
|
||||||
// Level 3: fetch upstreams of upstreams (top 5 of Level 2 upstreams only)
|
// Level 3: fetch upstreams-of-upstreams for top 5 Level 2 upstreams
|
||||||
const level3Raw = await Promise.allSettled(
|
const level3Raw = await Promise.allSettled(
|
||||||
upstreams.slice(0, 5).map(async (upstreamNode) => {
|
upstreams.slice(0, 5).map(async (upstreamNode) => {
|
||||||
const theirNeighbours = await fetchNeighbours(upstreamNode.asn);
|
const theirNeighbours = await fetchNeighbours(upstreamNode.asn);
|
||||||
const overviewResult = await fetchOverview(upstreamNode.asn);
|
|
||||||
// Their upstreams (left) = Level 3
|
|
||||||
const theirUpstreams = theirNeighbours
|
const theirUpstreams = theirNeighbours
|
||||||
.filter(n => n.type === 'left')
|
.filter(n => n.type === 'left')
|
||||||
.sort((a, b) => b.power - a.power)
|
.sort((a, b) => b.power - a.power)
|
||||||
.slice(0, 3); // Top 3 per Level-2 upstream
|
.slice(0, 3);
|
||||||
return {
|
return { parentAsn: upstreamNode.asn, theirUpstreams };
|
||||||
parentAsn: upstreamNode.asn,
|
|
||||||
parentName: overviewResult.name,
|
|
||||||
theirUpstreams,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Collect Level 3 nodes, resolve names for them
|
|
||||||
const level3Data = level3Raw
|
const level3Data = level3Raw
|
||||||
.filter(r => r.status === 'fulfilled')
|
.filter(r => r.status === 'fulfilled')
|
||||||
.map(r => r.value);
|
.map(r => r.value);
|
||||||
|
|
||||||
// Flatten all unique Level 3 ASNs and fetch their names
|
// Resolve names for Level 3 nodes
|
||||||
const level3Asns = [...new Set(
|
const level3Asns = [...new Set(level3Data.flatMap(d => d.theirUpstreams.map(n => n.asn)))];
|
||||||
level3Data.flatMap(d => d.theirUpstreams.map(n => n.asn))
|
const level3Names = await resolveNames(level3Asns);
|
||||||
)];
|
|
||||||
const level3Names = await Promise.allSettled(
|
|
||||||
level3Asns.map(a => fetchOverview(a))
|
|
||||||
);
|
|
||||||
const asnNameMap = {};
|
|
||||||
level3Names.forEach((r, i) => {
|
|
||||||
if (r.status === 'fulfilled') asnNameMap[level3Asns[i]] = r.value.name;
|
|
||||||
});
|
|
||||||
// Also include Level 2 names
|
|
||||||
[...upstreams, ...downstreams].forEach(n => {
|
|
||||||
if (!asnNameMap[n.asn]) asnNameMap[n.asn] = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build graph structure for frontend
|
// ── Build graph ───────────────────────────────────────────────────────
|
||||||
const graph = {
|
const graph = {
|
||||||
center: { asn, name: overview.name },
|
center: { asn, name: overview.name },
|
||||||
level2: {
|
level2: {
|
||||||
upstreams: upstreams.map(n => ({ asn: n.asn, name: asnNameMap[n.asn] || null, power: n.power, v4: n.v4_peers, v6: n.v6_peers })),
|
upstreams: upstreams.map(n => ({ asn: n.asn, name: level2Names[n.asn] || null, power: n.power, v4: n.v4_peers, v6: n.v6_peers })),
|
||||||
downstreams: downstreams.map(n => ({ asn: n.asn, name: asnNameMap[n.asn] || null, power: n.power, v4: n.v4_peers, v6: n.v6_peers })),
|
downstreams: downstreams.map(n => ({ asn: n.asn, name: level2Names[n.asn] || null, power: n.power, v4: n.v4_peers, v6: n.v6_peers })),
|
||||||
},
|
},
|
||||||
level3: level3Data.map(d => ({
|
level3: level3Data.map(d => ({
|
||||||
parentAsn: d.parentAsn,
|
parentAsn: d.parentAsn,
|
||||||
parentName: d.parentName,
|
parentName: level2Names[d.parentAsn] || null,
|
||||||
upstreams: d.theirUpstreams.map(n => ({ asn: n.asn, name: asnNameMap[n.asn] || null, power: n.power })),
|
upstreams: d.theirUpstreams.map(n => ({
|
||||||
|
asn: n.asn,
|
||||||
|
name: level3Names[n.asn] || null,
|
||||||
|
power: n.power,
|
||||||
|
})),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,7 +245,7 @@ router.get('/', async (req, res, next) => {
|
|||||||
name: overview.name,
|
name: overview.name,
|
||||||
announced: overview.announced,
|
announced: overview.announced,
|
||||||
type: overview.type,
|
type: overview.type,
|
||||||
prefixes: prefixes.slice(0, 100), // max 100 prefixes
|
prefixes: prefixes.slice(0, 100),
|
||||||
peeringdb,
|
peeringdb,
|
||||||
graph,
|
graph,
|
||||||
});
|
});
|
||||||
|
|||||||
43
compose.yml
43
compose.yml
@@ -1,46 +1,45 @@
|
|||||||
services:
|
services:
|
||||||
# Backend Service (Node.js App)
|
# Backend Service (Node.js App)
|
||||||
backend:
|
backend:
|
||||||
# Verwendet ein bereits gebautes Image
|
|
||||||
image: mrunknownde/utools-backend
|
image: mrunknownde/utools-backend
|
||||||
container_name: utools_backend
|
container_name: utools_backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
# Setze Umgebungsvariablen für das Backend
|
NODE_ENV: production
|
||||||
NODE_ENV: production # Wichtig für Performance und Logging
|
PORT: 3000
|
||||||
PORT: 3000 # Port innerhalb des Containers
|
LOG_LEVEL: info
|
||||||
LOG_LEVEL: info # Oder 'warn' für weniger Logs in Produktion
|
|
||||||
PING_COUNT: 4
|
PING_COUNT: 4
|
||||||
# Die DB-Pfade werden aus dem Backend-Dockerfile ENV genommen,
|
SENTRY_DSN: "https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568"
|
||||||
# könnten hier aber überschrieben werden, falls nötig.
|
# ASN Cache directory (filesystem persistence across restarts)
|
||||||
# GEOIP_CITY_DB: ./data/GeoLite2-City.mmdb
|
ASN_CACHE_DIR: /app/asn-cache
|
||||||
# GEOIP_ASN_DB: ./data/GeoLite2-ASN.mmdb
|
volumes:
|
||||||
# Sentry DSN aus der Umgebung/ .env Datei übernehmen
|
# Persistent ASN lookup cache — survives container restarts
|
||||||
SENTRY_DSN: "https://7ea70caba68f548fb96482a573006a7b@o447623.ingest.us.sentry.io/4509062020333568" # Wichtig für die Laufzeit
|
- asn_cache:/app/asn-cache
|
||||||
dns:
|
dns:
|
||||||
- 1.1.1.1 # Cloudflare DNS
|
- 1.1.1.1
|
||||||
- 1.0.0.1 # Cloudflare DNS
|
- 1.0.0.1
|
||||||
- 8.8.8.8 # Google DNS
|
- 8.8.8.8
|
||||||
- 8.8.4.4 # Google DNS
|
- 8.8.4.4
|
||||||
networks:
|
networks:
|
||||||
- utools_network # Verbinde mit unserem benutzerdefinierten Netzwerk
|
- utools_network
|
||||||
|
|
||||||
# Frontend Service (Nginx)
|
# Frontend Service (Nginx)
|
||||||
frontend:
|
frontend:
|
||||||
# Verwendet ein bereits gebautes Image
|
|
||||||
image: mrunknownde/utools-frontend
|
image: mrunknownde/utools-frontend
|
||||||
container_name: utools_frontend
|
container_name: utools_frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
# Mappe Port 8080 vom Host auf Port 80 im Container (wo Nginx lauscht)
|
|
||||||
# Zugriff von außen (Browser) erfolgt über localhost:8080
|
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend # Stellt sicher, dass Backend gestartet wird (aber nicht unbedingt bereit ist)
|
- backend
|
||||||
networks:
|
networks:
|
||||||
- utools_network # Verbinde mit unserem benutzerdefinierten Netzwerk
|
- utools_network
|
||||||
|
|
||||||
# Definiere ein benutzerdefiniertes Netzwerk (gute Praxis)
|
|
||||||
networks:
|
networks:
|
||||||
utools_network:
|
utools_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
# Named volume — ASN cache persists across container restarts
|
||||||
|
volumes:
|
||||||
|
asn_cache:
|
||||||
|
driver: local
|
||||||
|
|||||||
@@ -181,11 +181,12 @@
|
|||||||
Suite</span></h1>
|
Suite</span></h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">IP Info & Tools</a></li>
|
<li><a href="/">IP Info & Tools</a></li>
|
||||||
<li><a href="/subnet">Subnetz Rechner</a></li>
|
<li><a href="/subnet">Subnetz Rechner</a></li>
|
||||||
<li><a href="/dns" class="active-link">DNS Lookup</a></li>
|
<li><a href="/dns" class="active-link">DNS Lookup</a></li>
|
||||||
<li><a href="/whois">WHOIS Lookup</a></li>
|
<li><a href="/whois">WHOIS Lookup</a></li>
|
||||||
<li><a href="/mac">MAC Lookup</a></li>
|
<li><a href="/mac">MAC Lookup</a></li>
|
||||||
|
<li><a href="/asn">ASN Lookup</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -182,11 +182,12 @@
|
|||||||
Suite</span></h1>
|
Suite</span></h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">IP Info & Tools</a></li>
|
<li><a href="/">IP Info & Tools</a></li>
|
||||||
<li><a href="/subnet">Subnetz Rechner</a></li>
|
<li><a href="/subnet">Subnetz Rechner</a></li>
|
||||||
<li><a href="/dns">DNS Lookup</a></li>
|
<li><a href="/dns">DNS Lookup</a></li>
|
||||||
<li><a href="/whois">WHOIS Lookup</a></li>
|
<li><a href="/whois">WHOIS Lookup</a></li>
|
||||||
<li><a href="/mac" class="active-link">MAC Lookup</a></li>
|
<li><a href="/mac" class="active-link">MAC Lookup</a></li>
|
||||||
|
<li><a href="/asn">ASN Lookup</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -287,13 +287,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
updateField(coordsEl, data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
|
updateField(coordsEl, data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null);
|
||||||
updateField(timezoneEl, data.geo?.timezone, geoLoader); // Hide loader on last geo field
|
updateField(timezoneEl, data.geo?.timezone, geoLoader); // Hide loader on last geo field
|
||||||
|
|
||||||
updateField(asnNumberEl, data.asn?.number
|
// ASN — render as clickable link if has a number (not an error object)
|
||||||
? `AS${data.asn.number}`
|
const asnNum = (data.asn && !data.asn.error) ? data.asn.number : null;
|
||||||
: null, null, asnErrorEl);
|
if (asnNum && asnNumberEl) {
|
||||||
// Make ASN a clickable link to ASN Lookup
|
// Reveal the hidden data container manually (updateField won't run the link path via error branch)
|
||||||
if (data.asn?.number && asnNumberEl) {
|
const asnContainer = asnNumberEl.closest('div:not(.loader)');
|
||||||
|
if (asnContainer) asnContainer.classList.remove('hidden');
|
||||||
asnNumberEl.innerHTML =
|
asnNumberEl.innerHTML =
|
||||||
`<a href="/asn?asn=${data.asn.number}" class="hover:text-purple-200 underline decoration-dotted transition-colors" title="Open ASN Lookup">AS${data.asn.number}</a>`;
|
`<a href="/asn?asn=${asnNum}" class="hover:text-purple-200 underline decoration-dotted transition-colors" title="Open ASN Lookup">AS${asnNum}</a>`;
|
||||||
|
} else {
|
||||||
|
updateField(asnNumberEl, null, null, asnErrorEl, data.asn?.error || '-');
|
||||||
}
|
}
|
||||||
updateField(asnOrgEl, data.asn?.organization, asnLoader);
|
updateField(asnOrgEl, data.asn?.organization, asnLoader);
|
||||||
|
|
||||||
|
|||||||
@@ -165,11 +165,12 @@
|
|||||||
Suite</span></h1>
|
Suite</span></h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">IP Info & Tools</a></li>
|
<li><a href="/">IP Info & Tools</a></li>
|
||||||
<li><a href="/subnet" class="active-link">Subnetz Rechner</a></li>
|
<li><a href="/subnet" class="active-link">Subnetz Rechner</a></li>
|
||||||
<li><a href="/dns">DNS Lookup</a></li>
|
<li><a href="/dns">DNS Lookup</a></li>
|
||||||
<li><a href="/whois">WHOIS Lookup</a></li>
|
<li><a href="/whois">WHOIS Lookup</a></li>
|
||||||
<li><a href="/mac">MAC Lookup</a></li>
|
<li><a href="/mac">MAC Lookup</a></li>
|
||||||
|
<li><a href="/asn">ASN Lookup</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -181,11 +181,12 @@
|
|||||||
Suite</span></h1>
|
Suite</span></h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/">IP Info & Tools</a></li>
|
<li><a href="/">IP Info & Tools</a></li>
|
||||||
<li><a href="/subnet">Subnetz Rechner</a></li>
|
<li><a href="/subnet">Subnetz Rechner</a></li>
|
||||||
<li><a href="/dns">DNS Lookup</a></li>
|
<li><a href="/dns">DNS Lookup</a></li>
|
||||||
<li><a href="/whois" class="active-link">WHOIS Lookup</a></li>
|
<li><a href="/whois" class="active-link">WHOIS Lookup</a></li>
|
||||||
<li><a href="/mac">MAC Lookup</a></li>
|
<li><a href="/mac">MAC Lookup</a></li>
|
||||||
|
<li><a href="/asn">ASN Lookup</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user