diff --git a/CHANGELOG.md b/CHANGELOG.md index 220917c..89a3b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- support for using proxies. Proxy authentication is not yet supported. + ## 0.1.5 - 2025-04-14 ### Fixed diff --git a/README.md b/README.md index 0ee8b4e..c70df74 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,42 @@ If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable experience, it’s recommended to ensure the workspace is running prior to initiating the connection. +## Configuring and Testing workspace polling with HTTP & SOCKS5 Proxy + +This section explains how to set up a local proxy (without authentication which is not yet supported) and verify that +the plugin’s REST client works correctly when routed through it. + +We’ll use [mitmproxy](https://mitmproxy.org/) for this — it can act as both an HTTP and SOCKS5 proxy with SSL +interception. + +### Install mitmproxy + +1. Follow the [mitmproxy Install Guide](https://docs.mitmproxy.org/stable/overview-installation/) steps for your OS. +2. Start the proxy: + +```bash + +mitmweb --ssl-insecure --set stream_large_bodies="10m" + ``` + +### Configure Mitmproxy + +mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other: + +1. Open http://127.0.0.1:8081 in browser; +2. Navigate to `Options -> Edit Options` +3. Update the `Mode` field to `regular` in order to activate HTTP/HTTPS or to `socks5` +4. Proxy authentication can be enabled by updating the `proxyauth` to `username:password` + +### Configure Proxy in Toolbox + +1. Start Toolbox +2. From Toolbox hexagonal menu icon go to `Settings -> Proxy` +3. There are two options, to use system proxy settings or to manually configure the proxy details. +4. If we go manually, add `127.0.0.1` to the host and port `8080` for HTTP/HTTPS or `1080` for SOCKS5. +5. Before authenticating to the Coder deployment we need to tell the plugin where can we find mitmproxy + certificates. In Coder's Settings page, set the `TLS CA path` to `~/.mitmproxy/mitmproxy-ca-cert.pem` + ## Releasing 1. Check that the changelog lists all the important changes. diff --git a/gradle.properties b/gradle.properties index 858b19b..a4ec268 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.1.5 +version=0.2.0 group=com.coder.toolbox name=coder-toolbox diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt index b3f6f60..9e5eace 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt @@ -7,6 +7,7 @@ import com.coder.toolbox.util.toURL import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper +import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi @@ -21,7 +22,8 @@ data class CoderToolboxContext( val logger: Logger, val i18n: LocalizableStringFactory, val settingsStore: CoderSettingsStore, - val secrets: CoderSecretsStore + val secrets: CoderSecretsStore, + val proxySettings: ToolboxProxySettings, ) { /** * Try to find a URL. diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt index a310ee0..5ab89a2 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt @@ -7,10 +7,12 @@ import com.jetbrains.toolbox.api.core.PluginSecretStore import com.jetbrains.toolbox.api.core.PluginSettingsStore import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.core.diagnostics.Logger +import com.jetbrains.toolbox.api.core.getService import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension import com.jetbrains.toolbox.api.remoteDev.RemoteProvider import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper +import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi @@ -25,15 +27,16 @@ class CoderToolboxExtension : RemoteDevExtension { val logger = serviceLocator.getService(Logger::class.java) return CoderRemoteProvider( CoderToolboxContext( - serviceLocator.getService(ToolboxUi::class.java), - serviceLocator.getService(EnvironmentUiPageManager::class.java), - serviceLocator.getService(EnvironmentStateColorPalette::class.java), - serviceLocator.getService(ClientHelper::class.java), - serviceLocator.getService(CoroutineScope::class.java), - serviceLocator.getService(Logger::class.java), - serviceLocator.getService(LocalizableStringFactory::class.java), - CoderSettingsStore(serviceLocator.getService(PluginSettingsStore::class.java), Environment(), logger), - CoderSecretsStore(serviceLocator.getService(PluginSecretStore::class.java)), + serviceLocator.getService(), + serviceLocator.getService(), + serviceLocator.getService(), + serviceLocator.getService(), + serviceLocator.getService(), + serviceLocator.getService(), + serviceLocator.getService(), + CoderSettingsStore(serviceLocator.getService(), Environment(), logger), + CoderSecretsStore(serviceLocator.getService()), + serviceLocator.getService() ) ) } diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index feb2de4..b4eadaf 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -23,37 +23,24 @@ import com.coder.toolbox.util.getArch import com.coder.toolbox.util.getHeaders import com.coder.toolbox.util.getOS import com.squareup.moshi.Moshi -import okhttp3.Credentials import okhttp3.OkHttpClient import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import java.net.HttpURLConnection -import java.net.ProxySelector import java.net.URL import java.util.UUID import javax.net.ssl.X509TrustManager -/** - * Holds proxy information. - */ -data class ProxyValues( - val username: String?, - val password: String?, - val useAuth: Boolean, - val selector: ProxySelector, -) - /** * An HTTP client that can make requests to the Coder API. * * The token can be omitted if some other authentication mechanism is in use. */ open class CoderRestClient( - context: CoderToolboxContext, + private val context: CoderToolboxContext, val url: URL, val token: String?, - private val proxyValues: ProxyValues? = null, private val pluginVersion: String = "development", ) { private val settings = context.settingsStore.readOnly() @@ -81,22 +68,27 @@ open class CoderRestClient( val trustManagers = coderTrustManagers(settings.tls.caPath) var builder = OkHttpClient.Builder() - if (proxyValues != null) { - builder = - builder - .proxySelector(proxyValues.selector) - .proxyAuthenticator { _, response -> - if (proxyValues.useAuth && proxyValues.username != null && proxyValues.password != null) { - val credentials = Credentials.basic(proxyValues.username, proxyValues.password) - response.request.newBuilder() - .header("Proxy-Authorization", credentials) - .build() - } else { - null - } - } + if (context.proxySettings.getProxy() != null) { + context.logger.debug("proxy: ${context.proxySettings.getProxy()}") + builder.proxy(context.proxySettings.getProxy()) + } else if (context.proxySettings.getProxySelector() != null) { + context.logger.debug("proxy selector: ${context.proxySettings.getProxySelector()}") + builder.proxySelector(context.proxySettings.getProxySelector()!!) } + //TODO - add support for proxy auth. when Toolbox exposes them +// builder.proxyAuthenticator { _, response -> +// if (proxyValues.useAuth && proxyValues.username != null && proxyValues.password != null) { +// val credentials = Credentials.basic(proxyValues.username, proxyValues.password) +// response.request.newBuilder() +// .header("Proxy-Authorization", credentials) +// .build() +// } else { +// null +// } +// } +// } + if (token != null) { builder = builder.addInterceptor { it.proceed( diff --git a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt index de79422..cbc26cd 100644 --- a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt @@ -238,13 +238,10 @@ open class CoderProtocolHandler( if (settings.requireTokenAuth && token == null) { // User aborted. throw MissingArgumentException("Token is required") } - // The http client Toolbox gives us is already set up with the - // proxy config, so we do net need to explicitly add it. val client = CoderRestClient( context, deploymentURL.toURL(), token, - proxyValues = null, // TODO - not sure the above comment applies as we are creating our own http client PluginManager.pluginInfo.version ) client.authenticate() diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt index 30f757b..3abbae8 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt @@ -74,13 +74,10 @@ class ConnectStep( signInJob = context.cs.launch { try { statusField.textState.update { (context.i18n.ptrl("Authenticating to ${url.host}...")) } - // The http client Toolbox gives us is already set up with the - // proxy config, so we do net need to explicitly add it. val client = CoderRestClient( context, url, token, - proxyValues = null, PluginManager.pluginInfo.version, ) // allows interleaving with the back/cancel action diff --git a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt index ca9040e..9a32ded 100644 --- a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt @@ -30,6 +30,7 @@ import com.coder.toolbox.util.toURL import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper +import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi @@ -68,7 +69,8 @@ internal class CoderCLIManagerTest { Environment(), mockk(relaxed = true) ), - mockk() + mockk(), + mockk() ) /** diff --git a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt index 66b2465..0cee720 100644 --- a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt +++ b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt @@ -23,6 +23,7 @@ import com.coder.toolbox.util.sslContextFromPEMs import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper +import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi @@ -51,6 +52,7 @@ import java.nio.file.Path import java.util.UUID import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals @@ -104,8 +106,17 @@ class CoderRestClientTest { mockk(relaxed = true), mockk(), CoderSettingsStore(pluginTestSettingsStore(), Environment(), mockk(relaxed = true)), - mockk() - ) + mockk(), + object : ToolboxProxySettings { + override fun getProxy(): Proxy? = null + override fun getProxySelector(): ProxySelector? = null + override fun addProxyChangeListener(listener: Runnable) { + } + + override fun removeProxyChangeListener(listener: Runnable) { + } + }) + data class TestWorkspace(var workspace: Workspace, var resources: List? = emptyList()) @@ -529,6 +540,7 @@ class CoderRestClientTest { } @Test + @Ignore("Until proxy authentication is supported") fun usesProxy() { val settings = CoderSettingsStore(pluginTestSettingsStore(), Environment(), context.logger) val workspaces = listOf(DataGen.workspace("ws1")) @@ -545,26 +557,33 @@ class CoderRestClientTest { val srv2 = mockProxy() val client = CoderRestClient( - context.copy(settingsStore = settings), + context.copy(settingsStore = settings, proxySettings = object : ToolboxProxySettings { + override fun getProxy(): Proxy? = null + + override fun getProxySelector(): ProxySelector? { + return object : ProxySelector() { + override fun select(uri: URI): List = + listOf(Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", srv2.address.port))) + + override fun connectFailed( + uri: URI, + sa: SocketAddress, + ioe: IOException, + ) { + getDefault().connectFailed(uri, sa, ioe) + } + } + } + + override fun addProxyChangeListener(listener: Runnable) { + } + + override fun removeProxyChangeListener(listener: Runnable) { + } + + }), URL(url1), "token", - ProxyValues( - "foo", - "bar", - true, - object : ProxySelector() { - override fun select(uri: URI): List = - listOf(Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", srv2.address.port))) - - override fun connectFailed( - uri: URI, - sa: SocketAddress, - ioe: IOException, - ) { - getDefault().connectFailed(uri, sa, ioe) - } - }, - ), ) assertEquals(workspaces.map { ws -> ws.name }, runBlocking { client.workspaces() }.map { ws -> ws.name })