openvibers/openvibe.shared csp.js

csp.js

main 3.7 KB · 70 lines · javascript

1'use strict';
2/**
3 * openvibe-shared/csp — the network's Content-Security-Policy pieces, in one place.
4 *
5 * Every OpenVibe site builds a CSP header. Before this module each built its own string, so the composer's upload to
6 * OpenVibe.Media was blocked on any site that had not named `https://openvibe.media` in connect-src (the browser sends
7 * the multipart POST there, and a CSP refusal is a network error with no status). A site that uses these helpers gets
8 * Network, Events and Media in connect-src by default; a site that needs more (its own websocket, Cloudflare's beacon)
9 * adds it with `sites` or `extra`.
10 *
11 * const csp = require('openvibe-shared/csp');
12 * res.setHeader('Content-Security-Policy', csp.policy({ nonce, sites: ['wss://openvibe.chat'], frames: ['https://openvibe.bot'] }));
13 *
14 * `connectSources()` is the list on its own, for a site that keeps its own policy but wants the network's origins.
15 */
16const NETWORK = 'https://openvibe.network';
17const EVENTS = 'https://openvibe.events';
18const MEDIA = 'https://openvibe.media';
19
20const uniq = (list) => [...new Set(list.filter(Boolean).map(String))];
21
22/**
23 * The connect-src value: this site, the Network (sign-in, the Frame, settings, mentions), Events (release and
24 * notification streams) and Media (composer attachments). `sites` and `extra` add more.
25 */
26function connectSources({ self = true, sites = [], extra = [] } = {}) {
27 return uniq([self ? "'self'" : null, NETWORK, `wss://${NETWORK.slice(8)}`, EVENTS, `wss://${EVENTS.slice(8)}`, MEDIA, ...sites, ...extra]).join(' ');
28}
29
30/**
31 * A full policy. Defaults suit a server-rendered site that runs only its own scripts, with inline styles and pictures
32 * and media from anywhere over https.
33 *
34 * nonce the response's script nonce (inline scripts need it)
35 * script extra script origins
36 * style extra style origins
37 * images the img-src value (default https:)
38 * media the media-src value (default https:)
39 * frames frame-src entries (default 'self')
40 * formAction the form-action value (default 'self' https://openvibe.network)
41 * sites extra connect-src origins
42 * connect a full connect-src value, instead of connectSources()
43 * connectExtra extra connect-src entries
44 * objectSrc object-src (default 'none')
45 * baseUri base-uri (default 'self')
46 * frameAncestors frame-ancestors (default 'self')
47 * extra { directive: value } appended (e.g. { 'worker-src': "'self' blob:" })
48 */
49function policy({ nonce, script = [], style = [], images = 'https:', media = 'https:', frames = ["'self'"], formAction = `'self' ${NETWORK}`, connect, sites = [], connectExtra = [], objectSrc = "'none'", baseUri = "'self'", frameAncestors = "'self'", extra = {} } = {}) {
50 const directives = [
51 ["default-src", "'self'"],
52 ["script-src", ["'self'", nonce ? `'nonce-${nonce}'` : null, ...script].filter(Boolean).join(' ')],
53 ["style-src", ["'self'", "'unsafe-inline'", ...style].filter(Boolean).join(' ')],
54 ["img-src", images],
55 ["media-src", media],
56 ["font-src", "'self' data:"],
57 ["connect-src", connect || connectSources({ sites, extra: connectExtra })],
58 ["frame-src", frames.join(' ')],
59 ["frame-ancestors", frameAncestors],
60 ["base-uri", baseUri],
61 ["object-src", objectSrc],
62 ["form-action", formAction],
63 ];
64 const seen = new Set(directives.map(([d]) => d));
65 for (const [d, v] of Object.entries(extra)) if (!seen.has(d) && v != null) directives.push([d, String(v)]);
66 return directives.map(([d, v]) => `${d} ${v}`).join('; ');
67}
68
69module.exports = { NETWORK, EVENTS, MEDIA, MEDIA_UPLOAD_ORIGIN: MEDIA, connectSources, policy };
70