blob: 420f99f059c97c8f3ef52bcc6b778d022f3c372e [file] [log] [blame]
import {DEFAULT_RULES, isInjectRule, isValidRule, Operator, Rule} from './utils';
let rules: Rule[] = [...DEFAULT_RULES];
interface TabState {
tab: chrome.tabs.Tab;
}
// Enable / disable with one click and persist states per tab
const TabStates = new Map<number, TabState>();
// Load default configs and states when install
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.get(['rules'], (res) => {
const existingRules: Rule[] = res['rules'] || [];
const validRules = existingRules.filter(isValidRule);
if (!validRules.length) {
chrome.storage.sync.set({rules});
} else {
chrome.storage.sync.set({rules: validRules});
}
});
});
// Check if we should enable or disable the extension when activated tab changed
chrome.tabs.onActivated.addListener(() => {
checkCurrentTab();
});
// keep a reference to lastFocusedWindow
let lastFocusedWindow: chrome.tabs.Tab;
// Communication channel between background and content_script / popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'updateRules') {
rules = request.rules;
} else if (request.type === 'disableHelper') {
const tab = request.tab || {id: undefined};
chrome.browserAction.disable(tab.id);
chrome.browserAction.setIcon({tabId: tab.id, path: 'gray-32.png'});
chrome.browserAction.setPopup({tabId: tab.id, popup: ''});
TabStates.delete(tab.id);
} else if (request.type === 'isEnabled') {
const tab = sender.tab || lastFocusedWindow || {id: undefined};
sendResponse(TabStates.has(tab.id));
}
});
function onHeadersReceived(resp: chrome.webRequest.WebResponseHeadersDetails) {
if (!resp || !resp.responseHeaders) return {};
if (!TabStates.has(resp.tabId)) {
return {responseHeaders: resp.responseHeaders};
}
let len = resp.responseHeaders.length;
if (len > 0) {
while (--len) {
const header = resp.responseHeaders[len];
if (header.name.toUpperCase() === 'X-WEBKIT-CSP') {
header.value = '*';
break;
} else if (header.name.toLowerCase() === 'access-control-allow-origin') {
resp.responseHeaders[len].value = '*';
break;
} else if (
header.name.toLowerCase() === 'cache-control' ||
header.name.toLowerCase() === 'x-google-cache-control') {
header.value = 'max-age=0, no-cache, no-store, must-revalidate';
}
}
}
// add cors and cache anyway
resp.responseHeaders.push(
{'name': 'Access-Control-Allow-Origin', 'value': '*'});
resp.responseHeaders.push({
'name': 'Cache-Control',
'value': 'max-age=0, no-cache, no-store, must-revalidate'
});
const matches = rules.filter(isValidRule)
.filter(
rule => rule.operator === Operator.REMOVE_RESPONSE_HEADER
&& !rule.disabled
&& new RegExp(rule.target).test(resp.url));
matches.forEach(rule => {
const removedHeaders = rule.destination.split(",").map(name => name.toLowerCase());
resp.responseHeaders = resp.responseHeaders
.filter(h => !removedHeaders.includes(h.name.toLowerCase()));
});
const addMatches = rules.filter(isValidRule)
.filter(
rule => rule.operator === Operator.ADD_RESPONSE_HEADER
&& !rule.disabled
&& new RegExp(rule.target).test(resp.url));
addMatches.forEach(rule => {
const addedHeaders = rule.destination.split(",")
addedHeaders.forEach(addedHeader => {
const partial = addedHeader.split("=");
if (partial.length === 2) {
resp.responseHeaders.push({
'name': partial[0],
'value': partial[1]
});
}
});
});
return {responseHeaders: resp.responseHeaders};
}
function onBeforeRequest(details: chrome.webRequest.WebRequestBodyDetails) {
if (!TabStates.has(details.tabId)) {
return {cancel: false};
}
const matches = rules.filter(isValidRule)
.filter(
rule => !isInjectRule(rule) && !rule.disabled &&
new RegExp(rule.target).test(details.url));
const blockMatch = matches.find(rule => rule.operator === Operator.BLOCK);
const redirectMatch = matches.find(rule => rule.operator === Operator.REDIRECT);
// block match takes highest priority
if (blockMatch) {
return {cancel: true};
}
// then redirect
if (redirectMatch) {
return {
redirectUrl:
details.url.replace(new RegExp(redirectMatch.target), redirectMatch.destination),
};
}
// otherwise, don't do anything
return {cancel: false};
}
function onBeforeSendHeaders(
details: chrome.webRequest.WebRequestHeadersDetails) {
if (!details || !details.requestHeaders) return {};
if (!TabStates.has(details.tabId)) {
return {requestHeaders: details.requestHeaders};
}
let len = details.requestHeaders.length;
let added = false;
while (--len) {
const header = details.requestHeaders[len];
if (header.name.toLowerCase() === 'cache-control' ||
header.name.toLowerCase() === 'x-google-cache-control') {
header.value = 'max-age=0, no-cache, no-store, must-revalidate';
added = true;
}
}
if (!added) {
details.requestHeaders.push({
'name': 'Cache-Control',
'value': 'max-age=0, no-cache, no-store, must-revalidate'
});
}
const matches = rules.filter(isValidRule)
.filter(
rule => rule.operator === Operator.ADD_REQUEST_HEADER
&& !rule.disabled
&& new RegExp(rule.target).test(details.url));
matches.forEach(rule => {
const addedHeaders = rule.destination.split(",")
addedHeaders.forEach(addedHeader => {
const partial = addedHeader.split("=");
if (partial.length === 2) {
details.requestHeaders.push({
'name': partial[0],
'value': partial[1]
});
}
});
});
return {requestHeaders: details.requestHeaders};
}
// remove csp
// add cors header to all response
function setUpListeners() {
// if already registered, return
if (chrome.webRequest.onHeadersReceived.hasListener(onHeadersReceived)) {
return;
}
// in case any listeners already set up, remove them first
removeListeners();
chrome.webRequest.onHeadersReceived.addListener(
onHeadersReceived, {urls: ['<all_urls>']},
['blocking', 'responseHeaders']);
// blocking or redirecting
chrome.webRequest.onBeforeRequest.addListener(
onBeforeRequest, {urls: ['<all_urls>']}, ['blocking']);
// disabling cache
chrome.webRequest.onBeforeSendHeaders.addListener(
onBeforeSendHeaders, {urls: ['<all_urls>']},
['blocking', 'requestHeaders']);
}
function removeListeners() {
if (chrome.webRequest.onHeadersReceived.hasListener(onHeadersReceived)) {
chrome.webRequest.onHeadersReceived.removeListener(onHeadersReceived);
}
if (chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequest)) {
chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequest);
}
if (chrome.webRequest.onBeforeSendHeaders.hasListener(onBeforeSendHeaders)) {
chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
}
}
function enableHelper(tab: chrome.tabs.Tab) {
// disable -> enable
chrome.browserAction.setIcon({tabId: tab.id, path: 'icon-32.png'});
chrome.browserAction.setPopup({tabId: tab.id, popup: 'popup.html'});
TabStates.set(tab.id, {tab});
// set up listeners
setUpListeners();
}
function disableHelper(tab: chrome.tabs.Tab) {
chrome.browserAction.setIcon({tabId: tab.id, path: 'gray-32.png'});
chrome.browserAction.setPopup({tabId: tab.id, popup: ''});
TabStates.delete(tab.id);
// Remove listeners if no tab enabled
if (TabStates.size === 0) {
removeListeners();
}
}
chrome.browserAction.onClicked.addListener((tab) => {
if (TabStates.has(tab.id)) {
// enable -> disable
disableHelper(tab);
} else {
// disable -> enable
enableHelper(tab);
if (lastFocusedWindow) {
chrome.tabs.update(lastFocusedWindow.id!, {url: lastFocusedWindow.url});
}
}
});
// Enable / disable the helper based on state of this tab
// This will be called when tab was activated
function checkCurrentTab() {
chrome.tabs.query({'active': true, 'lastFocusedWindow': true}, ([tab]) => {
if (lastFocusedWindow === tab) return;
if (!tab || !tab.url) return;
if (TabStates.has(tab.id)) {
enableHelper(tab);
} else {
disableHelper(tab);
}
lastFocusedWindow = tab;
// read the latest states and rules
chrome.storage.sync.get(['rules'], (res) => {
rules = res['rules'] || [];
});
});
}
// check this so extension always have the right status
// even after page refreshes / reloads etc
setInterval(() => checkCurrentTab(), 1000);
// when removed, clear
chrome.tabs.onRemoved.addListener(tabId => {
TabStates.delete(tabId);
if (TabStates.size === 0) {
removeListeners();
}
});