@fedify/vocab-runtime
Advanced tools
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import process from "node:process"; | ||
| //#region deno.json | ||
| var name = "@fedify/vocab-runtime"; | ||
| var version = "2.1.19"; | ||
| //#endregion | ||
| //#region src/request.ts | ||
| /** | ||
| * Error thrown when fetching a JSON-LD document failed. | ||
| */ | ||
| var FetchError = class extends Error { | ||
| /** | ||
| * The URL that failed to fetch. | ||
| */ | ||
| url; | ||
| /** | ||
| * The HTTP response that failed, if available. | ||
| */ | ||
| response; | ||
| /** | ||
| * Constructs a new `FetchError`. | ||
| * | ||
| * @param url The URL that failed to fetch. | ||
| * @param message Error message. | ||
| * @param response The failed HTTP response, if available. | ||
| */ | ||
| constructor(url, message, response) { | ||
| super(message == null ? url.toString() : `${url}: ${message}`); | ||
| this.name = "FetchError"; | ||
| this.url = typeof url === "string" ? new URL(url) : url; | ||
| this.response = response; | ||
| } | ||
| }; | ||
| /** | ||
| * Creates a request for the given URL. | ||
| * @param url The URL to create the request for. | ||
| * @param options The options for the request. | ||
| * @returns The created request. | ||
| * @internal | ||
| */ | ||
| function createActivityPubRequest(url, options = {}) { | ||
| return new Request(url, { | ||
| headers: { | ||
| Accept: "application/activity+json, application/ld+json", | ||
| "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent) | ||
| }, | ||
| redirect: "manual" | ||
| }); | ||
| } | ||
| /** | ||
| * Gets the user agent string for the given application and URL. | ||
| * @param options The options for making the user agent string. | ||
| * @returns The user agent string. | ||
| * @since 1.3.0 | ||
| */ | ||
| function getUserAgent({ software, url } = {}) { | ||
| const fedify = `Fedify/${version}`; | ||
| const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${process.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${process.versions.node}` : null; | ||
| const userAgent = software == null ? [fedify] : [software, fedify]; | ||
| if (runtime != null) userAgent.push(runtime); | ||
| if (url != null) userAgent.push(`+${url.toString()}`); | ||
| return `${userAgent.shift()} (${userAgent.join("; ")})`; | ||
| } | ||
| /** | ||
| * Logs the request. | ||
| * @param request The request to log. | ||
| * @internal | ||
| */ | ||
| function logRequest(logger, request) { | ||
| logger.debug("Fetching document: {method} {url} {headers}", { | ||
| method: request.method, | ||
| url: request.url, | ||
| headers: Object.fromEntries(request.headers.entries()) | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { name as a, logRequest as i, createActivityPubRequest as n, version as o, getUserAgent as r, FetchError as t }; |
| const require_chunk = require("./chunk-Do9eywBl.cjs"); | ||
| let node_process = require("node:process"); | ||
| node_process = require_chunk.__toESM(node_process); | ||
| //#region deno.json | ||
| var name = "@fedify/vocab-runtime"; | ||
| var version = "2.1.19"; | ||
| //#endregion | ||
| //#region src/request.ts | ||
| /** | ||
| * Error thrown when fetching a JSON-LD document failed. | ||
| */ | ||
| var FetchError = class extends Error { | ||
| /** | ||
| * The URL that failed to fetch. | ||
| */ | ||
| url; | ||
| /** | ||
| * The HTTP response that failed, if available. | ||
| */ | ||
| response; | ||
| /** | ||
| * Constructs a new `FetchError`. | ||
| * | ||
| * @param url The URL that failed to fetch. | ||
| * @param message Error message. | ||
| * @param response The failed HTTP response, if available. | ||
| */ | ||
| constructor(url, message, response) { | ||
| super(message == null ? url.toString() : `${url}: ${message}`); | ||
| this.name = "FetchError"; | ||
| this.url = typeof url === "string" ? new URL(url) : url; | ||
| this.response = response; | ||
| } | ||
| }; | ||
| /** | ||
| * Creates a request for the given URL. | ||
| * @param url The URL to create the request for. | ||
| * @param options The options for the request. | ||
| * @returns The created request. | ||
| * @internal | ||
| */ | ||
| function createActivityPubRequest(url, options = {}) { | ||
| return new Request(url, { | ||
| headers: { | ||
| Accept: "application/activity+json, application/ld+json", | ||
| "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent) | ||
| }, | ||
| redirect: "manual" | ||
| }); | ||
| } | ||
| /** | ||
| * Gets the user agent string for the given application and URL. | ||
| * @param options The options for making the user agent string. | ||
| * @returns The user agent string. | ||
| * @since 1.3.0 | ||
| */ | ||
| function getUserAgent({ software, url } = {}) { | ||
| const fedify = `Fedify/${version}`; | ||
| const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${node_process.default.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${node_process.default.versions.node}` : null; | ||
| const userAgent = software == null ? [fedify] : [software, fedify]; | ||
| if (runtime != null) userAgent.push(runtime); | ||
| if (url != null) userAgent.push(`+${url.toString()}`); | ||
| return `${userAgent.shift()} (${userAgent.join("; ")})`; | ||
| } | ||
| /** | ||
| * Logs the request. | ||
| * @param request The request to log. | ||
| * @internal | ||
| */ | ||
| function logRequest(logger, request) { | ||
| logger.debug("Fetching document: {method} {url} {headers}", { | ||
| method: request.method, | ||
| url: request.url, | ||
| headers: Object.fromEntries(request.headers.entries()) | ||
| }); | ||
| } | ||
| //#endregion | ||
| Object.defineProperty(exports, "FetchError", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return FetchError; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "createActivityPubRequest", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return createActivityPubRequest; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "getUserAgent", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return getUserAgent; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "logRequest", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return logRequest; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "name", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return name; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "version", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return version; | ||
| } | ||
| }); |
| require("./chunk-Do9eywBl.cjs"); | ||
| let node_dns_promises = require("node:dns/promises"); | ||
| let node_net = require("node:net"); | ||
| //#region src/url.ts | ||
| var UrlError = class extends Error { | ||
| constructor(message, options) { | ||
| super(message, options); | ||
| this.name = "UrlError"; | ||
| } | ||
| }; | ||
| /** | ||
| * Validates a URL to prevent SSRF attacks. | ||
| */ | ||
| async function validatePublicUrl(url) { | ||
| const parsed = new URL(url); | ||
| if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsed.protocol}`); | ||
| let hostname = parsed.hostname; | ||
| if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.slice(1, -1); | ||
| if (hostname === "localhost") throw new UrlError("Localhost is not allowed"); | ||
| const hostnameFamily = (0, node_net.isIP)(hostname); | ||
| if (hostnameFamily !== 0) { | ||
| validatePublicIpAddress(hostname, hostnameFamily); | ||
| return; | ||
| } | ||
| if ("Deno" in globalThis && !(0, node_net.isIP)(hostname)) { | ||
| if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return; | ||
| } | ||
| if ("Bun" in globalThis) { | ||
| if (hostname === "example.com" || hostname.endsWith(".example.com")) return; | ||
| else if (hostname === "fedify-test.internal") throw new UrlError("Invalid or private address: fedify-test.internal"); | ||
| } | ||
| let addresses; | ||
| try { | ||
| addresses = await (0, node_dns_promises.lookup)(hostname, { all: true }); | ||
| } catch (error) { | ||
| throw new UrlError("DNS lookup failed", { cause: error }); | ||
| } | ||
| validateLookupAddresses(addresses); | ||
| } | ||
| /** | ||
| * Validates the IP addresses returned by `node:dns.lookup()`. | ||
| * | ||
| * Cloudflare Workers' `node:dns` implementation currently maps every record | ||
| * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a | ||
| * `LookupAddress`, even though Node.js specifies that the `address` field must | ||
| * contain an IPv4 or IPv6 literal. See: | ||
| * https://github.com/cloudflare/workerd/issues/6886 | ||
| * | ||
| * Work around that bug by ignoring non-IP entries only when the lookup also | ||
| * returns at least one actual IP address. This remains fail-closed: a result | ||
| * containing no IP addresses is rejected, and every returned IP address is | ||
| * still validated and must be public. This workaround can be revisited once | ||
| * the Workerd issue is fixed in supported Cloudflare Workers runtimes. | ||
| * | ||
| * @internal | ||
| */ | ||
| function validateLookupAddresses(addresses) { | ||
| let ipAddressCount = 0; | ||
| for (const { address } of addresses) { | ||
| const family = (0, node_net.isIP)(address); | ||
| if (family === 0) continue; | ||
| ipAddressCount++; | ||
| validatePublicIpAddress(address, family); | ||
| } | ||
| if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address"); | ||
| } | ||
| function validatePublicIpAddress(address, family) { | ||
| if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return; | ||
| throw new UrlError(`Invalid or private address: ${address}`); | ||
| } | ||
| function isValidPublicIPv4Address(address) { | ||
| const parts = parseIPv4Address(address); | ||
| if (parts == null) return false; | ||
| const value = ipv4PartsToNumber(parts); | ||
| return !nonPublicIPv4Prefixes.some(({ base, prefix }) => matchesIPv4Prefix(value, base, prefix)); | ||
| } | ||
| function isValidPublicIPv6Address(address) { | ||
| const words = parseIPv6Address(address); | ||
| if (words == null) return false; | ||
| if (nonPublicIPv6Prefixes.some(({ words: prefixWords, prefix }) => matchesIPv6Prefix(words, prefixWords, prefix))) return false; | ||
| for (const { extractIPv4, prefix, words: prefixWords } of ipv6WithIPv4Prefixes) { | ||
| if (!matchesIPv6Prefix(words, prefixWords, prefix)) continue; | ||
| const ipv4Address = extractIPv4(words); | ||
| if (ipv4Address != null && !isValidPublicIPv4Address(ipv4Address)) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function expandIPv6Address(address) { | ||
| address = address.toLowerCase(); | ||
| const ipv4Delimiter = address.lastIndexOf(":"); | ||
| if (address.includes(".") && ipv4Delimiter >= 0) { | ||
| const ipv4Parts = parseIPv4Address(address.substring(ipv4Delimiter + 1)); | ||
| if (ipv4Parts == null) return address; | ||
| const high = (ipv4Parts[0] << 8) + ipv4Parts[1]; | ||
| const low = (ipv4Parts[2] << 8) + ipv4Parts[3]; | ||
| address = address.substring(0, ipv4Delimiter + 1) + high.toString(16) + ":" + low.toString(16); | ||
| } | ||
| if (address === "::") return "0000:0000:0000:0000:0000:0000:0000:0000"; | ||
| if (address.startsWith("::")) address = "0000" + address; | ||
| if (address.endsWith("::")) address = address + "0000"; | ||
| address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":"); | ||
| return address.split(":").map((part) => part.padStart(4, "0")).join(":"); | ||
| } | ||
| const nonPublicIPv4Prefixes = [ | ||
| ipv4Prefix("0.0.0.0/8", "RFC 6890"), | ||
| ipv4Prefix("10.0.0.0/8", "RFC 1918"), | ||
| ipv4Prefix("100.64.0.0/10", "RFC 6598"), | ||
| ipv4Prefix("127.0.0.0/8", "RFC 1122"), | ||
| ipv4Prefix("169.254.0.0/16", "RFC 3927"), | ||
| ipv4Prefix("172.16.0.0/12", "RFC 1918"), | ||
| ipv4Prefix("192.0.0.0/24", "RFC 6890"), | ||
| ipv4Prefix("192.0.2.0/24", "RFC 5737"), | ||
| ipv4Prefix("192.88.99.0/24", "RFC 7526"), | ||
| ipv4Prefix("192.168.0.0/16", "RFC 1918"), | ||
| ipv4Prefix("198.18.0.0/15", "RFC 2544"), | ||
| ipv4Prefix("198.51.100.0/24", "RFC 5737"), | ||
| ipv4Prefix("203.0.113.0/24", "RFC 5737"), | ||
| ipv4Prefix("224.0.0.0/4", "RFC 5771"), | ||
| ipv4Prefix("240.0.0.0/4", "RFC 1112") | ||
| ]; | ||
| const nonPublicIPv6Prefixes = [ | ||
| ipv6Prefix("::/16", "RFC 4291"), | ||
| ipv6Prefix("2001::/32", "RFC 4380"), | ||
| ipv6Prefix("2002::/16", "RFC 3056"), | ||
| ipv6Prefix("64:ff9b:1::/48", "RFC 8215"), | ||
| ipv6Prefix("fc00::/7", "RFC 4193"), | ||
| ipv6Prefix("fe80::/10", "RFC 4291"), | ||
| ipv6Prefix("ff00::/8", "RFC 4291") | ||
| ]; | ||
| const ipv6WithIPv4Prefixes = [{ | ||
| ...ipv6Prefix("64:ff9b::/96", "RFC 6052"), | ||
| extractIPv4: (words) => ipv4FromWords(words[6], words[7]) | ||
| }]; | ||
| function ipv4Prefix(cidr, rfc) { | ||
| const [address, prefixText] = cidr.split("/"); | ||
| const prefix = parseInt(prefixText, 10); | ||
| const parts = parseIPv4Address(address); | ||
| if (parts == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error(`Invalid IPv4 prefix: ${cidr}`); | ||
| return { | ||
| cidr, | ||
| base: ipv4PartsToNumber(parts), | ||
| prefix, | ||
| rfc | ||
| }; | ||
| } | ||
| function ipv6Prefix(cidr, rfc) { | ||
| const [address, prefixText] = cidr.split("/"); | ||
| const prefix = parseInt(prefixText, 10); | ||
| const words = parseIPv6Address(address); | ||
| if (words == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 128) throw new Error(`Invalid IPv6 prefix: ${cidr}`); | ||
| return { | ||
| cidr, | ||
| words, | ||
| prefix, | ||
| rfc | ||
| }; | ||
| } | ||
| function parseIPv4Address(address) { | ||
| const parts = address.split(".").map((part) => { | ||
| if (!/^\d+$/.test(part)) return NaN; | ||
| return parseInt(part, 10); | ||
| }); | ||
| if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null; | ||
| return parts; | ||
| } | ||
| function parseIPv6Address(address) { | ||
| const parts = expandIPv6Address(address).split(":"); | ||
| if (parts.length !== 8) return null; | ||
| const words = parts.map((part) => { | ||
| if (!/^[0-9a-f]{1,4}$/i.test(part)) return NaN; | ||
| return parseInt(part, 16); | ||
| }); | ||
| if (words.some((word) => !Number.isInteger(word) || word < 0 || word > 65535)) return null; | ||
| return words; | ||
| } | ||
| function ipv4PartsToNumber(parts) { | ||
| return parts[0] * 2 ** 24 + parts[1] * 2 ** 16 + parts[2] * 2 ** 8 + parts[3]; | ||
| } | ||
| function ipv4FromWords(highWord, lowWord) { | ||
| return [ | ||
| highWord >> 8, | ||
| highWord & 255, | ||
| lowWord >> 8, | ||
| lowWord & 255 | ||
| ].join("."); | ||
| } | ||
| function matchesIPv4Prefix(address, prefixBase, prefixLength) { | ||
| const blockSize = 2 ** (32 - prefixLength); | ||
| return Math.floor(address / blockSize) === Math.floor(prefixBase / blockSize); | ||
| } | ||
| function matchesIPv6Prefix(address, prefixWords, prefixLength) { | ||
| let remaining = prefixLength; | ||
| for (let i = 0; i < 8 && remaining > 0; i++) if (remaining >= 16) { | ||
| if (address[i] !== prefixWords[i]) return false; | ||
| remaining -= 16; | ||
| } else { | ||
| const mask = 65535 << 16 - remaining & 65535; | ||
| if ((address[i] & mask) !== (prefixWords[i] & mask)) return false; | ||
| remaining = 0; | ||
| } | ||
| return true; | ||
| } | ||
| //#endregion | ||
| Object.defineProperty(exports, "UrlError", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return UrlError; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "expandIPv6Address", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return expandIPv6Address; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "isValidPublicIPv4Address", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return isValidPublicIPv4Address; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "isValidPublicIPv6Address", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return isValidPublicIPv6Address; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "validateLookupAddresses", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return validateLookupAddresses; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "validatePublicUrl", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return validatePublicUrl; | ||
| } | ||
| }); |
| import { lookup } from "node:dns/promises"; | ||
| import { isIP } from "node:net"; | ||
| //#region src/url.ts | ||
| var UrlError = class extends Error { | ||
| constructor(message, options) { | ||
| super(message, options); | ||
| this.name = "UrlError"; | ||
| } | ||
| }; | ||
| /** | ||
| * Validates a URL to prevent SSRF attacks. | ||
| */ | ||
| async function validatePublicUrl(url) { | ||
| const parsed = new URL(url); | ||
| if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsed.protocol}`); | ||
| let hostname = parsed.hostname; | ||
| if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.slice(1, -1); | ||
| if (hostname === "localhost") throw new UrlError("Localhost is not allowed"); | ||
| const hostnameFamily = isIP(hostname); | ||
| if (hostnameFamily !== 0) { | ||
| validatePublicIpAddress(hostname, hostnameFamily); | ||
| return; | ||
| } | ||
| if ("Deno" in globalThis && !isIP(hostname)) { | ||
| if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return; | ||
| } | ||
| if ("Bun" in globalThis) { | ||
| if (hostname === "example.com" || hostname.endsWith(".example.com")) return; | ||
| else if (hostname === "fedify-test.internal") throw new UrlError("Invalid or private address: fedify-test.internal"); | ||
| } | ||
| let addresses; | ||
| try { | ||
| addresses = await lookup(hostname, { all: true }); | ||
| } catch (error) { | ||
| throw new UrlError("DNS lookup failed", { cause: error }); | ||
| } | ||
| validateLookupAddresses(addresses); | ||
| } | ||
| /** | ||
| * Validates the IP addresses returned by `node:dns.lookup()`. | ||
| * | ||
| * Cloudflare Workers' `node:dns` implementation currently maps every record | ||
| * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a | ||
| * `LookupAddress`, even though Node.js specifies that the `address` field must | ||
| * contain an IPv4 or IPv6 literal. See: | ||
| * https://github.com/cloudflare/workerd/issues/6886 | ||
| * | ||
| * Work around that bug by ignoring non-IP entries only when the lookup also | ||
| * returns at least one actual IP address. This remains fail-closed: a result | ||
| * containing no IP addresses is rejected, and every returned IP address is | ||
| * still validated and must be public. This workaround can be revisited once | ||
| * the Workerd issue is fixed in supported Cloudflare Workers runtimes. | ||
| * | ||
| * @internal | ||
| */ | ||
| function validateLookupAddresses(addresses) { | ||
| let ipAddressCount = 0; | ||
| for (const { address } of addresses) { | ||
| const family = isIP(address); | ||
| if (family === 0) continue; | ||
| ipAddressCount++; | ||
| validatePublicIpAddress(address, family); | ||
| } | ||
| if (ipAddressCount === 0) throw new UrlError("DNS lookup did not return any IP address"); | ||
| } | ||
| function validatePublicIpAddress(address, family) { | ||
| if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return; | ||
| throw new UrlError(`Invalid or private address: ${address}`); | ||
| } | ||
| function isValidPublicIPv4Address(address) { | ||
| const parts = parseIPv4Address(address); | ||
| if (parts == null) return false; | ||
| const value = ipv4PartsToNumber(parts); | ||
| return !nonPublicIPv4Prefixes.some(({ base, prefix }) => matchesIPv4Prefix(value, base, prefix)); | ||
| } | ||
| function isValidPublicIPv6Address(address) { | ||
| const words = parseIPv6Address(address); | ||
| if (words == null) return false; | ||
| if (nonPublicIPv6Prefixes.some(({ words: prefixWords, prefix }) => matchesIPv6Prefix(words, prefixWords, prefix))) return false; | ||
| for (const { extractIPv4, prefix, words: prefixWords } of ipv6WithIPv4Prefixes) { | ||
| if (!matchesIPv6Prefix(words, prefixWords, prefix)) continue; | ||
| const ipv4Address = extractIPv4(words); | ||
| if (ipv4Address != null && !isValidPublicIPv4Address(ipv4Address)) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function expandIPv6Address(address) { | ||
| address = address.toLowerCase(); | ||
| const ipv4Delimiter = address.lastIndexOf(":"); | ||
| if (address.includes(".") && ipv4Delimiter >= 0) { | ||
| const ipv4Parts = parseIPv4Address(address.substring(ipv4Delimiter + 1)); | ||
| if (ipv4Parts == null) return address; | ||
| const high = (ipv4Parts[0] << 8) + ipv4Parts[1]; | ||
| const low = (ipv4Parts[2] << 8) + ipv4Parts[3]; | ||
| address = address.substring(0, ipv4Delimiter + 1) + high.toString(16) + ":" + low.toString(16); | ||
| } | ||
| if (address === "::") return "0000:0000:0000:0000:0000:0000:0000:0000"; | ||
| if (address.startsWith("::")) address = "0000" + address; | ||
| if (address.endsWith("::")) address = address + "0000"; | ||
| address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":"); | ||
| return address.split(":").map((part) => part.padStart(4, "0")).join(":"); | ||
| } | ||
| const nonPublicIPv4Prefixes = [ | ||
| ipv4Prefix("0.0.0.0/8", "RFC 6890"), | ||
| ipv4Prefix("10.0.0.0/8", "RFC 1918"), | ||
| ipv4Prefix("100.64.0.0/10", "RFC 6598"), | ||
| ipv4Prefix("127.0.0.0/8", "RFC 1122"), | ||
| ipv4Prefix("169.254.0.0/16", "RFC 3927"), | ||
| ipv4Prefix("172.16.0.0/12", "RFC 1918"), | ||
| ipv4Prefix("192.0.0.0/24", "RFC 6890"), | ||
| ipv4Prefix("192.0.2.0/24", "RFC 5737"), | ||
| ipv4Prefix("192.88.99.0/24", "RFC 7526"), | ||
| ipv4Prefix("192.168.0.0/16", "RFC 1918"), | ||
| ipv4Prefix("198.18.0.0/15", "RFC 2544"), | ||
| ipv4Prefix("198.51.100.0/24", "RFC 5737"), | ||
| ipv4Prefix("203.0.113.0/24", "RFC 5737"), | ||
| ipv4Prefix("224.0.0.0/4", "RFC 5771"), | ||
| ipv4Prefix("240.0.0.0/4", "RFC 1112") | ||
| ]; | ||
| const nonPublicIPv6Prefixes = [ | ||
| ipv6Prefix("::/16", "RFC 4291"), | ||
| ipv6Prefix("2001::/32", "RFC 4380"), | ||
| ipv6Prefix("2002::/16", "RFC 3056"), | ||
| ipv6Prefix("64:ff9b:1::/48", "RFC 8215"), | ||
| ipv6Prefix("fc00::/7", "RFC 4193"), | ||
| ipv6Prefix("fe80::/10", "RFC 4291"), | ||
| ipv6Prefix("ff00::/8", "RFC 4291") | ||
| ]; | ||
| const ipv6WithIPv4Prefixes = [{ | ||
| ...ipv6Prefix("64:ff9b::/96", "RFC 6052"), | ||
| extractIPv4: (words) => ipv4FromWords(words[6], words[7]) | ||
| }]; | ||
| function ipv4Prefix(cidr, rfc) { | ||
| const [address, prefixText] = cidr.split("/"); | ||
| const prefix = parseInt(prefixText, 10); | ||
| const parts = parseIPv4Address(address); | ||
| if (parts == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error(`Invalid IPv4 prefix: ${cidr}`); | ||
| return { | ||
| cidr, | ||
| base: ipv4PartsToNumber(parts), | ||
| prefix, | ||
| rfc | ||
| }; | ||
| } | ||
| function ipv6Prefix(cidr, rfc) { | ||
| const [address, prefixText] = cidr.split("/"); | ||
| const prefix = parseInt(prefixText, 10); | ||
| const words = parseIPv6Address(address); | ||
| if (words == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 128) throw new Error(`Invalid IPv6 prefix: ${cidr}`); | ||
| return { | ||
| cidr, | ||
| words, | ||
| prefix, | ||
| rfc | ||
| }; | ||
| } | ||
| function parseIPv4Address(address) { | ||
| const parts = address.split(".").map((part) => { | ||
| if (!/^\d+$/.test(part)) return NaN; | ||
| return parseInt(part, 10); | ||
| }); | ||
| if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null; | ||
| return parts; | ||
| } | ||
| function parseIPv6Address(address) { | ||
| const parts = expandIPv6Address(address).split(":"); | ||
| if (parts.length !== 8) return null; | ||
| const words = parts.map((part) => { | ||
| if (!/^[0-9a-f]{1,4}$/i.test(part)) return NaN; | ||
| return parseInt(part, 16); | ||
| }); | ||
| if (words.some((word) => !Number.isInteger(word) || word < 0 || word > 65535)) return null; | ||
| return words; | ||
| } | ||
| function ipv4PartsToNumber(parts) { | ||
| return parts[0] * 2 ** 24 + parts[1] * 2 ** 16 + parts[2] * 2 ** 8 + parts[3]; | ||
| } | ||
| function ipv4FromWords(highWord, lowWord) { | ||
| return [ | ||
| highWord >> 8, | ||
| highWord & 255, | ||
| lowWord >> 8, | ||
| lowWord & 255 | ||
| ].join("."); | ||
| } | ||
| function matchesIPv4Prefix(address, prefixBase, prefixLength) { | ||
| const blockSize = 2 ** (32 - prefixLength); | ||
| return Math.floor(address / blockSize) === Math.floor(prefixBase / blockSize); | ||
| } | ||
| function matchesIPv6Prefix(address, prefixWords, prefixLength) { | ||
| let remaining = prefixLength; | ||
| for (let i = 0; i < 8 && remaining > 0; i++) if (remaining >= 16) { | ||
| if (address[i] !== prefixWords[i]) return false; | ||
| remaining -= 16; | ||
| } else { | ||
| const mask = 65535 << 16 - remaining & 65535; | ||
| if ((address[i] & mask) !== (prefixWords[i] & mask)) return false; | ||
| remaining = 0; | ||
| } | ||
| return true; | ||
| } | ||
| //#endregion | ||
| export { validateLookupAddresses as a, isValidPublicIPv6Address as i, expandIPv6Address as n, validatePublicUrl as o, isValidPublicIPv4Address as r, UrlError as t }; |
+1
-1
| { | ||
| "name": "@fedify/vocab-runtime", | ||
| "version": "2.1.18", | ||
| "version": "2.1.19", | ||
| "license": "MIT", | ||
@@ -5,0 +5,0 @@ "exports": { |
+3
-1
@@ -420,3 +420,5 @@ /// <reference lib="esnext.temporal" /> | ||
| declare class UrlError extends Error { | ||
| constructor(message: string); | ||
| constructor(message: string, options?: { | ||
| cause?: unknown; | ||
| }); | ||
| } | ||
@@ -423,0 +425,0 @@ /** |
+3
-2
| /// <reference lib="esnext.temporal" /> | ||
| import { Logger } from "@logtape/logtape"; | ||
| //#region src/contexts.d.ts | ||
@@ -420,3 +419,5 @@ declare const preloadedContexts: Record<string, unknown>; | ||
| declare class UrlError extends Error { | ||
| constructor(message: string); | ||
| constructor(message: string, options?: { | ||
| cause?: unknown; | ||
| }); | ||
| } | ||
@@ -423,0 +424,0 @@ /** |
| require("./chunk-Do9eywBl.cjs"); | ||
| require("./docloader-CNNuAaeD.cjs"); | ||
| require("./url-Ds8K8vxN.cjs"); | ||
| require("./docloader-BPX9K2hq.cjs"); | ||
| require("./url-CWU5HT3Y.cjs"); | ||
| require("./key-DTTIntwb.cjs"); | ||
@@ -5,0 +5,0 @@ require("./multibase-F7LtMMsK.cjs"); |
@@ -1,3 +0,3 @@ | ||
| import "./docloader-DhbeXWJm.mjs"; | ||
| import "./url-m9Qzxy-Y.mjs"; | ||
| import "./docloader-BU_97FNC.mjs"; | ||
| import "./url-o0e-aXQY.mjs"; | ||
| import "./key-BeTHFQJK.mjs"; | ||
@@ -4,0 +4,0 @@ import "./multibase-BgU9XRf7.mjs"; |
| const require_chunk = require("./chunk-Do9eywBl.cjs"); | ||
| const require_docloader = require("./docloader-CNNuAaeD.cjs"); | ||
| const require_request = require("./request-B_7NkCEW.cjs"); | ||
| const require_url = require("./url-Ds8K8vxN.cjs"); | ||
| const require_docloader = require("./docloader-BPX9K2hq.cjs"); | ||
| const require_request = require("./request-Dn7JEQCQ.cjs"); | ||
| const require_url = require("./url-CWU5HT3Y.cjs"); | ||
| let node_assert = require("node:assert"); | ||
@@ -6,0 +6,0 @@ let node_test = require("node:test"); |
@@ -1,4 +0,4 @@ | ||
| import { n as preloadedContexts, t as getDocumentLoader } from "./docloader-DhbeXWJm.mjs"; | ||
| import { t as FetchError } from "./request-IHuVvkne.mjs"; | ||
| import { t as UrlError } from "./url-m9Qzxy-Y.mjs"; | ||
| import { n as preloadedContexts, t as getDocumentLoader } from "./docloader-BU_97FNC.mjs"; | ||
| import { t as FetchError } from "./request-DhGuwatz.mjs"; | ||
| import { t as UrlError } from "./url-o0e-aXQY.mjs"; | ||
| import { deepStrictEqual, ok, rejects } from "node:assert"; | ||
@@ -5,0 +5,0 @@ import { test } from "node:test"; |
| const require_chunk = require("./chunk-Do9eywBl.cjs"); | ||
| const require_request = require("./request-B_7NkCEW.cjs"); | ||
| const require_request = require("./request-Dn7JEQCQ.cjs"); | ||
| let node_assert = require("node:assert"); | ||
@@ -4,0 +4,0 @@ let node_test = require("node:test"); |
@@ -1,2 +0,2 @@ | ||
| import { o as version, r as getUserAgent } from "./request-IHuVvkne.mjs"; | ||
| import { o as version, r as getUserAgent } from "./request-DhGuwatz.mjs"; | ||
| import { deepStrictEqual } from "node:assert"; | ||
@@ -3,0 +3,0 @@ import { test } from "node:test"; |
| require("./chunk-Do9eywBl.cjs"); | ||
| const require_url = require("./url-Ds8K8vxN.cjs"); | ||
| const require_url = require("./url-CWU5HT3Y.cjs"); | ||
| let node_assert = require("node:assert"); | ||
@@ -32,2 +32,31 @@ let node_test = require("node:test"); | ||
| }); | ||
| (0, node_test.test)("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => { | ||
| require_url.validateLookupAddresses([ | ||
| { | ||
| address: "app-host.example.net.", | ||
| family: 4 | ||
| }, | ||
| { | ||
| address: "93.184.216.34", | ||
| family: 4 | ||
| }, | ||
| { | ||
| address: "2606:2800:220:1:248:1893:25c8:1946", | ||
| family: 6 | ||
| } | ||
| ]); | ||
| }); | ||
| (0, node_test.test)("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => { | ||
| (0, node_assert.throws)(() => require_url.validateLookupAddresses([{ | ||
| address: "private-host.example.net.", | ||
| family: 4 | ||
| }, { | ||
| address: "127.0.0.1", | ||
| family: 4 | ||
| }]), require_url.UrlError); | ||
| (0, node_assert.throws)(() => require_url.validateLookupAddresses([{ | ||
| address: "app-host.example.net.", | ||
| family: 4 | ||
| }]), require_url.UrlError); | ||
| }); | ||
| (0, node_test.test)("isValidPublicIPv4Address()", () => { | ||
@@ -34,0 +63,0 @@ (0, node_assert.ok)(require_url.isValidPublicIPv4Address("8.8.8.8")); |
@@ -1,3 +0,3 @@ | ||
| import { a as validatePublicUrl, i as isValidPublicIPv6Address, n as expandIPv6Address, r as isValidPublicIPv4Address, t as UrlError } from "./url-m9Qzxy-Y.mjs"; | ||
| import { deepStrictEqual, ok, rejects } from "node:assert"; | ||
| import { a as validateLookupAddresses, i as isValidPublicIPv6Address, n as expandIPv6Address, o as validatePublicUrl, r as isValidPublicIPv4Address, t as UrlError } from "./url-o0e-aXQY.mjs"; | ||
| import { deepStrictEqual, ok, rejects, throws } from "node:assert"; | ||
| import { test } from "node:test"; | ||
@@ -31,2 +31,31 @@ //#region src/url.test.ts | ||
| }); | ||
| test("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => { | ||
| validateLookupAddresses([ | ||
| { | ||
| address: "app-host.example.net.", | ||
| family: 4 | ||
| }, | ||
| { | ||
| address: "93.184.216.34", | ||
| family: 4 | ||
| }, | ||
| { | ||
| address: "2606:2800:220:1:248:1893:25c8:1946", | ||
| family: 6 | ||
| } | ||
| ]); | ||
| }); | ||
| test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => { | ||
| throws(() => validateLookupAddresses([{ | ||
| address: "private-host.example.net.", | ||
| family: 4 | ||
| }, { | ||
| address: "127.0.0.1", | ||
| family: 4 | ||
| }]), UrlError); | ||
| throws(() => validateLookupAddresses([{ | ||
| address: "app-host.example.net.", | ||
| family: 4 | ||
| }]), UrlError); | ||
| }); | ||
| test("isValidPublicIPv4Address()", () => { | ||
@@ -33,0 +62,0 @@ ok(isValidPublicIPv4Address("8.8.8.8")); |
+1
-1
| { | ||
| "name": "@fedify/vocab-runtime", | ||
| "version": "2.1.18", | ||
| "version": "2.1.19", | ||
| "homepage": "https://fedify.dev/", | ||
@@ -5,0 +5,0 @@ "repository": { |
+28
-1
@@ -1,2 +0,2 @@ | ||
| import { deepStrictEqual, ok, rejects } from "node:assert"; | ||
| import { deepStrictEqual, ok, rejects, throws } from "node:assert"; | ||
| import { test } from "node:test"; | ||
@@ -8,2 +8,3 @@ import { | ||
| UrlError, | ||
| validateLookupAddresses, | ||
| validatePublicUrl, | ||
@@ -69,2 +70,28 @@ } from "./url.ts"; | ||
| test("validateLookupAddresses() tolerates Cloudflare Workers CNAME entries", () => { | ||
| validateLookupAddresses([ | ||
| { address: "app-host.example.net.", family: 4 }, | ||
| { address: "93.184.216.34", family: 4 }, | ||
| { address: "2606:2800:220:1:248:1893:25c8:1946", family: 6 }, | ||
| ]); | ||
| }); | ||
| test("validateLookupAddresses() rejects unsafe or CNAME-only Cloudflare Workers results", () => { | ||
| throws( | ||
| () => | ||
| validateLookupAddresses([ | ||
| { address: "private-host.example.net.", family: 4 }, | ||
| { address: "127.0.0.1", family: 4 }, | ||
| ]), | ||
| UrlError, | ||
| ); | ||
| throws( | ||
| () => | ||
| validateLookupAddresses([ | ||
| { address: "app-host.example.net.", family: 4 }, | ||
| ]), | ||
| UrlError, | ||
| ); | ||
| }); | ||
| test("isValidPublicIPv4Address()", () => { | ||
@@ -71,0 +98,0 @@ ok(isValidPublicIPv4Address("8.8.8.8")); // Google DNS |
+35
-5
@@ -6,4 +6,4 @@ import type { LookupAddress } from "node:dns"; | ||
| export class UrlError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| constructor(message: string, options?: { cause?: unknown }) { | ||
| super(message, options); | ||
| this.name = "UrlError"; | ||
@@ -55,8 +55,38 @@ } | ||
| addresses = await lookup(hostname, { all: true }); | ||
| } catch { | ||
| addresses = []; | ||
| } catch (error) { | ||
| throw new UrlError("DNS lookup failed", { cause: error }); | ||
| } | ||
| for (const { address, family } of addresses) { | ||
| validateLookupAddresses(addresses); | ||
| } | ||
| /** | ||
| * Validates the IP addresses returned by `node:dns.lookup()`. | ||
| * | ||
| * Cloudflare Workers' `node:dns` implementation currently maps every record | ||
| * in a DNS-over-HTTPS `Answer` array—including CNAME records—to a | ||
| * `LookupAddress`, even though Node.js specifies that the `address` field must | ||
| * contain an IPv4 or IPv6 literal. See: | ||
| * https://github.com/cloudflare/workerd/issues/6886 | ||
| * | ||
| * Work around that bug by ignoring non-IP entries only when the lookup also | ||
| * returns at least one actual IP address. This remains fail-closed: a result | ||
| * containing no IP addresses is rejected, and every returned IP address is | ||
| * still validated and must be public. This workaround can be revisited once | ||
| * the Workerd issue is fixed in supported Cloudflare Workers runtimes. | ||
| * | ||
| * @internal | ||
| */ | ||
| export function validateLookupAddresses( | ||
| addresses: readonly LookupAddress[], | ||
| ): void { | ||
| let ipAddressCount = 0; | ||
| for (const { address } of addresses) { | ||
| const family = isIP(address); | ||
| if (family === 0) continue; | ||
| ipAddressCount++; | ||
| validatePublicIpAddress(address, family); | ||
| } | ||
| if (ipAddressCount === 0) { | ||
| throw new UrlError("DNS lookup did not return any IP address"); | ||
| } | ||
| } | ||
@@ -63,0 +93,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| const require_chunk = require("./chunk-Do9eywBl.cjs"); | ||
| let node_process = require("node:process"); | ||
| node_process = require_chunk.__toESM(node_process); | ||
| //#region deno.json | ||
| var name = "@fedify/vocab-runtime"; | ||
| var version = "2.1.18"; | ||
| //#endregion | ||
| //#region src/request.ts | ||
| /** | ||
| * Error thrown when fetching a JSON-LD document failed. | ||
| */ | ||
| var FetchError = class extends Error { | ||
| /** | ||
| * The URL that failed to fetch. | ||
| */ | ||
| url; | ||
| /** | ||
| * The HTTP response that failed, if available. | ||
| */ | ||
| response; | ||
| /** | ||
| * Constructs a new `FetchError`. | ||
| * | ||
| * @param url The URL that failed to fetch. | ||
| * @param message Error message. | ||
| * @param response The failed HTTP response, if available. | ||
| */ | ||
| constructor(url, message, response) { | ||
| super(message == null ? url.toString() : `${url}: ${message}`); | ||
| this.name = "FetchError"; | ||
| this.url = typeof url === "string" ? new URL(url) : url; | ||
| this.response = response; | ||
| } | ||
| }; | ||
| /** | ||
| * Creates a request for the given URL. | ||
| * @param url The URL to create the request for. | ||
| * @param options The options for the request. | ||
| * @returns The created request. | ||
| * @internal | ||
| */ | ||
| function createActivityPubRequest(url, options = {}) { | ||
| return new Request(url, { | ||
| headers: { | ||
| Accept: "application/activity+json, application/ld+json", | ||
| "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent) | ||
| }, | ||
| redirect: "manual" | ||
| }); | ||
| } | ||
| /** | ||
| * Gets the user agent string for the given application and URL. | ||
| * @param options The options for making the user agent string. | ||
| * @returns The user agent string. | ||
| * @since 1.3.0 | ||
| */ | ||
| function getUserAgent({ software, url } = {}) { | ||
| const fedify = `Fedify/${version}`; | ||
| const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${node_process.default.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${node_process.default.versions.node}` : null; | ||
| const userAgent = software == null ? [fedify] : [software, fedify]; | ||
| if (runtime != null) userAgent.push(runtime); | ||
| if (url != null) userAgent.push(`+${url.toString()}`); | ||
| return `${userAgent.shift()} (${userAgent.join("; ")})`; | ||
| } | ||
| /** | ||
| * Logs the request. | ||
| * @param request The request to log. | ||
| * @internal | ||
| */ | ||
| function logRequest(logger, request) { | ||
| logger.debug("Fetching document: {method} {url} {headers}", { | ||
| method: request.method, | ||
| url: request.url, | ||
| headers: Object.fromEntries(request.headers.entries()) | ||
| }); | ||
| } | ||
| //#endregion | ||
| Object.defineProperty(exports, "FetchError", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return FetchError; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "createActivityPubRequest", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return createActivityPubRequest; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "getUserAgent", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return getUserAgent; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "logRequest", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return logRequest; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "name", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return name; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "version", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return version; | ||
| } | ||
| }); |
| import process from "node:process"; | ||
| //#region deno.json | ||
| var name = "@fedify/vocab-runtime"; | ||
| var version = "2.1.18"; | ||
| //#endregion | ||
| //#region src/request.ts | ||
| /** | ||
| * Error thrown when fetching a JSON-LD document failed. | ||
| */ | ||
| var FetchError = class extends Error { | ||
| /** | ||
| * The URL that failed to fetch. | ||
| */ | ||
| url; | ||
| /** | ||
| * The HTTP response that failed, if available. | ||
| */ | ||
| response; | ||
| /** | ||
| * Constructs a new `FetchError`. | ||
| * | ||
| * @param url The URL that failed to fetch. | ||
| * @param message Error message. | ||
| * @param response The failed HTTP response, if available. | ||
| */ | ||
| constructor(url, message, response) { | ||
| super(message == null ? url.toString() : `${url}: ${message}`); | ||
| this.name = "FetchError"; | ||
| this.url = typeof url === "string" ? new URL(url) : url; | ||
| this.response = response; | ||
| } | ||
| }; | ||
| /** | ||
| * Creates a request for the given URL. | ||
| * @param url The URL to create the request for. | ||
| * @param options The options for the request. | ||
| * @returns The created request. | ||
| * @internal | ||
| */ | ||
| function createActivityPubRequest(url, options = {}) { | ||
| return new Request(url, { | ||
| headers: { | ||
| Accept: "application/activity+json, application/ld+json", | ||
| "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent) | ||
| }, | ||
| redirect: "manual" | ||
| }); | ||
| } | ||
| /** | ||
| * Gets the user agent string for the given application and URL. | ||
| * @param options The options for making the user agent string. | ||
| * @returns The user agent string. | ||
| * @since 1.3.0 | ||
| */ | ||
| function getUserAgent({ software, url } = {}) { | ||
| const fedify = `Fedify/${version}`; | ||
| const runtime = globalThis.Deno?.version?.deno != null ? `Deno/${Deno.version.deno}` : globalThis.process?.versions?.bun != null ? `Bun/${process.versions.bun}` : "navigator" in globalThis && navigator.userAgent === "Cloudflare-Workers" ? navigator.userAgent : globalThis.process?.versions?.node != null ? `Node.js/${process.versions.node}` : null; | ||
| const userAgent = software == null ? [fedify] : [software, fedify]; | ||
| if (runtime != null) userAgent.push(runtime); | ||
| if (url != null) userAgent.push(`+${url.toString()}`); | ||
| return `${userAgent.shift()} (${userAgent.join("; ")})`; | ||
| } | ||
| /** | ||
| * Logs the request. | ||
| * @param request The request to log. | ||
| * @internal | ||
| */ | ||
| function logRequest(logger, request) { | ||
| logger.debug("Fetching document: {method} {url} {headers}", { | ||
| method: request.method, | ||
| url: request.url, | ||
| headers: Object.fromEntries(request.headers.entries()) | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { name as a, logRequest as i, createActivityPubRequest as n, version as o, getUserAgent as r, FetchError as t }; |
| require("./chunk-Do9eywBl.cjs"); | ||
| let node_dns_promises = require("node:dns/promises"); | ||
| let node_net = require("node:net"); | ||
| //#region src/url.ts | ||
| var UrlError = class extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = "UrlError"; | ||
| } | ||
| }; | ||
| /** | ||
| * Validates a URL to prevent SSRF attacks. | ||
| */ | ||
| async function validatePublicUrl(url) { | ||
| const parsed = new URL(url); | ||
| if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsed.protocol}`); | ||
| let hostname = parsed.hostname; | ||
| if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.slice(1, -1); | ||
| if (hostname === "localhost") throw new UrlError("Localhost is not allowed"); | ||
| const hostnameFamily = (0, node_net.isIP)(hostname); | ||
| if (hostnameFamily !== 0) { | ||
| validatePublicIpAddress(hostname, hostnameFamily); | ||
| return; | ||
| } | ||
| if ("Deno" in globalThis && !(0, node_net.isIP)(hostname)) { | ||
| if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return; | ||
| } | ||
| if ("Bun" in globalThis) { | ||
| if (hostname === "example.com" || hostname.endsWith(".example.com")) return; | ||
| else if (hostname === "fedify-test.internal") throw new UrlError("Invalid or private address: fedify-test.internal"); | ||
| } | ||
| let addresses; | ||
| try { | ||
| addresses = await (0, node_dns_promises.lookup)(hostname, { all: true }); | ||
| } catch { | ||
| addresses = []; | ||
| } | ||
| for (const { address, family } of addresses) validatePublicIpAddress(address, family); | ||
| } | ||
| function validatePublicIpAddress(address, family) { | ||
| if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return; | ||
| throw new UrlError(`Invalid or private address: ${address}`); | ||
| } | ||
| function isValidPublicIPv4Address(address) { | ||
| const parts = parseIPv4Address(address); | ||
| if (parts == null) return false; | ||
| const value = ipv4PartsToNumber(parts); | ||
| return !nonPublicIPv4Prefixes.some(({ base, prefix }) => matchesIPv4Prefix(value, base, prefix)); | ||
| } | ||
| function isValidPublicIPv6Address(address) { | ||
| const words = parseIPv6Address(address); | ||
| if (words == null) return false; | ||
| if (nonPublicIPv6Prefixes.some(({ words: prefixWords, prefix }) => matchesIPv6Prefix(words, prefixWords, prefix))) return false; | ||
| for (const { extractIPv4, prefix, words: prefixWords } of ipv6WithIPv4Prefixes) { | ||
| if (!matchesIPv6Prefix(words, prefixWords, prefix)) continue; | ||
| const ipv4Address = extractIPv4(words); | ||
| if (ipv4Address != null && !isValidPublicIPv4Address(ipv4Address)) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function expandIPv6Address(address) { | ||
| address = address.toLowerCase(); | ||
| const ipv4Delimiter = address.lastIndexOf(":"); | ||
| if (address.includes(".") && ipv4Delimiter >= 0) { | ||
| const ipv4Parts = parseIPv4Address(address.substring(ipv4Delimiter + 1)); | ||
| if (ipv4Parts == null) return address; | ||
| const high = (ipv4Parts[0] << 8) + ipv4Parts[1]; | ||
| const low = (ipv4Parts[2] << 8) + ipv4Parts[3]; | ||
| address = address.substring(0, ipv4Delimiter + 1) + high.toString(16) + ":" + low.toString(16); | ||
| } | ||
| if (address === "::") return "0000:0000:0000:0000:0000:0000:0000:0000"; | ||
| if (address.startsWith("::")) address = "0000" + address; | ||
| if (address.endsWith("::")) address = address + "0000"; | ||
| address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":"); | ||
| return address.split(":").map((part) => part.padStart(4, "0")).join(":"); | ||
| } | ||
| const nonPublicIPv4Prefixes = [ | ||
| ipv4Prefix("0.0.0.0/8", "RFC 6890"), | ||
| ipv4Prefix("10.0.0.0/8", "RFC 1918"), | ||
| ipv4Prefix("100.64.0.0/10", "RFC 6598"), | ||
| ipv4Prefix("127.0.0.0/8", "RFC 1122"), | ||
| ipv4Prefix("169.254.0.0/16", "RFC 3927"), | ||
| ipv4Prefix("172.16.0.0/12", "RFC 1918"), | ||
| ipv4Prefix("192.0.0.0/24", "RFC 6890"), | ||
| ipv4Prefix("192.0.2.0/24", "RFC 5737"), | ||
| ipv4Prefix("192.88.99.0/24", "RFC 7526"), | ||
| ipv4Prefix("192.168.0.0/16", "RFC 1918"), | ||
| ipv4Prefix("198.18.0.0/15", "RFC 2544"), | ||
| ipv4Prefix("198.51.100.0/24", "RFC 5737"), | ||
| ipv4Prefix("203.0.113.0/24", "RFC 5737"), | ||
| ipv4Prefix("224.0.0.0/4", "RFC 5771"), | ||
| ipv4Prefix("240.0.0.0/4", "RFC 1112") | ||
| ]; | ||
| const nonPublicIPv6Prefixes = [ | ||
| ipv6Prefix("::/16", "RFC 4291"), | ||
| ipv6Prefix("2001::/32", "RFC 4380"), | ||
| ipv6Prefix("2002::/16", "RFC 3056"), | ||
| ipv6Prefix("64:ff9b:1::/48", "RFC 8215"), | ||
| ipv6Prefix("fc00::/7", "RFC 4193"), | ||
| ipv6Prefix("fe80::/10", "RFC 4291"), | ||
| ipv6Prefix("ff00::/8", "RFC 4291") | ||
| ]; | ||
| const ipv6WithIPv4Prefixes = [{ | ||
| ...ipv6Prefix("64:ff9b::/96", "RFC 6052"), | ||
| extractIPv4: (words) => ipv4FromWords(words[6], words[7]) | ||
| }]; | ||
| function ipv4Prefix(cidr, rfc) { | ||
| const [address, prefixText] = cidr.split("/"); | ||
| const prefix = parseInt(prefixText, 10); | ||
| const parts = parseIPv4Address(address); | ||
| if (parts == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error(`Invalid IPv4 prefix: ${cidr}`); | ||
| return { | ||
| cidr, | ||
| base: ipv4PartsToNumber(parts), | ||
| prefix, | ||
| rfc | ||
| }; | ||
| } | ||
| function ipv6Prefix(cidr, rfc) { | ||
| const [address, prefixText] = cidr.split("/"); | ||
| const prefix = parseInt(prefixText, 10); | ||
| const words = parseIPv6Address(address); | ||
| if (words == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 128) throw new Error(`Invalid IPv6 prefix: ${cidr}`); | ||
| return { | ||
| cidr, | ||
| words, | ||
| prefix, | ||
| rfc | ||
| }; | ||
| } | ||
| function parseIPv4Address(address) { | ||
| const parts = address.split(".").map((part) => { | ||
| if (!/^\d+$/.test(part)) return NaN; | ||
| return parseInt(part, 10); | ||
| }); | ||
| if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null; | ||
| return parts; | ||
| } | ||
| function parseIPv6Address(address) { | ||
| const parts = expandIPv6Address(address).split(":"); | ||
| if (parts.length !== 8) return null; | ||
| const words = parts.map((part) => { | ||
| if (!/^[0-9a-f]{1,4}$/i.test(part)) return NaN; | ||
| return parseInt(part, 16); | ||
| }); | ||
| if (words.some((word) => !Number.isInteger(word) || word < 0 || word > 65535)) return null; | ||
| return words; | ||
| } | ||
| function ipv4PartsToNumber(parts) { | ||
| return parts[0] * 2 ** 24 + parts[1] * 2 ** 16 + parts[2] * 2 ** 8 + parts[3]; | ||
| } | ||
| function ipv4FromWords(highWord, lowWord) { | ||
| return [ | ||
| highWord >> 8, | ||
| highWord & 255, | ||
| lowWord >> 8, | ||
| lowWord & 255 | ||
| ].join("."); | ||
| } | ||
| function matchesIPv4Prefix(address, prefixBase, prefixLength) { | ||
| const blockSize = 2 ** (32 - prefixLength); | ||
| return Math.floor(address / blockSize) === Math.floor(prefixBase / blockSize); | ||
| } | ||
| function matchesIPv6Prefix(address, prefixWords, prefixLength) { | ||
| let remaining = prefixLength; | ||
| for (let i = 0; i < 8 && remaining > 0; i++) if (remaining >= 16) { | ||
| if (address[i] !== prefixWords[i]) return false; | ||
| remaining -= 16; | ||
| } else { | ||
| const mask = 65535 << 16 - remaining & 65535; | ||
| if ((address[i] & mask) !== (prefixWords[i] & mask)) return false; | ||
| remaining = 0; | ||
| } | ||
| return true; | ||
| } | ||
| //#endregion | ||
| Object.defineProperty(exports, "UrlError", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return UrlError; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "expandIPv6Address", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return expandIPv6Address; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "isValidPublicIPv4Address", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return isValidPublicIPv4Address; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "isValidPublicIPv6Address", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return isValidPublicIPv6Address; | ||
| } | ||
| }); | ||
| Object.defineProperty(exports, "validatePublicUrl", { | ||
| enumerable: true, | ||
| get: function() { | ||
| return validatePublicUrl; | ||
| } | ||
| }); |
| import { lookup } from "node:dns/promises"; | ||
| import { isIP } from "node:net"; | ||
| //#region src/url.ts | ||
| var UrlError = class extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = "UrlError"; | ||
| } | ||
| }; | ||
| /** | ||
| * Validates a URL to prevent SSRF attacks. | ||
| */ | ||
| async function validatePublicUrl(url) { | ||
| const parsed = new URL(url); | ||
| if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new UrlError(`Unsupported protocol: ${parsed.protocol}`); | ||
| let hostname = parsed.hostname; | ||
| if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.slice(1, -1); | ||
| if (hostname === "localhost") throw new UrlError("Localhost is not allowed"); | ||
| const hostnameFamily = isIP(hostname); | ||
| if (hostnameFamily !== 0) { | ||
| validatePublicIpAddress(hostname, hostnameFamily); | ||
| return; | ||
| } | ||
| if ("Deno" in globalThis && !isIP(hostname)) { | ||
| if ((await Deno.permissions.query({ name: "net" })).state !== "granted") return; | ||
| } | ||
| if ("Bun" in globalThis) { | ||
| if (hostname === "example.com" || hostname.endsWith(".example.com")) return; | ||
| else if (hostname === "fedify-test.internal") throw new UrlError("Invalid or private address: fedify-test.internal"); | ||
| } | ||
| let addresses; | ||
| try { | ||
| addresses = await lookup(hostname, { all: true }); | ||
| } catch { | ||
| addresses = []; | ||
| } | ||
| for (const { address, family } of addresses) validatePublicIpAddress(address, family); | ||
| } | ||
| function validatePublicIpAddress(address, family) { | ||
| if (family === 4 && isValidPublicIPv4Address(address) || family === 6 && isValidPublicIPv6Address(address)) return; | ||
| throw new UrlError(`Invalid or private address: ${address}`); | ||
| } | ||
| function isValidPublicIPv4Address(address) { | ||
| const parts = parseIPv4Address(address); | ||
| if (parts == null) return false; | ||
| const value = ipv4PartsToNumber(parts); | ||
| return !nonPublicIPv4Prefixes.some(({ base, prefix }) => matchesIPv4Prefix(value, base, prefix)); | ||
| } | ||
| function isValidPublicIPv6Address(address) { | ||
| const words = parseIPv6Address(address); | ||
| if (words == null) return false; | ||
| if (nonPublicIPv6Prefixes.some(({ words: prefixWords, prefix }) => matchesIPv6Prefix(words, prefixWords, prefix))) return false; | ||
| for (const { extractIPv4, prefix, words: prefixWords } of ipv6WithIPv4Prefixes) { | ||
| if (!matchesIPv6Prefix(words, prefixWords, prefix)) continue; | ||
| const ipv4Address = extractIPv4(words); | ||
| if (ipv4Address != null && !isValidPublicIPv4Address(ipv4Address)) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function expandIPv6Address(address) { | ||
| address = address.toLowerCase(); | ||
| const ipv4Delimiter = address.lastIndexOf(":"); | ||
| if (address.includes(".") && ipv4Delimiter >= 0) { | ||
| const ipv4Parts = parseIPv4Address(address.substring(ipv4Delimiter + 1)); | ||
| if (ipv4Parts == null) return address; | ||
| const high = (ipv4Parts[0] << 8) + ipv4Parts[1]; | ||
| const low = (ipv4Parts[2] << 8) + ipv4Parts[3]; | ||
| address = address.substring(0, ipv4Delimiter + 1) + high.toString(16) + ":" + low.toString(16); | ||
| } | ||
| if (address === "::") return "0000:0000:0000:0000:0000:0000:0000:0000"; | ||
| if (address.startsWith("::")) address = "0000" + address; | ||
| if (address.endsWith("::")) address = address + "0000"; | ||
| address = address.replace("::", ":0000".repeat(8 - (address.match(/:/g) || []).length) + ":"); | ||
| return address.split(":").map((part) => part.padStart(4, "0")).join(":"); | ||
| } | ||
| const nonPublicIPv4Prefixes = [ | ||
| ipv4Prefix("0.0.0.0/8", "RFC 6890"), | ||
| ipv4Prefix("10.0.0.0/8", "RFC 1918"), | ||
| ipv4Prefix("100.64.0.0/10", "RFC 6598"), | ||
| ipv4Prefix("127.0.0.0/8", "RFC 1122"), | ||
| ipv4Prefix("169.254.0.0/16", "RFC 3927"), | ||
| ipv4Prefix("172.16.0.0/12", "RFC 1918"), | ||
| ipv4Prefix("192.0.0.0/24", "RFC 6890"), | ||
| ipv4Prefix("192.0.2.0/24", "RFC 5737"), | ||
| ipv4Prefix("192.88.99.0/24", "RFC 7526"), | ||
| ipv4Prefix("192.168.0.0/16", "RFC 1918"), | ||
| ipv4Prefix("198.18.0.0/15", "RFC 2544"), | ||
| ipv4Prefix("198.51.100.0/24", "RFC 5737"), | ||
| ipv4Prefix("203.0.113.0/24", "RFC 5737"), | ||
| ipv4Prefix("224.0.0.0/4", "RFC 5771"), | ||
| ipv4Prefix("240.0.0.0/4", "RFC 1112") | ||
| ]; | ||
| const nonPublicIPv6Prefixes = [ | ||
| ipv6Prefix("::/16", "RFC 4291"), | ||
| ipv6Prefix("2001::/32", "RFC 4380"), | ||
| ipv6Prefix("2002::/16", "RFC 3056"), | ||
| ipv6Prefix("64:ff9b:1::/48", "RFC 8215"), | ||
| ipv6Prefix("fc00::/7", "RFC 4193"), | ||
| ipv6Prefix("fe80::/10", "RFC 4291"), | ||
| ipv6Prefix("ff00::/8", "RFC 4291") | ||
| ]; | ||
| const ipv6WithIPv4Prefixes = [{ | ||
| ...ipv6Prefix("64:ff9b::/96", "RFC 6052"), | ||
| extractIPv4: (words) => ipv4FromWords(words[6], words[7]) | ||
| }]; | ||
| function ipv4Prefix(cidr, rfc) { | ||
| const [address, prefixText] = cidr.split("/"); | ||
| const prefix = parseInt(prefixText, 10); | ||
| const parts = parseIPv4Address(address); | ||
| if (parts == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error(`Invalid IPv4 prefix: ${cidr}`); | ||
| return { | ||
| cidr, | ||
| base: ipv4PartsToNumber(parts), | ||
| prefix, | ||
| rfc | ||
| }; | ||
| } | ||
| function ipv6Prefix(cidr, rfc) { | ||
| const [address, prefixText] = cidr.split("/"); | ||
| const prefix = parseInt(prefixText, 10); | ||
| const words = parseIPv6Address(address); | ||
| if (words == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 128) throw new Error(`Invalid IPv6 prefix: ${cidr}`); | ||
| return { | ||
| cidr, | ||
| words, | ||
| prefix, | ||
| rfc | ||
| }; | ||
| } | ||
| function parseIPv4Address(address) { | ||
| const parts = address.split(".").map((part) => { | ||
| if (!/^\d+$/.test(part)) return NaN; | ||
| return parseInt(part, 10); | ||
| }); | ||
| if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null; | ||
| return parts; | ||
| } | ||
| function parseIPv6Address(address) { | ||
| const parts = expandIPv6Address(address).split(":"); | ||
| if (parts.length !== 8) return null; | ||
| const words = parts.map((part) => { | ||
| if (!/^[0-9a-f]{1,4}$/i.test(part)) return NaN; | ||
| return parseInt(part, 16); | ||
| }); | ||
| if (words.some((word) => !Number.isInteger(word) || word < 0 || word > 65535)) return null; | ||
| return words; | ||
| } | ||
| function ipv4PartsToNumber(parts) { | ||
| return parts[0] * 2 ** 24 + parts[1] * 2 ** 16 + parts[2] * 2 ** 8 + parts[3]; | ||
| } | ||
| function ipv4FromWords(highWord, lowWord) { | ||
| return [ | ||
| highWord >> 8, | ||
| highWord & 255, | ||
| lowWord >> 8, | ||
| lowWord & 255 | ||
| ].join("."); | ||
| } | ||
| function matchesIPv4Prefix(address, prefixBase, prefixLength) { | ||
| const blockSize = 2 ** (32 - prefixLength); | ||
| return Math.floor(address / blockSize) === Math.floor(prefixBase / blockSize); | ||
| } | ||
| function matchesIPv6Prefix(address, prefixWords, prefixLength) { | ||
| let remaining = prefixLength; | ||
| for (let i = 0; i < 8 && remaining > 0; i++) if (remaining >= 16) { | ||
| if (address[i] !== prefixWords[i]) return false; | ||
| remaining -= 16; | ||
| } else { | ||
| const mask = 65535 << 16 - remaining & 65535; | ||
| if ((address[i] & mask) !== (prefixWords[i] & mask)) return false; | ||
| remaining = 0; | ||
| } | ||
| return true; | ||
| } | ||
| //#endregion | ||
| export { validatePublicUrl as a, isValidPublicIPv6Address as i, expandIPv6Address as n, isValidPublicIPv4Address as r, UrlError as t }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Network access
Supply chain riskThis module accesses the network.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1499120
0.55%41799
0.55%