/** * @fileoverview I2P Container Tab Manager * Handles container isolation, header scrubbing and tab management */ // Configuration constants const CONTAINER_CONFIG = { MESSAGES: { TITLE: chrome.i18n.getMessage("titlePreface"), WEB: chrome.i18n.getMessage("webPreface"), ROUTER: chrome.i18n.getMessage("routerPreface"), MAIL: chrome.i18n.getMessage("mailPreface"), TORRENT: chrome.i18n.getMessage("torrentPreface"), TUNNEL: chrome.i18n.getMessage("i2ptunnelPreface"), IRC: chrome.i18n.getMessage("ircPreface"), EXTENSION: chrome.i18n.getMessage("extensionPreface"), MUWIRE: chrome.i18n.getMessage("muwirePreface"), BOTE: chrome.i18n.getMessage("botePreface"), BLOG: chrome.i18n.getMessage("blogPreface"), BLOG_PRIVATE: chrome.i18n.getMessage("blogPrefacePrivate"), TOR: chrome.i18n.getMessage("torPreface"), TOR_PRIVATE: chrome.i18n.getMessage("torPrefacePrivate"), }, HEADER_CONFIG: { USER_AGENT: "MYOB/6.66 (AN/ON)", TITLE_PREFIX: "myob", }, URLS: { BASE: "http://proxy.i2p", LOCAL: { IRC: "http://127.0.0.1:7669", TOR: "http://127.0.0.1:7695", BLOG: "http://127.0.0.1:8084", }, POPUPS: { SECURITY: "security.html", LOCATION: "location.html", TORRENT: "torrent.html", }, ICONS: { I2P: "icons/i2plogo.png", INFOTOOPIE: "icons/infotoopie.png", INFOTOOPIES: "icons/infotoopies.png", INFOTOOPIEBT: "icons/infotoopiesbt.png", }, }, }; /** * Header Manager for privacy protection */ class HeaderManager { /** * Get container context for tab * @param {Object} tabInfo Tab information * @return {Promise} Container context or null for default contexts */ static async getContext(tabInfo) { try { if (!tabInfo || !tabInfo.cookieStoreId) { return null; // Return null for missing context info } // Handle default and private contexts if ( tabInfo.cookieStoreId === "firefox-default" || tabInfo.cookieStoreId === "firefox-private" ) { return null; // Return null for default/private contexts } // Only lookup context for container tabs const context = await browser.contextualIdentities.get( tabInfo.cookieStoreId ); return context; } catch (error) { console.debug("Tab is using default context"); // More descriptive message return null; // Return null on error } } /** * Process headers for request * @param {Object} requestDetails Request details * @return {Promise} Modified request */ static async processHeaders(requestDetails) { try { if ( !requestDetails || !requestDetails.tabId || requestDetails.tabId <= 0 ) { return HeaderManager.scrubHeaders(requestDetails); } const tab = await UITabManager.getTab(requestDetails.tabId); if (!tab) { return HeaderManager.scrubHeaders(requestDetails); } // Always scrub headers regardless of container context return HeaderManager.scrubHeaders(requestDetails); } catch (error) { console.error("Header processing failed:", error); // Fail safe: still scrub headers even if context lookup fails return HeaderManager.scrubHeaders(requestDetails); } } /** * Scrub privacy-sensitive headers * @param {Object} requestDetails Request details * @return {Object} Modified headers */ static scrubHeaders(requestDetails) { if (!requestDetails || !requestDetails.requestHeaders) { return { requestHeaders: [] }; } const headers = requestDetails.requestHeaders; headers.forEach((header) => { const headerName = header.name.toLowerCase(); if (headerName === "user-agent") { header.value = CONTAINER_CONFIG.HEADER_CONFIG.USER_AGENT; } if (headerName === "referer") { header.value = ""; } }); return { requestHeaders: headers }; } } /** * Tab Manager for handling browser tabs */ class UITabManager { /** * Get tab information * @param {number} tabId Tab ID * @returns {Promise} Tab info */ static async getTab(tabId) { try { if (!tabId || tabId <= 0) { return undefined; } return await browser.tabs.get(tabId); } catch (error) { console.error("Tab lookup failed:", error); return undefined; } } /** * Update tab URL * @param {Object} tab Browser tab * @param {string} url New URL */ static async updateTab(tab, url) { try { if (!tab || !tab.id || !url) { return; } await browser.tabs.update(tab.id, { url }); } catch (error) { console.error("Tab update failed:", error); } } /** * Process tab for container * @param {Object} tab Browser tab * @param {string} containerName Container name * @param {boolean} pin Whether to pin tab */ static async processContainerTab(tab, containerName, pin) { try { if (!tab || !containerName) { return; } return await ContainerManager.forceIntoContainer(tab, containerName, pin); } catch (error) { console.error("Container tab processing failed:", error); } } } /** * Container Manager for handling I2P container isolation */ /** * Container Manager for handling I2P container isolation */ class ContainerManager { /** * Force tab into container * @param {Object} tab Tab to isolate * @param {string} contextId Container context ID * @param {boolean} pin Whether to pin tab * @returns {Promise} Containerized tab */ static async forceIntoContainer(tab, contextId, pin = true) { try { if (!tab || !contextId) { throw new Error("Invalid tab or context"); } const context = await browser.contextualIdentities.query({ name: contextId, }); if (!context || !context[0]) { throw new Error(`Container not found: ${contextId}`); } // Important: Check if tab is already in ANY container if ( tab.cookieStoreId !== "firefox-default" && tab.cookieStoreId !== "firefox-private" ) { return tab; // Tab is already containerized } const newURL = URLManager.getContainerURL(contextId, tab.url); const newTab = await this.createContainerTab(context[0], newURL, pin); await this.cleanupTabs(tab, newTab, context[0], pin); return newTab; } catch (error) { console.error("Container isolation failed:", error); throw error; } } /** * Check if tab is in container * @param {Object} tab Browser tab * @returns {boolean} Whether tab is in container */ static isInContainer(tab) { return ( tab.cookieStoreId !== "firefox-default" && tab.cookieStoreId !== "firefox-private" ); } /** * Create new container tab * @private * @param {Object} context Container context * @param {string} url Tab URL * @param {boolean} pin Whether to pin tab * @returns {Promise} Created tab */ static async createContainerTab(context, url, pin) { return await browser.tabs.create({ active: true, cookieStoreId: context.cookieStoreId, url: url, pinned: pin, }); } /** * Clean up tabs after container move * @private * @param {Object} oldTab Original tab * @param {Object} newTab New container tab * @param {Object} context Container context * @param {boolean} pin Whether to pin tab */ static async cleanupTabs(oldTab, newTab, context, pin) { // Only remove the old tab if it's not in a container if (!this.isInContainer(oldTab)) { await browser.tabs.remove(oldTab.id); } if (pin) { await browser.tabs.move(newTab.id, { index: 0 }); // Only clean up other tabs if explicitly requested const tabs = await browser.tabs.query({ cookieStoreId: context.cookieStoreId, }); for (const tab of tabs) { if (tab.id !== newTab.id && !tab.pinned) { await browser.tabs.remove(tab.id); } } } await PageActionManager.setupSecurityPopup(newTab.id); } } /** * URL Manager for handling container URLs */ class URLManager { /** * Get URL for container context * @param {string} contextId Container context ID * @param {string} url Original URL * @returns {string} Container URL */ static getContainerURL(contextId, url) { if (!url.startsWith("moz-extension://")) { return url; } const routerUrl = this.getRouterBaseURL(); const urlMap = { [CONTAINER_CONFIG.MESSAGES.TITLE]: CONTAINER_CONFIG.URLS.BASE, [CONTAINER_CONFIG.MESSAGES.ROUTER]: `${routerUrl}console`, [CONTAINER_CONFIG.MESSAGES.TUNNEL]: `${routerUrl}i2ptunnel`, [CONTAINER_CONFIG.MESSAGES.MUWIRE]: `${routerUrl}MuWire`, [CONTAINER_CONFIG.MESSAGES.BOTE]: `${routerUrl}i2pbote`, [CONTAINER_CONFIG.MESSAGES.MAIL]: `${routerUrl}webmail`, [CONTAINER_CONFIG.MESSAGES.IRC]: CONTAINER_CONFIG.URLS.LOCAL.IRC, [CONTAINER_CONFIG.MESSAGES.TOR]: CONTAINER_CONFIG.URLS.LOCAL.TOR, [CONTAINER_CONFIG.MESSAGES.BLOG]: CONTAINER_CONFIG.URLS.LOCAL.BLOG, }; return urlMap[contextId] || CONTAINER_CONFIG.URLS.BASE; } /** * Get router base URL * @private * @returns {string} Router URL */ static getRouterBaseURL() { const host = control_host(); const port = control_port(); return `http://${host}:${port}/`; } /** * Fix torrent URL * @param {string} url Original URL * @returns {string} Fixed URL */ static fixTorrentURL(url) { if (!url.endsWith("xhr1.html")) { return url; } const urlParts = url.split("/"); const hostname = urlParts[2]; const protocol = url.substr(0, url.indexOf("://") + 3); return `${protocol}${hostname}/i2psnark/`; } /** * Check if URL is extension URL * @param {Object} details Request details * @returns {boolean} Whether URL is extension */ static isExtensionURL(details) { return details && details.url && details.url.startsWith("moz-extension://"); } // In scrub.js, URLManager class static getRouterHostType(url) { try { const urlObj = new URL(url); const isRouterConsole = urlObj.port === "7657" && (urlObj.hostname === "127.0.0.1" || urlObj.hostname === "localhost"); if (!isRouterConsole) { return null; } // Map router console paths to container types const routerPaths = { "/i2ptunnel/": "i2ptunnelmgr", "/i2ptunnelmgr": "i2ptunnelmgr", "/i2psnark/": "i2psnark", "/torrents": "i2psnark", "/susimail/": "webmail", "/webmail": "webmail", "/i2pbote/": "i2pbote", "/console": "routerconsole", "/home": "routerconsole", }; for (const [path, type] of Object.entries(routerPaths)) { if (urlObj.pathname.startsWith(path)) { return type; } } // Default to router console for root path if (urlObj.pathname === "/" || urlObj.pathname === "") { return "routerconsole"; } } catch (error) { console.error("Router host type check failed:", error); } return null; } } /** * Page Action Manager for browser toolbar icons */ class PageActionManager { /** * Set up page action for tab * @param {Object} tab Browser tab */ static async setupPageAction(tab) { try { if (!tab || !tab.id || !tab.url) { return; } const isHttps = tab.url.startsWith("https://"); const isI2p = tab.url.includes(".i2p"); if (isHttps && isI2p) { await this.setI2PSecurePageAction(tab); } else if (isHttps) { await this.checkI2PLocation(tab); } else if (isI2p) { await this.setI2PPageAction(tab); } await this.checkTorrentLocation(tab); } catch (error) { console.error("Page action setup failed:", error); } } /** * Set up security popup * @param {number} tabId Tab ID */ static async setupSecurityPopup(tabId) { try { await Promise.all([ browser.pageAction.setPopup({ tabId: tabId, popup: CONTAINER_CONFIG.URLS.POPUPS.SECURITY, }), browser.pageAction.show(tabId), ]); } catch (error) { console.error("Security popup setup failed:", error); } } /** * Set I2P secure page action * @private * @param {Object} tab Browser tab */ static async setI2PSecurePageAction(tab) { await Promise.all([ browser.pageAction.setPopup({ tabId: tab.id, popup: CONTAINER_CONFIG.URLS.POPUPS.SECURITY, }), browser.pageAction.setIcon({ path: CONTAINER_CONFIG.URLS.ICONS.INFOTOOPIES, tabId: tab.id, }), ]); } /** * Set I2P page action * @private * @param {Object} tab Browser tab */ static async setI2PPageAction(tab) { await Promise.all([ browser.pageAction.setPopup({ tabId: tab.id, popup: CONTAINER_CONFIG.URLS.POPUPS.SECURITY, }), browser.pageAction.setIcon({ path: CONTAINER_CONFIG.URLS.ICONS.INFOTOOPIE, tabId: tab.id, }), ]); } /** * Check for I2P location * @private * @param {Object} tab Browser tab */ static async checkI2PLocation(tab) { try { const response = await browser.tabs.sendMessage(tab.id, { req: "i2p-location", }); if ( response && response.content && response.content.toUpperCase() !== "NO-ALT-LOCATION" ) { await this.setLocationPageAction(tab, response.content); } } catch (error) { console.debug("No I2P location found"); } } /** * Check for torrent location * @private * @param {Object} tab Browser tab */ static async checkTorrentLocation(tab) { try { const response = await browser.tabs.sendMessage(tab.id, { req: "i2p-torrentlocation", }); if ( response && response.content && response.content.toUpperCase() !== "NO-ALT-LOCATION" ) { await this.setTorrentPageAction(tab, response.content); } } catch (error) { console.debug("No torrent location found"); } } /** * Set location page action * @private * @param {Object} tab Browser tab * @param {string} location I2P location */ static async setLocationPageAction(tab, location) { await Promise.all([ browser.pageAction.setPopup({ tabId: tab.id, popup: CONTAINER_CONFIG.URLS.POPUPS.LOCATION, }), browser.pageAction.setIcon({ path: CONTAINER_CONFIG.URLS.ICONS.I2P, tabId: tab.id, }), browser.pageAction.setTitle({ tabId: tab.id, title: location, }), browser.pageAction.show(tab.id), ]); } /** * Set torrent page action * @private * @param {Object} tab Browser tab * @param {string} location Torrent location */ static async setTorrentPageAction(tab, location) { await Promise.all([ browser.pageAction.setPopup({ tabId: tab.id, popup: CONTAINER_CONFIG.URLS.POPUPS.TORRENT, }), browser.pageAction.setIcon({ path: CONTAINER_CONFIG.URLS.ICONS.INFOTOOPIEBT, tabId: tab.id, }), browser.pageAction.setTitle({ tabId: tab.id, title: location, }), browser.pageAction.show(tab.id), ]); } } /** * Event Manager for browser events */ class EventManager { /** * Initialize event listeners */ static initializeListeners() { // Web request events with proper binding browser.webRequest.onBeforeRequest.addListener( (requestDetails) => RequestManager.handleRequest(requestDetails), // Use arrow function to preserve context { urls: [ "*://*.i2p/*", "*://localhost/*", "*://127.0.0.1/*", "*://*/*i2p*", ], } ); browser.webRequest.onBeforeSendHeaders.addListener( (requestDetails) => HeaderManager.processHeaders(requestDetails), // Use arrow function to preserve context { urls: ["*://*.i2p/*"] }, ["requestHeaders", "blocking"] ); // Tab events with proper binding const tabEvents = [ "onActivated", "onAttached", "onCreated", "onDetached", "onHighlighted", "onMoved", "onReplaced", ]; tabEvents.forEach((event) => { browser.tabs[event].addListener( async (info) => await EventManager.handleTabEvent(info) // Use arrow function to preserve context ); }); } /** * Handle tab event * @private * @param {Object} tab Browser tab */ static async handleTabEvent(tab) { try { if (typeof tab === "number") { const tabInfo = await browser.tabs.get(tab); await PageActionManager.setupPageAction(tabInfo); } else if (tab && typeof tab.tabId === "number") { const tabInfo = await browser.tabs.get(tab.tabId); await PageActionManager.setupPageAction(tabInfo); } else if (tab && Array.isArray(tab.tabIds)) { for (const tabId of tab.tabIds) { const tabInfo = await browser.tabs.get(tabId); await PageActionManager.setupPageAction(tabInfo); } } } catch (error) { console.error("Tab event handling failed:", error); } } /** * Handle headers * @private * @param {Object} headers Header details * @returns {Promise} Modified headers */ static handleHeaders(headers) { return new Promise((resolve) => { window.setTimeout(() => { if (headers.tabId !== undefined) { browser.pageAction .getPopup({ tabId: headers.tabId }) .then(PageActionManager.setupPageAction); } resolve({ responseHeaders: headers.responseHeaders }); }, 2000); }); } } /** * Request Manager for handling browser requests */ class RequestManager { /** * Handle web request * @param {Object} requestDetails Request details * @returns {Promise} Modified request */ static async handleRequest(requestDetails) { try { if (!requestDetails) { return requestDetails; } // Handle proxy requests if (RequestManager.isProxyRequest(requestDetails)) { await RequestManager.handleProxyRequest(requestDetails); return requestDetails; } // Handle tab requests if (requestDetails.tabId > 0) { return await RequestManager.handleTabRequest(requestDetails); } return requestDetails; } catch (error) { console.error("Request handling failed:", error); return requestDetails; } } /** * Handle proxy request * @private * @param {Object} requestDetails Request details */ static async handleProxyRequest(requestDetails) { try { await browser.cookies.set({ firstPartyDomain: this.getI2PHostname(requestDetails.url), url: requestDetails.url, secure: true, }); } catch (error) { console.error("Proxy request handling failed:", error); } } /** * Handle tab request * @private * @param {Object} requestDetails Request details * @returns {Promise} Modified request */ static async handleTabRequest(requestDetails) { try { const tab = await UITabManager.getTab(requestDetails.tabId); if (!tab) { return requestDetails; } const url = requestDetails.url; const routerHost = URLManager.getRouterHostType(url); const localHost = this.getLocalHostType(url); // Handle router console requests (127.0.0.1:7657) if (routerHost) { return await this.handleRouterRequest(tab, routerHost, requestDetails); } // Handle other local services if (localHost) { return await this.handleLocalRequest(tab, localHost, requestDetails); } // Handle I2P host requests if (this.isI2PHost(requestDetails)) { return await this.handleI2PRequest(tab, requestDetails); } return requestDetails; } catch (error) { console.error("Tab request handling failed:", error); return requestDetails; } } /** * Handle router request * @private * @param {Object} tab Browser tab * @param {string} routerHost Router host type * @param {Object} requestDetails Request details * @returns {Promise} Modified request */ static async handleRouterRequest(tab, routerHost, requestDetails) { const routerMap = { i2ptunnelmgr: { handler: this.handleTunnelRequest, container: CONTAINER_CONFIG.MESSAGES.TUNNEL, }, i2psnark: { handler: this.handleTorrentRequest, container: CONTAINER_CONFIG.MESSAGES.TORRENT, }, webmail: { handler: this.handleMailRequest, container: CONTAINER_CONFIG.MESSAGES.MAIL, }, i2pbote: { handler: this.handleBoteRequest, container: CONTAINER_CONFIG.MESSAGES.BOTE, }, routerconsole: { handler: this.handleConsoleRequest, container: CONTAINER_CONFIG.MESSAGES.ROUTER, }, }; const service = routerMap[routerHost]; if (!service) { return requestDetails; } return await this.processServiceRequest( tab, requestDetails, service.container, true ); } /** * Handle local request * @private * @param {Object} tab Browser tab * @param {string} localHost Local host type * @param {Object} requestDetails Request details * @returns {Promise} Modified request */ static async handleLocalRequest(tab, localHost, requestDetails) { const localMap = { blog: this.handleBlogRequest, irc: this.handleIRCRequest, tor: this.handleTorRequest, }; const handler = localMap[localHost]; if (!handler) { return requestDetails; } return await handler.call(this, tab, requestDetails); } /** * Handle I2P request * @private * @param {Object} tab Browser tab * @param {Object} requestDetails Request details * @returns {Promise} Modified request */ static async handleI2PRequest(tab, requestDetails) { try { await this.handleProxyRequest(requestDetails); // Only containerize if not already in a container if (!ContainerManager.isInContainer(tab)) { const containerTab = await UITabManager.processContainerTab( tab, CONTAINER_CONFIG.MESSAGES.TITLE, false ); if (containerTab) { await UITabManager.updateTab(containerTab, requestDetails.url); } } return requestDetails; } catch (error) { console.error("I2P request handling failed:", error); return requestDetails; } } /** * Service-specific request handlers */ static async handleTunnelRequest(tab, requestDetails) { return await this.processServiceRequest( tab, requestDetails, CONTAINER_CONFIG.MESSAGES.TUNNEL, true ); } static async handleTorrentRequest(tab, requestDetails) { const url = URLManager.fixTorrentURL(requestDetails.url); return await this.processServiceRequest( tab, { ...requestDetails, url }, CONTAINER_CONFIG.MESSAGES.TORRENT, true ); } static async handleMailRequest(tab, requestDetails) { return await this.processServiceRequest( tab, requestDetails, CONTAINER_CONFIG.MESSAGES.MAIL, true ); } static async handleBoteRequest(tab, requestDetails) { return await this.processServiceRequest( tab, requestDetails, CONTAINER_CONFIG.MESSAGES.BOTE, true ); } static async handleConsoleRequest(tab, requestDetails) { return await this.processServiceRequest( tab, requestDetails, CONTAINER_CONFIG.MESSAGES.ROUTER, true ); } static async handleBlogRequest(tab, requestDetails) { return await this.processServiceRequest( tab, requestDetails, CONTAINER_CONFIG.MESSAGES.BLOG, true ); } static async handleIRCRequest(tab, requestDetails) { return await this.processServiceRequest( tab, requestDetails, CONTAINER_CONFIG.MESSAGES.IRC, true ); } static async handleTorRequest(tab, requestDetails) { return await this.processServiceRequest( tab, requestDetails, CONTAINER_CONFIG.MESSAGES.TOR, true ); } /** * Process service request * @private * @param {Object} tab Browser tab * @param {Object} requestDetails Request details * @param {string} containerName Container name * @param {boolean} pin Whether to pin tab * @returns {Promise} Modified request */ /** * Process service request * @private * @param {Object} tab Browser tab * @param {Object} requestDetails Request details * @param {string} containerName Container name * @param {boolean} pin Whether to pin tab * @returns {Promise} Modified request */ static async processServiceRequest(tab, requestDetails, containerName, pin) { try { // Only containerize if not already in a container if (!ContainerManager.isInContainer(tab)) { const containerTab = await UITabManager.processContainerTab( tab, containerName, pin ); if (containerTab) { await UITabManager.updateTab(containerTab, requestDetails.url); } } return requestDetails; } catch (error) { console.error("Service request processing failed:", error); return requestDetails; } } /** * Utility methods */ static isProxyRequest(details) { if (details.url) { let url = new URL(details.url); return url.hostname === "proxy.i2p"; } return false; } static isI2PHost(details) { if (details.url) { let url = new URL(details.url); return url.hostname.endsWith(".i2p"); } return false; } static getI2PHostname(url) { try { return new URL(url).hostname; } catch (error) { return ""; } } /** * Get local host type * @private * @param {string} url URL to check * @returns {string|null} Local host type */ static getLocalHostType(url) { try { const urlObj = new URL(url); const isLocalhost = urlObj.hostname === "127.0.0.1" || urlObj.hostname === "localhost"; if (!isLocalhost) { return null; } // Map local service ports to container types const localServices = { 7669: "irc", // I2P IRC 7695: "tor", // Tor proxy 8084: "blog", // Local blog }; return localServices[urlObj.port] || null; } catch (error) { console.error("Local host type check failed:", error); return null; } } } // Initialize event listeners EventManager.initializeListeners(); // Export for testing if (typeof module !== "undefined" && module.exports) { module.exports = { HeaderManager, UITabManager, ContainerManager, URLManager, PageActionManager, EventManager, RequestManager, CONTAINER_CONFIG, }; }