diff --git a/src/content/about.html b/src/content/about.html index 54858bd..ae3c1ac 100644 --- a/src/content/about.html +++ b/src/content/about.html @@ -17,7 +17,13 @@

Changelog

8.10
+
Added direct HTTP authentication (Firefox 125+)
Added new options to the proxy types
+
Added QUIC (HTTP) option (Chrome only) (experimental) (#124)
+
Updated browser detection (firefox-extension/issues/220, #139, #141)
+
Updated Options page user interface
+
Updated toolbar popup user interface
+
Added console log for Save file errors (#144)
8.9
Added "Log" to the toolbar popup buttons (#44)
@@ -75,7 +81,7 @@

Changelog

Updated Options save process to fill blank proxy header title display (#74)
8.3
-
Added enterprise policy & managed storage feature (#42) (experimental)
+
Added enterprise policy & managed storage feature (experimental) (#42)
Added PAC "Store Locally" feature (#46) (experimental) (Chrome only)
Added PAC view feature
Fixed an issue with empty Global Exclude
@@ -108,7 +114,7 @@

Changelog

8.0
Added complete Light/Dark Theme
Added Exclude host feature
-
Added experimental Firefox on Android support (#21)
+
Added Firefox on Android support (experimental) (#21)
Added Get Location feature
Added Global Exclude
Added Host Pattern to proxy feature
@@ -129,7 +135,6 @@

Changelog

Credits

-
Developer
erosman
@@ -138,10 +143,10 @@

Credits

es: Luis Alfredo Figueroa Bracamontes
fa: Matin Kargar
fr: Hugo-C
-
ja_JP: Yuta Yamate
+
ja: Yuta Yamate
pl: Grzegorz Koryga
pt_BR:
-
ru: Kirill Motkov
+
ru: Kirill Motkov, krolchonok
uk: Sviatoslav Sydorenko
zh_CN: FeralMeow
zh_TW: samuikaze
diff --git a/src/content/action.js b/src/content/action.js index db43f23..8edc0f2 100644 --- a/src/content/action.js +++ b/src/content/action.js @@ -2,6 +2,7 @@ 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; diff --git a/src/content/app.js b/src/content/app.js index bf48b5c..0b4dd88 100644 --- a/src/content/app.js +++ b/src/content/app.js @@ -24,7 +24,12 @@ export const pref = { // ---------- App ------------------------------------------ export class App { - static firefox = navigator.userAgent.includes('Firefox'); + // https://github.com/foxyproxy/firefox-extension/issues/220 + // Proxy by patterns not working if firefox users change their userAgent to another platform + // Chrome does not support runtime.getBrowserInfo() + // Object.hasOwn(browser.runtime, 'getBrowserInfo') + // moz-extension: | chrome-extension: | safari-web-extension: + static firefox = browser.runtime.getURL('').startsWith('moz-extension:'); static android = navigator.userAgent.includes('Android'); // static chrome = navigator.userAgent.includes('Chrome'); static basic = browser.runtime.getManifest().name === browser.i18n.getMessage('extensionNameBasic'); @@ -63,11 +68,6 @@ export class App { return JSON.stringify(a) === JSON.stringify(b); } - static getFlag(cc) { - cc = /^[A-Z]{2}$/i.test(cc) && cc.toUpperCase(); - return cc ? String.fromCodePoint(...[...cc].map(i => i.charCodeAt() + 127397)) : '🌎'; - } - static parseURL(url) { // rebuild file:// url.startsWith('file://') && (url = 'http' + url.substring(4)); @@ -86,4 +86,48 @@ export class App { return url; } + + static getFlag(cc) { + cc = /^[A-Z]{2}$/i.test(cc) && cc.toUpperCase(); + return cc ? String.fromCodePoint(...[...cc].map(i => i.charCodeAt() + 127397)) : '🌎'; + } + + static isLocal(host) { + // check local network + const isIP = /^[\d.:]+$/.test(host); + switch (true) { + // --- localhost & + // case host === 'localhost': + case !host.includes('.'): // plain hostname (no dots) + case host.endsWith('.localhost'): // *.localhost + + // --- IPv4 + // case host === '127.0.0.1': + case isIP && host.startsWith('127.'): // 127.0.0.1 up to 127.255.255.254 + case isIP && host.startsWith('169.254.'): // 169.254.0.0/16 - 169.254.0.0 to 169.254.255.255 + case isIP && host.startsWith('192.168.'): // 192.168.0.0/16 - 192.168.0.0 to 192.168.255.255 + + // --- IPv6 + // case host === '[::1]': + case host.startsWith('[::1]'): // literal IPv6 [::1]:80 with/without port + case host.toUpperCase().startsWith('[FE80::]'): // literal IPv6 [FE80::]/10 + return true; + } + } + + static showFlag(item) { + switch (true) { + case !!item.cc: + return this.getFlag(item.cc); + + case item.type === 'direct': + return '⮕'; + + case this.isLocal(item.hostname): + return '🖥️'; + + default: + return '🌎'; + } + } } \ No newline at end of file diff --git a/src/content/authentication.js b/src/content/authentication.js index eb7ab02..27f18a2 100644 --- a/src/content/authentication.js +++ b/src/content/authentication.js @@ -5,6 +5,7 @@ // webRequest.onAuthRequired on Chrome mv3 is not usable due to removal of 'blocking' // https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/api/web_request/web_request_api.cc;l=2857 // Chrome 108 new permission 'webRequestAuthProvider' +// Firefox 126 added 'webRequestAuthProvider' permission support import './app.js'; diff --git a/src/content/background.js b/src/content/background.js index c24e05c..cbb34db 100644 --- a/src/content/background.js +++ b/src/content/background.js @@ -4,7 +4,6 @@ import {Proxy} from './proxy.js'; import './commands.js'; // ---------- Process Preferences -------------------------- -// eslint-disable-next-line no-unused-vars class ProcessPref { static { diff --git a/src/content/color.js b/src/content/color.js index 95fcb11..b7f0497 100644 --- a/src/content/color.js +++ b/src/content/color.js @@ -1,7 +1,7 @@ export class Color { static getRandom() { - return this.colors[Math.floor(Math.random()*this.colors.length)]; + return this.colors[Math.floor(Math.random() * this.colors.length)]; } static colors = [ diff --git a/src/content/commands.js b/src/content/commands.js index 714ea24..663b74d 100644 --- a/src/content/commands.js +++ b/src/content/commands.js @@ -2,7 +2,7 @@ // https://developer.chrome.com/docs/extensions/reference/commands/#event-onCommand // https://bugzilla.mozilla.org/show_bug.cgi?id=1843866 // Add tab parameter to commands.onCommand -// Firefox commands only returns command name +// Firefox commands only returns command name (tab added in FF126) // Chrome commands returns command, tab import {App} from './app.js'; @@ -10,7 +10,6 @@ import {Proxy} from './proxy.js'; import {OnRequest} from './on-request.js'; // ---------- Commands (Side Effect) ------------------------ -// eslint-disable-next-line no-unused-vars class Commands { static { @@ -21,6 +20,8 @@ class Commands { static async process(name, tab) { const pref = await browser.storage.local.get(); const host = pref.commands[name]; + const needTab = ['quickAdd', 'excludeHost', 'setTabProxy', 'unsetTabProxy'].includes(name); + tab ||= needTab && await browser.tabs.query({currentWindow: true, active: true}); switch (name) { case 'proxyByPatterns': @@ -36,7 +37,7 @@ class Commands { break; case 'quickAdd': - host && Proxy.quickAdd(pref, host); + host && Proxy.quickAdd(pref, host, tab); break; case 'excludeHost': @@ -47,13 +48,13 @@ class Commands { if (!App.firefox || !host) { break; } // firefox only const proxy = pref.data.find(i => i.active && host === `${i.hostname}:${i.port}`); - proxy && OnRequest.setTabProxy(proxy); + proxy && OnRequest.setTabProxy(tab, proxy); break; case 'unsetTabProxy': if (!App.firefox) { break; } // firefox only - OnRequest.unsetTabProxy(); + OnRequest.setTabProxy(tab); break; } } diff --git a/src/content/help.html b/src/content/help.html index 84b63f3..ef7e224 100644 --- a/src/content/help.html +++ b/src/content/help.html @@ -423,10 +423,11 @@

Toolbar Popup

Add host (with its protocol) to the Global Exclude
Set Tab Proxy (Firefox only)
+
Select a proxy from the drop-down list for the current tab
+
Click button to set
Tab Proxy is processed before normal proxy settings
-
Select a proxy for the current tab
Only top 10 active proxies are listed
-
FoxyProxy icon will show in the selected tab and mouse-over title displays selection details
+
Toolbar badge shows the proxy and mouse-over title displays the selection details
Unset Tab Proxy (Firefox only)
Unset proxy for the current tab
@@ -534,7 +535,7 @@

Incognito/Container

Firefox
Incognito/Container are processed before standard proxy settings (and after Tab Proxy)
-
Generic names are used for containers since getting their details requires "contextualIdentities" permissions which would enable containers automatically for the user
+
Generic names are used for containers since getting their details requires "contextualIdentities" permissions which would enable containers automatically for the user
@@ -701,7 +702,7 @@

Restore Defaults

Browser settings are not changed (e.g. previously set proxy values or Limit WebRTC).

Preferences: Import/Export

-

You can import/export Preferences (for backup or share) from/to a local file on your computer.

+

You can import/export Preferences (for backup or sharing) from/to a local file on your computer.

Import is non-destructive. Click Save to apply the changes.

Save

@@ -714,7 +715,7 @@

Proxies

FoxyProxy identifies proxies by their "hostname:post" or PAC URL.

-

In case more than one proxy with the same "hostname:post" is needed, one or more letters can be added to the port to separate them, and FoxyProxy will remove the letters later. (v8.8)

+

In case more than one proxy with the same "hostname:post" is needed, one or more letters can be added to the port to separate them, and FoxyProxy will remove the letters later. (v8.8) (Firefox only, not practical on Chrome)

 127.0.0.1:9050
@@ -766,6 +767,7 @@ 

Individual Proxy

Type
HTTP/HTTPS/SOCKS4/SOCKS5 to be used with hostname & port
PAC to be used with PAC URL
+
QUIC (HTTP/3) proxy is only supported by Chrome (not available on Firefox). Support for QUIC proxies in Chrome is currently experimental and not ready for production use.
Country
Set the proxy country flag
@@ -781,10 +783,11 @@

Individual Proxy

Username & Password
To be used for HTTP/HTTPS/SOCKS types
-
If not set, authentication will be handled by the browser
+
SOCKS5 username & password is only supported by Firefox (not available on Chrome)
+
If not set, HTTP/HTTPS authentication will be handled by the browser
Username & Password are port specific and different ones can be set for different ports
-
Proxy DNS (Firefox only)
+
Proxy DNS (Firefox only)
Option to pass DNS to the SOCKS proxy when using SOCKS
See also:
+
PAC URL (not available on Android)
Required for PAC type
Proxy Auto-Configuration file (PAC) can be used to handle all proxying configurations
@@ -825,6 +832,57 @@

Individual Proxy

+ + +

DNS (Domain Name System) Resolution

+

Following table demonstrates the DNS resolution when a proxy is used.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Is name resolution (DNS) done client side, or proxy side?
SchemeChromeFirefoxDoH (DNS over HTTPS)
HTTPName resolution is always done proxy side proxy sideSee Proxy DNS
HTTPS(expected to be proxy side)proxy sideSee Proxy DNS
SOCKS4Name resolution for target hosts is always done client side, and moreover must resolve to an IPv4 address (SOCKSv4 encodes target address as 4 octets, so IPv6 targets are not possible). See Proxy DNSSee Proxy DNS
SOCKS5Name resolution is always done proxy side See Proxy DNSSee Proxy DNS
QUICA QUIC proxy uses QUIC (UDP) as the underlying transport, but otherwise behaves as an HTTP proxy. n/an/a
+ + +

Local Servers

FoxyProxy can be used with local proxy servers:

@@ -846,10 +904,16 @@

Local Servers

- TOR - SOCKS5 + Burp Suit + HTTP 127.0.0.1 - 9050 + 8080 + + + Privoxy + HTTP + 127.0.0.1 + 8118 Psiphon @@ -858,22 +922,28 @@

Local Servers

60351 - Privoxy - HTTPS + TOR + SOCKS5 127.0.0.1 - 8118 + 9050 v2rayA - SOCKS5 + SOCKS5
http
http with shunt rules 127.0.0.1 - 20170 + 20170
20171
20172 - v2rayA - HTTP + NekoRay/NekoBox + SOCKS5
http + 127.0.0.1 + 2080
2081 + + + Shadowsocks + SOCKS5 127.0.0.1 - 20171
20172 (http with shunt rules) + 1080 diff --git a/src/content/i18n.js b/src/content/i18n.js index 8b193bb..47b3b1a 100644 --- a/src/content/i18n.js +++ b/src/content/i18n.js @@ -1,5 +1,4 @@ // ---------- Internationalization (Side Effect) ----------- -// eslint-disable-next-line no-unused-vars class I18n { static { diff --git a/src/content/iframe.css b/src/content/iframe.css index 324dad6..aac9b9a 100644 --- a/src/content/iframe.css +++ b/src/content/iframe.css @@ -220,15 +220,11 @@ th, td { border: 1px solid var(--border); vertical-align: top; -} - -td { padding: 0.5em; } thead th { font-size: 1.2em; - padding: 0.5em; } tbody th { diff --git a/src/content/import-export.js b/src/content/import-export.js index e8d735e..982099f 100644 --- a/src/content/import-export.js +++ b/src/content/import-export.js @@ -24,7 +24,7 @@ export class ImportExport { static readData(data, pref) { try { data = JSON.parse(data); } - catch(e) { + catch { App.notify(browser.i18n.getMessage('fileParseError')); // display the error return; } @@ -58,7 +58,8 @@ export class ImportExport { saveAs, conflictAction: 'uniquify' }) - .catch(() => {}); // Suppress Error: Download canceled by the user + // eslint-disable-next-line no-console + .catch(console.log); } static fileReader(file, callback) { diff --git a/src/content/on-request.js b/src/content/on-request.js index 2c5922f..d327f02 100644 --- a/src/content/on-request.js +++ b/src/content/on-request.js @@ -2,10 +2,19 @@ // Dynamic import is not available yet in MV3 service worker // Once implemented, module will be dynamically imported for Firefox only -// Support non-ASCII username/password for socks proxy // 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=1794464 +// Allow HTTP authentication in proxy.onRequest +// Fixed in Firefox 125 + +// https://bugzilla.mozilla.org/show_bug.cgi?id=1741375 +// Proxy DNS by default when using SOCKS v5 +// Firefox 128: defaults to true for SOCKS5 & false for SOCKS4 + +import {App} from './app.js'; import {Pattern} from './pattern.js'; import {Location} from './location.js'; @@ -21,6 +30,7 @@ export class OnRequest { this.net = []; // [start, end] strings this.tabProxy = {}; // tab proxy, may be lost in MV3 if bg is unloaded this.container = {}; // incognito/container proxy + this.browserVersion = 0; // used HTTP authentication // --- Firefox only if (browser?.proxy?.onRequest) { @@ -30,6 +40,8 @@ export class OnRequest { browser.tabs.onRemoved.addListener(tabId => delete this.tabProxy[tabId]); // mark incognito/container browser.tabs.onCreated.addListener(e => this.checkPageAction(e)); + // check for HTTP authentication use + browser.runtime.getBrowserInfo().then(info => this.browserVersion = parseInt(info.version)); } } @@ -77,7 +89,7 @@ export class OnRequest { switch (true) { // --- check local & global passthrough case this.bypass(e.url): - this.setAction(null, tabId); + this.setAction(tabId); return {type: 'direct'}; // --- tab proxy @@ -96,7 +108,7 @@ export class OnRequest { case this.mode === 'disable': // pass direct case this.mode === 'direct': // pass direct case this.mode.includes('://') && !/:\d+$/.test(this.mode): // PAC URL is set - this.setAction(null, tabId); + this.setAction(tabId); return {type: 'direct'}; case this.mode === 'pattern': // check if url matches patterns @@ -117,12 +129,12 @@ export class OnRequest { } } - this.setAction(null, tabId); + this.setAction(tabId); return {type: 'direct'}; // no match } static processProxy(proxy, tabId) { - this.setAction(proxy, tabId); + this.setAction(tabId, proxy); const {type, hostname: host, port, username, password, proxyDNS} = proxy || {}; if (!type || type === 'direct') { return {type: 'direct'}; } @@ -146,13 +158,15 @@ export class OnRequest { // https://searchfox.org/mozilla-central/source/toolkit/components/extensions/ProxyChannelFilter.sys.mjs#167 // proxyAuthorizationHeader on Firefox only applies to HTTPS (not HTTP and it breaks the API and sends DIRECT) // proxyAuthorizationHeader added to reduce the authentication request in webRequest.onAuthRequired - type === 'https' && (response.proxyAuthorizationHeader = 'Basic ' + btoa(proxy.username + ':' + proxy.password)); + // HTTP authentication fixed in Firefox 125 + (type === 'https' || this.browserVersion >= 125) && + (response.proxyAuthorizationHeader = 'Basic ' + btoa(proxy.username + ':' + proxy.password)); } return response; } - static setAction(item, tabId) { + static setAction(tabId, item) { // Set to -1 if the request isn't related to a tab if (tabId === -1) { return; } @@ -193,31 +207,12 @@ export class OnRequest { // it can't catch a domain set by user to 127.0.0.1 in the hosts file static localhost(url) { const [, host] = url.split(/:\/\/|\//, 2); // hostname with/without port - const isIP = /^[\d.:]+$/.test(host); - - switch (true) { - // --- localhost & - // case host === 'localhost': - case !host.includes('.'): // plain hostname (no dots) - case host.endsWith('.localhost'): // *.localhost - - // --- IPv4 - // case host === '127.0.0.1': - case isIP && host.startsWith('127.'): // 127.0.0.1 up to 127.255.255.254 - case isIP && host.startsWith('169.254.'): // 169.254.0.0/16 - case isIP && host.startsWith('192.168.'): // 192.168.0.0/16 192.168.0.0 192.168.255.255 - - // --- IPv6 - // case host === '[::1]': - case host.startsWith('[::1]'): // literal IPv6 [::1]:80 with/without port - case host.startsWith('[FE80::]'): // literal IPv6 [FE80::]/10 - return true; - } + return App.isLocal(host); } static isInNet(url) { // check if IP address - if(!/^[a-z]+:\/\/\d+(\.\d+){3}(:\d+)?\//.test(url)) { return; } + if (!/^[a-z]+:\/\/\d+(\.\d+){3}(:\d+)?\//.test(url)) { return; } const ipa = url.split(/[:/.]+/, 5).slice(1); // IP array const ip = ipa.map(i => i.padStart(3, '0')).join(''); // convert to padded string @@ -225,30 +220,31 @@ export class OnRequest { } // ---------- Tab Proxy ---------------------------------- - static async setTabProxy(pxy) { - const [tab] = await browser.tabs.query({currentWindow: true, active: true}); + static setTabProxy(tab, pxy) { + // const [tab] = await browser.tabs.query({currentWindow: true, active: true}); switch (true) { case !/https?:\/\/.+/.test(tab.url): // unacceptable URLs case this.bypass(tab.url): // check local & global passthrough return; } - this.tabProxy[tab.id] = pxy; - // PageAction.set(tab.id, pxy); + // set or unset + pxy ? this.tabProxy[tab.id] = pxy : delete this.tabProxy[tab.id]; + this.setAction(tab.id, pxy); } - static async unsetTabProxy() { - const [tab] = await browser.tabs.query({currentWindow: true, active: true}); - delete this.tabProxy[tab.id]; - // PageAction.unset(tab.id); - } + // static async unsetTabProxy() { + // const [tab] = await browser.tabs.query({currentWindow: true, active: true}); + // delete this.tabProxy[tab.id]; + // // PageAction.unset(tab.id); + // } // ---------- Update Page Action ------------------------- static onUpdated(tabId, changeInfo, tab) { if (changeInfo.status !== 'complete') { return; } const pxy = this.tabProxy[tabId]; - pxy ? this.setAction(pxy, tabId) : this.checkPageAction(tab); + pxy ? this.setAction(tab.id, pxy) : this.checkPageAction(tab); } // ---------- Incognito/Container ------------------------ @@ -256,6 +252,6 @@ export class OnRequest { if (tab.id === -1 || this.tabProxy[tab.id]) { return; } // not if tab proxy is set const pxy = tab.incognito ? this.container.incognito : this.container[tab.cookieStoreId]; - pxy && this.setAction(pxy, tab.id); + pxy && this.setAction(tab.id, pxy); } } \ No newline at end of file diff --git a/src/content/options.html b/src/content/options.html index 28a808d..128037f 100644 --- a/src/content/options.html +++ b/src/content/options.html @@ -191,21 +191,28 @@ diff --git a/src/content/options.js b/src/content/options.js index 481442d..11dbd5c 100644 --- a/src/content/options.js +++ b/src/content/options.js @@ -33,7 +33,6 @@ export class Popup { await App.getPref(); // ---------- Incognito Access ----------------------------- -// eslint-disable-next-line no-unused-vars class IncognitoAccess { static { @@ -62,7 +61,6 @@ class Toggle { // ---------- /Toggle -------------------------------------- // ---------- Theme ---------------------------------------- -// eslint-disable-next-line no-unused-vars class Theme { static { this.elem = [document, ...[...document.querySelectorAll('iframe')].map(i => i.contentDocument)]; @@ -82,6 +80,9 @@ class Options { static { // --- container + // using generic names + // https://bugzilla.mozilla.org/show_bug.cgi?id=1386673 + // Make Contextual Identity extensions be an optional permission this.container = document.querySelectorAll('.options .container select'); // --- keyboard Shortcut @@ -209,7 +210,7 @@ class Options { 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); + const del = Object.keys(syncObj).filter(i => /^\d+$/.test(i) && i * 1 >= pref.data.length); del[0] && browser.storage.sync.remove(del); }); }) @@ -247,7 +248,7 @@ class Options { Object.hasOwn(obj, i.dataset.id) && (obj[i.dataset.id] = i.type === 'checkbox' ? i.checked : i.value.trim()); }); - // --- check type: http | https | socks4 | socks5 | pac | direct + // --- check type: http | https | socks4 | socks5 | quic | pac | direct switch (true) { // DIRECT case obj.type === 'direct': @@ -266,7 +267,7 @@ class Options { obj.port = port; break; - // http | https | socks4 | socks5 + // http | https | socks4 | socks5 | quic case !obj.hostname: this.setInvalid(elem, 'hostname'); alert(browser.i18n.getMessage('hostnamePortError')); @@ -384,7 +385,6 @@ class Options { // ---------- /Options ------------------------------------- // ---------- browsingData --------------------------------- -// eslint-disable-next-line no-unused-vars class BrowsingData { static { @@ -420,7 +420,6 @@ class BrowsingData { // ---------- /browsingData -------------------------------- // ---------- WebRTC --------------------------------------- -// eslint-disable-next-line no-unused-vars class WebRTC { static { @@ -520,7 +519,7 @@ class Proxies { 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); + 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; @@ -529,59 +528,82 @@ class Proxies { pxy.dataset.type = e.target.value; // show/hide elements const id = e.target.options[e.target.selectedIndex].textContent; - const fillData = () => { - flag.textContent = id === 'DIRECT' ? '⮕' : '🌎'; + const fillData = (local) => { sumTitle.textContent = id; title.value = id; + + if (local) { + flag.textContent = '🖥️'; + hostname.value = '127.0.0.1'; + } }; switch (id) { case 'PAC': fillData(); + flag.textContent = '🌎'; break; case 'DIRECT': fillData(); + flag.textContent = '⮕'; hostname.value = id; break; - // --- auto-fill helpers - case 'TOR': - fillData(); - hostname.value = '127.0.0.1'; - port.value = '9050'; + // --- server auto-fill helpers + case 'Burp': + fillData(true); + port.value = '8080'; + break; + + case 'Privoxy': + fillData(true); + port.value = '8118'; break; case 'Psiphon': - fillData(); - hostname.value = '127.0.0.1'; + fillData(true); port.value = '60351'; break; - case 'Privoxy': - fillData(); - hostname.value = '127.0.0.1'; - port.value = '8118'; + case 'TOR': + fillData(true); + port.value = '9050'; break; // By default v2rayA will open 20170 (socks5), 20171 (http), 20172 (http with shunt rules) ports through the core case 'v2rayA-socks5': - fillData(); - hostname.value = '127.0.0.1'; + fillData(true); port.value = '20170'; break; case 'v2rayA-http': - fillData(); - hostname.value = '127.0.0.1'; + fillData(true); port.value = '20171'; break; case 'v2rayA-http-rules': - fillData(); - hostname.value = '127.0.0.1'; + fillData(true); port.value = '20172'; break; + + case 'NekoRay-socks5': + fillData(true); + port.value = '2080'; + break; + + case 'NekoRay-http': + fillData(true); + port.value = '2081'; + break; + + case 'Shadowsocks': + fillData(true); + port.value = '1080'; + break; + + default: + flag.textContent = App.getFlag(cc.value); } }); @@ -643,7 +665,7 @@ class Proxies { const pxyTitle = item.title || id; // --- summary - flag.textContent = App.getFlag(item.cc); + flag.textContent = App.showFlag(item); sumTitle.textContent = pxyTitle; active.checked = item.active; @@ -742,7 +764,7 @@ class Proxies { ImportExport.fileReader(file, data => { try { data = JSON.parse(data); } - catch(e) { + catch { App.notify(browser.i18n.getMessage('fileParseError')); // display the error return; } @@ -820,7 +842,6 @@ class Proxies { // ---------- /Proxies ------------------------------------- // ---------- Drag and Drop -------------------------------- -// eslint-disable-next-line no-unused-vars class Drag { static { @@ -845,7 +866,6 @@ class Drag { // ---------- /Drag and Drop ------------------------------- // ---------- Import FP Account ---------------------------- -// eslint-disable-next-line no-unused-vars class ImportFoxyProxyAccount { static { @@ -917,7 +937,7 @@ class ImportFoxyProxyAccount { return fetch('https://getfoxyproxy.org/webservices/get-accounts.php', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: `username=${encodeURIComponent(username)}&password=${(encodeURIComponent(password))}` + body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}` }) .then(response => response.json()) .then(data => { @@ -936,7 +956,6 @@ class ImportFoxyProxyAccount { // ---------- /Import FP Account --------------------------- // ---------- Import From URL ------------------------------ -// eslint-disable-next-line no-unused-vars class importFromUrl { static { @@ -971,7 +990,6 @@ class importFromUrl { // ---------- /Import From URL ----------------------------- // ---------- Import List ---------------------------------- -// eslint-disable-next-line no-unused-vars class ImportProxyList { static { @@ -1002,7 +1020,7 @@ class ImportProxyList { static parseSimple(item) { // example.com:3128:user:pass const [hostname, port, username = '', password = ''] = item.split(':'); - if (!hostname || !port || !(port*1)) { + if (!hostname || !port || !(port * 1)) { alert(`Error: ${item}`); return; } @@ -1097,7 +1115,6 @@ class ImportProxyList { // ---------- /Import List --------------------------------- // ---------- Import Older Export -------------------------- -// eslint-disable-next-line no-unused-vars class importFromOlder { static { @@ -1121,7 +1138,7 @@ class importFromOlder { static parseJSON(data) { try { data = JSON.parse(data); } - catch(e) { + catch { App.notify(browser.i18n.getMessage('fileParseError')); // display the error return; } @@ -1156,7 +1173,7 @@ class Tester { this.url.value = this.url.value.trim(); this.pattern.value = this.pattern.value.trim(); - if(!this.url.value || !this.pattern.value ) { + if (!this.url.value || !this.pattern.value) { this.result.textContent = '❌'; return; } @@ -1198,4 +1215,11 @@ ImportExport.init(pref, () => { // ---------- /Import/Export Preferences ------------------- // ---------- Navigation ----------------------------------- -Nav.get(); \ No newline at end of file +Nav.get(); + +// globalThis.FP = { +// proxies: document.querySelectorAll('details.proxy'), +// delete(n) { +// n.forEach(i => i.remove()); +// } +// }; \ No newline at end of file diff --git a/src/content/pattern.js b/src/content/pattern.js index 72d2544..9a62477 100644 --- a/src/content/pattern.js +++ b/src/content/pattern.js @@ -6,7 +6,7 @@ export class Pattern { new RegExp(pat); return true; } - catch(error) { + catch (error) { showError && alert([browser.i18n.getMessage('regexError'), str, error].join('\n')); } } @@ -80,7 +80,7 @@ export class Pattern { return [...Array(4)].map(() => { const n = Math.min(mask, 8); mask -= n; - return 256 - Math.pow(2, 8-n); + return 256 - Math.pow(2, 8 - n); }).join('.'); } @@ -88,7 +88,7 @@ export class Pattern { static getRange(ip, mask) { let st = ip.split('.'); // ip array const ma = mask.split('.'); // mask array - let end = st.map((v, i) => Math.min(v-ma[i]+255, 255) + ''); // netmask wildcard array + let end = st.map((v, i) => Math.min(v - ma[i] + 255, 255) + ''); // netmask wildcard array st = st.map(i => i.padStart(3, '0')).join(''); end = end.map(i => i.padStart(3, '0')).join(''); diff --git a/src/content/popup.css b/src/content/popup.css index 40bde39..ccd8f82 100644 --- a/src/content/popup.css +++ b/src/content/popup.css @@ -27,11 +27,16 @@ body { transition: opacity 0.5s; } +/* + https://bugzilla.mozilla.org/show_bug.cgi?id=1883896 + Remove UA styles for :is(article, aside, nav, section) h1 +*/ h1 { color: var(--nav-color); background-color: var(--nav-bg); margin: 0; padding: 0.5em; + font-size: 1.2em; } h1 img { @@ -116,6 +121,10 @@ div.list label:hover { color: var(--dim); } +.data.off { + display: none; +} + input[name="server"] { grid-row: span 2; transition: 0.5s ease-in-out; @@ -131,7 +140,7 @@ input#filter { background: url('../image/filter.svg') no-repeat left 0.5em center / 1em; padding-left: 2em; margin-bottom: 0.2em; - /* grid-column: span 2; */ + grid-column: span 2; } div.list label.off { diff --git a/src/content/popup.html b/src/content/popup.html index 12123f4..5962321 100644 --- a/src/content/popup.html +++ b/src/content/popup.html @@ -29,16 +29,26 @@

- - - + + + + + - - + + + + + + + +
diff --git a/src/content/popup.js b/src/content/popup.js index f7e742e..893e851 100644 --- a/src/content/popup.js +++ b/src/content/popup.js @@ -7,7 +7,6 @@ import './i18n.js'; await App.getPref(); // ---------- Popup ---------------------------------------- -// eslint-disable-next-line no-unused-vars class Popup { static { @@ -18,8 +17,24 @@ class Popup { document.querySelectorAll('button').forEach(i => i.addEventListener('click', e => this.processButtons(e))); this.list = document.querySelector('div.list'); - this.select = document.querySelector('select'); - this.proxyCache = {}; // used to find proxy + + // --- Quick Add (not for storage.managed) + this.quickAdd = document.querySelector('select#quickAdd'); + !pref.managed && this.quickAdd.addEventListener('change', (e) => { + if (!this.quickAdd.value) { return; } + + browser.runtime.sendMessage({id: 'quickAdd', pref, host: this.quickAdd.value, tab: this.tab}); + this.quickAdd.selectedIndex = 0; // reset select option + }); + + // --- Tab Proxy (not for storage.managed, firefox only) + this.tabProxy = document.querySelector('select#tabProxy'); + !pref.managed && App.firefox && this.tabProxy.addEventListener('change', () => { + if (!this.tab) { return; } + + const proxy = this.tabProxy.value && this.proxyCache[this.tabProxy.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'); @@ -53,12 +68,12 @@ class Popup { const id = item.type === 'pac' ? item.pac : `${item.hostname}:${item.port}`; const label = labelTemplate.cloneNode(true); const [flag, title, portNo, radio, data] = label.children; - flag.textContent = App.getFlag(item.cc); + flag.textContent = App.showFlag(item); title.textContent = item.title || id; portNo.textContent = item.port; radio.value = item.type === 'direct' ? 'direct' : id; radio.checked = id === pref.mode; - data.textContent = [item.city, ...Location.get(item.cc)].filter(Boolean).join('\n'); + data.textContent = [item.city, ...Location.get(item.cc)].filter(Boolean).join('\n') || item.hostname; docFrag.appendChild(label); }); @@ -69,26 +84,31 @@ class Popup { ); // --- Add Hosts to select - // filter out PAC, limit to 10 - pref.data.filter(i => i.active && i.type !== 'pac').filter((i, idx) => idx < 10).forEach(item => { - const flag = App.getFlag(item.cc); + // used to find proxy, filter out PAC, limit to 10 + this.proxyCache = pref.data.filter(i => i.active && i.type !== 'pac').filter((i, idx) => idx < 10); + + this.proxyCache.forEach((item, index) => { + const flag = App.showFlag(item); const value = `${item.hostname}:${item.port}`; const opt = new Option(flag + ' ' + (item.title || value), value); + opt.dataset.index = index; // opt.style.color = item.color; // supported on Chrome, not on Firefox docFrag.appendChild(opt); - - this.proxyCache[value] = item; // cache to find later }); - // add a DIRECT option to the end - // const opt = new Option('⮕ Direct', 'DIRECT'); - // docFrag.appendChild(opt); - // this.proxyCache['DIRECT'] = { - // type: 'direct', - // hostname: 'DIRECT' - // }; + this.quickAdd.appendChild(docFrag.cloneNode(true)); + this.tabProxy.appendChild(docFrag); - this.select.appendChild(docFrag); + App.firefox && this.checkTabProxy(); + } + + static async checkTabProxy() { + const [tab] = await browser.tabs.query({currentWindow: true, active: true}); + if (!/https?:\/\/.+/.test(tab.url)) { return; } // unacceptable URLs + + this.tab = tab; // cache tab + const item = await browser.runtime.sendMessage({id: 'getTabProxy'}); + item && (this.tabProxy.value = `${item.hostname}:${item.port}`); } static processSelect(mode) { @@ -124,34 +144,34 @@ class Popup { window.close(); break; - case 'quickAdd': - if (!this.select.value) { break; } - if (pref.managed) { break; } // not for storage.managed + // case 'quickAdd': + // if (!this.quickAdd.value) { break; } + // if (pref.managed) { break; } // not for storage.managed - browser.runtime.sendMessage({id: 'quickAdd', pref, host: this.select.value}); - this.select.selectedIndex = 0; // reset select option - break; + // browser.runtime.sendMessage({id: 'quickAdd', pref, host: this.quickAdd.value}); + // this.quickAdd.selectedIndex = 0; // reset select option + // break; case 'excludeHost': if (pref.managed) { break; } // not for storage.managed - browser.runtime.sendMessage({id: 'excludeHost', pref}); + browser.runtime.sendMessage({id: 'excludeHost', pref, tab: this.tab}); break; - case 'setTabProxy': - if (!App.firefox || !this.select.value) { break; } // firefox only - if (pref.managed) { break; } // not for storage.managed + // case 'setTabProxy': + // if (!App.firefox || !this.tabProxy.value) { break; } // firefox only + // if (pref.managed) { break; } // not for storage.managed - browser.runtime.sendMessage({id: 'setTabProxy', proxy: this.proxyCache[this.select.value]}); - this.select.selectedIndex = 0; // reset select option - break; + // this.tabId && browser.runtime.sendMessage({id: 'setTabProxy', proxy: this.proxyCache[this.tabProxy.value], tabId: this.tabId}); + // // this.tabProxy.selectedIndex = 0; // reset select option + // break; - case 'unsetTabProxy': - if (!App.firefox) { break; } // firefox only - if (pref.managed) { break; } // not for storage.managed + // case 'unsetTabProxy': + // if (!App.firefox) { break; } // firefox only + // if (pref.managed) { break; } // not for storage.managed - browser.runtime.sendMessage({id: 'unsetTabProxy'}); - break; + // browser.runtime.sendMessage({id: 'unsetTabProxy'}); + // break; } } diff --git a/src/content/proxy.js b/src/content/proxy.js index 190996a..d093304 100644 --- a/src/content/proxy.js +++ b/src/content/proxy.js @@ -23,7 +23,7 @@ export class Proxy { } static onMessage(message) { - const {id, pref, host, proxy, dark} = message; + const {id, pref, host, proxy, dark, tab} = message; switch (id) { case 'setProxy': Action.dark = dark; @@ -31,20 +31,23 @@ export class Proxy { break; case 'quickAdd': - this.quickAdd(pref, host); + this.quickAdd(pref, host, tab); break; case 'excludeHost': - this.excludeHost(pref); + this.excludeHost(pref, tab); break; case 'setTabProxy': - OnRequest.setTabProxy(proxy); + OnRequest.setTabProxy(tab, proxy); break; - case 'unsetTabProxy': - OnRequest.unsetTabProxy(); - break; + case 'getTabProxy': + return OnRequest.tabProxy[tab.id]; + + // case 'unsetTabProxy': + // OnRequest.unsetTabProxy(); + // break; } } @@ -233,6 +236,7 @@ if (find${idx} !== 'DIRECT') { return find${idx}; }`).join('\n\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") @@ -268,13 +272,13 @@ String.raw`function FindProxyForURL(url, host) { default: type = type.toUpperCase(); } - return `${type} ${hostname}:${parseInt(port)}`; // prepare for augmented port + return `${type} ${hostname}:${parseInt(port)}`; // prepare for augmented port } // ---------- Quick Add/Exclude Host --------------------- - static async quickAdd(pref, host) { - const activeTab = await this.getActiveTab(); - const url = this.getURL(activeTab[0].url); + static quickAdd(pref, host, tab) { + // const activeTab = tab ? [tab] : await this.getActiveTab(); + const url = this.getURL(tab.url); if (!url) { return; } const pattern = '^' + url.origin.replaceAll('.', '\\.') + '/'; @@ -294,9 +298,9 @@ String.raw`function FindProxyForURL(url, host) { } // Chrome commands returns command, tab - static async excludeHost(pref, tab) { - const activeTab = tab ? [tab] : await this.getActiveTab(); - const url = this.getURL(activeTab[0].url); + static excludeHost(pref, tab) { + // const activeTab = tab ? [tab] : await this.getActiveTab(); + const url = this.getURL(tab.url); if (!url) { return; } const pattern = url.host; @@ -313,9 +317,9 @@ String.raw`function FindProxyForURL(url, host) { this.set(pref); // update Proxy } - static getActiveTab() { - return browser.tabs.query({currentWindow: true, active: true}); - } + // static getActiveTab() { + // return browser.tabs.query({currentWindow: true, active: true}); + // } static getURL(str) { const url = new URL(str); diff --git a/src/content/show.js b/src/content/show.js index 2377171..2cc06bf 100644 --- a/src/content/show.js +++ b/src/content/show.js @@ -1,7 +1,6 @@ import {App} from './app.js'; // ---------- Show (Side Effect) ----------- -// eslint-disable-next-line no-unused-vars class Show { static { diff --git a/src/content/theme.css b/src/content/theme.css index 726eb85..5f0e4cb 100644 --- a/src/content/theme.css +++ b/src/content/theme.css @@ -33,7 +33,6 @@ background-color: var(--hover); } - @media screen and (prefers-color-scheme: dark) { :root.moonlight { --bg: #333;