init
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
Generated
+4172
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "proxy-container",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Firefox extension settings page only",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "web-ext run --source-dir=src",
|
||||||
|
"build": "web-ext build --source-dir=src --artifacts-dir=dist"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"web-ext": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
const STORAGE_KEY = "containerSettings";
|
||||||
|
|
||||||
|
const IGNORED_URL_PREFIXES = [
|
||||||
|
"about:",
|
||||||
|
"moz-extension:",
|
||||||
|
"chrome:",
|
||||||
|
"file:",
|
||||||
|
"view-source:",
|
||||||
|
"data:",
|
||||||
|
"blob:",
|
||||||
|
"javascript:"
|
||||||
|
];
|
||||||
|
|
||||||
|
const redirectedTabIds = new Set();
|
||||||
|
|
||||||
|
browser.action.onClicked.addListener(() => {
|
||||||
|
browser.runtime.openOptionsPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getContainerSettingsMap() {
|
||||||
|
const result = await browser.storage.local.get(STORAGE_KEY);
|
||||||
|
return result[STORAGE_KEY] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProxyType(type) {
|
||||||
|
if (type === "socks5") {
|
||||||
|
return "socks";
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedTypes = ["http", "https", "socks", "socks4"];
|
||||||
|
|
||||||
|
if (allowedTypes.includes(type)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "http";
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IGNORED_URL_PREFIXES.some((prefix) => url.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(value) {
|
||||||
|
return value.replace(/[|\\{}()[\]^$+?.*]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function globToRegex(glob) {
|
||||||
|
let pattern = String(glob || "").trim();
|
||||||
|
|
||||||
|
if (!pattern) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pattern.includes("://")) {
|
||||||
|
pattern = `*://${pattern}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = pattern.replace(
|
||||||
|
/^(\*:\/\/|https?:\/\/)\*\./,
|
||||||
|
"$1{SUBDOMAIN_OR_ROOT}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let regex = escapeRegex(pattern);
|
||||||
|
|
||||||
|
regex = regex
|
||||||
|
.replaceAll("\\*", ".*")
|
||||||
|
.replaceAll("\\?", ".");
|
||||||
|
|
||||||
|
regex = regex.replaceAll(
|
||||||
|
"\\{SUBDOMAIN_OR_ROOT\\}",
|
||||||
|
"([^/]+\\.)?"
|
||||||
|
);
|
||||||
|
|
||||||
|
return new RegExp(`^${regex}(/.*)?$`, "i");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testUrlRule(rule, url) {
|
||||||
|
try {
|
||||||
|
const regex = globToRegex(rule);
|
||||||
|
return regex ? regex.test(url) : false;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Invalid URL rule skipped:", rule, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findContainerForUrl(url) {
|
||||||
|
const settingsMap = await getContainerSettingsMap();
|
||||||
|
|
||||||
|
for (const [cookieStoreId, settings] of Object.entries(settingsMap)) {
|
||||||
|
const urlRules = settings.urlRules || [];
|
||||||
|
|
||||||
|
console.log(urlRules)
|
||||||
|
|
||||||
|
for (const rule of urlRules) {
|
||||||
|
if (testUrlRule(rule, url)) {
|
||||||
|
return cookieStoreId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function redirectTabToContainer(tabId, tab, targetCookieStoreId) {
|
||||||
|
if (!targetCookieStoreId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tab || !tab.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab.cookieStoreId === targetCookieStoreId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectedTabIds.has(tabId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectedTabIds.add(tabId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await browser.tabs.create({
|
||||||
|
url: tab.url,
|
||||||
|
cookieStoreId: targetCookieStoreId,
|
||||||
|
active: tab.active,
|
||||||
|
index: tab.index
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.tabs.remove(tabId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to redirect tab to container:", error);
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
redirectedTabIds.delete(tabId);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL routing:
|
||||||
|
* пользователь открывает URL -> правило находит контейнер -> вкладка переоткрывается в контейнере.
|
||||||
|
*/
|
||||||
|
browser.tabs.onUpdated.addListener(
|
||||||
|
async (tabId, changeInfo, tab) => {
|
||||||
|
if (!changeInfo.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = changeInfo.url;
|
||||||
|
|
||||||
|
if (shouldIgnoreUrl(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetCookieStoreId = await findContainerForUrl(url);
|
||||||
|
|
||||||
|
console.log("targetCookieStoreId", url)
|
||||||
|
|
||||||
|
if (!targetCookieStoreId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await redirectTabToContainer(tabId, tab, targetCookieStoreId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("URL routing failed:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties: ["url"]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-container proxy:
|
||||||
|
* запрос из контейнера -> cookieStoreId -> proxy-настройка контейнера.
|
||||||
|
*/
|
||||||
|
browser.proxy.onRequest.addListener(
|
||||||
|
async (requestDetails) => {
|
||||||
|
console.log("proxy")
|
||||||
|
const cookieStoreId = requestDetails.cookieStoreId;
|
||||||
|
|
||||||
|
if (!cookieStoreId || cookieStoreId === "firefox-default") {
|
||||||
|
return { type: "direct" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsMap = await getContainerSettingsMap();
|
||||||
|
const containerSettings = settingsMap[cookieStoreId];
|
||||||
|
|
||||||
|
if (!containerSettings || !containerSettings.proxy) {
|
||||||
|
return { type: "direct" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = containerSettings.proxy;
|
||||||
|
|
||||||
|
if (!proxy.enabled) {
|
||||||
|
return { type: "direct" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proxy.host || !proxy.port) {
|
||||||
|
return { type: "direct" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyInfo = {
|
||||||
|
type: normalizeProxyType(proxy.type),
|
||||||
|
host: proxy.host,
|
||||||
|
port: Number(proxy.port)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
proxy.proxyDNS === true &&
|
||||||
|
(proxyInfo.type === "socks" || proxyInfo.type === "socks4")
|
||||||
|
) {
|
||||||
|
proxyInfo.proxyDNS = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxyInfo;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: ["<all_urls>"]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
browser.proxy.onError.addListener((error) => {
|
||||||
|
console.error("Proxy error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy authentication.
|
||||||
|
*/
|
||||||
|
browser.webRequest.onAuthRequired.addListener(
|
||||||
|
async (details) => {
|
||||||
|
if (!details.isProxy) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookieStoreId = details.cookieStoreId;
|
||||||
|
|
||||||
|
if (!cookieStoreId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsMap = await getContainerSettingsMap();
|
||||||
|
const containerSettings = settingsMap[cookieStoreId];
|
||||||
|
|
||||||
|
if (!containerSettings || !containerSettings.proxy) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = containerSettings.proxy;
|
||||||
|
|
||||||
|
if (!proxy.enabled || !proxy.username) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authCredentials: {
|
||||||
|
username: proxy.username,
|
||||||
|
password: proxy.password || ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urls: ["<all_urls>"]
|
||||||
|
},
|
||||||
|
["blocking"]
|
||||||
|
);
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Proxy Container",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Firefox containers with regex routing and per-container proxy",
|
||||||
|
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"contextualIdentities",
|
||||||
|
"proxy",
|
||||||
|
"tabs",
|
||||||
|
"cookies",
|
||||||
|
"webRequest",
|
||||||
|
"webRequestBlocking"
|
||||||
|
],
|
||||||
|
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
|
||||||
|
"background": {
|
||||||
|
"scripts": [
|
||||||
|
"background.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"action": {
|
||||||
|
"default_title": "Открыть настройки Proxy Container"
|
||||||
|
},
|
||||||
|
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "proxy-container@example.local",
|
||||||
|
"strict_min_version": "91.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Proxy Container Settings</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 96px;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
word-break: break-all;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: #a40000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #006800;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: #eee;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Proxy Container</h1>
|
||||||
|
|
||||||
|
<p id="status"></p>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Создать контейнер</h2>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field">
|
||||||
|
<label for="name">Название контейнера</label>
|
||||||
|
<input id="name" type="text" placeholder="Work" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="color">Цвет</label>
|
||||||
|
<select id="color">
|
||||||
|
<option value="blue">blue</option>
|
||||||
|
<option value="turquoise">turquoise</option>
|
||||||
|
<option value="green">green</option>
|
||||||
|
<option value="yellow">yellow</option>
|
||||||
|
<option value="orange">orange</option>
|
||||||
|
<option value="red">red</option>
|
||||||
|
<option value="pink">pink</option>
|
||||||
|
<option value="purple">purple</option>
|
||||||
|
<option value="toolbar">toolbar</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="icon">Иконка</label>
|
||||||
|
<select id="icon">
|
||||||
|
<option value="fingerprint">fingerprint</option>
|
||||||
|
<option value="briefcase">briefcase</option>
|
||||||
|
<option value="cart">cart</option>
|
||||||
|
<option value="circle">circle</option>
|
||||||
|
<option value="dollar">dollar</option>
|
||||||
|
<option value="fence">fence</option>
|
||||||
|
<option value="gift">gift</option>
|
||||||
|
<option value="vacation">vacation</option>
|
||||||
|
<option value="food">food</option>
|
||||||
|
<option value="fruit">fruit</option>
|
||||||
|
<option value="pet">pet</option>
|
||||||
|
<option value="tree">tree</option>
|
||||||
|
<option value="chill">chill</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="urlRules">URL-правила, по одному на строку</label>
|
||||||
|
<textarea id="urlRules" placeholder="*.ru github.com/* https://*.example.com/*"></textarea>
|
||||||
|
<div class="muted">
|
||||||
|
Примеры: <code>*.ru</code>, <code>github.com/*</code>, <code>https://*.example.com/*</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Proxy</h3>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input id="proxyEnabled" type="checkbox" />
|
||||||
|
Использовать proxy для этого контейнера
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field">
|
||||||
|
<label for="proxyType">Proxy type</label>
|
||||||
|
<select id="proxyType">
|
||||||
|
<option value="http">http</option>
|
||||||
|
<option value="https">https</option>
|
||||||
|
<option value="socks">socks</option>
|
||||||
|
<option value="socks4">socks4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="proxyPort">Proxy port</label>
|
||||||
|
<input id="proxyPort" type="number" placeholder="8080" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="proxyHost">Proxy host</label>
|
||||||
|
<input id="proxyHost" type="text" placeholder="127.0.0.1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field">
|
||||||
|
<label for="proxyUsername">Proxy username</label>
|
||||||
|
<input id="proxyUsername" type="text" placeholder="необязательно" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="proxyPassword">Proxy password</label>
|
||||||
|
<input id="proxyPassword" type="password" placeholder="необязательно" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input id="proxyDNS" type="checkbox" />
|
||||||
|
Proxy DNS через SOCKS
|
||||||
|
</label>
|
||||||
|
<div class="muted">
|
||||||
|
Имеет смысл только для SOCKS/SOCKS4.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="createContainer">Создать контейнер</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Существующие контейнеры</h2>
|
||||||
|
<div id="containers"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="options.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+418
@@ -0,0 +1,418 @@
|
|||||||
|
const STORAGE_KEY = "containerSettings";
|
||||||
|
|
||||||
|
const CONTAINER_COLORS = [
|
||||||
|
"blue",
|
||||||
|
"turquoise",
|
||||||
|
"green",
|
||||||
|
"yellow",
|
||||||
|
"orange",
|
||||||
|
"red",
|
||||||
|
"pink",
|
||||||
|
"purple",
|
||||||
|
"toolbar"
|
||||||
|
];
|
||||||
|
|
||||||
|
const CONTAINER_ICONS = [
|
||||||
|
"fingerprint",
|
||||||
|
"briefcase",
|
||||||
|
"cart",
|
||||||
|
"circle",
|
||||||
|
"dollar",
|
||||||
|
"fence",
|
||||||
|
"gift",
|
||||||
|
"vacation",
|
||||||
|
"food",
|
||||||
|
"fruit",
|
||||||
|
"pet",
|
||||||
|
"tree",
|
||||||
|
"chill"
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROXY_TYPES = [
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"socks",
|
||||||
|
"socks4"
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_PROXY = {
|
||||||
|
enabled: false,
|
||||||
|
type: "http",
|
||||||
|
host: "",
|
||||||
|
port: 8080,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
proxyDNS: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const nameInput = document.getElementById("name");
|
||||||
|
const colorInput = document.getElementById("color");
|
||||||
|
const iconInput = document.getElementById("icon");
|
||||||
|
const urlRulesInput = document.getElementById("urlRules");
|
||||||
|
|
||||||
|
const proxyEnabledInput = document.getElementById("proxyEnabled");
|
||||||
|
const proxyTypeInput = document.getElementById("proxyType");
|
||||||
|
const proxyHostInput = document.getElementById("proxyHost");
|
||||||
|
const proxyPortInput = document.getElementById("proxyPort");
|
||||||
|
const proxyUsernameInput = document.getElementById("proxyUsername");
|
||||||
|
const proxyPasswordInput = document.getElementById("proxyPassword");
|
||||||
|
const proxyDNSInput = document.getElementById("proxyDNS");
|
||||||
|
|
||||||
|
const createButton = document.getElementById("createContainer");
|
||||||
|
const containersElement = document.getElementById("containers");
|
||||||
|
const statusElement = document.getElementById("status");
|
||||||
|
|
||||||
|
function setStatus(message, isError = false) {
|
||||||
|
statusElement.textContent = message;
|
||||||
|
statusElement.className = isError ? "danger" : "success";
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
setTimeout(() => {
|
||||||
|
statusElement.textContent = "";
|
||||||
|
statusElement.className = "";
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUrlRules(text) {
|
||||||
|
return text
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUrlRules(rules) {
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.includes(" ")) {
|
||||||
|
throw new Error(`URL-правило содержит пробел: ${rule}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.length > 500) {
|
||||||
|
throw new Error(`URL-правило слишком длинное: ${rule}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateProxy(proxy) {
|
||||||
|
if (!proxy.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PROXY_TYPES.includes(proxy.type)) {
|
||||||
|
throw new Error("Некорректный proxy type");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proxy.host) {
|
||||||
|
throw new Error("Proxy host пустой");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(proxy.port) || proxy.port < 1 || proxy.port > 65535) {
|
||||||
|
throw new Error("Proxy port должен быть от 1 до 65535");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSettingsMap() {
|
||||||
|
const result = await browser.storage.local.get(STORAGE_KEY);
|
||||||
|
return result[STORAGE_KEY] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettingsMap(settingsMap) {
|
||||||
|
await browser.storage.local.set({
|
||||||
|
[STORAGE_KEY]: settingsMap
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCreateFormProxy() {
|
||||||
|
return {
|
||||||
|
enabled: proxyEnabledInput.checked,
|
||||||
|
type: proxyTypeInput.value,
|
||||||
|
host: proxyHostInput.value.trim(),
|
||||||
|
port: Number(proxyPortInput.value),
|
||||||
|
username: proxyUsernameInput.value.trim(),
|
||||||
|
password: proxyPasswordInput.value,
|
||||||
|
proxyDNS: proxyDNSInput.checked
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCreateForm() {
|
||||||
|
nameInput.value = "";
|
||||||
|
colorInput.value = "blue";
|
||||||
|
iconInput.value = "fingerprint";
|
||||||
|
urlRulesInput.value = "";
|
||||||
|
|
||||||
|
proxyEnabledInput.checked = false;
|
||||||
|
proxyTypeInput.value = "http";
|
||||||
|
proxyHostInput.value = "";
|
||||||
|
proxyPortInput.value = "";
|
||||||
|
proxyUsernameInput.value = "";
|
||||||
|
proxyPasswordInput.value = "";
|
||||||
|
proxyDNSInput.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createContainer() {
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
const color = colorInput.value;
|
||||||
|
const icon = iconInput.value;
|
||||||
|
const urlRules = parseUrlRules(urlRulesInput.value);
|
||||||
|
const proxy = readCreateFormProxy();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
setStatus("Ошибка: название контейнера пустое", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateUrlRules(urlRules);
|
||||||
|
validateProxy(proxy);
|
||||||
|
|
||||||
|
const container = await browser.contextualIdentities.create({
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
icon
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsMap = await getSettingsMap();
|
||||||
|
|
||||||
|
settingsMap[container.cookieStoreId] = {
|
||||||
|
urlRules,
|
||||||
|
proxy
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveSettingsMap(settingsMap);
|
||||||
|
|
||||||
|
clearCreateForm();
|
||||||
|
|
||||||
|
setStatus("Контейнер создан");
|
||||||
|
await renderContainers();
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOptions(values, selectedValue) {
|
||||||
|
return values
|
||||||
|
.map((value) => {
|
||||||
|
const selected = value === selectedValue ? "selected" : "";
|
||||||
|
return `<option value="${escapeHtml(value)}" ${selected}>${escapeHtml(value)}</option>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCardProxy(card) {
|
||||||
|
return {
|
||||||
|
enabled: card.querySelector(".edit-proxy-enabled").checked,
|
||||||
|
type: card.querySelector(".edit-proxy-type").value,
|
||||||
|
host: card.querySelector(".edit-proxy-host").value.trim(),
|
||||||
|
port: Number(card.querySelector(".edit-proxy-port").value),
|
||||||
|
username: card.querySelector(".edit-proxy-username").value.trim(),
|
||||||
|
password: card.querySelector(".edit-proxy-password").value,
|
||||||
|
proxyDNS: card.querySelector(".edit-proxy-dns").checked
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateContainer(cookieStoreId) {
|
||||||
|
const card = document.querySelector(`[data-cookie-store-id="${cookieStoreId}"]`);
|
||||||
|
|
||||||
|
if (!card) {
|
||||||
|
setStatus("Ошибка: карточка контейнера не найдена", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = card.querySelector(".edit-name").value.trim();
|
||||||
|
const color = card.querySelector(".edit-color").value;
|
||||||
|
const icon = card.querySelector(".edit-icon").value;
|
||||||
|
const urlRules = parseUrlRules(card.querySelector(".edit-url-rules").value);
|
||||||
|
const proxy = readCardProxy(card);
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
setStatus("Ошибка: название контейнера пустое", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateUrlRules(urlRules);
|
||||||
|
validateProxy(proxy);
|
||||||
|
|
||||||
|
await browser.contextualIdentities.update(cookieStoreId, {
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
icon
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsMap = await getSettingsMap();
|
||||||
|
|
||||||
|
settingsMap[cookieStoreId] = {
|
||||||
|
urlRules,
|
||||||
|
proxy
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveSettingsMap(settingsMap);
|
||||||
|
|
||||||
|
setStatus("Контейнер обновлён");
|
||||||
|
await renderContainers();
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeContainer(cookieStoreId) {
|
||||||
|
const confirmed = confirm("Удалить контейнер? Это удалит Firefox-контейнер и его настройки.");
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await browser.contextualIdentities.remove(cookieStoreId);
|
||||||
|
|
||||||
|
const settingsMap = await getSettingsMap();
|
||||||
|
delete settingsMap[cookieStoreId];
|
||||||
|
|
||||||
|
await saveSettingsMap(settingsMap);
|
||||||
|
|
||||||
|
setStatus("Контейнер удалён");
|
||||||
|
await renderContainers();
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContainerCard(container, storedSettings) {
|
||||||
|
const urlRules = storedSettings.urlRules || [];
|
||||||
|
const proxy = {
|
||||||
|
...DEFAULT_PROXY,
|
||||||
|
...(storedSettings.proxy || {})
|
||||||
|
};
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "container-card";
|
||||||
|
card.dataset.cookieStoreId = container.cookieStoreId;
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="container-title">${escapeHtml(container.name)}</div>
|
||||||
|
<div class="small">cookieStoreId: ${escapeHtml(container.cookieStoreId)}</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>Название</label>
|
||||||
|
<input class="edit-name" type="text" value="${escapeHtml(container.name)}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>Цвет</label>
|
||||||
|
<select class="edit-color">
|
||||||
|
${createOptions(CONTAINER_COLORS, container.color)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>Иконка</label>
|
||||||
|
<select class="edit-icon">
|
||||||
|
${createOptions(CONTAINER_ICONS, container.icon)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>URL-правила</label>
|
||||||
|
<textarea class="edit-url-rules">${escapeHtml(urlRules.join("\n"))}</textarea>
|
||||||
|
<div class="muted">
|
||||||
|
Примеры: <code>*.ru</code>, <code>github.com/*</code>, <code>https://*.example.com/*</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Proxy</h3>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input class="edit-proxy-enabled" type="checkbox" ${proxy.enabled ? "checked" : ""} />
|
||||||
|
Использовать proxy для этого контейнера
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>Proxy type</label>
|
||||||
|
<select class="edit-proxy-type">
|
||||||
|
${createOptions(PROXY_TYPES, proxy.type)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>Proxy port</label>
|
||||||
|
<input class="edit-proxy-port" type="number" value="${escapeHtml(proxy.port)}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>Proxy host</label>
|
||||||
|
<input class="edit-proxy-host" type="text" value="${escapeHtml(proxy.host)}" placeholder="127.0.0.1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>Proxy username</label>
|
||||||
|
<input class="edit-proxy-username" type="text" value="${escapeHtml(proxy.username)}" placeholder="необязательно" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>Proxy password</label>
|
||||||
|
<input class="edit-proxy-password" type="password" value="${escapeHtml(proxy.password)}" placeholder="необязательно" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input class="edit-proxy-dns" type="checkbox" ${proxy.proxyDNS ? "checked" : ""} />
|
||||||
|
Proxy DNS через SOCKS
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save-container">Сохранить</button>
|
||||||
|
<button class="delete-container">Удалить</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
card.querySelector(".save-container").addEventListener("click", () => {
|
||||||
|
updateContainer(container.cookieStoreId);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.querySelector(".delete-container").addEventListener("click", () => {
|
||||||
|
removeContainer(container.cookieStoreId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderContainers() {
|
||||||
|
containersElement.innerHTML = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const containers = await browser.contextualIdentities.query({});
|
||||||
|
const settingsMap = await getSettingsMap();
|
||||||
|
|
||||||
|
if (!containers.length) {
|
||||||
|
containersElement.textContent = "Контейнеров пока нет.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const container of containers) {
|
||||||
|
const storedSettings = settingsMap[container.cookieStoreId] || {};
|
||||||
|
const card = renderContainerCard(container, storedSettings);
|
||||||
|
containersElement.appendChild(card);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createButton.addEventListener("click", createContainer);
|
||||||
|
|
||||||
|
renderContainers();
|
||||||
Reference in New Issue
Block a user