Skip to content

Commit

Permalink
Feature: Adding SRI hash(es) for HMR script resources (#1268)
Browse files Browse the repository at this point in the history
* feat(test-dev): Refactor test-dev logging mechanism

Removes the hard overwriting of console from jest and instead pipes it into a file that can be analyzed if something failes.

* feat(snowflake-build): Add synchronized SRI hash generator capability

* feat(build-snowflake): Adds integrity checks for HMR script injections

* tests(smoke): Replace snapshots with script tags including the SRI hash

resolves #1214

* fix(test-dev): Add special permission for opening file

* feat(snowpack): Refactor module and add more unit tests
  • Loading branch information
marpme authored Oct 11, 2020
1 parent 73c4a71 commit f57ca8a
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ test/build/**/build
test/create-snowpack-app/test-install
test/esinstall/**/web_modules
yarn-error.log
**/*.log
20 changes: 18 additions & 2 deletions snowpack/src/build/build-import-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type CSSModuleLoader from 'css-modules-loader-core';
import path from 'path';
import {readFileSync} from 'fs';
import {SnowpackConfig} from '../types/snowpack';
import {appendHtmlToHead, getExt} from '../util';
import {logger} from '../logger';
import {generateSRI} from './import-sri';

const SRI_CLIENT_HMR_SNOWPACK = generateSRI(
readFileSync(path.join(__dirname, '../../assets/hmr-client.js')),
);

const SRI_ERROR_HMR_SNOWPACK = generateSRI(
readFileSync(path.join(__dirname, '../../assets/hmr-error-overlay.js')),
);

export function getMetaUrlPath(urlPath: string, config: SnowpackConfig): string {
let {metaDir} = config.buildOptions || {};
Expand Down Expand Up @@ -75,9 +85,15 @@ export function wrapHtmlResponse({
});

if (hmr) {
let hmrScript = `<script type="module" src="${getMetaUrlPath('hmr-client.js', config)}"></script>`;
let hmrScript = `<script type="module" integrity="${SRI_CLIENT_HMR_SNOWPACK}" src="${getMetaUrlPath(
'hmr-client.js',
config,
)}"></script>`;
if (config.devOptions.hmrErrorOverlay) {
hmrScript += `<script type="module" src="${getMetaUrlPath('hmr-error-overlay.js', config)}"></script>`;
hmrScript += `<script type="module" integrity="${SRI_ERROR_HMR_SNOWPACK}" src="${getMetaUrlPath(
'hmr-error-overlay.js',
config,
)}"></script>`;
}
code = appendHtmlToHead(code, hmrScript);
}
Expand Down
10 changes: 10 additions & 0 deletions snowpack/src/build/import-sri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {createHash} from 'crypto';

type SupportedSRIAlgorithm = 'sha512' | 'sha384' | 'sha256';
const DEFAULT_CRYPTO_HASH = 'sha384';
const EMPTY_BUFFER = Buffer.from('');

export const generateSRI = (
buffer: Buffer = EMPTY_BUFFER,
hashAlgorithm: SupportedSRIAlgorithm = DEFAULT_CRYPTO_HASH,
) => `${hashAlgorithm}-${createHash(hashAlgorithm).update(buffer).digest('base64')}`;
2 changes: 1 addition & 1 deletion test-dev/__snapshots__/dev.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports[`snowpack dev smoke: html 1`] = `
<meta name=\\"description\\" content=\\"Web site created using create-snowpack-app\\" />
<link rel=\\"stylesheet\\" type=\\"text/css\\" href=\\"/index.css\\" />
<title>Snowpack App</title>
<script type=\\"module\\" src=\\"/__snowpack__/hmr-client.js\\"></script><script type=\\"module\\" src=\\"/__snowpack__/hmr-error-overlay.js\\"></script></head>
<script type=\\"module\\" integrity=\\"sha384-an8DWZrz7mI4kyT5gFIowFckvMzWJPMbX7ZW3wIzjwePuSntO6u7KFSoklwe94ij\\" src=\\"/__snowpack__/hmr-client.js\\"></script><script type=\\"module\\" integrity=\\"sha384-ZHQS30EqaFTVzyH5XP0i+sVAiN0+DiOd1AikxE6LsJbQaA40xjdaXluCLs/7hPlM\\" src=\\"/__snowpack__/hmr-error-overlay.js\\"></script></head>
<body>
<img id=\\"img\\" src=\\"/logo.svg\\" />
<canvas id=\\"canvas\\"></canvas>
Expand Down
17 changes: 14 additions & 3 deletions test-dev/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@ const execa = require('execa');
const {readdirSync, readFileSync, statSync, existsSync} = require('fs');
const glob = require('glob');
const os = require('os');
const fs = require('fs');
const {get} = require('httpie');

const debugFilePath = path.join(__dirname, './logs/debug.snowpack.log');
const errorFilePath = path.join(__dirname, './logs/error.snowpack.log');

describe('snowpack dev', () => {
let snowpackProcess;

beforeAll(() => {
fs.closeSync(fs.openSync(debugFilePath, 'a'));
fs.closeSync(fs.openSync(errorFilePath, 'a'));
});

afterEach(async () => {
snowpackProcess.cancel();
snowpackProcess.kill('SIGTERM', {
Expand All @@ -25,7 +35,6 @@ describe('snowpack dev', () => {
expect.assertions(3);

const cwd = path.join(__dirname, 'smoke');

// start the server
// NOTE: we tried spawning `yarn` here, but the process was not cleaned up
// correctly on CI and the action got stuck. npx does not cause that problem.
Expand All @@ -35,8 +44,10 @@ describe('snowpack dev', () => {
{cwd},
);

snowpackProcess.stdout.pipe(process.stdout);
snowpackProcess.stderr.pipe(process.stderr);
const streamLogFile = fs.createWriteStream(debugFilePath);
const streamErrorFile = fs.createWriteStream(errorFilePath);
snowpackProcess.stdout.pipe(streamLogFile);
snowpackProcess.stderr.pipe(streamErrorFile);

// await server to be ready and set a timeout in case something goes wrong
await new Promise((resolve, reject) => {
Expand Down
Empty file added test-dev/logs/.gitkeep
Empty file.
27 changes: 27 additions & 0 deletions test/snowpack/import-sri.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const {generateSRI} = require('../../snowpack/lib/build/import-sri');

const EMPTY = Buffer.from('');

test('empty buffer with SHA256', () => {
expect(generateSRI(EMPTY, 'sha256')).toBe('sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=');
});

test('empty buffer with SHA384', () => {
expect(generateSRI(EMPTY, 'sha384')).toBe(
'sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb',
);
});

test('empty buffer with SHA512', () => {
expect(generateSRI(EMPTY, 'sha512')).toBe(
'sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==',
);
});

test('verify that SHA384 is default SRI hash algorithm', () => {
expect(generateSRI(EMPTY, 'sha384')).toBe(generateSRI(EMPTY));
});

test('undefined should return empty hash result', () => {
expect(generateSRI()).toBe(generateSRI(EMPTY));
});

1 comment on commit f57ca8a

@vercel
Copy link

@vercel vercel bot commented on f57ca8a Oct 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.