238 行
7.2 KiB
JavaScript
238 行
7.2 KiB
JavaScript
/* global browser */
|
|
|
|
import * as shared from './shared.js';
|
|
import * as storage from './storage.js';
|
|
|
|
var glob = function(s, pattern) {
|
|
var p = pattern.split('*');
|
|
return s.startsWith(p[0]) && s.endsWith(p.at(-1));
|
|
};
|
|
|
|
var getHostname = function(url, patterns) {
|
|
var u = new URL(url);
|
|
|
|
for (var pattern of patterns) {
|
|
if (glob(u.hostname, pattern)) {
|
|
return pattern;
|
|
}
|
|
}
|
|
|
|
return u.hostname;
|
|
};
|
|
|
|
var setRule = async function(context, hostname, type, rule) {
|
|
var savedRules = await storage.get('savedRules');
|
|
await storage.change('rules', rules => {
|
|
if (hostname === 'first-party') {
|
|
context = '*';
|
|
}
|
|
if (!rules[context]) {
|
|
rules[context] = savedRules[context] || {};
|
|
}
|
|
if (!rules[context][hostname]) {
|
|
rules[context][hostname] = {};
|
|
}
|
|
if (rule) {
|
|
rules[context][hostname][type] = rule;
|
|
} else {
|
|
delete rules[context][hostname][type];
|
|
if (Object.keys(rules[context][hostname]).length === 0) {
|
|
delete rules[context][hostname];
|
|
}
|
|
if (Object.keys(rules[context]).length === 0 && !savedRules[context]) {
|
|
delete rules[context];
|
|
}
|
|
}
|
|
return rules;
|
|
});
|
|
};
|
|
|
|
var getPatterns = async function() {
|
|
var savedRules = await storage.get('savedRules');
|
|
return savedRules._patterns || [];
|
|
};
|
|
|
|
var getRules = async function(context) {
|
|
var [rules, savedRules] = await Promise.all([
|
|
storage.get('rules'),
|
|
storage.get('savedRules'),
|
|
]);
|
|
var restricted = {};
|
|
restricted['*'] = rules['*'] || savedRules['*'] || {};
|
|
restricted[context] = rules[context] || savedRules[context] || {};
|
|
restricted.dirty = !!rules[context];
|
|
return restricted;
|
|
};
|
|
|
|
var pushRequest = async function(tabId, hostname, type) {
|
|
var recording = await storage.get('recording');
|
|
if (recording) {
|
|
await storage.change('requests', requests => {
|
|
if (!requests[tabId]) {
|
|
requests[tabId] = {};
|
|
}
|
|
if (!requests[tabId][hostname]) {
|
|
requests[tabId][hostname] = {};
|
|
}
|
|
if (!requests[tabId][hostname][type]) {
|
|
requests[tabId][hostname][type] = 0;
|
|
}
|
|
requests[tabId][hostname][type] += 1;
|
|
return requests;
|
|
});
|
|
}
|
|
};
|
|
|
|
var clearRequests = async function(tabId) {
|
|
await storage.change('requests', requests => {
|
|
if (requests[tabId]) {
|
|
delete requests[tabId];
|
|
}
|
|
return requests;
|
|
});
|
|
};
|
|
|
|
var getCurrentTab = async function() {
|
|
var tabs = await browser.tabs.query({
|
|
active: true,
|
|
currentWindow: true,
|
|
});
|
|
return tabs[0];
|
|
};
|
|
|
|
browser.runtime.onMessage.addListener(async (msg, sender) => {
|
|
if (msg.type === 'get') {
|
|
const [tab, patterns] = await Promise.all([
|
|
getCurrentTab(),
|
|
getPatterns(),
|
|
]);
|
|
const context = getHostname(tab.url, patterns);
|
|
const [rules, requests, recording] = await Promise.all([
|
|
getRules(context),
|
|
storage.get('requests'),
|
|
storage.get('recording'),
|
|
]);
|
|
return {
|
|
context: context,
|
|
rules: rules,
|
|
requests: requests[tab.id] || {},
|
|
recording: recording,
|
|
};
|
|
} else if (msg.type === 'setRule') {
|
|
await setRule(
|
|
msg.data.context,
|
|
msg.data.hostname,
|
|
msg.data.type,
|
|
msg.data.value,
|
|
);
|
|
return await getRules(msg.data.context);
|
|
} else if (msg.type === 'commit') {
|
|
let r;
|
|
await storage.change('rules', rules => {
|
|
r = rules[msg.data];
|
|
delete rules[msg.data];
|
|
return rules;
|
|
});
|
|
await storage.change('savedRules', savedRules => {
|
|
if (Object.keys(r).length === 0) {
|
|
delete savedRules[msg.data];
|
|
} else {
|
|
savedRules[msg.data] = r;
|
|
}
|
|
return savedRules;
|
|
});
|
|
} else if (msg.type === 'reset') {
|
|
await storage.change('rules', rules => {
|
|
delete rules[msg.data];
|
|
return rules;
|
|
});
|
|
} else if (msg.type === 'securitypolicyviolation') {
|
|
await pushRequest(sender.tab.id, 'inline', msg.data);
|
|
} else if (msg.type === 'toggleRecording') {
|
|
await storage.change('recording', recording => !recording);
|
|
}
|
|
});
|
|
|
|
browser.tabs.onRemoved.addListener(clearRequests);
|
|
browser.webNavigation.onBeforeNavigate.addListener(async details => {
|
|
if (details.frameId === 0) {
|
|
await clearRequests(details.tabId);
|
|
}
|
|
});
|
|
|
|
browser.webRequest.onBeforeSendHeaders.addListener(async details => {
|
|
var patterns = await getPatterns();
|
|
var context = getHostname(details.documentUrl || details.url, patterns);
|
|
if (details.frameAncestors && details.frameAncestors.length) {
|
|
var last = details.frameAncestors.length - 1;
|
|
context = getHostname(details.frameAncestors[last].url, patterns);
|
|
}
|
|
var hostname = getHostname(details.url, patterns);
|
|
var type = shared.TYPE_MAP[details.type] || 'other';
|
|
|
|
var promises = [
|
|
getRules(context),
|
|
];
|
|
|
|
if (details.type !== 'main_frame') {
|
|
promises.push(pushRequest(details.tabId, hostname, type));
|
|
}
|
|
|
|
var isCookie = h => h.name.toLowerCase() === 'cookie';
|
|
if (details.requestHeaders.some(isCookie)) {
|
|
promises.push(pushRequest(details.tabId, hostname, 'cookie'));
|
|
}
|
|
|
|
var [rules, ..._rest] = await Promise.all(promises);
|
|
if (
|
|
details.type !== 'main_frame'
|
|
&& !shared.shouldAllow(rules, context, hostname, type)
|
|
) {
|
|
if (details.type === 'sub_frame') {
|
|
// this can in turn be blocked by a local CSP
|
|
return {redirectUrl: 'data:,' + encodeURIComponent(details.url)};
|
|
} else {
|
|
return {cancel: true};
|
|
}
|
|
}
|
|
|
|
if (shared.shouldAllow(rules, context, hostname, 'cookie')) {
|
|
return {requestHeaders: details.requestHeaders};
|
|
} else {
|
|
var filtered = details.requestHeaders.filter(h => !isCookie(h));
|
|
return {requestHeaders: filtered};
|
|
}
|
|
}, {urls: ['<all_urls>']}, ['blocking', 'requestHeaders']);
|
|
|
|
browser.webRequest.onHeadersReceived.addListener(async details => {
|
|
var patterns = await getPatterns();
|
|
var context = getHostname(details.url, patterns);
|
|
var [rules, recording] = await Promise.all([
|
|
getRules(context),
|
|
storage.get('recording'),
|
|
]);
|
|
var csp = (type, value) => {
|
|
var name = 'Content-Security-Policy';
|
|
if (shared.shouldAllow(rules, context, 'inline', type)) {
|
|
if (recording) {
|
|
name = 'Content-Security-Policy-Report-Only';
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
details.responseHeaders.push({
|
|
name: name,
|
|
value: value,
|
|
});
|
|
};
|
|
|
|
csp('css', "style-src 'self' *");
|
|
csp('script', "script-src 'self' *");
|
|
csp('media', "img-src 'self' *");
|
|
|
|
return {responseHeaders: details.responseHeaders};
|
|
}, {
|
|
urls: ['<all_urls>'],
|
|
types: ['main_frame'],
|
|
}, ['blocking', 'responseHeaders']);
|