webpack-userscript
Version:
A Webpack plugin for userscript projects.
206 lines • 8.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProcessSSRI = void 0;
const tslib_1 = require("tslib");
const node_path_1 = tslib_1.__importDefault(require("node:path"));
const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
const p_limit_1 = tslib_1.__importDefault(require("p-limit"));
const ssri_1 = require("ssri");
const url_1 = require("url");
const fs_1 = require("../fs");
const feature_1 = require("./feature");
class ProcessSSRI extends feature_1.Feature {
constructor() {
var _a;
super(...arguments);
this.name = 'ProcessSSRI';
this.allowedProtocols = new Set(['http:', 'https:']);
this.ssriLockDirty = false;
this.ssriLock = {};
this.limit = (0, p_limit_1.default)((_a = (typeof this.options.ssri === 'object'
? this.options.ssri.concurrency
: undefined)) !== null && _a !== void 0 ? _a : 6);
}
apply({ hooks }) {
const { ssri, root } = this.options;
if (!ssri) {
return;
}
// read lock
hooks.init.tapPromise(this.name, (compiler) => tslib_1.__awaiter(this, void 0, void 0, function* () {
const lockfile = this.getSSRILockFile(ssri);
if (!lockfile) {
return;
}
try {
this.ssriLock = this.parseSSRILock(yield (0, fs_1.readJSON)(node_path_1.default.resolve(root !== null && root !== void 0 ? root : compiler.context, lockfile), compiler.inputFileSystem));
}
catch (_a) { }
this.ssriLockDirty = false;
}));
// write lock
hooks.close.tapPromise(this.name, (compiler) => tslib_1.__awaiter(this, void 0, void 0, function* () {
const lock = this.getSSRILockFile(ssri);
if (!lock) {
return;
}
const lockfile = node_path_1.default.resolve(root !== null && root !== void 0 ? root : compiler.context, lock);
if (this.ssriLockDirty) {
const { intermediateFileSystem } = compiler;
const dir = node_path_1.default.dirname(lockfile);
const isNotRoot = node_path_1.default.dirname(dir) !== dir;
if (isNotRoot) {
yield (0, fs_1.mkdirp)(dir, intermediateFileSystem);
}
yield (0, fs_1.writeJSON)(lockfile, this.toRawSSRILock(this.ssriLock), intermediateFileSystem);
this.ssriLockDirty = false;
}
}));
hooks.headers.tapPromise(this.name, (headers) => tslib_1.__awaiter(this, void 0, void 0, function* () {
const targetURLs = this.getTargetURLs(headers, ssri).reduce((map, url) => map.set(url, this.normalizeURL(url)), new Map());
if (targetURLs.size === 0) {
return headers;
}
// merge integrities from ssri-lock
// and those provided within headers option (in respective tags)
for (const [url, normalizedURL] of targetURLs) {
const integrity = (0, ssri_1.parse)(this.parseSSRILike(url), {
strict: true,
});
if (integrity) {
if (this.ssriLock[normalizedURL]) {
integrity.merge(this.ssriLock[normalizedURL], {
strict: true,
});
}
this.ssriLockDirty = true;
this.ssriLock[normalizedURL] = integrity;
}
}
// compute and merge missing hashes based on specified algorithms option
yield Promise.all(Array.from(targetURLs.values()).map((normalizedURL) => tslib_1.__awaiter(this, void 0, void 0, function* () {
var _b;
const expectedAlgorithms = (_b = ssri.algorithms) !== null && _b !== void 0 ? _b : ['sha512'];
const missingAlgorithms = expectedAlgorithms.filter((alg) => { var _a; return !((_a = this.ssriLock[normalizedURL]) === null || _a === void 0 ? void 0 : _a[alg]); });
const newIntegrity = missingAlgorithms.length > 0
? yield this.limit(() => this.computeSSRI(normalizedURL, missingAlgorithms, {
strict: ssri.strict,
}))
: null;
if (newIntegrity) {
if (this.ssriLock[normalizedURL]) {
newIntegrity.merge(this.ssriLock[normalizedURL]);
}
this.ssriLock[normalizedURL] = newIntegrity;
this.ssriLockDirty = true;
}
})));
return Object.assign(Object.assign({}, headers), this.patchHeaders(headers, this.ssriLock));
}));
}
getSSRILockFile({ lock = true } = {}) {
return typeof lock === 'string'
? lock
: lock
? 'ssri-lock.json'
: undefined;
}
getTargetURLs(headers, options) {
const urls = [];
if (headers.require !== undefined) {
const requireURLs = (Array.isArray(headers.require) ? headers.require : [headers.require]).filter((url) => typeof url === 'string');
for (const urlStr of requireURLs) {
const url = this.parseURL(urlStr);
if (this.filterURL(url, 'require', options)) {
urls.push(this.stringifyURL(url));
}
}
}
if (headers.resource !== undefined) {
for (const urlStr of Object.values(headers.resource).filter((url) => typeof url === 'string')) {
const url = this.parseURL(urlStr);
if (this.filterURL(url, 'resource', options)) {
urls.push(this.stringifyURL(url));
}
}
}
return urls;
}
normalizeURL(url) {
const u = new url_1.URL('', this.parseURL(url));
u.hash = '';
return u.toString();
}
filterURL(url, tag, { include, exclude } = {}) {
if (!this.allowedProtocols.has(url.protocol)) {
return false;
}
if (include && !include(tag, url)) {
return false;
}
if (exclude && exclude(tag, url)) {
return false;
}
return true;
}
computeSSRI(url, algorithms, { strict }) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const response = yield (0, node_fetch_1.default)(url);
if (response.status !== 200 || response.body === null) {
throw new Error(`Failed to fetch SSRI sources. ` +
`[${response.status} ${response.statusText}] ${url}`);
}
return yield (0, ssri_1.fromStream)(response.body, {
algorithms,
strict,
});
});
}
parseSSRILike(url) {
return this.parseURL(url).hash.slice(1).replace(/[,;]/g, ' ');
}
parseSSRILock(rawSSRILock) {
return Object.fromEntries(Object.entries(rawSSRILock).map(([url, integrityLike]) => [
url,
(0, ssri_1.parse)(integrityLike, { strict: true }),
]));
}
toRawSSRILock(ssriLock) {
return Object.fromEntries(Object.entries(ssriLock).map(([url, integrity]) => [url, (0, ssri_1.stringify)(integrity, { strict: true })]));
}
parseURL(url) {
return new url_1.URL(url);
}
stringifyURL(url) {
return url.toString();
}
updateURL(url, ssriLock) {
const integrity = ssriLock[url];
if (!integrity)
return url;
const urlObj = this.parseURL(url);
urlObj.hash = '#' + (0, ssri_1.stringify)(integrity, { sep: ',', strict: true });
return urlObj.toString();
}
patchHeaders(headers, ssriLock) {
const headersProps = {};
if (headers.require !== undefined) {
if (Array.isArray(headers.require)) {
headersProps.require = headers.require
.filter((url) => typeof url === 'string')
.map((url) => this.updateURL(url, ssriLock));
}
else {
headersProps.require = this.updateURL(headers.require, ssriLock);
}
}
if (headers.resource !== undefined) {
headersProps.resource = Object.fromEntries(Object.entries(headers.resource)
.filter((entry) => typeof entry[1] === 'string')
.map(([name, url]) => [name, this.updateURL(url, ssriLock)]));
}
return headersProps;
}
}
exports.ProcessSSRI = ProcessSSRI;
//# sourceMappingURL=ssri.js.map