This commit is contained in:
Fabian Freund
2025-02-16 23:21:44 +01:00
parent ddb0f14059
commit 6368e30479
81 changed files with 20308 additions and 88 deletions
@@ -0,0 +1,114 @@
import { Store } from '../store/Store'
import { HttpProxySettings, HttpsProxySettings, ProxySettings } from '../domain/ProxySettings'
import { ProxyInfo, Socks5ProxyInfo } from '../domain/ProxyInfo'
import { ProxyType } from '../domain/ProxyType'
import BlockingResponse = browser.webRequest.BlockingResponse
import _OnAuthRequiredDetails = browser.webRequest._OnAuthRequiredDetails
import _OnRequestDetails = browser.proxy._OnRequestDetails
const localhosts = new Set(['localhost', '127.0.0.1', '[::1]'])
const containerIdentifier = 'firefox-container-'
type DoNotProxy = never[]
export const doNotProxy: DoNotProxy = []
const emergencyBreak: Socks5ProxyInfo = {
type: ProxyType.Socks5,
host: 'emergency-break-proxy.localhost',
port: 1,
failoverTimeout: 1,
username: 'nonexistent user',
password: 'dummy password',
proxyDNS: true
}
export default class BackgroundMain {
store: Store
constructor({ store }: { store: Store }) {
this.store = store
}
initializeAuthListener(cookieStoreId: string, proxy: HttpProxySettings | HttpsProxySettings): void {
const listener: (details: _OnAuthRequiredDetails) => BlockingResponse = (details) => {
if (!details.isProxy) return {}
if (details.cookieStoreId !== cookieStoreId) return {}
// TODO: Fix in @types/firefox-webext-browser
// @ts-expect-error
const info = details.proxyInfo
if (info.host !== proxy.host || info.port !== proxy.port || info.type !== proxy.type) return {}
const result = { authCredentials: { username: proxy.username, password: proxy.password } }
browser.webRequest.onAuthRequired.removeListener(listener)
return result
}
browser.webRequest.onAuthRequired.addListener(
listener,
{ urls: ['<all_urls>'] },
['blocking']
)
}
// TODO: Fix in @types/firefox-webext-browser
async onRequest(requestDetails: Pick<_OnRequestDetails, 'cookieStoreId' | 'url' | 'tabId'>): Promise<DoNotProxy | ProxyInfo[]> {
const tab = (await browser.tabs.get(requestDetails.tabId))
if (tab.cookieStoreId?.startsWith(containerIdentifier) === true) {
try {
const cookieStoreId = tab.cookieStoreId.substring(containerIdentifier.length)
const proxies = await this.store.getProxiesForContainer(cookieStoreId)
if (proxies.length > 0) {
proxies.forEach(p => {
if (p.type === ProxyType.Http || p.type === ProxyType.Https) {
this.initializeAuthListener(cookieStoreId, p)
}
})
const result: ProxyInfo[] = proxies.filter((p: ProxySettings) => {
try {
const documentUrl = new URL(requestDetails.url)
const isLocalhost = localhosts.has(documentUrl.hostname)
if (isLocalhost && p.doNotProxyLocal) {
return false
}
} catch (e) {
console.error(e)
}
return true
}).map(p => p.asProxyInfo())
if (result.length === 0) {
return [emergencyBreak]
}
return result
}
return [emergencyBreak]
} catch (e: unknown) {
console.error(`Error in onRequest listener: ${e as string}`)
return [emergencyBreak]
}
}
return doNotProxy
}
run(browser: { proxy: any }): void {
const filter = { urls: ['<all_urls>'] }
browser.proxy.onRequest.addListener(this.onRequest.bind(this), filter)
browser.proxy.onError.addListener((e: Error) => {
console.error('Proxy error', e)
})
}
}
@@ -0,0 +1,44 @@
import { Socks5ProxySettings } from 'src/domain/ProxySettings';
import { Store } from '../store/Store'
import BackgroundMain from './BackgroundMain'
console.log('Background script started')
const store = new Store()
interface Message {
action: 'setProxyPort' | 'addContainerProxy' | 'removeContainerProxy';
args: any;
}
const port = browser.runtime.connectNative("containerProxy");
port.onMessage.addListener((raw: unknown): void => {
const message = raw as Message;
switch (message.action) {
case "setProxyPort":
store.putProxy(new Socks5ProxySettings({
id: 'tor',
type: 'socks',
host: '127.0.0.1',
port: message.args,
doNotProxyLocal: true,
title: 'Tor',
proxyDNS: true,
}))
console.log('put tor port ' + message.args)
break
case "addContainerProxy":
store.setContainerProxyRelation(message.args, "tor")
console.log('added container relation ' + message.args)
break
case "removeContainerProxy":
store.removeContainerProxyRelation(message.args, "tor")
break
}
});
const backgroundListener = new BackgroundMain({ store })
backgroundListener.run(browser)
@@ -0,0 +1,36 @@
import { ProxyType } from './ProxyType'
export type ProxyInfo = Socks4ProxyInfo | Socks5ProxyInfo | HttpProxyInfo | HttpsProxyInfo
interface ProxyInfoBase<TY extends ProxyType> {
type: TY
host: string
port: number
failoverTimeout: number
}
export interface Socks5ProxyInfo extends ProxyInfoBase<ProxyType.Socks5> {
type: ProxyType.Socks5
username: string
password: string
proxyDNS: boolean
proxyAuthorizationHeader?: undefined // MUST not present, otherwise failure that cannot be prevented/recovered from
}
export interface Socks4ProxyInfo extends ProxyInfoBase<ProxyType.Socks4> {
type: ProxyType.Socks4
proxyDNS: boolean
proxyAuthorizationHeader?: undefined // MUST not present, otherwise failure that cannot be prevented/recovered from
}
export interface HttpsProxyInfo extends ProxyInfoBase<ProxyType.Https> {
type: ProxyType.Https
proxyDNS?: undefined
proxyAuthorizationHeader: string
}
export interface HttpProxyInfo extends ProxyInfoBase<ProxyType.Http> {
type: ProxyType.Http
proxyDNS?: undefined
proxyAuthorizationHeader?: undefined // MUST not present, otherwise failure that cannot be prevented/recovered from
}
@@ -0,0 +1,170 @@
import { ProxyType } from './ProxyType'
import { ProxyDao } from '../store/Store'
import { HttpProxyInfo, HttpsProxyInfo, ProxyInfo, Socks4ProxyInfo, Socks5ProxyInfo } from './ProxyInfo'
/* eslint-disable @typescript-eslint/no-namespace,@typescript-eslint/no-redeclare */
export type ProxySettings = Socks4ProxySettings | Socks5ProxySettings | HttpProxySettings | HttpsProxySettings
const failoverTimeout = 5
export namespace ProxySettings {
export function tryFromDao(dao: ProxyDao): ProxySettings | undefined {
switch (dao.type) {
case 'socks':
return new Socks5ProxySettings(dao)
case 'socks4':
return new Socks4ProxySettings(dao)
case 'http':
return new HttpProxySettings(dao)
case 'https':
return new HttpsProxySettings(dao)
default:
return undefined
}
}
}
abstract class ProxySettingsBase<PI extends ProxyInfo> {
readonly id: string
readonly title: string
readonly host: string
readonly port: number
readonly doNotProxyLocal: boolean
protected constructor(dao: ProxyDao) {
this.id = dao.id
this.title = dao.title
this.host = dao.host
this.port = dao.port
this.doNotProxyLocal = dao.doNotProxyLocal
}
abstract get type(): PI['type']
abstract asProxyInfo(): PI
abstract asDao(): ProxyDao
get url(): string {
return `${this.type}://${this.host}:${this.port}`
}
protected baseDao(): ProxyDao {
return {
id: this.id,
title: this.title,
type: this.type,
host: this.host,
port: this.port,
doNotProxyLocal: this.doNotProxyLocal
}
}
}
export class Socks5ProxySettings extends ProxySettingsBase<Socks5ProxyInfo> {
readonly username?: string
readonly password?: string
readonly proxyDNS: boolean
constructor(dao: ProxyDao) {
super(dao)
this.username = dao.username
this.password = dao.password
this.proxyDNS = dao.proxyDNS ?? true
}
get type(): ProxyType.Socks5 {
return ProxyType.Socks5
}
asProxyInfo(): Socks5ProxyInfo {
return {
type: this.type,
host: this.host,
port: this.port,
username: this.username ?? '',
password: this.password ?? '',
proxyDNS: this.proxyDNS,
failoverTimeout
}
}
asDao(): ProxyDao {
return { ...super.baseDao(), username: this.username, password: this.password, proxyDNS: this.proxyDNS }
}
}
export class Socks4ProxySettings extends ProxySettingsBase<Socks4ProxyInfo> {
readonly proxyDNS: boolean
constructor(dao: ProxyDao) {
super(dao)
this.proxyDNS = dao.proxyDNS ?? true
}
get type(): ProxyType.Socks4 {
return ProxyType.Socks4
}
asProxyInfo(): Socks4ProxyInfo {
return {
type: this.type,
host: this.host,
port: this.port,
proxyDNS: this.proxyDNS,
failoverTimeout
}
}
asDao(): ProxyDao {
return { ...super.baseDao(), proxyDNS: this.proxyDNS }
}
}
abstract class HttpBasedProxySettings<PI extends (HttpsProxyInfo | HttpProxyInfo)> extends ProxySettingsBase<PI> {
readonly username?: string
readonly password?: string
constructor(dao: ProxyDao) {
super(dao)
this.username = dao.username
this.password = dao.password
}
asDao(): ProxyDao {
return { ...super.baseDao(), username: this.username, password: this.password }
}
}
export class HttpsProxySettings extends HttpBasedProxySettings<HttpsProxyInfo> {
get type(): ProxyType.Https {
return ProxyType.Https
}
asProxyInfo(): HttpsProxyInfo {
return {
type: this.type,
host: this.host,
port: this.port,
proxyAuthorizationHeader: '', //generateAuthorizationHeader(this.username ?? '', this.password ?? ''),
failoverTimeout
}
}
}
export class HttpProxySettings extends HttpBasedProxySettings<HttpProxyInfo> {
get type(): ProxyType.Http {
return ProxyType.Http
}
asProxyInfo(): HttpProxyInfo {
return {
type: this.type,
host: this.host,
port: this.port,
failoverTimeout
}
}
}
@@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/no-namespace,no-redeclare,import/export */
export enum ProxyType {
Socks5 = 'socks',
Socks4 = 'socks4',
Http = 'http',
Https = 'https'
}
export namespace ProxyType {
export function tryFromString(s: string): ProxyType | undefined {
switch (s) {
case 'socks':
return ProxyType.Socks5
case 'socks4':
return ProxyType.Socks4
case 'http':
return ProxyType.Http
case 'https':
return ProxyType.Https
default:
return undefined
}
}
}
@@ -0,0 +1,28 @@
{
"manifest_version": 2,
"name": "Container proxy",
"version": "0.1.22",
"description": "Container Proxy",
"browser_specific_settings": {
"gecko": {
"id": "container-proxy@lensai.eu"
}
},
"permissions": [
"<all_urls>",
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"webRequest",
"webRequestBlocking",
"storage",
"contextualIdentities",
"cookies",
"proxy"
],
"background": {
"scripts": [
"background/index.js"
]
}
}
@@ -0,0 +1,164 @@
import { ProxyInfo } from '../domain/ProxyInfo'
import { ProxyType } from '../domain/ProxyType'
import { ProxySettings } from '../domain/ProxySettings'
const tryFromDao = ProxySettings.tryFromDao
/* eslint-disable @typescript-eslint/no-namespace,no-redeclare,import/export */
export interface ProxyDao {
id: string
title: string
type: string
host: string
port: number
username?: string
password?: string
proxyDNS?: boolean
doNotProxyLocal: boolean
}
export namespace ProxyDao {
export function toProxyInfo(proxy: Pick<ProxyDao, 'type' | 'host' | 'port' | 'username' | 'password' | 'proxyDNS'>): ProxyInfo | undefined {
const type = ProxyType.tryFromString(proxy.type)
if (type === undefined) {
return
}
const base = {
host: proxy.host,
port: proxy.port,
failoverTimeout: 5
}
switch (type) {
case ProxyType.Socks5:
return {
type,
...base,
username: proxy.username ?? '',
password: proxy.password ?? '',
proxyDNS: proxy.proxyDNS ?? true
}
case ProxyType.Socks4:
return {
type,
...base,
proxyDNS: proxy.proxyDNS ?? true
}
case ProxyType.Http:
return {
type,
...base
}
case ProxyType.Https:
return {
type,
...base,
proxyAuthorizationHeader: '' //generateAuthorizationHeader(proxy.username ?? '', proxy.password ?? '')
}
}
}
}
export class Store {
private proxies: ProxyDao[] = []
private relations: { [key: string]: string[] } = {}
async getAllProxies(): Promise<ProxySettings[]> {
const proxyDaos = await this.getAllProxyDaos()
const result: ProxySettings[] = proxyDaos.map(tryFromDao).filter(p => p !== undefined) as ProxySettings[]
return result
}
async getProxyById(id: string): Promise<ProxySettings | null> {
const proxies = await this.getAllProxies()
const index = proxies.findIndex(p => p.id === id)
if (index === -1) {
return null
} else {
return proxies[index]
}
}
async putProxy(proxy: ProxySettings): Promise<void> {
const proxies = await this.getAllProxyDaos()
const index = proxies.findIndex(p => p.id === proxy.id)
if (index !== -1) {
proxies[index] = proxy.asDao()
} else {
proxies.push(proxy.asDao())
}
await this.saveProxyDaos(proxies)
}
async deleteProxyById(id: string): Promise<void> {
const proxies = await this.getAllProxyDaos()
const index = proxies.findIndex(p => p.id === id)
if (index !== -1) {
proxies.splice(index, 1)
await this.saveProxyDaos(proxies)
}
}
async getRelations(): Promise<{ [key: string]: string[] }> {
return this.relations
}
async setContainerProxyRelation(cookieStoreId: string, proxyId: string): Promise<void> {
this.relations[cookieStoreId] = [proxyId]
}
async removeContainerProxyRelation(cookieStoreId: string, proxyId: string): Promise<void> {
const currentRelations = this.relations[cookieStoreId] ?? []
this.relations[cookieStoreId] = currentRelations.filter(id => id !== proxyId)
// If no relations left for this container, clean up by removing the key
if (this.relations[cookieStoreId].length === 0) {
delete this.relations[cookieStoreId]
}
}
async getProxiesForContainer(cookieStoreId: string): Promise<ProxySettings[]> {
const relations = await this.getRelations()
const proxyIds: string[] = relations[cookieStoreId] ?? []
if (proxyIds.length === 0) {
return []
}
const proxies = await this.getAllProxies()
const proxyById: { [key: string]: ProxySettings } = {}
proxies.forEach(function (p) { proxyById[p.id] = p })
return proxyIds.map(pId => proxyById[pId])
.filter(p => p !== undefined)
.map(fillInDefaults)
.map(tryFromDao)
.filter(p => p !== undefined) as ProxySettings[]
}
private async saveProxyDaos(p: ProxyDao[]): Promise<void> {
this.proxies = p
}
private async getAllProxyDaos(): Promise<ProxyDao[]> {
return this.proxies.map(fillInDefaults)
}
}
function fillInDefaults(proxy: Partial<ProxyDao>): ProxyDao {
if (proxy.title === undefined) {
proxy.title = ''
}
if (typeof proxy.doNotProxyLocal === 'undefined') {
proxy.doNotProxyLocal = true
}
if (typeof proxy.proxyDNS === 'undefined') {
if (proxy.type === 'socks' || proxy.type === 'socks4') {
proxy.proxyDNS = true
}
}
return proxy as ProxyDao
}