-
Notifications
You must be signed in to change notification settings - Fork 62
/
util.js
192 lines (175 loc) · 5.68 KB
/
util.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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
const tldts = require('tldts');
const crypto = require('crypto');
function getAllowlistedRule(rules, rulePath) {
return rules.find(function (x) {
return x.rule === rulePath;
});
}
function addDomainRules(allowlist, domain, rule) {
const found = allowlist[domain];
const existing = found || { rules: [] };
if (!found) {
allowlist[domain] = existing;
}
const newRules = rule.rules || [];
newRules.forEach(function (r) {
addPathRule(existing.rules, r);
});
// very basic substring checking to order more-specific rules earlier (doesn't deal with regexp rules)
existing.rules.sort(function (a, b) {
return a.rule.includes(b.rule) ? -1 : b.rule.includes(a.rule) ? 1 : 0;
});
}
function addAllowlistRule(allowlist, rule) {
const dom = tldts.getDomain(rule.rule);
addDomainRules(allowlist, dom, { rules: [rule] });
}
function addPathRule(rules, rule) {
const found = getAllowlistedRule(rules, rule.rule);
const existing = found || { rule: rule.rule, domains: [], reason: '' };
if (!found) {
rules.push(existing);
}
existing.domains = Array.from(new Set(existing.domains.concat(rule.domains).sort()));
if (existing.domains.includes('<all>')) {
existing.domains = ['<all>'];
}
if (existing.reason === undefined) {
return;
}
const reasons = existing.reason.split('; ');
const newReason = rule.reason;
if (!reasons.includes(rule.reason)) {
existing.reason = reasons
.concat([newReason])
.filter(function (x) {
return x !== '';
})
.join('; ');
}
}
function mergeAllowlistedTrackers(t1, t2) {
const res = {};
for (const dom in t1) {
addDomainRules(res, dom, t1[dom]);
}
for (const dom in t2) {
addDomainRules(res, dom, t2[dom]);
}
// Sort the resulting generated object by domain keys.
// This makes working with the generated config easier and more human-navigable.
return Object.keys(res)
.sort()
.reduce(function (acc, k) {
acc[k] = res[k];
return acc;
}, {});
}
/**
* Traverse the input (JSON data) and ensure any "reason" fields are strings in the output.
*
* This allows specifying reasons as an array of strings, and converts these to
* strings in the resulting data.
*/
function inlineReasonArrays(data) {
if (Array.isArray(data)) {
return data.map(inlineReasonArrays);
} else if (typeof data === 'object' && data !== null) {
const res = {};
for (const [k, v] of Object.entries(data)) {
if (k === 'reason') {
// we collapse list 'reason' field values into a single string
res[k] = Array.isArray(v) ? v.join(' ') : v;
} else {
res[k] = inlineReasonArrays(v);
}
}
return res;
} else {
return data;
}
}
/**
* All domains that may map to the given cnameTarget.
*/
function getCnameSources(tds, cnameTarget) {
return Object.entries(tds.cnames)
.filter(([k, v]) => v.endsWith(cnameTarget))
.map((kv) => kv[0]);
}
/**
* Generate rules which CNAME to the CNAMEd rule.
*/
function generateCnameRules(tds, cnamedRule) {
const dom = cnamedRule.rule.split('/')[0];
const sources = getCnameSources(tds, dom);
const resultRules = [];
for (const source of sources) {
resultRules.push({
...cnamedRule,
rule: cnamedRule.rule.replace(dom, source),
reason: 'CNAME ENTRY GENERATED FROM: ' + dom,
});
}
return resultRules;
}
/**
* Add CNAME entries to the allowlist to support platforms with incorrect CNAME resolution.
*/
function addCnameEntriesToAllowlist(tds, allowlist) {
Object.values(allowlist).forEach((ruleSet) =>
ruleSet.rules.forEach((rule) => {
generateCnameRules(tds, rule).forEach((rule) => addAllowlistRule(allowlist, rule));
}),
);
}
/**
* Adds a hash of each feature to each feature object of the provided config
*
* @param {object} config - the config object to update
*/
function addHashToFeatures(config) {
for (const key of Object.keys(config.features)) {
const featureString = JSON.stringify(config.features[key]);
config.features[key].hash = crypto.createHash('md5').update(featureString).digest('hex');
}
}
/**
* Removes reason fields from the config object
*
* @param {object} config - the config object to update
*/
function stripReasons(config) {
for (const key of Object.keys(config.features)) {
for (const exception of config.features[key].exceptions) {
delete exception.reason;
}
if (key === 'trackerAllowlist') {
for (const domain of Object.keys(config.features[key].settings.allowlistedTrackers)) {
for (const rule of config.features[key].settings.allowlistedTrackers[domain].rules) {
delete rule.reason;
}
}
}
if (key === 'customUserAgent') {
if (config.features[key].settings.omitApplicationSites) {
for (const exception of config.features[key].settings.omitApplicationSites) {
delete exception.reason;
}
}
if (config.features[key].settings.omitVersionSites) {
for (const exception of config.features[key].settings.omitVersionSites) {
delete exception.reason;
}
}
}
}
}
module.exports = {
addAllowlistRule,
addCnameEntriesToAllowlist,
inlineReasonArrays,
mergeAllowlistedTrackers,
addHashToFeatures,
stripReasons,
};