first commit
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>About</title>
|
||||
<link href="iframe.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<a href="#changelog">Changelog</a>
|
||||
<a href="#credits">Credits</a>
|
||||
</nav>
|
||||
|
||||
<article class="about">
|
||||
|
||||
<h2 id="changelog">Changelog</h2>
|
||||
<dl>
|
||||
|
||||
<dt>9.2</dt>
|
||||
<dd>Fixed an error when saving incomplete proxies (#197)</dd>
|
||||
|
||||
<dt>9.1</dt>
|
||||
<dd>Fixed an error on Firefox for Android (#196)</dd>
|
||||
|
||||
<dt>9.0</dt>
|
||||
<dd>Added "407 Proxy Authentication Required" to the log (#145)</dd>
|
||||
<dd>Added "Get Associated Domains" feature to Log (#191)</dd>
|
||||
<dd>Added "Get Associated Domains" option to add patterns</dd>
|
||||
<dd>Added Bulk Edit user interface feature (#131, #137, #169)</dd>
|
||||
<dd>Added Context Menu feature</dd>
|
||||
<dd>Added Ctrl-Click option to Add Pattern (#169)</dd>
|
||||
<dd>Added Ctrl-Click option to Add Proxy</dd>
|
||||
<dd>Added extra options to Import FoxyProxy Account</dd>
|
||||
<dd>Added link to Open Shortcut Settings link (Firefox 137)</dd>
|
||||
<dd>Added links to Help from Options</dd>
|
||||
<dd>Added Match Pattern support (#177)</dd>
|
||||
<dd>Added Open Link in New Tab Proxy feature (#149)</dd>
|
||||
<dd>Added Ping feature</dd>
|
||||
<dd>Added Proxy by Pattern tester</dd>
|
||||
<dd>Added show/hide feature to toolbar popup elements based on Tab URL</dd>
|
||||
<dd>Added Tab Proxy by patterns feature (Firefox only) (#180)</dd>
|
||||
<dd>Added Test feature</dd>
|
||||
<dd>Changed Firefox icons to PNG for compatibility with Chrome</dd>
|
||||
<dd>Enabled sending username without password for SOCKS (Firefox only)</dd>
|
||||
<dd>Fixed a Drag and Drop issue (#183)</dd>
|
||||
<dd>Fixed an issue with resetting containers (from 8.10) (#185)</dd>
|
||||
<dd>Increased minimum version to Firefox 128 (released 2024-07-09) due to Firefox root certificate expiry on 2025-03-14</dd>
|
||||
<dd>Removed Chrome v3 to v8 data migration decryption code</dd>
|
||||
<dd>Removed default network passthrough on Firefox in favour of <code>about:config</code> setting (Firefox 137+) (#178)</dd>
|
||||
<dd>Renamed "Quick Add" to "Include Host"</dd>
|
||||
<dd>Repurposed "Exclude Host" to match "Include Host" function</dd>
|
||||
<dd>Unified Firefox and Chrome "manifest.json"</dd>
|
||||
<dd>Updated browser badge code to reflect Global Exclude</dd>
|
||||
<dd>Updated data migration for Match Pattern use (#177)</dd>
|
||||
<dd>Updated Firefox manifest to MV3</dd>
|
||||
<dd>Updated Include/Exclude Host to generate Glob patterns</dd>
|
||||
<dd>Updated Limit WebRTC options (#162)</dd>
|
||||
<dd>Updated Options page user interface</dd>
|
||||
<dd>Updated Pattern Tester</dd>
|
||||
<dd>Updated Tab Proxy for MV3</dd>
|
||||
<dd>Updated toolbar popup user interface</dd>
|
||||
|
||||
<!--
|
||||
https://blog.mozilla.org/addons/2025/03/10/root-certificate-will-expire-on-14-march-users-need-to-update-firefox-to-prevent-add-on-breakage/
|
||||
https://blog.mozilla.org/addons/2024/07/10/manifest-v3-updates-landed-in-firefox-128/
|
||||
minimum version to Firefox 128:
|
||||
-----
|
||||
FF109, Ch88: "host_permissions": Property "host_permissions" is unsupported in Manifest Version 2
|
||||
FF112, Ch92: Added Background script module, removed "content/background.html"
|
||||
FF115: Deprecate browser_style in MV3 defaults to false
|
||||
FF115, Ch102: storage.session 10mb (Ch102-Ch114 1mb)
|
||||
FF121, Ch105: :has()
|
||||
FF126, Ch108: webRequestAuthProvider
|
||||
|
||||
FF117, Ch112 (2023-04-04): CSS nesting ?
|
||||
-->
|
||||
|
||||
<dt>8.10</dt>
|
||||
<dd>Added console log for Save file errors (#144)</dd>
|
||||
<dd>Added custom container option (#33, #161)</dd>
|
||||
<dd>Added direct HTTP authentication (Firefox 125+)</dd>
|
||||
<dd>Added folder to Auto Backup (#156)</dd>
|
||||
<dd>Added new options to the proxy types</dd>
|
||||
<dd>Added QUIC (HTTP) option (Chrome only) (experimental) (#124)</dd>
|
||||
<dd>Removed a bug that caused Glob wildcard to match domains with subdomains</dd>
|
||||
<dd>Updated browser detection (firefox-extension/issues/220, #139, #141)</dd>
|
||||
<dd>Updated default network passthrough on Firefox to include <code>localhost, 127.0.0.1, ::1</code> (firefox-extension/issues/159, #20, #134)</dd>
|
||||
<dd>Updated managed storage to allow Tab Proxy (#172)</dd>
|
||||
<dd>Updated Options page user interface</dd>
|
||||
<dd>Updated Tab Proxy to include new tab (#157)</dd>
|
||||
<dd>Updated toolbar popup user interface</dd>
|
||||
|
||||
<dt>8.9</dt>
|
||||
<dd>Added "Log" to the toolbar popup buttons (#44)</dd>
|
||||
<dd>Added limited log display on Chrome (experimental)</dd>
|
||||
<dd>Added Theme feature (#71, #100)</dd>
|
||||
<dd>Added toggle more options on toolbar popup (#54)</dd>
|
||||
<dd>Fixed proxy DNS in "Import Proxy List" (#102) (from 8.7)</dd>
|
||||
<dd>Fixed settings upgrade (import older) when hostname is missing (#108)</dd>
|
||||
<dd>Fixed settings upgrade (import older) when username/password is missing (#103)</dd>
|
||||
<dd>Fixed socks in "Import Proxy List" on some Firefox (#120)</dd>
|
||||
<dd>Increased log content</dd>
|
||||
<dd>Removed "Show Pattern Proxy" option and made it default (#57) (from 8.7) (Firefox only)</dd>
|
||||
<dd>Removed Tab Proxy page-action and set it to the toolbar icon (#114) (Firefox only)</dd>
|
||||
<dd>Updated add pattern user interface (#105)</dd>
|
||||
<dd>Updated code to process duplicate hostname:port (#33, #76)</dd>
|
||||
<dd>Updated options to disable "Store Locally" on Firefox (Chrome only)</dd>
|
||||
<dd>Updated toolbar popup include/exclude host feature</dd>
|
||||
<dd>Updated toolbar popup user interface</dd>
|
||||
|
||||
<dt>8.8</dt>
|
||||
<dd>Added Show hidden feature</dd>
|
||||
<dd>Fixed an issue with sync (#99)</dd>
|
||||
<dd>Updated code to process duplicate hostname:port (#33, #76)</dd>
|
||||
<dd>Updated user interface to hide patterns on FoxyProxy Basic</dd>
|
||||
|
||||
<dt>8.7</dt>
|
||||
<dd>Added Auto Backup feature</dd>
|
||||
<dd>Added FoxyProxy Basic detection (disabled for now)</dd>
|
||||
<dd>Added Help translation form</dd>
|
||||
<dd>Added "Show Pattern Proxy" option to show proxies when in "Proxy by Patterns" mode (#57) (Firefox only)</dd>
|
||||
<dd>Added pattern matching to the Log display (#91)</dd>
|
||||
<dd>Added proxy title to the toolbar icon mouse-over title display (#74)</dd>
|
||||
<dd>Changed the global Proxy DNS to per-proxy setting (#75)</dd>
|
||||
<dd>Fixed Firefox for Android compatibility (#60)</dd>
|
||||
<dd>Increased the maximum height of the pattern section before scrolling</dd>
|
||||
<dd>Removed Help document display on install/upgrade (#86)</dd>
|
||||
<dd>Updated default Firefox proxy resetting (#59)</dd>
|
||||
<dd>Updated PAC check to allow "file:" (#49)</dd>
|
||||
<dd>Updated pattern "Add" button text due to localisation issues (#88)</dd>
|
||||
<dd>Updated schema.json</dd>
|
||||
<dd>Updated wildcard to regular expression conversion (#72)</dd>
|
||||
|
||||
<dt>8.6</dt>
|
||||
<dd>Fixed an issue with migrating database from older versions (#69)</dd>
|
||||
<dd>Updated user interface to disable inapplicable options in proxies (#52)</dd>
|
||||
<dd>Updated wildcard to regular expression conversion (#72)</dd>
|
||||
|
||||
<dt>8.5</dt>
|
||||
<dd>Skipped</dd>
|
||||
|
||||
<dt>8.4</dt>
|
||||
<dd>Added light/dark theme detection for badge background color (#61)</dd>
|
||||
<dd>Enabled to run with "controlled_by_other_extensions" on Firefox (#68)</dd>
|
||||
<dd>Removed localhost and local network passthrough in Firefox (#50, #63, #64, #66, #71)</dd>
|
||||
<dd>Updated Options save process to fill blank proxy header title display (#74)</dd>
|
||||
|
||||
<dt>8.3</dt>
|
||||
<dd>Added enterprise policy and managed storage feature (experimental) (#42)</dd>
|
||||
<dd>Added PAC "Store Locally" feature (#46) (experimental) (Chrome only)</dd>
|
||||
<dd>Added PAC view feature</dd>
|
||||
<dd>Fixed an issue with empty Global Exclude</dd>
|
||||
<dd>Fixed an issue with upgrade sync data on Firefox (#53)</dd>
|
||||
<dd>Updated hostname check to allow "file:" for Unix Domain Socket (#47)</dd>
|
||||
<dd>Updated PAC check to allow "file:" (#49)</dd>
|
||||
<dd>Updated user interface to disable inapplicable options in proxies</dd>
|
||||
|
||||
<dt>8.2</dt>
|
||||
<dd>Added option to set the country to blank</dd>
|
||||
<dd>Fixed an issue with upgrade sync data on Chrome (#45)</dd>
|
||||
<dd>Updated Incognito process on Chrome</dd>
|
||||
|
||||
<dt>8.1</dt>
|
||||
<dd>Added Drag and Drop sorting of proxies (#29)</dd>
|
||||
<dd>Added duplicate proxy feature</dd>
|
||||
<dd>Added Incognito/Container proxy (firefox-extension/issues/22, #33)</dd>
|
||||
<dd>Added Keyboard Shortcut feature (firefox-extension/issues/217)</dd>
|
||||
<dd>Added pattern import/export</dd>
|
||||
<dd>Added port search to search filter</dd>
|
||||
<dd>Added search filter to toolbar popup (#23)</dd>
|
||||
<dd>Fixed a pattern conversion issue (#28)</dd>
|
||||
<dd>Fixed an error in Sync (#36)</dd>
|
||||
<dd>Fixed Tab Proxy icon when reloading a tab (#33)</dd>
|
||||
<dd>Increased log content</dd>
|
||||
<dd>Increased log display entries to 200</dd>
|
||||
<dd>Increased log width to full screen</dd>
|
||||
<dd>Updated Global Exclude process</dd>
|
||||
|
||||
<dt>8.0</dt>
|
||||
<dd>Added complete Light/Dark Theme</dd>
|
||||
<dd>Added Exclude host feature</dd>
|
||||
<dd>Added Firefox on Android support (experimental) (#21)</dd>
|
||||
<dd>Added Get Location feature</dd>
|
||||
<dd>Added Global Exclude</dd>
|
||||
<dd>Added Host Pattern to proxy feature</dd>
|
||||
<dd>Added Import FoxyProxy Account (Chrome)</dd>
|
||||
<dd>Added Import From URL</dd>
|
||||
<dd>Added Limit WebRTC feature (Foxyproxy_Chrome/issues/4)</dd>
|
||||
<dd>Added live Log (Firefox only)</dd>
|
||||
<dd>Added Tab Proxy feature (Firefox only)</dd>
|
||||
<dd>Changed "browsingData" to optional permissions</dd>
|
||||
<dd>Dropped "browser_style" in preparation for MV3</dd>
|
||||
<dd>Increased minimum version to Firefox 93 (released 2021-10-05)</dd>
|
||||
<dd>Unified code for Firefox, Chrome, and other Chromium-based browsers</dd>
|
||||
<dd>Unified storage to share between Firefox and Chrome</dd>
|
||||
<dd>Updated code and style for manifest v3 (MV3) compatibility</dd>
|
||||
<dd>Updated Import Proxy List</dd>
|
||||
<dd>Updated User Interface</dd>
|
||||
</dl>
|
||||
|
||||
<h2 id="credits">Credits</h2>
|
||||
<dl>
|
||||
<dt>Developer</dt>
|
||||
<dd><a href="https://github.com/erosman" target="_blank">erosman</a></dd>
|
||||
|
||||
<dt>Translations</dt>
|
||||
<dd><a href="https://github.com/foxyproxy/browser-extension" target="_blank">Github Public Repository</a></dd>
|
||||
<dd>es: <a href="https://github.com/LuisAlfredo92" target="_blank">Luis Alfredo Figueroa Bracamontes</a></dd>
|
||||
<dd>fa: <a href="https://github.com/axone13" target="_blank">Matin Kargar </a></dd>
|
||||
<dd>fr: <a href="https://github.com/Hugo-C" target="_blank">Hugo-C</a></dd>
|
||||
<dd>ja: <a href="https://github.com/yutayamate" target="_blank">Yuta Yamate</a></dd>
|
||||
<dd>pl: Grzegorz Koryga</dd>
|
||||
<dd>pt_BR: </dd>
|
||||
<dd>ru: <a href="https://github.com/sosiska" target="_blank">Kirill Motkov</a>, <a href="https://github.com/krolchonok" target="_blank">krolchonok</a></dd>
|
||||
<dd>uk: <a href="https://github.com/sponsors/webknjaz" target="_blank">Sviatoslav Sydorenko</a></dd>
|
||||
<dd>zh_CN: <a href="https://github.com/wsxy162" target="_blank">FeralMeow </a></dd>
|
||||
<dd>zh_TW: <a href="https://github.com/samuikaze" target="_blank">samuikaze</a></dd>
|
||||
|
||||
<dt>Founder</dt>
|
||||
<dd>
|
||||
<img class="figure" src="../image/ericjung.png" alt=""><br>
|
||||
<a href="mailto:eric.jung@getfoxyproxy.org">Eric H. Jung</a><br>
|
||||
Denver, Colorado, USA<br>
|
||||
Developer
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<p style="margin-top: 3em;">To support this free software, please please donate (<a href="https://buy.stripe.com/00gcNd1YQ2NQbKM148" target="_blank">Stripe</a>, <a href="https://www.paypal.me/ericjung2/5.99" target="_blank">PayPal</a>) or
|
||||
<a href="https://getfoxyproxy.org/order/" target="_blank">buy dedicated VPN/Proxy Servers</a> in over 100 countries.<br>
|
||||
<i>(including such remote places like <a href="https://wikipedia.org/wiki/Réunion" target="_blank">Reunion Island</a>)</i>
|
||||
</p>
|
||||
|
||||
<p style="font: 1.5em cursive;">Thank you for using FoxyProxy!</p>
|
||||
<img src="../image/logo.svg" style="width: 5em;" alt="">
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
// import {Flag} from './flag.js';
|
||||
import {Location} from './location.js';
|
||||
|
||||
export class Action {
|
||||
|
||||
// https://github.com/w3c/webextensions/issues/72#issuecomment-1848874359
|
||||
// 'prefers-color-scheme' detection in Chrome background service worker
|
||||
static dark = false;
|
||||
|
||||
static set(pref) {
|
||||
// --- set action/browserAction
|
||||
let title = '';
|
||||
let text = '';
|
||||
let color = this.dark ? '#444' : '#fff';
|
||||
switch (pref.mode) {
|
||||
case 'disable':
|
||||
title = browser.i18n.getMessage('disable');
|
||||
text = '⛔';
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
title = 'DIRECT';
|
||||
text = '⮕';
|
||||
break;
|
||||
|
||||
case 'pattern':
|
||||
title = browser.i18n.getMessage('proxyByPatterns');
|
||||
text = '🌐';
|
||||
break;
|
||||
|
||||
default:
|
||||
const item = pref.data.find(i => pref.mode === (i.type === 'pac' ? i.pac : `${i.hostname}:${i.port}`));
|
||||
if (item) {
|
||||
// Chrome 113-114 started having a bug showing unicode flags
|
||||
// const flag = Flag.get(item.cc);
|
||||
// const host = flag + ' ' + [item.hostname, item.port].filter(Boolean).join(':');
|
||||
const host = [item.hostname, item.port].filter(Boolean).join(':');
|
||||
title = [item.title, host, item.city, Location.get(item.cc)].filter(Boolean).join('\n');
|
||||
// text = item.cc ? flag : item.hostname;
|
||||
text = item.title || item.hostname;
|
||||
color = item.color;
|
||||
}
|
||||
}
|
||||
|
||||
browser.action.setBadgeBackgroundColor({color});
|
||||
browser.action.setTitle({title});
|
||||
browser.action.setBadgeText({text});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// ---------- Polyfill (Side Effect) -----------------------
|
||||
// Promise based 'browser' namespace is used to avoid conflict
|
||||
// Firefox 'chrome' API: MV2 callback | MV3 promise
|
||||
// Firefox/Edge: browser namespace | Chrome/Opera: chrome namespace
|
||||
globalThis.browser ??= chrome;
|
||||
|
||||
// ---------- Default Preferences --------------------------
|
||||
export const pref = {
|
||||
mode: 'disable',
|
||||
sync: false,
|
||||
autoBackup: false,
|
||||
passthrough: '',
|
||||
theme: '',
|
||||
container: {},
|
||||
commands: {},
|
||||
data: []
|
||||
};
|
||||
// ---------- /Default Preferences -------------------------
|
||||
|
||||
// ---------- App ------------------------------------------
|
||||
export class App {
|
||||
|
||||
// https://github.com/foxyproxy/firefox-extension/issues/220
|
||||
// navigator.userAgent identification fails in custom userAgent and browser forks
|
||||
// Chrome does not support runtime.getBrowserInfo()
|
||||
// getURL: moz-extension: | chrome-extension: | safari-web-extension:
|
||||
static firefox = browser.runtime.getURL('').startsWith('moz-extension:');
|
||||
static basic = browser.runtime.getManifest().name === browser.i18n.getMessage('extensionNameBasic');
|
||||
static android = navigator.userAgent.includes('Android');
|
||||
|
||||
// ---------- User Preferences ---------------------------
|
||||
// not syncing mode & sync (to have a choice), data (will be broken into parts)
|
||||
static syncProperties = Object.keys(pref).filter(i => !['mode', 'sync', 'data'].includes(i));
|
||||
|
||||
static defaultPref = JSON.stringify(pref);
|
||||
|
||||
static getDefaultPref() {
|
||||
return JSON.parse(this.defaultPref);
|
||||
}
|
||||
|
||||
static getPref() {
|
||||
// update pref with the saved version
|
||||
return browser.storage.local.get().then(result => {
|
||||
Object.keys(result).forEach(i => pref[i] = result[i]);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Helper functions ---------------------------
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=478654
|
||||
// Add support for SVG images in Web Notifications API -> CH107
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1353252
|
||||
// svg broken from bg service worker
|
||||
static notify(message, title = browser.i18n.getMessage('extensionName'), id = '') {
|
||||
browser.notifications.create(id, {
|
||||
type: 'basic',
|
||||
iconUrl: '/image/icon48.png',
|
||||
title,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
static equal(a, b) {
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
static parseURL(url) {
|
||||
// rebuild file://
|
||||
url.startsWith('file://') && (url = 'http' + url.substring(4));
|
||||
|
||||
try { url = new URL(url); }
|
||||
catch (error) {
|
||||
alert(`${url} ➜ ${error.message}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
// check protocol
|
||||
if (!['http:', 'https:', 'file:'].includes(url.protocol)) {
|
||||
alert(`${url} ➜ Unsupported Protocol ${url.protocol}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
static allowedTabProxy(url) {
|
||||
return /^https?:\/\/.+|^about:(blank|newtab)$/.test(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// webRequest.onAuthRequired: Firefox HTTP/HTTPS/WS/WSS | Chrome: HTTP/HTTPS
|
||||
// 'webRequestAuthProvider' permission Chrome 108, Firefox 126
|
||||
|
||||
export class Authentication {
|
||||
|
||||
static {
|
||||
this.data = {};
|
||||
// prevent bad authentication loop
|
||||
this.pending = {};
|
||||
// webRequest.onAuthRequired is only called for HTTP and HTTPS/TLS proxy servers
|
||||
const urls = ['<all_urls>'];
|
||||
browser.webRequest.onAuthRequired.addListener(e => this.process(e), {urls}, ['blocking']);
|
||||
browser.webRequest.onCompleted.addListener(e => this.clearPending(e), {urls});
|
||||
browser.webRequest.onErrorOccurred.addListener(e => this.clearPending(e), {urls});
|
||||
}
|
||||
|
||||
static init(data) {
|
||||
// reset data
|
||||
this.data = {};
|
||||
data.forEach(i => {
|
||||
const {hostname, port, username, password} = i;
|
||||
hostname && port && username && password &&
|
||||
(this.data[`${hostname}:${port}`] = {username, password});
|
||||
});
|
||||
}
|
||||
|
||||
static process(e) {
|
||||
// true for Proxy-Authenticate, false for WWW-Authenticate
|
||||
if (!e.isProxy) { return; }
|
||||
|
||||
// sending message to log.js
|
||||
browser.runtime.sendMessage({id: 'onAuthRequired', e});
|
||||
|
||||
// already sent once and pending
|
||||
if (this.pending[e.requestId]) {
|
||||
return {cancel: true};
|
||||
}
|
||||
|
||||
const {host, port} = e.challenger;
|
||||
const authCredentials = this.data[`${host}:${port}`];
|
||||
if (authCredentials) {
|
||||
// prevent bad authentication loop
|
||||
this.pending[e.requestId] = 1;
|
||||
return {authCredentials};
|
||||
}
|
||||
}
|
||||
|
||||
static clearPending(e) {
|
||||
delete this.pending[e.requestId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {Sync} from "./sync.js";
|
||||
import {Migrate} from './migrate.js';
|
||||
import {Proxy} from './proxy.js';
|
||||
import './commands.js';
|
||||
|
||||
// ---------- Process Preferences --------------------------
|
||||
class ProcessPref {
|
||||
|
||||
static {
|
||||
this.init();
|
||||
}
|
||||
|
||||
static async init() {
|
||||
const pref = await browser.storage.local.get();
|
||||
|
||||
// storage sync -> local update
|
||||
await Sync.get(pref);
|
||||
|
||||
// migrate after storage sync check
|
||||
await Migrate.init(pref);
|
||||
|
||||
// set proxy
|
||||
Proxy.set(pref);
|
||||
|
||||
// add listener after migrate
|
||||
Sync.init(pref);
|
||||
}
|
||||
}
|
||||
// ---------- /Process Preferences -------------------------
|
||||
|
||||
// ---------- Initialisation -------------------------------
|
||||
// browser.runtime.onInstalled.addListener(e => {
|
||||
// // show help
|
||||
// ['install', 'update'].includes(e.reason) && browser.tabs.create({url: '/content/help.html'});
|
||||
// });
|
||||
@@ -0,0 +1,35 @@
|
||||
import {App} from './app.js';
|
||||
|
||||
// ---------- browsingData (Side Effect) -------------------
|
||||
class BrowsingData {
|
||||
|
||||
static {
|
||||
document.querySelector('#deleteBrowsingData').addEventListener('click', () => this.process());
|
||||
this.init();
|
||||
}
|
||||
|
||||
static async init() {
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/permissions/request
|
||||
// Any permissions granted are retained by the extension, even over upgrade and disable/enable cycling.
|
||||
// check if permission is granted
|
||||
this.permission = await browser.permissions.contains({permissions: ['browsingData']});
|
||||
}
|
||||
|
||||
static async process() {
|
||||
if (!this.permission) {
|
||||
// request permission
|
||||
// Chrome appears to return true, granted silently without a popup prompt
|
||||
this.permission = await browser.permissions.request({permissions: ['browsingData']});
|
||||
if (!this.permission) { return; }
|
||||
}
|
||||
|
||||
if (!confirm(browser.i18n.getMessage('deleteBrowsingDataConfirm'))) { return; }
|
||||
|
||||
browser.browsingData.remove({}, {
|
||||
cookies: true,
|
||||
indexedDB: true,
|
||||
localStorage: true
|
||||
})
|
||||
.catch(error => App.notify(browser.i18n.getMessage('deleteBrowsingData') + '\n\n' + error.message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// ---------- Bulk Edit (Side Effect) ----------------------
|
||||
class BulkEdit {
|
||||
|
||||
static {
|
||||
const div = document.querySelector('.bulk-edit');
|
||||
[this.t1, this.t2, this.s1, this.s2, this.select] = div.children;
|
||||
|
||||
this.t1.addEventListener('change', () => this.toggleProxy('t1'));
|
||||
this.s1.addEventListener('change', () => this.toggleProxy('s1'));
|
||||
this.t2.addEventListener('change', () => this.togglePattern());
|
||||
this.select.addEventListener('change', () => this.process());
|
||||
}
|
||||
|
||||
static toggleProxy(i) {
|
||||
// remove previous selection
|
||||
document.querySelector(`details.proxy.${i}`)?.classList.remove(i);
|
||||
const n = this.getNumber(i);
|
||||
if (!n) { return; }
|
||||
|
||||
document.querySelector(`details.proxy:nth-of-type(${n})`)?.classList.add(i);
|
||||
|
||||
// reselect t2
|
||||
this.togglePattern();
|
||||
}
|
||||
|
||||
static togglePattern() {
|
||||
// remove previous selection
|
||||
const prev = document.querySelector('.pattern-row.t2');
|
||||
if (prev) {
|
||||
prev.classList.remove('t2');
|
||||
prev.closest('details').open = false;
|
||||
}
|
||||
|
||||
const n = this.getNumber('t2');
|
||||
if (!n) { return; }
|
||||
|
||||
const t = this.getNumber('t1') || this.getNumber('s1');
|
||||
if (!t) { return; }
|
||||
|
||||
const elem = document.querySelector(`details.proxy:nth-of-type(${t}) .pattern-row:nth-of-type(${n})`);
|
||||
if (elem) {
|
||||
elem.classList.add('t2');
|
||||
elem.closest('details').open = true;
|
||||
}
|
||||
}
|
||||
|
||||
static process() {
|
||||
if (!this.select.value) { return; }
|
||||
|
||||
const id = this.select.value;
|
||||
switch (id) {
|
||||
case 'openAll':
|
||||
case 'closeAll':
|
||||
this.getProxies().forEach(i => i.open = id === 'openAll');
|
||||
break;
|
||||
|
||||
case 'setType':
|
||||
case 'setPort':
|
||||
case 'setTitle':
|
||||
case 'setUsername':
|
||||
case 'setPassword':
|
||||
let s2 = this.s2.value.trim();
|
||||
if (!s2) { break; }
|
||||
|
||||
const ref = id.substring(3).toLowerCase();
|
||||
if (ref === 'type') {
|
||||
s2 = s2.toLowerCase();
|
||||
if (!['http', 'https', 'socks4', 'socks5', 'quic', 'pac', 'direct'].includes(s2)) { break; }
|
||||
}
|
||||
|
||||
document.querySelectorAll(`[data-id="${ref}"]`).forEach(i =>
|
||||
s2.startsWith('+') ? i.value += s2.substring(1) : i.value = s2);
|
||||
break;
|
||||
|
||||
case 'deleteProxy':
|
||||
this.deleteProxy();
|
||||
this.reset();
|
||||
break;
|
||||
|
||||
case 'moveProxy':
|
||||
this.moveProxy();
|
||||
this.reset();
|
||||
break;
|
||||
|
||||
case 'movePattern':
|
||||
this.movePattern();
|
||||
this.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// --- reset
|
||||
this.select.selectedIndex = 0;
|
||||
}
|
||||
|
||||
static reset() {
|
||||
document.querySelectorAll('details.proxy:is(.t1, .s1), .pattern-row.t2').forEach(i =>
|
||||
i.classList.remove('t1', 't2', 's1'));
|
||||
// ['t1', 't2', 's1'].forEach(i => this[i].value = '');
|
||||
}
|
||||
|
||||
static getProxies() {
|
||||
return document.querySelectorAll('details.proxy');
|
||||
}
|
||||
|
||||
static getNumber(i) {
|
||||
return this[i].checkValidity() && this[i].value ? this[i].value : null;
|
||||
}
|
||||
|
||||
static getSourceNumbers() {
|
||||
const n = this.s2.value.match(/\d+-\d+|\d+/g);
|
||||
if (!n) { return; }
|
||||
|
||||
let arr = [];
|
||||
n.forEach(i => {
|
||||
// check if number range e.g. 5-8
|
||||
const [a, b] = i.split('-');
|
||||
b ? arr.push(...Array.from({length: b - a + 1}, (_, i) => (a * 1) + i)) : arr.push(a);
|
||||
});
|
||||
|
||||
// map to index (-1), sort, remove duplicates
|
||||
arr = [...new Set(arr.map(i => i - 1).sort((a, b) => a - b))];
|
||||
return arr.length ? arr : null;
|
||||
}
|
||||
|
||||
static deleteProxy() {
|
||||
const n = this.getSourceNumbers();
|
||||
if (!n) { return; }
|
||||
|
||||
const p = this.getProxies();
|
||||
n.forEach(i => p[i]?.remove());
|
||||
}
|
||||
|
||||
static moveProxy() {
|
||||
let n = this.getSourceNumbers();
|
||||
if (!n) { return; }
|
||||
|
||||
const t1 = this.t1.value - 1;
|
||||
const p = this.getProxies();
|
||||
|
||||
// filter target, map to elements, filter non-existing
|
||||
n = n.filter(i => i !== t1).map(i => p[i]).filter(Boolean);
|
||||
if (!n[0]) { return; }
|
||||
|
||||
// before target or after all
|
||||
p[t1] ? p[t1].before(...n) : p[0].parentElement.append(...n);
|
||||
}
|
||||
|
||||
static movePattern() {
|
||||
const t1 = this.t1.value - 1;
|
||||
const s1 = this.s1.value - 1;
|
||||
|
||||
switch (true) {
|
||||
// move withing the same proxy
|
||||
case t1 === -1 || t1 === s1:
|
||||
s1 !== -1 && this.movePatternWithin(s1);
|
||||
break;
|
||||
|
||||
// move all patterns to target
|
||||
case s1 === -1:
|
||||
this.movePatternAll(t1);
|
||||
break;
|
||||
|
||||
// move source patterns to target
|
||||
default:
|
||||
this.movePatternSome(t1, s1);
|
||||
}
|
||||
}
|
||||
|
||||
static movePatternWithin(s1) {
|
||||
let n = this.getSourceNumbers();
|
||||
if (!n) { return; }
|
||||
|
||||
const p = this.getProxies();
|
||||
if (!p[s1]) { return; }
|
||||
|
||||
const t2 = this.t2.value - 1;
|
||||
|
||||
// filter target, map to elements, filter non-existing
|
||||
const pat = p[s1].querySelectorAll('.pattern-row');
|
||||
n = n.filter(i => i !== t2).map(i => pat[i]).filter(Boolean);
|
||||
if (!n[0]) { return; }
|
||||
|
||||
pat[t2] ? pat[t2].before(...n) : pat[0].parentElement.append(...n);
|
||||
}
|
||||
|
||||
static movePatternAll(t1) {
|
||||
const n = this.getSourceNumbers();
|
||||
if (!n) { return; }
|
||||
|
||||
const p = this.getProxies();
|
||||
if (!p[t1]) { return; }
|
||||
|
||||
// filter target, map to elements
|
||||
const pat = [];
|
||||
n.filter(i => i !== t1).forEach(i => p[i] && pat.push(...p[i].querySelectorAll('.pattern-row')));
|
||||
|
||||
const target = p[t1].querySelector('.pattern-box');
|
||||
const row = target.children?.[this.t2.value - 1];
|
||||
row ? row.before(...pat) : target.append(...pat);
|
||||
}
|
||||
|
||||
static movePatternSome(t1, s1) {
|
||||
let n = this.getSourceNumbers();
|
||||
if (!n) { return; }
|
||||
|
||||
const p = this.getProxies();
|
||||
if (!p[t1] || !p[s1]) { return; }
|
||||
|
||||
// map to elements, filter non-existing
|
||||
const pat = p[s1].querySelectorAll('.pattern-row');
|
||||
n = n.map(i => pat[i]).filter(Boolean);
|
||||
if (!n[0]) { return; }
|
||||
|
||||
const target = p[t1].querySelector('.pattern-box');
|
||||
const row = target.children?.[this.t2.value - 1];
|
||||
row ? row.before(...n) : target.append(...n);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export class Color {
|
||||
|
||||
static getRandom() {
|
||||
return this.colors[Math.floor(Math.random() * this.colors.length)];
|
||||
}
|
||||
|
||||
static colors = [
|
||||
'#faebd7', '#00ffff', '#7fffd4', '#f5f5dc', '#ffe4c4', '#ffebcd', '#0000ff', '#8a2be2', '#a52a2a', '#deb887',
|
||||
'#5f9ea0', '#7fff00', '#d2691e', '#ff7f50', '#6495ed', '#fff8dc', '#dc143c', '#00008b', '#008b8b', '#b8860b',
|
||||
'#a9a9a9', '#006400', '#bdb76b', '#8b008b', '#556b2f', '#ff8c00', '#9932cc', '#8b0000', '#e9967a', '#8fbc8f',
|
||||
'#483d8b', '#2f4f4f', '#00ced1', '#9400d3', '#ff1493', '#00bfff', '#696969', '#1e90ff', '#b22222', '#228b22',
|
||||
'#ff00ff', '#ffd700', '#daa520', '#808080', '#008000', '#adff2f', '#ff69b4', '#cd5c5c', '#4b0082', '#f0e68c',
|
||||
'#7cfc00', '#fffacd', '#add8e6', '#f08080', '#e0ffff', '#fafad2', '#d3d3d3', '#90ee90', '#ffb6c1', '#ffa07a',
|
||||
'#20b2aa', '#87cefa', '#778899', '#b0c4de', '#00ff00', '#32cd32', '#800000', '#66cdaa', '#0000cd', '#ba55d3',
|
||||
'#9370db', '#3cb371', '#7b68ee', '#00fa9a', '#48d1cc', '#c71585', '#191970', '#ffe4e1', '#ffe4b5', '#ffdead',
|
||||
'#000080', '#fdf5e6', '#808000', '#6b8e23', '#ffa500', '#ff4500', '#da70d6', '#eee8aa', '#98fb98', '#afeeee',
|
||||
'#db7093', '#ffefd5', '#ffdab9', '#cd853f', '#ffc0cb', '#dda0dd', '#b0e0e6', '#800080', '#ff0000', '#bc8f8f',
|
||||
'#4169e1', '#8b4513', '#fa8072', '#f4a460', '#2e8b57', '#fff5ee', '#a0522d', '#87ceeb', '#6a5acd', '#708090',
|
||||
'#00ff7f', '#4682b4', '#d2b48c', '#008080', '#d8bfd8', '#ff6347', '#40e0d0', '#ee82ee', '#f5deb3', '#ffff00',
|
||||
'#9acd32'];
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/commands/onCommand
|
||||
// https://developer.chrome.com/docs/extensions/reference/commands/#event-onCommand
|
||||
// Chrome commands returns command, tab
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1843866
|
||||
// Add tab parameter to commands.onCommand (fixed in Firefox 126)
|
||||
|
||||
import {App} from './app.js';
|
||||
import {Proxy} from './proxy.js';
|
||||
import {OnRequest} from './on-request.js';
|
||||
|
||||
// ---------- Commands (Side Effect) ------------------------
|
||||
class Commands {
|
||||
|
||||
static {
|
||||
// commands is not supported on Android
|
||||
browser.commands?.onCommand.addListener((...e) => this.process(...e));
|
||||
}
|
||||
|
||||
static async process(name, tab) {
|
||||
// firefox only Tab Proxy
|
||||
const tabProxy = ['setTabProxy', 'unsetTabProxy'].includes(name);
|
||||
if (!App.firefox && tabProxy) { return; }
|
||||
|
||||
const pref = await browser.storage.local.get();
|
||||
|
||||
// only Tab Proxy allowed for storage.managed
|
||||
if (pref.managed && !tabProxy) { return; }
|
||||
|
||||
const host = pref.commands[name];
|
||||
let proxy;
|
||||
|
||||
switch (name) {
|
||||
case 'proxyByPatterns':
|
||||
this.set(pref, 'pattern');
|
||||
break;
|
||||
|
||||
case 'disable':
|
||||
this.set(pref, 'disable');
|
||||
break;
|
||||
|
||||
case 'setProxy':
|
||||
host && this.set(pref, host);
|
||||
break;
|
||||
|
||||
case 'includeHost':
|
||||
case 'excludeHost':
|
||||
if (!host) { break; }
|
||||
|
||||
proxy = this.findProxy(pref, host);
|
||||
proxy && Proxy.includeHost(pref, proxy, tab, name);
|
||||
break;
|
||||
|
||||
case 'setTabProxy':
|
||||
if (!host) { break; }
|
||||
|
||||
proxy = this.findProxy(pref, host);
|
||||
proxy && OnRequest.setTabProxy(tab, proxy);
|
||||
break;
|
||||
|
||||
case 'unsetTabProxy':
|
||||
OnRequest.setTabProxy(tab);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static findProxy(pref, host) {
|
||||
return host && pref.data.find(i => i.active && host === `${i.hostname}:${i.port}`);
|
||||
}
|
||||
|
||||
static set(pref, mode) {
|
||||
pref.mode = mode;
|
||||
// save mode
|
||||
browser.storage.local.set({mode});
|
||||
// set proxy without menus update
|
||||
Proxy.set(pref, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/* ----- Light Theme ----- */
|
||||
:root {
|
||||
--color: #000;
|
||||
--bg: #fff;
|
||||
--alt-bg: #f5f5f5;
|
||||
--hover: #eaeaea;
|
||||
--highlight: #f90;
|
||||
|
||||
--body-bg: #630;
|
||||
--header: #c60;
|
||||
|
||||
--nav-bg: #420;
|
||||
--nav-hover: #851;
|
||||
--nav-color: cornsilk;
|
||||
|
||||
--btn-bg: #f90;
|
||||
--btn-hover: #e70;
|
||||
|
||||
--link: #e70;
|
||||
--border: #ddd;
|
||||
/* --shadow: #0004; */
|
||||
--dim: #777;
|
||||
--tr: #f5f5f5;
|
||||
}
|
||||
|
||||
/* ----- Dark Theme ----- */
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color: #fff;
|
||||
--bg: #420;
|
||||
--alt-bg: #666;
|
||||
--hover: #444;
|
||||
|
||||
/* --body-bg: #630; */
|
||||
--header: #e70;
|
||||
|
||||
--btn-bg: #f90;
|
||||
--btn-hover: #e70;
|
||||
|
||||
--link: #f90;
|
||||
--border: #777;
|
||||
/* --shadow: #fff8; */
|
||||
--dim: #ccc;
|
||||
--tr: #531;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----- General ----- */
|
||||
body {
|
||||
color: var(--color);
|
||||
background-color: var(--body-bg);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
select,
|
||||
textarea,
|
||||
input[type="number"],
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="url"] {
|
||||
width: 100%;
|
||||
color: inherit;
|
||||
background-color: var(--alt-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
:is(select,
|
||||
input[type="number"],
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="url"]):hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
label[for],
|
||||
input[type="checkbox"],
|
||||
summary,
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
opacity: 0.5;
|
||||
color: inherit;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.invalid,
|
||||
input:invalid {
|
||||
box-shadow: 1px 1px 4px #f20, -1px -1px 4px #f20;
|
||||
}
|
||||
|
||||
/* ----- Buttons ----- */
|
||||
button,
|
||||
label.flat {
|
||||
background-color: var(--btn-bg);
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
button.flat,
|
||||
label.flat {
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
padding: 0.4em 1em;
|
||||
min-width: 8em;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
label.flat:hover {
|
||||
background-color: var(--btn-hover);
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
select:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
button.plain {
|
||||
background-color: unset;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
display: table;
|
||||
color: #fff;
|
||||
font-size:0.9em;
|
||||
border-radius: 5px;
|
||||
padding: 0.5em 5em;
|
||||
margin: 1em auto 0;
|
||||
}
|
||||
/* ----- /Buttons ----- */
|
||||
@@ -0,0 +1,30 @@
|
||||
// ---------- Drag and Drop (Side Effect) ------------------
|
||||
class Drag {
|
||||
|
||||
static {
|
||||
this.proxyDiv = document.querySelector('div.proxy-div');
|
||||
this.proxyDiv.addEventListener("dragstart", e => this.dragstart(e));
|
||||
this.proxyDiv.addEventListener('dragover', e => this.dragover(e));
|
||||
this.proxyDiv.addEventListener('dragend', e => this.dragend(e));
|
||||
this.target = null;
|
||||
}
|
||||
|
||||
static dragstart(e) {
|
||||
if (e.target.localName === 'input') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
static dragover(e) {
|
||||
this.target = e.target.closest('details');
|
||||
}
|
||||
|
||||
static dragend(e) {
|
||||
if (!this.target) { return; }
|
||||
|
||||
const arr = [...this.proxyDiv.children];
|
||||
arr.indexOf(e.target) > arr.indexOf(this.target) ? this.target.before(e.target) : this.target.after(e.target);
|
||||
this.target = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// ---------- Unicode flag ---------------------------------
|
||||
export class Flag {
|
||||
|
||||
static get(cc) {
|
||||
cc = /^[A-Z]{2}$/i.test(cc) && cc.toUpperCase();
|
||||
return cc ? String.fromCodePoint(...[...cc].map(i => i.charCodeAt() + 127397)) : '🌎';
|
||||
}
|
||||
|
||||
static show(item) {
|
||||
switch (true) {
|
||||
case !!item.cc:
|
||||
return this.get(item.cc);
|
||||
|
||||
case item.type === 'direct':
|
||||
return '⮕';
|
||||
|
||||
case this.isLocal(item.hostname):
|
||||
return '🖥️';
|
||||
|
||||
default:
|
||||
return '🌎';
|
||||
}
|
||||
}
|
||||
|
||||
static isLocal(host) {
|
||||
// check local network
|
||||
const isIP = /^[\d.:]+$/.test(host);
|
||||
switch (true) {
|
||||
// --- localhost & <local>
|
||||
// case host === 'localhost':
|
||||
// plain hostname (no dots)
|
||||
case !host.includes('.'):
|
||||
// *.localhost
|
||||
case host.endsWith('.localhost'):
|
||||
|
||||
// --- IPv4
|
||||
// case host === '127.0.0.1':
|
||||
// 127.0.0.1 up to 127.255.255.254
|
||||
case isIP && host.startsWith('127.'):
|
||||
// 169.254.0.0/16 - 169.254.0.0 to 169.254.255.255
|
||||
case isIP && host.startsWith('169.254.'):
|
||||
// 192.168.0.0/16 - 192.168.0.0 to 192.168.255.255
|
||||
case isIP && host.startsWith('192.168.'):
|
||||
|
||||
// --- IPv6
|
||||
// case host === '[::1]':
|
||||
// literal IPv6 [::1]:80 with/without port
|
||||
case host.startsWith('[::1]'):
|
||||
// literal IPv6 [FE80::]/10
|
||||
case host.toUpperCase().startsWith('[FE80::]'):
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {Spinner} from './spinner.js';
|
||||
|
||||
// ---------- Get Location (Side Effect) -------------------
|
||||
class GetLocation {
|
||||
|
||||
static {
|
||||
this.proxyDiv = document.querySelector('div.proxy-div');
|
||||
document.querySelector('.proxy-top button[data-i18n="getLocation"]').addEventListener('click', () => this.process());
|
||||
}
|
||||
|
||||
static async process() {
|
||||
const ignore = ['127.0.0.1', 'localhost'];
|
||||
|
||||
let {data} = await browser.storage.local.get({data: []});
|
||||
data = data.filter(i => i.type !== 'direct' && !ignore.includes(i.hostname)).map(i => i.hostname);
|
||||
if (!data[0]) { return; }
|
||||
|
||||
// remove duplicates
|
||||
const hosts = [...new Set(data)];
|
||||
|
||||
Spinner.show();
|
||||
|
||||
fetch('https://getfoxyproxy.org/webservices/lookup.php?' + hosts.join('&'))
|
||||
.then(response => response.json())
|
||||
.then(json => this.updateLocation(json))
|
||||
.catch(error => {
|
||||
Spinner.hide();
|
||||
alert(error);
|
||||
});
|
||||
}
|
||||
|
||||
static updateLocation(json) {
|
||||
// update display
|
||||
this.proxyDiv.querySelectorAll('[data-id="cc"], [data-id="city"]').forEach(i => {
|
||||
const {hostname, id} = i.dataset;
|
||||
// cache old value to compare
|
||||
const value = i.value;
|
||||
json[hostname]?.[id] && (i.value = json[hostname][id]);
|
||||
// dispatch change event
|
||||
id === 'cc' && i.value !== value && i.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
Spinner.hide();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
// ---------- Internationalization (Side Effect) -----------
|
||||
class I18n {
|
||||
|
||||
static {
|
||||
document.querySelectorAll('template').forEach(i => this.set(i.content));
|
||||
this.set();
|
||||
// show after
|
||||
// document.body.style.opacity = 1;
|
||||
}
|
||||
|
||||
static set(target = document) {
|
||||
target.querySelectorAll('[data-i18n]').forEach(elem => {
|
||||
let [text, attr] = elem.dataset.i18n.split('|');
|
||||
text = browser.i18n.getMessage(text);
|
||||
attr ? elem.setAttribute(attr, text) : elem.append(text);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
@import 'default.css';
|
||||
@import 'theme.css';
|
||||
|
||||
/* ----- General ----- */
|
||||
:root {
|
||||
--nav-height: 2.5rem;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-padding-top: calc(var(--nav-height) + 0.5rem);
|
||||
}
|
||||
|
||||
body {
|
||||
/* Chrome sets font-size to 75% (16px x 75% = 12px) */
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 2em;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
|
||||
/* ----- h1-h5 ----- */
|
||||
h2 {
|
||||
color: var(--header);
|
||||
font-size: 2.5em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h2:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
:not(h2) + h3 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
:is(h1, h2, h3, h4, h5) span {
|
||||
color: var(--dim);
|
||||
font-size: 0.8em;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
/* ----- /h1-h5 ----- */
|
||||
|
||||
p {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
pre {
|
||||
border-left: 3px solid #ccc;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 0.3em;
|
||||
background-color: var(--hover);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: var(--color);
|
||||
padding: 1em 3.5em;
|
||||
font-style: italic;
|
||||
position: relative;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
blockquote::before,
|
||||
blockquote::after {
|
||||
color: #ccc;
|
||||
opacity: 0.6;
|
||||
font-size: 4em;
|
||||
position: absolute;
|
||||
content: '❝';
|
||||
top: 0;
|
||||
left: 0.1em;
|
||||
}
|
||||
|
||||
blockquote::after {
|
||||
bottom: -0.5em;
|
||||
right: 0.5em;
|
||||
}
|
||||
|
||||
cite {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
cite::before {
|
||||
content: '— source: ';
|
||||
}
|
||||
|
||||
img.figure {
|
||||
border-radius: 1em;
|
||||
border: 4px solid var(--nav-hover);
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
dd + dt {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
dd > dl {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
th span,
|
||||
dt span,
|
||||
dd span,
|
||||
li span {
|
||||
margin-left: 0.5em;
|
||||
color: var(--dim);
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
letter-spacing: normal;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
mark {
|
||||
color: var(--header);
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
max-height: 25em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ----- About ----- */
|
||||
.about dt {
|
||||
display: table;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 0.2em;
|
||||
min-width: 15vw;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* ----- /About ----- */
|
||||
|
||||
/* ----- Navigation ----- */
|
||||
nav {
|
||||
background-color: var(--bg);
|
||||
height: var(--nav-height);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
box-shadow: 0 3px 6px #0004;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--color);
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
/* ----- /Navigation ----- */
|
||||
|
||||
/* ----- Table ----- */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 1em;
|
||||
width: calc(100% - 2.5rem);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background-color: var(--alt-bg);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
tbody th {
|
||||
min-width: 10em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td pre,
|
||||
.code td,
|
||||
td.code {
|
||||
font-family: monospace;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.slim th,
|
||||
.slim td {
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
/* ----- /Table ----- */
|
||||
|
||||
/* ----- note, footnote, warning, experimental ----- */
|
||||
.note,
|
||||
.warning {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5em;
|
||||
border-left: 3px solid #17f;
|
||||
padding: 0.3em 0.5em 0.3em 2em;
|
||||
margin-top: 0.5em;
|
||||
position: relative;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.note::before,
|
||||
.warning::before {
|
||||
content: 'ⓘ';
|
||||
color: #17f;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.warning {
|
||||
border-left-color: #f90;
|
||||
}
|
||||
|
||||
.warning::before {
|
||||
content: '⚠️';
|
||||
}
|
||||
|
||||
.experimental::after {
|
||||
content: '';
|
||||
background: url('../image/beaker.svg') no-repeat center / contain;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.footnote {
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ----- span links ----- */
|
||||
.chrome-extension,
|
||||
.moz-extension {
|
||||
cursor: pointer;
|
||||
font-style: normal;
|
||||
font-size: 0.8em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ----- Translate ----- */
|
||||
.translate {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 1em 1em 0;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
.translate select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.translate input[type="submit"] {
|
||||
color: inherit;
|
||||
background-color: var(--alt-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.3em;
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
/* ----- /Translate ----- */
|
||||
@@ -0,0 +1,98 @@
|
||||
import {App} from './app.js';
|
||||
import {Proxies} from './options-proxies.js';
|
||||
import {Spinner} from './spinner.js';
|
||||
import {Toggle} from './toggle.js';
|
||||
import {Nav} from './nav.js';
|
||||
|
||||
// ---------- Import FoxyProxy Account (Side Effect) -------
|
||||
class ImportAccount {
|
||||
|
||||
static {
|
||||
this.username = document.querySelector('.import-account #username');
|
||||
this.password = document.querySelector('.import-account #password');
|
||||
Toggle.password(this.password.nextElementSibling);
|
||||
document.querySelector('.import-account button[data-i18n="import"]').addEventListener('click', () => this.process());
|
||||
}
|
||||
|
||||
static async process() {
|
||||
// --- check username/password
|
||||
const username = this.username.value.trim();
|
||||
const password = this.password.value.trim();
|
||||
if (!username || !password) {
|
||||
alert(browser.i18n.getMessage('userPassError'));
|
||||
return;
|
||||
}
|
||||
|
||||
Spinner.show();
|
||||
|
||||
const options = [...document.querySelectorAll('.import-account .account-options select')].map(i => i.value);
|
||||
// Array(3) [ "https", "hostname", "alt" ]
|
||||
const url = options.includes('alt') ?
|
||||
'https://bilestoad.com/webservices/get-accounts.php' :
|
||||
'https://getfoxyproxy.org/webservices/get-accounts.php';
|
||||
const ip = options.includes('ip');
|
||||
const socks = options.includes('socks5');
|
||||
const https = options.includes('https');
|
||||
|
||||
const proxyDiv = document.querySelector('div.proxy-div');
|
||||
const docFrag = document.createDocumentFragment();
|
||||
|
||||
// --- fetch data
|
||||
const data = await this.getAccount(url, username, password);
|
||||
if (data) {
|
||||
data.forEach(i => {
|
||||
// proxy template
|
||||
const pxy = {
|
||||
active: true,
|
||||
title: i.hostname.split('.')[0],
|
||||
type: socks ? 'socks5' : https ? 'https' : 'http',
|
||||
hostname: ip ? i.ip : i.hostname,
|
||||
port: socks ? i.socks5_port : https ? i.ssl_port : i.port[0],
|
||||
username: i.username,
|
||||
password: i.password,
|
||||
// convert UK to ISO 3166-1 GB
|
||||
cc: i.country_code === 'UK' ? 'GB' : i.country_code,
|
||||
city: i.city,
|
||||
// random color will be set
|
||||
color: '',
|
||||
pac: '',
|
||||
pacString: '',
|
||||
proxyDNS: true,
|
||||
include: [],
|
||||
exclude: [],
|
||||
tabProxy: [],
|
||||
};
|
||||
|
||||
docFrag.append(Proxies.addProxy(pxy));
|
||||
});
|
||||
|
||||
proxyDiv.append(docFrag);
|
||||
Nav.get('proxies');
|
||||
}
|
||||
|
||||
Spinner.hide();
|
||||
}
|
||||
|
||||
static async getAccount(url, username, password) {
|
||||
// --- fetch data
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!Array.isArray(data) || !data[0]?.hostname) {
|
||||
App.notify(browser.i18n.getMessage('error'));
|
||||
return;
|
||||
}
|
||||
|
||||
// import active accounts only
|
||||
data = data.filter(i => i.active === 'true');
|
||||
// sort by country
|
||||
data.sort((a, b) => a.country.localeCompare(b.country));
|
||||
return data;
|
||||
})
|
||||
.catch(error => App.notify(browser.i18n.getMessage('error') + '\n\n' + error.message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import {App} from './app.js';
|
||||
|
||||
// ---------- Import/Export Preferences --------------------
|
||||
export class ImportExport {
|
||||
|
||||
// pref references the same object in the memory and its value gets updated
|
||||
static init(pref, callback) {
|
||||
this.callback = callback;
|
||||
document.getElementById('file').addEventListener('change', e => this.import(e, pref));
|
||||
document.getElementById('export').addEventListener('click', () => this.export(pref));
|
||||
}
|
||||
|
||||
// import preferences
|
||||
static import(e, pref) {
|
||||
const file = e.target.files[0];
|
||||
switch (true) {
|
||||
case !file: App.notify(browser.i18n.getMessage('error'));
|
||||
return;
|
||||
// check file MIME type
|
||||
case !['text/plain', 'application/json'].includes(file.type):
|
||||
App.notify(browser.i18n.getMessage('fileTypeError'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileReader(file, r => this.readData(r, pref));
|
||||
}
|
||||
|
||||
static readData(data, pref) {
|
||||
try { data = JSON.parse(data); }
|
||||
catch {
|
||||
// display the error
|
||||
App.notify(browser.i18n.getMessage('fileParseError'));
|
||||
return;
|
||||
}
|
||||
|
||||
// update pref with the saved version
|
||||
Object.keys(pref).forEach(i => Object.hasOwn(data, i) && (pref[i] = data[i]));
|
||||
|
||||
// successful import
|
||||
this.callback();
|
||||
}
|
||||
|
||||
// export preferences
|
||||
static export(pref, saveAs = true, folder = '') {
|
||||
const data = JSON.stringify(pref, null, 2);
|
||||
const filename = `${folder}${browser.i18n.getMessage('extensionName')}_${new Date().toISOString().substring(0, 10)}.json`;
|
||||
this.saveFile({data, filename, type: 'application/json', saveAs});
|
||||
}
|
||||
|
||||
static saveFile({data, filename, saveAs = true, type = 'text/plain'}) {
|
||||
if (!browser.downloads) {
|
||||
const a = document.createElement('a');
|
||||
a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(data);
|
||||
a.setAttribute('download', filename);
|
||||
a.dispatchEvent(new MouseEvent('click'));
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([data], {type});
|
||||
browser.downloads.download({
|
||||
url: URL.createObjectURL(blob),
|
||||
filename,
|
||||
saveAs,
|
||||
conflictAction: 'uniquify'
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
.catch(console.log);
|
||||
}
|
||||
|
||||
static fileReader(file, callback) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => callback(reader.result);
|
||||
reader.onerror = () => App.notify(browser.i18n.getMessage('fileReadError'));
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import {Proxies} from './options-proxies.js';
|
||||
import {Nav} from './nav.js';
|
||||
|
||||
// ---------- Import List (Side Effect) --------------------
|
||||
class ImportList {
|
||||
|
||||
static {
|
||||
this.textarea = document.querySelector('.import-proxy-list textarea');
|
||||
document.querySelector('.import-proxy-list button').addEventListener('click', () => this.process());
|
||||
}
|
||||
|
||||
static process() {
|
||||
this.textarea.value = this.textarea.value.trim();
|
||||
if (!this.textarea.value) { return; }
|
||||
|
||||
const proxyDiv = document.querySelector('div.proxy-div');
|
||||
const docFrag = document.createDocumentFragment();
|
||||
|
||||
for (const item of this.textarea.value.split(/\n+/)) {
|
||||
// simple vs Extended format
|
||||
const pxy = item.includes('://') ? this.parseExtended(item) : this.parseSimple(item);
|
||||
// end on error
|
||||
if (!pxy) { return; }
|
||||
|
||||
docFrag.append(Proxies.addProxy(pxy));
|
||||
}
|
||||
|
||||
proxyDiv.append(docFrag);
|
||||
Nav.get('proxies');
|
||||
}
|
||||
|
||||
static parseSimple(item) {
|
||||
// example.com:3128:user:pass
|
||||
const [hostname, port, username = '', password = ''] = item.split(':');
|
||||
if (!hostname || !(port * 1)) {
|
||||
alert(`Error: ${item}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = port === '443' ? 'https' : 'http';
|
||||
|
||||
// proxy template
|
||||
const pxy = {
|
||||
active: true,
|
||||
title: '',
|
||||
type,
|
||||
hostname,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
cc: '',
|
||||
city: '',
|
||||
color: '',
|
||||
pac: '',
|
||||
pacString: '',
|
||||
proxyDNS: true,
|
||||
include: [],
|
||||
exclude: [],
|
||||
tabProxy: [],
|
||||
};
|
||||
|
||||
return pxy;
|
||||
}
|
||||
|
||||
static parseExtended(item) {
|
||||
// https://user:password@78.205.12.1:21?color=ff00bc&title=work%20proxy
|
||||
// https://example.com:443?active=false&title=Work&username=abcd&password=1234&cc=US&city=Miami
|
||||
let url;
|
||||
try { url = new URL(item); }
|
||||
catch (error) {
|
||||
alert(`${error}\n\n${item}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// convert old schemes to type
|
||||
let type = url.protocol.slice(0, -1);
|
||||
const scheme = {
|
||||
proxy: 'http',
|
||||
ssl: 'https',
|
||||
socks: 'socks5',
|
||||
};
|
||||
scheme[type] && (type = scheme[type]);
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1851426
|
||||
// Reland URL: protocol setter needs to be more restrictive around file (fixed in Firefox 120)
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1603699
|
||||
// Enable DefaultURI use for unknown schemes (fixed in Firefox 122)
|
||||
// missing hostname, port with socks protocol (#120)
|
||||
!url.hostname && (url = new URL('http:' + item.substring(url.protocol.length)));
|
||||
|
||||
const {hostname, port, username, password} = url;
|
||||
// set to pram, can be overridden in searchParams
|
||||
const pram = {type, hostname, port, username, password};
|
||||
|
||||
// prepare object, make parameter keys case-insensitive
|
||||
for (const [key, value] of url.searchParams) {
|
||||
pram[key.toLowerCase()] = value;
|
||||
}
|
||||
|
||||
// fix missing default port
|
||||
const defaultPort = {
|
||||
http: '80',
|
||||
https: '443',
|
||||
ws: '80',
|
||||
wss: '443'
|
||||
};
|
||||
!pram.port && defaultPort[type] && (pram.port = defaultPort[type]);
|
||||
|
||||
// proxy template
|
||||
const pxy = {
|
||||
// defaults to true
|
||||
active: pram.active !== 'false',
|
||||
title: pram.title || '',
|
||||
type: pram.type.toLowerCase(),
|
||||
hostname: pram.hostname,
|
||||
port: pram.port,
|
||||
username: decodeURIComponent(pram.username),
|
||||
password: decodeURIComponent(pram.password),
|
||||
cc: (pram.cc || pram.countrycode || '').toUpperCase(),
|
||||
city: pram.city || '',
|
||||
color: pram.color ? '#' + pram.color : '',
|
||||
pac: pram.pac || (pram.type === 'pac' && url.origin + url.pathname) || '',
|
||||
pacString: '',
|
||||
// defaults to true
|
||||
proxyDNS: pram.proxydns !== 'false',
|
||||
include: [],
|
||||
exclude: [],
|
||||
tabProxy: [],
|
||||
};
|
||||
|
||||
return pxy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {App} from './app.js';
|
||||
import {Migrate} from './migrate.js';
|
||||
|
||||
// ---------- Import Older Preferences ---------------------
|
||||
export class ImportOlder {
|
||||
|
||||
// pref references the same object in the memory and its value gets updated
|
||||
static init(pref, callback) {
|
||||
this.callback = callback;
|
||||
document.querySelector('.import-from-older input').addEventListener('change', e => this.process(e, pref));
|
||||
}
|
||||
|
||||
static process(e, pref) {
|
||||
const file = e.target.files[0];
|
||||
switch (true) {
|
||||
case !file: App.notify(browser.i18n.getMessage('error')); return;
|
||||
// check file MIME type
|
||||
case !['text/plain', 'application/json'].includes(file.type):
|
||||
App.notify(browser.i18n.getMessage('fileTypeError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => this.parseJSON(reader.result, pref);
|
||||
reader.onerror = () => App.notify(browser.i18n.getMessage('fileReadError'));
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
static parseJSON(data, pref) {
|
||||
try { data = JSON.parse(data); }
|
||||
catch {
|
||||
// display the error
|
||||
App.notify(browser.i18n.getMessage('fileParseError'));
|
||||
return;
|
||||
}
|
||||
|
||||
data = Object.hasOwn(data, 'settings') ? Migrate.convert3(data) : Migrate.convert7(data);
|
||||
// update pref with the saved version
|
||||
Object.keys(pref).forEach(i => Object.hasOwn(data, i) && (pref[i] = data[i]));
|
||||
|
||||
this.callback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {App} from './app.js';
|
||||
import {Spinner} from './spinner.js';
|
||||
|
||||
// ---------- Import from URL ------------------------------
|
||||
export class ImportUrl {
|
||||
|
||||
static {
|
||||
this.input = document.querySelector('.import-from-url input');
|
||||
}
|
||||
|
||||
// pref references the same object in the memory and its value gets updated
|
||||
static init(pref, callback) {
|
||||
this.callback = callback;
|
||||
document.querySelector('.import-from-url button').addEventListener('click', () => this.process(pref));
|
||||
}
|
||||
|
||||
static process(pref) {
|
||||
this.input.value = this.input.value.trim();
|
||||
if (!this.input.value) { return; }
|
||||
|
||||
Spinner.show();
|
||||
|
||||
// --- fetch data
|
||||
fetch(this.input.value)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// update pref with the saved version
|
||||
Object.keys(pref).forEach(i => Object.hasOwn(data, i) && (pref[i] = data[i]));
|
||||
|
||||
this.callback();
|
||||
Spinner.hide();
|
||||
})
|
||||
.catch(error => {
|
||||
App.notify(browser.i18n.getMessage('error') + '\n\n' + error.message);
|
||||
Spinner.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import {App} from './app.js';
|
||||
|
||||
// ---------- Incognito Access (Side Effect) ---------------
|
||||
class IncognitoAccess {
|
||||
|
||||
static {
|
||||
// https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/proxy/settings
|
||||
// Changing proxy settings requires private browsing window access because proxy settings affect private and non-private windows.
|
||||
// https://github.com/w3c/webextensions/issues/429
|
||||
// Inconsistency: incognito in proxy.settings
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1725981
|
||||
// proxy.settings is not supported on Android
|
||||
App.firefox && !App.android && browser.extension.isAllowedIncognitoAccess()
|
||||
.then(response => !response && alert(browser.i18n.getMessage('incognitoAccessError')));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
export class Location {
|
||||
|
||||
static get(cc = '') {
|
||||
return this.countryCode[cc] || '';
|
||||
}
|
||||
|
||||
// ISO 3166-1 country code
|
||||
static countryCode = {
|
||||
AA: "",
|
||||
AD: "Andorra",
|
||||
AE: "United Arab Emirates",
|
||||
AF: "Afghanistan",
|
||||
AG: "Antigua and Barbuda",
|
||||
AI: "Anguilla",
|
||||
AL: "Albania",
|
||||
AM: "Armenia",
|
||||
AO: "Angola",
|
||||
AQ: "Antarctica",
|
||||
AR: "Argentina",
|
||||
AS: "American Samoa",
|
||||
AT: "Austria",
|
||||
AU: "Australia",
|
||||
AW: "Aruba",
|
||||
AX: "Åland Islands",
|
||||
AZ: "Azerbaijan",
|
||||
BA: "Bosnia and Herzegovina",
|
||||
BB: "Barbados",
|
||||
BD: "Bangladesh",
|
||||
BE: "Belgium",
|
||||
BF: "Burkina Faso",
|
||||
BG: "Bulgaria",
|
||||
BH: "Bahrain",
|
||||
BI: "Burundi",
|
||||
BJ: "Benin",
|
||||
BL: "Saint Barthélemy",
|
||||
BM: "Bermuda",
|
||||
BN: "Brunei Darussalam",
|
||||
BO: "Bolivia, Plurinational State of",
|
||||
BQ: "Bonaire, Sint Eustatius and Saba",
|
||||
BR: "Brazil",
|
||||
BS: "Bahamas",
|
||||
BT: "Bhutan",
|
||||
BV: "Bouvet Island",
|
||||
BW: "Botswana",
|
||||
BY: "Belarus",
|
||||
BZ: "Belize",
|
||||
CA: "Canada",
|
||||
CC: "Cocos [Keeling] Islands",
|
||||
CD: "Congo, the Democratic Republic of the",
|
||||
CF: "Central African Republic",
|
||||
CG: "Congo, Republic of the",
|
||||
CH: "Switzerland",
|
||||
CI: "Ivory Coast (Côte d'Ivoire)",
|
||||
CK: "Cook Islands",
|
||||
CL: "Chile",
|
||||
CM: "Cameroon",
|
||||
CN: "China",
|
||||
CO: "Colombia",
|
||||
CR: "Costa Rica",
|
||||
CU: "Cuba",
|
||||
CV: "Cabo Verde",
|
||||
CW: "Curaçao",
|
||||
CX: "Christmas Island",
|
||||
CY: "Cyprus",
|
||||
CZ: "Czechia",
|
||||
DE: "Germany",
|
||||
DJ: "Djibouti",
|
||||
DK: "Denmark",
|
||||
DM: "Dominica",
|
||||
DO: "Dominican Republic",
|
||||
DZ: "Algeria",
|
||||
EC: "Ecuador",
|
||||
EE: "Estonia",
|
||||
EG: "Egypt",
|
||||
EH: "Western Sahara",
|
||||
ER: "Eritrea",
|
||||
ES: "Spain",
|
||||
ET: "Ethiopia",
|
||||
EU: "European Union",
|
||||
FI: "Finland",
|
||||
FJ: "Fiji",
|
||||
FK: "Falkland Islands (Malvinas)",
|
||||
FM: "Micronesia, Federated States of",
|
||||
FO: "Faroe Islands",
|
||||
FR: "France",
|
||||
GA: "Gabon",
|
||||
GB: "United Kingdom",
|
||||
GD: "Grenada",
|
||||
GE: "Georgia",
|
||||
GF: "French Guiana",
|
||||
GG: "Guernsey",
|
||||
GH: "Ghana",
|
||||
GI: "Gibraltar",
|
||||
GL: "Greenland",
|
||||
GM: "Gambia",
|
||||
GN: "Guinea",
|
||||
GP: "Guadeloupe",
|
||||
GQ: "Equatorial Guinea",
|
||||
GR: "Greece",
|
||||
GS: "South Georgia and the South Sandwich Islands",
|
||||
GT: "Guatemala",
|
||||
GU: "Guam",
|
||||
GW: "Guinea-Bissau",
|
||||
GY: "Guyana",
|
||||
HK: "Hong Kong",
|
||||
HM: "Heard Island and McDonald Islands",
|
||||
HN: "Honduras",
|
||||
HR: "Croatia",
|
||||
HT: "Haiti",
|
||||
HU: "Hungary",
|
||||
ID: "Indonesia",
|
||||
IE: "Ireland",
|
||||
IL: "Israel",
|
||||
IM: "Isle of Man",
|
||||
IN: "India",
|
||||
IO: "British Indian Ocean Territory",
|
||||
IQ: "Iraq",
|
||||
IR: "Iran, Islamic Republic Of",
|
||||
IS: "Iceland",
|
||||
IT: "Italy",
|
||||
JE: "Jersey",
|
||||
JM: "Jamaica",
|
||||
JO: "Jordan (Hashemite Kingdom of Jordan)",
|
||||
JP: "Japan",
|
||||
KE: "Kenya",
|
||||
KG: "Kyrgyzstan",
|
||||
KH: "Cambodia",
|
||||
KI: "Kiribati",
|
||||
KM: "Comoros",
|
||||
KN: "St Kitts and Nevis",
|
||||
KP: "North Korea",
|
||||
KR: "South Korea",
|
||||
KW: "Kuwait",
|
||||
KY: "Cayman Islands",
|
||||
KZ: "Kazakhstan",
|
||||
LA: "Laos (Lao People's Democratic Republic)",
|
||||
LB: "Lebanon",
|
||||
LC: "Saint Lucia",
|
||||
LI: "Liechtenstein",
|
||||
LK: "Sri Lanka",
|
||||
LR: "Liberia",
|
||||
LS: "Lesotho",
|
||||
LT: "Republic of Lithuania",
|
||||
LU: "Luxembourg",
|
||||
LV: "Latvia",
|
||||
LY: "Libya",
|
||||
MA: "Morocco",
|
||||
MC: "Monaco",
|
||||
MD: "Moldova, Republic of",
|
||||
ME: "Montenegro",
|
||||
MF: "Saint Martin (French part)",
|
||||
MG: "Madagascar",
|
||||
MH: "Marshall Islands",
|
||||
MK: "North Macedonia",
|
||||
ML: "Mali",
|
||||
MM: "Myanmar",
|
||||
MN: "Mongolia",
|
||||
MO: "Macao",
|
||||
MP: "Northern Mariana Islands",
|
||||
MQ: "Martinique",
|
||||
MR: "Mauritania",
|
||||
MS: "Montserrat",
|
||||
MT: "Malta",
|
||||
MU: "Mauritius",
|
||||
MV: "Maldives",
|
||||
MW: "Malawi",
|
||||
MX: "Mexico",
|
||||
MY: "Malaysia",
|
||||
MZ: "Mozambique",
|
||||
NA: "Namibia",
|
||||
NC: "New Caledonia",
|
||||
NE: "Niger",
|
||||
NF: "Norfolk Island",
|
||||
NG: "Nigeria",
|
||||
NI: "Nicaragua",
|
||||
NL: "Netherlands",
|
||||
NO: "Norway",
|
||||
NP: "Nepal",
|
||||
NR: "Nauru",
|
||||
NU: "Niue",
|
||||
NZ: "New Zealand",
|
||||
OM: "Oman",
|
||||
PA: "Panama",
|
||||
PE: "Peru",
|
||||
PF: "French Polynesia",
|
||||
PG: "Papua New Guinea",
|
||||
PH: "Philippines",
|
||||
PK: "Pakistan",
|
||||
PL: "Poland",
|
||||
PM: "Saint Pierre and Miquelon",
|
||||
PN: "Pitcairn Islands",
|
||||
PR: "Puerto Rico",
|
||||
PS: "Palestine",
|
||||
PT: "Portugal",
|
||||
PW: "Palau",
|
||||
PY: "Paraguay",
|
||||
QA: "Qatar",
|
||||
RE: "Réunion",
|
||||
RO: "Romania",
|
||||
RS: "Serbia",
|
||||
RU: "Russia (Russian Federation)",
|
||||
RW: "Rwanda",
|
||||
SA: "Saudi Arabia",
|
||||
SB: "Solomon Islands",
|
||||
SC: "Seychelles",
|
||||
SD: "Sudan",
|
||||
SE: "Sweden",
|
||||
SG: "Singapore",
|
||||
SH: "Saint Helena",
|
||||
SI: "Slovenia",
|
||||
SJ: "Svalbard and Jan Mayen",
|
||||
SK: "Slovakia",
|
||||
SL: "Sierra Leone",
|
||||
SM: "San Marino",
|
||||
SN: "Senegal",
|
||||
SO: "Somalia",
|
||||
SR: "Suriname",
|
||||
SS: "South Sudan",
|
||||
ST: "São Tomé and Príncipe",
|
||||
SV: "El Salvador",
|
||||
SX: "Sint Maarten (Dutch part)",
|
||||
SY: "Syria",
|
||||
SZ: "Eswatini",
|
||||
TC: "Turks and Caicos Islands",
|
||||
TD: "Chad",
|
||||
TF: "French Southern Territories",
|
||||
TG: "Togo",
|
||||
TH: "Thailand",
|
||||
TJ: "Tajikistan",
|
||||
TK: "Tokelau",
|
||||
TL: "Democratic Republic of Timor-Leste",
|
||||
TM: "Turkmenistan",
|
||||
TN: "Tunisia",
|
||||
TO: "Tonga",
|
||||
TR: "Türkiye",
|
||||
TT: "Trinidad and Tobago",
|
||||
TV: "Tuvalu",
|
||||
TW: "Taiwan",
|
||||
TZ: "Tanzania",
|
||||
UA: "Ukraine",
|
||||
UG: "Uganda",
|
||||
UM: "U.S. Minor Outlying Islands",
|
||||
US: "United States of America",
|
||||
UY: "Uruguay",
|
||||
UZ: "Uzbekistan",
|
||||
VA: "Vatican City",
|
||||
VC: "Saint Vincent and the Grenadines",
|
||||
VE: "Venezuela",
|
||||
VG: "British Virgin Islands",
|
||||
VI: "U.S. Virgin Islands",
|
||||
VN: "Vietnam",
|
||||
VU: "Vanuatu",
|
||||
WF: "Wallis and Futuna",
|
||||
WS: "Samoa",
|
||||
XK: "Kosovo",
|
||||
YE: "Yemen",
|
||||
YT: "Mayotte",
|
||||
ZA: "South Africa",
|
||||
ZM: "Zambia",
|
||||
ZW: "Zimbabwe",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/* ----- Log ----- */
|
||||
/* ----- Light Theme ----- */
|
||||
:root {
|
||||
--mark: #080;
|
||||
}
|
||||
|
||||
/* ----- Dark Theme ----- */
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--mark: #0c0;
|
||||
}
|
||||
}
|
||||
|
||||
section.log {
|
||||
padding: 0 0.5em;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.log .domain {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 0.5em;
|
||||
place-content: end;
|
||||
}
|
||||
|
||||
.log table {
|
||||
border-collapse: collapse;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
.log thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.log thead th {
|
||||
color: #fff;
|
||||
background-color: #999;
|
||||
padding: 0.5em 0.2em;
|
||||
font-size: 0.9em;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.log thead th img {
|
||||
width: 1em;
|
||||
vertical-align: unset;
|
||||
}
|
||||
|
||||
.log tr:hover {
|
||||
background-color: var(--hover) !important;
|
||||
}
|
||||
|
||||
.log tr:has(td[title*="407 Proxy"]) {
|
||||
color: var(--mark);
|
||||
}
|
||||
|
||||
.log tbody tr:nth-of-type(even) {
|
||||
background-color: var(--tr);
|
||||
}
|
||||
|
||||
.log td {
|
||||
font-size: 0.8em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.log td:nth-of-type(6),
|
||||
.log td:nth-of-type(7),
|
||||
.log td:nth-of-type(12) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 20em;
|
||||
}
|
||||
|
||||
.log td:nth-of-type(7) {
|
||||
max-width: 40em;
|
||||
}
|
||||
|
||||
.log td:nth-of-type(8) {
|
||||
border-left: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.log td:nth-of-type(12) {
|
||||
max-width: 5em;
|
||||
}
|
||||
|
||||
.log td.incognito::before {
|
||||
content: '';
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
background: url('../image/privateBrowsing.svg') no-repeat center / contain;
|
||||
}
|
||||
|
||||
.log tbody {
|
||||
counter-reset: n;
|
||||
}
|
||||
|
||||
.log tbody tr td:first-child::before {
|
||||
display: inline-block;
|
||||
color: #aaa;
|
||||
min-width: 1.5em;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.4em;
|
||||
pointer-events: none;
|
||||
counter-increment: n;
|
||||
content: counter(n);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import {Flag} from './flag.js';
|
||||
import {Pattern} from './pattern.js';
|
||||
import {Popup} from './options-popup.js';
|
||||
import {Nav} from './nav.js';
|
||||
|
||||
export class Log {
|
||||
|
||||
static {
|
||||
this.trTemplate = document.querySelector('.log template').content.firstElementChild;
|
||||
this.tbody = document.querySelector('.log tbody');
|
||||
// used to find proxy
|
||||
this.proxyCache = {};
|
||||
this.mode = 'disable';
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener(e => this.process(e), {urls: ['<all_urls>']});
|
||||
|
||||
// onAuthRequired message from authentication.js
|
||||
browser.runtime.onMessage.addListener((...e) => this.onMessage(...e));
|
||||
|
||||
// Get Associated Domains
|
||||
this.input = document.querySelector('.log input');
|
||||
document.querySelector('.log button').addEventListener('click', () => this.getDomains());
|
||||
this.select = document.querySelector('.popup select.popup-log-proxy');
|
||||
this.select.addEventListener('change', () => this.addPatterns());
|
||||
}
|
||||
|
||||
static onMessage(message) {
|
||||
const {id, e} = message;
|
||||
if (id !== 'onAuthRequired') { return; }
|
||||
|
||||
const tr = this.tbody.children[199] || this.trTemplate.cloneNode(true);
|
||||
const [, time, container, method, reqType, doc, url, title, type, host, port, pattern] = tr.children;
|
||||
|
||||
time.textContent = new Date(e.timeStamp).toLocaleTimeString();
|
||||
container.classList.toggle('incognito', !!e.incognito);
|
||||
container.textContent = e.cookieStoreId?.startsWith('firefox-container-') ? 'C' + e.cookieStoreId.substring(18) : '';
|
||||
method.textContent = e.method;
|
||||
reqType.textContent = e.statusCode;
|
||||
this.prepareOverflow(doc, e.statusLine || '');
|
||||
this.prepareOverflow(url, decodeURIComponent(e.url));
|
||||
|
||||
const info = e.challenger || {host: '', port: ''};
|
||||
const item = this.proxyCache[`${info.host}:${info.port}`];
|
||||
const flag = item?.cc ? Flag.get(item.cc) + ' ' : '';
|
||||
title.textContent = flag + (item?.title || '');
|
||||
title.style.borderLeftColor = item?.color || 'var(--border)';
|
||||
type.textContent = item?.type || '';
|
||||
host.textContent = info.host;
|
||||
port.textContent = info.port;
|
||||
pattern.textContent = '';
|
||||
|
||||
// in reverse order, new on top
|
||||
this.tbody.prepend(tr);
|
||||
}
|
||||
|
||||
static process(e) {
|
||||
const tr = this.tbody.children[199] || this.trTemplate.cloneNode(true);
|
||||
const [, time, container, method, reqType, doc, url, title, type, host, port, pattern] = tr.children;
|
||||
|
||||
// shortened forms similar to Developer Tools
|
||||
const shortType = {
|
||||
'main_frame': 'html',
|
||||
'sub_frame': 'iframe',
|
||||
image: 'img',
|
||||
script: 'js',
|
||||
stylesheet: 'css',
|
||||
websocket: 'ws',
|
||||
xmlhttprequest: 'xhr',
|
||||
};
|
||||
|
||||
time.textContent = new Date(e.timeStamp).toLocaleTimeString();
|
||||
container.classList.toggle('incognito', !!e.incognito);
|
||||
container.textContent = e.cookieStoreId?.startsWith('firefox-container-') ? 'C' + e.cookieStoreId.substring(18) : '';
|
||||
method.textContent = e.method;
|
||||
reqType.textContent = shortType[e.type] || e.type;
|
||||
// For a top-level document, documentUrl is undefined, chrome uses e.initiator
|
||||
this.prepareOverflow(doc, e.documentUrl || e.initiator || '');
|
||||
this.prepareOverflow(url, decodeURIComponent(e.url));
|
||||
|
||||
const info = e.proxyInfo || {host: '', port: '', type: ''};
|
||||
const item = this.proxyCache[`${info.host}:${info.port}`];
|
||||
const flag = item?.cc ? Flag.get(item.cc) + ' ' : '';
|
||||
title.textContent = flag + (item?.title || '');
|
||||
title.style.borderLeftColor = item?.color || 'var(--border)';
|
||||
type.textContent = info.type;
|
||||
host.textContent = info.host;
|
||||
port.textContent = info.port;
|
||||
|
||||
// show matching pattern in pattern mode
|
||||
const pat = this.mode === 'pattern' && item?.include.find(i => new RegExp(Pattern.get(i.pattern, i.type), 'i').test(e.url));
|
||||
const text = pat?.title || pat?.pattern || '';
|
||||
this.prepareOverflow(pattern, text);
|
||||
|
||||
// in reverse order, new on top
|
||||
this.tbody.prepend(tr);
|
||||
}
|
||||
|
||||
// set title, in case text overflows
|
||||
static prepareOverflow(elem, value) {
|
||||
elem.textContent = value;
|
||||
elem.title = value;
|
||||
}
|
||||
|
||||
static getDomains() {
|
||||
this.select.classList.add('on');
|
||||
const input = this.input.value.trim();
|
||||
if (!input) {
|
||||
// allow showing empty popup
|
||||
Popup.show('');
|
||||
return;
|
||||
}
|
||||
|
||||
// search Document URL column
|
||||
let list = document.querySelectorAll(`.log table td:nth-child(6)[title*="${input}" i]`);
|
||||
list = [...list].map(i => i.nextElementSibling.title.split(/\/+/)[1]);
|
||||
list = [...new Set(list)].sort();
|
||||
|
||||
// true -> show select
|
||||
Popup.show(list.join('\n'));
|
||||
}
|
||||
|
||||
static addPatterns() {
|
||||
const host = this.select.value;
|
||||
if (!host) { return; }
|
||||
|
||||
const text = this.select.previousElementSibling.value.trim();
|
||||
if (!text) { return; }
|
||||
|
||||
const title = this.input.value.trim();
|
||||
const data = text.split(/\s+/).map(i => ({
|
||||
include: 'include',
|
||||
type: 'wildcard',
|
||||
title,
|
||||
pattern: `://${i}/`,
|
||||
active: true
|
||||
}));
|
||||
|
||||
Popup.hide();
|
||||
Nav.get('proxies');
|
||||
|
||||
const ev = new CustomEvent('importPatternCustom', {
|
||||
detail: {host, data}
|
||||
});
|
||||
window.dispatchEvent(ev);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Using contextMenus namespace for compatibility with Chrome
|
||||
// It's not possible to create tools menu items (contexts: ["tools_menu"]) using the contextMenus namespace.
|
||||
|
||||
import {App} from './app.js';
|
||||
import {Proxy} from './proxy.js';
|
||||
import {OnRequest} from './on-request.js';
|
||||
import {Flag} from './flag.js';
|
||||
|
||||
// ---------- Context Menu ---------------------------------
|
||||
export class Menus {
|
||||
|
||||
static {
|
||||
// contextMenus is not supported on Android
|
||||
browser.contextMenus?.onClicked.addListener((...e) => this.process(...e));
|
||||
this.data = [];
|
||||
}
|
||||
|
||||
static init(pref) {
|
||||
// not available on Android
|
||||
if (!browser.contextMenus) { return; }
|
||||
|
||||
this.pref = pref;
|
||||
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/common/extensions/api/context_menus.json
|
||||
// chrome.contextMenus not promise yet -> Uncaught TypeError: Cannot read properties of undefined (reading 'then')
|
||||
browser.contextMenus.removeAll(() => this.addMenus(pref.data));
|
||||
}
|
||||
|
||||
static addMenus(data) {
|
||||
// not for PAC, limit to 10
|
||||
this.data = data.filter(i => i.active && i.type !== 'pac').slice(0, 10);
|
||||
if (!this.data[0]) { return; }
|
||||
|
||||
// --- create contextMenus
|
||||
// https://searchfox.org/mozilla-central/source/browser/components/extensions/parent/ext-menus.js#756
|
||||
// https://searchfox.org/mozilla-central/source/browser/components/extensions/parent/ext-menus.js#625-636
|
||||
// contexts defaults to ['page'], 'all' is also added in Firefox but not in Chrome
|
||||
// https://github.com/w3c/webextensions/issues/774
|
||||
// Inconsistency: contextMenus/Menus
|
||||
// child menu inherits parent's contexts but chrome has a problem with inheriting in "action" contextMenus
|
||||
const {basic, firefox} = App;
|
||||
const allowedPattern = !basic && !this.pref.managed;
|
||||
const documentUrlPatterns = ['http://*/*', 'https://*/*'];
|
||||
// menus.create requires an id for non-persistent background scripts.
|
||||
this.contextMenus = [
|
||||
...(allowedPattern ? [{id: 'includeHost', documentUrlPatterns}] : []),
|
||||
...(allowedPattern ? [{id: 'excludeHost', documentUrlPatterns}] : []),
|
||||
...(allowedPattern && firefox ? [{id: 'sep', type: 'separator', documentUrlPatterns}] : []),
|
||||
...(firefox ? [{id: 'tabProxy'}] : []),
|
||||
...(firefox ? [{parentId: 'tabProxy', id: 'tabProxy' + this.data.length, title: '\u00A0'}] : []),
|
||||
...(firefox ? [{id: 'openLinkTabProxy', contexts: ['link']}] : []),
|
||||
];
|
||||
|
||||
allowedPattern && this.addProxies('includeHost');
|
||||
allowedPattern && this.addProxies('excludeHost');
|
||||
firefox && this.addProxies('tabProxy');
|
||||
firefox && this.addProxies('openLinkTabProxy');
|
||||
|
||||
this.contextMenus.forEach(i => {
|
||||
// always use the same ID for i18n
|
||||
i.type !== 'separator' && (i.title ||= browser.i18n.getMessage(i.id));
|
||||
// add contexts
|
||||
// !i.parentId && (i.contexts ||= ['all']);
|
||||
i.contexts ||= ['all'];
|
||||
|
||||
browser.contextMenus.create(i);
|
||||
});
|
||||
}
|
||||
|
||||
static addProxies(parentId) {
|
||||
this.data.forEach((i, index) =>
|
||||
this.contextMenus.push({
|
||||
parentId,
|
||||
id: parentId + index,
|
||||
title: Flag.get(i.cc) + ' ' + (i.title || `${i.hostname}:${i.port}`)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
static async process(info, tab) {
|
||||
const pref = this.pref;
|
||||
const id = info.parentMenuItemId;
|
||||
const index = info.menuItemId.substring(id.length);
|
||||
const proxy = this.data[index];
|
||||
switch (id) {
|
||||
case 'includeHost':
|
||||
case 'excludeHost':
|
||||
Proxy.includeHost(pref, proxy, tab, id);
|
||||
break;
|
||||
|
||||
// --- firefox only
|
||||
case 'setTabProxy':
|
||||
OnRequest.setTabProxy(tab, proxy);
|
||||
break;
|
||||
|
||||
case 'openLinkTabProxy':
|
||||
tab = await browser.tabs.create({});
|
||||
OnRequest.setTabProxy(tab, proxy);
|
||||
browser.tabs.update(tab.id, {url: info.linkUrl});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
----- Patterns -----
|
||||
Chrome FoxyProxy v3 (old) featured full/partial url match pattern
|
||||
Firefox FoxyProxy v4 - v7 (old) featured host only match pattern
|
||||
- Migrating v4 - v7 storage data to full/partial url match pattern
|
||||
- Dropping select for http|https|all
|
||||
|
||||
----- SOCKS keyword -----
|
||||
https://developer.chrome.com/docs/extensions/reference/proxy/#proxy-rules
|
||||
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo
|
||||
https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md#http-proxy-scheme
|
||||
Chrome PAC schemes PROXY | HTTPS | SOCKS4/SOCKS | SOCKS5
|
||||
Chrome API schemes http | https | socks4 | socks5 | quic
|
||||
|
||||
Firefox PAC schemes PROXY/HTTP | HTTPS | SOCKS4/SOCKS | SOCKS5
|
||||
Firefox API types http | https | socks4 | socks (means socks5) | direct
|
||||
|
||||
Firefox/Chrome PAC 'SOCKS' means SOCKS4
|
||||
Firefox API 'SOCKS' means SOCKS5
|
||||
Code uses PAC 'SOCKS4/5' but converts to socks in Firefox API
|
||||
|
||||
----- host/hostname keywords -----
|
||||
Firefox/Chrome API/PAC use 'host' for domain/ip
|
||||
JavaScript uses 'hostname' for for domain/ip & 'host' for domain:port/ip:port
|
||||
Code uses JavaScript 'hostname' domain/ip
|
||||
*/
|
||||
|
||||
import {App} from './app.js';
|
||||
import {Pattern} from './pattern.js';
|
||||
import {Color} from './color.js';
|
||||
|
||||
/*
|
||||
Chrome v3 (old) encrypts username/passwords using CryptoJS 3.1.2
|
||||
CryptoJS library is used to migrate preferences to v8.0 but will be removed in future upgrades
|
||||
Original CryptoJS 3.1.2 aes.js https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/aes.js
|
||||
`export {CryptoJS};` was added to be able to import as ES6 module
|
||||
*/
|
||||
// removed in v9.0
|
||||
// import {CryptoJS} from '../lib/aes.3.1.2.js';
|
||||
|
||||
export class Migrate {
|
||||
|
||||
static async init(pref) {
|
||||
// --- 8.9
|
||||
// 8.9 remove showPatternProxy (from 8.7)
|
||||
// 8.8 tidy up left-over obj Sync typo mistake (from 8.0)
|
||||
// 8.7 change global proxyDNS to per-proxy (from 8.0)
|
||||
if (Object.hasOwn(pref, 'proxyDNS') && pref.data) {
|
||||
pref.data.forEach(i => i.proxyDNS = !!pref.proxyDNS);
|
||||
await browser.storage.local.set(pref);
|
||||
}
|
||||
// 8.1 remove globalExcludeWildcard, globalExcludeRegex (from 8.0)
|
||||
const keys = ['showPatternProxy', 'obj', 'proxyDNS', 'globalExcludeWildcard', 'globalExcludeRegex'];
|
||||
keys.forEach(i => delete pref[i]);
|
||||
await browser.storage.local.remove(keys);
|
||||
await browser.storage.sync.remove(keys);
|
||||
|
||||
// --- 8.0
|
||||
if (pref.data) { return; }
|
||||
|
||||
let db = {};
|
||||
switch (true) {
|
||||
case !Object.keys(pref)[0]:
|
||||
db = App.getDefaultPref();
|
||||
break;
|
||||
|
||||
case Object.hasOwn(pref, 'settings'):
|
||||
db = this.convert3(pref);
|
||||
break;
|
||||
|
||||
default:
|
||||
db = this.convert7(pref);
|
||||
}
|
||||
|
||||
if (Object.keys(pref)[0]) {
|
||||
// clear pref
|
||||
Object.keys(pref).forEach(i => delete pref[i]);
|
||||
await browser.storage.local.clear();
|
||||
}
|
||||
|
||||
// populate pref
|
||||
Object.keys(db).forEach(i => pref[i] = db[i]);
|
||||
|
||||
// --- update database
|
||||
await browser.storage.local.set(pref);
|
||||
// return pref;
|
||||
}
|
||||
|
||||
// static decrypt(str, key) {
|
||||
// return CryptoJS.AES.decrypt(str, key).toString(CryptoJS.enc.Utf8).split(/(?<!\\):/).map(i => i.replace(/\\:/g, ':'));
|
||||
// }
|
||||
|
||||
// --- Chrome v3
|
||||
static convert3(pref) {
|
||||
// https://groups.google.com/a/chromium.org/g/chromium-extensions/c/6qiMo0P-XS4
|
||||
// mode in v3 was saved to localStorage and not accessible in MV3 service worker
|
||||
|
||||
// new database format
|
||||
const db = App.getDefaultPref();
|
||||
db.sync = !!pref?.settings?.useSyncStorage;
|
||||
|
||||
// CryptoJS key
|
||||
// const sk = pref.settings.sk;
|
||||
pref.proxyList?.forEach(key => {
|
||||
const item = pref[key].data;
|
||||
// skip
|
||||
if (key === 'default' || !item?.patterns) { return; }
|
||||
// convert to actual type: http | https | socks4 | socks5 | + PAC
|
||||
// default HTTP (no HTTPS option in FP Chrome v3)
|
||||
const type = item.isSocks ? (item.socks === '5' ? 'socks5' : 'socks4') :
|
||||
(item.type === 'auto' ? 'pac' : 'http');
|
||||
|
||||
// removed in v9.0
|
||||
// decrypt username, password
|
||||
// const [username = '', password = ''] = sk ? this.decrypt(item.credentials, sk) : [];
|
||||
|
||||
// proxy template
|
||||
const pxy = {
|
||||
active: item.enabled,
|
||||
title: item.name || '',
|
||||
type,
|
||||
// rename to hostname
|
||||
hostname: item.host,
|
||||
port: item.port,
|
||||
username: '',
|
||||
password: '',
|
||||
// remove country, use CC in location.js
|
||||
cc: '',
|
||||
city: '',
|
||||
color: item.color || Color.getRandom(),
|
||||
pac: type === 'pac' ? item.configUrl : '',
|
||||
pacString: '',
|
||||
proxyDNS: true,
|
||||
include: [],
|
||||
exclude: [],
|
||||
tabProxy: [],
|
||||
};
|
||||
|
||||
// process include/exclude
|
||||
item.patterns.forEach(elem => {
|
||||
const p = elem.data;
|
||||
// skip
|
||||
if (!p?.type) { return; }
|
||||
|
||||
const pat = {
|
||||
active: true,
|
||||
// v3 keep patterns as they are
|
||||
pattern: p.url,
|
||||
title: p.name || '',
|
||||
// uses wildcard | regexp -> change to regex
|
||||
type: p.type === 'wildcard' ? 'wildcard' : 'regex'
|
||||
};
|
||||
|
||||
// Validate RegExp, deactivate on error
|
||||
!Pattern.validate(pat.pattern, pat.type) && (pat.active = false);
|
||||
|
||||
// whitelist: Inclusive/Exclusive
|
||||
p.whitelist === 'Inclusive' ? pxy.include.push(pat) : pxy.exclude.push(pat);
|
||||
});
|
||||
|
||||
db.data.push(pxy);
|
||||
});
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
// --- Firefox v6-7 (also used in Options -> Import Older Export)
|
||||
static convert7(pref) {
|
||||
const typeSet = {
|
||||
1: 'http', // PROXY_TYPE_HTTP
|
||||
2: 'https', // PROXY_TYPE_HTTPS
|
||||
3: 'socks5', // PROXY_TYPE_SOCKS5
|
||||
4: 'socks4', // PROXY_TYPE_SOCKS4
|
||||
5: 'direct' // PROXY_TYPE_NONE
|
||||
};
|
||||
|
||||
// new database format
|
||||
pref.mode ||= 'disable';
|
||||
// rename disabled -> disable
|
||||
pref.mode === 'disabled' && (pref.mode = 'disable');
|
||||
// rename patterns -> pattern
|
||||
pref.mode === 'patterns' && (pref.mode = 'pattern');
|
||||
// convert old mode
|
||||
if (pref[pref.mode]) {
|
||||
const i = pref[pref.mode];
|
||||
pref.mode = `${i.address}:${i.port}`;
|
||||
}
|
||||
|
||||
const db = App.getDefaultPref();
|
||||
db.sync = !!pref.sync;
|
||||
|
||||
// null value causes an error in hasOwn, direct proxies don't have 'address'
|
||||
const data = Object.values(pref).filter(i => i && ['address', 'type'].some(p => Object.hasOwn(i, p)));
|
||||
|
||||
// sort by index
|
||||
data.sort((a, b) => a.index - b.index);
|
||||
|
||||
data.forEach(i => {
|
||||
// proxy template
|
||||
const pxy = {
|
||||
// convert to boolean, some old databases have mixed types
|
||||
active: i.active === 'true' || i.active === true,
|
||||
title: i.title || '',
|
||||
// convert to actual type: http | https | socks4 | socks5 | direct | + add PAC
|
||||
type: typeSet[i.type],
|
||||
// rename to hostname
|
||||
hostname: i.address || '',
|
||||
port: i.port || '',
|
||||
username: i.username || '',
|
||||
password: i.password || '',
|
||||
// remove country, use CC in location.js
|
||||
cc: i.cc || '',
|
||||
city: '',
|
||||
color: i.color || Color.getRandom(),
|
||||
// add PAC option
|
||||
pac: '',
|
||||
pacString: '',
|
||||
proxyDNS: !!i.proxyDNS,
|
||||
// rename to include
|
||||
include: i.whitePatterns || [],
|
||||
// rename to exclude
|
||||
exclude: i.blackPatterns || [],
|
||||
tabProxy: [],
|
||||
};
|
||||
|
||||
// convert UK to ISO 3166-1 GB
|
||||
pxy.cc === 'UK' && (pxy.cc = 'GB');
|
||||
// type 'direct'
|
||||
pxy.type === 'direct' && (pxy.hostname = 'DIRECT');
|
||||
|
||||
/*
|
||||
{
|
||||
"active": true,
|
||||
"pattern": "*.example.com",
|
||||
"title": "example",
|
||||
"type": 1,
|
||||
// "protocols": 1,
|
||||
},
|
||||
*/
|
||||
|
||||
const patternSet = {
|
||||
1: 'wildcard',
|
||||
2: 'regex'
|
||||
};
|
||||
// process include/exclude
|
||||
[...pxy.include, ...pxy.exclude].forEach(i => {
|
||||
// convert to actual type: wildcard | regex
|
||||
i.type = patternSet[i.type];
|
||||
|
||||
// convert wildcard all | http | https to v3 patterns
|
||||
i.pattern = i.type === 'wildcard' ?
|
||||
this.convertWildcard(i.pattern, i.protocols) : this.convertRegEx(i.pattern, i.protocols);
|
||||
// no longer needed
|
||||
delete i.protocols;
|
||||
|
||||
// Validate RegExp, deactivate on error
|
||||
!Pattern.validate(i.pattern, i.type) && (i.active = false);
|
||||
|
||||
// convert v6-7 patterns to match pattern (v9.0)
|
||||
if (i.type === 'wildcard' && Pattern.validMatchPattern(i.pattern + '*')) {
|
||||
i.type === 'match';
|
||||
i.pattern += '*';
|
||||
}
|
||||
});
|
||||
|
||||
db.data.push(pxy);
|
||||
});
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
/*
|
||||
.bbc.co.uk exact domain and all subdomains
|
||||
*.bbc.co.uk exact domain and all subdomains
|
||||
**.bbc.co.uk subdomains only (not bbc.co.uk)
|
||||
*/
|
||||
static convertWildcard(pat, protocol) {
|
||||
const protocolSet = {
|
||||
// all | http | https
|
||||
1: '*://',
|
||||
2: 'http://',
|
||||
4: 'https://'
|
||||
};
|
||||
|
||||
return protocolSet[protocol] + (pat.startsWith('.') ? '*' : '') + pat + '/';
|
||||
}
|
||||
|
||||
static convertRegEx(pat, protocol) {
|
||||
const protocolSet = {
|
||||
// all | http | https
|
||||
1: '.+://',
|
||||
2: 'http://',
|
||||
4: 'https://'
|
||||
};
|
||||
|
||||
// remove start assertion
|
||||
pat.startsWith('^') && (pat = pat.substring(1));
|
||||
// remove end assertion
|
||||
pat.endsWith('$') && (pat = pat.slice(0, -1));
|
||||
|
||||
return protocolSet[protocol] + pat + '/';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/* ----- Navigation ----- */
|
||||
input[name="nav"],
|
||||
input[type="checkbox"].control {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav {
|
||||
background-color: var(--nav-bg);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
justify-content: start;
|
||||
align-items: end;
|
||||
color: var(--nav-color);
|
||||
height: var(--nav-height);
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
nav img {
|
||||
width: 2em;
|
||||
margin: 0.2em 0.5em;
|
||||
}
|
||||
|
||||
nav > label {
|
||||
padding: 0.5em 1em;
|
||||
/* transition: 0.5s; */
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
}
|
||||
|
||||
nav > label:hover {
|
||||
background-color: var(--nav-hover);
|
||||
}
|
||||
|
||||
/* nav > label img {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
nav label a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
nav label a:hover {
|
||||
text-decoration: none;
|
||||
} */
|
||||
|
||||
#nav1:checked ~ article section:nth-of-type(1),
|
||||
#nav2:checked ~ article section:nth-of-type(2),
|
||||
#nav3:checked ~ article section:nth-of-type(3),
|
||||
#nav4:checked ~ article section:nth-of-type(4),
|
||||
#nav5:checked ~ article section:nth-of-type(5),
|
||||
#nav6:checked ~ article section:nth-of-type(6),
|
||||
#nav7:checked ~ article section:nth-of-type(7),
|
||||
#nav8:checked ~ article section:nth-of-type(8) {
|
||||
display: block;
|
||||
/* animation: sect 0.5s ease-in-out; */
|
||||
}
|
||||
|
||||
#nav1:checked ~ .nav label[for="nav1"],
|
||||
#nav2:checked ~ .nav label[for="nav2"],
|
||||
#nav3:checked ~ .nav label[for="nav3"],
|
||||
#nav4:checked ~ .nav label[for="nav4"],
|
||||
#nav5:checked ~ .nav label[for="nav5"],
|
||||
#nav6:checked ~ .nav label[for="nav6"],
|
||||
#nav7:checked ~ .nav label[for="nav7"],
|
||||
#nav8:checked ~ .nav label[for="nav8"] {
|
||||
background-color: var(--body-bg);
|
||||
}
|
||||
|
||||
/* @keyframes sect {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
} */
|
||||
@@ -0,0 +1,52 @@
|
||||
import {App} from './app.js';
|
||||
|
||||
export class Nav {
|
||||
|
||||
static {
|
||||
document.querySelectorAll('label[for^="nav"]').forEach(i =>
|
||||
this[i.dataset.i18n] = i.control);
|
||||
}
|
||||
|
||||
static get(pram = location.search.substring(1)) {
|
||||
pram && this[pram] && (this[pram].checked = true);
|
||||
}
|
||||
|
||||
static {
|
||||
// --- openShortcutSettings FF137
|
||||
const shortcut = document.querySelector('.shortcut-link');
|
||||
// commands is not supported on Android
|
||||
if (!App.firefox || browser.commands?.openShortcutSettings) {
|
||||
shortcut.style.display = 'unset';
|
||||
shortcut.addEventListener('click', () =>
|
||||
App.firefox ? browser.commands?.openShortcutSettings() :
|
||||
browser.tabs.create({url: 'chrome://extensions/shortcuts'})
|
||||
);
|
||||
}
|
||||
|
||||
// help document
|
||||
const help = document.querySelector('iframe[src="help.html"]').contentDocument;
|
||||
|
||||
// --- data-link
|
||||
const helpLink = help.querySelector('.nav-link');
|
||||
document.querySelectorAll('[data-link]').forEach(i => i.addEventListener('click', e => {
|
||||
const {link} = e.target.dataset;
|
||||
if (!link) { return; }
|
||||
|
||||
Nav.get('help');
|
||||
helpLink.href = link;
|
||||
helpLink.click();
|
||||
}));
|
||||
|
||||
// --- Extension link in the Help
|
||||
// not for Firefox
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1956860
|
||||
|
||||
// chrome
|
||||
if (!App.firefox) {
|
||||
const link = help.querySelector('.chrome-extension');
|
||||
link.style.display = 'unset';
|
||||
link.addEventListener('click', () =>
|
||||
browser.tabs.create({url: 'chrome://extensions/?id=' + location.hostname}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1198822
|
||||
// Dynamic import is not available yet in MV3 service worker
|
||||
// Once implemented, module will be dynamically imported for Firefox only
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1853203
|
||||
// Support non-ASCII username/password for socks proxy (fixed in Firefox 119)
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1741375
|
||||
// Proxy DNS by default when using SOCKS v5 (fixed in Firefox 128, defaults to true for SOCKS5 & false for SOCKS4)
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1893670
|
||||
// Proxy DNS by default for SOCK4 proxies. Defaulting to SOCKS4a
|
||||
|
||||
// proxyAuthorizationHeader on Firefox only applied to HTTPS (HTTP broke the API and sent DIRECT)
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1794464
|
||||
// Allow HTTP authentication in proxy.onRequest (fixed in Firefox 125)
|
||||
|
||||
// proxy.onRequest only applies to http/https/ws/wss
|
||||
// it can not catch domains set by user to 127.0.0.1 in the hosts file
|
||||
|
||||
import {App} from './app.js';
|
||||
import {Pattern} from './pattern.js';
|
||||
import {Location} from './location.js';
|
||||
|
||||
// ---------- Firefox proxy.onRequest API ------------------
|
||||
export class OnRequest {
|
||||
|
||||
static {
|
||||
// --- default values
|
||||
this.mode = 'disable';
|
||||
// used for Single Proxy
|
||||
this.proxy = {};
|
||||
// used for Proxy by Pattern
|
||||
this.data = [];
|
||||
// RegExp string
|
||||
this.passthrough = [];
|
||||
// [start, end] strings
|
||||
this.net = [];
|
||||
// tab proxy, will be lost in MV3 background unloading
|
||||
this.tabProxy = {};
|
||||
// incognito/container proxy
|
||||
this.container = {};
|
||||
|
||||
// --- Firefox only
|
||||
if (browser.proxy.onRequest) {
|
||||
browser.proxy.onRequest.addListener(e => this.process(e), {urls: ['<all_urls>']});
|
||||
// check Tab for tab proxy
|
||||
browser.tabs.onUpdated.addListener((...e) => this.onUpdated(...e));
|
||||
// remove redundant data from this.tabProxy cache
|
||||
browser.tabs.onRemoved.addListener(tabId => delete this.tabProxy[tabId]);
|
||||
// mark incognito/container
|
||||
browser.tabs.onCreated.addListener(e => this.checkPageAction(e));
|
||||
|
||||
// prevent proxy.onRequest.addListener unloading in MV3 (default 30s)
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1771203
|
||||
// it can trigger DNS leak on reloading under limited circumstances
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1882276
|
||||
this.persist();
|
||||
}
|
||||
}
|
||||
|
||||
static persist() {
|
||||
// clear the previous interval & set a new one
|
||||
clearInterval(this.interval);
|
||||
this.interval = setInterval(() => browser.runtime.getPlatformInfo(), 25_000);
|
||||
}
|
||||
|
||||
static init(pref) {
|
||||
this.mode = pref.mode;
|
||||
[this.passthrough, , this.net] = Pattern.getPassthrough(pref.passthrough);
|
||||
|
||||
// filter data
|
||||
const data = pref.data.filter(i => i.active && i.type !== 'pac' && i.hostname);
|
||||
|
||||
// --- single proxy (false|undefined|proxy object)
|
||||
this.proxy = /:\d+[^/]*$/.test(pref.mode) && data.find(i => pref.mode === `${i.hostname}:${i.port}`);
|
||||
|
||||
// --- proxy by pattern
|
||||
this.data = data.filter(i => i.include[0] || i.exclude[0] || i.tabProxy?.[0]).map(item => {
|
||||
item.tabProxy ||= [];
|
||||
return {
|
||||
type: item.type,
|
||||
hostname: item.hostname,
|
||||
port: item.port,
|
||||
username: item.username,
|
||||
password: item.password,
|
||||
// proxyDNS used in mode pattern or single proxy
|
||||
proxyDNS: item.proxyDNS,
|
||||
include: item.include.filter(i => i.active).map(i => Pattern.get(i.pattern, i.type)),
|
||||
exclude: item.exclude.filter(i => i.active).map(i => Pattern.get(i.pattern, i.type)),
|
||||
tabProxy: item.tabProxy.filter(i => i.active).map(i => Pattern.get(i.pattern, i.type)),
|
||||
|
||||
// used for showPatternProxy
|
||||
title: item.title,
|
||||
cc: item.cc,
|
||||
city: item.city,
|
||||
color: item.color,
|
||||
};
|
||||
});
|
||||
|
||||
// --- incognito/container proxy
|
||||
// reset container
|
||||
this.container = {};
|
||||
pref.container && Object.entries(pref.container).forEach(([key, val]) => {
|
||||
// prefix key
|
||||
key.startsWith('container-') && (key = 'firefox-' + key);
|
||||
this.container[key] = val && data.find(i => val === `${i.hostname}:${i.port}`);
|
||||
});
|
||||
|
||||
// mirror as this.tabProxy is lost in MV3 background unloading
|
||||
browser.storage.session.get('tabProxy')
|
||||
.then(i => this.tabProxy = i.tabProxy || {});
|
||||
}
|
||||
|
||||
static process(e) {
|
||||
// reset interval
|
||||
this.persist();
|
||||
|
||||
const tabId = e.tabId;
|
||||
const fromTab = tabId !== -1;
|
||||
|
||||
// --- check Tab Proxy Pattern
|
||||
this.processTabProxy(tabId, e.url, e);
|
||||
|
||||
switch (true) {
|
||||
// --- check local & global passthrough
|
||||
case this.bypass(e.url):
|
||||
this.setAction(tabId);
|
||||
return {type: 'direct'};
|
||||
|
||||
// --- tab proxy
|
||||
case fromTab && !!this.tabProxy[tabId]:
|
||||
return this.processProxy(tabId, this.tabProxy[tabId]);
|
||||
|
||||
// --- incognito proxy
|
||||
case fromTab && e.incognito && !!this.container.incognito:
|
||||
return this.processProxy(tabId, this.container.incognito);
|
||||
|
||||
// --- container proxy
|
||||
case fromTab && e.cookieStoreId && !!this.container[e.cookieStoreId]:
|
||||
return this.processProxy(tabId, this.container[e.cookieStoreId]);
|
||||
|
||||
// --- standard operation
|
||||
// pass direct
|
||||
case this.mode === 'disable':
|
||||
case this.mode === 'direct':
|
||||
// PAC URL is set
|
||||
case this.mode.includes('://') && !/:\d+$/.test(this.mode):
|
||||
this.setAction(tabId);
|
||||
return {type: 'direct'};
|
||||
|
||||
// check if url matches patterns
|
||||
case this.mode === 'pattern':
|
||||
return this.processPattern(tabId, e.url);
|
||||
|
||||
// get the proxy for all
|
||||
default:
|
||||
return this.processProxy(tabId, this.proxy);
|
||||
}
|
||||
}
|
||||
|
||||
static processTabProxy(tabId, url, e) {
|
||||
if (this.mode !== 'pattern' || e.type !== 'main_frame' || this.tabProxy[tabId]) { return; }
|
||||
|
||||
const match = arr => arr.some(i => new RegExp(i, 'i').test(url));
|
||||
const proxy = this.data.find(i => match(i.tabProxy));
|
||||
proxy && (this.tabProxy[tabId] = proxy);
|
||||
}
|
||||
|
||||
static processPattern(tabId, url) {
|
||||
const match = arr => arr.some(i => new RegExp(i, 'i').test(url));
|
||||
const proxy = this.data.find(i => !match(i.exclude) && match(i.include));
|
||||
if (proxy) {
|
||||
return this.processProxy(tabId, proxy);
|
||||
}
|
||||
|
||||
// no match
|
||||
this.setAction(tabId);
|
||||
return {type: 'direct'};
|
||||
}
|
||||
|
||||
static processProxy(tabId, proxy) {
|
||||
this.setAction(tabId, proxy);
|
||||
const {type, hostname: host, port, username, password, proxyDNS} = proxy || {};
|
||||
if (!type || type === 'direct') { return {type: 'direct'}; }
|
||||
|
||||
const auth = username && password && type.startsWith('http');
|
||||
|
||||
const response = {
|
||||
host,
|
||||
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/ProxyChannelFilter.sys.mjs#102
|
||||
// Although API converts to number -> let port = Number.parseInt(proxyData.port, 10);
|
||||
// port 'number', prepare for augmented port
|
||||
port: parseInt(port),
|
||||
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/ProxyChannelFilter.sys.mjs#43
|
||||
// API uses socks for socks5
|
||||
type: type === 'socks5' ? 'socks' : type,
|
||||
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/ProxyChannelFilter.sys.mjs#135
|
||||
...(type.startsWith('socks') && {proxyDNS: !!proxyDNS}),
|
||||
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/ProxyChannelFilter.sys.mjs#117
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/ProxyChannelFilter.sys.mjs#126
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/ProxyChannelFilter.sys.mjs#231
|
||||
// only used for SOCKS 4/5, must be string and not undefined
|
||||
// allow sending username without password
|
||||
username,
|
||||
password,
|
||||
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/ProxyChannelFilter.sys.mjs#167
|
||||
// only for HTTP/HTTPS
|
||||
// use proxyAuthorizationHeader to reduce requests in webRequest.onAuthRequired
|
||||
...(auth && {proxyAuthorizationHeader: 'Basic ' + btoa(username + ':' + password)}),
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// browser.action here only relates to showPatternProxy from proxy.onRequest
|
||||
static setAction(tabId, item) {
|
||||
// Set to -1 if the request isn't related to a tab
|
||||
if (tabId === -1) { return; }
|
||||
|
||||
// --- reset values
|
||||
let title = '';
|
||||
let text = '';
|
||||
let color = '';
|
||||
|
||||
// --- set proxy details
|
||||
if (item) {
|
||||
const host = [item.hostname, item.port].filter(Boolean).join(':');
|
||||
title = [item.title, host, item.city, Location.get(item.cc)].filter(Boolean).join('\n');
|
||||
text = item.title || item.hostname;
|
||||
color = item.color;
|
||||
}
|
||||
|
||||
color && browser.action.setBadgeBackgroundColor({color, tabId});
|
||||
browser.action.setTitle({title, tabId});
|
||||
browser.action.setBadgeText({text, tabId});
|
||||
}
|
||||
|
||||
// ---------- passthrough --------------------------------
|
||||
// Firefox & Chrome have a default localhost bypass
|
||||
// Connections to localhost, 127.0.0.1/8, and ::1 are never proxied.
|
||||
// Firefox: "network.proxy.allow_hijacking_localhost"
|
||||
// Chrome: --proxy-bypass-list="<-loopback>"
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1854324
|
||||
// proxy.onRequest failure to bypass proxy for localhost (fixed in Firefox 137)
|
||||
static bypass(url) {
|
||||
switch (true) {
|
||||
// global passthrough
|
||||
case this.passthrough.some(i => new RegExp(i, 'i').test(url)):
|
||||
// global passthrough CIDR
|
||||
case this.net[0] && this.isInNet(url):
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static isInNet(url) {
|
||||
// check if IP address
|
||||
if (!/^[a-z]+:\/\/\d+(\.\d+){3}(:\d+)?\//.test(url)) { return; }
|
||||
|
||||
// IP array
|
||||
const ipa = url.split(/[:/.]+/, 5).slice(1);
|
||||
// convert to padded string
|
||||
const ip = ipa.map(i => i.padStart(3, '0')).join('');
|
||||
return this.net.some(([st, end]) => ip >= st && ip <= end);
|
||||
}
|
||||
|
||||
// ---------- Tab Proxy ----------------------------------
|
||||
static setTabProxy(tab, pxy) {
|
||||
switch (true) {
|
||||
// unacceptable URLs
|
||||
case !App.allowedTabProxy(tab.url):
|
||||
// check global passthrough
|
||||
case this.bypass(tab.url):
|
||||
return;
|
||||
}
|
||||
|
||||
// set or unset
|
||||
pxy ? this.tabProxy[tab.id] = pxy : delete this.tabProxy[tab.id];
|
||||
this.setAction(tab.id, pxy);
|
||||
|
||||
// mirror as this.tabProxy is lost in MV3 background unloading
|
||||
browser.storage.session.set({'tabProxy': this.tabProxy});
|
||||
}
|
||||
|
||||
// ---------- Update Page Action -------------------------
|
||||
static onUpdated(tabId, changeInfo, tab) {
|
||||
if (changeInfo.status !== 'complete') { return; }
|
||||
|
||||
const pxy = this.tabProxy[tabId];
|
||||
pxy ? this.setAction(tab.id, pxy) : this.checkPageAction(tab);
|
||||
}
|
||||
|
||||
// ---------- Incognito/Container ------------------------
|
||||
static checkPageAction(tab) {
|
||||
// not if tab proxy is set
|
||||
if (tab.id === -1 || this.tabProxy[tab.id]) { return; }
|
||||
|
||||
const pxy = tab.incognito ? this.container.incognito : this.container[tab.cookieStoreId];
|
||||
pxy && this.setAction(tab.id, pxy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// ---------- Filter Proxy (Side Effect) -------------------
|
||||
class Filter {
|
||||
|
||||
static {
|
||||
this.list = document.querySelector('div.proxy-div');
|
||||
const filter = document.querySelector('.filter');
|
||||
filter.addEventListener('input', e => this.process(e));
|
||||
}
|
||||
|
||||
static process(e) {
|
||||
const str = e.target.value.toLowerCase().trim();
|
||||
const elem = [...this.list.children];
|
||||
if (!str) {
|
||||
elem.forEach(i => i.classList.remove('off'));
|
||||
return;
|
||||
}
|
||||
|
||||
elem.forEach(item => {
|
||||
const proxyBox = item.children[1].children[0];
|
||||
const title = proxyBox.children[1].value;
|
||||
const hostname = proxyBox.children[3].value;
|
||||
const port = ':' + proxyBox.children[7].value;
|
||||
item.classList.toggle('off', ![title, hostname, port].some(i => i.toLowerCase().includes(str)));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* ----- Popup ----- */
|
||||
.popup {
|
||||
display: none;
|
||||
background-color: #0003;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.popup.on {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
animation: fade-in 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.popup span {
|
||||
background-color: var(--alt-bg);
|
||||
display: grid;
|
||||
place-content: center end;
|
||||
padding: 0 0.8em;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
border-radius: 0.5em 0.5em 0 0;
|
||||
/* set height to maintain the same result in chrome/firefox */
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.popup span:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
.popup textarea {
|
||||
width: 50vw;
|
||||
height: 50vh;
|
||||
resize: both;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.popup select {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup select.on {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export class Popup {
|
||||
|
||||
static {
|
||||
this.popup = document.querySelector('.popup');
|
||||
[this.close, this.textarea] = this.popup.children;
|
||||
this.close.addEventListener('click', () => this.hide());
|
||||
}
|
||||
|
||||
static show(text) {
|
||||
this.textarea.value += text + '\n';
|
||||
this.popup.classList.add('on');
|
||||
}
|
||||
|
||||
static hide() {
|
||||
this.popup.classList.remove('on');
|
||||
this.textarea.value = '';
|
||||
[...this.popup.children].forEach(i => {
|
||||
if (i.nodeName === 'SELECT') {
|
||||
i.selectedIndex = 0;
|
||||
i.classList.remove('on');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
import {App} from './app.js';
|
||||
import {ImportExport} from './import-export.js';
|
||||
import {Pattern} from './pattern.js';
|
||||
import {Flag} from './flag.js';
|
||||
import {Color} from './color.js';
|
||||
import {PAC} from './pac.js';
|
||||
import {Toggle} from './toggle.js';
|
||||
import {Tester} from './tester.js';
|
||||
import {Log} from './log.js';
|
||||
import {Nav} from './nav.js';
|
||||
|
||||
export class Proxies {
|
||||
|
||||
static {
|
||||
this.proxyDiv = document.querySelector('div.proxy-div');
|
||||
[this.proxyTemplate, this.patternTemplate] =
|
||||
document.querySelector('.proxy-section template').content.children;
|
||||
|
||||
// firefox only, disabling Tab Proxy in the template for chrome
|
||||
!App.firefox && (this.patternTemplate.children[1].lastElementChild.disabled = true);
|
||||
|
||||
// --- buttons
|
||||
document.querySelector('.proxy-top button[data-i18n="add"]').addEventListener('click', e => {
|
||||
// this.addProxy(null, e.ctrlKey)
|
||||
const [pxy, title] = this.addProxy();
|
||||
e.ctrlKey ? this.proxyDiv.prepend(pxy) : this.proxyDiv.append(pxy);
|
||||
pxy.open = true;
|
||||
pxy.draggable = false;
|
||||
title.focus();
|
||||
});
|
||||
|
||||
// used to find proxy
|
||||
this.proxyCache = {};
|
||||
// used to get the details for the log
|
||||
Log.proxyCache = this.proxyCache;
|
||||
// this.process();
|
||||
|
||||
// import pattern from log.js
|
||||
window.addEventListener('importPatternCustom', (e) => this.importPatternCustom(e));
|
||||
}
|
||||
|
||||
// pref references the same object in the memory and its value gets updated
|
||||
static process(pref) {
|
||||
Log.mode = pref.mode;
|
||||
// reset
|
||||
this.proxyDiv.textContent = '';
|
||||
const docFrag = document.createDocumentFragment();
|
||||
pref.data.forEach(i => docFrag.append(this.addProxy(i)));
|
||||
this.proxyDiv.append(docFrag);
|
||||
}
|
||||
|
||||
static addProxy(item, modifier) {
|
||||
// --- details: make a blank proxy with all event listeners
|
||||
const pxy = this.proxyTemplate.cloneNode(true);
|
||||
const summary = pxy.children[0];
|
||||
const proxyBox = pxy.children[1].children[0];
|
||||
const patternBox = pxy.children[1].children[2];
|
||||
|
||||
// disable draggable when details is open
|
||||
summary.addEventListener('click', () => pxy.draggable = pxy.open);
|
||||
|
||||
// --- summary
|
||||
const [flag, sumTitle, active, dup, del, up, down] = summary.children;
|
||||
dup.addEventListener('click', () => this.duplicateProxy(pxy));
|
||||
del.addEventListener('click', () => confirm(browser.i18n.getMessage('deleteConfirm')) && pxy.remove());
|
||||
up.addEventListener('click', () => pxy.previousElementSibling?.before(pxy));
|
||||
down.addEventListener('click', () => pxy.nextElementSibling?.after(pxy));
|
||||
|
||||
// proxy data
|
||||
const [title, hostname, type, port, cc, username, city, passwordSpan,
|
||||
colorSpan, pacSpan, proxyDNS] = [...proxyBox.children].filter((e, i) => i % 2);
|
||||
title.addEventListener('change', e => sumTitle.textContent = e.target.value);
|
||||
|
||||
const [pac, storeLocallyLabel, view] = pacSpan.children;
|
||||
|
||||
type.addEventListener('change', e => {
|
||||
// use for show/hide elements
|
||||
pxy.dataset.type = e.target.value;
|
||||
|
||||
const elem = e.target.selectedOptions[0];
|
||||
const data = elem.dataset;
|
||||
// --- server auto-fill helpers
|
||||
switch (true) {
|
||||
case ['flag', 'hostname', 'port'].some(i => data[i]):
|
||||
sumTitle.textContent = elem.textContent;
|
||||
title.value = elem.textContent;
|
||||
data.flag && (flag.textContent = data.flag);
|
||||
data.hostname && (hostname.value = data.hostname);
|
||||
data.port && (port.value = data.port);
|
||||
break;
|
||||
|
||||
default:
|
||||
flag.textContent = Flag.get(cc.value);
|
||||
}
|
||||
});
|
||||
|
||||
cc.addEventListener('change', () => flag.textContent = Flag.get(cc.value));
|
||||
Toggle.password(passwordSpan.children[1]);
|
||||
|
||||
// random color
|
||||
const color = item?.color || Color.getRandom();
|
||||
summary.style.borderLeftColor = color;
|
||||
const [colorInput, colorButton] = colorSpan.children;
|
||||
colorInput.value = color;
|
||||
colorInput.addEventListener('change', e => summary.style.borderLeftColor = e.target.value);
|
||||
|
||||
colorButton.addEventListener('click', e => {
|
||||
e.target.previousElementSibling.value = Color.getRandom();
|
||||
summary.style.borderLeftColor = e.target.previousElementSibling.value;
|
||||
});
|
||||
|
||||
pac.addEventListener('change', e => {
|
||||
const {hostname: h, port: p} = App.parseURL(e.target.value);
|
||||
if (!h) {
|
||||
e.target.classList.add('invalid');
|
||||
return;
|
||||
}
|
||||
hostname.value = h;
|
||||
port.value = p;
|
||||
type.value = 'pac';
|
||||
title.value ||= 'PAC';
|
||||
sumTitle.textContent ||= 'PAC';
|
||||
});
|
||||
|
||||
// ---patterns
|
||||
pxy.querySelector('button[data-i18n="add|title"]').addEventListener('click', e => {
|
||||
const elem = this.addPattern();
|
||||
e.ctrlKey ? patternBox.prepend(elem) : patternBox.append(elem);
|
||||
elem.children[4].focus();
|
||||
});
|
||||
pxy.querySelector('input[type="file"]').addEventListener('change', e => this.importPattern(e, patternBox));
|
||||
pxy.querySelector('button[data-i18n^="export"]').addEventListener('click', () =>
|
||||
this.exportPattern(patternBox, title.value.trim() || hostname.value.trim()));
|
||||
|
||||
// --- from add button
|
||||
if (!item) {
|
||||
return [pxy, title];
|
||||
}
|
||||
|
||||
// show/hide elements
|
||||
pxy.dataset.type = item.type;
|
||||
|
||||
const id = item.type === 'pac' ? item.pac : `${item.hostname}:${item.port}`;
|
||||
// cache to find later
|
||||
this.proxyCache[id] = item;
|
||||
|
||||
// --- populate with data
|
||||
const pxyTitle = item.title || id;
|
||||
|
||||
// --- summary
|
||||
flag.textContent = Flag.show(item);
|
||||
sumTitle.textContent = pxyTitle;
|
||||
active.checked = item.active;
|
||||
|
||||
// proxy details
|
||||
title.value = item.title;
|
||||
hostname.value = item.hostname;
|
||||
type.value = item.type;
|
||||
port.value = item.port;
|
||||
cc.value = item.cc;
|
||||
// used in "Get Location"
|
||||
cc.dataset.hostname = item.hostname;
|
||||
username.value = item.username;
|
||||
city.value = item.city;
|
||||
// used in "Get Location"
|
||||
city.dataset.hostname = item.hostname;
|
||||
passwordSpan.children[0].value = item.password;
|
||||
|
||||
pac.value = item.pac;
|
||||
storeLocallyLabel.children[0].checked = !!item.pacString;
|
||||
view.addEventListener('click', () => PAC.view(pac.value.trim()));
|
||||
|
||||
proxyDNS.checked = item.proxyDNS;
|
||||
|
||||
// --- patterns
|
||||
patternBox.dataset.host = `${item.hostname}:${item.port}`;
|
||||
item.tabProxy ||= [];
|
||||
item.include.forEach(i => patternBox.append(this.addPattern(i, 'include')));
|
||||
item.exclude.forEach(i => patternBox.append(this.addPattern(i, 'exclude')));
|
||||
item.tabProxy.forEach(i => patternBox.append(this.addPattern(i, 'tabProxy')));
|
||||
return pxy;
|
||||
}
|
||||
|
||||
static addPattern(item, inc) {
|
||||
// --- make a blank pattern with all event listeners
|
||||
const div = this.patternTemplate.cloneNode(true);
|
||||
const [quickAdd, include, type, title, pattern, active, test, del] = div.children;
|
||||
|
||||
quickAdd.addEventListener('change', e => {
|
||||
const opt = e.target.selectedOptions[0];
|
||||
type.value = opt.dataset.type;
|
||||
title.value = opt.textContent;
|
||||
pattern.value = opt.value;
|
||||
// reset select option
|
||||
e.target.selectedIndex = 0;
|
||||
});
|
||||
|
||||
test.addEventListener('click', () => {
|
||||
Tester.select.value = type.value;
|
||||
Tester.pattern.value = pattern.value;
|
||||
Tester.target = pattern;
|
||||
// reset pre DOM
|
||||
Tester.pre.textContent = Tester.pre.textContent.trim();
|
||||
// navigate to Tester tab
|
||||
Nav.get('tester');
|
||||
});
|
||||
|
||||
// del.addEventListener('click', () => confirm(browser.i18n.getMessage('deleteConfirm')) && div.remove());
|
||||
del.addEventListener('click', () => div.remove());
|
||||
|
||||
if (item) {
|
||||
include.value = inc;
|
||||
type.value = item.type;
|
||||
title.value = item.title;
|
||||
pattern.value = item.pattern;
|
||||
active.checked = item.active;
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
static async duplicateProxy(item) {
|
||||
// generating a new proxy as cloneNode() removes event listeners
|
||||
const pxy = await this.getProxyDetails(item);
|
||||
if (!pxy) { return; }
|
||||
|
||||
item.after(this.addProxy(pxy));
|
||||
// close orig proxy
|
||||
item.open = false;
|
||||
// open duplicated proxy
|
||||
item.nextElementSibling.open = true;
|
||||
}
|
||||
|
||||
// import pattern from log.js
|
||||
static importPatternCustom(e) {
|
||||
const {host, data} = e.detail;
|
||||
if (!host || !data) { return; }
|
||||
|
||||
const patternBox = document.querySelector(`.pattern-box[data-host="${host}"]`);
|
||||
if (!patternBox) { return; }
|
||||
|
||||
data.forEach(i => patternBox.append(this.addPattern(i, i.include)));
|
||||
patternBox.closest('details').open = true;
|
||||
patternBox.lastElementChild.children[4].focus();
|
||||
}
|
||||
|
||||
static importPattern(e, patternBox) {
|
||||
const file = e.target.files[0];
|
||||
switch (true) {
|
||||
case !file: App.notify(browser.i18n.getMessage('error'));
|
||||
return;
|
||||
// check file MIME type
|
||||
case !['text/plain', 'application/json'].includes(file.type):
|
||||
App.notify(browser.i18n.getMessage('fileTypeError'));
|
||||
return;
|
||||
}
|
||||
|
||||
ImportExport.fileReader(file, data => {
|
||||
try { data = JSON.parse(data); }
|
||||
catch {
|
||||
// display the error
|
||||
App.notify(browser.i18n.getMessage('fileParseError'));
|
||||
return;
|
||||
}
|
||||
|
||||
Array.isArray(data) && data.forEach(i => patternBox.append(this.addPattern(i, i.include)));
|
||||
});
|
||||
}
|
||||
|
||||
static exportPattern(patternBox, title = '') {
|
||||
const arr = [...patternBox.children].map(item => {
|
||||
const elem = item.children;
|
||||
return {
|
||||
include: elem[1].value,
|
||||
type: elem[2].value,
|
||||
title: elem[3].value.trim(),
|
||||
pattern: elem[4].value.trim(),
|
||||
active: elem[5].checked,
|
||||
};
|
||||
});
|
||||
|
||||
// no patterns to export
|
||||
if (!arr[0]) { return; }
|
||||
|
||||
title &&= '_' + title;
|
||||
const data = JSON.stringify(arr, null, 2);
|
||||
const filename = `${browser.i18n.getMessage('pattern')}${title}_${new Date().toISOString().substring(0, 10)}.json`;
|
||||
ImportExport.saveFile({data, filename, type: 'application/json'});
|
||||
}
|
||||
|
||||
static async getProxyDetails(elem) {
|
||||
// proxy template
|
||||
const obj = {
|
||||
active: true,
|
||||
title: '',
|
||||
type: 'http',
|
||||
hostname: '',
|
||||
port: '',
|
||||
username: '',
|
||||
password: '',
|
||||
cc: '',
|
||||
city: '',
|
||||
color: '',
|
||||
pac: '',
|
||||
pacString: '',
|
||||
proxyDNS: true,
|
||||
include: [],
|
||||
exclude: [],
|
||||
tabProxy: [],
|
||||
};
|
||||
|
||||
// --- populate values
|
||||
elem.querySelectorAll('[data-id]').forEach(i => {
|
||||
// reset
|
||||
i.classList.remove('invalid');
|
||||
Object.hasOwn(obj, i.dataset.id) && (obj[i.dataset.id] = i.type === 'checkbox' ? i.checked : i.value.trim());
|
||||
});
|
||||
|
||||
// --- check type: http | https | socks4 | socks5 | quic | pac | direct
|
||||
switch (true) {
|
||||
// DIRECT
|
||||
case obj.type === 'direct':
|
||||
obj.hostname = 'DIRECT';
|
||||
break;
|
||||
|
||||
// PAC
|
||||
case obj.type === 'pac':
|
||||
const {hostname, port} = App.parseURL(obj.pac);
|
||||
if (!hostname) {
|
||||
this.setInvalid(elem, 'pac');
|
||||
// alert(browser.i18n.getMessage('pacUrlError'));
|
||||
return;
|
||||
}
|
||||
obj.hostname = hostname;
|
||||
obj.port = port;
|
||||
break;
|
||||
|
||||
// http | https | socks4 | socks5 | quic
|
||||
case !obj.hostname:
|
||||
this.setInvalid(elem, 'hostname');
|
||||
alert(browser.i18n.getMessage('hostnamePortError'));
|
||||
return;
|
||||
|
||||
case !obj.port:
|
||||
this.setInvalid(elem, 'port');
|
||||
alert(browser.i18n.getMessage('hostnamePortError'));
|
||||
return;
|
||||
}
|
||||
|
||||
// --- title check
|
||||
if (!obj.title) {
|
||||
const id = obj.type === 'pac' ? obj.pac : `${obj.hostname}:${obj.port}`;
|
||||
elem.children[0].children[1].textContent = id;
|
||||
}
|
||||
|
||||
// --- check store locally for active PAC
|
||||
if (obj.active && obj.pac) {
|
||||
const storeLocally = elem.querySelector('.pac input[type="checkbox"]');
|
||||
if (storeLocally.checked) {
|
||||
const str = await PAC.get(obj.pac);
|
||||
/function\s+FindProxyForURL\s*\(/.test(str) && (obj.pacString = str.trim());
|
||||
}
|
||||
}
|
||||
|
||||
// --- check & build patterns
|
||||
const cache = [];
|
||||
for (const item of elem.querySelectorAll('.pattern-box .pattern-row')) {
|
||||
const [, inc, type, title, pattern, active] = item.children;
|
||||
// reset
|
||||
pattern.classList.remove('invalid');
|
||||
const pat = {
|
||||
type: type.value,
|
||||
title: title.value.trim(),
|
||||
pattern: pattern.value.trim(),
|
||||
active: active.checked,
|
||||
};
|
||||
|
||||
// --- test pattern
|
||||
// blank pattern
|
||||
if (!pat.pattern) { continue; }
|
||||
|
||||
if (!Pattern.validate(pat.pattern, pat.type, true)) {
|
||||
// show Proxy tab
|
||||
Nav.get('proxies');
|
||||
const details = item.closest('details');
|
||||
// open proxy
|
||||
details.open = true;
|
||||
pattern.classList.add('invalid');
|
||||
pattern.scrollIntoView({behavior: 'smooth'});
|
||||
return;
|
||||
}
|
||||
|
||||
// check for duplicate
|
||||
if (cache.includes(pat.pattern)) {
|
||||
item.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// cache to check for duplicates
|
||||
cache.push(pat.pattern);
|
||||
obj[inc.value].push(pat);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
static setInvalid(parent, id) {
|
||||
parent.open = true;
|
||||
const elem = parent.querySelector(`[data-id="${id}"]`);
|
||||
elem.classList.add('invalid');
|
||||
parent.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/* ----- show/hide elements ----- */
|
||||
details.proxy[data-type="direct"] :is(
|
||||
[data-i18n="port"], [data-id="port"],
|
||||
[data-i18n="username"], [data-id="username"],
|
||||
[data-i18n="password"], .password,
|
||||
[data-i18n="country"], [data-id="cc"],
|
||||
[data-i18n="city"], [data-id="city"],
|
||||
[data-type="pac"], .pac) {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details.proxy[data-type="pac"] :is(
|
||||
[data-i18n="port"], [data-id="port"],
|
||||
[data-i18n="username"], [data-id="username"],
|
||||
[data-i18n="password"], .password) {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details.proxy[data-type="pac"] :is(.pattern-head, .pattern-box) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details.proxy:not([data-type="pac"]) :is([data-type="pac"], .pac) {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details.proxy[data-type="socks4"] :is(
|
||||
[data-i18n="username"], [data-id="username"],
|
||||
[data-i18n="password"], .password) {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details.proxy:not([data-type="socks5"]) :is([data-i18n="proxyDNS"], [data-id="proxyDNS"]) {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* --- Chrome/Firefox --- */
|
||||
body.chrome .firefox,
|
||||
body:not(.chrome) .chrome,
|
||||
body.chrome details.proxy[data-type="socks5"] :is(
|
||||
[data-i18n="username"], [data-id="username"],
|
||||
[data-i18n="password"], .password) {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
caption.firefox {
|
||||
color: var(--nav-color);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body.chrome caption.firefox {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* --- Basic --- */
|
||||
.basic :is(.pattern-head, .pattern-box) {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
@import 'default.css';
|
||||
@import 'progress-bar.css';
|
||||
@import 'spinner.css';
|
||||
@import 'nav.css';
|
||||
@import 'theme.css';
|
||||
@import 'options-show.css';
|
||||
@import 'options-popup.css';
|
||||
@import 'toggle-switch.css';
|
||||
@import 'tester.css';
|
||||
@import 'log.css';
|
||||
|
||||
/* ----- Light Theme ----- */
|
||||
:root {
|
||||
--nav-height: 3rem;
|
||||
--max-width: 75rem;
|
||||
--pass: #080;
|
||||
--fail: #f00;
|
||||
}
|
||||
|
||||
/* ----- Dark Theme ----- */
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--pass: #0c0;
|
||||
--fail: #f90;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----- General ----- */
|
||||
html {
|
||||
scroll-padding-top: var(--nav-height);
|
||||
}
|
||||
|
||||
body {
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.5s;
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
background-image: url('../image/logo.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
right: 0;
|
||||
display: inline-block;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
opacity: 0.3;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
section {
|
||||
display: none;
|
||||
margin: 0 auto;
|
||||
max-width: var(--max-width);
|
||||
height: calc(100vh - var(--nav-height) - 2rem);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 99%;
|
||||
}
|
||||
|
||||
/* img {
|
||||
vertical-align: text-bottom;
|
||||
} */
|
||||
|
||||
label:has(input[type="checkbox"]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label > input[type="checkbox"] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
input[type="number"] {
|
||||
width: 4em;
|
||||
}
|
||||
*/
|
||||
|
||||
textarea {
|
||||
min-height: 6em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
background-color: var(--bg);
|
||||
border-radius: 0.5em;
|
||||
border: 0;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
/* fieldset label img {
|
||||
width: 1em;
|
||||
} */
|
||||
|
||||
.description {
|
||||
border-left: 2px solid #abc;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-left: 0.5em;
|
||||
margin-top: 0;
|
||||
padding-left: 0.3em;
|
||||
}
|
||||
|
||||
details {
|
||||
padding: 0;
|
||||
background-color: var(--alt-bg);
|
||||
border-radius: 5px;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
details > summary {
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
details > summary:hover,
|
||||
details[open] > summary {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
summary ~ * {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
details[open] summary ~ * {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ----- Import/Export ----- */
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
/* ----- /Import/Export ----- */
|
||||
|
||||
/* ----- Submit Button ----- */
|
||||
/* button[type="submit"] {
|
||||
display: table;
|
||||
color: #fff;
|
||||
background-color: var(--btn-bg);
|
||||
font-size:0.9em;
|
||||
margin: 1em auto 0;
|
||||
padding: 0.5em 5em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background-color: var(--btn-hover);
|
||||
} */
|
||||
/* ----- /Submit Button ----- */
|
||||
|
||||
/* ----- Button ----- */
|
||||
button.bin,
|
||||
button.test,
|
||||
button.close {
|
||||
width: 1.5em;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button.bin::before,
|
||||
button.test::before {
|
||||
content: '';
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
background: url('../image/bin.svg') no-repeat center / contain;
|
||||
}
|
||||
|
||||
button.test::before {
|
||||
background: url('../image/beaker.svg') no-repeat center / contain;
|
||||
}
|
||||
|
||||
button.close::before {
|
||||
content: '❌';
|
||||
}
|
||||
|
||||
.up:hover, .down:hover {
|
||||
color: #080;
|
||||
}
|
||||
|
||||
button.random {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
button.slim {
|
||||
padding: 0.2em 1em;
|
||||
min-width: unset;
|
||||
}
|
||||
/* ----- /Button ----- */
|
||||
|
||||
/* ----- data-link ----- */
|
||||
[data-link] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-link]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
[data-link]::before {
|
||||
content: '#';
|
||||
display: inline-block;
|
||||
width: 0.8em;
|
||||
margin-left: -0.8em;
|
||||
opacity: 0.5;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
[data-link]:hover::before {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
footer span[data-link] {
|
||||
font-size: 0.9em;
|
||||
place-self: end;
|
||||
}
|
||||
/* ----- /data-link ----- */
|
||||
|
||||
/* ----- Options ----- */
|
||||
/*
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="url"],
|
||||
select,
|
||||
textarea {
|
||||
font-size: 1em;
|
||||
} */
|
||||
|
||||
section.options fieldset * {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
border: 0;
|
||||
width: 4em;
|
||||
height: 1.7em;
|
||||
}
|
||||
|
||||
.options .buttons {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
justify-content: end;
|
||||
column-gap: 0.2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.options .theme,
|
||||
.options .container,
|
||||
.options .commands {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(10em, max-content) max-content;
|
||||
gap: 0.2em 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.options .container label,
|
||||
.options .commands label {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.container button {
|
||||
grid-column: span 2;
|
||||
justify-self: right;
|
||||
/* padding: 0.2em;
|
||||
min-width: 6em; */
|
||||
}
|
||||
|
||||
/* button strip */
|
||||
/* .options .buttons {
|
||||
padding: 0 1.5em;
|
||||
margin: 0 -1.5em 1em;
|
||||
background-color: var(--btn-bg);
|
||||
}
|
||||
|
||||
.options .buttons > * {
|
||||
border-left: 2px solid var(--bg);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.options .buttons > *:last-child {
|
||||
border-right: 2px solid var(--bg);
|
||||
} */
|
||||
|
||||
|
||||
|
||||
/* ----- /Options ----- */
|
||||
details .content {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
/* ----- Proxy ----- */
|
||||
.proxy-top {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto auto;
|
||||
gap: 0.2em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.proxy-top input.filter {
|
||||
background: url('../image/filter.svg') no-repeat left 0.5em center / 1em;
|
||||
padding-left: 2em;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
details.proxy .content * {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
details.proxy.off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details.proxy > summary {
|
||||
list-style: none;
|
||||
border-left: 1.5em solid var(--btn-bg);
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr repeat(5, auto);
|
||||
gap: 0.2em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
details.proxy > summary button.plain {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
details.proxy > summary span:first-of-type {
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
|
||||
details.proxy > summary span:nth-of-type(2):empty::before {
|
||||
content: 'title ...';
|
||||
font-style: italic;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.proxy-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 2fr;
|
||||
/* grid-auto-rows: min-content; */
|
||||
gap: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.password {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1em;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.proxy-box button.random {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.pac {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.2em 0.5em;
|
||||
font-size: 0.9em;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
.pac [type="url"] {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.pac button {
|
||||
/* padding: 0.2em 1em;
|
||||
min-width: unset; */
|
||||
place-self: center end;
|
||||
}
|
||||
|
||||
|
||||
/* ----- Pattern ----- */
|
||||
.pattern-box {
|
||||
padding: 0.2em;
|
||||
max-height: 20em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pattern-head,
|
||||
.pattern-row {
|
||||
display: grid;
|
||||
grid-template-columns: 3em 6em 6em 1fr 2fr 2em 1em 1em;
|
||||
gap: 0.2em;
|
||||
place-items: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.pattern-head {
|
||||
background-color: var(--bg);
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.pattern-head button {
|
||||
/* padding: 0.2em 1em;
|
||||
min-width: unset; */
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
/* .pattern-head button[data-i18n="add|title"] {
|
||||
grid-column: span 3;
|
||||
} */
|
||||
|
||||
.pattern-head button span.plus {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.pattern-head :nth-child(2) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
|
||||
/* import & export (label & button) */
|
||||
.pattern-head .plain {
|
||||
font-size: 1em;
|
||||
line-height: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pattern-head :last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
/* hide when there are no patterns */
|
||||
.pattern-head:has(+ .pattern-box:empty) :nth-child(n+3) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pattern-row {
|
||||
margin-top: 0.2em;
|
||||
padding: 0 0.4em;
|
||||
}
|
||||
|
||||
/* .pattern-row input:nth-of-type(2) {
|
||||
grid-column: span 3;
|
||||
} */
|
||||
|
||||
.proxy summary button,
|
||||
.pattern-box button.test,
|
||||
.pattern-box button.bin {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
/* border: none; */
|
||||
font-size: 1em;
|
||||
transition: 0.5s;
|
||||
color: #ccc;
|
||||
font-weight: 900;
|
||||
text-shadow: 0 -1px 1px #555, 0 1px 0 #fff;
|
||||
}
|
||||
/* ----- /Pattern ----- */
|
||||
|
||||
/* ----- bulk edit ----- */
|
||||
.bulk-edit {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 3.5em) 10em auto;
|
||||
justify-content: end;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
details.proxy.t1 {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
details.proxy:is(.t1, .s1) .pattern-row.t2 {
|
||||
outline: 1px solid blue;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
details.proxy.s1 {
|
||||
outline: 1px solid green;
|
||||
}
|
||||
|
||||
.proxy-div:empty ~ .bulk-edit {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
/* ----- /bulk-edit ----- */
|
||||
|
||||
/* ----- Import ----- */
|
||||
.import textarea {
|
||||
height: 10em;
|
||||
}
|
||||
|
||||
.import summary::marker {
|
||||
color: var(--header);
|
||||
}
|
||||
|
||||
.import details.import-account .content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr 1em;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.import details.import-account input#username {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.import .import-account .account-options {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-column: span 2;
|
||||
justify-content: start;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.import details button.flat {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.import details.import-account button.flat {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
|
||||
/* ----- /import ----- */
|
||||
|
||||
/* ----- Counter ----- */
|
||||
/* .proxy-section {
|
||||
counter-reset: n;
|
||||
}
|
||||
|
||||
.proxy-section details summary :first-child::before {
|
||||
counter-increment: n;
|
||||
content: counter(n);
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
color: var(--dim);
|
||||
} */
|
||||
/* ----- /Counter ----- */
|
||||
@@ -0,0 +1,781 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title data-i18n="options">FoxyProxy </title>
|
||||
<link href="../image/icon.svg" rel="icon" type="image/png">
|
||||
<link href="options.css" rel="stylesheet">
|
||||
<script type="module" src="options.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Navigation -->
|
||||
<input type="radio" name="nav" id="nav1">
|
||||
<input type="radio" name="nav" id="nav2">
|
||||
<input type="radio" name="nav" id="nav3" checked>
|
||||
<input type="radio" name="nav" id="nav4">
|
||||
<input type="radio" name="nav" id="nav5">
|
||||
<input type="radio" name="nav" id="nav6">
|
||||
<input type="radio" name="nav" id="nav7">
|
||||
|
||||
<div class="nav">
|
||||
<nav>
|
||||
<img src="../image/icon.svg" alt="">
|
||||
<label for="nav3" data-i18n="options"></label>
|
||||
<label for="nav4" data-i18n="proxies"></label>
|
||||
<label for="nav5" data-i18n="import"></label>
|
||||
<label for="nav6" data-i18n="tester"></label>
|
||||
<label for="nav7" data-i18n="log"></label>
|
||||
<label for="nav1" data-i18n="help"></label>
|
||||
<label for="nav2" data-i18n="about"></label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar"></div>
|
||||
|
||||
<article>
|
||||
<!-- spinner -->
|
||||
<div class="spinner"></div>
|
||||
|
||||
<!-- popup -->
|
||||
<div class="popup">
|
||||
<span>✕</span>
|
||||
<textarea autocomplete="off" spellcheck="false"></textarea>
|
||||
<select class="popup-log-proxy" autocomplete="off">
|
||||
<option value="" data-i18n="proxy" disabled selected></option>
|
||||
</select>
|
||||
<select class="popup-test-proxy" autocomplete="off">
|
||||
<option value="" data-i18n="proxy" disabled selected></option>
|
||||
</select>
|
||||
<select class="popup-server" autocomplete="off">
|
||||
<option value="" data-i18n="server" disabled selected></option>
|
||||
<option value="https://api.ipify.org/">ipify.org</option>
|
||||
<option value="https://checkip.amazonaws.com/">amazonaws.com (Amazon)</option>
|
||||
<option value="https://icanhazip.com/">icanhazip.com (Cloudflare)</option>
|
||||
<option value="https://ident.me/">ident.me</option>
|
||||
<option value="https://www.l2.io/ip">l2.io</option>
|
||||
<option value="https://www.trackip.net/ip">trackip.net</option>
|
||||
<option value="https://myip.dnsomatic.com/">dnsomatic.com</option>
|
||||
<hr>
|
||||
<option value="https://getfoxyproxy.org/geoip/?raw=1">getfoxyproxy.org</option>
|
||||
<option value="https://ipinfo.io/ip">ipinfo.io</option>
|
||||
<!-- <option value="https://ifconfig.co/">ifconfig.co</option> -->
|
||||
<option value="https://ipecho.net/plain">ipecho.net</option>
|
||||
<option value="https://whatismyip.akamai.com/">akamai.com</option>
|
||||
<option value="https://myexternalip.com/raw">myexternalip.com</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Help/About -->
|
||||
<section><iframe src="help.html"></iframe></section>
|
||||
<section><iframe src="about.html"></iframe></section>
|
||||
|
||||
<!-- user options -->
|
||||
<section class="options">
|
||||
<fieldset>
|
||||
<div class="buttons">
|
||||
<label class="flat" data-i18n="import"><input type="file" id="file" accept=".json"></label>
|
||||
<button type="button" id="export" class="flat" data-i18n="export"></button>
|
||||
<button type="button" class="flat" data-i18n="deleteBrowsingData" id="deleteBrowsingData"></button>
|
||||
<button type="button" class="flat" data-i18n="restoreDefaults"></button>
|
||||
</div>
|
||||
|
||||
<label data-i18n="enableSync"><input type="checkbox" id="sync"></label>
|
||||
<p class="description" data-i18n="enableSyncDescription"></p>
|
||||
|
||||
<label data-i18n="autoBackup"><input type="checkbox" id="autoBackup"></label>
|
||||
<p class="description" data-i18n="autoBackupDescription"></p>
|
||||
|
||||
<div class="theme" style="margin-bottom: 0;">
|
||||
<label data-i18n="limitWebRTC" data-link="#limit-webrtc"></label>
|
||||
<select id="limitWebRTC">
|
||||
<option value="default" data-i18n="default"></option>
|
||||
<option value="default_public_interface_only" data-i18n="publicInterfaceOnly"></option>
|
||||
<option value="default_public_and_private_interfaces" data-i18n="publicPrivateInterfaces"></option>
|
||||
<option value="disable_non_proxied_udp" data-i18n="disableNonProxied"></option>
|
||||
<option value="proxy_only" data-i18n="proxyOnly"></option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="description" data-i18n="limitWebRTCDescription"></p>
|
||||
|
||||
<!-- <label class="firefox" data-i18n="showPatternProxy"><input type="checkbox" id="showPatternProxy"></label>
|
||||
<p class="description firefox" data-i18n="showPatternProxyDescription"></p> -->
|
||||
|
||||
<div class="theme">
|
||||
<label data-i18n="theme" data-link="#theme"></label>
|
||||
<select id="theme">
|
||||
<option value="" data-i18n="default"></option>
|
||||
<option>moonlight</option>
|
||||
<option>moonlight alt</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label data-i18n="container" data-link="#container"></label>
|
||||
<p class="description" data-i18n="containerDescription"></p>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<label data-i18n="incognito"></label>
|
||||
<select name="incognito">
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
|
||||
<label class="firefox"><span data-i18n="container"></span> 1</label>
|
||||
<select class="firefox" name="container-1">
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
|
||||
<label class="firefox"><span data-i18n="container"></span> 2</label>
|
||||
<select class="firefox" name="container-2">
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
|
||||
<button type="button" class="flat slim firefox" data-i18n="add"></button>
|
||||
|
||||
<!-- container template -->
|
||||
<template>
|
||||
<label class="firefox"><span data-i18n="container"></span> </label>
|
||||
<select class="firefox" name="container-">
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- openShortcutSettings Firefox 137 -->
|
||||
<label data-i18n="shortcut" data-link="#keyboard-shortcut"></label> <button type="button" class="plain shortcut-link" style="margin-left: 0.5em; display: none;">🔗</button>
|
||||
<p class="description" data-i18n="shortcutDescription"></p>
|
||||
|
||||
<div class="commands">
|
||||
|
||||
<label data-i18n="setProxy"></label>
|
||||
<select name="setProxy">
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
|
||||
<label data-i18n="setTabProxy"></label>
|
||||
<select name="setTabProxy">
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
|
||||
<label data-i18n="includeHost"></label>
|
||||
<select name="includeHost">
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
|
||||
<label data-i18n="excludeHost"></label>
|
||||
<select name="excludeHost">
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<label data-i18n="globalExclude" data-link="#global-exclude"></label>
|
||||
<p class="description" data-i18n="globalExcludeDescription"></p>
|
||||
<textarea id="passthrough"></textarea>
|
||||
|
||||
|
||||
<button type="submit" data-i18n="saveOptions"></button>
|
||||
</fieldset>
|
||||
|
||||
|
||||
</section>
|
||||
<!-- /user options -->
|
||||
|
||||
<!-- proxy -->
|
||||
<section class="proxy-section">
|
||||
<fieldset>
|
||||
<div class="proxy-top">
|
||||
<button type="button" class="flat" data-i18n="add"></button>
|
||||
<input type="text" class="filter" autocomplete="off" spellcheck="false" placeholder="filter">
|
||||
|
||||
<button type="button" class="flat" data-i18n="ping">🛜 </button>
|
||||
<button type="button" class="flat" data-i18n="test"></button>
|
||||
<button type="button" class="flat" data-i18n="getLocation"></button>
|
||||
</div>
|
||||
|
||||
<div class="proxy-div"></div>
|
||||
|
||||
<div class="bulk-edit">
|
||||
<input type="number" autocomplete="off" min="1" step="1">
|
||||
<input type="number" autocomplete="off" min="1" step="1">
|
||||
<input type="number" autocomplete="off" min="1" step="1">
|
||||
<input type="text" autocomplete="off" spellcheck="false">
|
||||
|
||||
<select autocomplete="off">
|
||||
<option value="" disabled selected data-i18n="bulkEdit"></option>
|
||||
<option value="openAll" data-i18n="openAll"></option>
|
||||
<option value="closeAll" data-i18n="closeAll"></option>
|
||||
<hr>
|
||||
<option value="deleteProxy" data-i18n="deleteProxy"></option>
|
||||
<option value="moveProxy" data-i18n="moveProxy"></option>
|
||||
<option value="movePattern" data-i18n="movePattern"></option>
|
||||
<hr>
|
||||
<option value="setTitle" data-i18n="setTitle"></option>
|
||||
<option value="setType" data-i18n="setType"></option>
|
||||
<option value="setPort" data-i18n="setPort"></option>
|
||||
<option value="setUsername" data-i18n="setUsername"></option>
|
||||
<option value="setPassword" data-i18n="setPassword"></option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-i18n="saveOptions"></button>
|
||||
|
||||
<footer>
|
||||
<span data-i18n="help" data-link="#proxies"></span>
|
||||
</footer>
|
||||
</fieldset>
|
||||
|
||||
<!-- template -->
|
||||
<template>
|
||||
<details class="proxy" draggable="true">
|
||||
<summary>
|
||||
<span>🌎</span>
|
||||
<span></span>
|
||||
<input type="checkbox" class="toggle" data-id="active" checked>
|
||||
<button type="button" class="plain" data-i18n="duplicate|title">⎘</button>
|
||||
<button type="button" class="bin" data-i18n="delete|title"></button>
|
||||
<button type="button" class="up">▲</button>
|
||||
<button type="button" class="down">▼</button>
|
||||
</summary>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="proxy-box">
|
||||
<label data-i18n="title"></label>
|
||||
<input type="text" data-id="title" spellcheck="false" placeholder="title">
|
||||
|
||||
<label data-i18n="hostname"></label>
|
||||
<input type="text" data-id="hostname" spellcheck="false" placeholder="1.2.3.4, www.example.com">
|
||||
|
||||
<label data-i18n="type"></label>
|
||||
<select data-id="type">
|
||||
<optgroup data-i18n="type|label">
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="socks4">SOCKS4</option>
|
||||
<option value="socks5">SOCKS5</option>
|
||||
<option value="quic">QUIC</option>
|
||||
<option value="pac" data-flag="🌎">PAC</option>
|
||||
<option value="direct" data-hostname="DIRECT" data-flag="⮕">DIRECT</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup data-i18n="server|label">
|
||||
<!-- By default v2rayA will open 20170 (socks5), 20171 (http), 20172 (http with shunt rules) ports through the core -->
|
||||
<option value="http" data-hostname="127.0.0.1" data-port="8080" data-flag="🖥️">Burp</option>
|
||||
<option value="http" data-hostname="127.0.0.1" data-port="8118" data-flag="🖥️">Privoxy</option>
|
||||
<option value="socks5" data-hostname="127.0.0.1" data-port="60351" data-flag="🖥️">Psiphon</option>
|
||||
<option value="socks5" data-hostname="127.0.0.1" data-port="1080" data-flag="🖥️">Shadowsocks</option>
|
||||
<option value="socks5" data-hostname="127.0.0.1" data-port="9050" data-flag="🖥️">TOR</option>
|
||||
|
||||
<option value="http" data-hostname="127.0.0.1" data-port="2081" data-flag="🖥️">NekoRay HTTP</option>
|
||||
<option value="socks5" data-hostname="127.0.0.1" data-port="2080" data-flag="🖥️">NekoRay SOCKS5</option>
|
||||
|
||||
<option value="http" data-hostname="127.0.0.1" data-port="20171" data-flag="🖥️">V2RayA HTTP</option>
|
||||
<option value="http" data-hostname="127.0.0.1" data-port="20172" data-flag="🖥️">V2RayA HTTP (with Rule)</option>
|
||||
<option value="socks5" data-hostname="127.0.0.1" data-port="20170" data-flag="🖥️">V2RayA SOCKS5</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<label data-i18n="port"></label>
|
||||
<input type="text" data-id="port" placeholder="3128">
|
||||
|
||||
<label data-i18n="country"></label>
|
||||
<select data-id="cc">
|
||||
<option value=""> </option>
|
||||
<option value="AF">🇦🇫 Afghanistan</option>
|
||||
<option value="AL">🇦🇱 Albania</option>
|
||||
<option value="DZ">🇩🇿 Algeria</option>
|
||||
<option value="AS">🇦🇸 American Samoa</option>
|
||||
<option value="AD">🇦🇩 Andorra</option>
|
||||
<option value="AO">🇦🇴 Angola</option>
|
||||
<option value="AI">🇦🇮 Anguilla</option>
|
||||
<option value="AQ">🇦🇶 Antarctica</option>
|
||||
<option value="AG">🇦🇬 Antigua and Barbuda</option>
|
||||
<option value="AR">🇦🇷 Argentina</option>
|
||||
<option value="AM">🇦🇲 Armenia</option>
|
||||
<option value="AW">🇦🇼 Aruba</option>
|
||||
<option value="AU">🇦🇺 Australia</option>
|
||||
<option value="AT">🇦🇹 Austria</option>
|
||||
<option value="AZ">🇦🇿 Azerbaijan</option>
|
||||
<option value="AX">🇦🇽 Åland Islands</option>
|
||||
<option value="BS">🇧🇸 Bahamas</option>
|
||||
<option value="BH">🇧🇭 Bahrain</option>
|
||||
<option value="BD">🇧🇩 Bangladesh</option>
|
||||
<option value="BB">🇧🇧 Barbados</option>
|
||||
<option value="BY">🇧🇾 Belarus</option>
|
||||
<option value="BE">🇧🇪 Belgium</option>
|
||||
<option value="BZ">🇧🇿 Belize</option>
|
||||
<option value="BJ">🇧🇯 Benin</option>
|
||||
<option value="BM">🇧🇲 Bermuda</option>
|
||||
<option value="BT">🇧🇹 Bhutan</option>
|
||||
<option value="BO">🇧🇴 Bolivia, Plurinational State of</option>
|
||||
<option value="BQ">🇧🇶 Bonaire, Sint Eustatius and Saba</option>
|
||||
<option value="BA">🇧🇦 Bosnia and Herzegovina</option>
|
||||
<option value="BW">🇧🇼 Botswana</option>
|
||||
<option value="BV">🇧🇻 Bouvet Island</option>
|
||||
<option value="BR">🇧🇷 Brazil</option>
|
||||
<option value="IO">🇮🇴 British Indian Ocean Territory</option>
|
||||
<option value="VG">🇻🇬 British Virgin Islands</option>
|
||||
<option value="BN">🇧🇳 Brunei Darussalam</option>
|
||||
<option value="BG">🇧🇬 Bulgaria</option>
|
||||
<option value="BF">🇧🇫 Burkina Faso</option>
|
||||
<option value="BI">🇧🇮 Burundi</option>
|
||||
<option value="CV">🇨🇻 Cabo Verde</option>
|
||||
<option value="KH">🇰🇭 Cambodia</option>
|
||||
<option value="CM">🇨🇲 Cameroon</option>
|
||||
<option value="CA">🇨🇦 Canada</option>
|
||||
<option value="KY">🇰🇾 Cayman Islands</option>
|
||||
<option value="CF">🇨🇫 Central African Republic</option>
|
||||
<option value="TD">🇹🇩 Chad</option>
|
||||
<option value="CL">🇨🇱 Chile</option>
|
||||
<option value="CN">🇨🇳 China</option>
|
||||
<option value="CX">🇨🇽 Christmas Island</option>
|
||||
<option value="CC">🇨🇨 Cocos [Keeling] Islands</option>
|
||||
<option value="CO">🇨🇴 Colombia</option>
|
||||
<option value="KM">🇰🇲 Comoros</option>
|
||||
<option value="CG">🇨🇬 Congo, Republic of the</option>
|
||||
<option value="CD">🇨🇩 Congo, the Democratic Republic of the</option>
|
||||
<option value="CK">🇨🇰 Cook Islands</option>
|
||||
<option value="CR">🇨🇷 Costa Rica</option>
|
||||
<option value="HR">🇭🇷 Croatia</option>
|
||||
<option value="CU">🇨🇺 Cuba</option>
|
||||
<option value="CW">🇨🇼 Curaçao</option>
|
||||
<option value="CY">🇨🇾 Cyprus</option>
|
||||
<option value="CZ">🇨🇿 Czechia</option>
|
||||
<option value="TL">🇹🇱 Democratic Republic of Timor-Leste</option>
|
||||
<option value="DK">🇩🇰 Denmark</option>
|
||||
<option value="DJ">🇩🇯 Djibouti</option>
|
||||
<option value="DM">🇩🇲 Dominica</option>
|
||||
<option value="DO">🇩🇴 Dominican Republic</option>
|
||||
<option value="EC">🇪🇨 Ecuador</option>
|
||||
<option value="EG">🇪🇬 Egypt</option>
|
||||
<option value="SV">🇸🇻 El Salvador</option>
|
||||
<option value="GQ">🇬🇶 Equatorial Guinea</option>
|
||||
<option value="ER">🇪🇷 Eritrea</option>
|
||||
<option value="EE">🇪🇪 Estonia</option>
|
||||
<option value="SZ">🇸🇿 Eswatini</option>
|
||||
<option value="ET">🇪🇹 Ethiopia</option>
|
||||
<option value="EU">🇪🇺 European Union</option>
|
||||
<option value="FK">🇫🇰 Falkland Islands (Malvinas)</option>
|
||||
<option value="FO">🇫🇴 Faroe Islands</option>
|
||||
<option value="FJ">🇫🇯 Fiji</option>
|
||||
<option value="FI">🇫🇮 Finland</option>
|
||||
<option value="FR">🇫🇷 France</option>
|
||||
<option value="GF">🇬🇫 French Guiana</option>
|
||||
<option value="PF">🇵🇫 French Polynesia</option>
|
||||
<option value="TF">🇹🇫 French Southern Territories</option>
|
||||
<option value="GA">🇬🇦 Gabon</option>
|
||||
<option value="GM">🇬🇲 Gambia</option>
|
||||
<option value="GE">🇬🇪 Georgia</option>
|
||||
<option value="DE">🇩🇪 Germany</option>
|
||||
<option value="GH">🇬🇭 Ghana</option>
|
||||
<option value="GI">🇬🇮 Gibraltar</option>
|
||||
<option value="GR">🇬🇷 Greece</option>
|
||||
<option value="GL">🇬🇱 Greenland</option>
|
||||
<option value="GD">🇬🇩 Grenada</option>
|
||||
<option value="GP">🇬🇵 Guadeloupe</option>
|
||||
<option value="GU">🇬🇺 Guam</option>
|
||||
<option value="GT">🇬🇹 Guatemala</option>
|
||||
<option value="GG">🇬🇬 Guernsey</option>
|
||||
<option value="GN">🇬🇳 Guinea</option>
|
||||
<option value="GW">🇬🇼 Guinea-Bissau</option>
|
||||
<option value="GY">🇬🇾 Guyana</option>
|
||||
<option value="HT">🇭🇹 Haiti</option>
|
||||
<option value="HM">🇭🇲 Heard Island and McDonald Islands</option>
|
||||
<option value="HN">🇭🇳 Honduras</option>
|
||||
<option value="HK">🇭🇰 Hong Kong</option>
|
||||
<option value="HU">🇭🇺 Hungary</option>
|
||||
<option value="IS">🇮🇸 Iceland</option>
|
||||
<option value="IN">🇮🇳 India</option>
|
||||
<option value="ID">🇮🇩 Indonesia</option>
|
||||
<option value="IR">🇮🇷 Iran, Islamic Republic Of</option>
|
||||
<option value="IQ">🇮🇶 Iraq</option>
|
||||
<option value="IE">🇮🇪 Ireland</option>
|
||||
<option value="IM">🇮🇲 Isle of Man</option>
|
||||
<option value="IL">🇮🇱 Israel</option>
|
||||
<option value="IT">🇮🇹 Italy</option>
|
||||
<option value="CI">🇨🇮 Ivory Coast (Côte d'Ivoire)</option>
|
||||
<option value="JM">🇯🇲 Jamaica</option>
|
||||
<option value="JP">🇯🇵 Japan</option>
|
||||
<option value="JE">🇯🇪 Jersey</option>
|
||||
<option value="JO">🇯🇴 Jordan (Hashemite Kingdom of Jordan)</option>
|
||||
<option value="KZ">🇰🇿 Kazakhstan</option>
|
||||
<option value="KE">🇰🇪 Kenya</option>
|
||||
<option value="KI">🇰🇮 Kiribati</option>
|
||||
<option value="XK">🇽🇰 Kosovo</option>
|
||||
<option value="KW">🇰🇼 Kuwait</option>
|
||||
<option value="KG">🇰🇬 Kyrgyzstan</option>
|
||||
<option value="LA">🇱🇦 Laos (Lao People's Democratic Republic)</option>
|
||||
<option value="LV">🇱🇻 Latvia</option>
|
||||
<option value="LB">🇱🇧 Lebanon</option>
|
||||
<option value="LS">🇱🇸 Lesotho</option>
|
||||
<option value="LR">🇱🇷 Liberia</option>
|
||||
<option value="LY">🇱🇾 Libya</option>
|
||||
<option value="LI">🇱🇮 Liechtenstein</option>
|
||||
<option value="LT">🇱🇹 Lithuania</option>
|
||||
<option value="LU">🇱🇺 Luxembourg</option>
|
||||
<option value="MO">🇲🇴 Macao</option>
|
||||
<option value="MG">🇲🇬 Madagascar</option>
|
||||
<option value="MW">🇲🇼 Malawi</option>
|
||||
<option value="MY">🇲🇾 Malaysia</option>
|
||||
<option value="MV">🇲🇻 Maldives</option>
|
||||
<option value="ML">🇲🇱 Mali</option>
|
||||
<option value="MT">🇲🇹 Malta</option>
|
||||
<option value="MH">🇲🇭 Marshall Islands</option>
|
||||
<option value="MQ">🇲🇶 Martinique</option>
|
||||
<option value="MR">🇲🇷 Mauritania</option>
|
||||
<option value="MU">🇲🇺 Mauritius</option>
|
||||
<option value="YT">🇾🇹 Mayotte</option>
|
||||
<option value="MX">🇲🇽 Mexico</option>
|
||||
<option value="FM">🇫🇲 Micronesia, Federated States of</option>
|
||||
<option value="MD">🇲🇩 Moldova, Republic of</option>
|
||||
<option value="MC">🇲🇨 Monaco</option>
|
||||
<option value="MN">🇲🇳 Mongolia</option>
|
||||
<option value="ME">🇲🇪 Montenegro</option>
|
||||
<option value="MS">🇲🇸 Montserrat</option>
|
||||
<option value="MA">🇲🇦 Morocco</option>
|
||||
<option value="MZ">🇲🇿 Mozambique</option>
|
||||
<option value="MM">🇲🇲 Myanmar</option>
|
||||
<option value="NA">🇳🇦 Namibia</option>
|
||||
<option value="NR">🇳🇷 Nauru</option>
|
||||
<option value="NP">🇳🇵 Nepal</option>
|
||||
<option value="NL">🇳🇱 Netherlands</option>
|
||||
<option value="NC">🇳🇨 New Caledonia</option>
|
||||
<option value="NZ">🇳🇿 New Zealand</option>
|
||||
<option value="NI">🇳🇮 Nicaragua</option>
|
||||
<option value="NE">🇳🇪 Niger</option>
|
||||
<option value="NG">🇳🇬 Nigeria</option>
|
||||
<option value="NU">🇳🇺 Niue</option>
|
||||
<option value="NF">🇳🇫 Norfolk Island</option>
|
||||
<option value="KP">🇰🇵 North Korea</option>
|
||||
<option value="MK">🇲🇰 North Macedonia</option>
|
||||
<option value="MP">🇲🇵 Northern Mariana Islands</option>
|
||||
<option value="NO">🇳🇴 Norway</option>
|
||||
<option value="OM">🇴🇲 Oman</option>
|
||||
<option value="PK">🇵🇰 Pakistan</option>
|
||||
<option value="PW">🇵🇼 Palau</option>
|
||||
<option value="PS">🇵🇸 Palestine</option>
|
||||
<option value="PA">🇵🇦 Panama</option>
|
||||
<option value="PG">🇵🇬 Papua New Guinea</option>
|
||||
<option value="PY">🇵🇾 Paraguay</option>
|
||||
<option value="PE">🇵🇪 Peru</option>
|
||||
<option value="PH">🇵🇭 Philippines</option>
|
||||
<option value="PN">🇵🇳 Pitcairn Islands</option>
|
||||
<option value="PL">🇵🇱 Poland</option>
|
||||
<option value="PT">🇵🇹 Portugal</option>
|
||||
<option value="PR">🇵🇷 Puerto Rico</option>
|
||||
<option value="QA">🇶🇦 Qatar</option>
|
||||
<option value="LT">🇱🇹 Republic of Lithuania</option>
|
||||
<option value="RO">🇷🇴 Romania</option>
|
||||
<option value="RU">🇷🇺 Russia (Russian Federation)</option>
|
||||
<option value="RW">🇷🇼 Rwanda</option>
|
||||
<option value="RE">🇷🇪 Réunion</option>
|
||||
<option value="BL">🇧🇱 Saint Barthélemy</option>
|
||||
<option value="SH">🇸🇭 Saint Helena</option>
|
||||
<option value="LC">🇱🇨 Saint Lucia</option>
|
||||
<option value="MF">🇲🇫 Saint Martin (French part)</option>
|
||||
<option value="PM">🇵🇲 Saint Pierre and Miquelon</option>
|
||||
<option value="VC">🇻🇨 Saint Vincent and the Grenadines</option>
|
||||
<option value="WS">🇼🇸 Samoa</option>
|
||||
<option value="SM">🇸🇲 San Marino</option>
|
||||
<option value="SA">🇸🇦 Saudi Arabia</option>
|
||||
<option value="SN">🇸🇳 Senegal</option>
|
||||
<option value="RS">🇷🇸 Serbia</option>
|
||||
<option value="SC">🇸🇨 Seychelles</option>
|
||||
<option value="SL">🇸🇱 Sierra Leone</option>
|
||||
<option value="SG">🇸🇬 Singapore</option>
|
||||
<option value="SX">🇸🇽 Sint Maarten (Dutch part)</option>
|
||||
<option value="SK">🇸🇰 Slovakia</option>
|
||||
<option value="SI">🇸🇮 Slovenia</option>
|
||||
<option value="SB">🇸🇧 Solomon Islands</option>
|
||||
<option value="SO">🇸🇴 Somalia</option>
|
||||
<option value="ZA">🇿🇦 South Africa</option>
|
||||
<option value="GS">🇬🇸 South Georgia and the South Sandwich Islands</option>
|
||||
<option value="KR">🇰🇷 South Korea</option>
|
||||
<option value="SS">🇸🇸 South Sudan</option>
|
||||
<option value="ES">🇪🇸 Spain</option>
|
||||
<option value="LK">🇱🇰 Sri Lanka</option>
|
||||
<option value="KN">🇰🇳 St Kitts and Nevis</option>
|
||||
<option value="SD">🇸🇩 Sudan</option>
|
||||
<option value="SR">🇸🇷 Suriname</option>
|
||||
<option value="SJ">🇸🇯 Svalbard and Jan Mayen</option>
|
||||
<option value="SE">🇸🇪 Sweden</option>
|
||||
<option value="CH">🇨🇭 Switzerland</option>
|
||||
<option value="SY">🇸🇾 Syria</option>
|
||||
<option value="ST">🇸🇹 São Tomé and Príncipe</option>
|
||||
<option value="TW">🇹🇼 Taiwan</option>
|
||||
<option value="TJ">🇹🇯 Tajikistan</option>
|
||||
<option value="TZ">🇹🇿 Tanzania</option>
|
||||
<option value="TH">🇹🇭 Thailand</option>
|
||||
<option value="TG">🇹🇬 Togo</option>
|
||||
<option value="TK">🇹🇰 Tokelau</option>
|
||||
<option value="TO">🇹🇴 Tonga</option>
|
||||
<option value="TT">🇹🇹 Trinidad and Tobago</option>
|
||||
<option value="TN">🇹🇳 Tunisia</option>
|
||||
<option value="TM">🇹🇲 Turkmenistan</option>
|
||||
<option value="TC">🇹🇨 Turks and Caicos Islands</option>
|
||||
<option value="TV">🇹🇻 Tuvalu</option>
|
||||
<option value="TR">🇹🇷 Türkiye</option>
|
||||
<option value="UM">🇺🇲 U.S. Minor Outlying Islands</option>
|
||||
<option value="VI">🇻🇮 U.S. Virgin Islands</option>
|
||||
<option value="UG">🇺🇬 Uganda</option>
|
||||
<option value="UA">🇺🇦 Ukraine</option>
|
||||
<option value="AE">🇦🇪 United Arab Emirates</option>
|
||||
<option value="GB">🇬🇧 United Kingdom</option>
|
||||
<option value="US">🇺🇸 United States of America</option>
|
||||
<option value="UY">🇺🇾 Uruguay</option>
|
||||
<option value="UZ">🇺🇿 Uzbekistan</option>
|
||||
<option value="VU">🇻🇺 Vanuatu</option>
|
||||
<option value="VA">🇻🇦 Vatican City</option>
|
||||
<option value="VE">🇻🇪 Venezuela</option>
|
||||
<option value="VN">🇻🇳 Vietnam</option>
|
||||
<option value="WF">🇼🇫 Wallis and Futuna</option>
|
||||
<option value="EH">🇪🇭 Western Sahara</option>
|
||||
<option value="YE">🇾🇪 Yemen</option>
|
||||
<option value="ZM">🇿🇲 Zambia</option>
|
||||
<option value="ZW">🇿🇼 Zimbabwe</option>
|
||||
</select>
|
||||
|
||||
<label data-i18n="username"></label>
|
||||
<input type="text" data-id="username" spellcheck="false" placeholder="username">
|
||||
|
||||
<label data-i18n="city"></label>
|
||||
<input type="text" data-id="city" spellcheck="false" placeholder="city">
|
||||
|
||||
<label data-i18n="password"></label>
|
||||
<span class="password">
|
||||
<input type="password" data-id="password" spellcheck="false" placeholder="****">
|
||||
<button type="button" class="plain" data-i18n="togglePassword|title">👁</button>
|
||||
</span>
|
||||
|
||||
<label data-i18n="color"></label>
|
||||
<span>
|
||||
<input type="color" data-id="color" value="#ff9900">
|
||||
<button type="button" class="plain random" data-i18n="random|title">⟳</button>
|
||||
</span>
|
||||
|
||||
<label data-type="pac">PAC URL</label>
|
||||
<span class="pac">
|
||||
<input type="url" data-id="pac" data-type="pac" placeholder="PAC URL">
|
||||
<label class="pointer chrome" data-i18n="storeLocally"><input type="checkbox"></label>
|
||||
<button type="button" class="flat slim" data-i18n="view"></button>
|
||||
</span>
|
||||
|
||||
<label class="firefox" data-i18n="proxyDNS"></label>
|
||||
<input type="checkbox" class="toggle firefox" data-id="proxyDNS" checked>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="pattern-head">
|
||||
<button type="button" class="flat slim" data-i18n="add|title"><span class="plus">➕</span></button>
|
||||
<span data-i18n="proxyByPatterns"></span>
|
||||
<span data-i18n="title"></span>
|
||||
<span data-i18n="pattern"></span>
|
||||
<label class="plain" data-i18n="import|title"><input type="file" accept=".json">📥</label>
|
||||
<button type="button" class="plain" data-i18n="export|title">📤</button>
|
||||
</div>
|
||||
|
||||
<div class="pattern-box"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="pattern-row">
|
||||
<select>
|
||||
<option value="" disabled selected> </option>
|
||||
<option data-type="wildcard" value="*" data-i18n="all"></option>
|
||||
<option data-type="wildcard" value="http://">HTTP</option>
|
||||
<option data-type="wildcard" value="https://">HTTPS</option>
|
||||
<option data-type="regex" value="^(http|ws)s?://[^.]+/" data-i18n="plainHost"></option>
|
||||
<option data-type="regex" value="^(http|ws)s?://10(\.\d+){3}/">10.*.*.*</option>
|
||||
<option data-type="regex" value="^(http|ws)s?://127(\.\d+){3}/">127.*.*.*</option>
|
||||
<option data-type="regex" value="^(http|ws)s?://172\.16(\.\d+){2}/">172.16.*.*</option>
|
||||
<option data-type="regex" value="^(http|ws)s?://192\.168(\.\d+){2}/">192.168.*.*</option>
|
||||
</select>
|
||||
<select>
|
||||
<option value="include" data-i18n="include"></option>
|
||||
<option value="exclude" data-i18n="exclude"></option>
|
||||
<option value="tabProxy" data-i18n="tabProxy"></option>
|
||||
</select>
|
||||
<select>
|
||||
<option value="wildcard" data-i18n="wildcard"></option>
|
||||
<option value="match">Match</option>
|
||||
<option value="regex">Reg Exp</option>
|
||||
</select>
|
||||
<input type="text" spellcheck="false" placeholder="title">
|
||||
<input type="text" spellcheck="false" placeholder="://example.com/">
|
||||
<input type="checkbox" class="toggle" checked>
|
||||
<button type="button" class="test" data-i18n="test|title"></button>
|
||||
<button type="button" class="bin" data-i18n="delete|title"></button>
|
||||
</div>
|
||||
</template>
|
||||
<!-- /template -->
|
||||
</section>
|
||||
<!-- /proxy -->
|
||||
|
||||
<!-- import -->
|
||||
<section class="import">
|
||||
<fieldset>
|
||||
<details class="import-account">
|
||||
<summary data-i18n="importFoxyProxyAccount"></summary>
|
||||
<div class="content">
|
||||
<label data-i18n="username"></label>
|
||||
<input id="username" type="text" spellcheck="false" placeholder="username">
|
||||
|
||||
<label data-i18n="password"></label>
|
||||
<input id="password" type="password" spellcheck="false" placeholder="*****">
|
||||
<button type="button" class="plain" data-i18n="togglePassword|title">👁</button>
|
||||
|
||||
<label data-i18n="options"></label>
|
||||
<div class="account-options">
|
||||
<select>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="socks5">SOCKS5</option>
|
||||
</select>
|
||||
<select>
|
||||
<option value="hostname" data-i18n="hostname"></option>
|
||||
<option value="ip">IP</option>
|
||||
</select>
|
||||
<select>
|
||||
<option value="main" data-i18n="mainServer"></option>
|
||||
<option value="alt" data-i18n="altServer"></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<button type="button" class="flat" data-i18n="import"></button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="import-from-url">
|
||||
<summary data-i18n="importFromURL"></summary>
|
||||
<div class="content">
|
||||
<input type="url" placeholder="https://example.com/settings.json">
|
||||
<button type="button" class="flat" data-i18n="import"></button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="import-proxy-list">
|
||||
<summary data-i18n="importProxyList"></summary>
|
||||
<div class="content">
|
||||
<textarea></textarea>
|
||||
<button type="button" class="flat" data-i18n="import"></button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="import-from-older">
|
||||
<summary data-i18n="importFromOlderVersions"></summary>
|
||||
<div class="content">
|
||||
<label class="flat" data-i18n="import"><input type="file" accept=".json, .xml"></label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<footer>
|
||||
<span data-i18n="help" data-link="#import"></span>
|
||||
</footer>
|
||||
|
||||
|
||||
<!-- no need for submit SAVE button as all imports forward to Proxies Tab -->
|
||||
<!-- <button type="submit" data-i18n="saveOptions"></button> -->
|
||||
</fieldset>
|
||||
</section>
|
||||
<!-- /import -->
|
||||
|
||||
<!-- tester -->
|
||||
<section class="tester">
|
||||
<fieldset>
|
||||
|
||||
<h3 data-i18n="pattern" data-link="#pattern-guide"></h3>
|
||||
<p class="description" data-i18n="testerDescription"></p>
|
||||
<div class="tester-pattern">
|
||||
<select>
|
||||
<option value="wildcard" data-i18n="wildcard"></option>
|
||||
<option value="match">Match</option>
|
||||
<option value="regex">Reg Exp</option>
|
||||
</select>
|
||||
<input type="text" spellcheck="false" value="*://*.example.com/">
|
||||
</div>
|
||||
<pre contenteditable="true">https://example.com/
|
||||
https://example.org/
|
||||
http://help.example.com/
|
||||
https://help.example.com/abc
|
||||
https://google.com/ref=help.example.com</pre>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="button" class="flat" data-i18n="test"></button>
|
||||
<button type="button" class="flat" data-i18n="back">◁ </button>
|
||||
</div>
|
||||
|
||||
|
||||
<h3 data-i18n="proxyByPatterns"></h3>
|
||||
<p class="description" data-i18n="proxyByPatternsDescription"></p>
|
||||
<input type="url" spellcheck="false" pattern="https?://.*" placeholder="https://example.com/abc">
|
||||
<pre></pre>
|
||||
<button type="button" class="flat proxyByPatterns" data-i18n="test"></button>
|
||||
|
||||
<footer>
|
||||
<span data-i18n="help" data-link="#pattern-tester"></span>
|
||||
</footer>
|
||||
|
||||
</fieldset>
|
||||
</section>
|
||||
<!-- /tester -->
|
||||
|
||||
|
||||
<!-- log -->
|
||||
<section class="log">
|
||||
<div class="domain">
|
||||
<input type="text" autocomplete="off" spellcheck="false" placeholder="example.com">
|
||||
<button type="button" class="flat slim" data-i18n="getAssociatedDomains"></button>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<caption class="firefox" data-i18n="notAvailable"></caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 1.5em;"></th>
|
||||
<th style="width: 5em;" data-i18n="time"></th>
|
||||
<th style="width: 1em;"><img src="../image/container.svg" alt=""></th>
|
||||
<th style="width: 3em;" data-i18n="method"></th>
|
||||
<th style="width: 3em;" data-i18n="type"></th>
|
||||
<th data-i18n="documentURL" data-link="#log"></th>
|
||||
<th data-i18n="url" data-link="#log"></th>
|
||||
<th><span data-i18n="proxy"></span> <span data-i18n="title"></span></th>
|
||||
<th style="width: 3em;" data-i18n="type"></th>
|
||||
<th data-i18n="hostname"></th>
|
||||
<th style="width: 3em;" data-i18n="port"></th>
|
||||
<th data-i18n="pattern"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- template -->
|
||||
<template>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</template>
|
||||
</section>
|
||||
<!-- /log -->
|
||||
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,302 @@
|
||||
import {pref, App} from './app.js';
|
||||
import {Proxies} from './options-proxies.js';
|
||||
import {ImportExport} from './import-export.js';
|
||||
import {ImportUrl} from './import-url.js';
|
||||
import {ImportOlder} from './import-older.js';
|
||||
import {Flag} from './flag.js';
|
||||
import {ProgressBar} from './progress-bar.js';
|
||||
import {Nav} from './nav.js';
|
||||
import './get-location.js';
|
||||
import './incognito-access.js';
|
||||
import './browsing-data.js';
|
||||
import './webrtc.js';
|
||||
import './options-filter.js';
|
||||
import './bulk-edit.js';
|
||||
import './drag-drop.js';
|
||||
import './ping.js';
|
||||
import './test.js';
|
||||
import './import-account.js';
|
||||
import './import-list.js';
|
||||
import './show.js';
|
||||
import './i18n.js';
|
||||
import './theme.js';
|
||||
|
||||
// ---------- User Preferences -----------------------------
|
||||
await App.getPref();
|
||||
|
||||
// ---------- Options --------------------------------------
|
||||
class Options {
|
||||
|
||||
static {
|
||||
// --- keyboard Shortcut
|
||||
this.commands = document.querySelectorAll('.options .commands select');
|
||||
|
||||
// --- global passthrough
|
||||
this.passthrough = document.getElementById('passthrough');
|
||||
|
||||
// --- buttons
|
||||
document.querySelector('.options button[data-i18n="restoreDefaults"]').addEventListener('click', () => this.restoreDefaults());
|
||||
|
||||
this.init(['sync', 'autoBackup', 'theme', 'showPatternProxy', 'passthrough']);
|
||||
}
|
||||
|
||||
static init(keys = Object.keys(pref)) {
|
||||
// defaults to pref keys
|
||||
this.prefNode = document.querySelectorAll('#' + keys.join(',#'));
|
||||
// submit button
|
||||
document.querySelectorAll('button[type="submit"]').forEach(i => i.addEventListener('click', () => this.check()));
|
||||
|
||||
this.process();
|
||||
}
|
||||
|
||||
static process(save) {
|
||||
// 'save' is only set when clicking the button to save options
|
||||
this.prefNode.forEach(node => {
|
||||
// value: 'select-one', 'textarea', 'text', 'number'
|
||||
const attr = node.type === 'checkbox' ? 'checked' : 'value';
|
||||
save ? pref[node.id] = node[attr] : node[attr] = pref[node.id];
|
||||
});
|
||||
|
||||
// update saved pref
|
||||
save && !ProgressBar.show() && browser.storage.local.set(pref);
|
||||
this.fillContainerCommands(save);
|
||||
}
|
||||
|
||||
static async check() {
|
||||
// not for storage.managed
|
||||
if (pref.managed) { return; }
|
||||
|
||||
// --- global exclude, clean up, remove path, remove duplicates
|
||||
const passthrough = this.passthrough.value.trim();
|
||||
const [separator] = passthrough.match(/[\s,;]+/) || ['\n'];
|
||||
const arr = passthrough.split(/[\s,;]+/).filter(Boolean)
|
||||
.map(i => /[\d.]+\/\d+/.test(i) ? i : i.replace(/(?<=[a-z\d])\/[^\s,;]*/gi, ''));
|
||||
this.passthrough.value = [...new Set(arr)].join(separator);
|
||||
pref.passthrough = this.passthrough.value;
|
||||
|
||||
// --- check and build proxies & patterns
|
||||
const data = [];
|
||||
const cache = {};
|
||||
// using for loop to be able to break early
|
||||
for (const item of document.querySelectorAll('div.proxy-div details')) {
|
||||
const pxy = await Proxies.getProxyDetails(item);
|
||||
if (!pxy) { return; }
|
||||
|
||||
data.push(pxy);
|
||||
|
||||
// cache to update Proxies cache
|
||||
const id = pxy.type === 'pac' ? pxy.pac : `${pxy.hostname}:${pxy.port}`;
|
||||
cache[id] = pxy;
|
||||
}
|
||||
|
||||
// no errors, update pref.data
|
||||
pref.data = data;
|
||||
|
||||
// helper: remove if proxy is deleted or disabled
|
||||
const checkSelect = i => i.value && !cache[i.value]?.active && (i.value = '');
|
||||
|
||||
// --- container proxy
|
||||
const containerList = document.querySelectorAll('.options .container select');
|
||||
const container = {};
|
||||
containerList.forEach(i => {
|
||||
checkSelect(i);
|
||||
i.value && (container[i.name] = i.value);
|
||||
});
|
||||
// set to pref
|
||||
pref.container = container;
|
||||
|
||||
// --- keyboard shortcut proxy
|
||||
const commands = {};
|
||||
this.commands.forEach(i => {
|
||||
checkSelect(i);
|
||||
commands[i.name] = i.value;
|
||||
});
|
||||
// set to pref
|
||||
pref.commands = commands;
|
||||
|
||||
// --- check mode
|
||||
// get from storage in case it was changed while options page has been open
|
||||
let {mode} = await browser.storage.local.get({mode: 'disable'});
|
||||
switch (true) {
|
||||
case pref.mode.includes('://') && !/:\d+$/.test(pref.mode) && !pref.data.some(i => i.active && i.type === 'pac' && mode === i.pac):
|
||||
case pref.mode.includes(':') && !pref.data.some(i => i.active && i.type !== 'pac' && mode === `${i.hostname}:${i.port}`):
|
||||
case pref.mode === 'pattern' && !pref.data.some(i => i.active && i.include[0]):
|
||||
mode = 'disable';
|
||||
break;
|
||||
}
|
||||
pref.mode = mode;
|
||||
|
||||
// --- save options
|
||||
this.process(true);
|
||||
|
||||
// --- update Proxy
|
||||
// check 'prefers-color-scheme' since it is not available in background service worker
|
||||
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
browser.runtime.sendMessage({id: 'setProxy', pref, dark});
|
||||
|
||||
// --- Auto Backup
|
||||
pref.autoBackup && ImportExport.export(pref, false, `${browser.i18n.getMessage('extensionName')}/`);
|
||||
|
||||
// --- Sync
|
||||
this.sync(pref);
|
||||
}
|
||||
|
||||
// https://github.com/w3c/webextensions/issues/510
|
||||
// Proposal: Increase maximum item size in Storage sync quotas
|
||||
static sync(pref) {
|
||||
if (!pref.sync) { return; }
|
||||
|
||||
// convert array to object {...data} to avoid sync maximum item size limit
|
||||
const obj = {...pref.data};
|
||||
|
||||
// add other sync properties
|
||||
App.syncProperties.forEach(i => obj[i] = pref[i]);
|
||||
|
||||
// save changes to sync
|
||||
browser.storage.sync.set(obj)
|
||||
.then(() => {
|
||||
// delete left-over proxies
|
||||
browser.storage.sync.get()
|
||||
.then(syncObj => {
|
||||
// get & delete numerical keys that are equal or larger than data length, the rest are overwritten
|
||||
const del = Object.keys(syncObj).filter(i => /^\d+$/.test(i) && i * 1 >= pref.data.length);
|
||||
del[0] && browser.storage.sync.remove(del);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
App.notify(browser.i18n.getMessage('syncError') + '\n\n' + error.message);
|
||||
// disabling sync option to avoid repeated errors
|
||||
document.getElementById('sync').checked = false;
|
||||
browser.storage.local.set({sync: false});
|
||||
});
|
||||
}
|
||||
|
||||
static restoreDefaults() {
|
||||
if (!confirm(browser.i18n.getMessage('restoreDefaultsConfirm'))) { return; }
|
||||
|
||||
const db = App.getDefaultPref();
|
||||
Object.keys(db).forEach(i => pref[i] = db[i]);
|
||||
this.process();
|
||||
Proxies.process();
|
||||
}
|
||||
|
||||
static makeProxyOption() {
|
||||
// create proxy option
|
||||
const docFrag = document.createDocumentFragment();
|
||||
// filter out PAC, limit to 50
|
||||
pref.data.filter(i => i.active && i.type !== 'pac').slice(0, 50).forEach(i => {
|
||||
const flag = Flag.get(i.cc);
|
||||
const value = `${i.hostname}:${i.port}`;
|
||||
const opt = new Option(flag + ' ' + (i.title || value), value);
|
||||
// supported on Chrome, not on Firefox
|
||||
// opt.style.color = item.color;
|
||||
|
||||
docFrag.append(opt.cloneNode(true));
|
||||
});
|
||||
|
||||
return docFrag;
|
||||
}
|
||||
|
||||
// --- container & commands
|
||||
static fillContainerCommands(save) {
|
||||
// create proxy option
|
||||
const docFrag = this.makeProxyOption();
|
||||
|
||||
// not when clicking save
|
||||
if (!save) {
|
||||
this.addCustomContainer();
|
||||
|
||||
// populate the template select
|
||||
this.containerSelect.append(docFrag.cloneNode(true));
|
||||
|
||||
// add custom containers, sort by number
|
||||
const list = [...document.querySelectorAll('.options .container select')].map(i => i.name);
|
||||
Object.keys(pref.container).filter(i => !list.includes(i)).sort()
|
||||
.forEach(i => this.addContainer(i.substring(10)));
|
||||
}
|
||||
|
||||
const containerList = document.querySelectorAll('.options .container select');
|
||||
|
||||
// reset
|
||||
this.clearSelect(containerList);
|
||||
this.clearSelect(this.commands);
|
||||
|
||||
containerList.forEach(i => {
|
||||
i.append(docFrag.cloneNode(true));
|
||||
pref.container[i.name] && (i.value = pref.container[i.name]);
|
||||
});
|
||||
|
||||
this.commands.forEach(i => {
|
||||
i.append(docFrag.cloneNode(true));
|
||||
pref.commands[i.name] && (i.value = pref.commands[i.name]);
|
||||
});
|
||||
|
||||
// help fill log select elements
|
||||
document.querySelectorAll('.popup select:not(.popup-server)').forEach(i => i.append(docFrag.cloneNode(true)));
|
||||
}
|
||||
|
||||
static clearSelect(elem) {
|
||||
// remove children except the first one
|
||||
elem.forEach(i => i.replaceChildren(i.firstElementChild));
|
||||
}
|
||||
|
||||
static addCustomContainer() {
|
||||
// using generic names
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1386673
|
||||
// Make Contextual Identity extensions be an optional permission
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1947602
|
||||
// Allow limited read-only access to contextualIdentities.query without permission
|
||||
|
||||
[this.containerLabel, this.containerSelect] =
|
||||
document.querySelector('.options .container template').content.children;
|
||||
|
||||
this.containerButton = document.querySelector('.options .container button');
|
||||
this.containerButton.addEventListener('click', () => this.addContainer(prompt(browser.i18n.getMessage('addContainerPrompt'))));
|
||||
}
|
||||
|
||||
static addContainer(n) {
|
||||
n *= 1;
|
||||
if (!n || this.hasContainer(n)) { return; }
|
||||
|
||||
const label = this.containerLabel.cloneNode(true);
|
||||
const select = this.containerSelect.cloneNode(true);
|
||||
|
||||
label.append(n);
|
||||
select.name = `container-${n}`;
|
||||
this.containerButton.before(label, select);
|
||||
}
|
||||
|
||||
static hasContainer(n) {
|
||||
return document.querySelector(`.options .container select[name="container-${n}"]`);
|
||||
}
|
||||
}
|
||||
// ---------- /Options -------------------------------------
|
||||
|
||||
// ---------- Proxies --------------------------------------
|
||||
Proxies.process(pref);
|
||||
|
||||
// ---------- Import From URL ------------------------------
|
||||
ImportUrl.init(pref, () => {
|
||||
// set options after the pref update, update page display, show Proxy tab
|
||||
Options.process();
|
||||
Proxies.process(pref);
|
||||
Nav.get('proxies');
|
||||
});
|
||||
|
||||
// ---------- Import Older Preferences ---------------------
|
||||
ImportOlder.init(pref, () => {
|
||||
// set options after the pref update, update page display, show Proxy tab
|
||||
Options.process();
|
||||
Proxies.process(pref);
|
||||
Nav.get('proxies');
|
||||
});
|
||||
|
||||
// ---------- Import/Export Preferences --------------------
|
||||
ImportExport.init(pref, () => {
|
||||
// set options after the pref update, update page display
|
||||
Options.process();
|
||||
Proxies.process(pref);
|
||||
});
|
||||
|
||||
// ---------- Navigation -----------------------------------
|
||||
Nav.get();
|
||||
@@ -0,0 +1,21 @@
|
||||
import {Spinner} from './spinner.js';
|
||||
import {Popup} from './options-popup.js';
|
||||
|
||||
export class PAC {
|
||||
|
||||
static async view(url) {
|
||||
if (!url) { return; }
|
||||
|
||||
const text = await this.get(url);
|
||||
Popup.show(text);
|
||||
}
|
||||
|
||||
static async get(url) {
|
||||
Spinner.show();
|
||||
const text = await fetch(url)
|
||||
.then(response => response.text())
|
||||
.catch(error => error);
|
||||
Spinner.hide();
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
export class Pattern {
|
||||
|
||||
// showError from options.js, not from migrate.js
|
||||
static validate(str, type, showError) {
|
||||
// --- match pattern
|
||||
if (type === 'match') {
|
||||
if (this.validMatchPattern(str)) { return true; }
|
||||
|
||||
// not valid
|
||||
if (showError) {
|
||||
const error = this.checkMatchPattern(str);
|
||||
error && alert([browser.i18n.getMessage('regexError'), str, error].join('\n'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- wildcard & regex
|
||||
const pat = this.get(str, type);
|
||||
try {
|
||||
new RegExp(pat);
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
showError && alert([browser.i18n.getMessage('regexError'), str, error].join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
static get(str, type) {
|
||||
return type === 'wildcard' ? this.convertWildcard(str) :
|
||||
type === 'match' ? this.convertMatchPattern(str) : str;
|
||||
}
|
||||
|
||||
// convert wildcard to regex string
|
||||
static convertWildcard(str) {
|
||||
// catch all
|
||||
if (str === '*') { return '\\w+'; }
|
||||
|
||||
// no need to add scheme as search parameters are encoded url=https%3A%2F%2F
|
||||
// escape regular expression special characters, minus * ?
|
||||
return str.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/^\*|\*$/g, '') // trim start/end *
|
||||
.replaceAll('*', '.*')
|
||||
.replaceAll('?', '.');
|
||||
}
|
||||
|
||||
// convert match pattern to regex string
|
||||
static convertMatchPattern(str) {
|
||||
// catch all
|
||||
if (str === '<all_urls>') { return '\\w+'; }
|
||||
|
||||
// escape regular expression special characters, minus *
|
||||
str = str.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace('*://', '.+://') // convert * scheme
|
||||
.replace('://*\\.', '://(.+\\.)?') // match domains & subdomains
|
||||
.replaceAll('*', '.*');
|
||||
|
||||
// match pattern matches the whole URL
|
||||
return '^' + str + '$';
|
||||
}
|
||||
|
||||
static checkMatchPattern(str) {
|
||||
// catch all
|
||||
if (str === '<all_urls>') { return; }
|
||||
|
||||
const [, scheme, host] = str.match(/^(.+):\/\/([^/]+)\/(.*)$/) || [];
|
||||
|
||||
switch (true) {
|
||||
case !scheme || !host:
|
||||
// Invalid Pattern
|
||||
return browser.i18n.getMessage('invalidPatternError');
|
||||
|
||||
case !['*', 'http', 'https', 'ws', 'wss'].includes(scheme):
|
||||
// "*" in scheme must be the only character | Unsupported scheme
|
||||
const msg = scheme.includes('*') ? 'schemeError' : 'unsupportedSchemeError';
|
||||
return browser.i18n.getMessage(msg);
|
||||
|
||||
case host.substring(1).includes('*'):
|
||||
// "*" in host must be at the start
|
||||
return browser.i18n.getMessage('hostError');
|
||||
|
||||
case host.startsWith('*') && !host.startsWith('*.'):
|
||||
// "*" in host must be the only character or be followed by "."
|
||||
return browser.i18n.getMessage('hostDotError');
|
||||
|
||||
case host.includes(':'):
|
||||
// Host must not include a port number
|
||||
return browser.i18n.getMessage('portError');
|
||||
}
|
||||
}
|
||||
|
||||
// --- test match pattern validity
|
||||
static validMatchPattern(p) {
|
||||
// file: is not valid for proxying purpose
|
||||
return p === '<all_urls>' ||
|
||||
/^(https?|\*):\/\/(\*|\*\.[^*:/]+|[^*:/]+)\/.*$/i.test(p);
|
||||
}
|
||||
|
||||
static getPassthrough(str) {
|
||||
if (!str) { return [[], [], []]; }
|
||||
|
||||
// RegExp string
|
||||
const regex = [];
|
||||
// 10.0.0.0/24 -> [ip, mask] e.g ['10.0.0.0', '255.255.255.0']
|
||||
const ipMask = [];
|
||||
// 10.0.0.0/24 -> [start, end] e.g. ['010000000000', '010000000255']
|
||||
const stEnd = [];
|
||||
|
||||
str.split(/[\s,;]+/).forEach(i => {
|
||||
// The literal string <local> matches simple hostnames (no dots)
|
||||
if (i === '<local>') {
|
||||
regex.push('.+://[^.]+/');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- CIDR
|
||||
const [, ip, , mask] = i.match(/^(\d+(\.\d+){3})\/(\d+)$/) || [];
|
||||
if (ip && mask) {
|
||||
const netmask = this.getNetmask(mask);
|
||||
ipMask.push(ip, netmask);
|
||||
stEnd.push(this.getRange(ip, netmask));
|
||||
return;
|
||||
}
|
||||
|
||||
// --- pattern
|
||||
i = i.replaceAll('.', '\\.') // literal '.'
|
||||
.replaceAll('*', '.*'); // wildcard
|
||||
|
||||
// starting with '.'
|
||||
i.startsWith('\\.') && (i = '.+://.+' + i);
|
||||
// add scheme
|
||||
!i.includes('://') && (i = '.+://' + i);
|
||||
// add start assertion
|
||||
// !i.startsWith('^') && (i = '^' + i);
|
||||
// add pathname
|
||||
i += '/';
|
||||
regex.push(i);
|
||||
});
|
||||
|
||||
return [regex, ipMask, stEnd];
|
||||
}
|
||||
|
||||
// ---------- CIDR ---------------------------------------
|
||||
// convert mask to netmask
|
||||
static getNetmask(mask) {
|
||||
return [...Array(4)].map(() => {
|
||||
const n = Math.min(mask, 8);
|
||||
mask -= n;
|
||||
return 256 - Math.pow(2, 8 - n);
|
||||
}).join('.');
|
||||
}
|
||||
|
||||
// convert to padded start & end
|
||||
static getRange(ip, mask) {
|
||||
// ip array
|
||||
let st = ip.split('.');
|
||||
// mask array
|
||||
const ma = mask.split('.');
|
||||
// netmask wildcard array
|
||||
let end = st.map((v, i) => Math.min(v - ma[i] + 255, 255) + '');
|
||||
st = st.map(i => i.padStart(3, '0')).join('');
|
||||
end = end.map(i => i.padStart(3, '0')).join('');
|
||||
|
||||
return [st, end];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {Popup} from './options-popup.js';
|
||||
|
||||
// ---------- Ping (Side Effect) ---------------------------
|
||||
class Ping {
|
||||
|
||||
static {
|
||||
document.querySelector('.proxy-top button[data-i18n="ping"]').addEventListener('click', () => this.process());
|
||||
}
|
||||
|
||||
static async process() {
|
||||
let {data} = await browser.storage.local.get({data: []});
|
||||
data = data.filter(i => i.active);
|
||||
if (!data[0]) { return; }
|
||||
|
||||
// --- text formatting
|
||||
const n = 4;
|
||||
const pType = Math.max(...data.map(i => i.type.length)) + n;
|
||||
const pHost = Math.max(...data.map(i =>
|
||||
(i.title || i.pac || `${i.hostname}:${parseInt(i.port)}`).length)) + n;
|
||||
// performance.now() Firefox 280160 | Chrome 447156.4000000004
|
||||
const format = n => new Intl.NumberFormat().format(n.toFixed()).padStart(8, ' ');
|
||||
const dash = '--- --'.padStart(11, ' ');
|
||||
|
||||
data.forEach(i => {
|
||||
const t = performance.now();
|
||||
const host = `${i.hostname}:${parseInt(i.port)}`;
|
||||
const url = i.pac || (i.type.startsWith('http') ? `${i.type}://${host}/` : `http://${host}/`);
|
||||
const target = i.type.padEnd(pType, ' ') + (i.title || i.pac || host).padEnd(pHost, ' ');
|
||||
|
||||
if (['direct'].includes(i.type)) {
|
||||
Popup.show(`${target}${dash} ${i.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chrome a network request timeouts at 300 seconds, while in Firefox at 90 seconds.
|
||||
// AbortSignal.timeout FF100, Ch124
|
||||
fetch(url, {method: 'HEAD', cache: 'no-store', signal: AbortSignal.timeout(5000)})
|
||||
.then(r => {
|
||||
const st = ![200, 400].includes(r.status) ? ` ${r.status} ${r.statusText}` : '';
|
||||
Popup.show(`${target}${format(performance.now() - t)} ms${st}`);
|
||||
})
|
||||
.catch(e => Popup.show(`${target}${dash} ${e.message}`));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// ---------- Filter Proxy (Side Effect) -------------------
|
||||
class Filter {
|
||||
|
||||
static {
|
||||
this.list = document.querySelector('div.list');
|
||||
const filter = document.querySelector('.filter');
|
||||
filter.addEventListener('input', e => this.filterProxy(e));
|
||||
}
|
||||
|
||||
static filterProxy(e) {
|
||||
const str = e.target.value.toLowerCase().trim();
|
||||
const elem = [...this.list.children].slice(2); // not the first 2
|
||||
if (!str) {
|
||||
elem.forEach(i => i.classList.remove('off'));
|
||||
return;
|
||||
}
|
||||
|
||||
elem.forEach(item => {
|
||||
const title = item.children[1].textContent;
|
||||
const host = item.children[3].value; // input radio
|
||||
item.classList.toggle('off', ![title, host].some(i => i.toLowerCase().includes(str)));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
@import 'default.css';
|
||||
@import 'theme.css';
|
||||
|
||||
/* ----- Light Theme ----- */
|
||||
:root {
|
||||
--filter: opacity(0.4) grayscale(1);
|
||||
--selected: #fec8;
|
||||
}
|
||||
|
||||
/* for the default theme */
|
||||
:root:not([class]) {
|
||||
--nav-bg: #630;
|
||||
}
|
||||
|
||||
/* ----- Dark Theme ----- */
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--filter: opacity(1) grayscale(1);
|
||||
--selected: #222;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----- General ----- */
|
||||
body {
|
||||
opacity: 0;
|
||||
font-size: 12px;
|
||||
width: 25em;
|
||||
background-color: var(--bg);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
/*
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1883896
|
||||
Remove UA styles for :is(article, aside, nav, section) h1 (Nightly only) Firefox 125
|
||||
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1885509
|
||||
Remove UA styles for :is(article, aside, nav, section) h1 (staged rollout)
|
||||
*/
|
||||
h1 {
|
||||
color: var(--nav-color);
|
||||
background-color: var(--nav-bg);
|
||||
margin: 0;
|
||||
padding: 0.5em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h1 img {
|
||||
width: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
/* ----- Buttons ----- */
|
||||
div.popup-buttons {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
column-gap: 0.1em;
|
||||
}
|
||||
|
||||
button {
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.8em;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--btn-hover);
|
||||
}
|
||||
/* ----- /Buttons ----- */
|
||||
|
||||
/* ----- Main Display ----- */
|
||||
div.list {
|
||||
padding-top: 0.5em;
|
||||
min-height: 15em;
|
||||
max-height: 30em;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
div.list label {
|
||||
display: grid;
|
||||
grid-template-columns: 2.5em auto 1fr;
|
||||
column-gap: 0.5em;
|
||||
padding: 0.2em 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.list label:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
div.list label.off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.list label:has(input[name="server"]:checked) {
|
||||
background-color: var(--selected);
|
||||
}
|
||||
|
||||
.flag img {
|
||||
width: 1.2em;
|
||||
}
|
||||
|
||||
.flag img.off {
|
||||
filter: var(--filter);
|
||||
}
|
||||
|
||||
.flag {
|
||||
grid-row: span 2;
|
||||
font-size: 1.8em;
|
||||
line-height: 1em;
|
||||
place-self: start center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--header);
|
||||
font-size: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.port {
|
||||
color: var(--dim);
|
||||
place-self: end start;
|
||||
}
|
||||
|
||||
.data {
|
||||
grid-column: span 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre;
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
.data.off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[name="server"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- more section --- */
|
||||
summary {
|
||||
background-color: var(--alt-bg);
|
||||
padding: 0.2em 0.5em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
div.host {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0.5em;
|
||||
gap: 0.3em 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.host input.filter {
|
||||
background: url('../image/filter.svg') no-repeat left 0.5em center/1em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
div.host button {
|
||||
background-color: unset;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--color);
|
||||
font-weight: normal;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
div.host button:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
|
||||
/* ----- show/hide elements ----- */
|
||||
/* --- Chrome --- */
|
||||
.chrome .firefox {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ---managed --- */
|
||||
.managed .local {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* --- Basic --- */
|
||||
.basic .not-basic {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- scheme --- */
|
||||
.not-http select.http,
|
||||
.not-tab-proxy .tab-proxy {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
/* ----- /show/hide elements ----- */
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Popup</title>
|
||||
<link href="popup.css" rel="stylesheet">
|
||||
<script type="module" src="popup.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
<section>
|
||||
<h1 data-i18n="extensionName"><img src="../image/icon.svg" alt=""> </h1>
|
||||
|
||||
<div class="list">
|
||||
<label class="pattern not-basic">
|
||||
<span class="flag"><img src="../image/power.svg" alt=""></span>
|
||||
<span class="title" data-i18n="proxyByPatterns"></span>
|
||||
<input type="radio" name="server" value="pattern">
|
||||
</label>
|
||||
|
||||
<label class="disable">
|
||||
<span class="flag"><img class="off" src="../image/power.svg" alt=""></span>
|
||||
<span class="title" data-i18n="disable"></span>
|
||||
<input type="radio" name="server" value="disable" checked>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary data-i18n="more"></summary>
|
||||
<div class="host">
|
||||
<input type="text" class="filter" autocomplete="off" spellcheck="false" placeholder="filter">
|
||||
|
||||
<select id="tabProxy" class="firefox tab-proxy">
|
||||
<option value="" data-i18n="tabProxy" disabled selected></option>
|
||||
<option value=""> </option>
|
||||
</select>
|
||||
|
||||
<select id="includeHost" class="local http not-basic">
|
||||
<option value="" data-i18n="includeHost" disabled selected></option>
|
||||
</select>
|
||||
|
||||
<select id="excludeHost" class="local http not-basic">
|
||||
<option value="" data-i18n="excludeHost" disabled selected></option>
|
||||
</select>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="popup-buttons">
|
||||
<button type="button" data-i18n="options"></button>
|
||||
<button type="button" data-i18n="log"></button>
|
||||
<button type="button" data-i18n="ip"></button>
|
||||
<button type="button" data-i18n="location"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- template -->
|
||||
<template>
|
||||
<label>
|
||||
<span class="flag"></span>
|
||||
<span class="title"></span>
|
||||
<span class="port"></span>
|
||||
<input type="radio" name="server">
|
||||
<span class="data"></span>
|
||||
</label>
|
||||
</template>
|
||||
<!-- /template -->
|
||||
</section>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,170 @@
|
||||
import {pref, App} from './app.js';
|
||||
import {Location} from './location.js';
|
||||
import {Flag} from './flag.js';
|
||||
import './popup-filter.js';
|
||||
import './show.js';
|
||||
import './i18n.js';
|
||||
|
||||
// ---------- User Preferences -----------------------------
|
||||
await App.getPref();
|
||||
|
||||
// ---------- Popup ----------------------------------------
|
||||
class Popup {
|
||||
|
||||
static {
|
||||
// --- theme
|
||||
pref.theme && (document.documentElement.className = pref.theme);
|
||||
// show after
|
||||
document.body.style.opacity = 1;
|
||||
|
||||
document.querySelectorAll('button').forEach(i => i.addEventListener('click', e => this.processButtons(e)));
|
||||
|
||||
this.list = document.querySelector('div.list');
|
||||
|
||||
// --- Include/Exclude Host (not for storage.managed)
|
||||
this.includeHost = document.querySelector('select#includeHost');
|
||||
!pref.managed && this.includeHost.addEventListener('change', e => this.includeExclude(e));
|
||||
this.excludeHost = document.querySelector('select#excludeHost');
|
||||
!pref.managed && this.excludeHost.addEventListener('change', e => this.includeExclude(e));
|
||||
|
||||
// --- Tab Proxy (firefox only)
|
||||
this.tabProxy = document.querySelector('select#tabProxy');
|
||||
App.firefox && this.tabProxy.addEventListener('change', e => {
|
||||
if (!this.tab) { return; }
|
||||
|
||||
const {value, selectedOptions} = e.target;
|
||||
const proxy = value && this.proxyCache[selectedOptions[0].dataset.index];
|
||||
browser.runtime.sendMessage({id: 'setTabProxy', proxy, tab: this.tab});
|
||||
});
|
||||
|
||||
// disable buttons on storage.managed
|
||||
pref.managed && document.body.classList.add('managed');
|
||||
|
||||
// --- store details open toggle
|
||||
const details = document.querySelector('details');
|
||||
// defaults to true
|
||||
details.open = localStorage.getItem('more') !== 'false';
|
||||
details.addEventListener('toggle', () => localStorage.setItem('more', details.open));
|
||||
|
||||
this.process();
|
||||
}
|
||||
|
||||
static includeExclude(e) {
|
||||
const {id, value} = e.target;
|
||||
if (!value) { return; }
|
||||
// proxy object reference to pref is lost in chrome when sent from popup.js
|
||||
browser.runtime.sendMessage({id, pref, host: value, tab: this.tab});
|
||||
// reset select option
|
||||
e.target.selectedIndex = 0;
|
||||
}
|
||||
|
||||
static checkProxyByPatterns() {
|
||||
// check if there are patterns
|
||||
if (!pref.data.some(i => i.active && (i.include[0] || i.tabProxy?.[0]))) {
|
||||
// hide option if there are no patterns
|
||||
this.list.children[0].style.display = 'none';
|
||||
// show as disable
|
||||
pref.mode === 'pattern' && (pref.mode = 'disable');
|
||||
}
|
||||
|
||||
pref.mode === 'pattern' && (this.list.children[0].children[2].checked = true);
|
||||
}
|
||||
|
||||
static async process() {
|
||||
this.checkProxyByPatterns();
|
||||
|
||||
const labelTemplate = document.querySelector('template').content.firstElementChild;
|
||||
const docFrag = document.createDocumentFragment();
|
||||
|
||||
pref.data.filter(i => i.active).forEach(i => {
|
||||
const id = i.type === 'pac' ? i.pac : `${i.hostname}:${i.port}`;
|
||||
const label = labelTemplate.cloneNode(true);
|
||||
const [flag, title, port, radio, data] = label.children;
|
||||
flag.textContent = Flag.show(i);
|
||||
title.textContent = i.title || i.hostname;
|
||||
port.textContent = !i.title ? i.port : '';
|
||||
radio.value = i.type === 'direct' ? 'direct' : id;
|
||||
radio.checked = id === pref.mode;
|
||||
data.textContent = [i.city, Location.get(i.cc)].filter(Boolean).join(', ') || ' ';
|
||||
docFrag.append(label);
|
||||
});
|
||||
|
||||
this.list.append(docFrag);
|
||||
this.list.addEventListener('click', e =>
|
||||
// fires twice (click & label -> input)
|
||||
e.target.name === 'server' && this.processSelect(e.target.value, e)
|
||||
);
|
||||
|
||||
// --- Add Hosts to select
|
||||
// used to find proxy, filter out PAC, limit to 10
|
||||
this.proxyCache = pref.data.filter(i => i.active && i.type !== 'pac').slice(0, 10);
|
||||
|
||||
this.proxyCache.forEach((i, index) => {
|
||||
const flag = Flag.show(i);
|
||||
const value = `${i.hostname}:${i.port}`;
|
||||
const opt = new Option(flag + ' ' + (i.title || value), value);
|
||||
opt.dataset.index = index;
|
||||
// supported on Chrome, not on Firefox
|
||||
// opt.style.color = item.color;
|
||||
docFrag.append(opt);
|
||||
});
|
||||
|
||||
this.includeHost.append(docFrag.cloneNode(true));
|
||||
this.excludeHost.append(docFrag.cloneNode(true));
|
||||
this.tabProxy.append(docFrag);
|
||||
|
||||
// get active tab
|
||||
[this.tab] = await browser.tabs.query({currentWindow: true, active: true});
|
||||
|
||||
// --- show/hide selects
|
||||
document.body.classList.toggle('not-http', !this.tab.url.startsWith('http'));
|
||||
|
||||
// Check Tab proxy (Firefox only)
|
||||
const allowedTabProxy = App.firefox && App.allowedTabProxy(this.tab.url);
|
||||
allowedTabProxy && this.checkTabProxy();
|
||||
document.body.classList.toggle('not-tab-proxy', !allowedTabProxy);
|
||||
}
|
||||
|
||||
static checkTabProxy() {
|
||||
browser.runtime.sendMessage({id: 'getTabProxy', tab: this.tab})
|
||||
.then(i => i && (this.tabProxy.value = `${i.hostname}:${i.port}`));
|
||||
}
|
||||
|
||||
static processSelect(mode, e) {
|
||||
// disregard re-click
|
||||
if (mode === pref.mode) { return; }
|
||||
// not for storage.managed
|
||||
if (pref.managed) { return; }
|
||||
|
||||
// check 'prefers-color-scheme' since it is not available in background service worker
|
||||
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// save mode
|
||||
pref.mode = mode;
|
||||
browser.storage.local.set({mode});
|
||||
browser.runtime.sendMessage({id: 'setProxy', pref, dark, noDataChange: true});
|
||||
}
|
||||
|
||||
static processButtons(e) {
|
||||
switch (e.target.dataset.i18n) {
|
||||
case 'options':
|
||||
browser.runtime.openOptionsPage();
|
||||
break;
|
||||
|
||||
case 'location':
|
||||
browser.tabs.create({url: 'https://getfoxyproxy.org/geoip/'});
|
||||
break;
|
||||
|
||||
case 'ip':
|
||||
// sending message to the background script to complete even if popup gets closed
|
||||
browser.runtime.sendMessage({id: 'getIP'});
|
||||
break;
|
||||
|
||||
case 'log':
|
||||
browser.tabs.create({url: '/content/options.html?log'});
|
||||
break;
|
||||
}
|
||||
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* ----- Progress Bar ----- */
|
||||
.progress-bar {
|
||||
opacity: 0;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background: #3bb3e0 linear-gradient(124deg, #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, #1ddde8, #2b1de8, #dd00f3, #dd00f3);
|
||||
background-size: 900% 900%;
|
||||
transition: opacity 1s ease-in-out, width 0s ease-in-out 1s;
|
||||
animation: rainbow 3s ease infinite;
|
||||
}
|
||||
|
||||
.progress-bar.on {
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity 1s ease-in-out, width 2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% { background-position: 0% 100%; }
|
||||
50% { background-position: 100% 200%; }
|
||||
100% { background-position: 0% 100%; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// ---------- Progress Bar ---------------------------------
|
||||
export class ProgressBar {
|
||||
|
||||
static bar = document.querySelector('.progress-bar');
|
||||
|
||||
static show() {
|
||||
this.bar.classList.toggle('on');
|
||||
setTimeout(() => this.bar.classList.toggle('on'), 2000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/common/extensions/api/proxy.json
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/schemas/proxy.json
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1804693
|
||||
// Setting single proxy for all fails
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1495756
|
||||
// Issue 1495756: Support bypassList for PAC scripts in the chrome.proxy API
|
||||
// https://chromium-review.googlesource.com/c/chromium/src/+/5227338
|
||||
// Implement bypassList for PAC scripts in chrome.proxy API
|
||||
// Chrome bypassList applies to 'fixed_servers', not 'pac_script' or URL
|
||||
// Firefox passthrough applies to all set in proxy.settings.set, i.e. PAC URL
|
||||
// manual bypass list:
|
||||
// Chrome: pac_script data, not possible for URL
|
||||
// Firefox proxy.onRequest
|
||||
|
||||
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/parent/ext-proxy.js#236
|
||||
// throw new ExtensionError("proxy.settings is not supported on android.");
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1725981
|
||||
// Support proxy.settings API on Android
|
||||
|
||||
import {App} from './app.js';
|
||||
import {Authentication} from './authentication.js';
|
||||
import {OnRequest} from './on-request.js';
|
||||
import {Location} from './location.js';
|
||||
import {Pattern} from './pattern.js';
|
||||
import {Action} from './action.js';
|
||||
import {Menus} from './menus.js';
|
||||
|
||||
export class Proxy {
|
||||
|
||||
static {
|
||||
// from popup.js & options.js
|
||||
browser.runtime.onMessage.addListener((...e) => this.onMessage(...e));
|
||||
}
|
||||
|
||||
static onMessage(message) {
|
||||
// noDataChange comes from popup.js & test.js
|
||||
const {id, pref, host, proxy, dark, tab, noDataChange} = message;
|
||||
switch (id) {
|
||||
case 'setProxy':
|
||||
Action.dark = dark;
|
||||
this.set(pref, noDataChange);
|
||||
break;
|
||||
|
||||
case 'includeHost':
|
||||
case 'excludeHost':
|
||||
// proxy object reference to pref is lost in chrome when sent from popup.js
|
||||
const pxy = pref.data.find(i => i.active && host === `${i.hostname}:${i.port}`);
|
||||
this.includeHost(pref, pxy, tab, id);
|
||||
break;
|
||||
|
||||
case 'setTabProxy':
|
||||
OnRequest.setTabProxy(tab, proxy);
|
||||
break;
|
||||
|
||||
case 'getTabProxy':
|
||||
// need to return a promise for 'getTabProxy' from popup.js
|
||||
return Promise.resolve(OnRequest.tabProxy[tab.id]);
|
||||
|
||||
case 'getIP':
|
||||
this.getIP();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static async set(pref, noDataChange) {
|
||||
// check if proxy.settings is controlled_by_this_extension
|
||||
const conf = await this.getSettings();
|
||||
// not controlled_by_this_extension
|
||||
if (!conf) { return; }
|
||||
|
||||
// --- update authentication data
|
||||
noDataChange || Authentication.init(pref.data);
|
||||
|
||||
// --- update menus
|
||||
noDataChange || Menus.init(pref);
|
||||
|
||||
// --- check mode
|
||||
switch (true) {
|
||||
// no proxy, set to disable
|
||||
case !pref.data[0]:
|
||||
pref.mode = 'disable';
|
||||
break;
|
||||
|
||||
// no include pattern, set proxy to the first entry
|
||||
case pref.mode === 'pattern' && !pref.data.some(i => i.include[0] || i.exclude[0]):
|
||||
const pxy = pref.data[0];
|
||||
pref.mode = pxy.type === 'pac' ? pxy.pac : `${pxy.hostname}:${pxy.port}`;
|
||||
break;
|
||||
}
|
||||
|
||||
App.firefox ? Firefox.set(pref, conf) : Chrome.set(pref);
|
||||
Action.set(pref);
|
||||
}
|
||||
|
||||
static async getSettings() {
|
||||
if (App.android) { return {}; }
|
||||
|
||||
const conf = await browser.proxy.settings.get({});
|
||||
|
||||
// https://developer.chrome.com/docs/extensions/mv3/manifest/icons/
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=29683
|
||||
// Issue 29683: Extension icons should support SVG (Dec 8, 2009)
|
||||
// SVG is not supported by Chrome
|
||||
// Firefox: If each one of imageData and path is one of undefined, null or empty object,
|
||||
// the global icon will be reset to the manifest icon
|
||||
// Chrome -> Error: Either the path or imageData property must be specified.
|
||||
|
||||
// check if proxy.settings is controlled_by_this_extension
|
||||
const control = ['controlled_by_this_extension', 'controllable_by_this_extension'].includes(conf.levelOfControl);
|
||||
const path = control ? `/image/icon.png` : `/image/icon-off.png`;
|
||||
browser.action.setIcon({path});
|
||||
!control && browser.action.setTitle({title: browser.i18n.getMessage('controlledByOtherExtensions')});
|
||||
|
||||
// return null if Chrome and no control, allow Firefox to continue regardless
|
||||
return !App.firefox && !control ? null : conf;
|
||||
}
|
||||
|
||||
// ---------- Include/Exclude Host ----------------------
|
||||
static includeHost(pref, proxy, tab, inc) {
|
||||
// not for storage.managed
|
||||
if (pref.managed) { return; }
|
||||
|
||||
const url = this.getURL(tab.url);
|
||||
if (!url) { return; }
|
||||
|
||||
const pattern = url.origin + '/';
|
||||
const pat = {
|
||||
active: true,
|
||||
pattern,
|
||||
title: url.hostname,
|
||||
type: 'wildcard',
|
||||
};
|
||||
|
||||
inc === 'includeHost' ? proxy.include.push(pat) : proxy.exclude.push(pat);
|
||||
browser.storage.local.set({data: pref.data});
|
||||
// update Proxy, noDataChange
|
||||
pref.mode === 'pattern' && proxy.active && this.set(pref, true);
|
||||
}
|
||||
|
||||
static getURL(str) {
|
||||
const url = new URL(str);
|
||||
// unacceptable URLs
|
||||
if (!['http:', 'https:'].includes(url.protocol)) { return; }
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
// from popup.js
|
||||
static getIP() {
|
||||
fetch('https://getfoxyproxy.org/webservices/lookup.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!Object.keys(data)) {
|
||||
App.notify(browser.i18n.getMessage('error'));
|
||||
return;
|
||||
}
|
||||
|
||||
const [ip, {cc, city}] = Object.entries(data)[0];
|
||||
const text = [ip, city, Location.get(cc)].filter(Boolean).join('\n');
|
||||
App.notify(text);
|
||||
})
|
||||
.catch(error => App.notify(browser.i18n.getMessage('error') + '\n\n' + error.message));
|
||||
}
|
||||
}
|
||||
|
||||
class Firefox {
|
||||
|
||||
static async set(pref, conf) {
|
||||
// update OnRequest
|
||||
OnRequest.init(pref);
|
||||
|
||||
if (App.android) { return; }
|
||||
|
||||
// incognito access
|
||||
if (!await browser.extension.isAllowedIncognitoAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// retain settings as Network setting is partially customisable
|
||||
const value = conf.value;
|
||||
|
||||
switch (true) {
|
||||
// https://github.com/foxyproxy/browser-extension/issues/47
|
||||
// Unix domain socket SOCKS proxy support
|
||||
// regard file:///run/user/1000/proxy.socks:9999 as normal proxy (not PAC)
|
||||
|
||||
// sanitizeNoProxiesPref() "network.proxy.no_proxies_on"
|
||||
// https://searchfox.org/mozilla-central/source/browser/components/preferences/dialogs/connection.js#338
|
||||
|
||||
// --- Proxy Auto-Configuration (PAC) URL
|
||||
case pref.mode.includes('://') && !/:\d+$/.test(pref.mode):
|
||||
value.proxyType = 'autoConfig';
|
||||
value.autoConfigUrl = pref.mode;
|
||||
// convert to standard comma-separated
|
||||
value.passthrough = pref.passthrough.split(/[\s,;]+/).join(', ');
|
||||
value.proxyDNS = pref.proxyDNS;
|
||||
// no error if levelOfControl: "controlled_by_other_extensions"
|
||||
browser.proxy.settings.set({value});
|
||||
break;
|
||||
|
||||
// --- disable, direct, pattern, or single proxy
|
||||
default:
|
||||
browser.proxy.settings.clear({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Chrome {
|
||||
|
||||
static async set(pref) {
|
||||
// https://developer.chrome.com/docs/extensions/reference/types/
|
||||
// Scope and life cycle: regular | regular_only | incognito_persistent | incognito_session_only
|
||||
const config = {value: {}, scope: 'regular'};
|
||||
switch (true) {
|
||||
case pref.mode === 'disable':
|
||||
case pref.mode === 'direct':
|
||||
config.value.mode = 'system';
|
||||
break;
|
||||
|
||||
// --- Proxy Auto-Configuration (PAC) URL
|
||||
case pref.mode.includes('://') && !/:\d+$/.test(pref.mode):
|
||||
config.value.mode = 'pac_script';
|
||||
config.value.pacScript = {mandatory: true};
|
||||
config.value.pacScript.url = pref.mode;
|
||||
break;
|
||||
|
||||
// --- single proxy
|
||||
case pref.mode.includes(':'):
|
||||
const pxy = this.findProxy(pref);
|
||||
if (!pxy) { return; }
|
||||
|
||||
config.value.mode = 'fixed_servers';
|
||||
config.value.rules = this.getSingleProxyRule(pref, pxy);
|
||||
break;
|
||||
|
||||
// --- pattern
|
||||
default:
|
||||
config.value.mode = 'pac_script';
|
||||
config.value.pacScript = {mandatory: true};
|
||||
config.value.pacScript.data = this.getPacString(pref);
|
||||
}
|
||||
|
||||
browser.proxy.settings.set(config);
|
||||
|
||||
// --- incognito
|
||||
this.setIncognito(pref);
|
||||
}
|
||||
|
||||
static findProxy(pref, host = pref.mode) {
|
||||
return pref.data.find(i =>
|
||||
i.active && i.type !== 'pac' && i.hostname && host === `${i.hostname}:${i.port}`);
|
||||
}
|
||||
|
||||
static async setIncognito(pref) {
|
||||
// incognito access
|
||||
if (!await browser.extension.isAllowedIncognitoAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pxy = pref.container?.incognito && this.findProxy(pref, pref.container?.incognito);
|
||||
if (!pxy) {
|
||||
// unset incognito
|
||||
browser.proxy.settings.clear({scope: 'incognito_persistent'});
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {value: {}, scope: 'incognito_persistent'};
|
||||
config.value.mode = 'fixed_servers';
|
||||
config.value.rules = this.getSingleProxyRule(pref, pxy);
|
||||
browser.proxy.settings.set(config);
|
||||
}
|
||||
|
||||
static getSingleProxyRule(pref, pxy) {
|
||||
return {
|
||||
singleProxy: {
|
||||
scheme: pxy.type,
|
||||
host: pxy.hostname,
|
||||
// must be number, prepare for augmented port
|
||||
port: parseInt(pxy.port),
|
||||
},
|
||||
bypassList: pref.passthrough ? pref.passthrough.split(/[\s,;]+/) : []
|
||||
};
|
||||
}
|
||||
|
||||
static getProxyString(proxy) {
|
||||
let {type, hostname, port} = proxy;
|
||||
switch (type) {
|
||||
case 'direct':
|
||||
return 'DIRECT';
|
||||
|
||||
// chrome PAC doesn't support HTTP
|
||||
case 'http':
|
||||
type = 'PROXY';
|
||||
break;
|
||||
|
||||
default:
|
||||
type = type.toUpperCase();
|
||||
}
|
||||
// prepare for augmented port
|
||||
return `${type} ${hostname}:${parseInt(port)}`;
|
||||
}
|
||||
|
||||
static getPacString(pref) {
|
||||
// --- proxy by pattern
|
||||
const [passthrough, net] = Pattern.getPassthrough(pref.passthrough);
|
||||
|
||||
// filter data
|
||||
let data = pref.data.filter(i => i.active && i.type !== 'pac' && i.hostname);
|
||||
data = data.filter(i => i.include[0] || i.exclude[0]).map(item => {
|
||||
return {
|
||||
str: this.getProxyString(item),
|
||||
include: item.include.filter(i => i.active).map(i => Pattern.get(i.pattern, i.type)),
|
||||
exclude: item.exclude.filter(i => i.active).map(i => Pattern.get(i.pattern, i.type))
|
||||
};
|
||||
});
|
||||
|
||||
// add PAC rules from pacString
|
||||
let pacData = pref.data.filter(i => i.active && i.type === 'pac' && i.pacString);
|
||||
pacData = pacData.map((i, idx) => i.pacString.replace('FindProxyForURL', '$&' + idx) +
|
||||
`\nconst find${idx} = FindProxyForURL${idx}(url, host);
|
||||
if (find${idx} !== 'DIRECT') { return find${idx}; }`).join('\n\n');
|
||||
pacData &&= `\n${pacData}\n`;
|
||||
|
||||
// https://developer.chrome.com/docs/extensions/reference/proxy/#type-PacScript
|
||||
// https://github.com/w3c/webextensions/issues/339
|
||||
// Chrome pacScript doesn't support bypassList
|
||||
// https://issues.chromium.org/issues/40286640
|
||||
|
||||
// isInNet(host, "192.0.2.172", "255.255.255.255")
|
||||
|
||||
const pacString =
|
||||
String.raw`function FindProxyForURL(url, host) {
|
||||
const data = ${JSON.stringify(data)};
|
||||
const passthrough = ${JSON.stringify(passthrough)};
|
||||
const net = ${JSON.stringify(net)};
|
||||
const match = array => array.some(i => new RegExp(i, 'i').test(url));
|
||||
const inNet = () => net[0] && /^[\d.]+$/.test(host) && net.some(([ip, mask]) => isInNet(host, ip, mask));
|
||||
|
||||
if (match(passthrough) || inNet()) { return 'DIRECT'; }
|
||||
for (const proxy of data) {
|
||||
if (!match(proxy.exclude) && match(proxy.include)) { return proxy.str; }
|
||||
}
|
||||
${pacData}
|
||||
return 'DIRECT';
|
||||
}`;
|
||||
|
||||
return pacString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["mode", "data"],
|
||||
"properties": {
|
||||
"mode": {"type": "string"},
|
||||
"sync": {"type": "boolean"},
|
||||
"autoBackup": {"type": "boolean"},
|
||||
"passthrough": {"type": "string"},
|
||||
"theme": {"type": "string"},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active": {"type": "boolean"},
|
||||
"title": {"type": "string"},
|
||||
"type": {"type": "string", "enum": ["http", "https", "socks4", "socks5", "pac", "direct"]},
|
||||
"hostname": {"type": "string", "format": "hostname"},
|
||||
"port": {"type": "string", "pattern": "^\\d*$"},
|
||||
"username": {"type": "string"},
|
||||
"password": {"type": "string"},
|
||||
"cc": {"type": "string", "pattern": "^([A-Z]{2}|)$"},
|
||||
"city": {"type": "string"},
|
||||
"color": {"type": "string"},
|
||||
"pac": {"type": "string"},
|
||||
"pacString": {"type": "string"},
|
||||
"proxyDNS": {"type": "boolean"},
|
||||
"include": {
|
||||
"type": "array",
|
||||
"id": "include",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"type": "string", "enum": ["wildcard", "regex", "match"]},
|
||||
"title": {"type": "string"},
|
||||
"pattern": {"type": "string"},
|
||||
"active": {"type": "boolean"}
|
||||
},
|
||||
"required": ["type", "title", "pattern", "active"]
|
||||
}
|
||||
},
|
||||
"exclude": {"$ref": "include"},
|
||||
"tabProxy": {"$ref": "include"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"container": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"incognito": {"type": "string"},
|
||||
"container-1": {"type": "string"},
|
||||
"container-2": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"setProxy": {"type": "string"},
|
||||
"setTabProxy": {"type": "string"},
|
||||
"includeHost": {"type": "string"},
|
||||
"excludeHost": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {App} from './app.js';
|
||||
|
||||
// ---------- Show (Side Effect) ---------------------------
|
||||
class Show {
|
||||
|
||||
static {
|
||||
const {basic, firefox} = App;
|
||||
basic && document.body.classList.add('basic');
|
||||
!firefox && document.body.classList.add('chrome');
|
||||
|
||||
const elem = document.querySelector('img[src="../image/icon.svg"]');
|
||||
elem?.addEventListener('click', () => {
|
||||
basic && document.body.classList.toggle('basic');
|
||||
!firefox && document.body.classList.toggle('chrome');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/* ----- Spinner ----- */
|
||||
div.spinner {
|
||||
display: none;
|
||||
grid-auto-flow: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #0003;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.spinner::before {
|
||||
content: '';
|
||||
width: 8em;
|
||||
height: 8em;
|
||||
border: 1em solid #ddd;
|
||||
border-color: #ddd var(--btn-bg) #ddd var(--btn-bg);
|
||||
border-radius: 50%;
|
||||
animation: spin 1.5s linear infinite;
|
||||
margin-left: -2em;
|
||||
}
|
||||
|
||||
.spinner::after {
|
||||
content: '';
|
||||
background-image: url('../image/icon.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
width: 6em;
|
||||
height: 6em;
|
||||
margin-left: -8em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.spinner.on {
|
||||
display: grid;
|
||||
animation: fade-in 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// ---------- Spinner --------------------------------------
|
||||
export class Spinner {
|
||||
|
||||
static spinner = document.querySelector('.spinner');
|
||||
|
||||
static show() {
|
||||
this.spinner.classList.add('on');
|
||||
}
|
||||
|
||||
static hide() {
|
||||
this.spinner.classList.remove('on');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {App} from './app.js';
|
||||
|
||||
// ---------- Storage Sync ---------------------------------
|
||||
export class Sync {
|
||||
|
||||
static init(pref) {
|
||||
// not for storage.managed
|
||||
if (pref.managed) { return; }
|
||||
|
||||
// Firefox 101 (2022-05-31), Chrome 73
|
||||
browser.storage.sync.onChanged.addListener(e => this.onChanged(e));
|
||||
}
|
||||
|
||||
static async onChanged(changes) {
|
||||
const pref = await browser.storage.local.get();
|
||||
this.getSync(pref);
|
||||
}
|
||||
|
||||
static async get(pref) {
|
||||
// check storage.managed
|
||||
await this.getManaged(pref);
|
||||
// check storage.sync
|
||||
await this.getSync(pref);
|
||||
}
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1868153
|
||||
// On Firefox storage.managed returns undefined if not found
|
||||
static async getManaged(pref) {
|
||||
const result = await browser.storage.managed.get().catch(() => {});
|
||||
if (!Array.isArray(result?.data) || !result.data[0]) {
|
||||
// storage.managed not found, clean up
|
||||
if (Object.hasOwn(pref, 'managed')) {
|
||||
delete pref.managed;
|
||||
await browser.storage.local.remove('managed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// get default pref
|
||||
const db = App.getDefaultPref();
|
||||
// revert pref to default values
|
||||
Object.keys(db).forEach(i => pref[i] = db[i]);
|
||||
|
||||
// set data from storage.managed
|
||||
Object.keys(result).forEach(i => Object.hasOwn(pref, i) && (pref[i] = result[i]));
|
||||
// set pref.managed to use in options.js, popup.js
|
||||
pref.managed = true;
|
||||
// no sync for storage.managed
|
||||
pref.sync = false;
|
||||
|
||||
// --- update database
|
||||
await browser.storage.local.set(pref);
|
||||
}
|
||||
|
||||
static hasOldData(obj) {
|
||||
// FP v3 OR FP v7, null value causes an error
|
||||
return Object.hasOwn(obj, 'settings') || Object.values(obj).some(i => i && Object.hasOwn(i, 'address'));
|
||||
}
|
||||
|
||||
static async getSync(pref) {
|
||||
if (!pref.sync) { return; }
|
||||
if (pref.managed) { return; }
|
||||
|
||||
const syncPref = await browser.storage.sync.get();
|
||||
|
||||
// check sync from old version 3-7
|
||||
// (local has no data OR has old data) AND sync has old data
|
||||
if ((!Object.keys(pref)[0] || this.hasOldData(pref)) && this.hasOldData(syncPref)) {
|
||||
// set sync data to pref, to migrate next in background.js
|
||||
Object.keys(syncPref).forEach(i => pref[i] = syncPref[i]);
|
||||
return;
|
||||
}
|
||||
|
||||
// convert object to array & filter proxies
|
||||
const data = Object.values(syncPref).filter(i => Object.hasOwn(i, 'hostname'));
|
||||
|
||||
const obj = {};
|
||||
if (data[0] && !App.equal(pref.data, data)) {
|
||||
obj.data = data;
|
||||
pref.data = data;
|
||||
}
|
||||
|
||||
App.syncProperties.forEach(i => {
|
||||
if (Object.hasOwn(syncPref, i)) {
|
||||
obj[i] = syncPref[i];
|
||||
pref[i] = syncPref[i];
|
||||
}
|
||||
});
|
||||
|
||||
// update saved pref
|
||||
Object.keys(obj)[0] && await browser.storage.local.set(obj);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import {App} from './app.js';
|
||||
import {Popup} from './options-popup.js';
|
||||
|
||||
// ---------- Proxy Text (Side Effect) ---------------------------
|
||||
class ProxyTest {
|
||||
|
||||
static {
|
||||
document.querySelector('.proxy-top button[data-i18n="test"]').addEventListener('click', () => this.selectOptions());
|
||||
this.popupProxy = document.querySelector('.popup select.popup-test-proxy');
|
||||
this.popupServer = document.querySelector('.popup select.popup-server');
|
||||
this.popupServer.addEventListener('change', () => this.process());
|
||||
}
|
||||
|
||||
static selectOptions() {
|
||||
if (this.popupProxy.options.length < 2) {
|
||||
Popup.show('Did not find a suitable proxy');
|
||||
Popup.show('Ending the test');
|
||||
return;
|
||||
}
|
||||
|
||||
this.popupProxy.classList.add('on');
|
||||
this.popupServer.classList.add('on');
|
||||
|
||||
!App.firefox && Popup.show('On Chrome, proxy authentication must be done before starting the test');
|
||||
Popup.show('Please select a proxy (or the first one will be selected) and then a server for the test\n');
|
||||
}
|
||||
|
||||
static async process(e) {
|
||||
this.server = this.popupServer.value;
|
||||
if (!this.server) { return; }
|
||||
|
||||
Popup.show('Starting the proxy Test\n');
|
||||
|
||||
// check 'prefers-color-scheme' since it is not available in background service worker
|
||||
this.dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// --- get the IP check server
|
||||
const serverText = this.popupServer.selectedOptions[0].textContent;
|
||||
Popup.show(`IP check server: ${serverText}`);
|
||||
|
||||
// --- get the proxy for the test
|
||||
// selected proxy or the first proxy
|
||||
!this.popupProxy.value && (this.popupProxy.selectedIndex = 1);
|
||||
const id = this.popupProxy.value;
|
||||
const host = this.popupProxy.selectedOptions[0].textContent;
|
||||
|
||||
const pref = await browser.storage.local.get();
|
||||
const pxy = pref.data.find(i => id === `${i.hostname}:${i.port}`);
|
||||
|
||||
Popup.show(`Testing proxy: ${host}\n`);
|
||||
|
||||
// --- get real IP
|
||||
Popup.show('Setting mode to "Disable" to get your real IP');
|
||||
pref.mode = 'disable';
|
||||
await this.setProxy(pref);
|
||||
const realIP = await this.getIP();
|
||||
|
||||
// --- test Tab Proxy with mode disable
|
||||
App.firefox && await this.tabProxy(pxy, realIP);
|
||||
|
||||
// --- test single proxy
|
||||
Popup.show(`Setting mode to "${host}"`);
|
||||
pref.mode = `${pxy.hostname}:${pxy.port}`;
|
||||
await this.setProxy(pref);
|
||||
await this.getIP();
|
||||
|
||||
// --- test Proxy by Patterns
|
||||
Popup.show('Setting mode to "Proxy by Patterns"');
|
||||
// adding patterns to test
|
||||
pxy.include = [
|
||||
{
|
||||
type: 'wildcard',
|
||||
title: 'test',
|
||||
pattern: new URL(this.server).hostname,
|
||||
active: true
|
||||
},
|
||||
];
|
||||
pref.mode = 'pattern';
|
||||
await this.setProxy(pref);
|
||||
await this.getIP();
|
||||
|
||||
// --- reset to the original state
|
||||
this.reset();
|
||||
}
|
||||
|
||||
static async setProxy(pref) {
|
||||
// await runtime.sendMessage resolves early on Chrome
|
||||
App.firefox ? await this.sendMessage(pref) : await this.chromeSendMessage(pref);
|
||||
}
|
||||
|
||||
static sendMessage(pref) {
|
||||
return browser.runtime.sendMessage({id: 'setProxy', pref, dark: this.dark, noDataChange: true});
|
||||
}
|
||||
|
||||
static async chromeSendMessage(pref) {
|
||||
await new Promise(resolve => {
|
||||
const listener = () => {
|
||||
browser.proxy.settings.onChange.removeListener(listener);
|
||||
resolve();
|
||||
};
|
||||
browser.proxy.settings.onChange.addListener(listener);
|
||||
this.sendMessage(pref);
|
||||
});
|
||||
}
|
||||
|
||||
static async tabProxy(pxy, realIP) {
|
||||
Popup.show('Setting Tab Proxy with mode "Disable"');
|
||||
const tab = await browser.tabs.create({active: false});
|
||||
await browser.runtime.sendMessage({id: 'setTabProxy', proxy: pxy, tab});
|
||||
await new Promise(resolve => {
|
||||
const listener = e => {
|
||||
browser.tabs.remove(tab.id);
|
||||
browser.webRequest.onBeforeRequest.removeListener(listener);
|
||||
Popup.show(`Your IP: ${e.proxyInfo.host || realIP}\n`);
|
||||
resolve();
|
||||
};
|
||||
browser.webRequest.onBeforeRequest.addListener(listener, {urls: ['<all_urls>'], tabId: tab.id});
|
||||
browser.tabs.update(tab.id, {url: this.server});
|
||||
});
|
||||
}
|
||||
|
||||
static async reset() {
|
||||
Popup.show('Resetting options to their original state');
|
||||
const pref = await browser.storage.local.get();
|
||||
await this.setProxy(pref);
|
||||
Popup.show('Ending the proxy test\n');
|
||||
|
||||
// reset select elements
|
||||
this.popupProxy.selectedIndex = 0;
|
||||
this.popupServer.selectedIndex = 0;
|
||||
}
|
||||
|
||||
static async getIP() {
|
||||
// Chrome a network request timeouts at 300 seconds, while in Firefox at 90 seconds.
|
||||
// AbortSignal.timeout FF100, Ch124
|
||||
return fetch(this.server, {cache: 'no-store', signal: AbortSignal.timeout(5000)})
|
||||
.then(r => r.ok ? r.text() : this.response(r))
|
||||
.then(text => {
|
||||
// HTML response is not acceptable
|
||||
const ip = text.includes('<') ? 'undefined' : text.trim();
|
||||
Popup.show(`Your IP: ${ip}\n`);
|
||||
return ip;
|
||||
})
|
||||
.catch(e => Popup.show(`Your IP: undefined\nStatus: ${e.message}\n`));
|
||||
}
|
||||
|
||||
static response(r) {
|
||||
switch (r.status) {
|
||||
case 403:
|
||||
return 'undefined\nStatus: 403 Forbidden';
|
||||
|
||||
default:
|
||||
return `undefined\nStatus: ${r.status} ${r.statusText}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* ----- Tester ----- */
|
||||
.tester h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tester h3:nth-of-type(2) {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.tester .tester-pattern {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.tester input {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.tester .buttons {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.tester button:first-child{
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.tester button:last-child {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.tester pre {
|
||||
color: inherit;
|
||||
/* background-color: var(--alt-bg); */
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.3em;
|
||||
padding: 0.5em;
|
||||
font-size: 1.1em;
|
||||
min-height: 10em;
|
||||
background: linear-gradient(to right, var(--hover) 0, var(--hover) 2em, transparent 2em, transparent 100%);
|
||||
padding-left: 2.5em;
|
||||
}
|
||||
|
||||
.tester pre span {
|
||||
margin-left: -2em;
|
||||
}
|
||||
|
||||
.pass {
|
||||
color: var(--pass);
|
||||
}
|
||||
|
||||
.fail {
|
||||
color: var(--fail);
|
||||
}
|
||||
|
||||
:is(.pass, .fail)::before {
|
||||
content: '✔';
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
margin-right: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fail::before {
|
||||
content: '✘';
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import {Pattern} from './pattern.js';
|
||||
import {Nav} from './nav.js';
|
||||
|
||||
export class Tester {
|
||||
|
||||
static {
|
||||
this.select = document.querySelector('.tester select');
|
||||
this.select.addEventListener('change', () => this.process());
|
||||
[this.pattern, this.url] = document.querySelectorAll('.tester input');
|
||||
[this.pre, this.pre2] = document.querySelectorAll('.tester pre');
|
||||
|
||||
// convert generated HTML to plaintext
|
||||
this.pre.addEventListener('input', e => {
|
||||
if ((e.data || e.inputType === 'insertFromPaste') && this.pre.matches('.tested')) {
|
||||
[...this.pre.children].forEach(i => i.replaceWith(i.textContent));
|
||||
this.pre.classList.remove('tested');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('.tester button[data-i18n="back"]').addEventListener('click', () => this.back());
|
||||
document.querySelector('.tester button[data-i18n="test"]').addEventListener('click', () => this.process());
|
||||
document.querySelector('.tester button.proxyByPatterns').addEventListener('click', () => this.processURL());
|
||||
}
|
||||
|
||||
static process() {
|
||||
this.pattern.value = this.pattern.value.trim();
|
||||
const str = this.pre.textContent.trim();
|
||||
if (!this.pattern.value || !str) {
|
||||
return;
|
||||
}
|
||||
|
||||
// validate pattern
|
||||
const valid = Pattern.validate(this.pattern.value, this.select.value, true);
|
||||
if (!valid) { return; }
|
||||
|
||||
// convert pattern to regex string if needed
|
||||
const pat = Pattern.get(this.pattern.value, this.select.value);
|
||||
const regex = new RegExp(pat, 'i');
|
||||
const arr = [];
|
||||
str.split(/\s/).forEach(i => {
|
||||
const sp = document.createElement('span');
|
||||
sp.textContent = i;
|
||||
i.trim() && (sp.className = regex.test(i) ? 'pass' : 'fail');
|
||||
arr.push(sp, '\n');
|
||||
});
|
||||
|
||||
this.pre.textContent = '';
|
||||
this.pre.append(...arr);
|
||||
// mark pre, used for 'input' event
|
||||
this.pre.classList.add('tested');
|
||||
}
|
||||
|
||||
static back() {
|
||||
if (!this.target) { return; }
|
||||
|
||||
// show Proxy tab
|
||||
Nav.get('proxies');
|
||||
|
||||
// go to target proxy
|
||||
const details = this.target.closest('details');
|
||||
details.open = true;
|
||||
this.target.scrollIntoView({behavior: 'smooth'});
|
||||
this.target.focus();
|
||||
|
||||
// reset
|
||||
this.target = null;
|
||||
}
|
||||
|
||||
static async processURL() {
|
||||
const url = this.url.value.trim();
|
||||
if (!url || !this.url.checkValidity()) { return; }
|
||||
|
||||
let {data} = await browser.storage.local.get({data: []});
|
||||
data = data.filter(i => i.active && !i.pac && (i.include[0] || i.exclude[0] || i.tabProxy?.[0])).map(item => {
|
||||
item.tabProxy ||= [];
|
||||
return {
|
||||
type: item.type,
|
||||
hostname: item.hostname,
|
||||
port: item.port,
|
||||
title: item.title,
|
||||
include: item.include.filter(i => i.active),
|
||||
exclude: item.exclude.filter(i => i.active),
|
||||
tabProxy: item.tabProxy.filter(i => i.active),
|
||||
};
|
||||
});
|
||||
|
||||
const match = arr => arr.find(i => new RegExp(Pattern.get(i.pattern, i.type), 'i').test(url));
|
||||
|
||||
let arr = [];
|
||||
|
||||
data.forEach(i => {
|
||||
const target = i.title || `${i.hostname}:${parseInt(i.port)}`;
|
||||
if (!match(i.exclude)) {
|
||||
const p = match(i.include);
|
||||
p && arr.push([target, p.type, p.pattern]);
|
||||
}
|
||||
const p = match(i.tabProxy);
|
||||
p && arr.push([target, p.type, p.pattern, '(Tab Proxy)']);
|
||||
});
|
||||
|
||||
if (!arr[0]) {
|
||||
this.pre2.textContent = '⮕ DIRECT';
|
||||
return;
|
||||
}
|
||||
|
||||
// --- text formatting
|
||||
const n = 4;
|
||||
const w = 'wildcard'.length + n;
|
||||
const p0 = Math.max(...arr.map(i => i[0].length)) + n;
|
||||
const p2 = Math.max(...arr.map(i => i[2].length)) + n;
|
||||
|
||||
// undefined or null is converted to an empty string in join()
|
||||
arr = arr.map(i => [i[0].padEnd(p0, ' '), i[1].padEnd(w, ' '), i[2].padEnd(p2, ' '), i[3]].join('').trim());
|
||||
this.pre2.textContent = arr.join('\n');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* ---------- Alternative Themes ---------- */
|
||||
|
||||
/* ----- moonlight ----- */
|
||||
:root.moonlight {
|
||||
--bg: #fff;
|
||||
--body-bg: #f9f9fb;
|
||||
|
||||
--nav-bg: #d9d9db;
|
||||
--nav-hover: var(--hover);
|
||||
--nav-color: var(--color);
|
||||
}
|
||||
|
||||
:root.moonlight .flat {
|
||||
color: inherit;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
:root.moonlight .flat:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
:root.moonlight .pattern-head button span.plus {
|
||||
filter: unset;
|
||||
}
|
||||
|
||||
:root.moonlight .popup-buttons button {
|
||||
color: inherit;
|
||||
background-color: var(--alt-bg);
|
||||
}
|
||||
|
||||
:root.moonlight .popup-buttons button:hover {
|
||||
background-color: var(--hover);
|
||||
}
|
||||
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
:root.moonlight {
|
||||
--bg: #333;
|
||||
--body-bg: #444;
|
||||
--nav-bg: #000;
|
||||
}
|
||||
|
||||
:root.moonlight .pattern-head button span.plus {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
}
|
||||
/* ----- /moonlight ----- */
|
||||
|
||||
|
||||
|
||||
/* ----- alt ----- */
|
||||
:root.alt {
|
||||
--body-bg: var(--bg);
|
||||
}
|
||||
/* ----- /alt ----- */
|
||||
@@ -0,0 +1,18 @@
|
||||
// ---------- Theme (Side Effect) --------------------------
|
||||
class Theme {
|
||||
|
||||
static {
|
||||
this.elem = [document, ...[...document.querySelectorAll('iframe')].map(i => i.contentDocument)];
|
||||
document.getElementById('theme').addEventListener('change', e => this.set(e.target.value));
|
||||
|
||||
browser.storage.local.get('theme').then(i => {
|
||||
i.theme && this.set(i.theme);
|
||||
// show after
|
||||
document.body.style.opacity = 1;
|
||||
});
|
||||
}
|
||||
|
||||
static set(value) {
|
||||
this.elem.forEach(i => i.documentElement.className = value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/* ----- Toggle Switch ----- */
|
||||
/* round() FF118, Ch125 */
|
||||
.toggle {
|
||||
--toggle-dot: 12px;
|
||||
--toggle-dot-margin: 1px;
|
||||
--toggle-wh-ratio: 2.4;
|
||||
--toggle-w: calc(var(--toggle-dot) * var(--toggle-wh-ratio) + var(--toggle-dot-margin) * 2);
|
||||
--toggle-h: calc(var(--toggle-dot) + var(--toggle-dot-margin) * 2);
|
||||
--toggle-trans: calc(var(--toggle-w) - var(--toggle-h));
|
||||
|
||||
appearance: none;
|
||||
width: var(--toggle-w);
|
||||
height: var(--toggle-h);
|
||||
position: relative;
|
||||
border-radius: 50px;
|
||||
background-color: #ccc;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
.toggle::before {
|
||||
content: '';
|
||||
display: block;
|
||||
margin: var(--toggle-dot-margin);
|
||||
width: var(--toggle-dot);
|
||||
height: var(--toggle-dot);
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.toggle:checked {
|
||||
background-color: var(--btn-bg);
|
||||
}
|
||||
|
||||
.toggle:checked::before {
|
||||
transform: translateX(var(--toggle-trans));
|
||||
}
|
||||
|
||||
/* smaller toggle switch for patterns */
|
||||
.pattern-row .toggle {
|
||||
--toggle-dot: 10px;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// ---------- Toggle ---------------------------------------
|
||||
export class Toggle {
|
||||
|
||||
static password(elem) {
|
||||
elem.addEventListener('click', () => {
|
||||
const input = elem.previousElementSibling;
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {App} from './app.js';
|
||||
|
||||
// ---------- WebRTC (Side Effect) -------------------------
|
||||
class WebRTC {
|
||||
|
||||
static {
|
||||
this.webRTC = document.querySelector('#limitWebRTC');
|
||||
// firefox only option
|
||||
!App.firefox && (this.webRTC.lastElementChild.disabled = true);
|
||||
this.webRTC.addEventListener('change', () => this.process());
|
||||
this.init();
|
||||
}
|
||||
|
||||
static async init() {
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/permissions/request
|
||||
// Any permissions granted are retained by the extension, even over upgrade and disable/enable cycling.
|
||||
// check if permission is granted
|
||||
this.permission = await browser.permissions.contains({permissions: ['privacy']});
|
||||
|
||||
// check webRTCIPHandlingPolicy
|
||||
if (this.permission) {
|
||||
const result = await browser.privacy.network.webRTCIPHandlingPolicy.get({});
|
||||
this.webRTC.value = result.value;
|
||||
}
|
||||
}
|
||||
|
||||
static async process() {
|
||||
if (!this.permission) {
|
||||
// request permission, Firefox for Android version 102
|
||||
this.permission = await browser.permissions.request({permissions: ['privacy']});
|
||||
if (!this.permission) { return; }
|
||||
}
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1790270
|
||||
// WebRTC bypasses Network settings & proxy.onRequest
|
||||
// {"levelOfControl": "controllable_by_this_extension", "value": "default"}
|
||||
browser.privacy.network.webRTCIPHandlingPolicy.set({value: this.webRTC.value});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user