freshroute-server
Version:
Local development server for FreshRoute extension with API mocking and extended functionality
1,294 lines โข 53.7 kB
JavaScript
"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