20260718 Explorer 接入 VRP 检索查询
This commit is contained in:
104
ui/rpki-explorer/src/lib/vrpQuery.ts
Normal file
104
ui/rpki-explorer/src/lib/vrpQuery.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Classify a free-text search query into a VRP lookup target.
|
||||
*
|
||||
* The backend VRP lookup (`GET /runs/{id}/vrps/lookup`) accepts exactly one of
|
||||
* `ip` / `prefix`, plus an optional `asn` filter. An ASN-only query cannot be
|
||||
* served by the API — the UI surfaces that explicitly instead of guessing.
|
||||
*/
|
||||
|
||||
export type NetworkQuery =
|
||||
| { kind: "ip"; ip: string }
|
||||
| { kind: "prefix"; prefix: string }
|
||||
| { kind: "asn"; asn: number }
|
||||
| { kind: "none" };
|
||||
|
||||
const ASN_MAX = 4_294_967_295;
|
||||
|
||||
/** "AS45090" / "as45090" / "45090" → ASN, else null. */
|
||||
export function parseAsn(input: string): number | null {
|
||||
const match = /^(?:as)?(\d{1,10})$/i.exec(input.trim());
|
||||
if (!match) return null;
|
||||
const asn = Number(match[1]);
|
||||
return Number.isSafeInteger(asn) && asn <= ASN_MAX ? asn : null;
|
||||
}
|
||||
|
||||
function isIpv4Octets(addr: string): boolean {
|
||||
const parts = addr.split(".");
|
||||
return (
|
||||
parts.length === 4 &&
|
||||
parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255)
|
||||
);
|
||||
}
|
||||
|
||||
function isIpv6(addr: string): boolean {
|
||||
if (!/^[0-9a-fA-F:.]+$/.test(addr)) return false;
|
||||
if ((addr.match(/::/g) ?? []).length > 1) return false;
|
||||
const isGroup = (g: string, isLast: boolean) =>
|
||||
isLast && g.includes(".")
|
||||
? isIpv4Octets(g) // embedded IPv4 tail, e.g. "::ffff:192.0.2.1"
|
||||
: /^[0-9a-fA-F]{1,4}$/.test(g);
|
||||
const ipv4TailWeight = (groups: string[]) =>
|
||||
groups.length > 0 && groups[groups.length - 1].includes(".") ? 1 : 0;
|
||||
if (addr.includes("::")) {
|
||||
const [head, tail] = addr.split("::");
|
||||
const headGroups = head ? head.split(":") : [];
|
||||
const tailGroups = tail ? tail.split(":") : [];
|
||||
// "::" compresses at least one group; an IPv4 tail counts as two groups.
|
||||
const effective =
|
||||
headGroups.length + tailGroups.length + ipv4TailWeight(tailGroups);
|
||||
if (effective > 7) return false;
|
||||
return [...headGroups, ...tailGroups].every((g, i, all) =>
|
||||
isGroup(g, i === all.length - 1),
|
||||
);
|
||||
}
|
||||
const groups = addr.split(":");
|
||||
const expected = groups[groups.length - 1]?.includes(".") ? 7 : 8;
|
||||
if (groups.length !== expected) return false;
|
||||
return groups.every((g, i) => isGroup(g, i === groups.length - 1));
|
||||
}
|
||||
|
||||
/** Bare IPv4/IPv6 address (no "/") → normalized string, else null. */
|
||||
export function parseIpAddress(input: string): string | null {
|
||||
const s = input.trim();
|
||||
if (s.includes("/")) return null;
|
||||
if (isIpv4Octets(s)) return s;
|
||||
if (s.includes(":") && isIpv6(s)) return s;
|
||||
return null;
|
||||
}
|
||||
|
||||
function parsePrefixParts(s: string): [string, number] | null {
|
||||
const slash = s.indexOf("/");
|
||||
if (slash < 0 || slash !== s.lastIndexOf("/")) return null;
|
||||
const addr = s.slice(0, slash);
|
||||
const lenRaw = s.slice(slash + 1);
|
||||
if (!/^\d{1,3}$/.test(lenRaw)) return null;
|
||||
return [addr, Number(lenRaw)];
|
||||
}
|
||||
|
||||
/** "addr/len" CIDR with family-correct length → normalized string, else null. */
|
||||
export function parsePrefix(input: string): string | null {
|
||||
const s = input.trim();
|
||||
const parts = parsePrefixParts(s);
|
||||
if (!parts) return null;
|
||||
const [addr, len] = parts;
|
||||
if (isIpv4Octets(addr)) return len <= 32 ? `${addr}/${len}` : null;
|
||||
if (addr.includes(":") && isIpv6(addr)) return len <= 128 ? `${addr}/${len}` : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a query string. Precedence: CIDR prefix → bare IP → ASN → none.
|
||||
* ("192.0.2.1" also parses as digits+ dots only; ASN requires pure digits, so
|
||||
* no overlap. A bare number is always treated as an ASN.)
|
||||
*/
|
||||
export function classifyNetworkQuery(input: string): NetworkQuery {
|
||||
const s = input.trim();
|
||||
if (!s) return { kind: "none" };
|
||||
const prefix = parsePrefix(s);
|
||||
if (prefix) return { kind: "prefix", prefix };
|
||||
const ip = parseIpAddress(s);
|
||||
if (ip) return { kind: "ip", ip };
|
||||
const asn = parseAsn(s);
|
||||
if (asn !== null) return { kind: "asn", asn };
|
||||
return { kind: "none" };
|
||||
}
|
||||
Reference in New Issue
Block a user