Spaces:
Sleeping
Sleeping
| import { lookup } from 'node:dns/promises' | |
| import { realpath } from 'node:fs/promises' | |
| import { isIP } from 'node:net' | |
| import path from 'node:path' | |
| const BLOCKED_HOSTS = new Set([ | |
| 'localhost', | |
| 'localhost.localdomain', | |
| 'metadata.google.internal', | |
| ]) | |
| const SENSITIVE_BASENAMES = new Set([ | |
| '.npmrc', | |
| '.pypirc', | |
| 'credentials', | |
| 'credentials.json', | |
| 'id_dsa', | |
| 'id_ed25519', | |
| 'id_rsa', | |
| 'known_hosts', | |
| ]) | |
| function isPrivateIpv4(address) { | |
| const parts = address.split('.').map(Number) | |
| if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) { | |
| return true | |
| } | |
| const [a, b] = parts | |
| 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 === 168) || | |
| a >= 224 | |
| ) | |
| } | |
| function isPrivateIpv6(address) { | |
| const normalized = address.toLowerCase().split('%')[0] | |
| if (normalized === '::' || normalized === '::1') return true | |
| if (normalized.startsWith('fc') || normalized.startsWith('fd') || normalized.startsWith('fe8') || normalized.startsWith('fe9') || normalized.startsWith('fea') || normalized.startsWith('feb')) { | |
| return true | |
| } | |
| if (normalized.startsWith('::ffff:')) { | |
| return isPrivateIpv4(normalized.slice('::ffff:'.length)) | |
| } | |
| return false | |
| } | |
| export function isPrivateAddress(address) { | |
| const version = isIP(address) | |
| if (version === 4) return isPrivateIpv4(address) | |
| if (version === 6) return isPrivateIpv6(address) | |
| return true | |
| } | |
| export function parseAllowedHosts(value = process.env.SOURCE_ALLOWED_HOSTS ?? '') { | |
| return new Set( | |
| value | |
| .split(',') | |
| .map((host) => host.trim().toLowerCase()) | |
| .filter(Boolean), | |
| ) | |
| } | |
| export async function validatePublicUrl(rawUrl, { | |
| allowedHosts = parseAllowedHosts(), | |
| lookupFn = lookup, | |
| } = {}) { | |
| const url = new URL(rawUrl) | |
| if (!['http:', 'https:'].includes(url.protocol)) { | |
| throw new Error('Only http:// and https:// sources are allowed.') | |
| } | |
| if (url.username || url.password) { | |
| throw new Error('Source URLs cannot contain credentials.') | |
| } | |
| const hostname = url.hostname.toLowerCase().replace(/\.$/, '') | |
| if (BLOCKED_HOSTS.has(hostname) || hostname.endsWith('.localhost') || hostname.endsWith('.local')) { | |
| throw new Error('Local and private network sources are blocked.') | |
| } | |
| if (allowedHosts.size > 0 && !allowedHosts.has(hostname)) { | |
| throw new Error(`Host is not allowlisted: ${hostname}`) | |
| } | |
| if (isIP(hostname)) { | |
| if (isPrivateAddress(hostname)) throw new Error('Local and private network sources are blocked.') | |
| } else { | |
| const addresses = await lookupFn(hostname, { all: true, verbatim: true }) | |
| if (!addresses.length || addresses.some(({ address }) => isPrivateAddress(address))) { | |
| throw new Error('The source hostname resolves to a private or invalid address.') | |
| } | |
| } | |
| return url | |
| } | |
| export async function fetchPublic(rawUrl, { | |
| fetchImpl = fetch, | |
| lookupFn = lookup, | |
| allowedHosts = parseAllowedHosts(), | |
| maxRedirects = 5, | |
| init = {}, | |
| } = {}) { | |
| let current = rawUrl | |
| for (let redirect = 0; redirect <= maxRedirects; redirect += 1) { | |
| const url = await validatePublicUrl(current, { allowedHosts, lookupFn }) | |
| const response = await fetchImpl(url, { | |
| ...init, | |
| redirect: 'manual', | |
| headers: { | |
| 'user-agent': 'ComplexitySourceMCP/0.1 (+https://github.com/Complexity-ML)', | |
| ...(init.headers ?? {}), | |
| }, | |
| }) | |
| if (![301, 302, 303, 307, 308].includes(response.status)) return response | |
| const location = response.headers.get('location') | |
| if (!location) throw new Error('Source returned a redirect without a location.') | |
| current = new URL(location, url).toString() | |
| } | |
| throw new Error(`Source exceeded ${maxRedirects} redirects.`) | |
| } | |
| export function parseAllowedRoots(value = process.env.SOURCE_ROOTS ?? process.cwd()) { | |
| return value | |
| .split(path.delimiter) | |
| .map((root) => root.trim()) | |
| .filter(Boolean) | |
| .map((root) => path.resolve(root)) | |
| } | |
| export function assertNonSensitivePath(candidate) { | |
| const basename = path.basename(candidate).toLowerCase() | |
| const extension = path.extname(basename) | |
| const envFile = basename === '.env' || (basename.startsWith('.env.') && basename !== '.env.example') | |
| if ( | |
| envFile || | |
| SENSITIVE_BASENAMES.has(basename) || | |
| ['.key', '.p12', '.pfx', '.pem'].includes(extension) | |
| ) { | |
| throw new Error(`Sensitive credential file is not available as a source: ${basename}`) | |
| } | |
| } | |
| export async function resolveAllowedPath(requestedPath, roots = parseAllowedRoots()) { | |
| const candidate = await realpath(path.resolve(requestedPath)) | |
| const canonicalRoots = await Promise.all(roots.map((root) => realpath(root))) | |
| const allowed = canonicalRoots.some((root) => candidate === root || candidate.startsWith(`${root}${path.sep}`)) | |
| if (!allowed) throw new Error(`Path is outside the configured source roots: ${requestedPath}`) | |
| assertNonSensitivePath(candidate) | |
| return candidate | |
| } | |