Files
chrome-extenstions/foxyproxy/content/on-request.js
2026-01-20 21:53:59 +03:30

305 lines
11 KiB
JavaScript

// 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);
}
}