Files
containerSite/background.js
sal 448cd3e70d Keep auth redirects in originating container for session isolation
When a tab in a managed container navigates to an auth provider
(accounts.google.com), keep it in the current container instead of
reassigning. This ensures YouTube's Google login cookies stay in the
youtube.com container and aren't visible to drive.google.com.
2026-03-01 16:27:23 -06:00

513 lines
17 KiB
JavaScript

// ContainSite — Background Script
// Every site gets its own container. Auth redirects stay in the originating container.
const registeredScripts = {}; // cookieStoreId -> RegisteredContentScript
const containerProfiles = {}; // cookieStoreId -> { userAgent, languages } for HTTP header spoofing
let injectSourceCache = null;
let domainMap = {}; // baseDomain -> cookieStoreId
let pendingTabs = {}; // tabId -> true (tabs being redirected)
let cachedWhitelist = []; // domains that bypass containerization
const CONTAINER_COLORS = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple"];
const CONTAINER_ICONS = ["fingerprint", "fence", "briefcase", "cart", "circle", "gift", "tree", "chill"];
// All container IDs we've created — used for ownership checks on cross-domain navigation
const managedContainerIds = new Set();
// --- Domain Extraction ---
function extractDomain(url) {
try {
const u = new URL(url);
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
// Skip localhost and local IPs
const h = u.hostname;
if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h.endsWith(".local")) return null;
return h;
} catch(e) {
return null;
}
}
function getBaseDomain(hostname) {
const parts = hostname.split(".");
if (parts.length <= 2) return hostname;
const twoPartTLDs = ["co.uk", "co.jp", "co.kr", "com.au", "com.br", "co.nz", "co.in", "org.uk", "net.au"];
const lastTwo = parts.slice(-2).join(".");
if (twoPartTLDs.includes(lastTwo) && parts.length > 2) {
return parts.slice(-3).join(".");
}
return parts.slice(-2).join(".");
}
// --- Seed Management ---
function generateSeed() {
const arr = new Uint32Array(1);
crypto.getRandomValues(arr);
return arr[0];
}
// --- Inject Source Loading ---
async function getInjectSource() {
if (!injectSourceCache) {
const resp = await fetch(browser.runtime.getURL("inject.js"));
injectSourceCache = await resp.text();
}
return injectSourceCache;
}
// --- Per-Container Script Registration ---
async function registerForContainer(cookieStoreId, profile) {
if (registeredScripts[cookieStoreId]) {
try { await registeredScripts[cookieStoreId].unregister(); } catch(e) {}
delete registeredScripts[cookieStoreId];
}
const injectSource = await getInjectSource();
// Set config then run inject.js — both execute in ISOLATED world
// inject.js uses exportFunction/wrappedJSObject to modify page context (bypasses CSP)
const configCode = `window.__csConfig = ${JSON.stringify(profile)};`;
registeredScripts[cookieStoreId] = await browser.contentScripts.register({
matches: ["<all_urls>"],
js: [{ code: configCode }, { code: injectSource }],
runAt: "document_start",
allFrames: true,
cookieStoreId: cookieStoreId
});
}
async function buildProfileAndRegister(cookieStoreId, seed) {
const profile = generateFingerprintProfile(seed);
const vsStored = await browser.storage.local.get("vectorSettings");
profile.vectors = vsStored.vectorSettings || {};
// Cache profile for HTTP header spoofing
containerProfiles[cookieStoreId] = {
userAgent: profile.nav.userAgent,
languages: profile.nav.languages,
platform: profile.nav.platform
};
await registerForContainer(cookieStoreId, profile);
}
async function registerAllKnownContainers() {
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
const seeds = stored.containerSeeds || {};
const settings = stored.containerSettings || {};
for (const [cid, script] of Object.entries(registeredScripts)) {
try { await script.unregister(); } catch(e) {}
}
for (const key of Object.keys(registeredScripts)) {
delete registeredScripts[key];
}
for (const [cookieStoreId, seed] of Object.entries(seeds)) {
const cfg = settings[cookieStoreId] || { enabled: true };
if (!cfg.enabled) continue;
await buildProfileAndRegister(cookieStoreId, seed);
}
}
// --- Storage ---
async function loadDomainMap() {
const stored = await browser.storage.local.get("domainMap");
domainMap = stored.domainMap || {};
}
async function saveDomainMap() {
await browser.storage.local.set({ domainMap });
}
// --- Auto-Containment ---
async function getOrCreateContainerForDomain(baseDomain) {
if (domainMap[baseDomain]) {
return domainMap[baseDomain];
}
const colorIndex = Object.keys(domainMap).length % CONTAINER_COLORS.length;
const iconIndex = Object.keys(domainMap).length % CONTAINER_ICONS.length;
const container = await browser.contextualIdentities.create({
name: baseDomain,
color: CONTAINER_COLORS[colorIndex],
icon: CONTAINER_ICONS[iconIndex]
});
const cookieStoreId = container.cookieStoreId;
domainMap[baseDomain] = cookieStoreId;
managedContainerIds.add(cookieStoreId);
await saveDomainMap();
const stored = await browser.storage.local.get("containerSeeds");
const seeds = stored.containerSeeds || {};
seeds[cookieStoreId] = generateSeed();
await browser.storage.local.set({ containerSeeds: seeds });
await buildProfileAndRegister(cookieStoreId, seeds[cookieStoreId]);
return cookieStoreId;
}
// tabId -> baseDomain — tabs we just created, skip only for the same domain
const createdByUs = {};
// Handle a tab that needs to be in a container for a given domain
async function assignTabToContainer(tabId, url, baseDomain) {
// Skip tabs we just created — but only for the domain we created them for
if (createdByUs[tabId] === baseDomain) return;
// Skip whitelisted domains
if (cachedWhitelist.includes(baseDomain)) return;
if (pendingTabs[tabId]) return;
pendingTabs[tabId] = true;
try {
const tab = await browser.tabs.get(tabId);
const cookieStoreId = await getOrCreateContainerForDomain(baseDomain);
if (tab.cookieStoreId === cookieStoreId) {
// Already in the correct container
delete pendingTabs[tabId];
return;
}
if (tab.cookieStoreId !== "firefox-default") {
// Tab is in a non-default container — only reassign if it's one we manage
if (!managedContainerIds.has(tab.cookieStoreId)) {
// Not a ContainSite-managed container — leave it alone
delete pendingTabs[tabId];
return;
}
// Tab is in our container navigating to a different domain.
// If target is an auth provider, keep in current container so auth
// cookies stay isolated (e.g. YouTube login via accounts.google.com
// stays in the youtube.com container, not the google.com container)
const hostname = extractDomain(url);
if (hostname && AUTH_BYPASS_DOMAINS.includes(hostname)) {
delete pendingTabs[tabId];
return;
}
// Otherwise reassign to correct container
}
const newTab = await browser.tabs.create({
url: url,
cookieStoreId: cookieStoreId,
index: tab.index + 1,
active: tab.active
});
// Mark the new tab — only skip reassignment for this same domain
createdByUs[newTab.id] = baseDomain;
setTimeout(() => { delete createdByUs[newTab.id]; }, 5000);
await browser.tabs.remove(tabId);
} catch(e) {}
delete pendingTabs[tabId];
}
// Intercept new navigations
browser.webRequest.onBeforeRequest.addListener(
function(details) {
if (details.type !== "main_frame") return {};
if (details.tabId === -1) return {};
const domain = extractDomain(details.url);
if (!domain) return {};
const baseDomain = getBaseDomain(domain);
// Trigger async container assignment
assignTabToContainer(details.tabId, details.url, baseDomain);
return {};
},
{ urls: ["<all_urls>"] },
["blocking"]
);
// Handle in-tab navigations (address bar, link clicks)
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (!changeInfo.url) return;
if (pendingTabs[tabId]) return;
const domain = extractDomain(changeInfo.url);
if (!domain) return;
const baseDomain = getBaseDomain(domain);
await assignTabToContainer(tabId, changeInfo.url, baseDomain);
});
// Clean up tab tracking when tabs close
browser.tabs.onRemoved.addListener((tabId) => {
delete pendingTabs[tabId];
});
// --- Message Handling (from popup and options page) ---
browser.runtime.onMessage.addListener((message, sender) => {
if (message.type === "getContainerList") return handleGetContainerList();
if (message.type === "toggleContainer") return handleToggle(message.cookieStoreId, message.enabled);
if (message.type === "regenerateFingerprint") return handleRegenerate(message.cookieStoreId);
if (message.type === "regenerateAll") return handleRegenerateAll();
if (message.type === "resetAll") return handleResetAll();
if (message.type === "pruneContainers") return handlePruneContainers();
if (message.type === "deleteContainer") return handleDeleteContainer(message.cookieStoreId);
if (message.type === "getWhitelist") return handleGetWhitelist();
if (message.type === "setWhitelist") return handleSetWhitelist(message.whitelist);
if (message.type === "getVectorSettings") return handleGetVectorSettings();
if (message.type === "setVectorSettings") return handleSetVectorSettings(message.vectorSettings);
});
async function handleGetContainerList() {
const containers = await browser.contextualIdentities.query({});
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
const seeds = stored.containerSeeds || {};
const settings = stored.containerSettings || {};
const reverseDomainMap = {};
for (const [domain, cid] of Object.entries(domainMap)) {
reverseDomainMap[cid] = domain;
}
return containers.map(c => ({
name: c.name,
cookieStoreId: c.cookieStoreId,
color: c.color,
icon: c.icon,
domain: reverseDomainMap[c.cookieStoreId] || null,
enabled: (settings[c.cookieStoreId]?.enabled !== false),
hasSeed: !!seeds[c.cookieStoreId]
}));
}
async function handleToggle(cookieStoreId, enabled) {
const stored = await browser.storage.local.get("containerSettings");
const settings = stored.containerSettings || {};
settings[cookieStoreId] = { ...settings[cookieStoreId], enabled };
await browser.storage.local.set({ containerSettings: settings });
if (!enabled) {
if (registeredScripts[cookieStoreId]) {
try { await registeredScripts[cookieStoreId].unregister(); } catch(e) {}
delete registeredScripts[cookieStoreId];
}
} else {
const seedStored = await browser.storage.local.get("containerSeeds");
const seeds = seedStored.containerSeeds || {};
if (seeds[cookieStoreId]) {
await buildProfileAndRegister(cookieStoreId, seeds[cookieStoreId]);
}
}
return { ok: true };
}
async function handleRegenerate(cookieStoreId) {
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
const seeds = stored.containerSeeds || {};
const settings = stored.containerSettings || {};
seeds[cookieStoreId] = generateSeed();
await browser.storage.local.set({ containerSeeds: seeds });
const cfg = settings[cookieStoreId] || { enabled: true };
if (cfg.enabled) {
await buildProfileAndRegister(cookieStoreId, seeds[cookieStoreId]);
}
return { ok: true };
}
async function handleRegenerateAll() {
const stored = await browser.storage.local.get("containerSeeds");
const seeds = stored.containerSeeds || {};
for (const cid of Object.keys(seeds)) {
seeds[cid] = generateSeed();
}
await browser.storage.local.set({ containerSeeds: seeds });
await registerAllKnownContainers();
return { ok: true };
}
async function handleResetAll() {
// Unregister all content scripts
for (const [cid, script] of Object.entries(registeredScripts)) {
try { await script.unregister(); } catch(e) {}
}
for (const key of Object.keys(registeredScripts)) {
delete registeredScripts[key];
}
// Remove all ContainSite-managed containers
const containers = await browser.contextualIdentities.query({});
const ourContainerIds = new Set(Object.values(domainMap));
for (const c of containers) {
if (ourContainerIds.has(c.cookieStoreId)) {
try { await browser.contextualIdentities.remove(c.cookieStoreId); } catch(e) {}
}
}
// Clear all storage and caches
domainMap = {};
pendingTabs = {};
cachedWhitelist = [];
managedContainerIds.clear();
for (const key of Object.keys(containerProfiles)) delete containerProfiles[key];
await browser.storage.local.clear();
return { ok: true };
}
async function handlePruneContainers() {
// Remove containers that have no open tabs
const containers = await browser.contextualIdentities.query({});
const tabs = await browser.tabs.query({});
// Collect cookieStoreIds that have open tabs
const activeContainers = new Set(tabs.map(t => t.cookieStoreId));
let pruned = 0;
for (const c of containers) {
if (managedContainerIds.has(c.cookieStoreId) && !activeContainers.has(c.cookieStoreId)) {
try {
await browser.contextualIdentities.remove(c.cookieStoreId);
pruned++;
} catch(e) {}
// onRemoved listener handles domainMap + managedContainerIds cleanup
}
}
return { pruned };
}
async function handleDeleteContainer(cookieStoreId) {
try {
await browser.contextualIdentities.remove(cookieStoreId);
} catch(e) {}
const stored = await browser.storage.local.get(["containerSeeds", "containerSettings"]);
const seeds = stored.containerSeeds || {};
const settings = stored.containerSettings || {};
delete seeds[cookieStoreId];
delete settings[cookieStoreId];
await browser.storage.local.set({ containerSeeds: seeds, containerSettings: settings });
return { ok: true };
}
async function handleGetWhitelist() {
const stored = await browser.storage.local.get("whitelist");
return stored.whitelist || [];
}
async function handleSetWhitelist(whitelist) {
cachedWhitelist = whitelist;
await browser.storage.local.set({ whitelist });
return { ok: true };
}
async function handleGetVectorSettings() {
const stored = await browser.storage.local.get("vectorSettings");
return stored.vectorSettings || {};
}
async function handleSetVectorSettings(vectorSettings) {
await browser.storage.local.set({ vectorSettings });
await registerAllKnownContainers();
return { ok: true };
}
// --- Container Lifecycle ---
browser.contextualIdentities.onRemoved.addListener(async ({ contextualIdentity }) => {
const cid = contextualIdentity.cookieStoreId;
managedContainerIds.delete(cid);
delete containerProfiles[cid];
if (registeredScripts[cid]) {
try { await registeredScripts[cid].unregister(); } catch(e) {}
delete registeredScripts[cid];
}
for (const [domain, cookieStoreId] of Object.entries(domainMap)) {
if (cookieStoreId === cid) {
delete domainMap[domain];
}
}
await saveDomainMap();
});
// --- HTTP Header Spoofing ---
// Modifies User-Agent and Accept-Language headers to match each container's
// spoofed profile, preventing server-side detection of JS/HTTP header mismatch.
function formatAcceptLanguage(languages) {
if (!languages || languages.length === 0) return "en-US,en;q=0.5";
return languages.map((lang, i) => {
if (i === 0) return lang;
const q = Math.max(0.1, 1 - i * 0.1).toFixed(1);
return `${lang};q=${q}`;
}).join(",");
}
// Auth domains where UA spoofing breaks login flows
const AUTH_BYPASS_DOMAINS = ["accounts.google.com", "accounts.youtube.com"];
browser.webRequest.onBeforeSendHeaders.addListener(
function(details) {
// cookieStoreId is available in Firefox 77+ webRequest details
const profile = containerProfiles[details.cookieStoreId];
if (!profile) return {};
// Skip UA spoofing on auth domains so login isn't rejected
try {
const host = new URL(details.url).hostname;
if (AUTH_BYPASS_DOMAINS.includes(host)) return {};
} catch(e) {}
const headers = details.requestHeaders;
// Map platform to Client Hints platform name
const platformMap = {
"Win32": "Windows", "Linux x86_64": "Linux", "MacIntel": "macOS"
};
const chPlatform = platformMap[profile.platform] || "Unknown";
for (let i = headers.length - 1; i >= 0; i--) {
const name = headers[i].name.toLowerCase();
if (name === "user-agent") {
headers[i].value = profile.userAgent;
} else if (name === "accept-language") {
headers[i].value = formatAcceptLanguage(profile.languages);
} else if (name === "sec-ch-ua" || name === "sec-ch-ua-full-version-list") {
// Firefox doesn't normally send these, but strip if present
headers.splice(i, 1);
} else if (name === "sec-ch-ua-platform") {
headers[i].value = `"${chPlatform}"`;
} else if (name === "sec-ch-ua-mobile") {
headers[i].value = "?0";
}
}
return { requestHeaders: headers };
},
{ urls: ["<all_urls>"] },
["blocking", "requestHeaders"]
);
// --- Init ---
async function init() {
await loadDomainMap();
const stored = await browser.storage.local.get(["containerSeeds", "whitelist"]);
const seeds = stored.containerSeeds || {};
for (const cid of Object.keys(seeds)) {
managedContainerIds.add(cid);
}
cachedWhitelist = stored.whitelist || [];
await registerAllKnownContainers();
}
init();