From 9597dd361bf2e241ece83bb1ec73270a0fa5f7ae Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Wed, 10 May 2023 21:47:30 +1000 Subject: [PATCH] Add COOP+COEP+CORP headers (#6597) --- app/ide-desktop/lib/client/src/bin/server.ts | 4 +++ app/ide-desktop/lib/common/src/index.ts | 13 +++++++- app/ide-desktop/lib/content/esbuild-config.ts | 6 +++- app/ide-desktop/lib/content/src/index.ts | 4 +++ .../lib/content/src/serviceWorker.ts | 32 +++++++++++++++++++ app/ide-desktop/lib/content/watch.ts | 8 +++++ .../src/authentication/src/components/svg.tsx | 10 ++++++ .../src/dashboard/components/dashboard.tsx | 5 +-- .../src/dashboard/components/topBar.tsx | 9 +++--- .../lib/dashboard/src/serviceWorker.ts | 27 ++++++++++------ 10 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 app/ide-desktop/lib/content/src/serviceWorker.ts diff --git a/app/ide-desktop/lib/client/src/bin/server.ts b/app/ide-desktop/lib/client/src/bin/server.ts index 92261fc327f0..ed18a44305cc 100644 --- a/app/ide-desktop/lib/client/src/bin/server.ts +++ b/app/ide-desktop/lib/client/src/bin/server.ts @@ -8,6 +8,7 @@ import * as mime from 'mime-types' import * as portfinder from 'portfinder' import createServer from 'create-servers' +import * as common from 'enso-common' import * as contentConfig from 'enso-content-config' import * as paths from '../paths' @@ -105,6 +106,9 @@ export class Server { resource === '/preload.cjs.map' ? `${paths.APP_PATH}${resource}` : `${this.config.dir}${resource}` + for (const [header, value] of common.COOP_COEP_CORP_HEADERS) { + response.setHeader(header, value) + } fs.readFile(resourceFile, (err, data) => { if (err) { logger.error(`Resource '${resource}' not found.`) diff --git a/app/ide-desktop/lib/common/src/index.ts b/app/ide-desktop/lib/common/src/index.ts index 0b514c5da626..f95ae98b0713 100644 --- a/app/ide-desktop/lib/common/src/index.ts +++ b/app/ide-desktop/lib/common/src/index.ts @@ -1,4 +1,5 @@ -/** @file This module contains metadata about the product and distribution. +/** @file This module contains metadata about the product and distribution, + * and various other constants that are needed in multiple sibling packages. * * Code in this package is used by two or more sibling packages of this package. The code is defined * here when it is not possible for a sibling package to own that code without introducing a @@ -12,3 +13,13 @@ export const DEEP_LINK_SCHEME = 'enso' /** Name of the product. */ export const PRODUCT_NAME = 'Enso' + +/** COOP, COEP, and CORP headers: https://web.dev/coop-coep/ + * + * These are required to increase the resolution of `performance.now()` timers, + * making profiling a lot more accurate and consistent. */ +export const COOP_COEP_CORP_HEADERS: [header: string, value: string][] = [ + ['Cross-Origin-Embedder-Policy', 'require-corp'], + ['Cross-Origin-Opener-Policy', 'same-origin'], + ['Cross-Origin-Resource-Policy', 'same-origin'], +] diff --git a/app/ide-desktop/lib/content/esbuild-config.ts b/app/ide-desktop/lib/content/esbuild-config.ts index dc67ad28ba1f..eb80ad2b9f50 100644 --- a/app/ide-desktop/lib/content/esbuild-config.ts +++ b/app/ide-desktop/lib/content/esbuild-config.ts @@ -26,7 +26,11 @@ import esbuildPluginYaml from 'esbuild-plugin-yaml' import * as utils from '../../utils' import BUILD_INFO from '../../build.json' assert { type: 'json' } -export const THIS_PATH = pathModule.resolve(pathModule.dirname(url.fileURLToPath(import.meta.url))) +// ================= +// === Constants === +// ================= + +const THIS_PATH = pathModule.resolve(pathModule.dirname(url.fileURLToPath(import.meta.url))) // ============================= // === Environment variables === diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index b3f27b68b35c..ea413eb888ff 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -20,6 +20,9 @@ const logger = app.log.logger const ESBUILD_PATH = '/esbuild' /** SSE event indicating a build has finished. */ const ESBUILD_EVENT_NAME = 'change' +/** Path to the service worker that resolves all extensionless paths to `/index.html`. + * This service worker is required for client-side routing to work when doing local development. */ +const SERVICE_WORKER_PATH = '/serviceWorker.js' /** One second in milliseconds. */ const SECOND = 1000 /** Time in seconds after which a `fetchTimeout` ends. */ @@ -33,6 +36,7 @@ if (IS_DEV_MODE) { new EventSource(ESBUILD_PATH).addEventListener(ESBUILD_EVENT_NAME, () => { location.reload() }) + void navigator.serviceWorker.register(SERVICE_WORKER_PATH) } // ============= diff --git a/app/ide-desktop/lib/content/src/serviceWorker.ts b/app/ide-desktop/lib/content/src/serviceWorker.ts new file mode 100644 index 000000000000..332b8849acc8 --- /dev/null +++ b/app/ide-desktop/lib/content/src/serviceWorker.ts @@ -0,0 +1,32 @@ +/** @file A service worker that redirects paths without extensions to `/index.html`. + * This is required for paths like `/login`, which are handled by client-side routing, + * to work when developing locally on `localhost:8080`. */ +// Bring globals and interfaces specific to Web Workers into scope. +/// +import * as common from 'enso-common' + +// ===================== +// === Fetch handler === +// ===================== + +// We `declare` a variable here because Service Workers have a different global scope. +// eslint-disable-next-line no-restricted-syntax +declare const self: ServiceWorkerGlobalScope + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url) + if (url.hostname === 'localhost' && url.pathname !== '/esbuild') { + event.respondWith( + fetch(event.request.url).then(response => { + const clonedResponse = new Response(response.body, response) + for (const [header, value] of common.COOP_COEP_CORP_HEADERS) { + clonedResponse.headers.set(header, value) + } + return clonedResponse + }) + ) + return + } else { + return false + } +}) diff --git a/app/ide-desktop/lib/content/watch.ts b/app/ide-desktop/lib/content/watch.ts index 44385f47760a..a35afb242d0d 100644 --- a/app/ide-desktop/lib/content/watch.ts +++ b/app/ide-desktop/lib/content/watch.ts @@ -1,4 +1,7 @@ /** @file File watch and compile service. */ +import * as path from 'node:path' +import * as url from 'node:url' + import * as esbuild from 'esbuild' import * as portfinder from 'portfinder' import chalk from 'chalk' @@ -12,6 +15,7 @@ import * as dashboardBundler from '../dashboard/esbuild-config' const PORT = 8080 const HTTP_STATUS_OK = 200 +const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url))) // =============== // === Watcher === @@ -29,6 +33,10 @@ async function watch() { ...bundler.argumentsFromEnv(), devMode: true, }) + opts.entryPoints.push({ + in: path.resolve(THIS_PATH, 'src', 'serviceWorker.ts'), + out: 'serviceWorker', + }) const builder = await esbuild.context(opts) await builder.watch() await builder.serve({ diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx index a00b416e20e9..d36b439bf229 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/components/svg.tsx @@ -254,6 +254,16 @@ export const COMPUTER_ICON = ( ) +/** An icon representing a user without a profile picture. */ +export const DEFAULT_USER_ICON = ( + + + +) + export interface StopIconProps { className?: string } diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index 81fd843c3930..fb1d1329ad04 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -471,10 +471,7 @@ function Dashboard(props: DashboardProps) { key={user.user.organization_id} permissions={PERMISSION[user.permission]} > - + {svg.DEFAULT_USER_ICON} ))} diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx index 40f598c01750..41d7f838128f 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx @@ -120,14 +120,15 @@ function TopBar(props: TopBarProps) { {/* User profile and menu. */}
- { event.stopPropagation() setUserMenuVisible(!userMenuVisible) }} - /> + className="rounded-full w-8 h-8 bg-cover cursor-pointer" + > + {svg.DEFAULT_USER_ICON} +
) diff --git a/app/ide-desktop/lib/dashboard/src/serviceWorker.ts b/app/ide-desktop/lib/dashboard/src/serviceWorker.ts index 295ca28982ee..8c087b349962 100644 --- a/app/ide-desktop/lib/dashboard/src/serviceWorker.ts +++ b/app/ide-desktop/lib/dashboard/src/serviceWorker.ts @@ -1,6 +1,9 @@ /** @file A service worker that redirects paths without extensions to `/index.html`. - * This is only used in the cloud frontend. */ + * This is required for paths like `/login`, which are handled by client-side routing, + * to work when developing locally on `localhost:8081`. */ +// Bring globals and interfaces specific to Web Workers into scope. /// +import * as common from 'enso-common' // ===================== // === Fetch handler === @@ -12,17 +15,21 @@ declare const self: ServiceWorkerGlobalScope self.addEventListener('fetch', event => { const url = new URL(event.request.url) - if ( - url.hostname === 'localhost' && - /\/[^.]+$/.test(event.request.url) && - url.pathname !== '/esbuild' - ) { - event.respondWith(fetch('/index.html')) + if (url.hostname === 'localhost' && url.pathname !== '/esbuild') { + const responsePromise = /\/[^.]+$/.test(event.request.url) + ? fetch('/index.html') + : fetch(event.request.url) + event.respondWith( + responsePromise.then(response => { + const clonedResponse = new Response(response.body, response) + for (const [header, value] of common.COOP_COEP_CORP_HEADERS) { + clonedResponse.headers.set(header, value) + } + return clonedResponse + }) + ) return } else { return false } }) - -// Required for TypeScript to consider it a module, instead of in window scope. -export {}