@ocap/util
Version:
utils shared across multiple forge js libs, works in both node.js and browser
76 lines (74 loc) • 3.05 kB
JavaScript
import { Resolver } from "node:dns/promises";
import { URL } from "url";
import ipaddr from "ipaddr.js";
//#region src/url.ts
const MAX_URL_LENGTH = 256;
function parseIp(host) {
if (!ipaddr.isValid(host)) return null;
try {
return ipaddr.parse(host);
} catch {
return null;
}
}
function stripTrailingDots(host) {
return host.replace(/\.+$/, "");
}
function isBlockedHostname(host, blockedHosts, blockedSuffixes) {
const normalized = host.toLowerCase();
if (blockedHosts.includes(normalized)) return true;
if (blockedSuffixes.some((suffix) => normalized.endsWith(suffix))) return true;
return false;
}
function isUnicastAddress(host) {
const parsed = parseIp(host);
if (!parsed) return false;
const range = parsed.range();
if (range === "ipv4Mapped" && parsed.kind() === "ipv6") return parsed.toIPv4Address().range() === "unicast";
return range === "unicast";
}
async function resolveHostAddresses(host) {
try {
const resolver = new Resolver();
resolver.setServers(["8.8.8.8", "1.1.1.1"]);
const addresses = [];
try {
const ipv4Addresses = await resolver.resolve4(host);
addresses.push(...ipv4Addresses);
} catch (error) {
console.warn(`IPv4 resolution failed for ${host}:`, error?.message);
}
if (!addresses.length) try {
const ipv6Addresses = await resolver.resolve6(host);
addresses.push(...ipv6Addresses);
} catch (error) {
console.warn(`IPv6 resolution failed for ${host}:`, error?.message);
}
return addresses.filter(Boolean);
} catch (error) {
console.warn(`DNS resolution failed for host: ${host}, error: ${error?.message}`);
return [];
}
}
async function verifyUrl(url, options = {}) {
if (!url) throw new Error("URL is required");
const { maxLength = MAX_URL_LENGTH, protocols = ["https:"], blockedHosts = ["localhost"], blockedSuffixes = [], allowIp = false, checkDns = false } = options;
if (url.length > maxLength) throw new Error(`URL exceeds maximum length of ${maxLength} characters: ${url.length}`);
const { hostname, protocol } = new URL(url);
if (!protocols.includes(protocol)) throw new Error(`Protocol '${protocol}' is not allowed. Allowed protocols: ${protocols.join(", ")}`);
const normalizedHost = stripTrailingDots(hostname);
if (isBlockedHostname(normalizedHost, blockedHosts, blockedSuffixes)) throw new Error(`Hostname '${normalizedHost}' is blocked`);
const isIP = Boolean(parseIp(normalizedHost));
if (isIP) {
if (!allowIp) throw new Error(`IP addresses are not allowed: ${normalizedHost}`);
if (!isUnicastAddress(normalizedHost)) throw new Error(`IP address is not unicast: ${normalizedHost}`);
}
if (checkDns && !isIP) {
const resolved = await resolveHostAddresses(normalizedHost);
if (resolved.length === 0) throw new Error(`DNS resolution failed: no addresses found for ${normalizedHost}`);
if (!resolved.every((address) => isUnicastAddress(address))) throw new Error(`DNS resolved to non-unicast addresses for ${normalizedHost}`);
}
return true;
}
//#endregion
export { isUnicastAddress, parseIp, resolveHostAddresses, verifyUrl };