Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integrity configuration not working #327

Closed
niyoko opened this issue Dec 19, 2020 · 24 comments · Fixed by #392
Closed

integrity configuration not working #327

niyoko opened this issue Dec 19, 2020 · 24 comments · Fixed by #392
Labels

Comments

@niyoko
Copy link

niyoko commented Dec 19, 2020

Describe the bug
According to the docs, setting integrity: true should generate jsIntegrity in the output. But in my case, jsIntegrity is not generated.

To Reproduce
I have made a minimal reproducible example. Steps to reproduce the behavior:

  1. Clone https://github.com/niyoko/assets-webpack-plugin-issue
  2. Run npm install
  3. Run npm run build
  4. View dist/webpack-assets.json

Expected behavior
In dist/webpack-assets.json should includes integrity hash for the outputed JS.

Webpack Config

const path = require('path');
const AssetsPlugin = require('assets-webpack-plugin');
const SriPlugin = require('webpack-subresource-integrity');

module.exports = {
  mode: 'production',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name]-bundle-[contenthash].js',
    publicPath: '/js/',
    crossOriginLoading: 'anonymous',
  },
  plugins: [
    new AssetsPlugin({
      path: path.join(__dirname, 'dist'),
      prettyPrint: true,
      integrity: true,
    }),
    new SriPlugin({
      hashFuncNames: ['sha256', 'sha384'],
      enabled: true,
    }),
  ],
};

Desktop (please complete the following information):

  • OS: ArchLinux 5.4.83-1-lts
  • Node version: 14.15.1
  • Plugin version: 7.0.0

Additional context

  • webpack-subresource-integrity version: 1.5.2
@niyoko niyoko changed the title integrity configuration not working integrity configuration not working Dec 19, 2020
@ztoben ztoben added the bug label Dec 21, 2020
@roebuk
Copy link

roebuk commented Feb 22, 2021

I've also experienced this issue and after some digging the change from emit to after-emit introduced in 6.1.0 causes this issue. It's also only an issue when your file output contains a hash [name].[contenthash].js. If you're using [name].js, it works flawlessly.

If the this code is switched back from after-emit to emit everything appears to work as intended.

For example, from:

if (compiler.hooks) {
  const plugin = { name: 'AssetsWebpackPlugin' }
  compiler.hooks.afterEmit.tapAsync(plugin, afterEmit)
} else {
  compiler.plugin('after-emit', afterEmit)
}

to:

if (compiler.hooks) {	
  const plugin = { name: 'AssetsWebpackPlugin' }	
  compiler.hooks.emit.tapAsync(plugin, emit)	
} else {	
  compiler.plugin('emit', emit)	
}

@ztoben
Copy link
Owner

ztoben commented Feb 22, 2021

@roebuk, using after-emit rather than emit was so that we can access the emitted files in the processOutput function. I'm not sure what the best way forward is on this tbh.

@roebuk
Copy link

roebuk commented Feb 22, 2021

I don't know how webpack works, so this is a complete stab in the dark. Would it be possible to get the integrity details on emit, then attach those details to the rest of assets on after-emit?

It's certainly a bit of a faff, but the integrity details just don't seem to be present on after-emit.

@markcampbell24
Copy link

markcampbell24 commented Feb 22, 2021

@ztoben I'm in the same situation as @roebuk , my configuration is Webpack 5.22.0 and assets-webpack-plugin v7.
I too wasn't able to get the interity value either, however I tried switching from afterEmit to emit which seems to work just fine, however perhaps worth noting that I also have configuration for processOutput that seems to be working.

@babyginger
Copy link

Hi @ztoben, any news on this issue?

@ztoben
Copy link
Owner

ztoben commented Feb 26, 2021

@markcampbell24 would you mind sharing your config? Going to see if I can figure this out later today.

@ztoben
Copy link
Owner

ztoben commented Feb 26, 2021

@geldmacher Could you chime in here? I know you were having issues accessing the files in the processOutput function when we were using the afterEmit before. Is this still an issue? Sounds like this isn't the case for others.

@markcampbell24
Copy link

markcampbell24 commented Feb 26, 2021

@ztoben sure,

const path = require('path');
const webpack = require('webpack');
const fs = require('fs');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const AssetsWebpackPlugin = require('assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const SriPlugin = require('webpack-subresource-integrity');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const devMode = process.env.NODE_ENV !== 'production';
const minimizers = devMode ? [] : [new OptimizeCSSAssetsPlugin({}), new TerserPlugin()];

module.exports = {
  target: ["web", "es5"],
  externals: {
      moment: 'moment'
  },
  devtool: devMode ? 'cheap-source-map' : 'source-map',
  entry: {
    'main': './src/scripts/index.js',
    'style': './src/css/style.scss',
  },
  output: {
    crossOriginLoading: 'anonymous',
    filename: devMode ? '[name].bundle.min.js' : '[name].bundle.[contenthash].min.js',
    path: path.resolve(__dirname, 'dist')
  },
  optimization: {
    minimize: !devMode,
    minimizer: minimizers,
    realContentHash: false,
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        },
        library: {
          test: /[\\/]src[\\/]scripts[\\/]library[\\/]/,
          name: 'library',
          chunks: 'all'
        }
      }
    }
  },
  resolve: {
    alias: {
      'chart.js': 'chart.js/dist/Chart.bundle.js'
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
      },
      {
        test: /\.(css|s[ac]ss)$/i,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              url: (url) => {
                return fs.existsSync(path.resolve(__dirname, `src/images/`, `${url}`));
              }
            }
          },
          'postcss-loader',
          'sass-loader'
        ]
      },
      {
        test: /\.(svg|png|jpg|gif|cur)$/u,
        use: [
          {
            loader: 'file-loader',
            options: {
              publicPath: '../',
              context: 'src',
              name: devMode ? '[path][name].[ext]' : '[path][name].[contenthash:6].[ext]'
            }
          }
        ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          'file-loader',
        ],
      },
    ],
  },
  plugins: [
    new SriPlugin({
      hashFuncNames: ['sha384'],
      enabled: !devMode
    }),
    new CleanWebpackPlugin(),
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery'
    }),
    new BundleAnalyzerPlugin({ 
      analyzerMode: 'static' 
    }),
    new MiniCssExtractPlugin({
      filename: devMode ? 'css/[name].css' : 'css/[name].[contenthash:6].css',
      chunkFilename: devMode ? 'css/[id].css' : 'css/[id].[contenthash:6].css'
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, `node_modules/tinymce/plugins`),
          to: './tinymce/plugins'
        },
        {
          from: path.resolve(__dirname, `node_modules/tinymce/icons`),
          to: './tinymce/icons'
        },
        {
          from: path.resolve(__dirname, `node_modules/tinymce/themes/silver`),
          to: './tinymce/themes/silver'
        },
        {
          from: path.resolve(__dirname, `node_modules/tinymce/skins`),
          to: './tinymce/skins'
        },
      ]
    }),
    new RemoveEmptyScriptsPlugin(),
    new AssetsWebpackPlugin({
      fileName: 'webpack.assets.json',
      fullPath: false,
      path: './dist',
      integrity: !devMode,
      prettyPrint: true,
      processOutput: function (assets) {
        return JSON.stringify(assets).replace('"":', '"assets":');
      }
    }),
  ]
}

@geldmacher
Copy link
Contributor

geldmacher commented Feb 26, 2021

@ztoben Yes, with emit instead of afterEmit it is still not possible to interact with the generated files in processOutput, because they are not emitted yet.

But switching to emit does not enable the integrity hash in my case. Seems like the integrity property is no longer a property of the asset object.

I have tested it with the neweset version of Webpack and the SRI plugin.

@markcampbell24
Copy link

markcampbell24 commented Feb 26, 2021

@geldmacher Hiya, mine does exactly the same with latest verison of Webpack, SRI and assets-webpack-plugin unless you add this into the optimization section of the webpack config [plus the change from afterEmit to emit]

realContentHash: false,

then you get the integrity values in the output.

I'm by no means an expert in Webpack but thought it might be worth you looking at it

@geldmacher
Copy link
Contributor

I am not sure if this should be needed to make the whole setup work. realContentHash seems to be a good feature i would like to use. =) And it is enabled by default ...

What about generating integrity hashes on your own via the processOutput function? I am doing this as well with this function (besides other stuff). Since we have access to the generated files due to using afterEmit this is no big deal.

I am using this processOutput function in my Webpack config to transform the outputted json file to my needs while including integrity hashes as well:

const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

/**
 * Processing assets plugin output
 * @see https://www.npmjs.com/package/assets-webpack-plugin
 *
 * @param publicPath
 * @param outputPath
 * @param isProduction
 * @param includeIntegrityHashes
 * @param algo
 * @returns {function(*=): *}
 */
const processOutput = (
  publicPath,
  outputPath,
  isProduction,
  includeIntegrityHashes,
  algo = 'sha384'
) => {
  return (assets) => {

    delete assets.entrypoints;
    delete assets.integrity;

    const integrity = {};

    for (const asset in assets) {
      for (const fileType in assets[asset]) {
        if (!Array.isArray(assets[asset][fileType])) {
          assets[asset][fileType] = [assets[asset][fileType]];
        }

        if (isProduction) {
          let fileMayHaveWebpVersion = false;
          const webpOriginFileTypes = ['png', 'jpg', 'jpeg'];
          webpOriginFileTypes.forEach(webpOriginFileType => {
            if (fileType === webpOriginFileType) {
              fileMayHaveWebpVersion = true;
            }
          });

          for (const file of assets[asset][fileType]) {
            try {
              const filePath = path.resolve(
                outputPath,
                file.replace(publicPath, '')
              );
              const fileAccessible = fs.existsSync(filePath);
              const fileContent = fileAccessible
                ? fs.readFileSync(filePath, 'utf8')
                : ''
              ;

              if (fileMayHaveWebpVersion) {
                const webpFilePath = filePath + '.webp';
                const webpFileAccessible = fs.existsSync(
                  webpFilePath
                );

                if (webpFileAccessible) {
                  assets[asset]['webp'] = [file + '.webp'];
                }
              }

              if (includeIntegrityHashes) {
                if (file in integrity) {
                  continue;
                }
                if (fileContent) {
                  const fileHashes = [];
                  const hash = crypto.createHash(algo);
                  hash.update(fileContent, 'utf8');
                  fileHashes.push(
                    `${algo}-${hash.digest('base64')}`
                  );
                  integrity[file] = fileHashes.join(' ');
                }
              }
            } catch (e) {
              console.error(e);
            }
          }
        }
      }
    }

    const manifestContent = {
      entrypoints: assets,
    };
    if (includeIntegrityHashes && Object.keys(integrity).length > 0) {
      manifestContent.integrity = integrity;
    }

    return JSON.stringify(manifestContent, null, 2);
  };
};

module.exports = processOutput;

Should be possible to mimic the SRI plugin integration this way. Perhaps it is possible to integrate this as a native feature in this plugin @ztoben ? I am not sure if the SRI plugin is doing some extra stuff anyone would need in this plugin!?

@ztoben
Copy link
Owner

ztoben commented Feb 26, 2021

I think it'd make more sense to figure out a way to use the after-emit to process the realContentHash and use the emit for everything else. Would rather not re-implement the SRI stuff here. I'll think about this for a bit, but I think there must be a way to do this.

@markcampbell24
Copy link

Hi @ztoben, just wondered if you had any more news on this issue?

@ztoben
Copy link
Owner

ztoben commented Mar 1, 2021

@markcampbell24, no sorry not yet. Been extremely busy with work so I haven't had time.

@markcampbell24
Copy link

no worries thanks @ztoben

@markcampbell24
Copy link

Hi, any news on this issue @ztoben?

@markcampbell24
Copy link

Hi, any news on this issue?

@ztoben
Copy link
Owner

ztoben commented Mar 22, 2021

@markcampbell24 no, sorry I just don't have the time to work on it right now. Happy to take a look at a PR if you can figure it out.

@lebleb
Copy link
Contributor

lebleb commented Apr 22, 2021

Content and Map of this Source is no longer available

@markcampbell24 , Webpack 5 replaces the Sources in Compilation.assets with SizeOnlySource variants to reduce memory usage before afterEmit hook call. https://webpack.js.org/blog/2020-10-10-webpack-5-release/#sizeonlysource-after-emit

Is it not possible to update the hooks part for only Webpack 5?

from:

    if (compiler.hooks) {
      const plugin = { name: 'AssetsWebpackPlugin' }

      compiler.hooks.afterEmit.tapAsync(plugin, afterEmit)
    } else {
      compiler.plugin('after-emit', afterEmit)
    }

to:

   if (compiler.hooks) {
     const plugin = { name: 'AssetsWebpackPlugin' }

     compiler.hooks.emit.tapAsync(plugin, emitPlugin)
   } else {
     compiler.plugin('after-emit', emitPlugin)
   }

@ztoben
Copy link
Owner

ztoben commented Apr 23, 2021

Should be fixed in v7.1.0, let me know otherwise. Thanks again @lebleb!

@soluml
Copy link

soluml commented Apr 28, 2021

So I'm still running into issues with Webpack 5 under a certain condition:

If the output name contains "[contenthash]" and if the mode is set to "production". I have not disabled the real content hash under optimization.

If I disable realContentHash then everything works.

Any ideas @ztoben? This doesn't seem fully fixed quite yet.

@ztoben
Copy link
Owner

ztoben commented Apr 29, 2021

@soluml that sounds like a different issue. Could you open a ticket?

@Den-dp
Copy link
Contributor

Den-dp commented May 14, 2021

I found that the current fix from #392 (using emit hook instead of afterEmit) - doesn't play well with the new output.clean webpack option (See #404)

Den-dp added a commit to Den-dp/assets-webpack-plugin that referenced this issue May 17, 2021
To allow further refactoring/fixing of ztoben#327 since current solution breaks ztoben#404 and symfony/webpack-encore#969

Resolves: ztoben#392 ztoben#327
@Den-dp
Copy link
Contributor

Den-dp commented May 17, 2021

It looks like assetEmitted is the right hook for reading the integrity, but with only one exception - it's called for each emitted asset.

Simple solution like that works fine when we have only one asset (and even fixes #404)

-    const emitPlugin = (compilation, callback) => {
+    const emitPlugin = (file, { content, source, outputPath, compilation, targetPath }, callback) => {
-          const compilationAsset = compilation.assets[asset]
-          const integrity = compilationAsset && compilationAsset.integrity
+          const integrity = source && source.integrity
-    compiler.hooks.emit.tapAsync(plugin, emitPlugin)
+    compiler.hooks.assetEmitted.tapAsync(plugin, emitPlugin)

but doesn't work when we have multiple assets emitted (since called multiple times).

I also added test #406 for it in order to simplify further fixes

Den-dp added a commit to Den-dp/assets-webpack-plugin that referenced this issue Jun 23, 2021
To allow further refactoring/fixing of ztoben#327 since current solution breaks ztoben#404 and symfony/webpack-encore#969

Resolves: ztoben#392 ztoben#327
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants