Skip to content

Commit cd068a1

Browse files
committed
impl: support for using proxies to access Coder REST API
- 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 cd068a1

File tree

8 files changed

+81
-65
lines changed

8 files changed

+81
-65
lines changed

Diff for: CHANGELOG.md

+4
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

Diff for: src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt

+3-1
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.

Diff for: src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt

+12-9
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
}

Diff for: src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

+20-28
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(

Diff for: src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

-3
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()

Diff for: src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt

-3
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

Diff for: src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt

+3-1
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
/**

Diff for: src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt

+39-20
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)