94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
|
|
interface AssetExpandQuery {
|
|
set?: string;
|
|
}
|
|
|
|
async function fetchWithRetry(url: string, retries = 1, timeout = 10000): Promise<any> {
|
|
for (let i = 0; i <= retries; i++) {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
try {
|
|
const response = await fetch(url, { signal: controller.signal });
|
|
clearTimeout(timeoutId);
|
|
if (!response.ok) {
|
|
if (i === retries) throw new Error(`HTTP ${response.status}`);
|
|
continue;
|
|
}
|
|
return await response.json();
|
|
} catch (e) {
|
|
clearTimeout(timeoutId);
|
|
if (i === retries) throw e;
|
|
await new Promise(r => setTimeout(r, 1500));
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function assetExpandRoutes(fastify: FastifyInstance): Promise<void> {
|
|
fastify.get<{ Querystring: AssetExpandQuery }>(
|
|
'/asset-expand',
|
|
async (request: FastifyRequest<{ Querystring: AssetExpandQuery }>, reply: FastifyReply) => {
|
|
const setName = request.query.set || '';
|
|
|
|
reply.header('Access-Control-Allow-Origin', '*');
|
|
reply.header('Cache-Control', 'public, max-age=3600');
|
|
|
|
if (!setName) {
|
|
return reply.status(400).send({ error: 'set required (e.g. AS-FLEXOPTIX)' });
|
|
}
|
|
|
|
async function expandSet(name: string, depth: number, visited: Set<string>): Promise<{ asns: string[], sets: string[] }> {
|
|
if (depth > 4 || visited.has(name)) return { asns: [], sets: [] };
|
|
visited.add(name);
|
|
|
|
const url = `https://rest.db.ripe.net/search.json?query-string=${encodeURIComponent(name)}&type-filter=as-set&flags=no-referenced`;
|
|
let data;
|
|
try {
|
|
data = await fetchWithRetry(url);
|
|
} catch (e) {
|
|
// If RIPE DB fails on a specific set, gracefully continue with empty array for that branch
|
|
return { asns: [], sets: [] };
|
|
}
|
|
|
|
const asns: string[] = [];
|
|
const sets: string[] = [];
|
|
|
|
if (data && data.objects && data.objects.object) {
|
|
for (const obj of data.objects.object) {
|
|
const attrs = (obj.attributes && obj.attributes.attribute) || [];
|
|
for (const a of attrs) {
|
|
if (a.name === 'members') {
|
|
for (const m of (a.value || '').split(/[,\s]+/).filter(Boolean)) {
|
|
if (/^AS\d+$/i.test(m)) asns.push(m.toUpperCase());
|
|
else if (m.startsWith('AS-')) sets.push(m);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const sub of sets.slice(0, 10)) { // Limit sub-sets to prevent overwhelming API
|
|
const sub_r = await expandSet(sub, depth + 1, visited);
|
|
asns.push(...sub_r.asns);
|
|
}
|
|
return { asns: [...new Set(asns)], sets };
|
|
}
|
|
|
|
try {
|
|
const visited = new Set<string>();
|
|
const result = await expandSet(setName.toUpperCase(), 0, visited);
|
|
result.asns.sort((a, b) => parseInt(a.slice(2)) - parseInt(b.slice(2)));
|
|
|
|
return reply.status(200).send({
|
|
set: setName.toUpperCase(),
|
|
count: result.asns.length,
|
|
asns: result.asns,
|
|
sub_sets: result.sets
|
|
});
|
|
} catch (e: any) {
|
|
return reply.status(500).send({ error: e.message });
|
|
}
|
|
}
|
|
);
|
|
}
|