webpack-subresource-integrity
Version:
Webpack plugin for enabling Subresource Integrity
203 lines • 7.66 kB
JavaScript
/**
* Copyright (c) 2015-present, Waysact Pty Ltd
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { createHash } from "crypto";
import { Plugin } from "./plugin";
import { Reporter } from "./reporter";
import { findChunks, placeholderPrefix, generateSriHashPlaceholders, sriHashVariableReference, } from "./util";
import { install } from "./hooks";
import { AddLazySriRuntimeModule } from "./manifest";
import { thisPluginName, standardHashFuncNames } from "./globals";
/**
* The webpack-subresource-integrity plugin.
*
* @public
*/
export class SubresourceIntegrityPlugin {
options;
/**
* Create a new instance.
*
* @public
*/
constructor(options = {}) {
if (typeof options !== "object") {
throw new Error("webpack-subresource-integrity: argument must be an object");
}
this.options = {
hashFuncNames: ["sha384"],
enabled: "auto",
hashLoading: "eager",
...options,
};
}
/**
* @internal
*/
isEnabled(compilation) {
if (this.options.enabled === "auto") {
return compilation.options.mode !== "development";
}
return this.options.enabled;
}
/**
* @internal
*/
setup = (compilation, hwpHooks) => {
const reporter = new Reporter(compilation);
if (!this.validateOptions(compilation, reporter) ||
!this.isEnabled(compilation)) {
return;
}
const plugin = new Plugin(compilation, this.options, reporter);
if (typeof compilation.outputOptions.chunkLoading === "string" &&
["require", "async-node"].includes(compilation.outputOptions.chunkLoading)) {
reporter.warnNonWeb();
return;
}
compilation.hooks.beforeRuntimeRequirements.tap(thisPluginName, () => {
plugin.beforeRuntimeRequirements();
});
compilation.hooks.processAssets.tap({
name: thisPluginName,
stage: compilation.compiler.webpack.Compilation
.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
}, (records) => {
return plugin.processAssets(records);
});
compilation.hooks.afterProcessAssets.tap(thisPluginName, (records) => {
for (const chunk of compilation.chunks.values()) {
for (const chunkFile of chunk.files) {
const record = records[chunkFile];
if (record && record.source().includes(placeholderPrefix)) {
reporter.errorUnresolvedIntegrity(chunkFile);
}
}
}
});
compilation.compiler.webpack.optimize.RealContentHashPlugin.getCompilationHooks(compilation).updateHash.tap(thisPluginName, (input, oldHash) => {
// FIXME: remove type hack pending https://github.com/webpack/webpack/pull/12642#issuecomment-784744910
return plugin.updateHash(input, oldHash);
});
if (hwpHooks) {
hwpHooks.beforeAssetTagGeneration.tapPromise(thisPluginName, async (pluginArgs) => {
plugin.handleHwpPluginArgs(pluginArgs);
return pluginArgs;
});
hwpHooks.alterAssetTagGroups.tapPromise({
name: thisPluginName,
stage: 10000,
}, async (data) => {
plugin.handleHwpBodyTags(data);
return data;
});
}
const { mainTemplate } = compilation;
mainTemplate.hooks.jsonpScript.tap(thisPluginName, (source) => plugin.addAttribute("script", source));
mainTemplate.hooks.linkPreload.tap(thisPluginName, (source) => plugin.addAttribute("link", source));
mainTemplate.hooks.localVars.tap(thisPluginName, (source, chunk) => {
const allChunks = this.options.hashLoading === "lazy"
? plugin.getChildChunksToAddToChunkManifest(chunk)
: findChunks(chunk);
const includedChunks = chunk.getChunkMaps(false).hash;
if (Object.keys(includedChunks).length > 0) {
return compilation.compiler.webpack.Template.asString([
source,
`${sriHashVariableReference} = ` +
JSON.stringify(generateSriHashPlaceholders(Array.from(allChunks).filter((depChunk) => depChunk.id !== null &&
includedChunks[depChunk.id.toString()]), this.options.hashFuncNames)) +
";",
]);
}
return source;
});
if (this.options.hashLoading === "lazy") {
compilation.hooks.additionalChunkRuntimeRequirements.tap(thisPluginName, (chunk) => {
const childChunks = plugin.getChildChunksToAddToChunkManifest(chunk);
if (childChunks.size > 0 && !chunk.hasRuntime()) {
compilation.addRuntimeModule(chunk, new AddLazySriRuntimeModule(generateSriHashPlaceholders(childChunks, this.options.hashFuncNames), chunk.name ?? chunk.id));
}
});
}
};
/**
* @internal
*/
validateOptions = (compilation, reporter) => {
if (this.isEnabled(compilation) &&
!compilation.compiler.options.output.crossOriginLoading) {
reporter.warnCrossOriginPolicy();
}
return (this.validateHashFuncNames(reporter) && this.validateHashLoading(reporter));
};
/**
* @internal
*/
validateHashFuncNames = (reporter) => {
if (!Array.isArray(this.options.hashFuncNames)) {
reporter.errorHashFuncsNonArray(this.options.hashFuncNames);
return false;
}
else if (this.options.hashFuncNames.length === 0) {
reporter.errorHashFuncsEmpty();
return false;
}
else if (!this.options.hashFuncNames.every(this.validateHashFuncName.bind(this, reporter))) {
return false;
}
else {
this.warnStandardHashFunc(reporter);
return true;
}
};
/**
* @internal
*/
validateHashLoading = (reporter) => {
const supportedHashLoadingOptions = Object.freeze(["eager", "lazy"]);
if (supportedHashLoadingOptions.includes(this.options.hashLoading)) {
return true;
}
reporter.errorInvalidHashLoading(this.options.hashLoading, supportedHashLoadingOptions);
return false;
};
/**
* @internal
*/
warnStandardHashFunc = (reporter) => {
let foundStandardHashFunc = false;
this.options.hashFuncNames.forEach((hashFuncName) => {
if (standardHashFuncNames.indexOf(hashFuncName) >= 0) {
foundStandardHashFunc = true;
}
});
if (!foundStandardHashFunc) {
reporter.warnStandardHashFuncs();
}
};
/**
* @internal
*/
validateHashFuncName = (reporter, hashFuncName) => {
if (typeof hashFuncName !== "string" &&
!(hashFuncName instanceof String)) {
reporter.errorNonStringHashFunc(hashFuncName);
return false;
}
try {
createHash(hashFuncName);
}
catch (error) {
reporter.errorUnusableHashFunc(hashFuncName, error);
return false;
}
return true;
};
apply(compiler) {
install(compiler, this.setup);
}
}
//# sourceMappingURL=index.js.map