UNPKG

freshroute-server

Version:

Local development server for FreshRoute extension with API mocking and extended functionality

1,294 lines โ€ข 53.7 kB
"use strict"; importScripts("allowed-domains.js"); let isExtensionEnabled = true; let appliedRules = /* @__PURE__ */ new Set(); let isUpdatingRules = false; let environmentVariables = []; const { ALLOWED_DOMAINS, validateTargetUrl: validateTargetUrlDomain } = typeof module !== "undefined" && module.exports ? require("./allowed-domains.js") : typeof DomainValidator !== "undefined" ? DomainValidator : { ALLOWED_DOMAINS: [], validateTargetUrl: () => ({ allowed: false }) }; chrome.runtime.onInstalled.addListener(async () => { await chrome.storage.sync.set({ extensionEnabled: true, notificationsEnabled: true }); const existing = await chrome.storage.local.get(["rules", "groups"]); if (!existing.rules && !existing.groups) { await chrome.storage.local.set({ rules: [], groups: [] }); } await loadEnvironmentVariables(); console.log("FreshRoute installed"); }); chrome.runtime.onStartup.addListener(async () => { await loadEnvironmentVariables(); console.log("FreshRoute started"); }); chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { if (message.action === "mockRuleApplied") { await sendRuleActivityToDashboard({ rule: message.rule, originalUrl: message.originalUrl, method: message.method, statusCode: message.statusCode, responseType: message.responseType, timestamp: message.timestamp }); } }); chrome.storage.onChanged.addListener(async (changes, namespace) => { if (namespace === "sync") { if (changes.extensionEnabled) { isExtensionEnabled = changes.extensionEnabled.newValue; await updateDeclarativeRules(); } if (changes.notificationsEnabled) { notifyAllTabs("updateNotificationSettings", { enabled: changes.notificationsEnabled.newValue }); } if (changes.environmentVariables) { console.log("๐Ÿ”„ Environment variables changed, reloading and updating rules..."); await loadEnvironmentVariables(); await updateDeclarativeRules(); console.log("โœ… Rules updated after environment variable change"); } } if (namespace === "local") { if (changes.rules || changes.groups) { await updateDeclarativeRules(); appliedRules.clear(); } } }); chrome.webRequest.onBeforeRedirect.addListener(async (details) => { if (details.tabId === -1) return; const [syncData, localData] = await Promise.all([ chrome.storage.sync.get(["extensionEnabled", "notificationsEnabled"]), chrome.storage.local.get(["rules", "groups"]) ]); const { extensionEnabled, notificationsEnabled } = syncData; const { rules, groups } = localData; if (!extensionEnabled || !notificationsEnabled) return; let allRules = []; if (groups && groups.length > 0) { groups.forEach((group) => { if (group.enabled !== false) { group.rules.forEach((rule) => { if (rule.enabled && rule.type === "url_rewrite") { allRules.push(rule); } }); } }); } else if (rules && rules.length > 0) { allRules = rules.filter((rule) => rule.enabled && rule.type === "url_rewrite"); } for (const rule of allRules) { try { const sourceUrlWithVariables = substituteVariables(rule.sourceUrl); let isMatch = false; const matchingType = rule.matchingType || "regex"; switch (matchingType) { case "contains": isMatch = details.url.includes(sourceUrlWithVariables); break; case "equals": isMatch = details.url === sourceUrlWithVariables; break; case "startsWith": isMatch = details.url.startsWith(sourceUrlWithVariables); break; case "endsWith": isMatch = details.url.endsWith(sourceUrlWithVariables); break; case "wildcard": const wildcardRegexPattern = convertWildcardToRegex(sourceUrlWithVariables); const wildcardRegex = new RegExp(wildcardRegexPattern); isMatch = wildcardRegex.test(details.url); break; case "regex": default: const regex = new RegExp(sourceUrlWithVariables); isMatch = regex.test(details.url); break; } if (isMatch) { const ruleKey = `${rule.type}-${rule.name}-${details.url}`; if (!appliedRules.has(ruleKey)) { appliedRules.add(ruleKey); console.log(`๐Ÿ” Added to applied rules cache: ${ruleKey}`); setTimeout(() => { appliedRules.delete(ruleKey); console.log(`๐Ÿ—‘๏ธ Removed from applied rules cache: ${ruleKey}`); }, 1e4); console.log(`โœ… URL redirect detected: ${details.url} -> ${details.redirectUrl} (Rule: ${rule.name})`); await sendNotificationToTab(details.tabId, { action: "ruleApplied", ruleName: rule.name, ruleType: rule.type, url: details.url, redirectUrl: details.redirectUrl }); await sendRuleActivityToDashboard({ rule: { ...rule, type: "url" }, originalUrl: details.url, newUrl: details.redirectUrl, responseTime: details.responseTime || 0, timestamp: Date.now() }); break; } else { console.log(`๐Ÿ”„ Rule already applied recently: ${ruleKey}`); } } } catch (e) { } } }, { urls: ["<all_urls>"] }); let isNetworkRecording = false; let activeDebuggerTabs = /* @__PURE__ */ new Set(); let pendingRequests = /* @__PURE__ */ new Map(); let currentRecordingTab = null; let recordedRequests = []; chrome.tabs.onActivated.addListener(async (activeInfo) => { if (!isNetworkRecording) return; const newTabId = activeInfo.tabId; if (currentRecordingTab === newTabId) return; console.log("๐Ÿ”„ Tab switched during recording. Moving to tab:", newTabId); try { const tab = await chrome.tabs.get(newTabId); if (tab.url.startsWith("chrome://") || tab.url.startsWith("chrome-extension://")) { console.log("โš ๏ธ Cannot record chrome:// or extension pages"); return; } if (currentRecordingTab && activeDebuggerTabs.has(currentRecordingTab)) { try { await chrome.debugger.detach({ tabId: currentRecordingTab }); activeDebuggerTabs.delete(currentRecordingTab); console.log("๐Ÿ”Œ Detached debugger from previous tab:", currentRecordingTab); } catch (error) { console.log("โš ๏ธ Error detaching from previous tab:", error.message); } } try { await chrome.debugger.attach({ tabId: newTabId }, "1.3"); console.log("๐Ÿ”ง Enabling Network domain for new tab:", newTabId); const networkEnableResult = await chrome.debugger.sendCommand({ tabId: newTabId }, "Network.enable", { maxTotalBufferSize: 1e7, maxResourceBufferSize: 5e6, maxPostDataSize: 65536 }); console.log("โœ… Network domain enabled for new tab:", newTabId, "Result:", networkEnableResult); try { await chrome.debugger.sendCommand({ tabId: newTabId }, "Runtime.enable"); await chrome.debugger.sendCommand({ tabId: newTabId }, "Runtime.evaluate", { expression: 'console.log("๐Ÿงช CDP test from extension on new tab"); "test_result"' }); console.log("๐Ÿงช CDP test successful on new tab"); } catch (testError) { console.log("โš ๏ธ CDP test failed on new tab:", testError.message); } activeDebuggerTabs.add(newTabId); currentRecordingTab = newTabId; console.log("โœ… CDP network capture moved to new tab:", newTabId, tab.url); chrome.runtime.sendMessage({ type: "recordingTabSwitched", tabId: newTabId, url: tab.url }).catch(() => { }); } catch (error) { console.log("โš ๏ธ Could not attach debugger to new tab:", newTabId, error.message); currentRecordingTab = null; } } catch (error) { console.error("โŒ Error handling tab switch during recording:", error); } }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const handleAsync = async () => { if (message.action === "updateRules") { try { await loadEnvironmentVariables(); await updateDeclarativeRules(); sendResponse({ success: true }); } catch (error) { console.error("โŒ Error updating rules:", error); sendResponse({ success: false, error: error.message }); } } else if (message.action === "getCurrentRules") { try { const rules = await chrome.declarativeNetRequest.getDynamicRules(); sendResponse({ rules }); } catch (error) { sendResponse({ success: false, error: error.message }); } } else if (message.action === "debugHeaders") { try { const result = await debugHeaderRules(); sendResponse(result); } catch (error) { sendResponse({ success: false, error: error.message }); } } else if (message.type === "startNetworkRecording") { console.log("๐ŸŽฌ Starting network recording..."); isNetworkRecording = true; recordedRequests = []; await startCDPNetworkCapture(); console.log("๐Ÿ“ก Network recording started (WebRequest API + CDP if available)"); sendResponse({ success: true, message: "Network recording started" }); } else if (message.type === "stopNetworkRecording") { console.log("๐Ÿ›‘ Stopping network recording..."); isNetworkRecording = false; await stopCDPNetworkCapture(); console.log("๐Ÿ“Š Network recording stopped. Total requests:", recordedRequests.length); sendResponse({ success: true, message: "Network recording stopped", requestCount: recordedRequests.length }); } else if (message.type === "networkResponseBody") { handleNetworkResponseBody(message.data); sendResponse({ success: true }); } else if (message.action === "updateExtensionEnabled") { try { await chrome.storage.sync.set({ extensionEnabled: message.enabled }); isExtensionEnabled = message.enabled; await updateDeclarativeRules(); sendResponse({ success: true }); } catch (error) { console.error("โŒ Error updating extension enabled state:", error); sendResponse({ success: false, error: error.message }); } } else if (message.action === "openOptionsPage") { try { chrome.runtime.openOptionsPage(); sendResponse({ success: true }); } catch (error) { console.error("โŒ Error opening options page:", error); sendResponse({ success: false, error: error.message }); } } }; handleAsync(); return true; }); async function startCDPNetworkCapture() { try { const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!activeTab) { console.log("โš ๏ธ No active tab found"); return; } if (activeTab.url.startsWith("chrome://") || activeTab.url.startsWith("chrome-extension://")) { console.log("โš ๏ธ Cannot attach debugger to chrome:// or extension pages"); chrome.debugger.onEvent.removeListener(handleCDPNetworkEvent); chrome.debugger.onDetach.removeListener(handleDebuggerDetach); chrome.debugger.onEvent.addListener(handleCDPNetworkEvent); chrome.debugger.onDetach.addListener(handleDebuggerDetach); console.log("โœ… CDP event listeners ready for debuggable tabs"); return; } try { console.log("๐Ÿ”Œ Attaching debugger to active tab:", activeTab.id, activeTab.url); await chrome.debugger.attach({ tabId: activeTab.id }, "1.3"); activeDebuggerTabs.add(activeTab.id); currentRecordingTab = activeTab.id; console.log("๐Ÿ”ง Enabling Network domain for tab:", activeTab.id); const networkEnableResult = await chrome.debugger.sendCommand({ tabId: activeTab.id }, "Network.enable", { maxTotalBufferSize: 1e7, maxResourceBufferSize: 5e6, maxPostDataSize: 65536 }); console.log("โœ… CDP network capture enabled for tab:", activeTab.id, "Result:", networkEnableResult); try { await chrome.debugger.sendCommand({ tabId: activeTab.id }, "Runtime.enable"); await chrome.debugger.sendCommand({ tabId: activeTab.id }, "Runtime.evaluate", { expression: 'console.log("๐Ÿงช CDP test from extension"); "test_result"' }); console.log("๐Ÿงช CDP test successful"); } catch (testError) { console.log("โš ๏ธ CDP test failed:", testError.message); } } catch (error) { console.log("โš ๏ธ Could not attach debugger to tab:", activeTab.id, error.message); console.log("๐Ÿ“ก Falling back to WebRequest API only (no response bodies)"); } chrome.debugger.onEvent.removeListener(handleCDPNetworkEvent); chrome.debugger.onDetach.removeListener(handleDebuggerDetach); chrome.debugger.onEvent.addListener(handleCDPNetworkEvent); chrome.debugger.onDetach.addListener(handleDebuggerDetach); console.log("โœ… CDP event listeners attached"); } catch (error) { console.error("โŒ Error starting CDP network capture:", error); } } function handleDebuggerDetach(source, reason) { console.log("๐Ÿ”Œ Debugger detached from tab:", source.tabId, "Reason:", reason); activeDebuggerTabs.delete(source.tabId); if (currentRecordingTab === source.tabId) { currentRecordingTab = null; console.log("๐Ÿ“ Current recording tab was detached"); } if (reason === "canceled_by_user") { console.log("โš ๏ธ User cancelled debugging - response bodies will not be captured"); chrome.runtime.sendMessage({ type: "debuggerDetached", reason: "User cancelled debugging notification" }).catch(() => { }); } } async function stopCDPNetworkCapture() { try { chrome.debugger.onEvent.removeListener(handleCDPNetworkEvent); chrome.debugger.onDetach.removeListener(handleDebuggerDetach); for (const tabId of activeDebuggerTabs) { try { await chrome.debugger.detach({ tabId }); console.log("๐Ÿ”Œ Detached debugger from tab:", tabId); } catch (error) { console.log("โš ๏ธ Error detaching debugger from tab:", tabId, error.message); } } activeDebuggerTabs.clear(); pendingRequests.clear(); currentRecordingTab = null; console.log("โœ… CDP network capture stopped"); } catch (error) { console.error("โŒ Error stopping CDP network capture:", error); } } async function handleCDPNetworkEvent(debuggeeId, method, params) { if (!isNetworkRecording) { return; } try { if (method === "Network.requestWillBeSent") { const { requestId, request } = params; pendingRequests.set(requestId, { url: request.url, method: request.method, requestHeaders: request.headers, requestBody: request.postData || null, timestamp: Date.now() }); console.log("๐Ÿ“ก CDP Network.requestWillBeSent for:", request.url); console.log("๐Ÿ“‹ Request headers captured:", Object.keys(request.headers || {}).length); } else if (method === "Network.responseReceived") { const { requestId, response } = params; const requestInfo = pendingRequests.get(requestId); if (requestInfo) { requestInfo.response = response; requestInfo.status = response.status; requestInfo.headers = response.headers; try { const responseBody = await chrome.debugger.sendCommand(debuggeeId, "Network.getResponseBody", { requestId }); if (responseBody && responseBody.body) { let decodedBody; try { if (responseBody.base64Encoded) { console.log("๐Ÿ” Decoding base64 response body..."); decodedBody = atob(responseBody.body); console.log("โœ… Base64 decoded successfully, length:", decodedBody.length); } else { console.log("๐Ÿ“ Using plain text response body..."); decodedBody = responseBody.body; console.log("โœ… Plain text body, length:", decodedBody.length); } } catch (error) { console.error("โŒ Error decoding response body:", error); decodedBody = responseBody.body; } const responseData = { url: requestInfo.url, method: requestInfo.method, status: response.status, headers: response.headers, responseBody: decodedBody, requestHeaders: requestInfo.requestHeaders, requestBody: requestInfo.requestBody }; console.log("๐Ÿ“ฅ CDP captured response body for:", requestInfo.url, "Length:", responseData.responseBody.length); handleNetworkResponseBody(responseData); pendingRequests.delete(requestId); } } catch (error) { } } } else if (method === "Network.loadingFinished") { const { requestId } = params; const requestInfo = pendingRequests.get(requestId); if (!requestInfo) { return; } try { const responseBody = await chrome.debugger.sendCommand(debuggeeId, "Network.getResponseBody", { requestId }); if (responseBody && responseBody.body) { let decodedBody; try { if (responseBody.base64Encoded) { console.log("๐Ÿ” [loadingFinished] Decoding base64 response body..."); decodedBody = atob(responseBody.body); console.log("โœ… [loadingFinished] Base64 decoded successfully, length:", decodedBody.length); } else { console.log("๐Ÿ“ [loadingFinished] Using plain text response body..."); decodedBody = responseBody.body; console.log("โœ… [loadingFinished] Plain text body, length:", decodedBody.length); } } catch (error) { console.error("โŒ [loadingFinished] Error decoding response body:", error); decodedBody = responseBody.body; } const responseData = { url: requestInfo.url, method: requestInfo.method, status: requestInfo.status || 200, headers: requestInfo.headers || {}, responseBody: decodedBody, requestHeaders: requestInfo.requestHeaders, requestBody: requestInfo.requestBody }; console.log("๐Ÿ“ฅ CDP captured response body for:", requestInfo.url, "Length:", responseData.responseBody.length); handleNetworkResponseBody(responseData); } } catch (error) { } finally { pendingRequests.delete(requestId); } } else if (method === "Network.loadingFailed") { const { requestId } = params; const requestInfo = pendingRequests.get(requestId); if (requestInfo) { try { const responseBody = await chrome.debugger.sendCommand(debuggeeId, "Network.getResponseBody", { requestId }); if (responseBody && responseBody.body) { let decodedBody; try { if (responseBody.base64Encoded) { console.log("๐Ÿ” [loadingFailed] Decoding base64 response body..."); decodedBody = atob(responseBody.body); console.log("โœ… [loadingFailed] Base64 decoded successfully, length:", decodedBody.length); } else { console.log("๐Ÿ“ [loadingFailed] Using plain text response body..."); decodedBody = responseBody.body; console.log("โœ… [loadingFailed] Plain text body, length:", decodedBody.length); } } catch (error) { console.error("โŒ [loadingFailed] Error decoding response body:", error); decodedBody = responseBody.body; } const responseData = { url: requestInfo.url, method: requestInfo.method, status: requestInfo.status || 0, headers: requestInfo.headers || {}, responseBody: decodedBody, requestHeaders: requestInfo.requestHeaders, requestBody: requestInfo.requestBody }; console.log("๐Ÿ“ฅ CDP captured response body for failed request:", requestInfo.url, "Length:", responseData.responseBody.length); handleNetworkResponseBody(responseData); } } catch (error) { } } pendingRequests.delete(requestId); } else if (method === "Network.requestIntercepted") { const { interceptionId, request } = params; try { await chrome.debugger.sendCommand(debuggeeId, "Network.continueInterceptedRequest", { interceptionId }); } catch (error) { } } } catch (error) { console.error("โŒ Error handling CDP network event:", error); } } function handleNetworkResponseBody(responseData) { try { let matchingRequest = recordedRequests.find( (req) => req.url === responseData.url && req.method === responseData.method && !req.responseBody // Only update if we don't have response body yet ); if (!matchingRequest) { matchingRequest = recordedRequests.find( (req) => req.url === responseData.url && !req.responseBody ); } if (matchingRequest) { matchingRequest.responseBody = responseData.responseBody; if (responseData.status) matchingRequest.status = responseData.status; if (responseData.headers) matchingRequest.responseHeaders = responseData.headers; if (responseData.requestHeaders) matchingRequest.requestHeaders = responseData.requestHeaders; if (responseData.requestBody) matchingRequest.requestBody = responseData.requestBody; console.log("โœ… Updated request with response body:", responseData.url, "Length:", responseData.responseBody.length); console.log("๐Ÿ” Response body preview:", responseData.responseBody.substring(0, 200) + (responseData.responseBody.length > 200 ? "..." : "")); console.log("๐Ÿ“ค Sending networkRequestUpdated message to UI..."); chrome.runtime.sendMessage({ type: "networkRequestUpdated", request: matchingRequest }).then(() => { console.log("๐Ÿ“ค networkRequestUpdated message sent successfully"); }).catch((error) => { console.log("โš ๏ธ networkRequestUpdated message failed:", error.message); }); } else { const newRequest = { id: Date.now().toString(), url: responseData.url, method: responseData.method, status: responseData.status, responseHeaders: responseData.headers, responseBody: responseData.responseBody, requestHeaders: responseData.requestHeaders, requestBody: responseData.requestBody, timestamp: Date.now() }; recordedRequests.push(newRequest); console.log("๐Ÿ“ Created new request with response body:", responseData.url); chrome.runtime.sendMessage({ type: "networkRequestCaptured", request: newRequest }).catch(() => { }); } } catch (error) { console.error("โŒ Error handling network response body:", error); } } async function debugHeaderRules() { try { const [syncData, localData] = await Promise.all([ chrome.storage.sync.get(["extensionEnabled"]), chrome.storage.local.get(["rules", "groups"]) ]); const { extensionEnabled } = syncData; const { rules, groups } = localData; const result = { extensionEnabled, totalGroups: groups ? groups.length : 0, totalRules: 0, headerRules: [], currentDynamicRules: [] }; let allRules = []; if (groups && groups.length > 0) { groups.forEach((group, groupIndex) => { if (group.enabled !== false) { if (group.rules) { group.rules.forEach((rule, ruleIndex) => { if (rule.enabled) { allRules.push({ ...rule, _groupIndex: groupIndex, _ruleIndex: ruleIndex }); } }); } } }); } else if (rules && rules.length > 0) { allRules = rules.filter((rule) => rule.enabled); } result.totalRules = allRules.length; const headerRules = allRules.filter((rule) => rule.type === "modify_headers"); result.headerRules = headerRules.map((rule) => { const validHeaders = []; const invalidHeaders = []; rule.headers.forEach((header) => { const validation = validateHeaderForDeclarativeNetRequest(header); if (validation.allowed) { validHeaders.push(header); } else { invalidHeaders.push({ header, reason: validation.reason }); } }); return { name: rule.name, urlPattern: rule.urlPattern, totalHeaders: rule.headers.length, validHeaders: validHeaders.length, invalidHeaders: invalidHeaders.length, invalidHeaderDetails: invalidHeaders, enabled: rule.enabled }; }); const dynamicRules = await chrome.declarativeNetRequest.getDynamicRules(); result.currentDynamicRules = dynamicRules.map((rule) => ({ id: rule.id, priority: rule.priority, action: rule.action, condition: rule.condition })); console.log("๐Ÿ” Header Rules Debug Info:", result); return { success: true, data: result }; } catch (error) { console.error("โŒ Error debugging header rules:", error); return { success: false, error: error.message }; } } function calculateRuleSpecificity(sourcePattern) { let specificity = 0; specificity += 10; if (sourcePattern.includes("https://")) specificity += 5; if (sourcePattern.includes("http://")) specificity += 3; const portMatch = sourcePattern.match(/:(\d+)/); if (portMatch) { const port = parseInt(portMatch[1]); specificity += 1e3; specificity += port * 10; if (port !== 80 && port !== 443) specificity += 500; } const domainParts = sourcePattern.split("/")[2] || ""; if (domainParts) { const cleanDomain = domainParts.split(":")[0]; const subdomains = cleanDomain.split(".").length - 2; specificity += subdomains * 5; if (!cleanDomain.includes("*") && !cleanDomain.includes("(") && !cleanDomain.includes("[")) { specificity += 15; } specificity += cleanDomain.length * 0.1; } const pathPart = sourcePattern.split("/").slice(3).join("/"); if (pathPart) { specificity += pathPart.length * 0.1; if (!pathPart.includes(".*") && !pathPart.includes("(.*)")) { specificity += 10; } const segments = pathPart.split("/").filter((s) => s.length > 0); specificity += segments.length * 3; } const escapeCount = (sourcePattern.match(/\\/g) || []).length; specificity += escapeCount * 2; const characterClassCount = (sourcePattern.match(/\[.*?\]/g) || []).length; specificity += characterClassCount * 3; return Math.floor(specificity); } function normalizeRegexPattern(sourcePattern) { let normalized = sourcePattern; if (!normalized.startsWith("^")) { normalized = "^" + normalized; } if (!normalized.endsWith("$")) { normalized = normalized + "$"; } const parts = normalized.split("://"); if (parts.length === 2) { const protocol = parts[0]; const rest = parts[1]; const urlParts = rest.split("/"); if (urlParts.length > 0) { let domainPart = urlParts[0]; domainPart = domainPart.replace(/([^\\])\./g, "$1\\."); if (domainPart.startsWith(".")) { domainPart = "\\" + domainPart; } urlParts[0] = domainPart; normalized = protocol + "://" + urlParts.join("/"); } } try { new RegExp(normalized); return normalized; } catch (e) { console.warn(`โš ๏ธ Normalized regex failed, using original: ${e.message}`); return sourcePattern; } } async function loadEnvironmentVariables() { try { const result = await chrome.storage.sync.get(["environmentVariables"]); const oldCount = environmentVariables.length; environmentVariables = result.environmentVariables || []; console.log(`๐Ÿ“‹ Loaded ${environmentVariables.length} environment variables (was ${oldCount})`); if (environmentVariables.length > 0) { console.log("๐Ÿ“ Environment variables:", environmentVariables.map((v) => `${v.name}="${v.value}"`)); } } catch (error) { console.error("โŒ Error loading environment variables:", error); environmentVariables = []; } } function substituteVariables(text) { if (!text || typeof text !== "string") return text; let result = text; let substitutionsMade = 0; environmentVariables.forEach((variable) => { const pattern = new RegExp(`\\{\\{${variable.name}\\}\\}`, "g"); const matches = text.match(pattern); if (matches) { result = result.replace(pattern, variable.value); substitutionsMade += matches.length; console.log(`๐Ÿ”„ Substituted {{${variable.name}}} with "${variable.value}" (${matches.length} times)`); } }); if (substitutionsMade > 0) { console.log(`๐Ÿ“ Variable substitution: "${text}" โ†’ "${result}"`); } return result; } async function sendRuleActivityToDashboard(ruleData) { try { const tabs = await chrome.tabs.query({ url: chrome.runtime.getURL("dashboard.html") }); for (const tab of tabs) { try { await chrome.tabs.sendMessage(tab.id, { type: "ruleApplied", rule: ruleData.rule, originalUrl: ruleData.originalUrl, newUrl: ruleData.newUrl, responseTime: ruleData.responseTime || 0, timestamp: Date.now() }); } catch (error) { console.log("Could not send to dashboard tab:", error.message); } } } catch (error) { console.error("Error sending rule activity to dashboard:", error); } } function convertMatchingTypeToRegex(sourceUrl, matchingType) { let regexPattern = sourceUrl; switch (matchingType) { case "contains": regexPattern = `.*${escapeRegexChars(sourceUrl)}.*`; break; case "equals": regexPattern = `^${escapeRegexChars(sourceUrl)}$`; break; case "startsWith": regexPattern = `^${escapeRegexChars(sourceUrl)}.*`; break; case "endsWith": regexPattern = `.*${escapeRegexChars(sourceUrl)}$`; break; case "wildcard": regexPattern = convertWildcardToRegex(sourceUrl); break; } return regexPattern; } function escapeRegexChars(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function convertTargetUrlForMatchingType(targetUrl, matchingType) { switch (matchingType) { case "startsWith": return targetUrl + "\\1"; case "contains": case "equals": case "endsWith": return targetUrl; case "wildcard": case "regex": return targetUrl.replace(/\$(\d+)/g, "\\$1"); default: return targetUrl.replace(/\$(\d+)/g, "\\$1"); } } function convertWildcardToRegex(wildcardPattern) { let regexPattern = wildcardPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); regexPattern = regexPattern.replace(/\*/g, "([^/]*)"); if (regexPattern.includes("/") && regexPattern.match(/\([^/]*\)/g)) { regexPattern = regexPattern.replace(/\/\([^/]*\)$/g, "/(.*)"); } return regexPattern; } async function getMockServerBaseUrl() { const { mockServerHttpUrl = "http://localhost:3001" } = await chrome.storage.sync.get(["mockServerHttpUrl"]); return mockServerHttpUrl; } async function createMockServerUrl(mockRule) { const { mockServerHttpUrl = "http://localhost:3001" } = await chrome.storage.sync.get(["mockServerHttpUrl"]); const mockEndpoint = `${mockServerHttpUrl}/api/mock-response`; const params = new URLSearchParams(); params.set("ruleId", mockRule.id || "unknown"); params.set("ruleName", mockRule.name || "Mock Response"); params.set("method", mockRule.method || "ANY"); params.set("urlPattern", mockRule.urlPattern || ""); params.set("matchingType", mockRule.matchingType || "regex"); params.set("statusCode", mockRule.statusCode || "200"); params.set("responseType", mockRule.responseType || "json"); let responseBody = mockRule.responseBody || "{}"; if (typeof responseBody === "object" && responseBody !== null) { try { responseBody = JSON.stringify(responseBody); } catch (e) { console.warn("โš ๏ธ Failed to stringify response body, using empty object:", e); responseBody = "{}"; } } else if (typeof responseBody !== "string") { responseBody = String(responseBody); } params.set("responseBody", responseBody); params.set("responseHeaders", JSON.stringify(mockRule.responseHeaders || {})); params.set("delay", mockRule.delay || "0"); const finalUrl = `${mockEndpoint}?${params.toString()}`; console.log(`๐Ÿ”— Generated mock server URL: ${finalUrl}`); return finalUrl; } async function sendMockRulesToServer(mockRules) { try { const mockServerUrl = await getMockServerBaseUrl(); const response = await fetch(`${mockServerUrl}/api/extension-rules`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rules: mockRules }) }); if (response.ok) { const result = await response.json(); console.log(`โœ… Sent ${result.rulesStored} mock rules to server`); } else { console.error("โŒ Failed to send mock rules to server:", response.status); } } catch (error) { console.error("โŒ Error sending mock rules to server:", error); } } async function updateDeclarativeRules() { if (isUpdatingRules) { console.log("Rule update already in progress, skipping..."); return; } isUpdatingRules = true; console.log("Starting rule update..."); try { let existingRules = await chrome.declarativeNetRequest.getDynamicRules(); console.log(`Found ${existingRules.length} existing dynamic rules`); if (existingRules.length > 0) { const ruleIds = existingRules.map((rule) => rule.id); await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: ruleIds }); await new Promise((resolve) => setTimeout(resolve, 200)); existingRules = await chrome.declarativeNetRequest.getDynamicRules(); if (existingRules.length > 0) { console.warn(`Warning: ${existingRules.length} rules still exist after clearing`); const remainingIds = existingRules.map((rule) => rule.id); await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: remainingIds }); await new Promise((resolve) => setTimeout(resolve, 200)); } } const [syncData, localData] = await Promise.all([ chrome.storage.sync.get(["extensionEnabled"]), chrome.storage.local.get(["rules", "groups"]) ]); const { extensionEnabled } = syncData; const { rules, groups } = localData; if (!extensionEnabled) { console.log("Extension disabled, no rules to add"); return; } let allRules = []; if (groups && groups.length > 0) { groups.forEach((group, groupIndex) => { if (group.enabled !== false) { if (group.rules) { group.rules.forEach((rule, ruleIndex) => { if (rule.enabled) { const processedRule = { ...rule, _groupIndex: groupIndex, _ruleIndex: ruleIndex }; if (rule.type === "url_rewrite") { processedRule.sourceUrl = substituteVariables(rule.sourceUrl); processedRule.targetUrl = substituteVariables(rule.targetUrl); } else if (rule.type === "modify_headers") { processedRule.urlPattern = substituteVariables(rule.urlPattern); processedRule.headers = rule.headers.map((header) => ({ ...header, name: substituteVariables(header.name), value: substituteVariables(header.value) })); } else if (rule.type === "mock_response") { processedRule.urlPattern = substituteVariables(rule.urlPattern); } allRules.push(processedRule); } }); } } }); } else if (rules && rules.length > 0) { allRules = rules.filter((rule) => rule.enabled).map((rule) => { const processedRule = { ...rule }; if (rule.type === "url_rewrite") { processedRule.sourceUrl = substituteVariables(rule.sourceUrl); processedRule.targetUrl = substituteVariables(rule.targetUrl); } else if (rule.type === "modify_headers") { processedRule.urlPattern = substituteVariables(rule.urlPattern); processedRule.headers = rule.headers.map((header) => ({ ...header, name: substituteVariables(header.name), value: substituteVariables(header.value) })); } else if (rule.type === "mock_response") { processedRule.urlPattern = substituteVariables(rule.urlPattern); } return processedRule; }); } if (allRules.length === 0) { console.log("No enabled rules to add"); return; } const declarativeRules = []; const usedIds = /* @__PURE__ */ new Set(); let ruleId = 1; const urlRewriteRules = allRules.filter((rule) => rule.type === "url_rewrite"); const mockResponseRules = allRules.filter((rule) => rule.type === "mock_response"); const headerRules = allRules.filter((rule) => rule.type === "modify_headers"); if (urlRewriteRules.length > 0) { for (let i = 0; i < urlRewriteRules.length; i++) { const rule = urlRewriteRules[i]; while (usedIds.has(ruleId)) { ruleId++; } const uniqueId = ruleId++; usedIds.add(uniqueId); const regexPattern = convertMatchingTypeToRegex(rule.sourceUrl, rule.matchingType || "regex"); const normalizedPattern = normalizeRegexPattern(regexPattern); try { new RegExp(normalizedPattern); } catch (e) { console.error(`โŒ Invalid source regex for rule "${rule.name}": ${normalizedPattern}`, e); continue; } const specificity = calculateRuleSpecificity(normalizedPattern); const priority = Math.min(3e3, 1e3 + specificity); if (rule.preserveOriginalHost) { const mainFrameRule = { id: uniqueId, priority: priority + 100, // Higher priority for main frame action: { type: "redirect", redirect: { regexSubstitution: convertTargetUrlForMatchingType(rule.targetUrl, rule.matchingType || "regex") } }, condition: { regexFilter: normalizedPattern, resourceTypes: ["main_frame"] } }; declarativeRules.push(mainFrameRule); while (usedIds.has(ruleId)) { ruleId++; } const nonMainFrameId = ruleId++; usedIds.add(nonMainFrameId); } else { const regexSubstitution = convertTargetUrlForMatchingType(rule.targetUrl, rule.matchingType || "regex"); const dnrRule = { id: uniqueId, priority, action: { type: "redirect", redirect: { regexSubstitution } }, condition: { regexFilter: normalizedPattern, resourceTypes: [ "main_frame", // Navigation requests "sub_frame", // iframe requests "xmlhttprequest", // AJAX requests "other" // Other resource types ] } }; declarativeRules.push(dnrRule); } } } if (mockResponseRules.length > 0) { for (let i = 0; i < mockResponseRules.length; i++) { const rule = mockResponseRules[i]; while (usedIds.has(ruleId)) { ruleId++; } const uniqueId = ruleId++; usedIds.add(uniqueId); const regexPattern = convertMatchingTypeToRegex(rule.urlPattern, rule.matchingType || "regex"); const normalizedPattern = normalizeRegexPattern(regexPattern); try { new RegExp(normalizedPattern); } catch (e) { console.error(`โŒ Invalid URL pattern for mock rule "${rule.name}": ${normalizedPattern}`, e); continue; } const specificity = calculateRuleSpecificity(normalizedPattern); const priority = Math.min(3e3, 2e3 + specificity); const mockServerUrl = await createMockServerUrl(rule); const mockServerBaseUrl = await getMockServerBaseUrl(); const redirectUrl = `${mockServerBaseUrl}/api/mock-response/${rule.id}?originalUrl=\\0`; const dnrRule = { id: uniqueId, priority, action: { type: "redirect", redirect: { regexSubstitution: redirectUrl } }, condition: { regexFilter: normalizedPattern, resourceTypes: [ "main_frame", // Navigation requests "sub_frame", // iframe requests "xmlhttprequest", // AJAX requests "other" // Other resource types ] } }; if (rule.method && rule.method !== "ANY") { dnrRule.condition.requestMethods = [rule.method.toLowerCase()]; } declarativeRules.push(dnrRule); console.log(`โœ… Added mock response rule "${rule.name}" redirecting to mock server`); console.log(`๐Ÿ” Mock rule details:`, { id: uniqueId, priority, pattern: normalizedPattern, redirectUrl, method: rule.method, matchingType: rule.matchingType }); } } if (headerRules.length > 0) { for (let i = 0; i < headerRules.length; i++) { const rule = headerRules[i]; const requestHeaders = []; const responseHeaders = []; rule.headers.forEach((header) => { const headerValidation = validateHeaderForDeclarativeNetRequest(header); if (!headerValidation.allowed) { console.warn(`โš ๏ธ Header "${header.name}" cannot be modified via declarativeNetRequest: ${headerValidation.reason}`); return; } const headerAction = { header: header.name, operation: header.operation, // 'set', 'append', 'remove' value: header.operation !== "remove" ? header.value : void 0 }; if (header.target === "request") { requestHeaders.push(headerAction); } else if (header.target === "response") { responseHeaders.push(headerAction); } }); if (requestHeaders.length > 0) { while (usedIds.has(ruleId)) { ruleId++; } const uniqueId = ruleId++; usedIds.add(uniqueId); try { const matchingType = rule.matchingType || "regex"; const regexPattern = convertMatchingTypeToRegex(rule.urlPattern, matchingType); const normalizedUrlPattern = normalizeRegexPattern(regexPattern); new RegExp(normalizedUrlPattern); const requestRule = { id: uniqueId, priority: 100, // Lower priority than URL rewrites action: { type: "modifyHeaders", requestHeaders }, condition: { regexFilter: normalizedUrlPattern, resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest", "other"] } }; declarativeRules.push(requestRule); console.log(`โœ… Added request header rule "${rule.name}" with ${requestHeaders.length} headers`); } catch (e) { console.error(`โŒ Invalid URL pattern for header rule "${rule.name}": ${rule.urlPattern}`, e); } } if (responseHeaders.length > 0) { while (usedIds.has(ruleId)) { ruleId++; } const uniqueId = ruleId++; usedIds.add(uniqueId); try { const matchingType = rule.matchingType || "regex"; const regexPattern = convertMatchingTypeToRegex(rule.urlPattern, matchingType); const normalizedUrlPattern = normalizeRegexPattern(regexPattern); new RegExp(normalizedUrlPattern); const responseRule = { id: uniqueId, priority: 100, // Lower priority than URL rewrites action: { type: "modifyHeaders", responseHeaders }, condition: { regexFilter: normalizedUrlPattern, resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest", "other"] } }; declarativeRules.push(responseRule); console.log(`โœ… Added response header rule "${rule.name}" with ${responseHeaders.length} headers`); } catch (e) { console.error(`โŒ Invalid URL pattern for header rule "${rule.name}": ${rule.urlPattern}`, e); } } if (requestHeaders.length === 0 && responseHeaders.length === 0) { console.warn(`โš ๏ธ No valid headers found for rule "${rule.name}" - all headers may be restricted`); } } } if (mockResponseRules.length > 0) { await sendMockRulesToServer(mockResponseRules); } const allIds = declarativeRules.map((rule) => rule.id); const uniqueIds = new Set(allIds); if (allIds.length !== uniqueIds.size) { console.error("Duplicate rule IDs detected in final rules array:", allIds); throw new Error("Duplicate rule IDs generated"); } if (declarativeRules.length > 0) { await chrome.declarativeNetRequest.updateDynamicRules({ addRules: declarativeRules }); console.log(`โœ… Successfully updated ${declarativeRules.length} declarative rules`); } } catch (error) { console.error("โŒ Error updating declarative rules:", error); try { const existingRules = await chrome.declarativeNetRequest.getDynamicRules(); const ruleIds = existingRules.map((rule) => rule.id); if (ruleIds.length > 0) { await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: ruleIds }); } } catch (clearError) { console.error("โŒ Failed to clear rules after error:", clearError); } } finally { isUpdatingRules = false; } } function validateHeaderForDeclarativeNetRequest(header) { const headerName = header.name.toLowerCase(); const universallyRestrictedHeaders = [ // Browser-controlled headers "host", "connection", "upgrade", "expect", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding" ]; if (universallyRestrictedHeaders.includes(headerName)) { return { allowed: false, reason: `Header "${header.name}" is browser-controlled and cannot be modified` }; } if (header.target === "request") { return { allowed: true }; } if (header.target === "response") { const corsHeaders = [ "access-control-allow-origin", "access-control-allow-methods", "access-control-allow-headers", "access-control-allow-credentials", "access-control-expose-headers", "access-control-max-age" ]; if (corsHeaders.includes(headerName)) { return { allowed: false, reason: `CORS header "${header.name}" cannot be modified via extensions for security reasons. Must be set server-side.` }; } const securityHeaders = [ "content-security-policy", "x-frame-options", "strict-transport-security", "upgrade-insecure-requests", "set-cookie", "set-cookie2" ]; if (securityHeaders.includes(headerName)) { return { allowed: false, reason: `Security header "${header.name}" cannot be modified via declarativeNetRequest` }; } const allowedResponseHeaders = [ "cache-control", "content-type", "content-disposition", "content-encoding", "expires", "last-modified", "etag", "vary", "server", "x-powered-by", "x-custom-header", "x-api-version", "x-rate-limit", "x-debug-info" ]; if (headerName.startsWith("x-") || allowedResponseHeaders.includes(headerName)) { return { allowed: true }; } console.warn(`โš ๏ธ Response header "${header.name}" may not be modifiable. Only custom (x-*) and specific headers are typically allowed.`); return { allowed: true, // Try anyway, but warn warning: `Header "${header.name}" may not work - response headers are heavily restricted` }; } return { allowed: true }; } async function notifyAllTabs(action, data) { try { const tabs = await chrome.tabs.query({}); for (const tab of tabs) { if (tab.url && (tab.url.startsWith("http") || tab.url.startsWith("https"))) { chrome.tabs.sendMessage(tab.id, { action, ...data }).catch(() => { }); } } } catch (error) { console.log("Error notifying tabs:", error); } } async function sendNotificationToTab(tabId, notificationData, maxRetries = 2) { let attempts = 0; const tryNotification = async () => { attempts++; try { const tab = await chrome.tabs.get(tabId); if (!tab || !tab.url || !tab.url.startsWith("http") && !tab.url.startsWith("https")) { console.log(`๐Ÿ“ฑ Skipping n