Add better Domain/Events/Status rendering, implement zod schema parsing & handling

This commit is contained in:
Xevion
2023-01-15 00:05:37 -06:00
parent 65cf8ba83c
commit 3010cf22b3
5 changed files with 125 additions and 130 deletions

View File

@@ -1,13 +1,14 @@
import type {FunctionComponent, ReactNode} from "react"; import type {FunctionComponent, ReactNode} from "react";
import React, {Fragment} from "react"; import React, {Fragment} from "react";
import type {DomainType} from "@/components/DomainType";
import {rdapStatusInfo} from "@/constants"; import {rdapStatusInfo} from "@/constants";
import type {Domain} from "@/responses";
import Events from "@/components/Events"
export type DomainProps = { export type DomainProps = {
data: DomainType; data: Domain;
}; };
const Domain: FunctionComponent<DomainProps> = ({data}: DomainProps) => { const DomainCard: FunctionComponent<DomainProps> = ({data}: DomainProps) => {
const properties: [string | ReactNode, string | ReactNode][] = []; const properties: [string | ReactNode, string | ReactNode][] = [];
if (data.unicodeName) { if (data.unicodeName) {
@@ -17,18 +18,20 @@ const Domain: FunctionComponent<DomainProps> = ({data}: DomainProps) => {
properties.push(["Name", data.ldhName]) properties.push(["Name", data.ldhName])
} }
if (data.handle) properties.push(["Handle", data.handle]); properties.push(["Handle", data.handle]);
// if (data.events) properties.push properties.push(["Events", <Events key={0} data={data.events} />])
if (data.status) properties.push([
"Status",
data.status.map((statusKey, index) =>
<span title={rdapStatusInfo[statusKey]!} key={index}>
{statusKey}
</span>)
])
properties.push([
"Status",
<ul key={2} className="list-disc">
{data.status.map((statusKey, index) =>
<li title={rdapStatusInfo[statusKey]} key={index}>
{statusKey}
</li>)}
</ul>
])
return <div className="card"> return <div className="card">
<div className="card-header">{data.name} ({data.handle})</div> <div className="card-header">{data.ldhName ?? data.unicodeName} ({data.handle})</div>
<div className="card-body"> <div className="card-body">
<dl> <dl>
{ {
@@ -44,4 +47,4 @@ const Domain: FunctionComponent<DomainProps> = ({data}: DomainProps) => {
</div> </div>
} }
export default Domain; export default DomainCard;

View File

@@ -1,61 +0,0 @@
import type {FunctionComponent} from "react";
import Domain from "@/components/Domain";
export type Link = {
value: string;
rel: string;
href: string;
type: string
}
export type ObjectTypes = 'domain' | 'nameserver' | 'entity' | 'autnum' | 'ip network';
export type DomainType = {
objectClassName: 'domain';
handle: string;
unicodeName: string;
ldhName: string;
links: Link[];
nameservers: NameserverType[];
entities: EntityType[];
status: string[]
}
export type NameserverType = {
objectClassName: 'nameserver';
};
export type EntityType = {
objectClassName: 'entity';
};
export type AutnumType = {
objectClassName: 'autnum';
};
export type IpNetworkType = {
objectClassName: 'ip network';
};
export type ObjectProps = {
data: DomainType | NameserverType | EntityType | AutnumType | IpNetworkType;
};
const GenericObject: FunctionComponent<ObjectProps> = ({data}: ObjectProps) => {
switch (data.objectClassName) {
case "domain":
return <Domain data={data}/>
case "autnum":
case "entity":
case "ip network":
case "nameserver":
default:
return <div className="card my-2">
<div className="card-header">Not implemented</div>
</div>
}
// const title: string = (data.unicodeName ?? data.ldhName ?? data.handle)?.toUpperCase() ?? "Response";
// return <div className="card">
// <div className="card-header">{title}</div>
// {objectFragment}
// </div>
}
export default GenericObject;

View File

@@ -1,9 +1,26 @@
import type {FunctionComponent} from "react"; import type {FunctionComponent} from "react";
import type {Event} from "@/responses";
import {Fragment} from "react";
export type Event = { export type EventsProps = {
eventAction: string; data: Event[];
eventDate: string; }
const Events: FunctionComponent<EventsProps> = ({data}) => {
return <dl>
{data.map(({eventAction, eventDate, eventActor}, index) => {
return <Fragment key={index}>
<dt className="font-weight-bolder">
{eventAction}:
</dt>
<dd>
<span
title={eventDate.toString()}>{eventDate.toString()}
</span>
{eventActor != null ? `(by ${eventActor})` : null}
</dd>
</Fragment>
})}
</dl>
} }
const Events: FunctionComponent<EventsProps> = () => {
} export default Events;

View File

@@ -0,0 +1,31 @@
import type {FunctionComponent} from "react";
import DomainCard from "@/components/DomainCard";
import type {Domain, AutonomousNumber, Entity, Nameserver, IpNetwork} from "@/responses";
export type ParsedGeneric = Domain | Nameserver | Entity | AutonomousNumber | IpNetwork;
export type ObjectProps = {
data: ParsedGeneric;
};
const Generic: FunctionComponent<ObjectProps> = ({data}: ObjectProps) => {
switch (data.objectClassName) {
case "domain":
return <DomainCard data={data}/>
case "autnum":
case "entity":
case "ip network":
case "nameserver":
default:
return <div className="card my-2">
<div className="card-header">Not implemented</div>
</div>
}
// const title: string = (data.unicodeName ?? data.ldhName ?? data.handle)?.toUpperCase() ?? "Response";
// return <div className="card">
// <div className="card-header">{title}</div>
// {objectFragment}
// </div>
}
export default Generic;

View File

@@ -1,44 +1,51 @@
import {type NextPage} from "next"; import {type NextPage} from "next";
import Head from "next/head"; import Head from "next/head";
import type {ObjectType} from "../types"; import type {ObjectType} from "@/types";
import {placeholders} from "../constants"; import {placeholders, registryURLs} from "@/constants";
import {asnMatch, domainMatch, entityMatch, getBestURL, getRDAPURL, getType, ipMatch, showSpinner} from "../rdap"; import {domainMatch, getBestURL, getType} from "@/rdap";
import type {FormEvent} from "react";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {truthy} from "../helpers"; import {truthy} from "@/helpers";
import {registryURLs} from "../constants"; import axios from "axios";
import axios, {AxiosResponse} from "axios"; import type {ParsedGeneric} from "@/components/Generic";
import update from "immutability-helper"; import Generic from "@/components/Generic";
import GenericObject, {Link} from "../components/DomainType"; import type {ZodSchema} from "zod";
import {DomainSchema} from "@/responses";
const Index: NextPage = () => { const Index: NextPage = () => {
const [uriType, setUriType] = useState<ObjectType>('domain'); const [uriType, setUriType] = useState<ObjectType>('domain');
const [requestJSContact, setRequestJSContact] = useState(false); const [requestJSContact, setRequestJSContact] = useState(false);
const [followReferral, setFollowReferral] = useState(false); const [followReferral, setFollowReferral] = useState(false);
const [object, setObject] = useState<string>(""); const [object, setObject] = useState<string>("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [response, setResponse] = useState<any | null>(null); const [response, setResponse] = useState<ParsedGeneric | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [registryData, setRegistryData] = useState<Record<string, Any>>({}); const [registryData, setRegistryData] = useState<Record<string, any>>({});
// Change the selected type automatically // Change the selected type automatically
useEffect(function () { useEffect(function () {
const new_type = getType(object); const newType = getType(object);
if (new_type != null && new_type != uriType) if (newType != null && newType != uriType)
setUriType(new_type) setUriType(newType)
}, [object]); }, [object]);
async function loadRegistryData() { async function loadRegistryData() {
setLoading(true); setLoading(true);
console.log('Retrieving registry ..')
let registersLoaded = 0;
const totalRegisters = Object.keys(registryURLs).length;
const responses = await Promise.all(Object.entries(registryURLs).map(async ([url, registryType]) => { const responses = await Promise.all(Object.entries(registryURLs).map(async ([url, registryType]) => {
const response = await axios.get(url); const response = await axios.get(url);
registersLoaded++;
console.log(`Registered loaded ${registersLoaded}/${totalRegisters}`)
return { return {
registryType, registryType,
response: response.data response: response.data
}; };
})) }))
console.log('Registry data set.')
setRegistryData(() => { setRegistryData(() => {
return Object.fromEntries( return Object.fromEntries(
responses.map(({registryType, response}) => [registryType, response.services]) responses.map(({registryType, response}) => [registryType, response.services])
@@ -93,40 +100,44 @@ const Index: NextPage = () => {
} }
function submit(e) { async function submit(e?: FormEvent) {
e?.preventDefault(); e?.preventDefault();
console.log(`Submit invoked. ${uriType}/${JSON.stringify(object)}`)
const queryParams = requestJSContact ? '?jscard=1' : ''; const queryParams = requestJSContact ? '?jscard=1' : '';
const [url, schema]: [string, ZodSchema<ParsedGeneric>] | [null, null] = (function () {
const url = (function () {
switch (uriType) { switch (uriType) {
case 'url': // case 'url':
return object; // return [object];
case 'tld': // case 'tld':
return `https://root.rdap.org/domain/${object}${queryParams}`; // return `https://root.rdap.org/domain/${object}${queryParams}`;
case 'registrar': // case 'registrar':
return `https://registrars.rdap.org/entity/${object}-IANA${queryParams}`; // return `https://registrars.rdap.org/entity/${object}-IANA${queryParams}`;
case 'json': // case 'json':
return `json://${object}` // return `json://${object}`
case 'domain': case 'domain':
const temp = getRDAPURL(object); const temp = getRDAPURL(object);
if (temp) return `${temp}${queryParams}` if (temp) return [`${temp}${queryParams}`, DomainSchema]
return null; return [null, null];
default: default:
setError(`No RDAP URL available for ${uriType} ${object}.`); setError(`No RDAP URL available for ${uriType} ${object}.`);
return null; return [null, null];
} }
})() })()
if (url) sendQuery(url, followReferral); console.log(`URL: ${url ?? "null"}`)
if (url != null)
await sendQuery(url, schema, followReferral);
} }
async function sendQuery(url: string, followReferral = false) { async function sendQuery(url: string, schema: ZodSchema<ParsedGeneric>, followReferral = false) {
setLoading(true); setLoading(true);
let data: ParsedGeneric | null = null;
if (url.startsWith('json://')) { if (url.startsWith('json://')) {
console.log('Mock JSON query detected.')
// run the callback with a mock XHR // run the callback with a mock XHR
await handleResponse(JSON.parse(url.substring(7))) data = schema.parse(JSON.parse(url.substring(7)))
} else { } else {
try { try {
const response = await axios.get(url, {responseType: "json"}) const response = await axios.get(url, {responseType: "json"})
@@ -134,19 +145,18 @@ const Index: NextPage = () => {
setError('This object does not exist.'); setError('This object does not exist.');
else if (response.status != 200) else if (response.status != 200)
setError(`Error ${response.status}: ${response.statusText}`) setError(`Error ${response.status}: ${response.statusText}`)
await handleResponse(response, followReferral) data = schema.parse(response.data);
} catch (e) { } catch (e) {
console.log(e);
setLoading(false); setLoading(false);
setError(e.toString()) if (e instanceof Error)
setError(e.toString())
return;
} }
} }
}
// callback executed when a response is received if (followReferral && data?.links != null) {
async function handleResponse(data: { links: Link[] }, followReferral = false) { console.log('Using followReferral.')
setLoading(false);
if (followReferral && data.links != null) {
for (const link of data.links) { for (const link of data.links) {
if ('related' == link.rel && 'application/rdap+json' == link.type && link.href.match(/^(https?:|)\/\//i)) { if ('related' == link.rel && 'application/rdap+json' == link.type && link.href.match(/^(https?:|)\/\//i)) {
await sendQuery(link.href, false) await sendQuery(link.href, false)
@@ -155,10 +165,11 @@ const Index: NextPage = () => {
} }
} }
setLoading(false);
console.log(data);
try { try {
// div.appendChild(processObject(xhr.response, true));
setResponse(data); setResponse(data);
const url = `${window.location.href}?type=${encodeURIComponent(uriType)}&object=${object}&request-jscontact=${requestJSContact ? 1 : 0}&follow-referral=${followReferral ? 1 : 0}` const url = `${window.location.origin}?type=${encodeURIComponent(uriType)}&object=${object}&request-jscontact=${requestJSContact ? 1 : 0}&follow-referral=${followReferral ? 1 : 0}`
window.history.pushState(null, document.title, url); window.history.pushState(null, document.title, url);
} catch (e) { } catch (e) {
@@ -185,15 +196,9 @@ const Index: NextPage = () => {
submit(null); submit(null);
} }
loadRegistryData().catch(console.error); loadRegistryData().catch(console.error);
}, []) }, [])
useEffect(() => {
if (!loading && registryData.domain != undefined)
console.log(registryData);
}, [loading])
return ( return (
<> <>
<Head> <Head>
@@ -241,7 +246,7 @@ const Index: NextPage = () => {
<br/> <br/>
<div className="container"> <div className="container mx-auto max-w-screen-lg">
<form onSubmit={submit} className="form-inline"> <form onSubmit={submit} className="form-inline">
<div className="col p-0"> <div className="col p-0">
<div className="input-group"> <div className="input-group">
@@ -296,7 +301,7 @@ const Index: NextPage = () => {
</div> </div>
<div id="output-div"> <div id="output-div">
{response != null ? <GenericObject data={response.data}/> : null} {response != null ? <Generic data={response}/> : null}
</div> </div>
<p>This page implements a <em>completely private lookup tool</em> for domain names, IP addresses and <p>This page implements a <em>completely private lookup tool</em> for domain names, IP addresses and