Initial release — per-site container isolation with unique device fingerprints
Automatic per-domain containers with hardened fingerprint spoofing: canvas, WebGL, audio, navigator, screen, timezone, WebRTC, fonts, ClientRects, plugins, battery, and connection APIs.
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.xpi
|
||||
*.zip
|
||||
icons/icon.svg
|
||||
391
background.js
Normal file
391
background.js
Normal file
@@ -0,0 +1,391 @@
|
||||
// ContainSite — Background Script
|
||||
// Every site gets its own container. Auth redirects stay in the originating container.
|
||||
|
||||
const registeredScripts = {}; // cookieStoreId -> RegisteredContentScript
|
||||
let injectSourceCache = null;
|
||||
let domainMap = {}; // baseDomain -> cookieStoreId
|
||||
let pendingTabs = {}; // tabId -> true (tabs being redirected)
|
||||
let tabOrigins = {}; // tabId -> cookieStoreId (tracks which container a tab was assigned to)
|
||||
|
||||
const CONTAINER_COLORS = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple"];
|
||||
const CONTAINER_ICONS = ["fingerprint", "fence", "briefcase", "cart", "circle", "gift", "tree", "chill"];
|
||||
|
||||
// --- 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 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;
|
||||
const profile = generateFingerprintProfile(seed);
|
||||
await registerForContainer(cookieStoreId, profile);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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;
|
||||
await saveDomainMap();
|
||||
|
||||
const stored = await browser.storage.local.get("containerSeeds");
|
||||
const seeds = stored.containerSeeds || {};
|
||||
seeds[cookieStoreId] = generateSeed();
|
||||
await browser.storage.local.set({ containerSeeds: seeds });
|
||||
|
||||
const profile = generateFingerprintProfile(seeds[cookieStoreId]);
|
||||
await registerForContainer(cookieStoreId, profile);
|
||||
|
||||
return cookieStoreId;
|
||||
}
|
||||
|
||||
// Set of tabIds we just created — skip these entirely to prevent loops
|
||||
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
|
||||
if (createdByUs[tabId]) return;
|
||||
if (pendingTabs[tabId]) return;
|
||||
pendingTabs[tabId] = true;
|
||||
|
||||
try {
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
|
||||
// If the tab is in ANY non-default container, leave it alone.
|
||||
// Either it's one of ours, or the user put it there intentionally.
|
||||
if (tab.cookieStoreId !== "firefox-default") {
|
||||
delete pendingTabs[tabId];
|
||||
return;
|
||||
}
|
||||
|
||||
// Tab is in the default (uncontained) context — assign it to the right container
|
||||
const cookieStoreId = await getOrCreateContainerForDomain(baseDomain);
|
||||
|
||||
if (tab.cookieStoreId === cookieStoreId) {
|
||||
delete pendingTabs[tabId];
|
||||
return;
|
||||
}
|
||||
|
||||
const newTab = await browser.tabs.create({
|
||||
url: url,
|
||||
cookieStoreId: cookieStoreId,
|
||||
index: tab.index + 1,
|
||||
active: tab.active
|
||||
});
|
||||
// Mark the new tab so we never redirect it again
|
||||
createdByUs[newTab.id] = true;
|
||||
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];
|
||||
delete tabOrigins[tabId];
|
||||
});
|
||||
|
||||
// --- Message Handling (from popup) ---
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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]) {
|
||||
const profile = generateFingerprintProfile(seeds[cookieStoreId]);
|
||||
await registerForContainer(cookieStoreId, profile);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
const profile = generateFingerprintProfile(seeds[cookieStoreId]);
|
||||
await registerForContainer(cookieStoreId, profile);
|
||||
}
|
||||
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
|
||||
domainMap = {};
|
||||
pendingTabs = {};
|
||||
tabOrigins = {};
|
||||
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 ourContainerIds = new Set(Object.values(domainMap));
|
||||
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 (ourContainerIds.has(c.cookieStoreId) && !activeContainers.has(c.cookieStoreId)) {
|
||||
try {
|
||||
await browser.contextualIdentities.remove(c.cookieStoreId);
|
||||
pruned++;
|
||||
} catch(e) {}
|
||||
// domainMap cleanup happens via the onRemoved listener
|
||||
}
|
||||
}
|
||||
return { pruned };
|
||||
}
|
||||
|
||||
// --- Container Lifecycle ---
|
||||
|
||||
browser.contextualIdentities.onRemoved.addListener(async ({ contextualIdentity }) => {
|
||||
const cid = contextualIdentity.cookieStoreId;
|
||||
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();
|
||||
});
|
||||
|
||||
// --- Init ---
|
||||
|
||||
async function init() {
|
||||
await loadDomainMap();
|
||||
await registerAllKnownContainers();
|
||||
}
|
||||
|
||||
init();
|
||||
BIN
icons/icon-48.png
Normal file
BIN
icons/icon-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
icons/icon-96.png
Normal file
BIN
icons/icon-96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
448
inject.js
Normal file
448
inject.js
Normal file
@@ -0,0 +1,448 @@
|
||||
// ContainSite — Hardened fingerprint overrides
|
||||
// Uses Firefox exportFunction/wrappedJSObject APIs (bypasses CSP)
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
const CONFIG = window.__csConfig;
|
||||
if (!CONFIG) return;
|
||||
delete window.__csConfig;
|
||||
|
||||
const pageWindow = window.wrappedJSObject;
|
||||
|
||||
// --- PRNG (Mulberry32) ---
|
||||
function mulberry32(seed) {
|
||||
return function() {
|
||||
seed |= 0;
|
||||
seed = seed + 0x6D2B79F5 | 0;
|
||||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||
t = t + Math.imul(t ^ (t >>> 7), 61 | t) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CANVAS SPOOFING
|
||||
// =========================================================================
|
||||
|
||||
const origGetImageData = window.CanvasRenderingContext2D.prototype.getImageData;
|
||||
const origPutImageData = window.CanvasRenderingContext2D.prototype.putImageData;
|
||||
|
||||
function addCanvasNoise(ctx, canvas) {
|
||||
try {
|
||||
const w = canvas.width, h = canvas.height;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
const imgData = origGetImageData.call(ctx, 0, 0, w, h);
|
||||
const data = imgData.data;
|
||||
const rng = mulberry32(CONFIG.canvasSeed);
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (rng() < 0.1) {
|
||||
const ch = (rng() * 3) | 0;
|
||||
const delta = rng() < 0.5 ? -1 : 1;
|
||||
data[i + ch] = Math.max(0, Math.min(255, data[i + ch] + delta));
|
||||
}
|
||||
}
|
||||
origPutImageData.call(ctx, imgData, 0, 0);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
const origToDataURL = window.HTMLCanvasElement.prototype.toDataURL;
|
||||
exportFunction(function(...args) {
|
||||
try {
|
||||
const ctx = this.getContext("2d");
|
||||
if (ctx) addCanvasNoise(ctx, this);
|
||||
} catch(e) {}
|
||||
return origToDataURL.apply(this, args);
|
||||
}, pageWindow.HTMLCanvasElement.prototype, { defineAs: "toDataURL" });
|
||||
|
||||
const origToBlob = window.HTMLCanvasElement.prototype.toBlob;
|
||||
exportFunction(function(callback, ...args) {
|
||||
try {
|
||||
const ctx = this.getContext("2d");
|
||||
if (ctx) addCanvasNoise(ctx, this);
|
||||
} catch(e) {}
|
||||
return origToBlob.call(this, callback, ...args);
|
||||
}, pageWindow.HTMLCanvasElement.prototype, { defineAs: "toBlob" });
|
||||
|
||||
exportFunction(function(...args) {
|
||||
const imgData = origGetImageData.apply(this, args);
|
||||
const data = imgData.data;
|
||||
const rng = mulberry32(CONFIG.canvasSeed);
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (rng() < 0.1) {
|
||||
const ch = (rng() * 3) | 0;
|
||||
const delta = rng() < 0.5 ? -1 : 1;
|
||||
data[i + ch] = Math.max(0, Math.min(255, data[i + ch] + delta));
|
||||
}
|
||||
}
|
||||
return imgData;
|
||||
}, pageWindow.CanvasRenderingContext2D.prototype, { defineAs: "getImageData" });
|
||||
|
||||
// =========================================================================
|
||||
// WEBGL SPOOFING
|
||||
// =========================================================================
|
||||
|
||||
const UNMASKED_VENDOR = 0x9245;
|
||||
const UNMASKED_RENDERER = 0x9246;
|
||||
|
||||
function patchWebGL(protoName) {
|
||||
const pageProto = pageWindow[protoName];
|
||||
if (!pageProto) return;
|
||||
const origProto = window[protoName];
|
||||
if (!origProto) return;
|
||||
|
||||
const origGetParam = origProto.prototype.getParameter;
|
||||
exportFunction(function(pname) {
|
||||
if (pname === UNMASKED_VENDOR) return CONFIG.webgl.vendor;
|
||||
if (pname === UNMASKED_RENDERER) return CONFIG.webgl.renderer;
|
||||
return origGetParam.call(this, pname);
|
||||
}, pageProto.prototype, { defineAs: "getParameter" });
|
||||
}
|
||||
|
||||
patchWebGL("WebGLRenderingContext");
|
||||
patchWebGL("WebGL2RenderingContext");
|
||||
|
||||
// =========================================================================
|
||||
// AUDIO SPOOFING
|
||||
// =========================================================================
|
||||
|
||||
const origGetFloatFreq = window.AnalyserNode.prototype.getFloatFrequencyData;
|
||||
exportFunction(function(array) {
|
||||
origGetFloatFreq.call(this, array);
|
||||
const rng = mulberry32(CONFIG.audioSeed);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i] !== 0) array[i] += (rng() - 0.5) * 0.0001;
|
||||
}
|
||||
}, pageWindow.AnalyserNode.prototype, { defineAs: "getFloatFrequencyData" });
|
||||
|
||||
const origGetByteFreq = window.AnalyserNode.prototype.getByteFrequencyData;
|
||||
exportFunction(function(array) {
|
||||
origGetByteFreq.call(this, array);
|
||||
const rng = mulberry32(CONFIG.audioSeed);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i] !== 0 && rng() < 0.05) {
|
||||
array[i] = Math.max(0, Math.min(255, array[i] + (rng() < 0.5 ? -1 : 1)));
|
||||
}
|
||||
}
|
||||
}, pageWindow.AnalyserNode.prototype, { defineAs: "getByteFrequencyData" });
|
||||
|
||||
const origGetChannelData = window.AudioBuffer.prototype.getChannelData;
|
||||
exportFunction(function(channel) {
|
||||
const data = origGetChannelData.call(this, channel);
|
||||
const rng = mulberry32(CONFIG.audioSeed);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i] !== 0) data[i] += (rng() - 0.5) * 0.0001;
|
||||
}
|
||||
return data;
|
||||
}, pageWindow.AudioBuffer.prototype, { defineAs: "getChannelData" });
|
||||
|
||||
// =========================================================================
|
||||
// NAVIGATOR SPOOFING
|
||||
// =========================================================================
|
||||
|
||||
const navOverrides = {
|
||||
hardwareConcurrency: CONFIG.nav.hardwareConcurrency,
|
||||
platform: CONFIG.nav.platform,
|
||||
deviceMemory: CONFIG.nav.deviceMemory,
|
||||
maxTouchPoints: CONFIG.nav.maxTouchPoints
|
||||
};
|
||||
|
||||
for (const [prop, value] of Object.entries(navOverrides)) {
|
||||
if (value !== undefined) {
|
||||
Object.defineProperty(pageWindow.Navigator.prototype, prop, {
|
||||
get: exportFunction(function() { return value; }, pageWindow),
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const frozenLangs = CONFIG.nav.languages;
|
||||
Object.defineProperty(pageWindow.Navigator.prototype, "languages", {
|
||||
get: exportFunction(function() {
|
||||
return cloneInto(frozenLangs, pageWindow, { freeze: true });
|
||||
}, pageWindow),
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(pageWindow.Navigator.prototype, "language", {
|
||||
get: exportFunction(function() { return frozenLangs[0]; }, pageWindow),
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
// Spoof plugins and mimeTypes as empty
|
||||
Object.defineProperty(pageWindow.Navigator.prototype, "plugins", {
|
||||
get: exportFunction(function() {
|
||||
return cloneInto([], pageWindow);
|
||||
}, pageWindow),
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(pageWindow.Navigator.prototype, "mimeTypes", {
|
||||
get: exportFunction(function() {
|
||||
return cloneInto([], pageWindow);
|
||||
}, pageWindow),
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
// Spoof connection info
|
||||
if (pageWindow.navigator.connection) {
|
||||
try {
|
||||
Object.defineProperty(pageWindow.Navigator.prototype, "connection", {
|
||||
get: exportFunction(function() {
|
||||
return cloneInto({
|
||||
effectiveType: "4g",
|
||||
downlink: 10,
|
||||
rtt: 50,
|
||||
saveData: false
|
||||
}, pageWindow);
|
||||
}, pageWindow),
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// Block Battery API
|
||||
if (pageWindow.navigator.getBattery) {
|
||||
exportFunction(function() {
|
||||
return new pageWindow.Promise(exportFunction(function(resolve) {
|
||||
resolve(cloneInto({
|
||||
charging: true,
|
||||
chargingTime: 0,
|
||||
dischargingTime: Infinity,
|
||||
level: 1.0,
|
||||
addEventListener: function() {},
|
||||
removeEventListener: function() {}
|
||||
}, pageWindow, { cloneFunctions: true }));
|
||||
}, pageWindow));
|
||||
}, pageWindow.Navigator.prototype, { defineAs: "getBattery" });
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SCREEN SPOOFING
|
||||
// =========================================================================
|
||||
|
||||
const screenOverrides = {
|
||||
width: CONFIG.screen.width,
|
||||
height: CONFIG.screen.height,
|
||||
availWidth: CONFIG.screen.width,
|
||||
availHeight: CONFIG.screen.height - 40,
|
||||
colorDepth: CONFIG.screen.colorDepth,
|
||||
pixelDepth: CONFIG.screen.colorDepth
|
||||
};
|
||||
|
||||
for (const [prop, value] of Object.entries(screenOverrides)) {
|
||||
Object.defineProperty(pageWindow.Screen.prototype, prop, {
|
||||
get: exportFunction(function() { return value; }, pageWindow),
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(pageWindow, "outerWidth", {
|
||||
get: exportFunction(function() { return CONFIG.screen.width; }, pageWindow),
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(pageWindow, "outerHeight", {
|
||||
get: exportFunction(function() { return CONFIG.screen.height; }, pageWindow),
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(pageWindow, "innerWidth", {
|
||||
get: exportFunction(function() { return CONFIG.screen.width; }, pageWindow),
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(pageWindow, "innerHeight", {
|
||||
get: exportFunction(function() { return CONFIG.screen.height - 80; }, pageWindow),
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// TIMEZONE SPOOFING
|
||||
// =========================================================================
|
||||
|
||||
if (CONFIG.timezone) {
|
||||
const tzName = CONFIG.timezone.name;
|
||||
const tzOffset = CONFIG.timezone.offset;
|
||||
|
||||
// Override getTimezoneOffset
|
||||
const origGetTimezoneOffset = window.Date.prototype.getTimezoneOffset;
|
||||
exportFunction(function() {
|
||||
return tzOffset;
|
||||
}, pageWindow.Date.prototype, { defineAs: "getTimezoneOffset" });
|
||||
|
||||
// Override Intl.DateTimeFormat.prototype.resolvedOptions to report spoofed timezone
|
||||
const OrigDateTimeFormat = window.Intl.DateTimeFormat;
|
||||
const origResolvedOptions = OrigDateTimeFormat.prototype.resolvedOptions;
|
||||
exportFunction(function() {
|
||||
const opts = origResolvedOptions.call(this);
|
||||
try { opts.timeZone = tzName; } catch(e) {}
|
||||
return opts;
|
||||
}, pageWindow.Intl.DateTimeFormat.prototype, { defineAs: "resolvedOptions" });
|
||||
|
||||
// Override Date.prototype.toString and toTimeString to reflect spoofed timezone
|
||||
const origToString = window.Date.prototype.toString;
|
||||
const origToTimeString = window.Date.prototype.toTimeString;
|
||||
|
||||
function formatTzAbbrev(tzName) {
|
||||
// Generate a plausible timezone abbreviation
|
||||
const abbrevMap = {
|
||||
"America/New_York": "EST", "America/Chicago": "CST",
|
||||
"America/Denver": "MST", "America/Los_Angeles": "PST",
|
||||
"Europe/London": "GMT", "Europe/Berlin": "CET",
|
||||
"Europe/Paris": "CET", "Asia/Tokyo": "JST",
|
||||
"Australia/Sydney": "AEST", "America/Toronto": "EST",
|
||||
"America/Phoenix": "MST"
|
||||
};
|
||||
return abbrevMap[tzName] || "UTC";
|
||||
}
|
||||
|
||||
function buildTzString(date) {
|
||||
try {
|
||||
const fmt = new OrigDateTimeFormat("en-US", {
|
||||
timeZone: tzName,
|
||||
weekday: "short", year: "numeric", month: "short", day: "2-digit",
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
|
||||
});
|
||||
const parts = fmt.format(date);
|
||||
const sign = tzOffset <= 0 ? "+" : "-";
|
||||
const absOff = Math.abs(tzOffset);
|
||||
const h = String(Math.floor(absOff / 60)).padStart(2, "0");
|
||||
const m = String(absOff % 60).padStart(2, "0");
|
||||
const abbrev = formatTzAbbrev(tzName);
|
||||
return `${parts} GMT${sign}${h}${m} (${abbrev})`;
|
||||
} catch(e) {
|
||||
return origToString.call(date);
|
||||
}
|
||||
}
|
||||
|
||||
exportFunction(function() {
|
||||
return buildTzString(this);
|
||||
}, pageWindow.Date.prototype, { defineAs: "toString" });
|
||||
|
||||
exportFunction(function() {
|
||||
try {
|
||||
const fmt = new OrigDateTimeFormat("en-US", {
|
||||
timeZone: tzName,
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false
|
||||
});
|
||||
const parts = fmt.format(this);
|
||||
const sign = tzOffset <= 0 ? "+" : "-";
|
||||
const absOff = Math.abs(tzOffset);
|
||||
const h = String(Math.floor(absOff / 60)).padStart(2, "0");
|
||||
const m = String(absOff % 60).padStart(2, "0");
|
||||
const abbrev = formatTzAbbrev(tzName);
|
||||
return `${parts} GMT${sign}${h}${m} (${abbrev})`;
|
||||
} catch(e) {
|
||||
return origToTimeString.call(this);
|
||||
}
|
||||
}, pageWindow.Date.prototype, { defineAs: "toTimeString" });
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// WEBRTC LEAK PROTECTION
|
||||
// =========================================================================
|
||||
|
||||
if (CONFIG.webrtc && CONFIG.webrtc.blockLocal) {
|
||||
// Wrap RTCPeerConnection to prevent local IP leaks
|
||||
if (pageWindow.RTCPeerConnection) {
|
||||
const OrigRTC = window.RTCPeerConnection;
|
||||
const wrappedRTC = exportFunction(function(config, constraints) {
|
||||
// Force TURN-only to prevent local candidate leaks
|
||||
if (config && config.iceServers) {
|
||||
config.iceTransportPolicy = "relay";
|
||||
}
|
||||
const pc = new OrigRTC(config, constraints);
|
||||
return pc;
|
||||
}, pageWindow);
|
||||
|
||||
try {
|
||||
wrappedRTC.prototype = pageWindow.RTCPeerConnection.prototype;
|
||||
pageWindow.RTCPeerConnection = wrappedRTC;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (pageWindow.webkitRTCPeerConnection) {
|
||||
try {
|
||||
pageWindow.webkitRTCPeerConnection = pageWindow.RTCPeerConnection;
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FONT FINGERPRINT PROTECTION
|
||||
// =========================================================================
|
||||
|
||||
if (CONFIG.fontSeed) {
|
||||
const fontRng = mulberry32(CONFIG.fontSeed);
|
||||
|
||||
// Add subtle noise to measureText to prevent font enumeration
|
||||
const origMeasureText = window.CanvasRenderingContext2D.prototype.measureText;
|
||||
exportFunction(function(text) {
|
||||
const metrics = origMeasureText.call(this, text);
|
||||
|
||||
// Add deterministic sub-pixel noise to width
|
||||
const noise = (fontRng() - 0.5) * 0.3;
|
||||
const origWidth = metrics.width;
|
||||
|
||||
try {
|
||||
Object.defineProperty(metrics, "width", {
|
||||
get: function() { return origWidth + noise; },
|
||||
configurable: true
|
||||
});
|
||||
} catch(e) {}
|
||||
|
||||
return metrics;
|
||||
}, pageWindow.CanvasRenderingContext2D.prototype, { defineAs: "measureText" });
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CLIENTRECTS FINGERPRINT PROTECTION
|
||||
// =========================================================================
|
||||
|
||||
if (CONFIG.rectSeed) {
|
||||
const rectRng = mulberry32(CONFIG.rectSeed);
|
||||
|
||||
function addRectNoise(rect) {
|
||||
const noise = (rectRng() - 0.5) * 0.1;
|
||||
try {
|
||||
const origX = rect.x, origY = rect.y;
|
||||
const origW = rect.width, origH = rect.height;
|
||||
const origT = rect.top, origL = rect.left;
|
||||
const origB = rect.bottom, origR = rect.right;
|
||||
|
||||
Object.defineProperties(rect, {
|
||||
x: { get: () => origX + noise, configurable: true },
|
||||
y: { get: () => origY + noise, configurable: true },
|
||||
width: { get: () => origW + noise, configurable: true },
|
||||
height: { get: () => origH + noise, configurable: true },
|
||||
top: { get: () => origT + noise, configurable: true },
|
||||
left: { get: () => origL + noise, configurable: true },
|
||||
bottom: { get: () => origB + noise, configurable: true },
|
||||
right: { get: () => origR + noise, configurable: true }
|
||||
});
|
||||
} catch(e) {}
|
||||
return rect;
|
||||
}
|
||||
|
||||
const origGetBCR = window.Element.prototype.getBoundingClientRect;
|
||||
exportFunction(function() {
|
||||
const rect = origGetBCR.call(this);
|
||||
return addRectNoise(rect);
|
||||
}, pageWindow.Element.prototype, { defineAs: "getBoundingClientRect" });
|
||||
|
||||
const origGetCR = window.Element.prototype.getClientRects;
|
||||
exportFunction(function() {
|
||||
const rects = origGetCR.call(this);
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
addRectNoise(rects[i]);
|
||||
}
|
||||
return rects;
|
||||
}, pageWindow.Element.prototype, { defineAs: "getClientRects" });
|
||||
}
|
||||
|
||||
})();
|
||||
122
lib/fingerprint-gen.js
Normal file
122
lib/fingerprint-gen.js
Normal file
@@ -0,0 +1,122 @@
|
||||
// Deterministic fingerprint profile generator
|
||||
// Given the same seed, always produces the same device identity
|
||||
|
||||
// Real hardware values — spoofed values must NEVER match these
|
||||
const REAL_HARDWARE = {
|
||||
hardwareConcurrency: 4, // 2 cores / 4 threads
|
||||
screenWidth: 1920,
|
||||
screenHeight: 1080
|
||||
};
|
||||
|
||||
function generateFingerprintProfile(masterSeed) {
|
||||
const rng = mulberry32(masterSeed);
|
||||
|
||||
function pick(arr) {
|
||||
return arr[Math.floor(rng() * arr.length)];
|
||||
}
|
||||
|
||||
// Pick from array, but never the excluded value. Rerolls if needed.
|
||||
function pickExcluding(arr, exclude) {
|
||||
const filtered = arr.filter(v => {
|
||||
if (typeof exclude === "object" && exclude !== null) {
|
||||
return Object.keys(exclude).some(k => v[k] !== exclude[k]);
|
||||
}
|
||||
return v !== exclude;
|
||||
});
|
||||
return filtered.length > 0 ? pick(filtered) : pick(arr);
|
||||
}
|
||||
|
||||
function subSeed() {
|
||||
return (rng() * 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
const platforms = ["Win32", "Linux x86_64", "MacIntel"];
|
||||
|
||||
const vendors = [
|
||||
"Google Inc. (NVIDIA)",
|
||||
"Google Inc. (AMD)",
|
||||
"Google Inc. (Intel)",
|
||||
"Google Inc."
|
||||
];
|
||||
|
||||
const renderers = [
|
||||
"ANGLE (NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"ANGLE (AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"ANGLE (Intel HD Graphics 630 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"ANGLE (NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"ANGLE (AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0)",
|
||||
"Mesa Intel(R) UHD Graphics 620",
|
||||
"Mesa AMD Radeon RX 580",
|
||||
"ANGLE (Intel, Mesa Intel(R) UHD Graphics 620, OpenGL 4.6)"
|
||||
];
|
||||
|
||||
const resolutions = [
|
||||
{ width: 2560, height: 1440 },
|
||||
{ width: 1366, height: 768 },
|
||||
{ width: 1536, height: 864 },
|
||||
{ width: 1440, height: 900 },
|
||||
{ width: 1680, height: 1050 },
|
||||
{ width: 2560, height: 1080 },
|
||||
{ width: 3440, height: 1440 },
|
||||
{ width: 1600, height: 900 }
|
||||
];
|
||||
|
||||
const languageSets = [
|
||||
["en-US", "en"],
|
||||
["en-GB", "en"],
|
||||
["en-US"],
|
||||
["de-DE", "de", "en-US", "en"],
|
||||
["fr-FR", "fr", "en-US", "en"]
|
||||
];
|
||||
|
||||
// Exclude real hardwareConcurrency (4)
|
||||
const hardwareConcurrencies = [2, 6, 8, 12, 16];
|
||||
const deviceMemories = [4, 8, 16, 32];
|
||||
const colorDepths = [24, 30, 32];
|
||||
|
||||
// Timezones for spoofing — common real-world timezones
|
||||
const timezones = [
|
||||
{ name: "America/New_York", offset: 300 },
|
||||
{ name: "America/Chicago", offset: 360 },
|
||||
{ name: "America/Denver", offset: 420 },
|
||||
{ name: "America/Los_Angeles", offset: 480 },
|
||||
{ name: "Europe/London", offset: 0 },
|
||||
{ name: "Europe/Berlin", offset: -60 },
|
||||
{ name: "Europe/Paris", offset: -60 },
|
||||
{ name: "Asia/Tokyo", offset: -540 },
|
||||
{ name: "Australia/Sydney", offset: -660 },
|
||||
{ name: "America/Toronto", offset: 300 },
|
||||
{ name: "America/Phoenix", offset: 420 }
|
||||
];
|
||||
|
||||
// Resolution: never match real 1920x1080
|
||||
const res = pickExcluding(resolutions, { width: REAL_HARDWARE.screenWidth, height: REAL_HARDWARE.screenHeight });
|
||||
|
||||
return {
|
||||
seed: masterSeed,
|
||||
canvasSeed: subSeed(),
|
||||
audioSeed: subSeed(),
|
||||
fontSeed: subSeed(),
|
||||
rectSeed: subSeed(),
|
||||
nav: {
|
||||
hardwareConcurrency: pick(hardwareConcurrencies),
|
||||
platform: pick(platforms),
|
||||
languages: pick(languageSets),
|
||||
deviceMemory: pick(deviceMemories),
|
||||
maxTouchPoints: 0
|
||||
},
|
||||
screen: {
|
||||
width: res.width,
|
||||
height: res.height,
|
||||
colorDepth: pick(colorDepths)
|
||||
},
|
||||
webgl: {
|
||||
vendor: pick(vendors),
|
||||
renderer: pick(renderers)
|
||||
},
|
||||
timezone: pick(timezones),
|
||||
webrtc: {
|
||||
blockLocal: true
|
||||
}
|
||||
};
|
||||
}
|
||||
11
lib/prng.js
Normal file
11
lib/prng.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// Mulberry32 — fast, deterministic 32-bit PRNG
|
||||
// Same seed always produces the same sequence
|
||||
function mulberry32(seed) {
|
||||
return function() {
|
||||
seed |= 0;
|
||||
seed = seed + 0x6D2B79F5 | 0;
|
||||
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||
t = t + Math.imul(t ^ (t >>> 7), 61 | t) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
36
manifest.json
Normal file
36
manifest.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "ContainSite",
|
||||
"version": "0.1.0",
|
||||
"description": "Per-container fingerprint isolation — each container gets its own device identity",
|
||||
"permissions": [
|
||||
"contextualIdentities",
|
||||
"cookies",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["lib/prng.js", "lib/fingerprint-gen.js", "background.js"]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_icon": {
|
||||
"48": "icons/icon-48.png",
|
||||
"96": "icons/icon-96.png"
|
||||
},
|
||||
"default_title": "ContainSite"
|
||||
},
|
||||
"icons": {
|
||||
"48": "icons/icon-48.png",
|
||||
"96": "icons/icon-96.png"
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "containsite@salmutt.dev",
|
||||
"strict_min_version": "100.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
popup/popup.css
Normal file
35
popup/popup.css
Normal file
@@ -0,0 +1,35 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { width: 300px; font: 13px/1.4 system-ui, sans-serif; color: #e0e0e0; background: #1e1e2e; }
|
||||
h1 { padding: 10px 12px 6px; font-size: 15px; font-weight: 600; border-bottom: 1px solid #333; }
|
||||
#container-list { max-height: 320px; overflow-y: auto; }
|
||||
.row { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-bottom: 1px solid #2a2a3a; }
|
||||
.row:hover { background: #2a2a3a; }
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.name { flex: 1; overflow: hidden; min-width: 0; }
|
||||
.name-primary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.name-domain { font-size: 10px; color: #888; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.toggle { appearance: none; width: 32px; height: 18px; background: #444; border-radius: 9px; position: relative; cursor: pointer; flex-shrink: 0; }
|
||||
.toggle::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; background: #888; border-radius: 50%; transition: .15s; }
|
||||
.toggle:checked { background: #4a9eff; }
|
||||
.toggle:checked::after { left: 16px; background: #fff; }
|
||||
.regen { background: none; border: 1px solid #555; color: #aaa; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; flex-shrink: 0; }
|
||||
.regen:hover { border-color: #888; color: #ddd; }
|
||||
.actions { padding: 8px 12px; border-top: 1px solid #333; }
|
||||
#regen-all { width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
||||
#regen-all:hover { background: #444; color: #fff; }
|
||||
.actions-row { display: flex; gap: 6px; margin-top: 6px; }
|
||||
.actions-row button { flex: 1; padding: 6px; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
||||
.secondary { background: #2a2a3a; border: 1px solid #555; color: #aaa; }
|
||||
.secondary:hover { background: #333; color: #ddd; }
|
||||
.danger { background: #3a1a1a; border: 1px solid #663333; color: #ff613d; }
|
||||
.danger:hover { background: #4a2020; color: #ff8866; }
|
||||
|
||||
.dot-blue { background: #37adff; }
|
||||
.dot-turquoise { background: #00c79a; }
|
||||
.dot-green { background: #51cd00; }
|
||||
.dot-yellow { background: #ffcb00; }
|
||||
.dot-orange { background: #ff9f00; }
|
||||
.dot-red { background: #ff613d; }
|
||||
.dot-pink { background: #ff4bda; }
|
||||
.dot-purple { background: #af51f5; }
|
||||
.dot-toolbar { background: #888; }
|
||||
19
popup/popup.html
Normal file
19
popup/popup.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>ContainSite</h1>
|
||||
<div id="container-list"></div>
|
||||
<div class="actions">
|
||||
<button id="regen-all">Regenerate All</button>
|
||||
<div class="actions-row">
|
||||
<button id="prune" class="secondary">Prune Unused</button>
|
||||
<button id="reset" class="danger">Reset All</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
88
popup/popup.js
Normal file
88
popup/popup.js
Normal file
@@ -0,0 +1,88 @@
|
||||
async function loadContainers() {
|
||||
const containers = await browser.runtime.sendMessage({ type: "getContainerList" });
|
||||
const list = document.getElementById("container-list");
|
||||
list.innerHTML = "";
|
||||
|
||||
for (const c of containers) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "row";
|
||||
|
||||
const dot = document.createElement("span");
|
||||
dot.className = `dot dot-${c.color}`;
|
||||
row.appendChild(dot);
|
||||
|
||||
const nameWrap = document.createElement("div");
|
||||
nameWrap.className = "name";
|
||||
const name = document.createElement("div");
|
||||
name.className = "name-primary";
|
||||
name.textContent = c.name;
|
||||
nameWrap.appendChild(name);
|
||||
if (c.domain) {
|
||||
const domain = document.createElement("div");
|
||||
domain.className = "name-domain";
|
||||
domain.textContent = c.domain;
|
||||
nameWrap.appendChild(domain);
|
||||
}
|
||||
row.appendChild(nameWrap);
|
||||
|
||||
const toggle = document.createElement("input");
|
||||
toggle.type = "checkbox";
|
||||
toggle.className = "toggle";
|
||||
toggle.checked = c.enabled;
|
||||
toggle.addEventListener("change", async () => {
|
||||
await browser.runtime.sendMessage({
|
||||
type: "toggleContainer",
|
||||
cookieStoreId: c.cookieStoreId,
|
||||
enabled: toggle.checked
|
||||
});
|
||||
});
|
||||
row.appendChild(toggle);
|
||||
|
||||
const regen = document.createElement("button");
|
||||
regen.className = "regen";
|
||||
regen.textContent = "New";
|
||||
regen.title = "Generate new fingerprint";
|
||||
regen.addEventListener("click", async () => {
|
||||
regen.textContent = "...";
|
||||
await browser.runtime.sendMessage({
|
||||
type: "regenerateFingerprint",
|
||||
cookieStoreId: c.cookieStoreId
|
||||
});
|
||||
regen.textContent = "OK";
|
||||
setTimeout(() => { regen.textContent = "New"; }, 800);
|
||||
});
|
||||
row.appendChild(regen);
|
||||
|
||||
list.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("regen-all").addEventListener("click", async (e) => {
|
||||
e.target.textContent = "Regenerating...";
|
||||
await browser.runtime.sendMessage({ type: "regenerateAll" });
|
||||
e.target.textContent = "Done!";
|
||||
setTimeout(() => { e.target.textContent = "Regenerate All"; }, 800);
|
||||
});
|
||||
|
||||
document.getElementById("prune").addEventListener("click", async (e) => {
|
||||
e.target.textContent = "Pruning...";
|
||||
const result = await browser.runtime.sendMessage({ type: "pruneContainers" });
|
||||
e.target.textContent = `Removed ${result.pruned}`;
|
||||
setTimeout(() => {
|
||||
e.target.textContent = "Prune Unused";
|
||||
loadContainers();
|
||||
}, 1200);
|
||||
});
|
||||
|
||||
document.getElementById("reset").addEventListener("click", async (e) => {
|
||||
if (!confirm("Remove all ContainSite containers and data? You will need to log in to all sites again.")) return;
|
||||
e.target.textContent = "Resetting...";
|
||||
await browser.runtime.sendMessage({ type: "resetAll" });
|
||||
e.target.textContent = "Done!";
|
||||
setTimeout(() => {
|
||||
e.target.textContent = "Reset All";
|
||||
loadContainers();
|
||||
}, 1200);
|
||||
});
|
||||
|
||||
loadContainers();
|
||||
Reference in New Issue
Block a user