Skip to content

Commit

Permalink
feat(sri): add native plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
LingyuCoder committed Feb 11, 2025
1 parent 584b43a commit ef242b5
Show file tree
Hide file tree
Showing 16 changed files with 723 additions and 101 deletions.
104 changes: 64 additions & 40 deletions crates/rspack_plugin_sri/src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use rayon::iter::{IntoParallelRefIterator, ParallelBridge, ParallelIterator};
use rspack_core::{
chunk_graph_chunk::ChunkId,
rspack_sources::{ReplaceSource, Source},
ChunkUkey, Compilation, CompilationAssets, CompilationProcessAssets, CrossOriginLoading,
ChunkUkey, Compilation, CompilationAfterProcessAssets, CompilationAssets,
CompilationProcessAssets, CrossOriginLoading,
};
use rspack_error::{Diagnostic, Result};
use rspack_hook::plugin_hook;
Expand All @@ -14,9 +15,11 @@ use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use crate::{
config::IntegrityHtmlPlugin,
integrity::{compute_integrity, SRIHashFunction},
util::{make_placeholder, use_any_hash, PLACEHOLDER_REGEX},
util::{make_placeholder, use_any_hash, PLACEHOLDER_PREFIX, PLACEHOLDER_REGEX},
IntegrityCallbackData, SRIPlugin, SRIPluginInner,
};

#[derive(Debug, Clone)]
struct ProcessChunkResult {
pub file: String,
pub source: Option<Arc<dyn Source>>,
Expand All @@ -40,7 +43,7 @@ fn process_chunks(
compilation.push_diagnostic(Diagnostic::warn(
"SubResourceIntegrity".to_string(),
r#"SRI requires a cross-origin policy, defaulting to "anonymous".
Set webpack option output.crossOriginLoading to a value other than false
Set rspack option output.crossOriginLoading to a value other than false
to make this warning go away.
See https://w3c.github.io/webappsec-subresource-integrity/#cross-origin-data-leakage"#
.to_string(),
Expand Down Expand Up @@ -84,54 +87,53 @@ See https://w3c.github.io/webappsec-subresource-integrity/#cross-origin-data-lea

let mut should_warn_content_hash = false;
for result in results {
for warning in result.warnings {
compilation.push_diagnostic(Diagnostic::warn(
"SubResourceIntegrity".to_string(),
warning,
));
}

let Some(integrity) = result.integrity else {
continue;
};

integrities.insert(result.file.clone(), integrity.clone());
if let Some(placeholder) = result.placeholder {
hash_by_placeholders.insert(placeholder, integrity.clone());
}

let real_content_hash = compilation.options.optimization.real_content_hash;

if let Some(source) = result.source {
if let Some(error) = compilation
.update_asset(&result.file, |_, info| Ok((Arc::new(source), info)))
.update_asset(&result.file, |_, info| {
if use_any_hash(&info) && (info.content_hash.is_empty() || !real_content_hash) {
should_warn_content_hash = true;
}

let mut new_info = info.clone();
new_info.content_hash.insert(integrity);
Ok((Arc::new(source), new_info))
})
.err()
{
compilation.push_diagnostic(Diagnostic::error(
"SubResourceIntegrity".to_string(),
format!("Failed to update asset '{}': {}", result.file, error),
));
} else {
let asset = compilation
.assets()
.get(&result.file)
.expect("should have asset");
if use_any_hash(&asset.info)
&& (asset.info.content_hash.is_empty()
|| !compilation.options.optimization.real_content_hash)
{
should_warn_content_hash = true;
}
}

if should_warn_content_hash {
compilation.push_diagnostic(Diagnostic::warn(
"SubResourceIntegrity".to_string(),
r#"Using [hash], [fullhash], [modulehash], or [chunkhash] is dangerous
}
}
if should_warn_content_hash {
compilation.push_diagnostic(Diagnostic::warn(
"SubResourceIntegrity".to_string(),
r#"Using [hash], [fullhash], [modulehash], or [chunkhash] is dangerous
with SRI. The same is true for [contenthash] when realContentHash is disabled.
Use [contenthash] and ensure realContentHash is enabled. See the README for
more information."#
.to_string(),
));
}
}

if let (Some(placeholder), Some(integrity)) = (result.placeholder, result.integrity.clone()) {
hash_by_placeholders.insert(placeholder, integrity);
}

if let Some(integrity) = result.integrity {
integrities.insert(result.file, integrity);
}

for warning in result.warnings {
compilation.push_diagnostic(Diagnostic::warn(
"SubResourceIntegrity".to_string(),
warning,
));
}
.to_string(),
));
}
}

Expand All @@ -151,7 +153,7 @@ fn process_chunk_source(
let mut warnings = vec![];
let source_content = source.source();
if source_content.contains("webpackHotUpdate") {
warnings.push("webpack-subresource-integrity may interfere with hot reloading. Consider disabling this plugin in development mode.".to_string());
warnings.push("SubResourceIntegrity: SubResourceIntegrityPlugin may interfere with hot reloading. Consider disabling this plugin in development mode.".to_string());
}

// replace placeholders with integrity hash
Expand Down Expand Up @@ -261,6 +263,28 @@ pub async fn handle_assets(&self, compilation: &mut Compilation) -> Result<()> {
Ok(())
}

#[plugin_hook(CompilationAfterProcessAssets for SRIPlugin)]
pub async fn detect_unresolved_integrity(&self, compilation: &mut Compilation) -> Result<()> {
let mut contain_unresolved_files = vec![];
for chunk in compilation.chunk_by_ukey.values() {
for file in chunk.files() {
if let Some(source) = compilation.assets().get(file).and_then(|a| a.get_source()) {
if source.source().contains(PLACEHOLDER_PREFIX.as_str()) {
contain_unresolved_files.push(file.to_string());
}
}
}
}

for file in contain_unresolved_files {
compilation.push_diagnostic(Diagnostic::error(
"SubResourceIntegrity".to_string(),
format!("Asset {} contains unresolved integrity placeholders", file),
));
}
Ok(())
}

#[plugin_hook(RealContentHashPluginUpdateHash for SRIPlugin)]
pub async fn update_hash(
&self,
Expand Down
20 changes: 18 additions & 2 deletions crates/rspack_plugin_sri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod util;

use std::sync::LazyLock;

use asset::{handle_assets, update_hash};
use asset::{detect_unresolved_integrity, handle_assets, update_hash};
use config::SRICompilationContext;
pub use config::{
IntegrityCallbackData, IntegrityCallbackFn, IntegrityHtmlPlugin, SRIPluginOptions,
Expand All @@ -16,7 +16,7 @@ use html::{alter_asset_tag_groups, before_asset_tag_generation};
pub use integrity::SRIHashFunction;
use rspack_core::{
ChunkLoading, ChunkLoadingType, Compilation, CompilationId, CompilationParams,
CompilerThisCompilation, Plugin, PluginContext,
CompilerThisCompilation, CrossOriginLoading, Plugin, PluginContext,
};
use rspack_error::{Diagnostic, Result};
use rspack_hook::{plugin, plugin_hook};
Expand Down Expand Up @@ -106,6 +106,16 @@ async fn handle_compilation(
.update_hash
.tap(update_hash::new(self));

if matches!(
compilation.options.output.cross_origin_loading,
CrossOriginLoading::Disable
) {
compilation.push_diagnostic(Diagnostic::error(
"SubResourceIntegrity".to_string(),
"rspack option output.crossOriginLoading not set, code splitting will not work!".to_string(),
));
}

let mut runtime_plugin_hooks = RuntimePlugin::get_compilation_hooks_mut(compilation.id());
runtime_plugin_hooks
.create_script
Expand Down Expand Up @@ -158,6 +168,12 @@ impl Plugin for SRIPlugin {
.process_assets
.tap(handle_assets::new(self));

ctx
.context
.compilation_hooks
.after_process_assets
.tap(detect_unresolved_integrity::new(self));

ctx
.context
.compiler_hooks
Expand Down
28 changes: 9 additions & 19 deletions crates/rspack_plugin_sri/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,13 @@ use crate::{
SRIHashFunction, SRIPlugin, SRIPluginInner,
};

fn add_attribute(
tag: &str,
code: &str,
cross_origin_loading: &CrossOriginLoading,
) -> Result<String> {
if matches!(cross_origin_loading, CrossOriginLoading::Disable) {
Err(error!(
"rspack option output.crossOriginLoading not set, code splitting will not work!"
))
} else {
Ok(format!(
"{}\n{tag}.integrity = {}[chunkId];\n{tag}.crossOrigin = {};",
code,
SRI_HASH_VARIABLE_REFERENCE.as_str(),
cross_origin_loading
))
}
fn add_attribute(tag: &str, code: &str, cross_origin_loading: &CrossOriginLoading) -> String {
format!(
"{}\n{tag}.integrity = {}[chunkId];\n{tag}.crossOrigin = {};",
code,
SRI_HASH_VARIABLE_REFERENCE.as_str(),
cross_origin_loading
)
}

#[impl_runtime_module]
Expand Down Expand Up @@ -128,14 +118,14 @@ fn generate_sri_hash_placeholders(
#[plugin_hook(RuntimePluginCreateScript for SRIPlugin)]
pub async fn create_script(&self, mut data: CreateScriptData) -> Result<CreateScriptData> {
let ctx = SRIPlugin::get_compilation_sri_context(data.chunk.compilation_id);
data.code = add_attribute("script", &data.code, &ctx.cross_origin_loading)?;
data.code = add_attribute("script", &data.code, &ctx.cross_origin_loading);
Ok(data)
}

#[plugin_hook(RuntimePluginLinkPreload for SRIPlugin)]
pub async fn link_preload(&self, mut data: LinkPreloadData) -> Result<LinkPreloadData> {
let ctx = SRIPlugin::get_compilation_sri_context(data.chunk.compilation_id);
data.code = add_attribute("link", &data.code, &ctx.cross_origin_loading)?;
data.code = add_attribute("link", &data.code, &ctx.cross_origin_loading);
Ok(data)
}

Expand Down
51 changes: 47 additions & 4 deletions packages/rspack/src/builtin-plugin/SRIPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
import { join, relative, sep } from "node:path";
import {
BuiltinPluginName,
JsRspackError,
type RawIntegrityData,
type RawSRIPluginOptions
} from "@rspack/binding";
Expand Down Expand Up @@ -100,11 +101,26 @@ const NativeSRIPlugin = create(
export class SRIPlugin extends NativeSRIPlugin {
private integrities: Map<string, string> = new Map();
private options: SRIPluginOptions;
private validateError: Error | null = null
constructor(options: SRIPluginOptions) {
validateSRIPluginOptions(options);
const finalOptions = {
let validateError: Error | null = null;
if (typeof options !== "object") {
throw new Error("SubResourceIntegrity: argument must be an object");
}
try {
validateSRIPluginOptions(options);
} catch (e) {
validateError = e as Error;
}

const finalOptions = validateError ? {
hashFuncNames: ["sha384"],
htmlPlugin: NATIVE_HTML_PLUGIN,
enabled: false
} : {
hashFuncNames: options.hashFuncNames ?? ["sha384"],
htmlPlugin: options.htmlPlugin ?? NATIVE_HTML_PLUGIN
htmlPlugin: options.htmlPlugin ?? NATIVE_HTML_PLUGIN,
enabled: options.enabled ?? "auto"
};
super({
...finalOptions,
Expand All @@ -114,7 +130,8 @@ export class SRIPlugin extends NativeSRIPlugin {
);
}
});
this.options = finalOptions;
this.validateError = validateError;
this.options = finalOptions as SRIPluginOptions;
}

private isEnabled(compiler: Compiler) {
Expand Down Expand Up @@ -198,11 +215,37 @@ export class SRIPlugin extends NativeSRIPlugin {

apply(compiler: Compiler): void {
if (!this.isEnabled(compiler)) {
if (this.validateError) {
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
compilation.errors.push(this.validateError as unknown as JsRspackError);
});
}
return;
}

super.apply(compiler);

compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
compilation.hooks.statsFactory.tap(PLUGIN_NAME, statsFactory => {
statsFactory.hooks.extract
.for("asset")
.tap(PLUGIN_NAME, (object, asset) => {
const contenthash = asset.info?.contenthash;
if (contenthash) {
const shaHashes = (
Array.isArray(contenthash) ? contenthash : [contenthash]
).filter((hash: unknown) => String(hash).match(/^sha[0-9]+-/));
if (shaHashes.length > 0) {
(object as unknown as {
integrity: string;
}).integrity =
shaHashes.join(" ");
}
}
});
});
});

if (
typeof this.options.htmlPlugin === "string" &&
this.options.htmlPlugin !== NATIVE_HTML_PLUGIN
Expand Down
Loading

0 comments on commit ef242b5

Please sign in to comment.