UNPKG

@just-every/mcp-read-website-fast

Version:

Markdown Content Preprocessor - Fetch web pages, extract content, convert to clean Markdown

173 lines (172 loc) 5.95 kB
import { lookup } from 'node:dns/promises'; import { isIP } from 'node:net'; const IPV4_BLOCKED_RANGES = [ [0x00000000n, 0xff000000n], [0x0a000000n, 0xff000000n], [0x64400000n, 0xffc00000n], [0x7f000000n, 0xff000000n], [0xa9fe0000n, 0xffff0000n], [0xac100000n, 0xfff00000n], [0xc0000000n, 0xffffff00n], [0xc0000200n, 0xffffff00n], [0xc0586300n, 0xffffff00n], [0xc0a80000n, 0xffff0000n], [0xc6120000n, 0xfffe0000n], [0xc6336400n, 0xffffff00n], [0xcb007100n, 0xffffff00n], [0xe0000000n, 0xf0000000n], [0xf0000000n, 0xf0000000n], ]; const IPV6_BLOCKED_RANGES = [ [0n, (1n << 128n) - 1n], [1n, (1n << 128n) - 1n], [0xffffn << 32n, ((1n << 96n) - 1n) << 32n], [0x0064ff9b000000000000000000000000n, prefixMask(96)], [0x01000000000000000000000000000000n, prefixMask(64)], [0x20010000000000000000000000000000n, prefixMask(23)], [0x20020000000000000000000000000000n, prefixMask(16)], [0xfc000000000000000000000000000000n, prefixMask(7)], [0xfe800000000000000000000000000000n, prefixMask(10)], [0xff000000000000000000000000000000n, prefixMask(8)], ]; export class UrlPolicyError extends Error { constructor(message) { super(message); this.name = 'UrlPolicyError'; } } export async function assertPublicHttpUrl(url) { return (await resolvePublicHttpUrl(url)).url; } export async function resolvePublicHttpUrl(url) { let parsed; try { parsed = new URL(url); } catch { throw new UrlPolicyError(`Invalid URL: ${url}`); } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { throw new UrlPolicyError(`URL scheme is not allowed: ${parsed.protocol}`); } if (parsed.username || parsed.password) { throw new UrlPolicyError('URL credentials are not allowed'); } const hostname = normalizeHostname(parsed.hostname); const hostIpVersion = isIP(hostname); if (hostIpVersion !== 0) { const family = ipVersionToFamily(hostIpVersion); assertPublicIp(hostname, family); return { url: parsed, hostname, addresses: [{ address: hostname, family }], }; } let addresses; try { addresses = await lookup(hostname, { all: true, verbatim: true }); } catch (error) { throw new UrlPolicyError(`Unable to resolve hostname "${hostname}": ${error instanceof Error ? error.message : 'Unknown DNS error'}`); } if (addresses.length === 0) { throw new UrlPolicyError(`Hostname "${hostname}" did not resolve`); } const publicAddresses = addresses.map(address => { const family = ipVersionToFamily(address.family); assertPublicIp(address.address, family); return { address: address.address, family }; }); return { url: parsed, hostname, addresses: publicAddresses }; } function normalizeHostname(hostname) { if (hostname.startsWith('[') && hostname.endsWith(']')) { return hostname.slice(1, -1); } return hostname; } function assertPublicIp(address, family) { if (family === 4) { if (isBlockedIpv4(address)) { throw new UrlPolicyError(`IP address is not allowed: ${address}`); } return; } if (family === 6) { const ipv4Mapped = address .toLowerCase() .match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); if (ipv4Mapped) { assertPublicIp(ipv4Mapped[1], 4); return; } if (isBlockedIpv6(address)) { throw new UrlPolicyError(`IP address is not allowed: ${address}`); } return; } throw new UrlPolicyError(`Unknown IP address family for ${address}`); } function ipVersionToFamily(family) { if (family === 4 || family === 6) { return family; } throw new UrlPolicyError(`Unknown IP address family: ${family}`); } function isBlockedIpv4(address) { const numeric = ipv4ToNumber(address); return IPV4_BLOCKED_RANGES.some(([range, mask]) => (numeric & mask) === range); } function ipv4ToNumber(address) { const parts = address.split('.').map(part => Number(part)); if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) { throw new UrlPolicyError(`Invalid IPv4 address: ${address}`); } return ((BigInt(parts[0]) << 24n) + (BigInt(parts[1]) << 16n) + (BigInt(parts[2]) << 8n) + BigInt(parts[3])); } function isBlockedIpv6(address) { const numeric = ipv6ToBigInt(address); return IPV6_BLOCKED_RANGES.some(([range, mask]) => (numeric & mask) === range); } function ipv6ToBigInt(address) { const lower = address.toLowerCase(); const [head = '', tail = ''] = lower.split('::'); const headParts = parseIpv6Parts(head); const tailParts = parseIpv6Parts(tail); const missing = 8 - headParts.length - tailParts.length; if (lower.includes('::')) { if (missing < 1) { throw new UrlPolicyError(`Invalid IPv6 address: ${address}`); } } else if (headParts.length !== 8) { throw new UrlPolicyError(`Invalid IPv6 address: ${address}`); } const parts = lower.includes('::') ? [...headParts, ...Array(missing).fill(0), ...tailParts] : headParts; return parts.reduce((acc, part) => (acc << 16n) + BigInt(part), 0n); } function parseIpv6Parts(value) { if (!value) { return []; } return value.split(':').map(part => { const parsed = Number.parseInt(part, 16); if (!part || !Number.isInteger(parsed) || parsed < 0 || parsed > 0xffff) { throw new UrlPolicyError(`Invalid IPv6 address part: ${part}`); } return parsed; }); } function prefixMask(prefixLength) { return ((1n << BigInt(prefixLength)) - 1n) << BigInt(128 - prefixLength); }