diff --git a/mindspace-public-delivery.mjs b/mindspace-public-delivery.mjs index 8f96024..b4395e8 100644 --- a/mindspace-public-delivery.mjs +++ b/mindspace-public-delivery.mjs @@ -1,5 +1,20 @@ +import crypto from 'node:crypto'; import path from 'node:path'; +const INLINE_SCRIPT_PATTERN = /]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi; + +export function collectInlineScriptHashes(html) { + const hashes = []; + const seen = new Set(); + for (const match of String(html ?? '').matchAll(INLINE_SCRIPT_PATTERN)) { + const hash = crypto.createHash('sha256').update(match[1] ?? '').digest('base64'); + if (seen.has(hash)) continue; + seen.add(hash); + hashes.push(hash); + } + return hashes; +} + export async function handleMindSpaceLongImageDownload({ query, filePath, @@ -63,12 +78,13 @@ export function decorateMindSpacePublishedHtml({ ? injectPublicFileShareButton(nextHtml) : { html: nextHtml, scriptHashes: [] }; nextHtml = shareInjection.html; + const scriptHashes = collectInlineScriptHashes(nextHtml); return { html: nextHtml, csp: publishedPageCsp(nextHtml, { embed, wechatShare, - scriptHashes: shareInjection.scriptHashes, + scriptHashes, }), allowEmbedFrame, }; diff --git a/mindspace-public-delivery.test.mjs b/mindspace-public-delivery.test.mjs index 7a9d1be..41a745d 100644 --- a/mindspace-public-delivery.test.mjs +++ b/mindspace-public-delivery.test.mjs @@ -1,6 +1,22 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { decorateMindSpacePublishedHtml, handleMindSpaceLongImageDownload } from './mindspace-public-delivery.mjs'; +import { + collectInlineScriptHashes, + decorateMindSpacePublishedHtml, + handleMindSpaceLongImageDownload, +} from './mindspace-public-delivery.mjs'; + +test('collectInlineScriptHashes returns sha256 hashes for inline scripts only', () => { + const html = ` + + + +`; + const hashes = collectInlineScriptHashes(html); + assert.equal(hashes.length, 2); + assert.match(hashes[0], /^[A-Za-z0-9+/]+=*$/); + assert.notEqual(hashes[0], hashes[1]); +}); test('handleMindSpaceLongImageDownload renders and downloads when requested', async () => { const calls = []; @@ -79,8 +95,10 @@ test('handleMindSpaceLongImageDownload falls back to local html path without pag }); test('decorateMindSpacePublishedHtml returns decorated html and csp', () => { + const pageScript = "document.body.dataset.ready='1'"; + const shareScript = "document.body.dataset.share='1'"; const result = decorateMindSpacePublishedHtml({ - html: 'demo', + html: `demo`, embed: false, context: { origin: 'https://mm.tkmind.cn', @@ -92,16 +110,23 @@ test('decorateMindSpacePublishedHtml returns decorated html and csp', () => { preparePublicationHtmlForEmbed: (value) => value, injectOgTags: (value, meta) => `${value}`, injectWechatShareBridge: (value, meta) => `${value}`, - injectPublicFileShareButton: (value) => ({ html: `${value}`, scriptHashes: ['abc'] }), + injectPublicFileShareButton: (value) => ({ + html: value.replace('', ``), + scriptHashes: ['ignored'], + }), publishedPageCsp: (_value, options) => JSON.stringify(options), isWechatUserAgent: () => true, }); assert.match(result.html, /og:/); assert.match(result.html, /wechat:/); - assert.match(result.html, /share/); + assert.match(result.html, /dataset\.share/); assert.equal(result.allowEmbedFrame, false); - assert.equal(result.csp, JSON.stringify({ embed: false, wechatShare: true, scriptHashes: ['abc'] })); + const options = JSON.parse(result.csp); + assert.equal(options.embed, false); + assert.equal(options.wechatShare, true); + assert.deepEqual(options.scriptHashes, collectInlineScriptHashes(result.html)); + assert.equal(options.scriptHashes.length, 2); }); test('decorateMindSpacePublishedHtml supports embed mode without share injection', () => {