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
@@ -10,7 +10,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.6.1")
classpath("com.android.tools.build:gradle:8.7.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
@@ -0,0 +1 @@
../../../../../javascript/container_proxy/dist/
@@ -5,6 +5,7 @@
package eu.lensai.flutter_mozilla_components
import android.content.Context
import eu.lensai.flutter_mozilla_components.feature.ContainerProxyFeature
import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature
import eu.lensai.flutter_mozilla_components.feature.PrefManagerFeature
import mozilla.components.browser.engine.gecko.GeckoEngine
@@ -50,6 +51,7 @@ object EngineProvider {
WebCompatFeature.install(it)
CookieManagerFeature.install(it)
PrefManagerFeature.install(it)
ContainerProxyFeature.install(it)
}
}
@@ -6,6 +6,7 @@ import androidx.fragment.app.FragmentActivity
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
import eu.lensai.flutter_mozilla_components.api.GeckoAddonsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoContainerProxyApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoDeleteBrowsingDataControllerImpl
import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
@@ -20,6 +21,7 @@ import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelega
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController
import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
@@ -46,6 +48,12 @@ import mozilla.components.support.base.log.sink.AndroidLogSink
/** FlutterMozillaComponentsPlugin */
class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
companion object {
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
private var isGeckoInitialized = false
}
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
@@ -58,11 +66,18 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
private var isPlatformViewRegistered = false
private var pendingFragmentShow = false
init {
Log.addSink(AndroidLogSink())
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
synchronized(this) {
if(!isGeckoInitialized) {
Log.addSink(AndroidLogSink())
setupGeckoEngine(flutterPluginBinding)
isGeckoInitialized = true
}
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
private fun setupGeckoEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
_flutterPluginBinding = flutterPluginBinding
_flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
@@ -99,6 +114,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl())
GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl())
GeckoPrefApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPrefApiImpl())
GeckoContainerProxyApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoContainerProxyApiImpl())
GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl())
GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl(
selectionActionDelegate
@@ -169,8 +185,4 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
isPlatformViewRegistered = false
pendingFragmentShow = false
}
companion object {
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
}
}
@@ -0,0 +1,18 @@
package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.feature.ContainerProxyFeature
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
override fun setProxyPort(port: Long) {
ContainerProxyFeature.scheduleRequest("setProxyPort", port.toInt())
}
override fun addContainerProxy(contextId: String) {
ContainerProxyFeature.scheduleRequest("addContainerProxy", contextId)
}
override fun removeContainerProxy(contextId: String) {
ContainerProxyFeature.scheduleRequest("removeContainerProxy", contextId)
}
}
@@ -0,0 +1,67 @@
package eu.lensai.flutter_mozilla_components.feature
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.concept.engine.webextension.WebExtensionRuntime
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.webextensions.WebExtensionController
import org.json.JSONObject
object ContainerProxyFeature {
private val logger = Logger("container_proxy")
private const val CONTAINER_PROXY_REPORTER_EXTENSION_ID = "container-proxy@lensai.eu"
private const val CONTAINER_PROXY_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/container_proxy/"
private const val CONTAINER_PROXY_REPORTER_MESSAGING_ID = "containerProxy"
@VisibleForTesting
// This is an internal var to make it mutable for unit testing purposes only
internal var extensionController = WebExtensionController(
CONTAINER_PROXY_REPORTER_EXTENSION_ID,
CONTAINER_PROXY_REPORTER_EXTENSION_URL,
CONTAINER_PROXY_REPORTER_MESSAGING_ID,
)
fun scheduleRequest(command: String, args: Any) {
val message = JSONObject()
message.put("action", command);
message.put("args", args)
runBlocking {
withContext(Dispatchers.Default) {
extensionController.sendBackgroundMessage(message)
}
}
}
private class ContainerProxyBackgroundMessageHandler() : MessageHandler {
}
/**
* Installs the web extension in the runtime through the WebExtensionRuntime install method
*
* @param runtime a WebExtensionRuntime.
* @param productName a custom product name used to automatically label reports. Defaults to
* "android-components".
*/
fun install(runtime: WebExtensionRuntime) {
extensionController.registerBackgroundMessageHandler(
ContainerProxyBackgroundMessageHandler()
)
extensionController.install(
runtime,
onSuccess = {
logger.debug("Installed ContainerProxy webextension: ${it.id}")
},
onError = { throwable ->
logger.error("Failed to install ContainerProxy webextension: ", throwable)
},
)
}
}
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
// Autogenerated from Pigeon (v24.1.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -2567,6 +2567,78 @@ interface GeckoPrefApi {
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoContainerProxyApi {
fun setProxyPort(port: Long)
fun addContainerProxy(contextId: String)
fun removeContainerProxy(contextId: String)
companion object {
/** The codec used by GeckoContainerProxyApi. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
/** Sets up an instance of `GeckoContainerProxyApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoContainerProxyApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setProxyPort$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val portArg = args[0] as Long
val wrapped: List<Any?> = try {
api.setProxyPort(portArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.addContainerProxy$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val contextIdArg = args[0] as String
val wrapped: List<Any?> = try {
api.addContainerProxy(contextIdArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.removeContainerProxy$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val contextIdArg = args[0] as String
val wrapped: List<Any?> = try {
api.removeContainerProxy(contextIdArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoCookieApi {
fun getCookie(firstPartyDomain: String?, name: String, partitionKey: CookiePartitionKey?, storeId: String?, url: String, callback: (Result<Cookie>) -> Unit)
fun getAllCookies(domain: String?, firstPartyDomain: String?, name: String?, partitionKey: CookiePartitionKey?, storeId: String?, url: String, callback: (Result<List<Cookie>>) -> Unit)
@@ -11,12 +11,12 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
jvmTarget = JavaVersion.VERSION_17
}
defaultConfig {
@@ -18,7 +18,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.3.1" apply false
id "com.android.application" version "8.7.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}
@@ -0,0 +1,8 @@
.idea
*.iml
dist
web-ext-artifacts
source-artifacts
node_modules
tmpProfile
*.module.scss.d.ts
@@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2019, Bekh-Ivanov Aleksey
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,2 @@
# Notes to Reviewer
@@ -0,0 +1,20 @@
[![Join the chat at https://gitter.im/firefox-container-proxy/community](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/firefox-container-proxy/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![CircleCI](https://circleci.com/gh/bekh6ex/firefox-container-proxy/tree/master.svg?style=svg)](https://circleci.com/gh/bekh6ex/firefox-container-proxy/tree/master)
[![Translation status](https://hosted.weblate.org/widgets/firefox-container-proxy/-/firefox-container-proxy/svg-badge.svg)](https://hosted.weblate.org/engage/firefox-container-proxy/)
[Extension page](https://addons.mozilla.org/en-US/firefox/addon/container-proxy/)
## Permissions
* **cookies**: needed to identify to which container request belongs
* **webRequest** and **webRequestBlocking**: to supply credentials for proxy authorization (but not for normal web authorization)
## Good to know
There is a known issue with DNS leak happening in non-default containers when uBlock is installed. The issue is not resolvable by this extension, but can be resolved adjusting in uBlock settings. See [comment](https://github.com/bekh6ex/firefox-container-proxy/issues/23#issuecomment-773249909)
## Translation
[![Translation status](https://hosted.weblate.org/widgets/firefox-container-proxy/-/firefox-container-proxy/multi-auto.svg)](https://hosted.weblate.org/engage/firefox-container-proxy/)
[Translate to your language](https://hosted.weblate.org/engage/firefox-container-proxy/)
@@ -0,0 +1,2 @@
// We need to tell TypeScript that when we write "import styles from './styles.scss' we mean to load a module (to look for a './styles.scss.d.ts').
declare module '*.module.scss';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,112 @@
{
"name": "container-proxy",
"version": "0.1.0",
"description": "[Firefox extension] Assign a proxy to a Firefox container [Extension page](https://addons.mozilla.org/en-US/firefox/addon/container-proxy/)",
"scripts": {
"test": "pnpm run test:unit",
"bundle": "web-ext -s dist build --overwrite-dest",
"bundle:src": "V=$(cat src/manifest.json | jq -r .version) && zip -r -X source-artifacts/container-proxy-v${V}.zip src babel.config.js LICENSE package.json package-lock.json README.md tsconfig.json webpack.config.js",
"test:func": "pnpm run build && mocha test/functional/",
"test:unit": "ts-mocha --require ts-node/register test/unit/**/*.test.ts",
"codeStyle:check": "standardx && ts-standard",
"codeStyle:fix": "standardx --fix && ts-standard --fix",
"lint": "addons-linter dist",
"debug": "web-ext run -s dist --verbose --browser-console --keep-profile-changes -p ./tmpProfile --start-url about:debugging --start-url about:addons --pref=intl.locale.requested=en #--pref=intl.locale.matchOS=false",
"ci:test-unit": "pnpm run test:unit",
"ci:test-func": "HEADLESS=true pnpm run test:func",
"ci:style-check": "pnpm run codeStyle:check",
"ci:lint": "pnpm run build && pnpm run lint",
"build": "webpack",
"build:watch": "webpack --watch",
"clean": "rm -rf ./dist"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bekh6ex/firefox-container-proxy.git"
},
"author": "Aleksei Bekh-Ivanov",
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/bekh6ex/firefox-container-proxy/issues"
},
"homepage": "https://addons.mozilla.org/en-US/firefox/addon/container-proxy/",
"devDependencies": {
"@types/chai": "^5.0.1",
"@types/firefox-webext-browser": "^120.0.4",
"@types/mocha": "^10.0.10",
"addons-linter": "^7.8.0",
"chai": "^5.1.2",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"css-modules-typescript-loader": "^4.0.1",
"esm": "^3.2.25",
"mocha": "^10.8.2",
"sass-loader": "^16.0.4",
"sinon": "^8.1.1",
"standardx": "^7.0.0",
"style-loader": "^4.0.0",
"ts-loader": "^9.2.6",
"ts-mocha": "^10.0.0",
"ts-standard": "^12.0.2",
"tsconfig-paths-webpack-plugin": "^4.2.0",
"typescript": "^5.7.3",
"web-ext": "^8.4.0",
"webextensions-api-fake": "^1.3.0",
"webextensions-geckodriver": "^0.7.0",
"webpack": "^5.59.1",
"webpack-cli": "^6.0.1"
},
"standardx": {
"cache": false,
"parser": "@typescript-eslint/parser",
"env": {
"browser": true,
"mocha": true
},
"globals": [
"browser",
"expect",
"store"
],
"ignore": [
"dist",
"/src/lib/*",
"src/**/*.ts",
"test/**/*.ts"
]
},
"ts-standard": {
"ignore": [
"dist",
"src/**/*.js",
"test/**/*.js",
"src/options/import/FoxyProxyConverter.ts",
"test/unit/options/import/FoxyProxyConverter.test.ts"
],
"cache": false,
"env": {
"browser": true,
"mocha": true
},
"globals": [
"browser",
"expect",
"store"
]
},
"eslintConfig": {
"rules": {
"no-unused-expressions": "error"
},
"overrides": [
{
"files": [
"*.test.js"
],
"rules": {
"no-unused-expressions": "off"
}
}
]
}
}
@@ -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
}
@@ -0,0 +1,166 @@
const PageObject = require('./page-objects/PageObject.js')
const OptionsPageObject = require('./page-objects/OptionsPageObject.js')
const path = require('path')
const assert = require('chai')
const expect = assert.expect
const webExtensionsGeckoDriver = require('webextensions-geckodriver')
const { webdriver, firefox } = webExtensionsGeckoDriver
const { until, By } = webdriver
const manifestPath = path.resolve(path.join(__dirname, '../../dist/manifest.json'))
describe('Container Proxy extension', function () {
let geckodriver
this.timeout(30000)
before(async () => {
const fxOptions = new firefox.Options()
if (process.env.HEADLESS) {
fxOptions.headless()
.windowSize({ height: 1080, width: 1920 })
}
const webExtension = await webExtensionsGeckoDriver(manifestPath, { fxOptions })
geckodriver = webExtension.geckodriver
})
it('should add a proxy', async () => {
const helper = new Helper(geckodriver)
const options = await helper.openOptionsPage()
let proxyList = await options.openProxyList()
const proxyForm = await proxyList.openAddProxyForm()
await proxyForm.selectProtocol('socks')
await proxyForm.typeInServer('localhost')
await proxyForm.typeInPort(1080)
await proxyForm.typeInUsername('user')
await proxyForm.typeInPassword('password')
await proxyForm.testSettings()
proxyList = await proxyForm.saveSettings()
const proxyLabel = 'socks://localhost:1080'
await geckodriver.wait(async () => {
const row = await geckodriver.wait(until.elementLocated(
By.css('.proxy-list-item:first-of-type')
), 2000)
const label = row.findElement(By.css('.proxy-name'))
const text = await label.getText()
return text === proxyLabel
}, 1000, 'Should show proxy in the list')
const assign = await options.openAssignProxy()
const defaultContainerSelect = await assign.defaultContainerSelect()
await defaultContainerSelect.selectByLabel(proxyLabel)
})
it.skip('should contain IP address text', async () => {
await geckodriver.setContext(firefox.Context.CONTENT)
await geckodriver.get('https://api.duckduckgo.com/?q=ip&no_html=1&format=json&t=firefox-container-proxy-extension')
const text = await geckodriver.getPageSource()
expect(text).to.include('Your IP address is')
})
it('should successfully use SOCKS5 proxy for default container', async () => {
const helper = new Helper(geckodriver)
const optionsPage = await helper.openOptionsPage()
const proxyList = await optionsPage.openProxyList()
const addProxyForm = await proxyList.openAddProxyForm()
const title = 'Valid SOCKS5 proxy'
await addProxyForm.addProxy({
title: title,
type: 'socks',
server: 'localhost',
port: 1080,
username: 'user',
password: 'password'
})
// TODO: Check if username and password are actually verified by "dante"
const assignProxy = await optionsPage.openAssignProxy()
await assignProxy.selectForDefaultContainer(title)
await helper.assertCanGetTheIpAddress()
})
it('should fail with incorrect SOCKS5 proxy settings', async () => {
const helper = new Helper(geckodriver)
const optionsPage = await helper.openOptionsPage()
const proxyList = await optionsPage.openProxyList()
const addProxyForm = await proxyList.openAddProxyForm()
const title = 'Incorrectly setup SOCKS5 proxy'
await addProxyForm.addProxy({
title: title,
type: 'socks',
server: 'localhost',
port: 999,
username: 'user',
password: 'password'
})
const assignProxy = await optionsPage.openAssignProxy()
await assignProxy.selectForDefaultContainer(title)
await helper.assertProxyFailure()
})
after(function () {
geckodriver.quit()
})
})
class Helper extends PageObject {
toolbarButton = By.id('container-proxy_bekh-ivanov_me-browser-action')
/**
* @return {Promise<OptionsPageObject>}
*/
async openOptionsPage() {
await this._driver.setContext(firefox.Context.CHROME)
await this.click(this.toolbarButton)
await this._driver.setContext(firefox.Context.CONTENT)
let windowHandle
await this._driver.wait(async () => {
const windowHandles = await this._driver.getAllWindowHandles()
windowHandle = windowHandles[windowHandles.length - 1]
try {
await this._driver.switchTo().window(windowHandle)
} catch (e) {
return false
}
const title = await this._driver.getTitle()
return title === 'Container Proxy extension settings'
}, 2000, 'Should have opened Container Proxy extension settings')
return this.createPageObject(OptionsPageObject)
}
async assertCanGetTheIpAddress() {
await this._driver.setContext(firefox.Context.CONTENT)
await this._driver.get('https://duckduckgo.com/?q=ip&ia=answer&atb=v150-1')
const text = await this._driver.getPageSource()
expect(text).to.include('Your IP address is')
}
async assertProxyFailure() {
await this._driver.setContext(firefox.Context.CONTENT)
try {
await this._driver.get('https://api.duckduckgo.com/?q=ip&no_html=1&format=json&t=firefox-container-proxy-extension')
} catch (e) {
}
const text = await this._driver.getPageSource()
expect(text).to.include('Firefox is configured to use a proxy server that is refusing connections.')
}
}
@@ -0,0 +1,10 @@
version: "3"
services:
socks:
build: ./socks
ports:
- "1080:1080"
http:
build: ./http
ports:
- "3128:3128"
@@ -0,0 +1,11 @@
FROM datadog/squid
RUN apt update && apt install -y apache2-utils
RUN htpasswd -b -c /etc/squid/passwords userhttp passwordhttp
ADD squid.conf /etc/squid/squid.conf
RUN chmod o+rw /var/run
USER proxy
@@ -0,0 +1,17 @@
auth_param basic program /usr/lib/squid3/basic_ncsa_auth /etc/squid/passwords
#auth_param basic children 5 startup = 5 idle = 1
auth_param basic realm proxy
auth_param basic credentialsttl 1 second
connect_timeout 1 second
acl authenticated proxy_auth REQUIRED
http_access allow authenticated
logformat splunk_recommended_squid %ts.%03tu logformat=splunk_recommended_squid duration=%tr src_ip=%>a src_port=%>p dest_ip=%<a dest_port=%<p user_ident="%[ui" user="%[un" local_time=[%tl] http_method=%rm request_method_from_client=%<rm request_method_to_server=%>rm url="%ru" http_referrer="%{Referer}>h" http_user_agent="%{User-Agent}>h" status=%>Hs vendor_action=%Ss dest_status=%Sh total_time_milliseconds=%<tt http_content_type="%mt" bytes=%st bytes_in=%>st bytes_out=%<st sni=ssl::>sni
logfile_rotate 0
cache_log stdio:/proc/self/fd/1 splunk_recommended_squid
access_log stdio:/proc/self/fd/1 splunk_recommended_squid
cache_store_log stdio:/proc/self/fd/1 splunk_recommended_squid
# Choose the port you want. Below we set it to default 3128.
http_port 3128
@@ -0,0 +1,4 @@
user:$apr1$L24dGKxY$xqj/npbeiBkMZEH7s9Iae/
@@ -0,0 +1,3 @@
FROM vimagick/dante
RUN useradd user && echo user:password | chpasswd
@@ -0,0 +1,117 @@
import BackgroundMain, { doNotProxy } from '../../src/background/BackgroundMain'
import { Store } from '../../src/store/Store'
import webExtensionsApiFake from 'webextensions-api-fake'
import { expect } from 'chai'
import { ProxySettings } from '../../src/domain/ProxySettings'
const tryFromDao = ProxySettings.tryFromDao
/* eslint-disable @typescript-eslint/no-unused-expressions */
const store = new Store()
describe('BackgroundMain', function () {
beforeEach(() => {
// @ts-expect-error
global.browser = webExtensionsApiFake()
})
afterEach(() => {
// @ts-expect-error
delete global.browser
})
const backgroundMain = new BackgroundMain({ store: store })
// TODO: Add test for proxyDNS property
describe('onRequest', function () {
it('should return empty array if no proxy is set up', async () => {
const result = await backgroundMain.onRequest({ cookieStoreId: 'firefox-default', url: 'https://google.com', tabId: 0 })
expect(result).to.be.deep.equal(doNotProxy)
})
it('should return proxy if proxy is set up', async () => {
await givenSomeProxyIsSetUpForContainer({ containerId: 'firefox-default', host: undefined, doNotProxyLocal: undefined })
const result = await backgroundMain.onRequest({ cookieStoreId: 'firefox-default', url: 'https://google.com', tabId: 0 })
expect(result).to.be.an('array')
expect(result).to.be.not.empty
})
it('should remove doNotProxyLocal flag from proxy settings if proxy is set up', async () => {
await givenSomeProxyIsSetUpForContainer({ containerId: 'firefox-default', host: undefined, doNotProxyLocal: undefined })
const result = await backgroundMain.onRequest({ cookieStoreId: 'firefox-default', url: 'https://google.com', tabId: 0 })
expect((result[0] as any).doNotProxyLocal).to.be.undefined
})
it('should return proxy for the container if url is invalid', async () => {
// To be more on a safe side
await givenSomeProxyIsSetUpForContainer({ containerId: 'firefox-default', host: undefined, doNotProxyLocal: undefined })
const result = await backgroundMain.onRequest({ cookieStoreId: 'firefox-default', url: 'np-protocol-url.com', tabId: 0 })
expect(result).to.be.an('array')
expect(result).to.be.not.empty
})
// Connections to localhost, 127.0.0.1, and ::1 are never proxied. (From FF settings)
const localAddresses = [
'http://localhost/index.html',
'https://localhost/index.html',
'http://127.0.0.1/',
'https://127.0.0.1/',
'http://[::1]/test',
'https://[::1]/test',
'http://[0:0:0:0:0:0:0:1]/test',
'https://[0:0:0:0:0:0:0:1]/test',
'https://user:password@127.0.0.1:123/',
'http://[::1]:123/test'
]
describe('proxying of local addresses is disabled', () => {
localAddresses.forEach(url => {
it(`should return empty array if the address is local: ${url}`, async () => {
await givenSomeProxyIsSetUpForContainer({ containerId: 'container1', host: undefined, doNotProxyLocal: true })
const result = await backgroundMain.onRequest({ cookieStoreId: 'container1', url, tabId: 0 })
expect(result).to.be.deep.equal(doNotProxy)
})
})
})
describe('proxying of local addresses is enabled', () => {
localAddresses.forEach(url => {
it(`should return array with proxy: ${url}`, async () => {
const host = 'proxyX.example.com'
await givenSomeProxyIsSetUpForContainer({ host, containerId: 'container1', doNotProxyLocal: false })
const result = await backgroundMain.onRequest({ cookieStoreId: 'container1', url, tabId: 0 })
expect(result[0].host).to.be.equal(host)
})
})
})
})
})
async function givenSomeProxyIsSetUpForContainer({ host, containerId, doNotProxyLocal }: any): Promise<void> {
const proxyId = 'proxy1'
const proxy: any = {
id: proxyId,
type: 'socks',
host: (host as string) ?? 'example.com',
port: 1080
}
if (typeof doNotProxyLocal !== 'undefined') {
proxy.doNotProxyLocal = doNotProxyLocal
}
await store.putProxy(tryFromDao(proxy) as ProxySettings)
await store.setContainerProxyRelation(containerId, proxyId)
}
@@ -0,0 +1,167 @@
import { ProxyDao, Store } from '../../src/store/Store'
import webExtensionsApiFake from 'webextensions-api-fake'
import { expect } from 'chai'
import { ProxySettings } from '../../src/domain/ProxySettings'
const tryFromDao = ProxySettings.tryFromDao
/* eslint-disable @typescript-eslint/no-unused-expressions */
describe('Store', () => {
const store = new Store()
beforeEach(() => {
(global as any).browser = webExtensionsApiFake()
})
afterEach(() => {
delete (global as any).browser
})
function someProxyWith(id: string, props: Partial<ProxyDao> = {}): ProxySettings {
const dao = {
id: id,
title: 'some title',
type: 'socks',
host: 'localhost',
port: 1080,
username: 'user',
password: 'password',
proxyDNS: true,
failoverTimeout: 5,
doNotProxyLocal: true
}
return tryFromDao({ ...dao, ...props }) as ProxySettings
}
describe('getAllProxies', function () {
it('should return empty array if nothing is saved', async () => {
const proxies = await store.getAllProxies()
expect(proxies).to.be.an('array')
expect(proxies).to.be.empty
})
})
it('should put and get the proxy back', async () => {
const id = 'someId'
const proxy = someProxyWith(id)
await store.putProxy(proxy)
const gotProxy = await store.getProxyById(id)
expect(gotProxy).to.be.deep.equal(proxy)
})
it('when get, returns doNotProxyLocal `true` if was not set when put', async () => {
const id = 'someId'
const proxy = someProxyWith(id)
delete (proxy as any).doNotProxyLocal
await store.putProxy(proxy)
const gotProxy = await store.getProxyById(id)
expect(gotProxy?.doNotProxyLocal).to.be.equal(true)
})
it('stores doNotProxyLocal value', async () => {
const id = 'someId'
const proxy = someProxyWith(id, { doNotProxyLocal: false })
await store.putProxy(proxy)
const gotProxy = await store.getProxyById(id)
expect(gotProxy?.doNotProxyLocal).to.be.equal(false)
})
it('should be able to delete proxy', async () => {
const id = 'someId'
await store.putProxy(someProxyWith(id))
await store.deleteProxyById(id)
const result = await store.getProxyById(id)
expect(result).to.be.null
})
// describe('getProxiesForContainer', () => {
// it('should find proxy by container id if one present', async () => {
// const cookieStoreId = 'container-1'
// const proxyId = 'proxy-1'
// // TODO Store should probably take care of this
// const relations = {
// [cookieStoreId]: [proxyId]
// }
// const givenProxy = someProxyWith(proxyId)
// await store.putProxy(givenProxy)
// await browser.storage.local.set({ relations: relations })
// const [gotProxy] = await store.getProxiesForContainer(cookieStoreId)
// expect(gotProxy).to.be.deep.equal(givenProxy)
// })
// it('doNotProxyLocal should be `true` if not set', async () => {
// const cookieStoreId = 'container-1'
// const proxyId = 'proxy-1'
// // TODO Store should probably take care of this
// const relations = {
// [cookieStoreId]: [proxyId]
// }
// const givenProxy = someProxyWith(proxyId)
// delete (givenProxy as any).doNotProxyLocal
// await store.putProxy(givenProxy)
// await browser.storage.local.set({ relations: relations })
// const [gotProxy] = await store.getProxiesForContainer(cookieStoreId)
// expect(gotProxy.doNotProxyLocal).to.be.equal(true)
// })
// it('should return empty array if proxy not set for the container', async () => {
// const cookieStoreId = 'container-1'
// const result = await store.getProxiesForContainer(cookieStoreId)
// expect(result).to.be.empty
// })
// it('should return empty array if proxy is set for the container but does not exist', async () => {
// const cookieStoreId = 'container-1'
// const proxyId = 'something-absent'
// const relations = {
// [cookieStoreId]: [proxyId]
// }
// await browser.storage.local.set({ relations: relations })
// const result = await store.getProxiesForContainer(cookieStoreId)
// expect(result).to.be.empty
// })
// })
describe('setContainerProxyRelation', function () {
it('should set the relation', async () => {
await store.setContainerProxyRelation('container1', 'proxy1')
const relations = await store.getRelations()
expect(relations.container1).to.be.deep.equal(['proxy1'])
})
it('should not remove existing relations', async () => {
await store.setContainerProxyRelation('container1', 'proxy1')
await store.setContainerProxyRelation('container2', 'proxy2')
const relations = await store.getRelations()
expect(relations.container1).to.be.deep.equal(['proxy1'])
expect(relations.container2).to.be.deep.equal(['proxy2'])
})
})
})
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "./dist/",
"module": "ES2020",
"moduleResolution": "node",
"noEmitOnError": true,
"target": "es2019",
"sourceMap": true,
"allowJs": true,
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"plugins": [{ "name": "typescript-plugin-css-modules" }]
},
"include": [
"src/**/*",
"test/unit/**/*",
"declarations.d.ts"
]
}
@@ -0,0 +1,60 @@
/* eslint-disable */
const path = require('path')
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
module.exports = {
entry: {
background: './src/background/index.ts',
},
devtool: 'source-map',
mode: 'development',
performance: {
hints: false,
},
node: false,
optimization: {
minimize: false,
moduleIds: 'named',
chunkIds: 'named',
concatenateModules: false,
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.module.scss'],
plugins: [new TsconfigPathsPlugin()],
},
output: {
filename: '[name]/index.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.module\.scss$/i,
use: ['style-loader', 'css-modules-typescript-loader', {
loader: 'css-loader',
options: { modules: { localIdentName: '[name]__[local]--[hash:base64:5]' } }
}, 'sass-loader'],
},
]
},
plugins: [
new CopyPlugin({
patterns: [
'README.md',
'LICENSE',
'src/manifest.json',
],
}),
],
devServer: {
hot: false,
inline: false,
writeToDisk: true,
},
}
@@ -3,6 +3,7 @@ export 'src/data/models/load_url_flags.dart';
export 'src/data/models/source.dart';
export 'src/domain/entities/default_selection_actions.dart';
export 'src/domain/services/gecko_addon.dart';
export 'src/domain/services/gecko_container_proxy.dart';
export 'src/domain/services/gecko_cookie.dart';
export 'src/domain/services/gecko_delete_browser_data.dart';
export 'src/domain/services/gecko_engine_settings.dart';
@@ -0,0 +1,17 @@
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
final _apiInstance = GeckoContainerProxyApi();
class GeckoContainerProxyService {
Future<void> setProxyPort(int port) {
return _apiInstance.setProxyPort(port);
}
Future<void> addContainerProxy(String contextId) {
return _apiInstance.addContainerProxy(contextId);
}
Future<void> removeContainerProxy(String contextId) {
return _apiInstance.removeContainerProxy(contextId);
}
}
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
// Autogenerated from Pigeon (v24.1.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
@@ -2668,6 +2668,86 @@ class GeckoPrefApi {
}
}
class GeckoContainerProxyApi {
/// Constructor for [GeckoContainerProxyApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
GeckoContainerProxyApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
Future<void> setProxyPort(int port) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setProxyPort$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_channel.send(<Object?>[port]) as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
Future<void> addContainerProxy(String contextId) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.addContainerProxy$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_channel.send(<Object?>[contextId]) as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
Future<void> removeContainerProxy(String contextId) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.removeContainerProxy$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_channel.send(<Object?>[contextId]) as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
}
class GeckoCookieApi {
/// Constructor for [GeckoCookieApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
@@ -889,6 +889,13 @@ abstract class GeckoPrefApi {
void resetPrefs(List<String>? preferenceNames);
}
@HostApi()
abstract class GeckoContainerProxyApi {
void setProxyPort(int port);
void addContainerProxy(String contextId);
void removeContainerProxy(String contextId);
}
@HostApi()
abstract class GeckoCookieApi {
@async
@@ -16,8 +16,8 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
lint: ^2.3.0
pigeon: ^24.1.0
lint: ^2.4.0
pigeon: ^24.1.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
+5 -5
View File
@@ -2,14 +2,14 @@ group = "me.movenext.sqlite3_vec"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "1.8.22"
ext.kotlin_version = "1.9.22"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.1.0")
classpath("com.android.tools.build:gradle:8.7.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
@@ -30,12 +30,12 @@ android {
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
jvmTarget = JavaVersion.VERSION_17
}
sourceSets {
@@ -11,12 +11,12 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
jvmTarget = JavaVersion.VERSION_17
}
defaultConfig {
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id "com.android.application" version "8.7.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.22" apply false
}
include ":app"
+2 -2
View File
@@ -28,8 +28,8 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
sqlite3: ^2.7.2
sqlite3_flutter_libs: ^0.5.29+1
sqlite3: ^2.7.3
sqlite3_flutter_libs: ^0.5.30
dev_dependencies:
integration_test:
+2 -2
View File
@@ -10,12 +10,12 @@ environment:
dependencies:
flutter:
sdk: flutter
sqlite3: ^2.7.2
sqlite3: ^2.7.3
dev_dependencies:
flutter_test:
sdk: flutter
lint: ^2.3.0
lint: ^2.4.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec