From 3c8e1ce716504469b61c0859ca0efd84532a4042 Mon Sep 17 00:00:00 2001 From: Xevion Date: Sun, 19 Feb 2023 23:29:16 -0600 Subject: [PATCH] Commit latest version of useLookup hook --- src/hooks/useLookup.tsx | 150 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/hooks/useLookup.tsx diff --git a/src/hooks/useLookup.tsx b/src/hooks/useLookup.tsx new file mode 100644 index 0000000..f7b1d9c --- /dev/null +++ b/src/hooks/useLookup.tsx @@ -0,0 +1,150 @@ +import React, {useEffect, useMemo, useState} from "react"; +import {domainMatchPredicate, getBestURL, getType} from "@/rdap"; +import type {AutonomousNumber, Domain, IpNetwork, Register, RootRegistryType, TargetType} from "@/types"; +import {registryURLs} from "@/constants"; +import axios from "axios"; +import {AutonomousNumberSchema, DomainSchema, IpNetworkSchema, RegisterSchema, RootRegistryEnum} from "@/schema"; +import {truncated} from "@/helpers"; +import {ZodSchema} from "zod"; + +export type WarningHandler = (warning: { message: string }) => void; +type BootstrapMatcher = (value: string) => boolean; + +const useLookup = (warningHandler?: WarningHandler) => { + const [registryData, setRegistryData] = useState>({} as Record); + const [error, setError] = useState(null); + const [target, setTarget] = useState(""); + + const uriType = useMemo(function () { + return getType(target) ?? 'unknown'; + }, [target]); + + // Fetch & load a specific registry's data into memory. + async function loadBootstrap(type: RootRegistryType) { + // Fetch the bootstrapping file from the registry + const response = await axios.get(registryURLs[type]); + if (response.status != 200) + throw new Error(`Error: ${response.statusText}`) + + // Parse it, so we don't make any false assumptions during development & while maintaining the tool. + const parsedRegister = RegisterSchema.safeParse(response.data); + if (!parsedRegister.success) + throw new Error(`Could not parse IANA bootstrap response (${type}).`) + + // Set it in state so we can use it. + setRegistryData((prev) => ({ + ...prev, + [type]: parsedRegister.data + })); + } + + function getURL(type: RootRegistryType, lookupTarget: string): string { + const bootstrap = registryData[type]; + if (bootstrap == null) throw new Error(`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`) + + switch (type) { + case "domain": + for (const bootstrapItem of bootstrap.services) { + if (bootstrapItem[0].some(domainMatchPredicate)) + return getBestURL(bootstrapItem[1]); + } + throw new Error(`No matching domain found.`) + case "ip4": + throw new Error(`No matching ip4 found.`) + break; + case "ip6": + throw new Error(`No matching ip6 found.`) + break; + case "entity": + throw new Error(`No matching entity found.`) + break; + case "autnum": + throw new Error(`No matching autnum found.`) + break; + default: + throw new Error("") + } + } + + useEffect(() => { + const preload = async () => { + if (uriType === 'unknown') return; + const registryUri = RootRegistryEnum.safeParse(uriType); + if (!registryUri.success) return; + console.log({registryData, registryUri: registryUri.data}); + if (registryData[registryUri.data] != null) return; + + try { + await loadBootstrap(registryUri.data); + } catch (e) { + if (warningHandler != undefined) { + const message = e instanceof Error ? `(${truncated(e.message, 15)})` : '.'; + warningHandler({ + message: `Failed to preload registry${message}` + }) + } + } + } + + preload().catch(console.error); + }, [target]); + + async function getAndParse(url: string, schema: ZodSchema): Promise { + const response = await axios.get(url); + if (response.status == 200) + return schema.parse(response.data) as T + } + + async function submitInternal() { + if (target == null) + throw new Error("A target must be given in order to execute a lookup.") + + const targetType = getType(target); + + switch (targetType) { + // Block scoped case to allow url const reuse + case "ip4": { + await loadBootstrap("ip4"); + const url = getURL(targetType, target); + return getAndParse(url, IpNetworkSchema) + } + case "ip6": { + await loadBootstrap("ip6"); + const url = getURL(targetType, target); + return getAndParse(url, IpNetworkSchema); + } + case "domain": { + await loadBootstrap("domain"); + const url = getURL(targetType, target); + return getAndParse(url, DomainSchema); + } + case "autnum": { + await loadBootstrap("autnum"); + const url = getURL(targetType, target); + return getAndParse(url, AutonomousNumberSchema); + } + case null: + throw new Error("The type could not be detected given the target.") + case "url": + case "tld": + case "registrar": + case "json": + default: + throw new Error("The type detected has not been implemented.") + } + } + + async function submit() { + try { + const data = await submitInternal(); + } catch (e) { + if (!(e instanceof Error)) + return setError("An unknown, unprocessable error has occurred."); + return setError(e.message); + } + } + + return {error, setTarget, submit, currentType: uriType}; +} + +export default useLookup; \ No newline at end of file