Fixes a bug where a directory with the same name as a file would be used as the matching entry instead of that file depending on the order they appear in the tarball.
205 lines
6.5 KiB
JavaScript
205 lines
6.5 KiB
JavaScript
import path from 'path';
|
|
|
|
import addLeadingSlash from '../utils/addLeadingSlash';
|
|
import createPackageURL from '../utils/createPackageURL';
|
|
import createSearch from '../utils/createSearch';
|
|
import fetchNpmPackage from '../utils/fetchNpmPackage';
|
|
import getIntegrity from '../utils/getIntegrity';
|
|
import getContentType from '../utils/getContentType';
|
|
|
|
function indexRedirect(req, res, entry) {
|
|
// Redirect to the index file so relative imports
|
|
// resolve correctly.
|
|
res
|
|
.set({
|
|
'Cache-Control': 'public, max-age=31536000', // 1 year
|
|
'Cache-Tag': 'redirect, index-redirect'
|
|
})
|
|
.redirect(
|
|
302,
|
|
createPackageURL(
|
|
req.packageName,
|
|
req.packageVersion,
|
|
addLeadingSlash(entry.name),
|
|
createSearch(req.query)
|
|
)
|
|
);
|
|
}
|
|
|
|
function stripLeadingSegment(name) {
|
|
return name.replace(/^[^/]+\/?/, '');
|
|
}
|
|
|
|
/**
|
|
* Search the given tarball for entries that match the given name.
|
|
* Follows node's resolution algorithm.
|
|
* https://nodejs.org/api/modules.html#modules_all_together
|
|
*/
|
|
function searchEntries(tarballStream, entryName, wantsIndex) {
|
|
return new Promise((resolve, reject) => {
|
|
const jsEntryName = `${entryName}.js`;
|
|
const jsonEntryName = `${entryName}.json`;
|
|
const entries = {};
|
|
|
|
let foundEntry;
|
|
|
|
if (entryName === '') {
|
|
foundEntry = entries[''] = { name: '', type: 'directory' };
|
|
}
|
|
|
|
tarballStream
|
|
.on('error', reject)
|
|
.on('entry', (header, stream, next) => {
|
|
const entry = {
|
|
// Most packages have header names that look like `package/index.js`
|
|
// so we shorten that to just `index.js` here. A few packages use a
|
|
// prefix other than `package/`. e.g. the firebase package uses the
|
|
// `firebase_npm/` prefix. So we just strip the first dir name.
|
|
name: stripLeadingSegment(header.name),
|
|
type: header.type
|
|
};
|
|
|
|
// Skip non-files and files that don't match the entryName.
|
|
if (entry.type !== 'file' || entry.name.indexOf(entryName) !== 0) {
|
|
stream.resume();
|
|
stream.on('end', next);
|
|
return;
|
|
}
|
|
|
|
entries[entry.name] = entry;
|
|
|
|
// Dynamically create "directory" entries for all directories
|
|
// that are in this file's path. Some tarballs omit these entries
|
|
// for some reason, so this is the "brute force" method.
|
|
let dir = path.dirname(entry.name);
|
|
while (dir !== '.') {
|
|
entries[dir] = entries[dir] || { name: dir, type: 'directory' };
|
|
dir = path.dirname(dir);
|
|
}
|
|
|
|
if (
|
|
entry.name === entryName ||
|
|
// Allow accessing e.g. `/index.js` or `/index.json`
|
|
// using `/index` for compatibility with npm
|
|
(!wantsIndex && entry.name === jsEntryName) ||
|
|
(!wantsIndex && entry.name === jsonEntryName)
|
|
) {
|
|
if (foundEntry) {
|
|
if (
|
|
foundEntry.name !== entryName &&
|
|
(entry.name === entryName ||
|
|
(entry.name === jsEntryName &&
|
|
foundEntry.name === jsonEntryName))
|
|
) {
|
|
// This entry is higher priority than the one
|
|
// we already found. Replace it.
|
|
delete foundEntry.content;
|
|
foundEntry = entry;
|
|
}
|
|
} else {
|
|
foundEntry = entry;
|
|
}
|
|
}
|
|
|
|
const chunks = [];
|
|
|
|
stream
|
|
.on('data', chunk => chunks.push(chunk))
|
|
.on('end', () => {
|
|
const content = Buffer.concat(chunks);
|
|
|
|
// Set some extra properties for files that we will
|
|
// need to serve them and for ?meta listings.
|
|
entry.contentType = getContentType(entry.name);
|
|
entry.integrity = getIntegrity(content);
|
|
entry.lastModified = header.mtime.toUTCString();
|
|
entry.size = content.length;
|
|
|
|
// Set the content only for the foundEntry and
|
|
// discard the buffer for all others.
|
|
if (entry === foundEntry) {
|
|
entry.content = content;
|
|
}
|
|
|
|
next();
|
|
});
|
|
})
|
|
.on('finish', () => {
|
|
resolve({
|
|
entries,
|
|
// If we didn't find a matching file entry,
|
|
// try a directory entry with the same name.
|
|
foundEntry: foundEntry || entries[entryName] || null
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
const leadingSlash = /^\//;
|
|
const multipleSlash = /\/\/+/;
|
|
const trailingSlash = /\/$/;
|
|
|
|
/**
|
|
* Fetch and search the archive to try and find the requested file.
|
|
* Redirect to the "index" file if a directory was requested.
|
|
*/
|
|
export default function findFile(req, res, next) {
|
|
fetchNpmPackage(req.packageConfig).then(tarballStream => {
|
|
const wantsIndex = trailingSlash.test(req.filename);
|
|
|
|
// The name of the file/directory we're looking for.
|
|
const entryName = req.filename
|
|
.replace(multipleSlash, '/')
|
|
.replace(trailingSlash, '')
|
|
.replace(leadingSlash, '');
|
|
|
|
searchEntries(tarballStream, entryName, wantsIndex).then(
|
|
({ entries, foundEntry }) => {
|
|
if (!foundEntry) {
|
|
return res
|
|
.status(404)
|
|
.set({
|
|
'Cache-Control': 'public, max-age=31536000', // 1 year
|
|
'Cache-Tag': 'missing, missing-entry'
|
|
})
|
|
.type('text')
|
|
.send(`Cannot find "${req.filename}" in ${req.packageSpec}`);
|
|
}
|
|
|
|
// If the foundEntry is a directory and there is no trailing slash
|
|
// on the request path, we need to redirect to some "index" file
|
|
// inside that directory. This is so our URLs work in a similar way
|
|
// to require("lib") in node where it searches for `lib/index.js`
|
|
// and `lib/index.json` when `lib` is a directory.
|
|
if (foundEntry.type === 'directory' && !wantsIndex) {
|
|
const indexEntry =
|
|
entries[path.join(entryName, 'index.js')] ||
|
|
entries[path.join(entryName, 'index.json')];
|
|
|
|
if (indexEntry && indexEntry.type === 'file') {
|
|
return indexRedirect(req, res, indexEntry);
|
|
} else {
|
|
return res
|
|
.status(404)
|
|
.set({
|
|
'Cache-Control': 'public, max-age=31536000', // 1 year
|
|
'Cache-Tag': 'missing, missing-index'
|
|
})
|
|
.type('text')
|
|
.send(
|
|
`Cannot find an index in "${req.filename}" in ${
|
|
req.packageSpec
|
|
}`
|
|
);
|
|
}
|
|
}
|
|
|
|
req.entries = entries;
|
|
req.entry = foundEntry;
|
|
|
|
next();
|
|
}
|
|
);
|
|
});
|
|
}
|