Skip to content

Commit 1a1ba0b

Browse files
committed
Test and readability changes
1 parent 4bac275 commit 1a1ba0b

File tree

2 files changed

+50
-44
lines changed

2 files changed

+50
-44
lines changed

packages/angular/build/src/utils/index-file/auto-csp.ts

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { htmlRewritingStream } from './html-rewriting-stream';
9+
import { RewritingStream } from 'parse5-html-rewriting-stream';
1010
import { StartTag } from 'parse5-sax-parser';
11+
import { htmlRewritingStream } from './html-rewriting-stream';
1112
import * as crypto from 'crypto';
12-
import { RewritingStream } from 'parse5-html-rewriting-stream';
1313

1414
/**
1515
* The hash function to use for hash directives to use in the CSP.
@@ -43,7 +43,6 @@ const JS_MIME_TYPES = new Set([
4343
* Store the appropriate attributes of a sourced script tag to generate the loader script.
4444
*/
4545
interface SrcScriptTag {
46-
scriptType: 'src';
4746
src: string;
4847
type?: string;
4948
async: boolean;
@@ -92,29 +91,11 @@ function shouldDynamicallyLoadScriptTagBasedOnType(scriptType: string | undefine
9291
* include whitespaces and newlines!
9392
* @returns The hash of the text formatted appropriately for CSP.
9493
*/
95-
export function hashScriptText(scriptText: string): string {
94+
export function hashTextContent(scriptText: string): string {
9695
const hash = crypto.createHash(HASH_FUNCTION).update(scriptText, 'utf-8').digest('base64');
9796
return `'${HASH_FUNCTION}-${hash}'`;
9897
}
9998

100-
/**
101-
* Generates the dynamic loading script and puts it in the rewriter and adds the hash of the dynamic
102-
* loader script to the collection of hashes to add to the <meta> tag CSP.
103-
*
104-
* @param scriptContent The current streak of <script src="..."> tags
105-
* @param hashes The array of hashes to include in the final CSP
106-
* @param rewriter Where to emit tags to
107-
*/
108-
function emitLoaderScript(
109-
scriptContent: SrcScriptTag[],
110-
hashes: string[],
111-
rewriter: RewritingStream,
112-
) {
113-
const loaderScript = createLoaderScript(scriptContent);
114-
hashes.push(hashScriptText(loaderScript));
115-
rewriter.emitRaw(`<script>${loaderScript}</script>`);
116-
}
117-
11899
/**
119100
* Finds all `<script>` tags and creates a dynamic script loading block for consecutive `<script>` with `src` attributes.
120101
* Hashes all scripts, both inline and generated dynamic script loading blocks.
@@ -130,6 +111,17 @@ export async function autoCsp(html: string): Promise<string> {
130111
let scriptContent: SrcScriptTag[] = [];
131112
let hashes: string[] = [];
132113

114+
/**
115+
* Generates the dynamic loading script and puts it in the rewriter and adds the hash of the dynamic
116+
* loader script to the collection of hashes to add to the <meta> tag CSP.
117+
*/
118+
function emitLoaderScript() {
119+
const loaderScript = createLoaderScript(scriptContent);
120+
hashes.push(hashTextContent(loaderScript));
121+
rewriter.emitRaw(`<script>${loaderScript}</script>`);
122+
scriptContent = [];
123+
}
124+
133125
rewriter.on('startTag', (tag, html) => {
134126
if (tag.tagName === 'script') {
135127
openedScriptTag = tag;
@@ -140,7 +132,6 @@ export async function autoCsp(html: string): Promise<string> {
140132
const scriptType = getScriptAttributeValue(tag, 'type');
141133
if (shouldDynamicallyLoadScriptTagBasedOnType(scriptType)) {
142134
scriptContent.push({
143-
scriptType: 'src',
144135
src: src,
145136
type: scriptType,
146137
async: !(getScriptAttributeValue(tag, 'async') === undefined),
@@ -157,15 +148,14 @@ export async function autoCsp(html: string): Promise<string> {
157148
// (One edge case is where there are no more opening tags after the last <script src="..."> is
158149
// closed, but this case is handled below with the final </body> tag.)
159150
if (scriptContent.length > 0) {
160-
emitLoaderScript(scriptContent, hashes, rewriter);
161-
scriptContent = [];
151+
emitLoaderScript();
162152
}
163153
rewriter.emitStartTag(tag);
164154
});
165155

166156
rewriter.on('text', (tag, html) => {
167157
if (openedScriptTag && !getScriptAttributeValue(openedScriptTag, 'src')) {
168-
hashes.push(hashScriptText(html));
158+
hashes.push(hashTextContent(html));
169159
}
170160
rewriter.emitText(tag);
171161
});
@@ -185,8 +175,7 @@ export async function autoCsp(html: string): Promise<string> {
185175
if (tag.tagName === 'body' || tag.tagName === 'html') {
186176
// Write the loader script if a string of <script>s were the last opening tag of the document.
187177
if (scriptContent.length > 0) {
188-
emitLoaderScript(scriptContent, hashes, rewriter);
189-
scriptContent = [];
178+
emitLoaderScript();
190179
}
191180
}
192181
rewriter.emitEndTag(tag);

packages/angular/build/src/utils/index-file/auto-csp_spec.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,20 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { autoCsp, hashScriptText } from './auto-csp';
9+
import { autoCsp, hashTextContent } from './auto-csp';
10+
11+
// Utility function to grab the meta tag CSPs from the HTML response.
12+
const getCsps = (html: string) => {
13+
return Array.from(
14+
html.matchAll(/<meta http-equiv="Content-Security-Policy" content="([^"]*)">/g),
15+
).map((m) => m[1]); // Only capture group.
16+
};
17+
18+
const ONE_HASH_CSP =
19+
/script-src 'strict-dynamic' 'sha256-[^']+' https: 'unsafe-inline';object-src 'none';base-uri 'self';/;
20+
21+
const FOUR_HASH_CSP =
22+
/script-src 'strict-dynamic' (?:'sha256-[^']+' ){4}https: 'unsafe-inline';object-src 'none';base-uri 'self';/;
1023

1124
describe('auto-csp', () => {
1225
it('should rewrite a single inline script', async () => {
@@ -21,9 +34,10 @@ describe('auto-csp', () => {
2134
</html>
2235
`);
2336

24-
expect(result).toContain(
25-
`<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' ${hashScriptText("console.log('foo');")} https: 'unsafe-inline';object-src 'none';base-uri 'self';">`,
26-
);
37+
const csps = getCsps(result);
38+
expect(csps.length).toBe(1);
39+
expect(csps[0]).toMatch(ONE_HASH_CSP);
40+
expect(csps[0]).toContain(hashTextContent("console.log('foo');"));
2741
});
2842

2943
it('should rewrite a single source script', async () => {
@@ -38,9 +52,9 @@ describe('auto-csp', () => {
3852
</html>
3953
`);
4054

41-
expect(result).toContain(
42-
`<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' 'sha256-cfa69N/DhgtxzDzIHo+IFj9KPigQLDJgb6ZGZa3g5Cs=' https: 'unsafe-inline';object-src 'none';base-uri 'self';">`,
43-
);
55+
const csps = getCsps(result);
56+
expect(csps.length).toBe(1);
57+
expect(csps[0]).toMatch(ONE_HASH_CSP);
4458
expect(result).toContain(`var scripts = [['./main.js', undefined, false, false]];`);
4559
});
4660

@@ -56,9 +70,9 @@ describe('auto-csp', () => {
5670
</html>
5771
`);
5872

59-
expect(result).toContain(
60-
`<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' 'sha256-cfa69N/DhgtxzDzIHo+IFj9KPigQLDJgb6ZGZa3g5Cs=' https: 'unsafe-inline';object-src 'none';base-uri 'self';">`,
61-
);
73+
const csps = getCsps(result);
74+
expect(csps.length).toBe(1);
75+
expect(csps[0]).toMatch(ONE_HASH_CSP);
6276
// Our loader script appears after the HTML text content.
6377
expect(result).toMatch(
6478
/Some text<\/div>\s*<script>\s*var scripts = \[\['.\/main.js', undefined, false, false\]\];/,
@@ -80,9 +94,9 @@ describe('auto-csp', () => {
8094
</html>
8195
`);
8296

83-
expect(result).toContain(
84-
`<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' 'sha256-oK8+CQgKHPljcYJpTNKJt/y0A0oiBIm3LRke3EhzHVQ=' https: 'unsafe-inline';object-src 'none';base-uri 'self';">`,
85-
);
97+
const csps = getCsps(result);
98+
expect(csps.length).toBe(1);
99+
expect(csps[0]).toMatch(ONE_HASH_CSP);
86100
expect(result).toContain(
87101
`var scripts = [['./main1.js', undefined, false, false],['./main2.js', undefined, true, false],['./main3.js', 'module', true, true]];`,
88102
);
@@ -107,9 +121,12 @@ describe('auto-csp', () => {
107121
</html>
108122
`);
109123

110-
expect(result).toContain(
111-
`<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' ${hashScriptText("console.log('foo');")} 'sha256-6q4qOp9MMB///5kaRda2I++J9l0mJiqWRxQ9/8hoSyw=' ${hashScriptText("console.log('bar');")} 'sha256-AUmEDzNdja438OLB3B8Opyxy9B3Tr1Tib+aaGZdhhWQ=' https: 'unsafe-inline';object-src 'none';base-uri 'self';">`,
112-
);
124+
const csps = getCsps(result);
125+
expect(csps.length).toBe(1);
126+
// Exactly four hashes for the four scripts that remain (inline, loader, inline, loader).
127+
expect(csps[0]).toMatch(FOUR_HASH_CSP);
128+
expect(csps[0]).toContain(hashTextContent("console.log('foo');"));
129+
expect(csps[0]).toContain(hashTextContent("console.log('bar');"));
113130
// Loader script for main.js and main2.js appear after 'foo' and before 'bar'.
114131
expect(result).toMatch(
115132
/console.log\('foo'\);<\/script>\s*<script>\s*var scripts = \[\['.\/main.js', undefined, false, false\],\['.\/main2.js', undefined, false, false\]\];[\s\S]*console.log\('bar'\);/,

0 commit comments

Comments
 (0)