-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
136 lines (106 loc) · 3.63 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const through = require('through2');
const cheerio = require('cheerio');
const PluginError = require('plugin-error');
const PLUGIN_NAME = 'gulp-sri-hash';
const DEFAULT_ALGO = 'sha384';
const DEFAULT_SELECTOR = 'link[href][rel=stylesheet]:not([integrity]), script[src]:not([integrity])';
const supportedAlgos = new Set(['sha256', 'sha384', 'sha512']);
const cache = new Map();
const normalizePath = (node, { prefix }) => {
let src = node.name === 'script' ? node.attribs.src : node.attribs.href;
if (!src) {
return null;
}
// strip prefix if present and match
if (prefix.length && src.indexOf(prefix) === 0) {
src = src.slice(prefix.length);
}
// ignore paths that look like like urls as they cannot be resolved on local filesystem
if (src.match(/^(https?:)?\/\//)) {
return null;
}
// make path "rel-absolute"
if (src.charCodeAt(0) !== path.sep.charCodeAt(0)) {
src = path.sep + src;
}
// remove query-string from path
if (src.includes('?')) {
return src.substr(0, src.indexOf('?'));
}
return src;
};
const resolveRelativePath = (file, localPath) => path.join(path.dirname(file.path), localPath);
const resolveAbsolutePath = (file, localPath) => path.normalize(file.base + localPath);
const calculateSri = (fullPath, algorithm) => {
const file = fs.readFileSync(fullPath);
return crypto.createHash(algorithm).update(file).digest('base64');
};
const getFileHash = (fullPath, algorithm) => {
if (!cache.has(fullPath)) {
cache.set(fullPath, [algorithm, calculateSri(fullPath, algorithm)].join('-'));
}
return cache.get(fullPath);
};
const getParser = (file, mutateVinyl) => {
if (mutateVinyl && file.cheerio) {
return file.cheerio;
}
const parser = cheerio.load(file.contents, { decodeEntities: false });
if (mutateVinyl) {
Object.assign(file, {
cheerio: parser,
});
}
return parser;
};
const updateDOM = (file, config) => {
const $ = getParser(file, config.cacheParser);
const $candidates = $(config.selector);
const resolver = config.relative ? resolveRelativePath : resolveAbsolutePath;
const addIntegrityAttribute = (idx, node) => {
const localPath = normalizePath(node, config);
if (localPath) {
$(node).attr('integrity', getFileHash(resolver(file, localPath), config.algo));
if ($(node).attr('crossorigin') !== 'use-credentials') {
$(node).attr('crossorigin', 'anonymous');
}
}
};
if ($candidates.length > 0) {
$candidates.each(addIntegrityAttribute);
Object.assign(file, { contents: Buffer.from($.html()) });
}
return file;
};
const transformFactory = (config) => function transform(file, encoding, callback) {
if (file.isBuffer()) {
return callback(null, updateDOM(file, config));
}
if (file.isStream()) {
this.emit('error', new PluginError(PLUGIN_NAME, 'Streams are not supported!'));
}
return callback(null, file);
};
const configure = (options = {}) => {
const config = {
algo: options.algo || DEFAULT_ALGO,
prefix: options.prefix || '',
selector: options.selector || DEFAULT_SELECTOR,
relative: !!options.relative || false,
cacheParser: !!options.cacheParser || false,
};
if (!supportedAlgos.has(config.algo)) {
throw new PluginError(PLUGIN_NAME, 'Hashing algorithm is unsupported');
}
return config;
};
const gulpSriHashPlugin = (options) => {
// always clear cache per invocation, e.g. when part of a `gulp.watch`
cache.clear();
return through.obj(transformFactory(configure(options)));
};
module.exports = gulpSriHashPlugin;
module.exports.PLUGIN_NAME = PLUGIN_NAME;