Add COOP+COEP+CORP headers (#6597)

This commit is contained in:
somebody1234 2023-05-10 21:47:30 +10:00 committed by GitHub
parent 0d9186b23c
commit 9597dd361b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 98 additions and 20 deletions

View File

@ -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.`)

View File

@ -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'],
]

View File

@ -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 ===

View File

@ -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)
}
// =============

View File

@ -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.
/// <reference lib="WebWorker" />
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
}
})

View File

@ -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({

View File

@ -254,6 +254,16 @@ export const COMPUTER_ICON = (
</svg>
)
/** An icon representing a user without a profile picture. */
export const DEFAULT_USER_ICON = (
<svg height={32} width={32} viewBox="2 2 20 20" xmlns="http://www.w3.org/2000/svg">
<path
d="M6 20a10 10 0 0 1 6 -18 10 10 0 0 1 6 18 6 6 0 0 0 -4 -5 4.3 4.3 0 0 0 -2 -8 4.3 4.3 0 0 0 -2 8 6 6 0 0 0 -4 5"
fill="#888888"
/>
</svg>
)
export interface StopIconProps {
className?: string
}

View File

@ -471,10 +471,7 @@ function Dashboard(props: DashboardProps) {
key={user.user.organization_id}
permissions={PERMISSION[user.permission]}
>
<img
className="rounded-full h-6"
src="https://faces-img.xcdn.link/image-lorem-face-4742.jpg"
/>
{svg.DEFAULT_USER_ICON}
</PermissionDisplay>
))}
</>

View File

@ -120,14 +120,15 @@ function TopBar(props: TopBarProps) {
</a>
{/* User profile and menu. */}
<div className="transform">
<img
src="https://faces-img.xcdn.link/image-lorem-face-4742.jpg"
className="rounded-full w-8 h-8 bg-cover cursor-pointer"
<div
onClick={event => {
event.stopPropagation()
setUserMenuVisible(!userMenuVisible)
}}
/>
className="rounded-full w-8 h-8 bg-cover cursor-pointer"
>
{svg.DEFAULT_USER_ICON}
</div>
</div>
</div>
)

View File

@ -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.
/// <reference lib="WebWorker" />
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 {}