tor push
This commit is contained in:
+114
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+44
@@ -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)
|
||||
+36
@@ -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
|
||||
}
|
||||
+170
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user