Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

- support for using proxies. Proxy authentication is not yet supported.

## 0.1.5 - 2025-04-14

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version=0.1.5
version=0.2.0
group=com.coder.toolbox
name=coder-toolbox
4 changes: 3 additions & 1 deletion src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
21 changes: 12 additions & 9 deletions src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ToolboxUi>(),
serviceLocator.getService<EnvironmentUiPageManager>(),
serviceLocator.getService<EnvironmentStateColorPalette>(),
serviceLocator.getService<ClientHelper>(),
serviceLocator.getService<CoroutineScope>(),
serviceLocator.getService<Logger>(),
serviceLocator.getService<LocalizableStringFactory>(),
CoderSettingsStore(serviceLocator.getService<PluginSettingsStore>(), Environment(), logger),
CoderSecretsStore(serviceLocator.getService<PluginSecretStore>()),
serviceLocator.getService<ToolboxProxySettings>()
)
)
}
Expand Down
48 changes: 20 additions & 28 deletions src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 0 additions & 3 deletions src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -68,7 +69,8 @@ internal class CoderCLIManagerTest {
Environment(),
mockk<Logger>(relaxed = true)
),
mockk<CoderSecretsStore>()
mockk<CoderSecretsStore>(),
mockk<ToolboxProxySettings>()
)

/**
Expand Down
59 changes: 39 additions & 20 deletions src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -104,8 +106,17 @@ class CoderRestClientTest {
mockk<Logger>(relaxed = true),
mockk<LocalizableStringFactory>(),
CoderSettingsStore(pluginTestSettingsStore(), Environment(), mockk<Logger>(relaxed = true)),
mockk<CoderSecretsStore>()
)
mockk<CoderSecretsStore>(),
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<WorkspaceResource>? = emptyList())

Expand Down Expand Up @@ -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"))
Expand All @@ -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<Proxy> =
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<Proxy> =
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 })
Expand Down
Loading