UNPKG

ipfs-gateway-emulator

Version:

A server emulating IPFS gateway behaviour, for local preview and end-to-end tests

108 lines (98 loc) 3.69 kB
/** * The IPFS path-gateway behaviours this server emulates, expressed as pure * functions so they can be tested without binding a port. * * A path gateway serves a site under `/ipfs/<cid>/`, which differs from a * normal static host in two ways that routinely break apps: * * 1. the `/ipfs/<cid>` prefix is invisible to the site, so it must be * stripped before looking a file up, and * 2. a root-relative URL such as `/app.js` escapes the CID root and 404s, * even though it works fine when the same site is served from `/`. * * The second is the whole point of testing against this emulator: it fails * locally in the same way it would fail on a real gateway. */ /** `/ipfs/<cid>/rest` -> `/rest`, `/ipfs/<cid>` -> `/`. */ export function stripIpfsPrefix(pathname) { if (!pathname.startsWith('/ipfs/')) { return pathname; } const afterPrefix = pathname.slice('/ipfs/'.length); const slashIndex = afterPrefix.indexOf('/'); if (slashIndex === -1) { // `/ipfs/<cid>` with no trailing slash: the CID root itself. return '/'; } return pathname.slice('/ipfs/'.length + slashIndex); } /** * Decide what to do with a request, without touching the filesystem. * * `only` restricts which base paths are served, so a project can prove it works * under one addressing scheme in isolation: * - `'hash'` only `/ipfs/<cid>/...` is served, everything else 404s * - any other non-empty value: only root paths are served, `/ipfs/` 404s * - unset: both are served * * Returns either `{type: 'notFound', message}` or * `{type: 'serve', logicalPath}` where `logicalPath` is the path to resolve * against the served directory. */ export function routeRequest({pathname, referer, only}) { const isIpfsPath = pathname.startsWith('/ipfs/'); if (isIpfsPath) { if (only && only !== 'hash') { return {type: 'notFound', message: 'Not Found'}; } return {type: 'serve', logicalPath: stripIpfsPrefix(pathname)}; } // A root-relative request whose referer sits under /ipfs/ is exactly the // case a real path gateway cannot serve: the URL has escaped the CID root. if (referer) { let refererPath; try { refererPath = new URL(referer).pathname; } catch { refererPath = undefined; // a malformed referer is not our problem } if (refererPath && refererPath.startsWith('/ipfs/')) { return {type: 'notFound', message: 'Not Found (referer)'}; } } if (only === 'hash') { return {type: 'notFound', message: 'Not Found'}; } return {type: 'serve', logicalPath: pathname}; } /** * Parse the `--fail` spec, `<status>:<folder>[,<folder>]*`, used to make chosen * paths fail on demand so a client's error handling can be exercised. */ export function parseFailSpec(spec) { if (!spec) { return undefined; } const separatorIndex = spec.indexOf(':'); if (separatorIndex === -1) { return undefined; } const statusText = spec.slice(0, separatorIndex); const parsed = Number.parseInt(statusText, 10); const status = Number.isNaN(parsed) ? 500 : parsed; const folders = spec .slice(separatorIndex + 1) .split(',') .map((folder) => folder.trim()) .filter((folder) => folder.length > 0); return {status, folders}; } /** Whether `logicalPath` falls under one of the `--fail` folders. */ export function matchesFailFolder(logicalPath, folders) { const normalised = logicalPath.startsWith('/') ? logicalPath.slice(1) : logicalPath; return folders.some((folder) => normalised === folder || normalised.startsWith(`${folder}/`)); } /** Directories are redirected to a trailing slash, as a real gateway does. */ export function needsTrailingSlash(pathname) { return pathname.length > 0 && !pathname.endsWith('/'); }