/***** Service Worker JS for Chrome Extension: Tracking Proxy Helper *****/ /*** Initialisation ***/ let tpdm = { activeTabId: null, activeWindowId: null, extInit: false, tpl: { tpId:'', eventCount:0, events:[] } }; //tpdm.data = tpdm.data || JSON.parse(JSON.stringify(tpdm.tpl)); tpdm.data = tpdm.data || {}; //console.log('Worker loaded'); /*** Helper Function Library ***/ /** * Function to Update the Badge Text */ function updateBadge(text, tabId) { chrome.action.setBadgeBackgroundColor({ color: '#fe6845', tabId: tabId }); chrome.action.setBadgeTextColor({ color: '#ffffff', tabId: tabId }); chrome.action.setBadgeText({ text: text.toString(), tabId: tabId }); } /** * Function to Clear the Badge */ function clearBadge(tabId) { chrome.action.setBadgeText({ text: '', tabId: tabId }); } /** * Function to Reset the Badge Text */ function resetBadge(tabId) { if (typeof tpdm.data[tabId]!='object') tpdm.data[tabId] = JSON.parse(JSON.stringify(tpdm.tpl)); tpdm.data[tabId] = JSON.parse(JSON.stringify(tpdm.tpl)); clearBadge(tabId); } /** * Function to Increment Badge Counter */ function incrementBadge(tabId) { if (typeof tpdm.data[tabId]!='object') tpdm.data[tabId] = JSON.parse(JSON.stringify(tpdm.tpl)); tpdm.data[tabId].eventCount += 1; updateBadge(tpdm.data[tabId].eventCount, tabId); } /** * Function to get the current active tab ID */ function getCurrentTabId(callback) { chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { if (tabs.length > 0) { callback(tabs[0].id); } else { //console.error('No active tab found'); } }); } /** * Load options from storage and set declarativeNetRequest rules */ function loadOptionsAndSetRules() { console.log('Tracking Proxy Helper installed'); chrome.storage.sync.get([ 'headerValue', 'urls', 'blockGtm', 'gtmContainerId', 'gtmContainerIdIsRegex', 'trackingProxyUrl', 'trackingProxyUrlIsRegex', 'blockTrackingProxy', 'scriptUrl', 'scriptUrlIsRegex' ], function(data) { const headerValue = data.headerValue || ''; const urls = data.urls || []; const blockGtm = data.blockGtm || false; const gtmContainerId = data.gtmContainerId || ''; const gtmContainerIdIsRegex = data.gtmContainerIdIsRegex || false; const trackingProxyUrl = data.trackingProxyUrl || ''; const trackingProxyUrlIsRegex = data.trackingProxyUrlIsRegex || false; const blockTrackingProxy = data.blockTrackingProxy || false; const scriptUrl = data.scriptUrl || ''; const scriptUrlIsRegex = data.scriptUrlIsRegex || false; // If no header value or no URLs are defined, do not set any rules if (!headerValue || urls.length === 0) { //console.log('No header value or URLs defined, no rules set'); return; } // Create conditions based on the URLs and regex const conditions = urls .filter(urlObj => urlObj.url) // Filter out empty URLs .map((urlObj, index) => { const condition = { id: index + 1, priority: 1, action: { type: "modifyHeaders", requestHeaders: [ { header: "Tp-Dev", operation: "set", value: headerValue } ] }, condition: { regexFilter: urlObj.isRegex ? urlObj.url : `^${urlObj.url}$`, isUrlFilterCaseSensitive: false, //resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest"] resourceTypes: ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", "other"] } }; console.log('Condition created:', condition); return condition; }); const rules = conditions.map((condition, index) => ({ ...condition, id: index + 1 })); const scriptUrlMatches = (url) => { if (!scriptUrl) return false; if (scriptUrlIsRegex) { const regex = new RegExp(scriptUrl); return regex.test(url); } return url.includes(scriptUrl); }; // Add GTM blocking rule if blockGtm is enabled and scriptUrl matches if (blockGtm && gtmContainerId && scriptUrlMatches(scriptUrl)) { const gtmUrl = gtmContainerIdIsRegex ? gtmContainerId : `https://www.googletagmanager.com/gtm.js?id=${gtmContainerId}`; rules.push({ id: rules.length + 1, priority: 1, action: { type: "block" }, condition: { regexFilter: gtmContainerIdIsRegex ? gtmUrl : `https://www.googletagmanager.com/gtm.js\\?id=${gtmContainerId}`, resourceTypes: ["script"] } }); console.log('Google Tag Manager blocked'); } // Add Tracking Proxy blocking rule if blockTrackingProxy is enabled and scriptUrl matches if (blockTrackingProxy && trackingProxyUrl && scriptUrlMatches(scriptUrl)) { const proxyUrl = trackingProxyUrlIsRegex ? trackingProxyUrl : `^${trackingProxyUrl}$`; rules.push({ id: rules.length + 2, priority: 1, action: { type: "block" }, condition: { regexFilter: proxyUrl, resourceTypes: ["script", "xmlhttprequest"] } }); console.log('Tracking Proxy blocked'); } if (rules.length === 0) { //console.log('No valid URLs to match, no rules set'); return; } // Update the rules chrome.declarativeNetRequest.updateDynamicRules({ addRules: rules, removeRuleIds: rules.map(rule => rule.id) }, () => { if (chrome.runtime.lastError) { console.error('Error setting declarativeNetRequest rules:', chrome.runtime.lastError); } else { console.log('DeclarativeNetRequest rules set up successfully'); } }); }); } /*** Chrome Extension Function Library ***/ // Function to Initialise the Badge and add Request Header chrome.runtime.onInstalled.addListener(() => { // Clear badge on installation clearBadge(); // Load options and set dynamic rules loadOptionsAndSetRules(); }); chrome.storage.onChanged.addListener((changes, namespace) => { if (namespace === 'sync') { console.log('Options changed, updating rules'); loadOptionsAndSetRules(); } }); // Listener for Extension Icon Clicks chrome.action.onClicked.addListener((tab) => { //console.log('Action button clicked', tab); let tabId = tab.id; let windowId = tab.windowId; chrome.tabs.create({ url: 'popup.html' }); }); // Listener for internal messages chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { //console.log('TP Received message:', message); if (message.type === 'CHECK_TPT') { getCurrentTabId((tabId) => { tpdm.activeTabId = tabId; //console.log('CHECK_TPT (Tab: '+tabId+'):', message); if (message.hasTPT) { tpdm.data.eventCount = 0; resetBadge(tabId); if (typeof tpdm.data[tabId]!='object') tpdm.data[tabId] = JSON.parse(JSON.stringify(tpdm.tpl)); tpdm.data[tabId].tpId = message.tpobj.cid; } else { clearBadge(tabId); } }); } else if (message.type === 'TRACKING_PROXY_EVENT' && message.details) { const eventData = message.details.eventData; const metaData = message.details.metaData; //console.log('TRACKING_PROXY_EVENT',message); incrementBadge(metaData.tabId); if (typeof tpdm.data[metaData.tabId]!='object') tpdm.data[metaData.tabId] = JSON.parse(JSON.stringify(tpdm.tpl)); tpdm.data[metaData.tabId].events.push(JSON.parse(JSON.stringify(eventData))); } else if (message.type === 'REQUEST_POPUP_DATA') { //getCurrentTabId((tabId) => { if (typeof tpdm.data[tpdm.activeTabId]!='object') tpdm.data[tpdm.activeTabId] = JSON.parse(JSON.stringify(tpdm.tpl)); let obj = { tpId: tpdm.data[tpdm.activeTabId].tpId, tabId: tpdm.activeTabId, events: tpdm.data[tpdm.activeTabId].events }; //console.log('TP Received message:'+tpdm.data[tpdm.activeTabId].tpId, obj); sendResponse(obj); //}); } else { //console.log('Unknown message type:', message.type); } }); // Listeners for Tab-Change chrome.tabs.onActivated.addListener(function(tab) { //console.log('TAB ACTIVATED', tab); tpdm.activeTabId = tab.tabId; tpdm.activeWindowId = tab.windowId; }); // Listeners for Tab-Update chrome.tabs.onUpdated.addListener(function(activeInfo, changeInfo, tab) { let tabId = tab.id; let windowId = tab.windowId; if (typeof changeInfo == 'object' && typeof changeInfo.status == 'string') { if (changeInfo.status == 'complete') { //console.log('TAB Load complete: ' + tab.url, tab); } } }); chrome.runtime.onStartup.addListener(function() { // Initialisiert alle Tabs beim Start der Erweiterung chrome.tabs.query({}, function(tabs) { tabs.forEach(tab => { resetBadge(tab.id); }); }); }); chrome.webNavigation.onBeforeNavigate.addListener(function(details) { if (details.frameId === 0) { // Haupt-Frame der Navigation //console.log('Navigation started for Tab:', details.tabId); resetBadge(details.tabId); } }); // Intercepting network requests to detect Tracking Proxy Events chrome.webRequest.onBeforeRequest.addListener( function(details) { if (details.method === "POST" && details.type === "xmlhttprequest" && details.tabId >= 0) { let eventData = {}; if (details.requestBody) { if (details.requestBody.formData) { eventData = JSON.parse(JSON.stringify(details.requestBody.formData)); //console.log('TRACKING_PROXY_REQUEST 1', eventData); } else if (details.requestBody.raw && details.requestBody.raw.length > 0 && details.requestBody.raw[0].bytes) { //console.warn('TRACKING_PROXY_REQUEST 0', details); try { const decodedString = decodeURIComponent(String.fromCharCode.apply(null, new Uint8Array(details.requestBody.raw[0].bytes))); if (decodedString.charAt(0) === '{') { eventData = JSON.parse(decodedString); } } catch (e) { //console.error('Failed to parse request body as JSON:', e); return; } } else { //console.log('Payload is no JSON object:', details); return; } } if (!eventData || !eventData.cid) { //console.log('Payload has no cid:', details); return; } let metaData = { documentId:details.documentId, frameId:details.frameId, initiator:details.initiator, requestId:details.requestId, tabId:details.tabId, timeStamp:details.timeStamp, url:details.url }; //console.log('TRACKING_PROXY_REQUEST available', eventData); chrome.tabs.query({ /*active: true, currentWindow: true*/ }, function(tabs) { if (tabs.length > 0) { //console.log('TRACKING_PROXY_REQUEST available', { eventData:eventData, metaData:metaData }); chrome.tabs.sendMessage(details.tabId, { type: 'TRACKING_PROXY_REQUEST', details:{eventData:eventData, metaData:metaData} }, (response) => { if (chrome.runtime.lastError) { //console.error('Error sending message to content script: '+chrome.runtime.lastError, { eventData:eventData, metaData:metaData }); } //else { console.log('TRACKING_PROXY_REQUEST sent', { eventData:eventData, metaData:metaData }); } }); } }); } }, { urls: [""] }, ["requestBody"] // "blocking","requestBody","responseHeaders" | see: https://stackoverflow.com/questions/33106709/chrome-webrequest-doesnt-see-post-data-in-requestbody ); // EOF