openvibers/openvibe.media Commits
Unknown paths answer Express's own 404 page under Media's CSP, which now allows Cloudflare Web Analytics (browser check, WS-Q task 3): server/not-found.js keeps default-src 'none' and adds script-src https://static.cloudflareinsights.com (the beacon Cloudflare injects at the edge) and connect-src https://cloudflareinsights.com (where it reports). Media's pages send no policy of their own, and Express's default 404 policy blocked the beacon: a CSP error on every not-found page. The network's privacy text already says Cloudflare may measure performance. Tests: not-found-csp (new), observability (the real server's unknown path)
Changes
@@@ -185,6 +185,7 @@ app.use('/api/v2/:app/objects', require('./objects/routes')); // canonical objapp.use('/api/v2/:app/jobs', require('./jobs/routes')); // media jobs: thumbnails, split/remux, invariant scansapp.use('/o', require('./objects/routes').publicRouter); // object bytes (public, or signed)app.use('/', require('./public/routes')); // /v /c /p /t /f++app.use(require('./not-found').notFound); // nothing matched: 404 with Media's CSP (Cloudflare's beacon allowed)// eslint-disable-next-line no-unused-varsapp.use((err, req, res, next) => {nnew file mode 100644@@@ -0,0 +1,36 @@++'use strict';++/**++ * The answer for a path nothing matched: Express's own 404 page ("Cannot GET /x", default-src 'none'),++ * plus Cloudflare Web Analytics. Cloudflare injects its beacon into every HTML answer at the edge, and++ * the network's privacy text says Cloudflare may measure performance; under Express's default policy the++ * beacon was blocked and logged a CSP error on every not-found page (browser check, OpenVibe.Host++ * scripts/browser-check.js). Media's pages send no policy of their own, so this is Media's one CSP.++ */++const CSP = [++ "default-src 'none'",++ // Cloudflare Web Analytics: script-src loads the beacon, connect-src is where it reports.++ 'script-src https://static.cloudflareinsights.com',++ 'connect-src https://cloudflareinsights.com',++].join('; ');++++const ESC = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };++const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ESC[c]);++++function notFound(req, res) {++ res.status(404);++ res.set('Content-Security-Policy', CSP);++ res.set('X-Content-Type-Options', 'nosniff');++ res.type('html').send(`<!DOCTYPE html>++<html lang="en">++<head>++<meta charset="utf-8">++<title>Error</title>++</head>++<body>++<pre>Cannot ${esc(req.method)} ${esc(req.originalUrl)}</pre>++</body>++</html>++`);++}++++module.exports = { notFound, CSP };nnew file mode 100644@@@ -0,0 +1,56 @@++'use strict';++// server/not-found.js: an unknown path gets Express's own 404 page ("Cannot GET /x") under Media's CSP,++// which is default-src 'none' plus Cloudflare Web Analytics (script-src static.cloudflareinsights.com,++// connect-src cloudflareinsights.com). Cloudflare injects the beacon at the edge; Express's default++// policy blocked it and logged a CSP error on every not-found page (browser check, OpenVibe.Host).++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const http = require('http');++const express = require('express');++const { notFound, CSP } = require('../server/not-found');++++function send(base, p, method = 'GET') {++ return new Promise((resolve, reject) => {++ const req = http.request(base + p, { method, headers: { accept: 'text/html' } }, (res) => {++ let b = ''; res.on('data', (d) => { b += d; });++ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: b }));++ });++ req.on('error', reject); req.end();++ });++}++++(async () => {++ const app = express();++ app.get('/known', (req, res) => res.send('ok'));++ app.use(notFound);++ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });++ const base = `http://127.0.0.1:${server.address().port}`;++ try {++ assert.strictEqual(CSP, "default-src 'none'; script-src https://static.cloudflareinsights.com; connect-src https://cloudflareinsights.com");++ assert.strictEqual((await send(base, '/known')).status, 200);++++ const r = await send(base, '/__ovcheck-404');++ assert.strictEqual(r.status, 404);++ assert.strictEqual(r.headers['content-security-policy'], CSP);++ assert.strictEqual(r.headers['x-content-type-options'], 'nosniff');++ assert.match(r.headers['content-type'], /^text\/html/);++ assert.ok(r.body.includes('<pre>Cannot GET /__ovcheck-404</pre>'), r.body);++++ const x = await send(base, '/x%3Cb%3E?q=<script>');++ assert.strictEqual(x.status, 404);++ assert.ok(!x.body.includes('<script>'), 'the path is escaped');++++ const post = await send(base, '/nope', 'POST');++ assert.ok(post.body.includes('<pre>Cannot POST /nope</pre>'));++ const head = await send(base, '/nope', 'HEAD');++ assert.deepStrictEqual([head.status, head.body], [404, '']);++++ // index.js mounts it after every route and before the error handler.++ const idx = fs.readFileSync(path.join(__dirname, '..', 'server', 'index.js'), 'utf8');++ const at = idx.indexOf("app.use(require('./not-found').notFound)");++ assert.ok(at > idx.indexOf("app.use('/', require('./public/routes'))"), 'after the last routes');++ assert.ok(at < idx.indexOf('app.use((err, req, res, next) =>'), 'before the error handler');++ console.log('not-found: 404 under Media\'s CSP, Cloudflare Web Analytics allowed');++ } finally { server.close(); }++})().catch((e) => { console.error(e); process.exit(1); });@@@ -146,6 +146,12 @@ const listen = (app) => new Promise((resolve) => { const s = app.listen(0, '127.assert.ok(m.body.includes('http_requests_total{method="GET",route="/api/ready",status_class="2xx"} 1\n'), m.body.slice(0, 400));const h = await request(b2, '/healthz');assert.strictEqual(h.status, 200, '/healthz still answers');++ // An unknown path is index.js's last handler (server/not-found.js): a 404 whose CSP lets Cloudflare's beacon run.++ const nf = await new Promise((resolve, reject) => http.get(`${b2}/__ovcheck-404`, { headers: { accept: 'text/html' } }, (res) => {++ let b = ''; res.on('data', (d) => { b += d; }); res.on('end', () => resolve({ status: res.statusCode, csp: res.headers['content-security-policy'], body: b }));++ }).on('error', reject));++ assert.deepStrictEqual([nf.status, nf.csp], [404, require('../server/not-found').CSP]);++ assert.ok(nf.body.includes('<pre>Cannot GET /__ovcheck-404</pre>'));// The release manifest (registry.release-manifest@1) names where open tabs report updates,// and those reports land in /metrics.const rel = await request(b2, '/release.json');
@@ -185,6 +185,7 @@ app.use('/api/v2/:app/objects', require('./objects/routes')); // canonical obj
@@ -185,6 +185,7 @@ app.use('/api/v2/:app/objects', require('./objects/routes')); // canonical obj
app.use('/api/v2/:app/jobs', require('./jobs/routes')); // media jobs: thumbnails, split/remux, invariant scans
app.use('/api/v2/:app/jobs', require('./jobs/routes')); // media jobs: thumbnails, split/remux, invariant scans
app.use('/o', require('./objects/routes').publicRouter); // object bytes (public, or signed)
app.use('/o', require('./objects/routes').publicRouter); // object bytes (public, or signed)
app.use('/', require('./public/routes')); // /v /c /p /t /f
app.use('/', require('./public/routes')); // /v /c /p /t /f
+app.use(require('./not-found').notFound); // nothing matched: 404 with Media's CSP (Cloudflare's beacon allowed)
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
app.use((err, req, res, next) => {
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+'use strict';
+/**
+ * The answer for a path nothing matched: Express's own 404 page ("Cannot GET /x", default-src 'none'),
+ * plus Cloudflare Web Analytics. Cloudflare injects its beacon into every HTML answer at the edge, and
+ * the network's privacy text says Cloudflare may measure performance; under Express's default policy the
+ * beacon was blocked and logged a CSP error on every not-found page (browser check, OpenVibe.Host
+ * scripts/browser-check.js). Media's pages send no policy of their own, so this is Media's one CSP.
+ */
+const CSP = [
+ "default-src 'none'",
+ // Cloudflare Web Analytics: script-src loads the beacon, connect-src is where it reports.
+ 'script-src https://static.cloudflareinsights.com',
+ 'connect-src https://cloudflareinsights.com',
+].join('; ');
+
+const ESC = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
+const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ESC[c]);
+
+function notFound(req, res) {
+ res.status(404);
+ res.set('Content-Security-Policy', CSP);
+ res.set('X-Content-Type-Options', 'nosniff');
+ res.type('html').send(`<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Error</title>
+</head>
+<body>
+<pre>Cannot ${esc(req.method)} ${esc(req.originalUrl)}</pre>
+</body>
+</html>
+`);
+}
+
+module.exports = { notFound, CSP };
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+'use strict';
+// server/not-found.js: an unknown path gets Express's own 404 page ("Cannot GET /x") under Media's CSP,
+// which is default-src 'none' plus Cloudflare Web Analytics (script-src static.cloudflareinsights.com,
+// connect-src cloudflareinsights.com). Cloudflare injects the beacon at the edge; Express's default
+// policy blocked it and logged a CSP error on every not-found page (browser check, OpenVibe.Host).
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+const express = require('express');
+const { notFound, CSP } = require('../server/not-found');
+
+function send(base, p, method = 'GET') {
+ return new Promise((resolve, reject) => {
+ const req = http.request(base + p, { method, headers: { accept: 'text/html' } }, (res) => {
+ let b = ''; res.on('data', (d) => { b += d; });
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: b }));
+ });
+ req.on('error', reject); req.end();
+ });
+}
+
+(async () => {
+ const app = express();
+ app.get('/known', (req, res) => res.send('ok'));
+ app.use(notFound);
+ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
+ const base = `http://127.0.0.1:${server.address().port}`;
+ try {
+ assert.strictEqual(CSP, "default-src 'none'; script-src https://static.cloudflareinsights.com; connect-src https://cloudflareinsights.com");
+ assert.strictEqual((await send(base, '/known')).status, 200);
+
+ const r = await send(base, '/__ovcheck-404');
+ assert.strictEqual(r.status, 404);
+ assert.strictEqual(r.headers['content-security-policy'], CSP);
+ assert.strictEqual(r.headers['x-content-type-options'], 'nosniff');
+ assert.match(r.headers['content-type'], /^text\/html/);
+ assert.ok(r.body.includes('<pre>Cannot GET /__ovcheck-404</pre>'), r.body);
+
+ const x = await send(base, '/x%3Cb%3E?q=<script>');
+ assert.strictEqual(x.status, 404);
+ assert.ok(!x.body.includes('<script>'), 'the path is escaped');
+
+ const post = await send(base, '/nope', 'POST');
+ assert.ok(post.body.includes('<pre>Cannot POST /nope</pre>'));
+ const head = await send(base, '/nope', 'HEAD');
+ assert.deepStrictEqual([head.status, head.body], [404, '']);
+
+ // index.js mounts it after every route and before the error handler.
+ const idx = fs.readFileSync(path.join(__dirname, '..', 'server', 'index.js'), 'utf8');
+ const at = idx.indexOf("app.use(require('./not-found').notFound)");
+ assert.ok(at > idx.indexOf("app.use('/', require('./public/routes'))"), 'after the last routes');
+ assert.ok(at < idx.indexOf('app.use((err, req, res, next) =>'), 'before the error handler');
+ console.log('not-found: 404 under Media\'s CSP, Cloudflare Web Analytics allowed');
+ } finally { server.close(); }
+})().catch((e) => { console.error(e); process.exit(1); });
@@ -146,6 +146,12 @@ const listen = (app) => new Promise((resolve) => { const s = app.listen(0, '127.
@@ -146,6 +146,12 @@ const listen = (app) => new Promise((resolve) => { const s = app.listen(0, '127.
assert.ok(m.body.includes('http_requests_total{method="GET",route="/api/ready",status_class="2xx"} 1\n'), m.body.slice(0, 400));
assert.ok(m.body.includes('http_requests_total{method="GET",route="/api/ready",status_class="2xx"} 1\n'), m.body.slice(0, 400));
const h = await request(b2, '/healthz');
const h = await request(b2, '/healthz');
assert.strictEqual(h.status, 200, '/healthz still answers');
assert.strictEqual(h.status, 200, '/healthz still answers');
+ // An unknown path is index.js's last handler (server/not-found.js): a 404 whose CSP lets Cloudflare's beacon run.
+ const nf = await new Promise((resolve, reject) => http.get(`${b2}/__ovcheck-404`, { headers: { accept: 'text/html' } }, (res) => {
+ let b = ''; res.on('data', (d) => { b += d; }); res.on('end', () => resolve({ status: res.statusCode, csp: res.headers['content-security-policy'], body: b }));
+ }).on('error', reject));
+ assert.deepStrictEqual([nf.status, nf.csp], [404, require('../server/not-found').CSP]);
+ assert.ok(nf.body.includes('<pre>Cannot GET /__ovcheck-404</pre>'));
// The release manifest (registry.release-manifest@1) names where open tabs report updates,
// The release manifest (registry.release-manifest@1) names where open tabs report updates,
// and those reports land in /metrics.
// and those reports land in /metrics.
const rel = await request(b2, '/release.json');
const rel = await request(b2, '/release.json');