mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-11-12 10:17:47 +00:00
Refs #1554. `index.html`, `area-map.html` and `geofilter-builder.html` load Leaflet, leaflet.heat and Chart.js from **unpkg.com** on every page view. That causes three problems: - **Privacy:** every visitor sends a request to a third party. - **Availability:** where unpkg is blocked (script blockers, filtered networks) or down, the map and every chart are gone. The observer page then fails with `Error: Chart is not defined`. - **#1554:** the CSP target there, `script-src 'self'`, blocks these scripts just like the inline handlers the issue lists, but they are missing from its inventory. ## Change - **Vendored libraries:** `public/vendor/leaflet-1.9.4/` (with `images/`), `vendor/leaflet-heat-0.2.0.js` and `vendor/chart-4.5.1.umd.min.js`. - They are the unpkg dist files byte for byte, verified against the SRI hashes `index.html` already pins. - The two standalone pages loaded Leaflet without SRI; they now carry it. - This is the same pattern as `vendor/leaflet.markercluster.js`. - **`.gitattributes`:** `public/vendor/** -text`. Otherwise `* text=auto eol=lf` rewrites `leaflet.css`, which ships with CRLF, and the browser refuses it for the integrity mismatch. I hit exactly that while preparing this. - **`scripts/instrument-frontend.sh`:** now copies `vendor/` recursively. The flat `cp public/vendor/*` left the E2E server without `vendor/leaflet-1.9.4/`, which showed up as `L is not defined`. - **New test `tests/unit/test-vendored-libs.js`:** - no off-site `<script src>` or stylesheet in the served pages - every `integrity` attribute matches its vendored file - the images Leaflet's stylesheet references exist I checked that it fails against the old `index.html`. ## Checks - `sh test-all.sh` passes. - These E2E suites pass against the fixture: `test-e2e-playwright.js`, `test-analytics-fluid-charts.js`, `test-issue-1236-map-mobile-e2e.js` and `test-issue-1329-map-controls-accordion-e2e.js`. - On a live instance, a headless browser with `unpkg.com`, `jsdelivr` and `cdnjs` blocked: - the observer page renders all four charts - the map loads tiles and markers - `typeof L` and `typeof Chart` are defined - no CDN request is made and no page error occurs ## Not in this PR Swagger UI on `/api/docs` (`cmd/server/openapi.go`) still loads `swagger-ui-dist` from unpkg. I can vendor it in a follow-up if wanted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
2.4 KiB
JavaScript
55 lines
2.4 KiB
JavaScript
/**
|
|
* The pages load their third-party libraries (Leaflet, leaflet.heat,
|
|
* Chart.js) from public/vendor/, not from a CDN: no request to a third party
|
|
* on every page view, and maps and charts keep working where the CDN is
|
|
* blocked or down. This pins both halves:
|
|
* - no <script src> or <link href> in the served pages points off-site
|
|
* - every vendored file carrying an integrity attribute matches it, so a
|
|
* re-download or upgrade that changes the bytes fails here, not in a
|
|
* browser that refuses to run the script.
|
|
*
|
|
* Usage: node tests/unit/test-vendored-libs.js
|
|
*/
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const PUBLIC = path.join(__dirname, '..', '..', 'public');
|
|
const PAGES = ['index.html', 'area-map.html', 'geofilter-builder.html'];
|
|
let pass = 0, fail = 0;
|
|
|
|
function test(name, fn) {
|
|
try { fn(); pass++; console.log(' ✅ ' + name); } catch (e) { fail++; console.log(' ❌ ' + name + ': ' + e.message); }
|
|
}
|
|
|
|
for (const page of PAGES) {
|
|
const html = fs.readFileSync(path.join(PUBLIC, page), 'utf8');
|
|
const tags = html.match(/<(script|link)\b[^>]*>/g) || [];
|
|
test(page + ': no script or stylesheet from another origin', () => {
|
|
const offsite = tags.filter((t) => /\b(src|href)="(https?:)?\/\//.test(t) && (/<script/.test(t) || /rel="stylesheet"/.test(t)));
|
|
if (offsite.length) throw new Error(offsite.join(' | '));
|
|
});
|
|
for (const tag of tags) {
|
|
const ref = (tag.match(/\b(?:src|href)="([^"?]+)/) || [])[1];
|
|
const sri = (tag.match(/\bintegrity="([^"]+)"/) || [])[1];
|
|
if (!ref || !sri) continue;
|
|
test(page + ': ' + ref + ' matches its integrity hash', () => {
|
|
const [alg, want] = sri.split(/-(.+)/);
|
|
const got = crypto.createHash(alg).update(fs.readFileSync(path.join(PUBLIC, ref))).digest('base64');
|
|
if (got !== want) throw new Error(alg + ' is ' + got + ', integrity says ' + want);
|
|
});
|
|
}
|
|
}
|
|
|
|
test('Leaflet images referenced by its stylesheet are vendored', () => {
|
|
const dir = path.join(PUBLIC, 'vendor', 'leaflet-1.9.4');
|
|
const css = fs.readFileSync(path.join(dir, 'leaflet.css'), 'utf8');
|
|
const missing = [...css.matchAll(/url\((images\/[^)]+)\)/g)].map((m) => m[1])
|
|
.filter((f) => !fs.existsSync(path.join(dir, f)));
|
|
if (missing.length) throw new Error('missing ' + missing.join(', '));
|
|
});
|
|
|
|
console.log(`\nVendored libraries: ${pass} passed, ${fail} failed`);
|
|
process.exit(fail > 0 ? 1 : 0);
|