vite-plugin-sri4 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +104 -126
- package/dist/index.js +104 -126
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,191 +1,169 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
4
|
var fetch = require('node-fetch');
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
algorithm: 'sha384',
|
|
8
|
+
bypassDomains: [],
|
|
9
|
+
crossorigin: 'anonymous',
|
|
10
|
+
debug: false
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function log(message, options) {
|
|
14
|
+
if (options.debug) {
|
|
15
|
+
console.log(`[vite-plugin-sri4] ${message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
function computeSri(content, algorithm = 'sha384') {
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
try {
|
|
21
|
+
const hash = node_crypto.createHash(algorithm)
|
|
22
|
+
.update(content)
|
|
23
|
+
.digest('base64');
|
|
24
|
+
return `${algorithm}-${hash}`;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`[vite-plugin-sri4] Failed to compute SRI hash: ${error}`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
* Check if an external resource supports CORS.
|
|
21
|
-
* It sends a HEAD request to the given URL and examines the "Access-Control-Allow-Origin" header.
|
|
22
|
-
* Adjust the logic to match your security policy.
|
|
23
|
-
* @param {string} url - The URL to check.
|
|
24
|
-
* @returns {Promise<boolean>} - A promise that resolves to true if CORS is enabled.
|
|
25
|
-
*/
|
|
26
|
-
async function externalResourceIsCorsEnabled(url) {
|
|
31
|
+
async function externalResourceIsCorsEnabled(url, options) {
|
|
27
32
|
try {
|
|
28
|
-
const response = await fetch(url, {
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
method: 'HEAD',
|
|
35
|
+
timeout: 5000 // 5 seconds timeout
|
|
36
|
+
});
|
|
29
37
|
const acao = response.headers.get('access-control-allow-origin');
|
|
30
|
-
|
|
31
|
-
if (acao && (acao === '*' || acao.includes('your-domain.com'))) {
|
|
38
|
+
if (acao && (acao === '*' || acao.includes(options.domain || ''))) {
|
|
32
39
|
return true;
|
|
33
40
|
}
|
|
34
41
|
return false;
|
|
35
42
|
} catch (error) {
|
|
36
|
-
|
|
43
|
+
log(`Failed to fetch CORS headers from ${url}: ${error}`, options);
|
|
37
44
|
return false;
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
/**
|
|
42
|
-
* Helper function to perform an asynchronous replacement in a string.
|
|
43
|
-
* @param {string} str - The input string.
|
|
44
|
-
* @param {RegExp} regex - The regular expression to match parts of the string.
|
|
45
|
-
* @param {Function} asyncFn - An async function to compute the replacement.
|
|
46
|
-
* @returns {Promise<string>} - The string with replaced values.
|
|
47
|
-
*/
|
|
48
48
|
async function replaceAsync(str, regex, asyncFn) {
|
|
49
|
+
const promises = [];
|
|
49
50
|
const matches = [];
|
|
51
|
+
|
|
50
52
|
str.replace(regex, (...args) => {
|
|
51
53
|
matches.push(args);
|
|
54
|
+
promises.push(asyncFn(...args));
|
|
52
55
|
return '';
|
|
53
56
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
|
|
58
|
+
const results = await Promise.all(promises);
|
|
59
|
+
|
|
60
|
+
let lastIndex = 0;
|
|
61
|
+
let result = '';
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < matches.length; i++) {
|
|
64
|
+
const match = matches[i][0];
|
|
65
|
+
const index = str.indexOf(match, lastIndex);
|
|
66
|
+
result += str.slice(lastIndex, index) + results[i];
|
|
67
|
+
lastIndex = index + match.length;
|
|
58
68
|
}
|
|
59
|
-
|
|
69
|
+
|
|
70
|
+
result += str.slice(lastIndex);
|
|
71
|
+
return result;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
/**
|
|
63
|
-
* Determines if the URL belongs to a domain specified in the bypassDomains array.
|
|
64
|
-
* If so, the SRI injection will be skipped for that resource.
|
|
65
|
-
* @param {string} url - The URL to check.
|
|
66
|
-
* @param {Array<string>} bypassDomains - Array of domains to bypass SRI injection.
|
|
67
|
-
* @returns {boolean} - True if the URL should bypass SRI injection.
|
|
68
|
-
*/
|
|
69
74
|
function isBypassDomain(url, bypassDomains = []) {
|
|
70
75
|
if (!bypassDomains.length) return false;
|
|
71
76
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return bypassDomains.some(
|
|
77
|
+
const parsedUrl = url.startsWith('//')
|
|
78
|
+
? new URL(url, 'http://dummy')
|
|
79
|
+
: new URL(url);
|
|
80
|
+
return bypassDomains.some(
|
|
81
|
+
(domain) => parsedUrl.hostname === domain ||
|
|
82
|
+
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
83
|
+
);
|
|
76
84
|
} catch (e) {
|
|
77
85
|
return false;
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* Plugin options:
|
|
85
|
-
* - algorithm: The algorithm used to compute SRI hash (default: 'sha384').
|
|
86
|
-
* - bypassDomains: Array of domains for which to skip injecting the integrity attribute.
|
|
87
|
-
*
|
|
88
|
-
* This plugin works during the build process:
|
|
89
|
-
* 1. In the generateBundle hook, it calculates the SRI hash for all assets/chunks and stores them in sriMap.
|
|
90
|
-
* 2. In the transformIndexHtml hook, it injects the integrity and crossorigin attributes into the HTML.
|
|
91
|
-
* For external links, it verifies via a CORS check if the resource supports cross-origin access.
|
|
92
|
-
*
|
|
93
|
-
* @param {Object} options - Plugin configuration options.
|
|
94
|
-
* @returns {Object} - The Vite plugin.
|
|
95
|
-
*/
|
|
96
|
-
function sri(options = {}) {
|
|
97
|
-
// Use the provided algorithm from options, defaulting to 'sha384' if not specified.
|
|
98
|
-
const algorithm = options.algorithm || 'sha384';
|
|
99
|
-
// Array for domains to bypass SRI injection.
|
|
100
|
-
const bypassDomains = options.bypassDomains || [];
|
|
101
|
-
// Map to store SRI hashes for assets; key is the file name.
|
|
102
|
-
const sriMap = {};
|
|
89
|
+
function sri(userOptions = {}) {
|
|
90
|
+
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
91
|
+
const sriMap = new Map();
|
|
103
92
|
|
|
104
93
|
return {
|
|
105
94
|
name: 'vite-plugin-sri4',
|
|
106
95
|
apply: 'build',
|
|
107
96
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
97
|
+
configResolved(config) {
|
|
98
|
+
options.domain = config.server?.host || '';
|
|
99
|
+
},
|
|
100
|
+
|
|
112
101
|
async generateBundle(_, bundle) {
|
|
113
102
|
for (const fileName in bundle) {
|
|
114
103
|
const chunk = bundle[fileName];
|
|
115
104
|
if (chunk.type === 'chunk' || chunk.type === 'asset') {
|
|
116
105
|
const content = chunk.code || chunk.source;
|
|
117
106
|
if (content) {
|
|
118
|
-
|
|
119
|
-
|
|
107
|
+
const hash = computeSri(content, options.algorithm);
|
|
108
|
+
if (hash) {
|
|
109
|
+
sriMap.set(fileName, hash);
|
|
110
|
+
log(`Computed SRI for ${fileName}: ${hash}`, options);
|
|
111
|
+
}
|
|
120
112
|
}
|
|
121
113
|
}
|
|
122
114
|
}
|
|
123
115
|
},
|
|
124
116
|
|
|
125
|
-
/**
|
|
126
|
-
* The transformIndexHtml hook processes the generated HTML and injects the integrity and crossorigin attributes.
|
|
127
|
-
* For external resources, a CORS check is performed first.
|
|
128
|
-
* If the URL belongs to a bypass domain, the injection is skipped.
|
|
129
|
-
* @param {string} html - The HTML content to transform.
|
|
130
|
-
* @returns {Promise<string>} - The transformed HTML.
|
|
131
|
-
*/
|
|
132
117
|
async transformIndexHtml(html) {
|
|
133
|
-
|
|
118
|
+
const hasIntegrity = (tag) => /integrity=/i.test(tag);
|
|
134
119
|
const isExternalUrl = (url) => /^(https?:)?\/\//i.test(url);
|
|
135
120
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
121
|
+
const processTag = async (match, tag, src, moduleSrc) => {
|
|
122
|
+
const actualSrc = src || moduleSrc;
|
|
123
|
+
|
|
124
|
+
if (hasIntegrity(tag)) {
|
|
125
|
+
return tag;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (isExternalUrl(actualSrc) &&
|
|
129
|
+
isBypassDomain(actualSrc, options.bypassDomains)) {
|
|
130
|
+
log(`Skipping SRI injection for bypass domain: ${actualSrc}`, options);
|
|
131
|
+
return tag;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isExternalUrl(actualSrc)) {
|
|
135
|
+
const corsOk = await externalResourceIsCorsEnabled(actualSrc, options);
|
|
136
|
+
if (!corsOk) {
|
|
137
|
+
log(`External resource ${actualSrc} does not support CORS`, options);
|
|
144
138
|
return tag;
|
|
145
139
|
}
|
|
140
|
+
}
|
|
146
141
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// For relative URLs or valid external URLs, use the file name as the key.
|
|
156
|
-
const fileName = src.startsWith('/') ? src.slice(1) : src;
|
|
157
|
-
if (sriMap[fileName]) {
|
|
158
|
-
return tag.replace(/>$/, ` integrity="${sriMap[fileName]}" crossorigin="anonymous">`);
|
|
159
|
-
}
|
|
160
|
-
return tag;
|
|
142
|
+
const fileName = actualSrc.startsWith('/') ? actualSrc.slice(1) : actualSrc;
|
|
143
|
+
const integrity = sriMap.get(fileName);
|
|
144
|
+
|
|
145
|
+
if (integrity) {
|
|
146
|
+
return tag.replace(
|
|
147
|
+
/>$/,
|
|
148
|
+
` integrity="${integrity}" crossorigin="${options.crossorigin}">`
|
|
149
|
+
);
|
|
161
150
|
}
|
|
151
|
+
|
|
152
|
+
return tag;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Process scripts
|
|
156
|
+
html = await replaceAsync(
|
|
157
|
+
html,
|
|
158
|
+
/(<script[^>]+(?:src="([^"]+)"[^>]*|type="module"[^>]*src="([^"]+)"[^>]*)>)/g,
|
|
159
|
+
processTag
|
|
162
160
|
);
|
|
163
161
|
|
|
164
|
-
// Process
|
|
162
|
+
// Process links
|
|
165
163
|
html = await replaceAsync(
|
|
166
164
|
html,
|
|
167
165
|
/(<link[^>]+href="([^"]+)"[^>]*>)/g,
|
|
168
|
-
|
|
169
|
-
// Skip SRI injection for external URLs that are in the bypass list.
|
|
170
|
-
if (isExternalUrl(href) && isBypassDomain(href, bypassDomains)) {
|
|
171
|
-
console.log(`Skipping SRI injection for bypass domain: ${href}`);
|
|
172
|
-
return tag;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (isExternalUrl(href)) {
|
|
176
|
-
// For external links not in the bypass list, perform a CORS check.
|
|
177
|
-
const corsOk = await externalResourceIsCorsEnabled(href);
|
|
178
|
-
if (!corsOk) {
|
|
179
|
-
console.warn(`External resource ${href} does not support CORS. Skipping SRI injection.`);
|
|
180
|
-
return tag;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
const fileName = href.startsWith('/') ? href.slice(1) : href;
|
|
184
|
-
if (sriMap[fileName]) {
|
|
185
|
-
return tag.replace(/>$/, ` integrity="${sriMap[fileName]}" crossorigin="anonymous">`);
|
|
186
|
-
}
|
|
187
|
-
return tag;
|
|
188
|
-
}
|
|
166
|
+
processTag
|
|
189
167
|
);
|
|
190
168
|
|
|
191
169
|
return html;
|
package/dist/index.js
CHANGED
|
@@ -1,189 +1,167 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
2
|
import fetch from 'node-fetch';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
const DEFAULT_OPTIONS = {
|
|
5
|
+
algorithm: 'sha384',
|
|
6
|
+
bypassDomains: [],
|
|
7
|
+
crossorigin: 'anonymous',
|
|
8
|
+
debug: false
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function log(message, options) {
|
|
12
|
+
if (options.debug) {
|
|
13
|
+
console.log(`[vite-plugin-sri4] ${message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
function computeSri(content, algorithm = 'sha384') {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
try {
|
|
19
|
+
const hash = createHash(algorithm)
|
|
20
|
+
.update(content)
|
|
21
|
+
.digest('base64');
|
|
22
|
+
return `${algorithm}-${hash}`;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(`[vite-plugin-sri4] Failed to compute SRI hash: ${error}`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
* Check if an external resource supports CORS.
|
|
19
|
-
* It sends a HEAD request to the given URL and examines the "Access-Control-Allow-Origin" header.
|
|
20
|
-
* Adjust the logic to match your security policy.
|
|
21
|
-
* @param {string} url - The URL to check.
|
|
22
|
-
* @returns {Promise<boolean>} - A promise that resolves to true if CORS is enabled.
|
|
23
|
-
*/
|
|
24
|
-
async function externalResourceIsCorsEnabled(url) {
|
|
29
|
+
async function externalResourceIsCorsEnabled(url, options) {
|
|
25
30
|
try {
|
|
26
|
-
const response = await fetch(url, {
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
method: 'HEAD',
|
|
33
|
+
timeout: 5000 // 5 seconds timeout
|
|
34
|
+
});
|
|
27
35
|
const acao = response.headers.get('access-control-allow-origin');
|
|
28
|
-
|
|
29
|
-
if (acao && (acao === '*' || acao.includes('your-domain.com'))) {
|
|
36
|
+
if (acao && (acao === '*' || acao.includes(options.domain || ''))) {
|
|
30
37
|
return true;
|
|
31
38
|
}
|
|
32
39
|
return false;
|
|
33
40
|
} catch (error) {
|
|
34
|
-
|
|
41
|
+
log(`Failed to fetch CORS headers from ${url}: ${error}`, options);
|
|
35
42
|
return false;
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
/**
|
|
40
|
-
* Helper function to perform an asynchronous replacement in a string.
|
|
41
|
-
* @param {string} str - The input string.
|
|
42
|
-
* @param {RegExp} regex - The regular expression to match parts of the string.
|
|
43
|
-
* @param {Function} asyncFn - An async function to compute the replacement.
|
|
44
|
-
* @returns {Promise<string>} - The string with replaced values.
|
|
45
|
-
*/
|
|
46
46
|
async function replaceAsync(str, regex, asyncFn) {
|
|
47
|
+
const promises = [];
|
|
47
48
|
const matches = [];
|
|
49
|
+
|
|
48
50
|
str.replace(regex, (...args) => {
|
|
49
51
|
matches.push(args);
|
|
52
|
+
promises.push(asyncFn(...args));
|
|
50
53
|
return '';
|
|
51
54
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
const results = await Promise.all(promises);
|
|
57
|
+
|
|
58
|
+
let lastIndex = 0;
|
|
59
|
+
let result = '';
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < matches.length; i++) {
|
|
62
|
+
const match = matches[i][0];
|
|
63
|
+
const index = str.indexOf(match, lastIndex);
|
|
64
|
+
result += str.slice(lastIndex, index) + results[i];
|
|
65
|
+
lastIndex = index + match.length;
|
|
56
66
|
}
|
|
57
|
-
|
|
67
|
+
|
|
68
|
+
result += str.slice(lastIndex);
|
|
69
|
+
return result;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
/**
|
|
61
|
-
* Determines if the URL belongs to a domain specified in the bypassDomains array.
|
|
62
|
-
* If so, the SRI injection will be skipped for that resource.
|
|
63
|
-
* @param {string} url - The URL to check.
|
|
64
|
-
* @param {Array<string>} bypassDomains - Array of domains to bypass SRI injection.
|
|
65
|
-
* @returns {boolean} - True if the URL should bypass SRI injection.
|
|
66
|
-
*/
|
|
67
72
|
function isBypassDomain(url, bypassDomains = []) {
|
|
68
73
|
if (!bypassDomains.length) return false;
|
|
69
74
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return bypassDomains.some(
|
|
75
|
+
const parsedUrl = url.startsWith('//')
|
|
76
|
+
? new URL(url, 'http://dummy')
|
|
77
|
+
: new URL(url);
|
|
78
|
+
return bypassDomains.some(
|
|
79
|
+
(domain) => parsedUrl.hostname === domain ||
|
|
80
|
+
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
81
|
+
);
|
|
74
82
|
} catch (e) {
|
|
75
83
|
return false;
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* Plugin options:
|
|
83
|
-
* - algorithm: The algorithm used to compute SRI hash (default: 'sha384').
|
|
84
|
-
* - bypassDomains: Array of domains for which to skip injecting the integrity attribute.
|
|
85
|
-
*
|
|
86
|
-
* This plugin works during the build process:
|
|
87
|
-
* 1. In the generateBundle hook, it calculates the SRI hash for all assets/chunks and stores them in sriMap.
|
|
88
|
-
* 2. In the transformIndexHtml hook, it injects the integrity and crossorigin attributes into the HTML.
|
|
89
|
-
* For external links, it verifies via a CORS check if the resource supports cross-origin access.
|
|
90
|
-
*
|
|
91
|
-
* @param {Object} options - Plugin configuration options.
|
|
92
|
-
* @returns {Object} - The Vite plugin.
|
|
93
|
-
*/
|
|
94
|
-
function sri(options = {}) {
|
|
95
|
-
// Use the provided algorithm from options, defaulting to 'sha384' if not specified.
|
|
96
|
-
const algorithm = options.algorithm || 'sha384';
|
|
97
|
-
// Array for domains to bypass SRI injection.
|
|
98
|
-
const bypassDomains = options.bypassDomains || [];
|
|
99
|
-
// Map to store SRI hashes for assets; key is the file name.
|
|
100
|
-
const sriMap = {};
|
|
87
|
+
function sri(userOptions = {}) {
|
|
88
|
+
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
89
|
+
const sriMap = new Map();
|
|
101
90
|
|
|
102
91
|
return {
|
|
103
92
|
name: 'vite-plugin-sri4',
|
|
104
93
|
apply: 'build',
|
|
105
94
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
configResolved(config) {
|
|
96
|
+
options.domain = config.server?.host || '';
|
|
97
|
+
},
|
|
98
|
+
|
|
110
99
|
async generateBundle(_, bundle) {
|
|
111
100
|
for (const fileName in bundle) {
|
|
112
101
|
const chunk = bundle[fileName];
|
|
113
102
|
if (chunk.type === 'chunk' || chunk.type === 'asset') {
|
|
114
103
|
const content = chunk.code || chunk.source;
|
|
115
104
|
if (content) {
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
const hash = computeSri(content, options.algorithm);
|
|
106
|
+
if (hash) {
|
|
107
|
+
sriMap.set(fileName, hash);
|
|
108
|
+
log(`Computed SRI for ${fileName}: ${hash}`, options);
|
|
109
|
+
}
|
|
118
110
|
}
|
|
119
111
|
}
|
|
120
112
|
}
|
|
121
113
|
},
|
|
122
114
|
|
|
123
|
-
/**
|
|
124
|
-
* The transformIndexHtml hook processes the generated HTML and injects the integrity and crossorigin attributes.
|
|
125
|
-
* For external resources, a CORS check is performed first.
|
|
126
|
-
* If the URL belongs to a bypass domain, the injection is skipped.
|
|
127
|
-
* @param {string} html - The HTML content to transform.
|
|
128
|
-
* @returns {Promise<string>} - The transformed HTML.
|
|
129
|
-
*/
|
|
130
115
|
async transformIndexHtml(html) {
|
|
131
|
-
|
|
116
|
+
const hasIntegrity = (tag) => /integrity=/i.test(tag);
|
|
132
117
|
const isExternalUrl = (url) => /^(https?:)?\/\//i.test(url);
|
|
133
118
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
119
|
+
const processTag = async (match, tag, src, moduleSrc) => {
|
|
120
|
+
const actualSrc = src || moduleSrc;
|
|
121
|
+
|
|
122
|
+
if (hasIntegrity(tag)) {
|
|
123
|
+
return tag;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isExternalUrl(actualSrc) &&
|
|
127
|
+
isBypassDomain(actualSrc, options.bypassDomains)) {
|
|
128
|
+
log(`Skipping SRI injection for bypass domain: ${actualSrc}`, options);
|
|
129
|
+
return tag;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isExternalUrl(actualSrc)) {
|
|
133
|
+
const corsOk = await externalResourceIsCorsEnabled(actualSrc, options);
|
|
134
|
+
if (!corsOk) {
|
|
135
|
+
log(`External resource ${actualSrc} does not support CORS`, options);
|
|
142
136
|
return tag;
|
|
143
137
|
}
|
|
138
|
+
}
|
|
144
139
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// For relative URLs or valid external URLs, use the file name as the key.
|
|
154
|
-
const fileName = src.startsWith('/') ? src.slice(1) : src;
|
|
155
|
-
if (sriMap[fileName]) {
|
|
156
|
-
return tag.replace(/>$/, ` integrity="${sriMap[fileName]}" crossorigin="anonymous">`);
|
|
157
|
-
}
|
|
158
|
-
return tag;
|
|
140
|
+
const fileName = actualSrc.startsWith('/') ? actualSrc.slice(1) : actualSrc;
|
|
141
|
+
const integrity = sriMap.get(fileName);
|
|
142
|
+
|
|
143
|
+
if (integrity) {
|
|
144
|
+
return tag.replace(
|
|
145
|
+
/>$/,
|
|
146
|
+
` integrity="${integrity}" crossorigin="${options.crossorigin}">`
|
|
147
|
+
);
|
|
159
148
|
}
|
|
149
|
+
|
|
150
|
+
return tag;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Process scripts
|
|
154
|
+
html = await replaceAsync(
|
|
155
|
+
html,
|
|
156
|
+
/(<script[^>]+(?:src="([^"]+)"[^>]*|type="module"[^>]*src="([^"]+)"[^>]*)>)/g,
|
|
157
|
+
processTag
|
|
160
158
|
);
|
|
161
159
|
|
|
162
|
-
// Process
|
|
160
|
+
// Process links
|
|
163
161
|
html = await replaceAsync(
|
|
164
162
|
html,
|
|
165
163
|
/(<link[^>]+href="([^"]+)"[^>]*>)/g,
|
|
166
|
-
|
|
167
|
-
// Skip SRI injection for external URLs that are in the bypass list.
|
|
168
|
-
if (isExternalUrl(href) && isBypassDomain(href, bypassDomains)) {
|
|
169
|
-
console.log(`Skipping SRI injection for bypass domain: ${href}`);
|
|
170
|
-
return tag;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (isExternalUrl(href)) {
|
|
174
|
-
// For external links not in the bypass list, perform a CORS check.
|
|
175
|
-
const corsOk = await externalResourceIsCorsEnabled(href);
|
|
176
|
-
if (!corsOk) {
|
|
177
|
-
console.warn(`External resource ${href} does not support CORS. Skipping SRI injection.`);
|
|
178
|
-
return tag;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
const fileName = href.startsWith('/') ? href.slice(1) : href;
|
|
182
|
-
if (sriMap[fileName]) {
|
|
183
|
-
return tag.replace(/>$/, ` integrity="${sriMap[fileName]}" crossorigin="anonymous">`);
|
|
184
|
-
}
|
|
185
|
-
return tag;
|
|
186
|
-
}
|
|
164
|
+
processTag
|
|
187
165
|
);
|
|
188
166
|
|
|
189
167
|
return html;
|