@fedify/fedify
Version:
An ActivityPub server framework
68 lines (64 loc) • 2.93 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import { URLPattern } from "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { lookup } from "node:dns/promises";
import { isIP } from "node:net";
//#region runtime/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.substring(1, hostname.length - 2);
if (hostname === "localhost") throw new UrlError("Localhost is not allowed");
if ("Deno" in globalThis && !isIP(hostname)) {
const netPermission = await Deno.permissions.query({ name: "net" });
if (netPermission.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) if (family === 4 && !isValidPublicIPv4Address(address) || family === 6 && !isValidPublicIPv6Address(address) || family < 4 || family === 5 || family > 6) throw new UrlError(`Invalid or private address: ${address}`);
}
function isValidPublicIPv4Address(address) {
const parts = address.split(".");
const first = parseInt(parts[0]);
if (first === 0 || first === 10 || first === 127) return false;
const second = parseInt(parts[1]);
if (first === 169 && second === 254) return false;
if (first === 172 && second >= 16 && second <= 31) return false;
if (first === 192 && second === 168) return false;
return true;
}
function isValidPublicIPv6Address(address) {
address = expandIPv6Address(address);
if (address.at(4) !== ":") return false;
const firstWord = parseInt(address.substring(0, 4), 16);
return !(firstWord >= 64512 && firstWord <= 65023 || firstWord >= 65152 && firstWord <= 65215 || firstWord === 0 || firstWord >= 65280);
}
function expandIPv6Address(address) {
address = address.toLowerCase();
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) + ":");
const parts = address.split(":");
return parts.map((part) => part.padStart(4, "0")).join(":");
}
//#endregion
export { UrlError, expandIPv6Address, isValidPublicIPv4Address, isValidPublicIPv6Address, validatePublicUrl };