UNPKG

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

Version:

Fast screenshot capture tool for web pages - optimized for Claude Vision API

161 lines (160 loc) 5.42 kB
import { lookup } from 'dns/promises'; import { isIP } from 'net'; const SAFE_PROTOCOLS = new Set(['http:', 'https:']); const LOCALHOST_HOSTNAMES = new Set(['localhost', 'localhost.localdomain']); function normalizeHostname(hostname) { return hostname.replace(/^\[|\]$/g, '').toLowerCase(); } function parseIpv4(address) { const parts = address.split('.'); if (parts.length !== 4) return null; const bytes = parts.map(part => Number(part)); if (bytes.some(byte => !Number.isInteger(byte) || byte < 0 || byte > 255 || !/^\d{1,3}$/.test(String(byte)))) { return null; } return bytes; } function isBlockedIpv4(address) { const bytes = parseIpv4(address); if (!bytes) return true; const [a, b, c, d] = bytes; return (a === 0 || a === 10 || a === 127 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 0 && c === 0) || (a === 192 && b === 0 && c === 2) || (a === 192 && b === 88 && c === 99) || (a === 192 && b === 168) || (a === 198 && (b === 18 || b === 19)) || (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113) || a >= 224 || (a === 255 && b === 255 && c === 255 && d === 255)); } function isLoopbackIpv4(address) { const bytes = parseIpv4(address); return bytes !== null && bytes[0] === 127; } function getMappedIpv4FromIpv6(address) { const normalized = address.toLowerCase(); const mappedIpv4 = normalized.match(/(?:::ffff:)(\d+\.\d+\.\d+\.\d+)$/); const mappedHexIpv4 = normalized.match(/(?:::ffff:)([0-9a-f]{1,4}):([0-9a-f]{1,4})$/); if (mappedIpv4) { return mappedIpv4[1]; } if (mappedHexIpv4) { const high = parseInt(mappedHexIpv4[1], 16); const low = parseInt(mappedHexIpv4[2], 16); return [ (high >> 8) & 255, high & 255, (low >> 8) & 255, low & 255, ].join('.'); } return null; } function isBlockedIpv6(address) { const normalized = address.toLowerCase(); const mappedAddress = getMappedIpv4FromIpv6(normalized); if (mappedAddress) { return isBlockedIpv4(mappedAddress); } return (normalized === '::' || normalized === '::1' || normalized.startsWith('fc') || normalized.startsWith('fd') || normalized.startsWith('fe8') || normalized.startsWith('fe9') || normalized.startsWith('fea') || normalized.startsWith('feb') || normalized.startsWith('ff') || normalized.startsWith('2001:db8:') || normalized === '2001:db8::'); } function isBlockedIpAddress(address) { const ipVersion = isIP(address); if (ipVersion === 4) { return isBlockedIpv4(address); } if (ipVersion === 6) { return isBlockedIpv6(address); } return true; } function isLocalhostHostname(hostname) { return LOCALHOST_HOSTNAMES.has(hostname) || hostname.endsWith('.localhost'); } function isBlockedHostname(hostname) { return hostname.endsWith('.local') || hostname.endsWith('.internal'); } function isLoopbackIpv6(address) { const normalized = address.toLowerCase(); const mappedAddress = getMappedIpv4FromIpv6(normalized); return (normalized === '::1' || (mappedAddress !== null && isLoopbackIpv4(mappedAddress))); } function isAllowedLocalAddress(address) { const ipVersion = isIP(address); if (ipVersion === 4) { return isLoopbackIpv4(address); } if (ipVersion === 6) { return isLoopbackIpv6(address); } return false; } async function resolveHostname(hostname) { const records = await lookup(hostname, { all: true, verbatim: false, }); return records.map(record => record.address); } export async function assertSafeCaptureUrl(url) { let parsedUrl; try { parsedUrl = new URL(url); } catch { throw new Error(`Blocked unsafe capture URL: ${url} is not a valid URL`); } if (!SAFE_PROTOCOLS.has(parsedUrl.protocol)) { throw new Error(`Blocked unsafe capture URL: ${parsedUrl.protocol || 'unknown'} URLs are not allowed`); } const hostname = normalizeHostname(parsedUrl.hostname); if (!hostname) { throw new Error(`Blocked unsafe capture URL: ${parsedUrl.hostname} is not an allowed host`); } if (isLocalhostHostname(hostname)) { return; } if (isBlockedHostname(hostname)) { throw new Error(`Blocked unsafe capture URL: ${parsedUrl.hostname} is not an allowed host`); } if (isIP(hostname)) { if (isAllowedLocalAddress(hostname)) { return; } if (isBlockedIpAddress(hostname)) { throw new Error(`Blocked unsafe capture URL: ${hostname} is not an allowed destination`); } return; } const addresses = await resolveHostname(hostname); if (addresses.length === 0) { throw new Error(`Blocked unsafe capture URL: ${hostname} did not resolve to an address`); } const blockedAddress = addresses.find(address => isBlockedIpAddress(address)); if (blockedAddress) { throw new Error(`Blocked unsafe capture URL: ${hostname} resolves to unsafe address ${blockedAddress}`); } }