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
JavaScript
/**
* 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('/');
}