diff --git a/package.json b/package.json index c45104f..189a55f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "axios": "^1.2.2", + "immutability-helper": "^3.1.1", "ipaddr.js": "^2.0.1", "next": "13.1.1", "react": "18.2.0", diff --git a/public/shortcut-icon.svg b/public/shortcut-icon.svg new file mode 100644 index 0000000..65bfe47 --- /dev/null +++ b/public/shortcut-icon.svg @@ -0,0 +1,2 @@ + +image/svg+xmlOpenclipartdatabase symbol2010-11-08T22:08:43database symbol in metallic stylehttps://openclipart.org/detail/94723/database-symbol-by-rg1024rg1024databaseserversymbol \ No newline at end of file diff --git a/src/components/Domain.tsx b/src/components/Domain.tsx new file mode 100644 index 0000000..9003aa8 --- /dev/null +++ b/src/components/Domain.tsx @@ -0,0 +1,46 @@ +import React, {Fragment, FunctionComponent, ReactNode} from "react"; +import {DomainType} from "./DomainType"; +import {rdapStatusInfo} from "../constants"; + +export type DomainProps = { + data: DomainType; +}; + +const Domain: FunctionComponent = ({data}: DomainProps) => { + const properties: [string | ReactNode, string | ReactNode][] = []; + + if (data.unicodeName) { + properties.push(["Name", data.unicodeName]); + properties.push(["ASCII Name", data.ldhName]); + } else { + properties.push(["Name", data.ldhName]) + } + + if (data.handle) properties.push(["Handle", data.handle]); + // if (data.events) properties.push + if (data.status) properties.push([ + "Status", + data.status.map((statusKey, index) => + + {statusKey} + ) + ]) + + return
+
{data.name} ({data.handle})
+
+
+ { + properties.map(([name, value], index) => { + return +
{name}:
+
{value}
+
+ } + )} +
+
+
+} + +export default Domain; \ No newline at end of file diff --git a/src/components/DomainType.tsx b/src/components/DomainType.tsx new file mode 100644 index 0000000..032c5c3 --- /dev/null +++ b/src/components/DomainType.tsx @@ -0,0 +1,62 @@ +import type {FunctionComponent} from "react"; +import {useMemo} from "react"; +import Domain from "./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 = ({data}: ObjectProps) => { + switch (data.objectClassName) { + case "domain": + return + case "autnum": + case "entity": + case "ip network": + case "nameserver": + default: + return
+
Not implemented
+
+ } + + // const title: string = (data.unicodeName ?? data.ldhName ?? data.handle)?.toUpperCase() ?? "Response"; + // return
+ //
{title}
+ // {objectFragment} + //
+} + +export default GenericObject; \ No newline at end of file diff --git a/src/components/Events.tsx b/src/components/Events.tsx new file mode 100644 index 0000000..d670b06 --- /dev/null +++ b/src/components/Events.tsx @@ -0,0 +1,9 @@ +import {FunctionComponent} from "react"; + +export type Event = { + eventAction: string; + eventDate: string; +} +const Events: FunctionComponent = () => { + +} \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..dcdff17 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,59 @@ +// see https://www.iana.org/assignments/rdap-json-values +import type {ExtendedUri, RdapStatusType, Uri} from "./types"; + +export const rdapStatusInfo: Record = { + "validated": "Signifies that the data of the object instance has been found to be accurate. This type of status is usually found on entity object instances to note the validity of identifying contact information.", + "renew prohibited": "Renewal or reregistration of the object instance is forbidden.", + "update prohibited": "Updates to the object instance are forbidden.", + "transfer prohibited": "Transfers of the registration from one registrar to another are forbidden. This type of status normally applies to DNR domain names.", + "delete prohibited": "Deletion of the registration of the object instance is forbidden. This type of status normally applies to DNR domain names.", + "proxy": "The registration of the object instance has been performed by a third party. This is most commonly applied to entities.", + "private": "The information of the object instance is not designated for public consumption. This is most commonly applied to entities.", + "removed": "Some of the information of the object instance has not been made available and has been removed. This is most commonly applied to entities.", + "obscured": "Some of the information of the object instance has been altered for the purposes of not readily revealing the actual information of the object instance. This is most commonly applied to entities.", + "associated": "The object instance is associated with other object instances in the registry. This is most commonly used to signify that a nameserver is associated with a domain or that an entity is associated with a network resource or domain.", + "active": "The object instance is in use. For domain names, it signifies that the domain name is published in DNS. For network and autnum registrations it signifies that they are allocated or assigned for use in operational networks. This maps to the Extensible Provisioning Protocol (EPP) [RFC5730] 'OK' status.", + "inactive": "The object instance is not in use. See 'active'.", + "locked": "Changes to the object instance cannot be made, including the association of other object instances.", + "pending create": "A request has been received for the creation of the object instance but this action is not yet complete.", + "pending renew": "A request has been received for the renewal of the object instance but this action is not yet complete.", + "pending transfer": "A request has been received for the transfer of the object instance but this action is not yet complete.", + "pending update": "A request has been received for the update or modification of the object instance but this action is not yet complete.", + "pending delete": "A request has been received for the deletion or removal of the object instance but this action is not yet complete. For domains, this might mean that the name is no longer published in DNS but has not yet been purged from the registry database.", + "add period": "This grace period is provided after the initial registration of the object. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the registration. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'addPeriod' status.", + "auto renew period": "This grace period is provided after an object registration period expires and is extended (renewed) automatically by the server. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the auto renewal. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'autoRenewPeriod' status.", + "client delete prohibited": "The client requested that requests to delete the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientDeleteProhibited' status.", + "client hold": "The client requested that the DNS delegation information MUST NOT be published for the object. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'clientHold' status.", + "client renew prohibited": "The client requested that requests to renew the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'clientRenewProhibited' status.", + "client transfer prohibited": "The client requested that requests to transfer the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientTransferProhibited' status.", + "client update prohibited": "The client requested that requests to update the object (other than to remove this status) MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'clientUpdateProhibited' status.", + "pending restore": "An object is in the process of being restored after being in the redemption period state. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'pendingRestore' status.", + "redemption period": "A delete has been received, but the object has not yet been purged because an opportunity exists to restore the object and abort the deletion process. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'redemptionPeriod' status.", + "renew period": "This grace period is provided after an object registration period is explicitly extended (renewed) by the client. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the renewal. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'renewPeriod' status.", + "server delete prohibited": "The server set the status so that requests to delete the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverDeleteProhibited' status.", + "server renew prohibited": "The server set the status so that requests to renew the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'serverRenewProhibited' status.", + "server transfer prohibited": "The server set the status so that requests to transfer the object MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverTransferProhibited' status.", + "server update prohibited": "The server set the status so that requests to update the object (other than to remove this status) MUST be rejected. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731], Extensible Provisioning Protocol (EPP) Host Mapping [RFC5732], and Extensible Provisioning Protocol (EPP) Contact Mapping [RFC5733] 'serverUpdateProhibited' status.", + "server hold": "The server set the status so that DNS delegation information MUST NOT be published for the object. This maps to the Extensible Provisioning Protocol (EPP) Domain Name Mapping [RFC5731] 'serverHold' status.", + "transfer period": "This grace period is provided after the successful transfer of object registration sponsorship from one client to another client. If the object is deleted by the client during this period, the server provides a credit to the client for the cost of the transfer. This maps to the Domain Registry Grace Period Mapping for the Extensible Provisioning Protocol (EPP) [RFC3915] 'transferPeriod' status." +}; + +// list of RDAP bootstrap registry URLs +export const registryURLs: Record = { + "https://data.iana.org/rdap/asn.json": "autnum", + "https://data.iana.org/rdap/dns.json": "domain", + "https://data.iana.org/rdap/ipv4.json": "ip4", + "https://data.iana.org/rdap/ipv6.json": "ip6", + "https://data.iana.org/rdap/object-tags.json": "entity", +}; + +export const placeholders: Record = { + 'ip': '192.168.0.1/16', + 'autnum': '65535', + 'entity': 'ABC123-EXAMPLE', + 'url': 'https://rdap.org/domain/example.com', + 'tld': 'example', + 'registrar': '9999', + 'json': '{ (paste JSON) }', + 'domain': 'example.com' +} \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..c787ac4 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,5 @@ +export function truthy(value: string | null | undefined) { + if (value == undefined) return false; + return value.toLowerCase() == 'true' || value == '1'; +} + diff --git a/src/pages/index.tsx b/src/pages/index.tsx index be7e2dc..7da42cd 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,11 +1,351 @@ -import {NextPage} from "next"; +import {type NextPage} from "next"; +import Head from "next/head"; +import type {Uri} from "../types"; +import {placeholders} from "../constants"; +import {asnMatch, domainMatch, entityMatch, getBestURL, getRDAPURL, getType, ipMatch, showSpinner} from "../rdap"; +import {useEffect, useState} from "react"; +import {truthy} from "../helpers"; +import {registryURLs} from "../constants"; +import axios, {AxiosResponse} from "axios"; +import update from "immutability-helper"; +import GenericObject, {Link} from "../components/DomainType"; const Index: NextPage = () => { - return
-
- This project is a work-in-progress. Check back soon. -
-
-} + const [uriType, setUriType] = useState('domain'); -export default Index; \ No newline at end of file + + const [requestJSContact, setRequestJSContact] = useState(false); + const [followReferral, setFollowReferral] = useState(false); + const [object, setObject] = useState(""); + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(null); + const [error, setError] = useState(null); + const [registryData, setRegistryData] = useState>({}); + + // Change the selected type automatically + useEffect(function () { + const new_type = getType(object); + if (new_type != null && new_type != uriType) + setUriType(new_type) + }, [object]); + + async function loadRegistryData() { + setLoading(true); + const responses = await Promise.all(Object.entries(registryURLs).map(async ([url, registryType]) => { + const response = await axios.get(url); + return { + registryType, + response: response.data + }; + })) + + setRegistryData(() => { + return Object.fromEntries( + responses.map(({registryType, response}) => [registryType, response.services]) + ) + }) + setLoading(false); + } + + // construct an RDAP URL for the given object + function getRDAPURL(object: string): string | null { + let urls: string[] = []; + + const service: [string[], string[]][] | [string[], string[], string[]][] = registryData[uriType]; + services: + for (const serviceItem of service) { + // special case for object tags, since the registrant email address is in the 0th position + const [rangeIndex, urlIndex] = uriType == 'entity' ? [1, 2] : [0, 1]; + + for (const tld of serviceItem[rangeIndex]!) { + let match = false; + + switch (uriType) { + case 'domain': + match = domainMatch(tld, object); + break; + // case "autnum": + // match = asnMatch(range, object); + // break; + // case "entity": + // match = entityMatch(range, object); + // break; + // case "ip": + // match = ipMatch(range, object); + // break; + } + + if (match) { + urls = serviceItem[urlIndex]!; + break services; + } + } + } + + + // no match + if (urls.length == 0) return null; + + let url = getBestURL(urls); + // some bootstrap entries have a trailing slash, some don't + if (!url.endsWith('/')) url += '/'; + return `${url + uriType}/${object}`; + } + + + function submit(e) { + e?.preventDefault(); + + const queryParams = requestJSContact ? '?jscard=1' : ''; + + const url = (function () { + switch (uriType) { + case 'url': + return object; + case 'tld': + return `https://root.rdap.org/domain/${object}${queryParams}`; + case 'registrar': + return `https://registrars.rdap.org/entity/${object}-IANA${queryParams}`; + case 'json': + return `json://${object}` + case 'domain': + const temp = getRDAPURL(object); + if (temp) return `${temp}${queryParams}` + return null; + default: + setError(`No RDAP URL available for ${uriType} ${object}.`); + return null; + } + })() + + if (url) sendQuery(url, followReferral); + } + + async function sendQuery(url: string, followReferral = false) { + setLoading(true); + + if (url.startsWith('json://')) { + // run the callback with a mock XHR + await handleResponse(JSON.parse(url.substring(7))) + } else { + try { + const response = await axios.get(url, {responseType: "json"}) + if (response.status == 404) + setError('This object does not exist.'); + else if (response.status != 200) + setError(`Error ${response.status}: ${response.statusText}`) + await handleResponse(response, followReferral) + } catch (e) { + setLoading(false); + setError(e.toString()) + } + } + } + + // callback executed when a response is received + async function handleResponse(data: { links: Link[] }, followReferral = false) { + setLoading(false); + + if (followReferral && data.links != null) { + for (const link of data.links) { + if ('related' == link.rel && 'application/rdap+json' == link.type && link.href.match(/^(https?:|)\/\//i)) { + await sendQuery(link.href, false) + return; + } + } + } + + try { + // div.appendChild(processObject(xhr.response, true)); + setResponse(data); + const url = `${window.location.href}?type=${encodeURIComponent(uriType)}&object=${object}&request-jscontact=${requestJSContact ? 1 : 0}&follow-referral=${followReferral ? 1 : 0}` + window.history.pushState(null, document.title, url); + + } catch (e) { + setError(`Exception: ${e.message} (line ${e.lineNumber})`); + } + } + + useEffect(() => { + // Load parameters from URL query string on page load + const params = new URLSearchParams(window.location.search); + if (params.has('type')) + setUriType(params.get('type') as Uri); + else if (params.has('object')) + setObject(params.get('object')!); + + if (params.has('request-jscontact') && truthy(params.get('request-jscontact'))) + setRequestJSContact(true); + + if (params.has('follow-referral') && truthy(params.get('follow-referral'))) + setFollowReferral(true); + + if (params.has('object') && (params.get('object')?.length ?? 0) > 0) { + setObject(params.get('object')!); + submit(null); + } + + + loadRegistryData().catch(console.error); + }, []) + + useEffect(() => { + if (!loading && registryData.domain != undefined) + console.log(registryData); + }, [loading]) + + return ( + <> + + rdap.xevion.dev + + + + + + <> + + + +
+ +
+
+
+
+ +
+ +
+ + { + setObject(e.target.value); + }} required/> + +
+ +
+
+
+
+ +
+
+ Options:  + +   + +
+
+ +
+ {response != null ? : null} +
+ +

This page implements a completely private lookup tool for domain names, IP addresses and + Autonymous System Numbers (ASNs). Only the relevant registry sees your query: your browser will + directly + connect to the registry's RDAP server using an encrypted HTTPS connection to protect the + confidentiality of + your query. If you click the "Follow referral to registrar's RDAP + record" checkbox, then the + sponsoring + registrar may also see your query.

+
    +
  • Click here for more information about + what RDAP is + and how it differs from traditional Whois. +
  • +
  • Most generic TLDs now support RDAP, but only a few ccTLDs have deployed RDAP so far. To see + which TLDs + support RDAP, click here. +
  • +
  • There is no bootstrap registry for top-level domains or ICANN-accredited registrars; instead + these queries are sent to the + + {"{"}root,registrars{"}"}.rdap.org servers + . +
  • +
  • To submit feedback, click here. Please contact the + relevant + registry or registrar if you have an issue with the content of an RDAP response. +
  • +
  • This tool is Free Software; for the license, click here. To fork a + copy of the git + repository, click + here. +
  • +
  • This page uses ipaddr.js by whitequark. +
  • +
+ +
+ + + ); +}; + +export default Index; diff --git a/src/rdap.ts b/src/rdap.ts new file mode 100644 index 0000000..e5ef314 --- /dev/null +++ b/src/rdap.ts @@ -0,0 +1,740 @@ +import ipaddr from "ipaddr.js"; +import {registryURLs, rdapStatusInfo, placeholders} from "./constants"; +import type {Uri} from "./types"; + +// keeps track of how many registries we've loaded +let loadedRegistries = 0; + +// registry data is stored in this +let registryData = {}; + +// keeps track of the elements we've created so we can assign a unique ID +let elementCounter = 123456; + +const cardTitles = { + "domain": "Domain Name", + "ip network": "IP Network", + "nameserver": "Nameserver", + "entity": "Entity", + "autnum": "AS Number", +}; + +export function domainMatch(tld: string, domain: string): boolean { + return domain.toUpperCase().endsWith(`.${tld.toUpperCase()}`); +} + +export function asnMatch(range, asn) { + var [min, max] = range.split('-', 2); + min = parseInt(min); + max = parseInt(max); + + return (asn >= min && asn <= max); +} + +export function entityMatch(tag: string, handle: string) { + return handle.toUpperCase().endsWith('-' + tag.toUpperCase()); +} + +export function ipMatch(prefix: string, ip: string) { + var parsedIp = ipaddr.parse(ip); + let cidr = ipaddr.parseCIDR(prefix); + return (parsedIp.kind() == cidr[0].kind() && parsedIp.match(cidr)); +} + +// return the first HTTPS url, or the first URL +export function getBestURL(urls: string[]): string { + urls.forEach((url) => { + if (url.startsWith('https://')) + return url; + }) + return urls[0]!; +} + +// given a URL, injects that URL into the query input, +// and initiates an RDAP query +export function runQuery(url) { + var type = document.getElementById('type'); + + for (var i = 0; i < type.options.length; i++) if ('url' == type.options[i].value) type.selectedIndex = i; + document.getElementById('object').value = url; + doQuery(); +} + +export function showSpinner(msg) { + msg = msg ? msg : 'Loading...'; + + var div = document.getElementById('output-div'); + div.innerHTML = ''; + + var spinner = document.createElement('div'); + spinner.classList.add('spinner-border'); + spinner.role = 'status'; + var span = spinner.appendChild(document.createElement('span')); + span.classList.add('sr-only'); + span.appendChild(document.createTextNode(msg)); + + div.appendChild(spinner); + + var msgDiv = document.createElement('div'); + msgDiv.id = 'spinner-msg'; + msgDiv.appendChild(document.createTextNode(msg)); + div.appendChild(msgDiv); +} + +// export function handleError(error) { +// var div = document.getElementById('output-div'); +// div.innerHTML = ''; +// div.appendChild(createErrorNode(error)); +// } + +export function createErrorNode(error) { + el = document.createElement('p'); + el.classList.add('error', 'alert', 'alert-warning'); + el.appendChild(document.createTextNode(error)); + + return el; +} + +// process an RDAP object. Argument is a JSON object, return +// value is an element that can be inserted into the page +export function processObject(object, toplevel, followReferral = true) { + if (!object) { + console.log(object); + return false; + } + + var dl = document.createElement('dl'); + + switch (object.objectClassName) { + case 'domain': + processDomain(object, dl, toplevel); + break; + + case 'nameserver': + processNameserver(object, dl, toplevel); + break; + + case 'entity': + processEntity(object, dl, toplevel); + break; + + case 'autnum': + processAutnum(object, dl, toplevel); + break; + + case 'ip network': + processIp(object, dl, toplevel); + break; + + default: + if (object.errorCode) { + return createErrorNode(object.errorCode + ' error: ' + object.title); + + } else { + processUnknown(object, dl, toplevel); + + } + } + + var card = document.createElement('div'); + card.classList.add('card'); + + var titleText = ''; + if (object.unicodeName) { + titleText = object.unicodeName.toUpperCase(); + + } else if (object.ldhName) { + titleText = object.ldhName.toUpperCase(); + + } else if (object.handle) { + titleText = object.handle.toUpperCase(); + + } + + if (object.handle && object.handle != titleText) titleText += ' (' + object.handle + ')'; + + if (titleText.length > 0) { + titleText = cardTitles[object.objectClassName] + ' ' + titleText; + + } else if (!toplevel) { + titleText = cardTitles[object.objectClassName]; + + } else { + titleText = 'Response'; + + } + + var title = document.createElement('div'); + title.classList.add('card-header', 'font-weight-bold'); + title.appendChild(document.createTextNode(titleText)); + card.appendChild(title); + + var body = document.createElement('div'); + body.classList.add('card-body'); + + body.appendChild(dl); + + card.appendChild(body); + return card; +} + +// simplify the process of adding a name => value to a definition list +export function addProperty(dl, name, value) { + + var dt = document.createElement('dt'); + dt.classList.add('rdap-property-name'); + dt.appendChild(document.createTextNode(name)); + dl.appendChild(dt); + + var dd = document.createElement('dd'); + dd.classList.add('rdap-property-value'); + if (value instanceof Node) { + dd.appendChild(value); + + } else { + dd.appendChild(document.createTextNode(String(value))); + + } + dl.appendChild(dd); +} + +// called by the individual object processors, since all RDAP objects have a similar set of +// properties. the first argument is the RDAP object and the second is the
element +// being used to display that object. +export function processCommonObjectProperties(object, dl) { + // if (object.objectClassName) addProperty(dl, 'Object Type:', object.objectClassName); + // if (object.handle) addProperty(dl, 'Handle:', object.handle); + if (object.status) processStatus(object.status, dl); + if (object.events) processEvents(object.events, dl); + if (object.entities) processEntities(object.entities, dl); + if (object.remarks) processRemarks(object.remarks, dl); + if (object.notices) processNotices(object.notices, dl); + if (object.links) processLinks(object.links, dl); + if (object.lang) addProperty(dl, 'Language:', object.lang); + if (object.port43) addProperty(dl, 'Whois Server:', object.port43); + if (object.rdapConformance) processrdapConformance(object.rdapConformance, dl); + + var div = document.createElement('div'); + div.id = 'element-' + ++elementCounter; + + var button = document.createElement('button'); + button.classList.add('btn', 'btn-secondary'); + button.appendChild(document.createTextNode('Show')); + button.onclick = new Function('showRawData("' + div.id + '");return false'); + div.appendChild(button); + + var pre = document.createElement('pre'); + pre.style = 'display:none;visibility:hidden'; + pre.appendChild(document.createTextNode(JSON.stringify(object, null, 2))); + div.appendChild(pre); + + addProperty(dl, 'Raw Data:', div); +} + +// call back for "Show Raw Data" button +export function showRawData(id) { + var div = document.getElementById(id); + div.childNodes[0].style = 'display:none;visibility:hidden'; + div.childNodes[1].style = 'display:block;visibility:visible'; +} + +// convert an array into a bulleted list +export function createList(list) { + var ul = document.createElement('ul'); + + for (var i = 0; i < list.length; i++) { + var li = document.createElement('li'); + if (list[i] instanceof Node) { + li.appendChild(list[i]); + + } else { + li.appendChild(document.createTextNode(list[i])); + + } + ul.appendChild(li); + } + + return ul; +} + +// add the RDAP conformance of the response +export function processrdapConformance(rdapConformance, dl) { + addProperty(dl, 'Conformance:', createList(rdapConformance)); +} + +// add the object's status codes +export function processStatus(status, dl) { + var s = new Array; + for (var i = 0; i < status.length; i++) { + var span = document.createElement('span'); + span.classList.add('rdap-status-code'); + span.appendChild(document.createTextNode(status[i])); + span.setAttribute("title", rdapStatusInfo[status[i]]); + s.push(span); + } + addProperty(dl, 'Status:', createList(s)); +} + +// add the object's events +export function processEvents(events, dl) { + var sdl = document.createElement('dl'); + + for (var i = 0; i < events.length; i++) { + var span1 = document.createElement('span'); + span1.appendChild(document.createTextNode(new Date(events[i].eventDate).toLocaleString())); + span1.classList.add('rdap-event-time'); + span1.setAttribute('title', events[i].eventDate); + + var span2 = document.createElement('span'); + span2.appendChild(span1); + + if (events[i].eventActor) { + span2.appendChild(document.createTextNode(' (by ' + events[i].eventActor + ')')); + } + addProperty(sdl, events[i].eventAction + ':', span2); + } + + addProperty(dl, 'Events:', sdl); +} + +// add the object's links +export function processLinks(links, dl) { + var ul = document.createElement('ul'); + + for (var i = 0; i < links.length; i++) { + li = document.createElement('li'); + + var title = (links[i].title ? links[i].title : links[i].href); + + var link; + if (links[i].type && 0 == links[i].type.indexOf('application/rdap+json')) { + link = createRDAPLink(links[i].href, title); + + } else { + link = document.createElement('a'); + link.rel = 'noopener'; + link.title = link.href = links[i].href; + link.target = '_new'; + link.appendChild(document.createTextNode(title)); + + } + + li.appendChild(link); + + if (links[i].rel) li.appendChild(document.createTextNode(' (' + links[i].rel + ')')); + + ul.appendChild(li); + } + + addProperty(dl, 'Links:', ul); +} + +// add the object's entities +export function processEntities(entities, dl) { + var div = document.createElement('div'); + + for (var i = 0; i < entities.length; i++) div.appendChild(processObject(entities[i])); + + addProperty(dl, 'Entities:', div); +} + +// add the object's remarks +export function processRemarks(remarks, dl) { + addProperty(dl, 'Remarks:', processRemarksOrNotices(remarks)); + +} + +// add the responses's notices +export function processNotices(notices, dl) { + addProperty(dl, 'Notices:', processRemarksOrNotices(notices)); +} + +// command handler for remarks and notices +export function processRemarksOrNotices(things) { + var div = document.createElement('div'); + + for (var i = 0; i < things.length; i++) { + var section = document.createElement('section'); + section.classList.add('card'); + div.appendChild(section); + + var title = document.createElement('header'); + title.classList.add('card-header', 'font-weight-bold'); + title.appendChild(document.createTextNode(things[i].title)); + section.appendChild(title); + + var body = document.createElement('div'); + body.classList.add('card-body'); + section.appendChild(body); + + if (things[i].description) for (var j = 0; j < things[i].description.length; j++) { + var p = document.createElement('p'); + p.innerHTML = convertURLstoLinks(things[i].description[j]); + body.appendChild(p); + } + + if (things[i].links) { + var ldl = document.createElement('dl'); + processLinks(things[i].links, ldl); + body.appendChild(ldl); + } + } + + return div; +} + +// naively match URLs in plain text and convert to links +export function convertURLstoLinks(str) { + return str.replace( + /(https?:\/\/[^\s]+[^\.])/g, + '$1' + ); +} + +// process a domain +export function processDomain(object, dl, toplevel = false) { + + if (toplevel) document.title = 'Domain ' + (object.unicodeName ? object.unicodeName : object.ldhName).toUpperCase() + ' - RDAP Lookup'; + + if (object.unicodeName) { + addProperty(dl, 'Name:', object.unicodeName); + addProperty(dl, 'ASCII Name:', object.ldhName); + + } else { + addProperty(dl, 'Name:', object.ldhName); + + } + + if (object.handle) addProperty(dl, 'Handle:', object.handle); + + // process events, status and entities here, then set them to null so processCommonObjectProperties() + // doesn't process them again. this makes the output look more like a traditional whois record: + if (object.events) processEvents(object.events, dl); + if (object.status) processStatus(object.status, dl); + if (object.entities) processEntities(object.entities, dl); + + object.events = object.status = object.entities = null; + + if (object.nameservers) { + var div = document.createElement('div'); + + for (var i = 0; i < object.nameservers.length; i++) div.appendChild(processObject(object.nameservers[i])); + + addProperty(dl, 'Nameservers:', div); + } + + addProperty(dl, 'DNSSEC:', object.secureDNS && object.secureDNS.delegationSigned ? 'Secure' : 'Insecure'); + + processCommonObjectProperties(object, dl); +} + +// process a nameserver +export function processNameserver(object, dl, toplevel = false) { + + if (toplevel) document.title = 'Nameserver ' + object.ldhName + ' - RDAP Lookup'; + + addProperty(dl, 'Host Name:', object.ldhName); + if (object.unicodeName) addProperty(dl, 'Internationalised Domain Name:', object.unicodeName); + if (object.handle) addProperty(dl, 'Handle:', object.handle); + + if (object.ipAddresses) { + if (object.ipAddresses.v4) { + for (var i = 0; i < object.ipAddresses.v4.length; i++) { + addProperty(dl, 'IP Address:', createRDAPLink('https://rdap.org/ip/' + object.ipAddresses.v4[i], object.ipAddresses.v4[i])); + } + } + + if (object.ipAddresses.v6) { + for (var i = 0; i < object.ipAddresses.v6.length; i++) { + addProperty(dl, 'IP Address:', createRDAPLink('https://rdap.org/ip/' + object.ipAddresses.v6[i], object.ipAddresses.v6[i])); + } + } + } + + processCommonObjectProperties(object, dl); +} + +// process an entity +export function processEntity(object, dl, toplevel = false) { + + if (toplevel) document.title = 'Entity ' + object.handle + ' - RDAP Lookup'; + + if (object.handle) addProperty(dl, 'Handle:', object.handle); + + if (object.publicIds) { + for (var i = 0; i < object.publicIds.length; i++) addProperty(dl, object.publicIds[i].type + ':', object.publicIds[i].identifier); + } + + if (object.roles) addProperty(dl, 'Roles:', createList(object.roles)); + + if (object.jscard) { + addProperty(dl, 'Contact Information:', processJSCard(object.jscard)); + + } else if (object.jscard_0) { + addProperty(dl, 'Contact Information:', processJSCard(object.jscard_0)); + + } else if (object.vcardArray && object.vcardArray[1]) { + addProperty(dl, 'Contact Information:', processVCardArray(object.vcardArray[1])); + } + + processCommonObjectProperties(object, dl); +} + +// process an entity's vCard +export function processVCardArray(vcard) { + var vdl = document.createElement('dl'); + + for (var i = 0; i < vcard.length; i++) { + var node = vcard[i]; + + var type = node[0]; + var value = node[3]; + + if ('version' == type) { + continue; + + } else if ('fn' == type) { + type = 'Name'; + + } else if ('n' == type) { + continue; + + } else if ('org' == type) { + type = 'Organization'; + + } else if ('tel' == type) { + type = 'Phone'; + + if (node[1].type) for (var j = 0; j < node[1].type; j++) if ('fax' == node[1].type[j]) { + type = 'Fax'; + break; + } + + var link = document.createElement('a'); + link.href = (0 == value.indexOf('tel:') ? '' : 'tel:') + value; + link.appendChild(document.createTextNode(value)); + + value = link; + + } else if ('adr' == type) { + type = 'Address'; + + if (node[1].label) { + var div = document.createElement('div'); + strings = node[1].label.split("\n"); + for (var j = 0; j < strings.length; j++) { + div.appendChild(document.createTextNode(strings[j])); + if (j < strings.length - 1) div.appendChild(document.createElement('br')); + } + + value = div; + + } else if (value) { + var div = document.createElement('div'); + + for (var j = 0; j < value.length; j++) { + if (value[j] && value[j].length > 0) { + div.appendChild(document.createTextNode(value[j])); + div.appendChild(document.createElement('br')); + } + } + + value = div; + } + + } else if ('email' == type) { + type = 'Email'; + + var link = document.createElement('a'); + link.href = 'mailto:' + value; + link.appendChild(document.createTextNode(value)); + + value = link; + + } else if ('contact-uri' == type) { + type = 'Contact URL'; + + var link = document.createElement('a'); + link.href = value; + link.appendChild(document.createTextNode(value)); + + value = link; + } + + if (value) addProperty(vdl, type + ':', value); + } + + addProperty(vdl, 'Contact format:', 'jCard'); + + return vdl; +} + +export function processJSCard(jscard) { + var vdl = document.createElement('dl'); + + if (jscard.fullName) addProperty(vdl, 'Name:', jscard.fullName); + + if (jscard.organizations) { + for (const k in jscard.organizations) { + addProperty(vdl, 'Organization:', jscard.organizations[k].name); + } + } + + if (jscard.addresses) { + for (const k in jscard.addresses) { + addProperty(vdl, 'Address:', processJSCardAddress(jscard.addresses[k])); + } + } + + if (jscard.emails) { + for (const k in jscard.emails) { + var link = document.createElement('a'); + link.href = 'mailto:' + jscard.emails[k].email; + link.appendChild(document.createTextNode(jscard.emails[k].email)); + + addProperty(vdl, 'Email Address:', link); + } + } + + if (jscard.phones) { + for (const k in jscard.phones) { + var link = document.createElement('a'); + link.href = jscard.phones[k].phone; + link.appendChild(document.createTextNode(jscard.phones[k].phone)); + + addProperty(vdl, (jscard.phones[k].features.fax ? 'Fax:' : 'Phone:'), link); + } + } + + addProperty(vdl, 'Contact format:', 'JSContact'); + + return vdl; +} + +export function processJSCardAddress(address) { + var dl = document.createElement('dl'); + for (k in address) { + v = address[k]; + if ('street' == k) { + var addr = document.createElement('span'); + for (var i = 0; i < v.length; i++) { + if (i > 1) addr.appendChild(document.createElement('br')); + addr.appendChild(document.createTextNode(v[i])); + } + addProperty(dl, 'Street:', addr); + + } else if ('locality' == k) { + addProperty(dl, 'City:', v); + + } else if ('region' == k) { + addProperty(dl, 'State/Province:', v); + + } else if ('postcode' == k) { + addProperty(dl, 'Postal Code:', v); + + } else if ('countryCode' == k) { + addProperty(dl, 'Country:', v); + + } + } + return dl; +} + +// process an AS number +export function processAutnum(object, dl, toplevel = false) { + + if (toplevel) document.title = 'AS Number ' + object.handle + ' - RDAP Lookup'; + + if (object.name) addProperty(dl, 'Network Name:', object.name); + if (object.type) addProperty(dl, 'Network Type:', object.type); + + processCommonObjectProperties(object, dl); +} + +// process an IP or IP block +export function processIp(object, dl, toplevel = false) { + + if (toplevel) document.title = 'IP Network ' + object.handle + ' - RDAP Lookup'; + + if (object.ipVersion) addProperty(dl, 'IP Version:', object.ipVersion); + if (object.startAddress && object.endAddress) addProperty(dl, 'Address Range:', object.startAddress + ' - ' + object.endAddress); + if (object.name) addProperty(dl, 'Network Name:', object.name); + if (object.type) addProperty(dl, 'Network Type:', object.type); + if (object.country) addProperty(dl, 'Country:', object.country); + if (object.parentHandle) addProperty(dl, 'Parent Network:', object.parentHandle); + if (object.cidr0_cidrs) addProperty(dl, 'CIDR Prefix(es):', processCIDRs(object.cidr0_cidrs)); + + processCommonObjectProperties(object, dl); +} + +export function processCIDRs(cidrs) { + var list = document.createElement('ul'); + for (i = 0; i < cidrs.length; i++) { + var cidr = (cidrs[i].v6prefix ? cidrs[i].v6prefix : cidrs[i].v4prefix) + '/' + cidrs[i].length; + list.appendChild(document.createElement('li')).appendChild(createRDAPLink('https://rdap.org/ip/' + cidr, cidr)); + } + return list; +} + +export function processUnknown(object, dl, toplevel = false) { + processCommonObjectProperties(object, dl); +} + +// given an object, return the "self" URL (if any) +export function getSelfLink(object) { + if (object.links) for (var i = 0; i < object.links.length; i++) if ('self' == object.links[i].rel) return object.links[i].href; + + return null; +} + +// create an RDAP link: a link pointing to an RDAP URL +// that when clicked, causes an RDAP query to be made +export function createRDAPLink(url, title) { + var link = document.createElement('a'); + + link.href = 'javascript:void(0)'; + link.title = url; + link.onclick = new Function("runQuery('" + url + "')"); + link.appendChild(document.createTextNode(title)); + + return link; +} + +const URIPatterns: [RegExp, Uri][] = [ + [/^\d+$/, "autnum"], + [/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/?\d*$/, "ip"], + [/^[0-9a-f:]{2,}\/?\d*$/, "ip"], + [/^https?:/, "url"], + [/^{/, "json"], + [/./, "domain"], +]; + +// guess the type from the input value +export function getType(value: string): Uri | null { + for (let i = 0; i < URIPatterns.length; i++) + if (URIPatterns[i]![0].test(value)) { + return URIPatterns[i]![1]; + } + return null; +} + +export function loadRegistries(callback) { + showSpinner('Loading bootstrap registries...'); + for (const url in registryURLs) { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + + xhr.timeout = 25000; + xhr.responseType = 'json'; + xhr.onload = () => handleRegistryResponse(xhr); + + xhr.send(); + } +} + + +// event handler for when the submit button is pressed, or +// when the user clicks on a link to an RDAP URL +export function doQuery() { + +} \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index 333c72b..6b17b97 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2,4 +2,8 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +[title]:not(.no-title) { + border-bottom: 1px dashed silver; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..c587aeb --- /dev/null +++ b/src/types.ts @@ -0,0 +1,9 @@ +export type Uri = 'ip' | 'autnum' | 'entity' | 'url' | 'tld' | 'registrar' | 'json' | 'domain'; +export type ExtendedUri = Omit | 'ip4' | 'ip6'; +export type RdapStatusType = "validated" | "renew prohibited" | "update prohibited" | "transfer prohibited" + | "delete prohibited" | "proxy" | "private" | "removed" | "obscured" | "associated" | "active" | "inactive" + | "locked" | "pending create" | "pending renew" | "pending transfer" | "pending update" | "pending delete" + | "add period" | "auto renew period" | "client delete prohibited" | "client hold" | "client renew prohibited" + | "client transfer prohibited" | "client update prohibited" | "pending restore" | "redemption period" + | "renew period" | "server delete prohibited" | "server renew prohibited" | "server transfer prohibited" + | "server update prohibited" | "server hold" | "transfer period"; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 815b3ff..8c775d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1342,6 +1342,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immutability-helper@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-3.1.1.tgz#2b86b2286ed3b1241c9e23b7b21e0444f52f77b7" + integrity sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"