mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-11 08:08:18 -06:00
refactor: reorganize project structure and consolidate network utilities
Major restructuring to improve codebase organization: - Moved test files to src/__tests__/ directory - Reorganized UI components from src/components/common to src/components/ui - Consolidated RDAP-related code into src/rdap/ directory structure - Split network helpers into modular files (asn.ts, ipv4.ts, ipv6.ts) - Created centralized exports via src/lib/network/index.ts - Migrated utility functions from src/helpers.ts to src/lib/utils.ts - Separated RDAP services into dedicated modules (rdap-api.ts, registry.ts, url-resolver.ts) This refactoring enhances code maintainability and follows a clearer separation of concerns.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { asnInRange } from "./helpers";
|
||||
import { asnInRange } from "@/lib/network";
|
||||
|
||||
describe("asnInRange", () => {
|
||||
describe("basic matching", () => {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { ipv4InCIDR, ipv6InCIDR } from "./helpers";
|
||||
import { ipv4InCIDR, ipv6InCIDR } from "@/lib/network";
|
||||
|
||||
describe("ipv4InCIDR", () => {
|
||||
describe("basic matching", () => {
|
||||
@@ -1,8 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getType } from "./rdap";
|
||||
import type { Register, RootRegistryType } from "./types";
|
||||
import { registryURLs } from "./constants";
|
||||
import { getType } from "@/rdap/utils";
|
||||
import type { Register, RootRegistryType } from "@/rdap/schemas";
|
||||
import { registryURLs } from "@/rdap/constants";
|
||||
|
||||
// Integration tests that fetch real IANA bootstrap data
|
||||
// These are slower but test against actual registries
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { getType } from "./rdap";
|
||||
import type { Register } from "./types";
|
||||
import { getType } from "@/rdap/utils";
|
||||
import type { Register } from "@/rdap/schemas";
|
||||
|
||||
// Mock registry getter (matches real IANA structure: [email, tags, urls])
|
||||
const mockRegistry: Register = {
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import type { FunctionComponent } from "react";
|
||||
import { useState } from "react";
|
||||
import { onPromise, preventDefault } from "@/helpers";
|
||||
import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/types";
|
||||
import { TargetTypeEnum } from "@/schema";
|
||||
import { onPromise, preventDefault } from "@/lib/utils";
|
||||
import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/rdap/schemas";
|
||||
import { TargetTypeEnum } from "@/rdap/schemas";
|
||||
import { MagnifyingGlassIcon, ReloadIcon, LockClosedIcon } from "@radix-ui/react-icons";
|
||||
import { TextField, Select, Flex, Checkbox, Text, IconButton } from "@radix-ui/themes";
|
||||
import type { Maybe } from "true-myth";
|
||||
import { placeholders } from "@/constants";
|
||||
import { placeholders } from "@/rdap/constants";
|
||||
|
||||
/**
|
||||
* Props for the LookupInput component.
|
||||
|
||||
163
src/helpers.ts
163
src/helpers.ts
@@ -1,163 +0,0 @@
|
||||
import type { SyntheticEvent } from "react";
|
||||
|
||||
declare global {
|
||||
interface ObjectConstructor {
|
||||
entries<T extends object>(o: T): [keyof T, T[keyof T]][];
|
||||
}
|
||||
}
|
||||
|
||||
export function truthy(value: string | null | undefined) {
|
||||
if (value == undefined) return false;
|
||||
return value.toLowerCase() == "true" || value == "1";
|
||||
}
|
||||
|
||||
export function onPromise<T>(promise: (event: SyntheticEvent) => Promise<T>) {
|
||||
return (event: SyntheticEvent) => {
|
||||
if (promise) {
|
||||
promise(event).catch((error) => {
|
||||
console.log("Unexpected error", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a string dynamically to ensure maxLength is not exceeded & an ellipsis is used.
|
||||
* Behavior undefined when ellipsis exceeds {maxLength}.
|
||||
* @param input The input string
|
||||
* @param maxLength A positive number representing the maximum length the input string should be.
|
||||
* @param ellipsis A string representing what should be placed on the end when the max length is hit.
|
||||
*/
|
||||
export function truncated(input: string, maxLength: number, ellipsis = "...") {
|
||||
if (maxLength <= 0) return "";
|
||||
if (input.length <= maxLength) return input;
|
||||
return input.substring(0, Math.max(0, maxLength - ellipsis.length)) + ellipsis;
|
||||
}
|
||||
|
||||
export function preventDefault(event: SyntheticEvent | Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IPv4 address string to a 32-bit integer
|
||||
*/
|
||||
function ipv4ToInt(ip: string): number {
|
||||
const parts = ip.split(".").map(Number);
|
||||
if (parts.length !== 4) return 0;
|
||||
const [a, b, c, d] = parts;
|
||||
if (a === undefined || b === undefined || c === undefined || d === undefined) return 0;
|
||||
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IPv4 address falls within a CIDR range
|
||||
* @param ip The IP address to check (e.g., "192.168.1.1")
|
||||
* @param cidr The CIDR range to check against (e.g., "192.168.0.0/16")
|
||||
* @returns true if the IP is within the CIDR range
|
||||
*/
|
||||
export function ipv4InCIDR(ip: string, cidr: string): boolean {
|
||||
const [rangeIp, prefixLenStr] = cidr.split("/");
|
||||
const prefixLen = parseInt(prefixLenStr ?? "", 10);
|
||||
|
||||
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Special case: /0 matches all IPs
|
||||
if (prefixLen === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ipInt = ipv4ToInt(ip);
|
||||
const rangeInt = ipv4ToInt(rangeIp);
|
||||
const mask = (0xffffffff << (32 - prefixLen)) >>> 0;
|
||||
|
||||
return (ipInt & mask) === (rangeInt & mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IPv6 address to a BigInt representation
|
||||
*/
|
||||
function ipv6ToBigInt(ip: string): bigint {
|
||||
// Expand :: notation
|
||||
const expandedIp = expandIPv6(ip);
|
||||
const parts = expandedIp.split(":");
|
||||
|
||||
let result = BigInt(0);
|
||||
for (const part of parts) {
|
||||
result = (result << BigInt(16)) | BigInt(parseInt(part, 16));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand IPv6 address shorthand notation
|
||||
*/
|
||||
function expandIPv6(ip: string): string {
|
||||
if (ip.includes("::")) {
|
||||
const [left, right] = ip.split("::");
|
||||
const leftParts = left ? left.split(":") : [];
|
||||
const rightParts = right ? right.split(":") : [];
|
||||
const missingParts = 8 - leftParts.length - rightParts.length;
|
||||
const middleParts: string[] = Array(missingParts).fill("0") as string[];
|
||||
const allParts = [...leftParts, ...middleParts, ...rightParts];
|
||||
return allParts.map((p: string) => p.padStart(4, "0")).join(":");
|
||||
}
|
||||
return ip
|
||||
.split(":")
|
||||
.map((p: string) => p.padStart(4, "0"))
|
||||
.join(":");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IPv6 address falls within a CIDR range
|
||||
* @param ip The IPv6 address to check (e.g., "2001:db8::1")
|
||||
* @param cidr The CIDR range to check against (e.g., "2001:db8::/32")
|
||||
* @returns true if the IP is within the CIDR range
|
||||
*/
|
||||
export function ipv6InCIDR(ip: string, cidr: string): boolean {
|
||||
const [rangeIp, prefixLenStr] = cidr.split("/");
|
||||
const prefixLen = parseInt(prefixLenStr ?? "", 10);
|
||||
|
||||
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 128) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const ipInt = ipv6ToBigInt(ip);
|
||||
const rangeInt = ipv6ToBigInt(rangeIp);
|
||||
const maxMask = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
|
||||
const mask = (maxMask << BigInt(128 - prefixLen)) & maxMask;
|
||||
|
||||
return (ipInt & mask) === (rangeInt & mask);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ASN falls within a range
|
||||
* @param asn The ASN number to check (e.g., 13335 for Cloudflare)
|
||||
* @param range The range to check against (e.g., "13312-18431")
|
||||
* @returns true if the ASN is within the range
|
||||
*/
|
||||
export function asnInRange(asn: number, range: string): boolean {
|
||||
const parts = range.split("-");
|
||||
|
||||
if (parts.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const start = parseInt(parts[0] ?? "", 10);
|
||||
const end = parseInt(parts[1] ?? "", 10);
|
||||
|
||||
if (isNaN(start) || isNaN(end) || start < 0 || end < 0 || start > end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (asn < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return asn >= start && asn <= end;
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { domainMatchPredicate, getBestURL, getType } from "@/rdap";
|
||||
import type {
|
||||
AutonomousNumber,
|
||||
Domain,
|
||||
IpNetwork,
|
||||
Register,
|
||||
RootRegistryType,
|
||||
SubmitProps,
|
||||
TargetType,
|
||||
} from "@/types";
|
||||
import { registryURLs } from "@/constants";
|
||||
import {
|
||||
AutonomousNumberSchema,
|
||||
DomainSchema,
|
||||
IpNetworkSchema,
|
||||
RegisterSchema,
|
||||
RootRegistryEnum,
|
||||
} from "@/schema";
|
||||
import { truncated, ipv4InCIDR, ipv6InCIDR, asnInRange } from "@/helpers";
|
||||
import type { ZodSchema } from "zod";
|
||||
import type { ParsedGeneric } from "@/components/lookup/Generic";
|
||||
import { Maybe, Result } from "true-myth";
|
||||
|
||||
export type WarningHandler = (warning: { message: string }) => void;
|
||||
export type MetaParsedGeneric = {
|
||||
data: ParsedGeneric;
|
||||
url: string;
|
||||
completeTime: Date;
|
||||
};
|
||||
|
||||
// An array of schemas to try and parse unknown JSON data with.
|
||||
const schemas = [DomainSchema, AutonomousNumberSchema, IpNetworkSchema];
|
||||
|
||||
const useLookup = (warningHandler?: WarningHandler) => {
|
||||
/**
|
||||
* A reference to the registry data, which is used to cache the registry data in memory.
|
||||
* This uses TargetType as the key, meaning v4/v6 IP/CIDR lookups are differentiated.
|
||||
*/
|
||||
const registryDataRef = useRef<Record<RootRegistryType, Register | null>>({
|
||||
autnum: null,
|
||||
domain: null,
|
||||
ip4: null,
|
||||
ip6: null,
|
||||
entity: null,
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [target, setTarget] = useState<string>("");
|
||||
const [uriType, setUriType] = useState<Maybe<TargetType>>(Maybe.nothing());
|
||||
|
||||
// Used by a callback on LookupInput to forcibly set the type of the lookup.
|
||||
const [currentType, setTargetType] = useState<TargetType | null>(null);
|
||||
|
||||
// Used to allow repeatable lookups when weird errors happen.
|
||||
const repeatableRef = useRef<string>("");
|
||||
|
||||
useCallback(async () => {
|
||||
if (currentType != null) return Maybe.just(currentType);
|
||||
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(Maybe.nothing(), (type) =>
|
||||
Maybe.just(type)
|
||||
);
|
||||
setUriType(uri);
|
||||
}, [target, currentType, getTypeEasy]);
|
||||
|
||||
// Fetch & load a specific registry's data into memory.
|
||||
async function loadBootstrap(type: RootRegistryType, force = false) {
|
||||
// Early preload exit condition
|
||||
if (registryDataRef.current[type] != null && !force) return;
|
||||
|
||||
// Fetch the bootstrapping file from the registry
|
||||
const response = await fetch(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(await response.json());
|
||||
if (!parsedRegister.success)
|
||||
throw new Error(`Could not parse IANA bootstrap response (type: ${type}).`);
|
||||
|
||||
// Set it in state so we can use it.
|
||||
registryDataRef.current = {
|
||||
...registryDataRef.current,
|
||||
[type]: parsedRegister.data,
|
||||
};
|
||||
}
|
||||
|
||||
async function getRegistry(type: RootRegistryType): Promise<Register> {
|
||||
if (registryDataRef.current[type] == null) await loadBootstrap(type);
|
||||
const registry = registryDataRef.current[type];
|
||||
if (registry == null)
|
||||
throw new Error(`Could not load bootstrap data for ${type} registry.`);
|
||||
return registry;
|
||||
}
|
||||
|
||||
async function getTypeEasy(target: string): Promise<Result<TargetType, Error>> {
|
||||
return getType(target, getRegistry);
|
||||
}
|
||||
|
||||
function getRegistryURL(type: RootRegistryType, lookupTarget: string): string {
|
||||
const bootstrap = registryDataRef.current[type];
|
||||
if (bootstrap == null)
|
||||
throw new Error(`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`);
|
||||
|
||||
let url: string | null = null;
|
||||
|
||||
typeSwitch: switch (type) {
|
||||
case "domain":
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
if (bootstrapItem[0].some(domainMatchPredicate(lookupTarget))) {
|
||||
// min length of 1 is validated in zod schema
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching domain found.`);
|
||||
case "ip4": {
|
||||
// Extract the IP address without CIDR suffix for matching
|
||||
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
// bootstrapItem[0] contains CIDR ranges like ["1.0.0.0/8", "2.0.0.0/8"]
|
||||
if (bootstrapItem[0].some((cidr) => ipv4InCIDR(ipAddress, cidr))) {
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching IPv4 registry found for ${lookupTarget}.`);
|
||||
}
|
||||
case "ip6": {
|
||||
// Extract the IP address without CIDR suffix for matching
|
||||
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
// bootstrapItem[0] contains CIDR ranges like ["2001:0200::/23", "2001:0400::/23"]
|
||||
if (bootstrapItem[0].some((cidr) => ipv6InCIDR(ipAddress, cidr))) {
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching IPv6 registry found for ${lookupTarget}.`);
|
||||
}
|
||||
case "autnum": {
|
||||
// Extract ASN number from "AS12345" format
|
||||
const asnMatch = lookupTarget.match(/^AS(\d+)$/i);
|
||||
if (!asnMatch || !asnMatch[1]) {
|
||||
throw new Error(`Invalid ASN format: ${lookupTarget}`);
|
||||
}
|
||||
|
||||
const asnNumber = parseInt(asnMatch[1], 10);
|
||||
if (isNaN(asnNumber)) {
|
||||
throw new Error(`Invalid ASN number: ${lookupTarget}`);
|
||||
}
|
||||
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
// bootstrapItem[0] contains ASN ranges like ["64512-65534", "13312-18431"]
|
||||
if (bootstrapItem[0].some((range) => asnInRange(asnNumber, range))) {
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching registry found for ${lookupTarget}.`);
|
||||
}
|
||||
case "entity":
|
||||
throw new Error(`No matching entity found.`);
|
||||
default:
|
||||
throw new Error("Invalid lookup target provided.");
|
||||
}
|
||||
|
||||
if (url == null) throw new Error("No lookup target was resolved.");
|
||||
|
||||
// Map internal types to RDAP endpoint paths
|
||||
// ip4 and ip6 both use the 'ip' endpoint in RDAP
|
||||
const rdapPath = type === "ip4" || type === "ip6" ? "ip" : type;
|
||||
|
||||
return `${url}${rdapPath}/${lookupTarget}`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const preload = async () => {
|
||||
if (uriType.isNothing) return;
|
||||
|
||||
const registryUri = RootRegistryEnum.safeParse(uriType.value);
|
||||
if (!registryUri.success) return;
|
||||
if (registryDataRef.current[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, uriType, warningHandler]);
|
||||
|
||||
async function getAndParse<T>(url: string, schema: ZodSchema<T>): Promise<Result<T, Error>> {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.status == 200) {
|
||||
const result = schema.safeParse(await response.json());
|
||||
|
||||
if (result.success === false) {
|
||||
// flatten the errors to make them more readable and simple
|
||||
const flatErrors = result.error.flatten(function (issue) {
|
||||
const path = issue.path.map((value) => value.toString()).join(".");
|
||||
return `${path}: ${issue.message}`;
|
||||
});
|
||||
|
||||
// combine them all, wrap them in a new error, and return it
|
||||
return Result.err(
|
||||
new Error(
|
||||
[
|
||||
"Could not parse the response from the registry.",
|
||||
...flatErrors.formErrors,
|
||||
...Object.values(flatErrors.fieldErrors).flat(),
|
||||
].join("\n\t")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(result.data);
|
||||
}
|
||||
|
||||
switch (response.status) {
|
||||
case 302:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that the resource requested is available at a different location."
|
||||
)
|
||||
);
|
||||
case 400:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that the request was malformed or could not be processed. Check that you typed in the correct information and try again."
|
||||
)
|
||||
);
|
||||
case 403:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that the request was forbidden. This could be due to rate limiting, abusive behavior, or other reasons. Try again later or contact the registry for more information."
|
||||
)
|
||||
);
|
||||
|
||||
case 404:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that the resource requested could not be found; the resource either does not exist, or is something that the registry does not track (i.e. this software queried incorrectly, which is unlikely)."
|
||||
)
|
||||
);
|
||||
case 500:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that an internal server error occurred. This could be due to a misconfiguration, a bug, or other reasons. Try again later or contact the registry for more information."
|
||||
)
|
||||
);
|
||||
default:
|
||||
return Result.err(
|
||||
new Error(`The registry did not return an OK status code: ${response.status}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitInternal(
|
||||
target: string
|
||||
): Promise<Result<{ data: ParsedGeneric; url: string }, Error>> {
|
||||
if (target == null || target.length == 0)
|
||||
return Result.err(new Error("A target must be given in order to execute a lookup."));
|
||||
|
||||
const targetType = await getTypeEasy(target);
|
||||
|
||||
if (targetType.isErr) {
|
||||
return Result.err(
|
||||
new Error("Unable to determine type, unable to send query", {
|
||||
cause: targetType.error,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
switch (targetType.value) {
|
||||
// Block scoped case to allow url const reuse
|
||||
case "ip4": {
|
||||
await loadBootstrap("ip4");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "ip6": {
|
||||
await loadBootstrap("ip6");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "domain": {
|
||||
await loadBootstrap("domain");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
|
||||
// HTTP
|
||||
if (url.startsWith("http://") && url != repeatableRef.current) {
|
||||
repeatableRef.current = url;
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry this domain belongs to uses HTTP, which is not secure. " +
|
||||
"In order to prevent a cryptic error from appearing due to mixed active content, " +
|
||||
"or worse, a CORS error, this lookup has been blocked. Try again to force the lookup."
|
||||
)
|
||||
);
|
||||
}
|
||||
const result = await getAndParse<Domain>(url, DomainSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "autnum": {
|
||||
await loadBootstrap("autnum");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
const result = await getAndParse<AutonomousNumber>(url, AutonomousNumberSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "tld": {
|
||||
// remove the leading dot
|
||||
const value = target.startsWith(".") ? target.slice(1) : target;
|
||||
const url = `https://root.rdap.org/domain/${value}`;
|
||||
const result = await getAndParse<Domain>(url, DomainSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "url": {
|
||||
const response = await fetch(target);
|
||||
|
||||
if (response.status != 200)
|
||||
return Result.err(
|
||||
new Error(
|
||||
`The URL provided returned a non-200 status code: ${response.status}.`
|
||||
)
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Try each schema until one works
|
||||
for (const schema of schemas) {
|
||||
const result = schema.safeParse(data);
|
||||
if (result.success) return Result.ok({ data: result.data, url: target });
|
||||
}
|
||||
|
||||
return Result.err(new Error("No schema was able to parse the response."));
|
||||
}
|
||||
case "json": {
|
||||
const data = JSON.parse(target);
|
||||
for (const schema of schemas) {
|
||||
const result = schema.safeParse(data);
|
||||
if (result.success) return Result.ok({ data: result.data, url: "" });
|
||||
}
|
||||
}
|
||||
case "registrar": {
|
||||
}
|
||||
default:
|
||||
return Result.err(new Error("The type detected has not been implemented."));
|
||||
}
|
||||
}
|
||||
|
||||
async function submit({ target }: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
|
||||
try {
|
||||
// target is already set in state, but it's also provided by the form callback, so we'll use it.
|
||||
const response = await submitInternal(target);
|
||||
|
||||
if (response.isErr) {
|
||||
setError(response.error.message);
|
||||
console.error(response.error);
|
||||
} else setError(null);
|
||||
|
||||
return response.isOk
|
||||
? Maybe.just({
|
||||
data: response.value.data,
|
||||
url: response.value.url,
|
||||
completeTime: new Date(),
|
||||
})
|
||||
: Maybe.nothing();
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) setError("An unknown, unprocessable error has occurred.");
|
||||
else setError(e.message);
|
||||
console.error(e);
|
||||
return Maybe.nothing();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
setTarget,
|
||||
setTargetType,
|
||||
submit,
|
||||
currentType: uriType,
|
||||
getType: getTypeEasy,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLookup;
|
||||
@@ -33,3 +33,30 @@ export function findASN(asn: number, ranges: string[]) {
|
||||
}
|
||||
return -1; // Failure case
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ASN falls within a range
|
||||
* @param asn The ASN number to check (e.g., 13335 for Cloudflare)
|
||||
* @param range The range to check against (e.g., "13312-18431")
|
||||
* @returns true if the ASN is within the range
|
||||
*/
|
||||
export function asnInRange(asn: number, range: string): boolean {
|
||||
const parts = range.split("-");
|
||||
|
||||
if (parts.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const start = parseInt(parts[0] ?? "", 10);
|
||||
const end = parseInt(parts[1] ?? "", 10);
|
||||
|
||||
if (isNaN(start) || isNaN(end) || start < 0 || end < 0 || start > end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (asn < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return asn >= start && asn <= end;
|
||||
}
|
||||
3
src/lib/network/index.ts
Normal file
3
src/lib/network/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { ipv4InCIDR } from "@/lib/network/ipv4";
|
||||
export { ipv6InCIDR } from "@/lib/network/ipv6";
|
||||
export { findASN, asnInRange } from "@/lib/network/asn";
|
||||
36
src/lib/network/ipv4.ts
Normal file
36
src/lib/network/ipv4.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Convert an IPv4 address string to a 32-bit integer
|
||||
*/
|
||||
function ipv4ToInt(ip: string): number {
|
||||
const parts = ip.split(".").map(Number);
|
||||
if (parts.length !== 4) return 0;
|
||||
const [a, b, c, d] = parts;
|
||||
if (a === undefined || b === undefined || c === undefined || d === undefined) return 0;
|
||||
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IPv4 address falls within a CIDR range
|
||||
* @param ip The IP address to check (e.g., "192.168.1.1")
|
||||
* @param cidr The CIDR range to check against (e.g., "192.168.0.0/16")
|
||||
* @returns true if the IP is within the CIDR range
|
||||
*/
|
||||
export function ipv4InCIDR(ip: string, cidr: string): boolean {
|
||||
const [rangeIp, prefixLenStr] = cidr.split("/");
|
||||
const prefixLen = parseInt(prefixLenStr ?? "", 10);
|
||||
|
||||
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Special case: /0 matches all IPs
|
||||
if (prefixLen === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ipInt = ipv4ToInt(ip);
|
||||
const rangeInt = ipv4ToInt(rangeIp);
|
||||
const mask = (0xffffffff << (32 - prefixLen)) >>> 0;
|
||||
|
||||
return (ipInt & mask) === (rangeInt & mask);
|
||||
}
|
||||
59
src/lib/network/ipv6.ts
Normal file
59
src/lib/network/ipv6.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Expand IPv6 address shorthand notation
|
||||
*/
|
||||
function expandIPv6(ip: string): string {
|
||||
if (ip.includes("::")) {
|
||||
const [left, right] = ip.split("::");
|
||||
const leftParts = left ? left.split(":") : [];
|
||||
const rightParts = right ? right.split(":") : [];
|
||||
const missingParts = 8 - leftParts.length - rightParts.length;
|
||||
const middleParts: string[] = Array(missingParts).fill("0") as string[];
|
||||
const allParts = [...leftParts, ...middleParts, ...rightParts];
|
||||
return allParts.map((p: string) => p.padStart(4, "0")).join(":");
|
||||
}
|
||||
return ip
|
||||
.split(":")
|
||||
.map((p: string) => p.padStart(4, "0"))
|
||||
.join(":");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IPv6 address to a BigInt representation
|
||||
*/
|
||||
function ipv6ToBigInt(ip: string): bigint {
|
||||
// Expand :: notation
|
||||
const expandedIp = expandIPv6(ip);
|
||||
const parts = expandedIp.split(":");
|
||||
|
||||
let result = BigInt(0);
|
||||
for (const part of parts) {
|
||||
result = (result << BigInt(16)) | BigInt(parseInt(part, 16));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IPv6 address falls within a CIDR range
|
||||
* @param ip The IPv6 address to check (e.g., "2001:db8::1")
|
||||
* @param cidr The CIDR range to check against (e.g., "2001:db8::/32")
|
||||
* @returns true if the IP is within the CIDR range
|
||||
*/
|
||||
export function ipv6InCIDR(ip: string, cidr: string): boolean {
|
||||
const [rangeIp, prefixLenStr] = cidr.split("/");
|
||||
const prefixLen = parseInt(prefixLenStr ?? "", 10);
|
||||
|
||||
if (!rangeIp || isNaN(prefixLen) || prefixLen < 0 || prefixLen > 128) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const ipInt = ipv6ToBigInt(ip);
|
||||
const rangeInt = ipv6ToBigInt(rangeIp);
|
||||
const maxMask = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
|
||||
const mask = (maxMask << BigInt(128 - prefixLen)) & maxMask;
|
||||
|
||||
return (ipInt & mask) === (rangeInt & mask);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,55 @@
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* Extends the global ObjectConstructor interface to allow for stronger typing
|
||||
* of Object.entries, ensuring that it returns an array of key-value pairs
|
||||
* where keys are limited to the keys of the provided object and values are properly typed.
|
||||
*/
|
||||
declare global {
|
||||
interface ObjectConstructor {
|
||||
entries<T extends object>(o: T): [keyof T, T[keyof T]][];
|
||||
}
|
||||
}
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function truthy(value: string | null | undefined) {
|
||||
if (value == undefined) return false;
|
||||
return value.toLowerCase() == "true" || value == "1";
|
||||
}
|
||||
|
||||
export function onPromise<T>(promise: (event: SyntheticEvent) => Promise<T>) {
|
||||
return (event: SyntheticEvent) => {
|
||||
if (promise) {
|
||||
promise(event).catch((error) => {
|
||||
console.log("Unexpected error", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a string dynamically to ensure maxLength is not exceeded & an ellipsis is used.
|
||||
* Behavior undefined when ellipsis exceeds {maxLength}.
|
||||
* @param input The input string
|
||||
* @param maxLength A positive number representing the maximum length the input string should be.
|
||||
* @param ellipsis A string representing what should be placed on the end when the max length is hit.
|
||||
*/
|
||||
export function truncated(input: string, maxLength: number, ellipsis = "...") {
|
||||
if (maxLength <= 0) return "";
|
||||
if (input.length <= maxLength) return input;
|
||||
return input.substring(0, Math.max(0, maxLength - ellipsis.length)) + ellipsis;
|
||||
}
|
||||
|
||||
/**
|
||||
* A functional form of `event.preventDefault()`.
|
||||
* @param event The event to prevent the default action of.
|
||||
* @returns Nothing.
|
||||
*/
|
||||
export function preventDefault(event: SyntheticEvent | Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import "@fontsource-variable/inter";
|
||||
import "@fontsource/ibm-plex-mono/400.css";
|
||||
import "@radix-ui/themes/styles.css";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useState } from "react";
|
||||
import Generic from "@/components/lookup/Generic";
|
||||
import type { MetaParsedGeneric } from "@/hooks/useLookup";
|
||||
import useLookup from "@/hooks/useLookup";
|
||||
import LookupInput from "@/components/form/LookupInput";
|
||||
import ErrorCard from "@/components/common/ErrorCard";
|
||||
import { ThemeToggle } from "@/components/common/ThemeToggle";
|
||||
import Generic from "@/rdap/components/Generic";
|
||||
import type { MetaParsedGeneric } from "@/rdap/hooks/useLookup";
|
||||
import useLookup from "@/rdap/hooks/useLookup";
|
||||
import LookupInput from "@/rdap/components/LookupInput";
|
||||
import ErrorCard from "@/components/ErrorCard";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
import { Maybe } from "true-myth";
|
||||
import type { TargetType } from "@/types";
|
||||
import type { TargetType } from "@/rdap/schemas";
|
||||
import { Flex, Container, Section, Text, Link } from "@radix-ui/themes";
|
||||
|
||||
const Index: NextPage = () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import type { AutonomousNumber } from "@/types";
|
||||
import Events from "@/components/lookup/Events";
|
||||
import Property from "@/components/common/Property";
|
||||
import PropertyList from "@/components/common/PropertyList";
|
||||
import AbstractCard from "@/components/common/AbstractCard";
|
||||
import type { AutonomousNumber } from "@/rdap/schemas";
|
||||
import Events from "@/rdap/components/Events";
|
||||
import Property from "@/components/Property";
|
||||
import PropertyList from "@/components/PropertyList";
|
||||
import AbstractCard from "@/components/AbstractCard";
|
||||
import { Flex, Text, DataList, Badge } from "@radix-ui/themes";
|
||||
|
||||
export type AutnumCardProps = {
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { rdapStatusInfo } from "@/constants";
|
||||
import type { Domain } from "@/types";
|
||||
import Events from "@/components/lookup/Events";
|
||||
import Property from "@/components/common/Property";
|
||||
import PropertyList from "@/components/common/PropertyList";
|
||||
import AbstractCard from "@/components/common/AbstractCard";
|
||||
import { rdapStatusInfo } from "@/rdap/constants";
|
||||
import type { Domain } from "@/rdap/schemas";
|
||||
import Events from "@/rdap/components/Events";
|
||||
import Property from "@/components/Property";
|
||||
import PropertyList from "@/components/PropertyList";
|
||||
import AbstractCard from "@/components/AbstractCard";
|
||||
import { Flex, Text, DataList, Badge } from "@radix-ui/themes";
|
||||
|
||||
export type DomainProps = {
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import type { Entity } from "@/types";
|
||||
import Property from "@/components/common/Property";
|
||||
import PropertyList from "@/components/common/PropertyList";
|
||||
import AbstractCard from "@/components/common/AbstractCard";
|
||||
import type { Entity } from "@/rdap/schemas";
|
||||
import Property from "@/components/Property";
|
||||
import PropertyList from "@/components/PropertyList";
|
||||
import AbstractCard from "@/components/AbstractCard";
|
||||
import { Flex, DataList, Badge, Text } from "@radix-ui/themes";
|
||||
|
||||
export type EntityCardProps = {
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import type { Event } from "@/types";
|
||||
import DynamicDate from "@/components/common/DynamicDate";
|
||||
import type { Event } from "@/rdap/schemas";
|
||||
import DynamicDate from "@/components/DynamicDate";
|
||||
import { Table, Text } from "@radix-ui/themes";
|
||||
|
||||
export type EventsProps = {
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import DomainCard from "@/components/lookup/DomainCard";
|
||||
import IPCard from "@/components/lookup/IPCard";
|
||||
import AutnumCard from "@/components/lookup/AutnumCard";
|
||||
import EntityCard from "@/components/lookup/EntityCard";
|
||||
import NameserverCard from "@/components/lookup/NameserverCard";
|
||||
import type { Domain, AutonomousNumber, Entity, Nameserver, IpNetwork } from "@/types";
|
||||
import AbstractCard from "@/components/common/AbstractCard";
|
||||
import DomainCard from "@/rdap/components/DomainCard";
|
||||
import IPCard from "@/rdap/components/IPCard";
|
||||
import AutnumCard from "@/rdap/components/AutnumCard";
|
||||
import EntityCard from "@/rdap/components/EntityCard";
|
||||
import NameserverCard from "@/rdap/components/NameserverCard";
|
||||
import type { Domain, AutonomousNumber, Entity, Nameserver, IpNetwork } from "@/rdap/schemas";
|
||||
import AbstractCard from "@/components/AbstractCard";
|
||||
|
||||
export type ParsedGeneric = Domain | Nameserver | Entity | AutonomousNumber | IpNetwork;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import type { IpNetwork } from "@/types";
|
||||
import Events from "@/components/lookup/Events";
|
||||
import Property from "@/components/common/Property";
|
||||
import PropertyList from "@/components/common/PropertyList";
|
||||
import AbstractCard from "@/components/common/AbstractCard";
|
||||
import type { IpNetwork } from "@/rdap/schemas";
|
||||
import Events from "@/rdap/components/Events";
|
||||
import Property from "@/components/Property";
|
||||
import PropertyList from "@/components/PropertyList";
|
||||
import AbstractCard from "@/components/AbstractCard";
|
||||
import { Flex, Text, DataList, Badge } from "@radix-ui/themes";
|
||||
|
||||
export type IPCardProps = {
|
||||
246
src/rdap/components/LookupInput.tsx
Normal file
246
src/rdap/components/LookupInput.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import type { FunctionComponent } from "react";
|
||||
import { useState } from "react";
|
||||
import { onPromise, preventDefault } from "@/lib/utils";
|
||||
import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/rdap/schemas";
|
||||
import { TargetTypeEnum } from "@/rdap/schemas";
|
||||
import { MagnifyingGlassIcon, ReloadIcon, LockClosedIcon } from "@radix-ui/react-icons";
|
||||
import { TextField, Select, Flex, Checkbox, Text, IconButton } from "@radix-ui/themes";
|
||||
import type { Maybe } from "true-myth";
|
||||
import { placeholders } from "@/rdap/constants";
|
||||
|
||||
/**
|
||||
* Props for the LookupInput component.
|
||||
*/
|
||||
type LookupInputProps = {
|
||||
isLoading?: boolean;
|
||||
/**
|
||||
* Callback function called when a type of registry is detected when a user changes their input.
|
||||
* @param type - The detected type of registry.
|
||||
* @returns A promise.
|
||||
*/
|
||||
onRegistry?: (type: TargetType) => Promise<void>;
|
||||
/**
|
||||
* Callback function called when a user hits submit.
|
||||
* @param props - The submit props.
|
||||
* @returns A promise.
|
||||
*/
|
||||
onSubmit?: (props: SubmitProps) => Promise<void>;
|
||||
/**
|
||||
* Callback function called when a user changes their input (text search) or explicitly changes the type of search.
|
||||
* @param target - The target object containing the search target and target type.
|
||||
* @returns Nothing.
|
||||
*/
|
||||
onChange?: (target: { target: string; targetType: TargetType | null }) => Promise<void>;
|
||||
detectedType: Maybe<TargetType>;
|
||||
};
|
||||
|
||||
const LookupInput: FunctionComponent<LookupInputProps> = ({
|
||||
isLoading,
|
||||
onSubmit,
|
||||
onChange,
|
||||
detectedType,
|
||||
}: LookupInputProps) => {
|
||||
const { register, handleSubmit, getValues, control } = useForm<SubmitProps>({
|
||||
defaultValues: {
|
||||
target: "",
|
||||
// Not used at this time.
|
||||
followReferral: false,
|
||||
requestJSContact: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A mapping of available (simple) target types to their long-form human-readable names.
|
||||
*/
|
||||
const objectNames: Record<SimplifiedTargetType | "auto", string> = {
|
||||
auto: "Autodetect",
|
||||
domain: "Domain",
|
||||
ip: "IP/CIDR", // IPv4/IPv6 are combined into this option
|
||||
tld: "TLD",
|
||||
autnum: "AS Number",
|
||||
entity: "Entity Handle",
|
||||
registrar: "Registrar",
|
||||
url: "URL",
|
||||
json: "JSON",
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of precise target types to their simplified short-form names.
|
||||
*/
|
||||
const targetShortNames: Record<TargetType, string> = {
|
||||
domain: "Domain",
|
||||
tld: "TLD",
|
||||
ip4: "IPv4",
|
||||
ip6: "IPv6",
|
||||
autnum: "ASN",
|
||||
entity: "Entity",
|
||||
registrar: "Registrar",
|
||||
url: "URL",
|
||||
json: "JSON",
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the selected value in the LookupInput component.
|
||||
*/
|
||||
const [selected, setSelected] = useState<SimplifiedTargetType | "auto">("auto");
|
||||
|
||||
/**
|
||||
* Retrieves the target type based on the provided value.
|
||||
* @param value - The value to retrieve the target type for.
|
||||
* @returns The target type as ObjectType or null.
|
||||
*/
|
||||
function retrieveTargetType(value?: string | null): TargetType | null {
|
||||
// If the value is null and the selected value is null, return null.
|
||||
if (value == null) value = selected;
|
||||
|
||||
// 'auto' means 'do whatever' so we return null.
|
||||
if (value == "auto") return null;
|
||||
|
||||
// Validate the value is a valid TargetType
|
||||
const result = TargetTypeEnum.safeParse(value);
|
||||
return result.success ? result.data : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className="pb-2.5"
|
||||
onSubmit={onSubmit != undefined ? onPromise(handleSubmit(onSubmit)) : preventDefault}
|
||||
>
|
||||
<Flex direction="column" gap="3">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</label>
|
||||
<Flex gap="0" style={{ position: "relative" }}>
|
||||
<TextField.Root
|
||||
size="3"
|
||||
placeholder={placeholders[selected]}
|
||||
disabled={isLoading}
|
||||
{...register("target", {
|
||||
required: true,
|
||||
onChange: () => {
|
||||
if (onChange != undefined)
|
||||
void onChange({
|
||||
target: getValues("target"),
|
||||
targetType: retrieveTargetType(null),
|
||||
});
|
||||
},
|
||||
})}
|
||||
style={{
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
border: "1px solid var(--gray-7)",
|
||||
borderRight: "none",
|
||||
boxShadow: "none",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<TextField.Slot side="left">
|
||||
<IconButton
|
||||
size="1"
|
||||
variant="ghost"
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
tabIndex={-1}
|
||||
style={{ cursor: isLoading ? "not-allowed" : "pointer" }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ReloadIcon className="animate-spin" width="16" height="16" />
|
||||
) : (
|
||||
<MagnifyingGlassIcon width="16" height="16" />
|
||||
)}
|
||||
</IconButton>
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<Select.Root
|
||||
value={selected}
|
||||
onValueChange={(value) => {
|
||||
setSelected(value as SimplifiedTargetType | "auto");
|
||||
|
||||
if (onChange != undefined)
|
||||
void onChange({
|
||||
target: getValues("target"),
|
||||
targetType: retrieveTargetType(value),
|
||||
});
|
||||
}}
|
||||
disabled={isLoading}
|
||||
size="3"
|
||||
>
|
||||
<Select.Trigger
|
||||
style={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
|
||||
minWidth: "150px",
|
||||
}}
|
||||
>
|
||||
{selected == "auto" ? (
|
||||
detectedType.isJust ? (
|
||||
<Text>Auto ({targetShortNames[detectedType.value]})</Text>
|
||||
) : (
|
||||
objectNames["auto"]
|
||||
)
|
||||
) : (
|
||||
<Flex align="center" gap="2">
|
||||
<LockClosedIcon width="14" height="14" />
|
||||
{objectNames[selected]}
|
||||
</Flex>
|
||||
)}
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content position="popper">
|
||||
{Object.entries(objectNames).map(([key, value]) => (
|
||||
<Select.Item key={key} value={key}>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="between"
|
||||
gap="2"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{value}
|
||||
</Flex>
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
|
||||
<Flex pl="3" gapX="5" gapY="2" wrap="wrap">
|
||||
<Flex asChild align="center" gap="2">
|
||||
<Text as="label" size="2">
|
||||
<Controller
|
||||
name="requestJSContact"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
Request JSContact
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex asChild align="center" gap="2">
|
||||
<Text as="label" size="2">
|
||||
<Controller
|
||||
name="followReferral"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
Follow referral to registrar's RDAP record
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LookupInput;
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import type { Nameserver } from "@/types";
|
||||
import Property from "@/components/common/Property";
|
||||
import AbstractCard from "@/components/common/AbstractCard";
|
||||
import type { Nameserver } from "@/rdap/schemas";
|
||||
import Property from "@/components/Property";
|
||||
import AbstractCard from "@/components/AbstractCard";
|
||||
import { Flex, DataList, Badge, Text } from "@radix-ui/themes";
|
||||
|
||||
export type NameserverCardProps = {
|
||||
@@ -1,5 +1,5 @@
|
||||
// see https://www.iana.org/assignments/rdap-json-values
|
||||
import type { RdapStatusType, RootRegistryType, SimplifiedTargetType } from "@/types";
|
||||
import type { RdapStatusType, RootRegistryType, SimplifiedTargetType } from "@/rdap/schemas";
|
||||
|
||||
export const rdapStatusInfo: Record<RdapStatusType, string> = {
|
||||
validated:
|
||||
208
src/rdap/hooks/useLookup.tsx
Normal file
208
src/rdap/hooks/useLookup.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getType } from "@/rdap/utils";
|
||||
import type { AutonomousNumber, Domain, IpNetwork, SubmitProps, TargetType } from "@/rdap/schemas";
|
||||
import {
|
||||
AutonomousNumberSchema,
|
||||
DomainSchema,
|
||||
IpNetworkSchema,
|
||||
RootRegistryEnum,
|
||||
} from "@/rdap/schemas";
|
||||
import { truncated } from "@/lib/utils";
|
||||
import type { ParsedGeneric } from "@/rdap/components/Generic";
|
||||
import { Maybe, Result } from "true-myth";
|
||||
import { loadBootstrap, getRegistry } from "@/rdap/services/registry";
|
||||
import { getRegistryURL } from "@/rdap/services/url-resolver";
|
||||
import { getAndParse } from "@/rdap/services/rdap-api";
|
||||
|
||||
export type WarningHandler = (warning: { message: string }) => void;
|
||||
export type MetaParsedGeneric = {
|
||||
data: ParsedGeneric;
|
||||
url: string;
|
||||
completeTime: Date;
|
||||
};
|
||||
|
||||
// An array of schemas to try and parse unknown JSON data with.
|
||||
const schemas = [DomainSchema, AutonomousNumberSchema, IpNetworkSchema];
|
||||
|
||||
const useLookup = (warningHandler?: WarningHandler) => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [target, setTarget] = useState<string>("");
|
||||
const [uriType, setUriType] = useState<Maybe<TargetType>>(Maybe.nothing());
|
||||
|
||||
// Used by a callback on LookupInput to forcibly set the type of the lookup.
|
||||
const [currentType, setTargetType] = useState<TargetType | null>(null);
|
||||
|
||||
// Used to allow repeatable lookups when weird errors happen.
|
||||
const repeatableRef = useRef<string>("");
|
||||
|
||||
const getTypeEasy = useCallback(async (target: string): Promise<Result<TargetType, Error>> => {
|
||||
return getType(target, getRegistry);
|
||||
}, []);
|
||||
|
||||
useCallback(async () => {
|
||||
if (currentType != null) return Maybe.just(currentType);
|
||||
const uri: Maybe<TargetType> = (await getTypeEasy(target)).mapOr(Maybe.nothing(), (type) =>
|
||||
Maybe.just(type)
|
||||
);
|
||||
setUriType(uri);
|
||||
}, [target, currentType, getTypeEasy]);
|
||||
|
||||
useEffect(() => {
|
||||
const preload = async () => {
|
||||
if (uriType.isNothing) return;
|
||||
|
||||
const registryUri = RootRegistryEnum.safeParse(uriType.value);
|
||||
if (!registryUri.success) 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, uriType, warningHandler]);
|
||||
|
||||
async function submitInternal(
|
||||
target: string
|
||||
): Promise<Result<{ data: ParsedGeneric; url: string }, Error>> {
|
||||
if (target == null || target.length == 0)
|
||||
return Result.err(new Error("A target must be given in order to execute a lookup."));
|
||||
|
||||
const targetType = await getTypeEasy(target);
|
||||
|
||||
if (targetType.isErr) {
|
||||
return Result.err(
|
||||
new Error("Unable to determine type, unable to send query", {
|
||||
cause: targetType.error,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
switch (targetType.value) {
|
||||
// Block scoped case to allow url const reuse
|
||||
case "ip4": {
|
||||
await loadBootstrap("ip4");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "ip6": {
|
||||
await loadBootstrap("ip6");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "domain": {
|
||||
await loadBootstrap("domain");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
|
||||
// HTTP
|
||||
if (url.startsWith("http://") && url != repeatableRef.current) {
|
||||
repeatableRef.current = url;
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry this domain belongs to uses HTTP, which is not secure. " +
|
||||
"In order to prevent a cryptic error from appearing due to mixed active content, " +
|
||||
"or worse, a CORS error, this lookup has been blocked. Try again to force the lookup."
|
||||
)
|
||||
);
|
||||
}
|
||||
const result = await getAndParse<Domain>(url, DomainSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "autnum": {
|
||||
await loadBootstrap("autnum");
|
||||
const url = getRegistryURL(targetType.value, target);
|
||||
const result = await getAndParse<AutonomousNumber>(url, AutonomousNumberSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "tld": {
|
||||
// remove the leading dot
|
||||
const value = target.startsWith(".") ? target.slice(1) : target;
|
||||
const url = `https://root.rdap.org/domain/${value}`;
|
||||
const result = await getAndParse<Domain>(url, DomainSchema);
|
||||
if (result.isErr) return Result.err(result.error);
|
||||
return Result.ok({ data: result.value, url });
|
||||
}
|
||||
case "url": {
|
||||
const response = await fetch(target);
|
||||
|
||||
if (response.status != 200)
|
||||
return Result.err(
|
||||
new Error(
|
||||
`The URL provided returned a non-200 status code: ${response.status}.`
|
||||
)
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Try each schema until one works
|
||||
for (const schema of schemas) {
|
||||
const result = schema.safeParse(data);
|
||||
if (result.success) return Result.ok({ data: result.data, url: target });
|
||||
}
|
||||
|
||||
return Result.err(new Error("No schema was able to parse the response."));
|
||||
}
|
||||
case "json": {
|
||||
const data = JSON.parse(target);
|
||||
for (const schema of schemas) {
|
||||
const result = schema.safeParse(data);
|
||||
if (result.success) return Result.ok({ data: result.data, url: "" });
|
||||
}
|
||||
}
|
||||
case "registrar": {
|
||||
}
|
||||
default:
|
||||
return Result.err(new Error("The type detected has not been implemented."));
|
||||
}
|
||||
}
|
||||
|
||||
async function submit({ target }: SubmitProps): Promise<Maybe<MetaParsedGeneric>> {
|
||||
try {
|
||||
// target is already set in state, but it's also provided by the form callback, so we'll use it.
|
||||
const response = await submitInternal(target);
|
||||
|
||||
if (response.isErr) {
|
||||
setError(response.error.message);
|
||||
console.error(response.error);
|
||||
} else setError(null);
|
||||
|
||||
return response.isOk
|
||||
? Maybe.just({
|
||||
data: response.value.data,
|
||||
url: response.value.url,
|
||||
completeTime: new Date(),
|
||||
})
|
||||
: Maybe.nothing();
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) setError("An unknown, unprocessable error has occurred.");
|
||||
else setError(e.message);
|
||||
console.error(e);
|
||||
return Maybe.nothing();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
setTarget,
|
||||
setTargetType,
|
||||
submit,
|
||||
currentType: uriType,
|
||||
getType: getTypeEasy,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLookup;
|
||||
@@ -1,5 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// ============================================================================
|
||||
// Enums
|
||||
// ============================================================================
|
||||
|
||||
export const TargetTypeEnum = z.enum([
|
||||
"autnum",
|
||||
"domain",
|
||||
@@ -51,6 +55,10 @@ export const StatusEnum = z.enum([
|
||||
"transfer period",
|
||||
]);
|
||||
|
||||
// ============================================================================
|
||||
// Schemas
|
||||
// ============================================================================
|
||||
|
||||
export const LinkSchema = z.object({
|
||||
value: z.string().optional(), // de-facto optional
|
||||
rel: z.string().optional(), // de-facto optional
|
||||
@@ -91,7 +99,6 @@ export const NoticeSchema = z.object({
|
||||
title: z.string().optional(),
|
||||
links: z.array(LinkSchema).optional(),
|
||||
});
|
||||
export type Notice = z.infer<typeof NoticeSchema>;
|
||||
|
||||
export const IpNetworkSchema = z.object({
|
||||
objectClassName: z.literal("ip network"),
|
||||
@@ -158,3 +165,33 @@ export const RegisterSchema = z.object({
|
||||
services: z.array(RegistrarSchema),
|
||||
version: z.string(),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// TypeScript Types
|
||||
// ============================================================================
|
||||
|
||||
// All precise target types that can be placed in the search bar.
|
||||
export type TargetType = z.infer<typeof TargetTypeEnum>;
|
||||
|
||||
// Target types that can be selected by the user; IPv4 and IPv6 are combined into a single type for simplicity (IP/CIDR)
|
||||
export type SimplifiedTargetType = Exclude<TargetType, "ip4" | "ip6"> | "ip";
|
||||
|
||||
// Root registry types that associate with a bootstrap file provided by the RDAP registry.
|
||||
export type RootRegistryType = z.infer<typeof RootRegistryEnum>;
|
||||
|
||||
export type RdapStatusType = z.infer<typeof StatusEnum>;
|
||||
export type Link = z.infer<typeof LinkSchema>;
|
||||
export type Entity = z.infer<typeof EntitySchema>;
|
||||
export type Nameserver = z.infer<typeof NameserverSchema>;
|
||||
export type Event = z.infer<typeof EventSchema>;
|
||||
export type Notice = z.infer<typeof NoticeSchema>;
|
||||
export type IpNetwork = z.infer<typeof IpNetworkSchema>;
|
||||
export type AutonomousNumber = z.infer<typeof AutonomousNumberSchema>;
|
||||
export type Register = z.infer<typeof RegisterSchema>;
|
||||
export type Domain = z.infer<typeof DomainSchema>;
|
||||
|
||||
export type SubmitProps = {
|
||||
target: string;
|
||||
requestJSContact: boolean;
|
||||
followReferral: boolean;
|
||||
};
|
||||
72
src/rdap/services/rdap-api.ts
Normal file
72
src/rdap/services/rdap-api.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { ZodSchema } from "zod";
|
||||
import { Result } from "true-myth";
|
||||
|
||||
/**
|
||||
* Fetch and parse RDAP data from a URL
|
||||
*/
|
||||
export async function getAndParse<T>(url: string, schema: ZodSchema<T>): Promise<Result<T, Error>> {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.status == 200) {
|
||||
const result = schema.safeParse(await response.json());
|
||||
|
||||
if (result.success === false) {
|
||||
// flatten the errors to make them more readable and simple
|
||||
const flatErrors = result.error.flatten(function (issue) {
|
||||
const path = issue.path.map((value) => value.toString()).join(".");
|
||||
return `${path}: ${issue.message}`;
|
||||
});
|
||||
|
||||
// combine them all, wrap them in a new error, and return it
|
||||
return Result.err(
|
||||
new Error(
|
||||
[
|
||||
"Could not parse the response from the registry.",
|
||||
...flatErrors.formErrors,
|
||||
...Object.values(flatErrors.fieldErrors).flat(),
|
||||
].join("\n\t")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(result.data);
|
||||
}
|
||||
|
||||
switch (response.status) {
|
||||
case 302:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that the resource requested is available at a different location."
|
||||
)
|
||||
);
|
||||
case 400:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that the request was malformed or could not be processed. Check that you typed in the correct information and try again."
|
||||
)
|
||||
);
|
||||
case 403:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that the request was forbidden. This could be due to rate limiting, abusive behavior, or other reasons. Try again later or contact the registry for more information."
|
||||
)
|
||||
);
|
||||
|
||||
case 404:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that the resource requested could not be found; the resource either does not exist, or is something that the registry does not track (i.e. this software queried incorrectly, which is unlikely)."
|
||||
)
|
||||
);
|
||||
case 500:
|
||||
return Result.err(
|
||||
new Error(
|
||||
"The registry indicated that an internal server error occurred. This could be due to a misconfiguration, a bug, or other reasons. Try again later or contact the registry for more information."
|
||||
)
|
||||
);
|
||||
default:
|
||||
return Result.err(
|
||||
new Error(`The registry did not return an OK status code: ${response.status}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
51
src/rdap/services/registry.ts
Normal file
51
src/rdap/services/registry.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Register, RootRegistryType } from "@/rdap/schemas";
|
||||
import { RegisterSchema } from "@/rdap/schemas";
|
||||
import { registryURLs } from "@/rdap/constants";
|
||||
|
||||
/**
|
||||
* Registry cache to avoid re-fetching bootstrap data
|
||||
*/
|
||||
const registryCache: Record<RootRegistryType, Register | null> = {
|
||||
autnum: null,
|
||||
domain: null,
|
||||
ip4: null,
|
||||
ip6: null,
|
||||
entity: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch & load a specific registry's bootstrap data
|
||||
*/
|
||||
export async function loadBootstrap(type: RootRegistryType, force = false): Promise<void> {
|
||||
// Early exit if already loaded and not forcing
|
||||
if (registryCache[type] != null && !force) return;
|
||||
|
||||
// Fetch the bootstrapping file from the registry
|
||||
const response = await fetch(registryURLs[type]);
|
||||
if (response.status != 200) throw new Error(`Error: ${response.statusText}`);
|
||||
|
||||
// Parse it to ensure data integrity
|
||||
const parsedRegister = RegisterSchema.safeParse(await response.json());
|
||||
if (!parsedRegister.success)
|
||||
throw new Error(`Could not parse IANA bootstrap response (type: ${type}).`);
|
||||
|
||||
// Cache the result
|
||||
registryCache[type] = parsedRegister.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registry's bootstrap data, loading it if necessary
|
||||
*/
|
||||
export async function getRegistry(type: RootRegistryType): Promise<Register> {
|
||||
if (registryCache[type] == null) await loadBootstrap(type);
|
||||
const registry = registryCache[type];
|
||||
if (registry == null) throw new Error(`Could not load bootstrap data for ${type} registry.`);
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cached registry data (or null if not loaded)
|
||||
*/
|
||||
export function getCachedRegistry(type: RootRegistryType): Register | null {
|
||||
return registryCache[type];
|
||||
}
|
||||
84
src/rdap/services/url-resolver.ts
Normal file
84
src/rdap/services/url-resolver.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { RootRegistryType } from "@/rdap/schemas";
|
||||
import { getCachedRegistry } from "@/rdap/services/registry";
|
||||
import { domainMatchPredicate, getBestURL } from "@/rdap/utils";
|
||||
import { ipv4InCIDR, ipv6InCIDR, asnInRange } from "@/lib/network";
|
||||
|
||||
/**
|
||||
* Resolve the RDAP URL for a given registry type and lookup target
|
||||
*/
|
||||
export function getRegistryURL(type: RootRegistryType, lookupTarget: string): string {
|
||||
const bootstrap = getCachedRegistry(type);
|
||||
if (bootstrap == null)
|
||||
throw new Error(`Cannot acquire RDAP URL without bootstrap data for ${type} lookup.`);
|
||||
|
||||
let url: string | null = null;
|
||||
|
||||
typeSwitch: switch (type) {
|
||||
case "domain":
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
if (bootstrapItem[0].some(domainMatchPredicate(lookupTarget))) {
|
||||
// min length of 1 is validated in zod schema
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching domain found.`);
|
||||
case "ip4": {
|
||||
// Extract the IP address without CIDR suffix for matching
|
||||
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
// bootstrapItem[0] contains CIDR ranges like ["1.0.0.0/8", "2.0.0.0/8"]
|
||||
if (bootstrapItem[0].some((cidr) => ipv4InCIDR(ipAddress, cidr))) {
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching IPv4 registry found for ${lookupTarget}.`);
|
||||
}
|
||||
case "ip6": {
|
||||
// Extract the IP address without CIDR suffix for matching
|
||||
const ipAddress = lookupTarget.split("/")[0] ?? lookupTarget;
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
// bootstrapItem[0] contains CIDR ranges like ["2001:0200::/23", "2001:0400::/23"]
|
||||
if (bootstrapItem[0].some((cidr) => ipv6InCIDR(ipAddress, cidr))) {
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching IPv6 registry found for ${lookupTarget}.`);
|
||||
}
|
||||
case "autnum": {
|
||||
// Extract ASN number from "AS12345" format
|
||||
const asnMatch = lookupTarget.match(/^AS(\d+)$/i);
|
||||
if (!asnMatch || !asnMatch[1]) {
|
||||
throw new Error(`Invalid ASN format: ${lookupTarget}`);
|
||||
}
|
||||
|
||||
const asnNumber = parseInt(asnMatch[1], 10);
|
||||
if (isNaN(asnNumber)) {
|
||||
throw new Error(`Invalid ASN number: ${lookupTarget}`);
|
||||
}
|
||||
|
||||
for (const bootstrapItem of bootstrap.services) {
|
||||
// bootstrapItem[0] contains ASN ranges like ["64512-65534", "13312-18431"]
|
||||
if (bootstrapItem[0].some((range) => asnInRange(asnNumber, range))) {
|
||||
url = getBestURL(bootstrapItem[1] as [string, ...string[]]);
|
||||
break typeSwitch;
|
||||
}
|
||||
}
|
||||
throw new Error(`No matching registry found for ${lookupTarget}.`);
|
||||
}
|
||||
case "entity":
|
||||
throw new Error(`No matching entity found.`);
|
||||
default:
|
||||
throw new Error("Invalid lookup target provided.");
|
||||
}
|
||||
|
||||
if (url == null) throw new Error("No lookup target was resolved.");
|
||||
|
||||
// Map internal types to RDAP endpoint paths
|
||||
// ip4 and ip6 both use the 'ip' endpoint in RDAP
|
||||
const rdapPath = type === "ip4" || type === "ip6" ? "ip" : type;
|
||||
|
||||
return `${url}${rdapPath}/${lookupTarget}`;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Register, RootRegistryType, TargetType } from "@/types";
|
||||
import type { Register, RootRegistryType, TargetType } from "@/rdap/schemas";
|
||||
import { Result } from "true-myth";
|
||||
|
||||
export function domainMatchPredicate(domain: string): (tld: string) => boolean {
|
||||
39
src/types.ts
39
src/types.ts
@@ -1,39 +0,0 @@
|
||||
import type { z } from "zod";
|
||||
import type {
|
||||
AutonomousNumberSchema,
|
||||
DomainSchema,
|
||||
EntitySchema,
|
||||
EventSchema,
|
||||
IpNetworkSchema,
|
||||
LinkSchema,
|
||||
NameserverSchema,
|
||||
TargetTypeEnum,
|
||||
RegisterSchema,
|
||||
StatusEnum,
|
||||
RootRegistryEnum,
|
||||
} from "@/schema";
|
||||
|
||||
// All precise target types that can be placed in the search bar.
|
||||
export type TargetType = z.infer<typeof TargetTypeEnum>;
|
||||
|
||||
// Target types that can be selected by the user; IPv4 and IPv6 are combined into a single type for simplicity (IP/CIDR)
|
||||
export type SimplifiedTargetType = Exclude<TargetType, "ip4" | "ip6"> | "ip";
|
||||
|
||||
// Root registry types that associate with a bootstrap file provided by the RDAP registry.
|
||||
export type RootRegistryType = z.infer<typeof RootRegistryEnum>;
|
||||
|
||||
export type RdapStatusType = z.infer<typeof StatusEnum>;
|
||||
export type Link = z.infer<typeof LinkSchema>;
|
||||
export type Entity = z.infer<typeof EntitySchema>;
|
||||
export type Nameserver = z.infer<typeof NameserverSchema>;
|
||||
export type Event = z.infer<typeof EventSchema>;
|
||||
export type IpNetwork = z.infer<typeof IpNetworkSchema>;
|
||||
export type AutonomousNumber = z.infer<typeof AutonomousNumberSchema>;
|
||||
export type Register = z.infer<typeof RegisterSchema>;
|
||||
export type Domain = z.infer<typeof DomainSchema>;
|
||||
|
||||
export type SubmitProps = {
|
||||
target: string;
|
||||
requestJSContact: boolean;
|
||||
followReferral: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user