diff --git a/CHANGELOG.md b/CHANGELOG.md index 55d2a43..46b28aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). - +## Unreleased + +- Added the `--cross-origin-isolated` CLI flag to enable [cross-origin isolation](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated). ## [0.7.1] 2024-07-18 diff --git a/README.md b/README.md index 61eb287..4838d2e 100644 --- a/README.md +++ b/README.md @@ -872,3 +872,4 @@ tach http://example.com | `--trace` | `false` | Enable performance tracing ([details](#performance-traces)) | | `--trace-log-dir` | `${cwd}/logs` | The directory to put tracing log files. Defaults to `${cwd}/logs`. | | `--trace-cat` | [default categories](./src/defaults.ts) | The tracing categories to record. Should be a string of comma-separated category names | +| `--cross-origin-isolated` | `false` | Add HTTP headers to enable [cross-origin isolation](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated). | diff --git a/src/cli.ts b/src/cli.ts index 67966e8..1ca1ab5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -162,6 +162,7 @@ $ tach http://example.com mountPoints, resolveBareModules: config.resolveBareModules, cache: config.mode !== 'manual', + crossOriginIsolated: config.crossOriginIsolated, }); for (const spec of specs) { servers.set(spec, server); diff --git a/src/config.ts b/src/config.ts index f1e943e..7e07d68 100644 --- a/src/config.ts +++ b/src/config.ts @@ -37,6 +37,7 @@ export interface Config { npmrc?: string; csvFileStats: string; csvFileRaw: string; + crossOriginIsolated: boolean; } export async function makeConfig(opts: Opts): Promise { @@ -55,6 +56,7 @@ export async function makeConfig(opts: Opts): Promise { ? parseGithubCheckFlag(opts['github-check']) : undefined, remoteAccessibleHost: opts['remote-accessible-host'], + crossOriginIsolated: opts['cross-origin-isolated'], }; let config: Config; @@ -175,6 +177,10 @@ export function applyDefaults(partial: Partial): Config { : defaults.resolveBareModules, root: partial.root !== undefined ? partial.root : defaults.root, timeout: partial.timeout !== undefined ? partial.timeout : defaults.timeout, + crossOriginIsolated: + partial.crossOriginIsolated !== undefined + ? partial.crossOriginIsolated + : defaults.crossOriginIsolated, }; } diff --git a/src/cross-origin-isolation.ts b/src/cross-origin-isolation.ts new file mode 100644 index 0000000..29dc5dd --- /dev/null +++ b/src/cross-origin-isolation.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +import {Middleware} from 'koa'; + +// Enable cross-origin isolation for more precise timers: +// https://developer.chrome.com/blog/cross-origin-isolated-hr-timers/ +export function crossOriginIsolation(): Middleware { + // Based on https://github.com/fishel-feng/koa-isolated + return async function isolated(ctx, next) { + ctx.set('Cross-Origin-Opener-Policy', 'same-origin'); + ctx.set('Cross-Origin-Embedder-Policy', 'require-corp'); + await next(); + }; +} diff --git a/src/defaults.ts b/src/defaults.ts index 4d698e1..ccca3d3 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -20,6 +20,7 @@ export const mode = 'automatic'; export const resolveBareModules = true; export const forceCleanNpmInstall = false; export const measurementExpression = 'window.tachometerResult'; +export const crossOriginIsolated = false; export const traceLogDir = path.join(process.cwd(), 'logs'); export const traceCategories = [ 'blink', diff --git a/src/flags.ts b/src/flags.ts index 57e9dd0..da3df1c 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -235,6 +235,11 @@ export const optDefs: commandLineUsage.OptionDefinition[] = [ type: String, defaultValue: defaults.traceCategories.join(','), }, + { + name: 'cross-origin-isolated', + description: 'Add HTTP headers to enable cross-origin isolation', + type: Boolean, + }, ]; export interface Opts { @@ -266,6 +271,7 @@ export interface Opts { trace: boolean; 'trace-log-dir': string; 'trace-cat': string; + 'cross-origin-isolated': boolean; // Extra arguments not associated with a flag are put here. These are our // benchmark names/URLs. diff --git a/src/server.ts b/src/server.ts index 2dad277..b21e571 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,6 +19,7 @@ import {nodeResolve} from 'koa-node-resolve'; import {BenchmarkResponse, Deferred} from './types.js'; import {NpmInstall} from './versions.js'; +import {crossOriginIsolation} from './cross-origin-isolation.js'; import * as url from 'url'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); @@ -31,6 +32,7 @@ export interface ServerOpts { mountPoints: MountPoint[]; resolveBareModules: boolean; cache: boolean; + crossOriginIsolated: boolean; } export interface MountPoint { @@ -91,6 +93,9 @@ export class Server { this.server = server; const app = new Koa(); + if (opts.crossOriginIsolated) { + app.use(crossOriginIsolation()); + } app.use(bodyParser()); app.use(mount('/submitResults', this.submitResults.bind(this))); app.use(this.instrumentRequests.bind(this)); diff --git a/src/test/config_test.ts b/src/test/config_test.ts index b828449..a97a71e 100644 --- a/src/test/config_test.ts +++ b/src/test/config_test.ts @@ -46,6 +46,7 @@ suite('makeConfig', function () { csvFileStats: '', csvFileRaw: '', githubCheck: undefined, + crossOriginIsolated: false, benchmarks: [ { browser: { @@ -92,6 +93,7 @@ suite('makeConfig', function () { csvFileRaw: '', // TODO(aomarks) Be consistent about undefined vs unset. githubCheck: undefined, + crossOriginIsolated: false, benchmarks: [ { browser: { @@ -137,6 +139,7 @@ suite('makeConfig', function () { csvFileStats: '', csvFileRaw: '', githubCheck: undefined, + crossOriginIsolated: false, benchmarks: [ { browser: { @@ -190,6 +193,7 @@ suite('makeConfig', function () { remoteAccessibleHost: '', // TODO(aomarks) Be consistent about undefined vs unset. githubCheck: undefined, + crossOriginIsolated: false, benchmarks: [ { browser: { @@ -237,6 +241,7 @@ suite('makeConfig', function () { remoteAccessibleHost: '', // TODO(aomarks) Be consistent about undefined vs unset. githubCheck: undefined, + crossOriginIsolated: false, benchmarks: [ { browser: { diff --git a/src/test/server_test.ts b/src/test/server_test.ts index 2b09251..cf40c93 100644 --- a/src/test/server_test.ts +++ b/src/test/server_test.ts @@ -17,20 +17,25 @@ import {testData} from './test_helpers.js'; suite('server', () => { let server: Server; + const defaultOptions = { + host: 'localhost', + ports: [0], // random + root: testData, + resolveBareModules: true, + npmInstalls: [], + mountPoints: [ + { + diskPath: testData, + urlPath: '/', + }, + ], + cache: true, + crossOriginIsolated: false, + }; + setup(async () => { server = await Server.start({ - host: 'localhost', - ports: [0], // random - root: testData, - resolveBareModules: true, - npmInstalls: [], - mountPoints: [ - { - diskPath: testData, - urlPath: '/', - }, - ], - cache: true, + ...defaultOptions, }); }); @@ -88,18 +93,8 @@ suite('server', () => { await server.close(); server = await Server.start({ - host: 'localhost', - ports: [0], // random - root: testData, - resolveBareModules: true, + ...defaultOptions, npmInstalls: [{installDir, packageJson}], - mountPoints: [ - { - diskPath: testData, - urlPath: '/', - }, - ], - cache: true, }); }); @@ -137,4 +132,36 @@ suite('server', () => { session = server.endSession(); assert.equal(session.bytesSent, 0); }); + + test('cross-origin isolation is disabled by default', async () => { + const res = await fetch(`${server.url}/import-bare-module.html`); + + assert.equal(res.headers.get('Cross-Origin-Opener-Policy'), null); + assert.equal(res.headers.get('Cross-Origin-Embedder-Policy'), null); + }); + + suite('cross origin isolation enabled', async () => { + setup(async () => { + // Close the base server and replace it with a custom server that is + // configured with crossOriginIsolated=true + await server.close(); + server = await Server.start({ + ...defaultOptions, + crossOriginIsolated: true, + }); + }); + + test('cross-origin isolation can be enabled', async () => { + const res = await fetch(`${server.url}/import-bare-module.html`); + + assert.equal( + res.headers.get('Cross-Origin-Opener-Policy'), + 'same-origin' + ); + assert.equal( + res.headers.get('Cross-Origin-Embedder-Policy'), + 'require-corp' + ); + }); + }); });