Skip to content

Commit ec7753b

Browse files
authored
impl: support for using proxies to access Coder REST API (#89)
- proxy credentials are not yet supported as they were not exposed in Toolbox settings - system and manual proxy configuration supported. - manual tests included system wide settings and manual http settings. - resolves #39
1 parent 1792901 commit ec7753b

File tree

10 files changed

+118
-66
lines changed

10 files changed

+118
-66
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- support for using proxies. Proxy authentication is not yet supported.
8+
59
## 0.1.5 - 2025-04-14
610

711
### Fixed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,42 @@ If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open
101101
page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable
102102
experience, it’s recommended to ensure the workspace is running prior to initiating the connection.
103103

104+
## Configuring and Testing workspace polling with HTTP & SOCKS5 Proxy
105+
106+
This section explains how to set up a local proxy (without authentication which is not yet supported) and verify that
107+
the plugin’s REST client works correctly when routed through it.
108+
109+
We’ll use [mitmproxy](https://mitmproxy.org/) for this — it can act as both an HTTP and SOCKS5 proxy with SSL
110+
interception.
111+
112+
### Install mitmproxy
113+
114+
1. Follow the [mitmproxy Install Guide](https://docs.mitmproxy.org/stable/overview-installation/) steps for your OS.
115+
2. Start the proxy:
116+
117+
```bash
118+
119+
mitmweb --ssl-insecure --set stream_large_bodies="10m"
120+
```
121+
122+
### Configure Mitmproxy
123+
124+
mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other:
125+
126+
1. Open http://127.0.0.1:8081 in browser;
127+
2. Navigate to `Options -> Edit Options`
128+
3. Update the `Mode` field to `regular` in order to activate HTTP/HTTPS or to `socks5`
129+
4. Proxy authentication can be enabled by updating the `proxyauth` to `username:password`
130+
131+
### Configure Proxy in Toolbox
132+
133+
1. Start Toolbox
134+
2. From Toolbox hexagonal menu icon go to `Settings -> Proxy`
135+
3. There are two options, to use system proxy settings or to manually configure the proxy details.
136+
4. If we go manually, add `127.0.0.1` to the host and port `8080` for HTTP/HTTPS or `1080` for SOCKS5.
137+
5. Before authenticating to the Coder deployment we need to tell the plugin where can we find mitmproxy
138+
certificates. In Coder's Settings page, set the `TLS CA path` to `~/.mitmproxy/mitmproxy-ca-cert.pem`
139+
104140
## Releasing
105141

106142
1. Check that the changelog lists all the important changes.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.1.5
1+
version=0.2.0
22
group=com.coder.toolbox
33
name=coder-toolbox

src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.coder.toolbox.util.toURL
77
import com.jetbrains.toolbox.api.core.diagnostics.Logger
88
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
99
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
10+
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
1011
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
1112
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1213
import com.jetbrains.toolbox.api.ui.ToolboxUi
@@ -21,7 +22,8 @@ data class CoderToolboxContext(
2122
val logger: Logger,
2223
val i18n: LocalizableStringFactory,
2324
val settingsStore: CoderSettingsStore,
24-
val secrets: CoderSecretsStore
25+
val secrets: CoderSecretsStore,
26+
val proxySettings: ToolboxProxySettings,
2527
) {
2628
/**
2729
* Try to find a URL.

src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import com.jetbrains.toolbox.api.core.PluginSecretStore
77
import com.jetbrains.toolbox.api.core.PluginSettingsStore
88
import com.jetbrains.toolbox.api.core.ServiceLocator
99
import com.jetbrains.toolbox.api.core.diagnostics.Logger
10+
import com.jetbrains.toolbox.api.core.getService
1011
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
1112
import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension
1213
import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
1314
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
15+
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
1416
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
1517
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1618
import com.jetbrains.toolbox.api.ui.ToolboxUi
@@ -25,15 +27,16 @@ class CoderToolboxExtension : RemoteDevExtension {
2527
val logger = serviceLocator.getService(Logger::class.java)
2628
return CoderRemoteProvider(
2729
CoderToolboxContext(
28-
serviceLocator.getService(ToolboxUi::class.java),
29-
serviceLocator.getService(EnvironmentUiPageManager::class.java),
30-
serviceLocator.getService(EnvironmentStateColorPalette::class.java),
31-
serviceLocator.getService(ClientHelper::class.java),
32-
serviceLocator.getService(CoroutineScope::class.java),
33-
serviceLocator.getService(Logger::class.java),
34-
serviceLocator.getService(LocalizableStringFactory::class.java),
35-
CoderSettingsStore(serviceLocator.getService(PluginSettingsStore::class.java), Environment(), logger),
36-
CoderSecretsStore(serviceLocator.getService(PluginSecretStore::class.java)),
30+
serviceLocator.getService<ToolboxUi>(),
31+
serviceLocator.getService<EnvironmentUiPageManager>(),
32+
serviceLocator.getService<EnvironmentStateColorPalette>(),
33+
serviceLocator.getService<ClientHelper>(),
34+
serviceLocator.getService<CoroutineScope>(),
35+
serviceLocator.getService<Logger>(),
36+
serviceLocator.getService<LocalizableStringFactory>(),
37+
CoderSettingsStore(serviceLocator.getService<PluginSettingsStore>(), Environment(), logger),
38+
CoderSecretsStore(serviceLocator.getService<PluginSecretStore>()),
39+
serviceLocator.getService<ToolboxProxySettings>()
3740
)
3841
)
3942
}

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,24 @@ import com.coder.toolbox.util.getArch
2323
import com.coder.toolbox.util.getHeaders
2424
import com.coder.toolbox.util.getOS
2525
import com.squareup.moshi.Moshi
26-
import okhttp3.Credentials
2726
import okhttp3.OkHttpClient
2827
import retrofit2.Response
2928
import retrofit2.Retrofit
3029
import retrofit2.converter.moshi.MoshiConverterFactory
3130
import java.net.HttpURLConnection
32-
import java.net.ProxySelector
3331
import java.net.URL
3432
import java.util.UUID
3533
import javax.net.ssl.X509TrustManager
3634

37-
/**
38-
* Holds proxy information.
39-
*/
40-
data class ProxyValues(
41-
val username: String?,
42-
val password: String?,
43-
val useAuth: Boolean,
44-
val selector: ProxySelector,
45-
)
46-
4735
/**
4836
* An HTTP client that can make requests to the Coder API.
4937
*
5038
* The token can be omitted if some other authentication mechanism is in use.
5139
*/
5240
open class CoderRestClient(
53-
context: CoderToolboxContext,
41+
private val context: CoderToolboxContext,
5442
val url: URL,
5543
val token: String?,
56-
private val proxyValues: ProxyValues? = null,
5744
private val pluginVersion: String = "development",
5845
) {
5946
private val settings = context.settingsStore.readOnly()
@@ -81,22 +68,27 @@ open class CoderRestClient(
8168
val trustManagers = coderTrustManagers(settings.tls.caPath)
8269
var builder = OkHttpClient.Builder()
8370

84-
if (proxyValues != null) {
85-
builder =
86-
builder
87-
.proxySelector(proxyValues.selector)
88-
.proxyAuthenticator { _, response ->
89-
if (proxyValues.useAuth && proxyValues.username != null && proxyValues.password != null) {
90-
val credentials = Credentials.basic(proxyValues.username, proxyValues.password)
91-
response.request.newBuilder()
92-
.header("Proxy-Authorization", credentials)
93-
.build()
94-
} else {
95-
null
96-
}
97-
}
71+
if (context.proxySettings.getProxy() != null) {
72+
context.logger.debug("proxy: ${context.proxySettings.getProxy()}")
73+
builder.proxy(context.proxySettings.getProxy())
74+
} else if (context.proxySettings.getProxySelector() != null) {
75+
context.logger.debug("proxy selector: ${context.proxySettings.getProxySelector()}")
76+
builder.proxySelector(context.proxySettings.getProxySelector()!!)
9877
}
9978

79+
//TODO - add support for proxy auth. when Toolbox exposes them
80+
// builder.proxyAuthenticator { _, response ->
81+
// if (proxyValues.useAuth && proxyValues.username != null && proxyValues.password != null) {
82+
// val credentials = Credentials.basic(proxyValues.username, proxyValues.password)
83+
// response.request.newBuilder()
84+
// .header("Proxy-Authorization", credentials)
85+
// .build()
86+
// } else {
87+
// null
88+
// }
89+
// }
90+
// }
91+
10092
if (token != null) {
10193
builder = builder.addInterceptor {
10294
it.proceed(

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,10 @@ open class CoderProtocolHandler(
238238
if (settings.requireTokenAuth && token == null) { // User aborted.
239239
throw MissingArgumentException("Token is required")
240240
}
241-
// The http client Toolbox gives us is already set up with the
242-
// proxy config, so we do net need to explicitly add it.
243241
val client = CoderRestClient(
244242
context,
245243
deploymentURL.toURL(),
246244
token,
247-
proxyValues = null, // TODO - not sure the above comment applies as we are creating our own http client
248245
PluginManager.pluginInfo.version
249246
)
250247
client.authenticate()

src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,10 @@ class ConnectStep(
7474
signInJob = context.cs.launch {
7575
try {
7676
statusField.textState.update { (context.i18n.ptrl("Authenticating to ${url.host}...")) }
77-
// The http client Toolbox gives us is already set up with the
78-
// proxy config, so we do net need to explicitly add it.
7977
val client = CoderRestClient(
8078
context,
8179
url,
8280
token,
83-
proxyValues = null,
8481
PluginManager.pluginInfo.version,
8582
)
8683
// allows interleaving with the back/cancel action

src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.coder.toolbox.util.toURL
3030
import com.jetbrains.toolbox.api.core.diagnostics.Logger
3131
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
3232
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
33+
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
3334
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
3435
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
3536
import com.jetbrains.toolbox.api.ui.ToolboxUi
@@ -68,7 +69,8 @@ internal class CoderCLIManagerTest {
6869
Environment(),
6970
mockk<Logger>(relaxed = true)
7071
),
71-
mockk<CoderSecretsStore>()
72+
mockk<CoderSecretsStore>(),
73+
mockk<ToolboxProxySettings>()
7274
)
7375

7476
/**

src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.coder.toolbox.util.sslContextFromPEMs
2323
import com.jetbrains.toolbox.api.core.diagnostics.Logger
2424
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
2525
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
26+
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
2627
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
2728
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
2829
import com.jetbrains.toolbox.api.ui.ToolboxUi
@@ -51,6 +52,7 @@ import java.nio.file.Path
5152
import java.util.UUID
5253
import javax.net.ssl.SSLHandshakeException
5354
import javax.net.ssl.SSLPeerUnverifiedException
55+
import kotlin.test.Ignore
5456
import kotlin.test.Test
5557
import kotlin.test.assertContains
5658
import kotlin.test.assertEquals
@@ -104,8 +106,17 @@ class CoderRestClientTest {
104106
mockk<Logger>(relaxed = true),
105107
mockk<LocalizableStringFactory>(),
106108
CoderSettingsStore(pluginTestSettingsStore(), Environment(), mockk<Logger>(relaxed = true)),
107-
mockk<CoderSecretsStore>()
108-
)
109+
mockk<CoderSecretsStore>(),
110+
object : ToolboxProxySettings {
111+
override fun getProxy(): Proxy? = null
112+
override fun getProxySelector(): ProxySelector? = null
113+
override fun addProxyChangeListener(listener: Runnable) {
114+
}
115+
116+
override fun removeProxyChangeListener(listener: Runnable) {
117+
}
118+
})
119+
109120

110121
data class TestWorkspace(var workspace: Workspace, var resources: List<WorkspaceResource>? = emptyList())
111122

@@ -529,6 +540,7 @@ class CoderRestClientTest {
529540
}
530541

531542
@Test
543+
@Ignore("Until proxy authentication is supported")
532544
fun usesProxy() {
533545
val settings = CoderSettingsStore(pluginTestSettingsStore(), Environment(), context.logger)
534546
val workspaces = listOf(DataGen.workspace("ws1"))
@@ -545,26 +557,33 @@ class CoderRestClientTest {
545557
val srv2 = mockProxy()
546558
val client =
547559
CoderRestClient(
548-
context.copy(settingsStore = settings),
560+
context.copy(settingsStore = settings, proxySettings = object : ToolboxProxySettings {
561+
override fun getProxy(): Proxy? = null
562+
563+
override fun getProxySelector(): ProxySelector? {
564+
return object : ProxySelector() {
565+
override fun select(uri: URI): List<Proxy> =
566+
listOf(Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", srv2.address.port)))
567+
568+
override fun connectFailed(
569+
uri: URI,
570+
sa: SocketAddress,
571+
ioe: IOException,
572+
) {
573+
getDefault().connectFailed(uri, sa, ioe)
574+
}
575+
}
576+
}
577+
578+
override fun addProxyChangeListener(listener: Runnable) {
579+
}
580+
581+
override fun removeProxyChangeListener(listener: Runnable) {
582+
}
583+
584+
}),
549585
URL(url1),
550586
"token",
551-
ProxyValues(
552-
"foo",
553-
"bar",
554-
true,
555-
object : ProxySelector() {
556-
override fun select(uri: URI): List<Proxy> =
557-
listOf(Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", srv2.address.port)))
558-
559-
override fun connectFailed(
560-
uri: URI,
561-
sa: SocketAddress,
562-
ioe: IOException,
563-
) {
564-
getDefault().connectFailed(uri, sa, ioe)
565-
}
566-
},
567-
),
568587
)
569588

570589
assertEquals(workspaces.map { ws -> ws.name }, runBlocking { client.workspaces() }.map { ws -> ws.name })

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy