Skip to content

impl: support for Toolbox 2.7 #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

### Added

- support for basic authentication for HTTP/HTTPS proxy
- support for Toolbox 2.7 release

### Changed

- improved message while loading the workspace

## 0.3.2 - 2025-06-25

### Changed
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ experience, it’s recommended to ensure the workspace is running prior to initi

## 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
This section explains how to set up a local proxy 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
Expand All @@ -134,6 +134,12 @@ mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other:
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`
5. Alternatively you can run the following commands:

```bash
mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode regular --proxyauth proxyUsername:proxyPassword
mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode socks5
```

### Configure Proxy in Toolbox

Expand All @@ -144,6 +150,10 @@ mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other:
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`

> ⚠️ Note: Coder Toolbox plugin handles only HTTP/HTTPS proxy authentication.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> ⚠️ Note: Coder Toolbox plugin handles only HTTP/HTTPS proxy authentication.
> [!NOTE]
> Coder Toolbox plugin handles only HTTP/HTTPS proxy authentication.

> SOCKS5 proxy authentication is currently not supported due to limitations
> described in: https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0

## Debugging and Reporting issues

Enabling debug logging is essential for diagnosing issues with the Toolbox plugin, especially when SSH
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.3.2
version=0.4.0
group=com.coder.toolbox
name=coder-toolbox
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
toolbox-plugin-api = "1.1.41749"
toolbox-plugin-api = "1.3.46097"
kotlin = "2.1.10"
coroutines = "1.10.1"
serialization = "1.8.0"
Expand Down
25 changes: 7 additions & 18 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ class CoderRemoteProvider(
// On the first load, automatically log in if we can.
private var firstRun = true
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(context.deploymentUrl.toString()))
private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString()))
private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized)

override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...")
override val environments: MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>> = MutableStateFlow(
LoadableState.Loading
)
Expand Down Expand Up @@ -167,7 +168,7 @@ class CoderRemoteProvider(
close()
// force auto-login
firstRun = true
goToEnvironmentsPage()
context.envPageManager.showPluginEnvironmentsPage()
break
}
}
Expand Down Expand Up @@ -317,25 +318,13 @@ class CoderRemoteProvider(
close()
// start initialization with the new settings
this@CoderRemoteProvider.client = restClient
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(restClient.url.toString()))
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))

environments.showLoadingMessage()
pollJob = poll(restClient, cli)
}
}

/**
* Make Toolbox ask for the page again. Use any time we need to change the
* root page (for example, sign-in or the environment list).
*
* When moving between related pages, instead use ui.showUiPage() and
* ui.hideUiPage() which stacks and has built-in back navigation, rather
* than using multiple root pages.
*/
private fun goToEnvironmentsPage() {
context.envPageManager.showPluginEnvironmentsPage()
}

/**
* Return the sign-in page if we do not have a valid client.

Expand Down Expand Up @@ -377,7 +366,7 @@ class CoderRemoteProvider(

private fun shouldDoAutoSetup(): Boolean = firstRun && context.secrets.rememberMe == true

private suspend fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
private fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
// Store the URL and token for use next time.
context.secrets.lastDeploymentURL = client.url.toString()
context.secrets.lastToken = client.token ?: ""
Expand All @@ -387,9 +376,9 @@ class CoderRemoteProvider(
this.client = client
pollJob?.cancel()
environments.showLoadingMessage()
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(client.url.toString()))
coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString()))
pollJob = poll(client, cli)
context.refreshMainPage()
context.envPageManager.showPluginEnvironmentsPage()
}

private fun MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>>.showLoadingMessage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
return CustomRemoteEnvironmentStateV2(
context.i18n.pnotr(label),
color = getStateColor(context),
reachable = ready() || unhealthy(),
isReachable = ready() || unhealthy(),
// TODO@JB: How does this work? Would like a spinner for pending states.
icon = getStateIcon()
iconId = getStateIcon().id,
isPriorityShow = true
)
}

Expand Down
27 changes: 15 additions & 12 deletions src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import com.coder.toolbox.util.coderTrustManagers
import com.coder.toolbox.util.getArch
import com.coder.toolbox.util.getHeaders
import com.coder.toolbox.util.getOS
import com.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth
import com.squareup.moshi.Moshi
import okhttp3.Credentials
import okhttp3.OkHttpClient
import retrofit2.Response
import retrofit2.Retrofit
Expand Down Expand Up @@ -78,18 +80,19 @@ open class CoderRestClient(
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
// }
// }
// }
// Note: This handles only HTTP/HTTPS proxy authentication.
// SOCKS5 proxy authentication is currently not supported due to limitations described in:
// https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0
builder.proxyAuthenticator { _, response ->
val proxyAuth = context.proxySettings.getProxyAuth()
if (proxyAuth == null || proxyAuth !is ProxyAuth.Basic) {
return@proxyAuthenticator null
}
val credentials = Credentials.basic(proxyAuth.username, proxyAuth.password)
response.request.newBuilder()
.header("Proxy-Authorization", credentials)
.build()
}

if (token != null) {
builder = builder.addInterceptor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class CoderCliSetupWizardPage(
client: CoderRestClient,
cli: CoderCLIManager,
) -> Unit,
) : CoderPage(context.i18n.ptrl("Setting up Coder"), false) {
) : CoderPage(MutableStateFlow(context.i18n.ptrl("Setting up Coder")), false) {
private val shouldAutoSetup = MutableStateFlow(initialAutoSetup)
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
context.ui.showUiPage(settingsPage)
Expand Down
11 changes: 9 additions & 2 deletions src/main/kotlin/com/coder/toolbox/views/CoderPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
import com.jetbrains.toolbox.api.ui.components.UiPage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

/**
* Base page that handles the icon, displaying error notifications, and
Expand All @@ -19,9 +20,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
* to use the mouse.
*/
abstract class CoderPage(
title: LocalizableString,
private val titleObservable: MutableStateFlow<LocalizableString>,
showIcon: Boolean = true,
) : UiPage(title) {
) : UiPage(titleObservable) {

fun setTitle(title: LocalizableString) {
titleObservable.update {
title
}
}

/**
* Return the icon, if showing one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch
* I have not been able to test this page.
*/
class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<Boolean>) :
CoderPage(context.i18n.ptrl("Coder Settings"), false) {
CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) {
private val settings = context.settingsStore.readOnly()

// TODO: Copy over the descriptions, holding until I can test this page.
Expand Down
5 changes: 2 additions & 3 deletions src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.coder.toolbox.views

import com.coder.toolbox.CoderToolboxContext
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.components.UiField
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -14,7 +13,7 @@ import kotlinx.coroutines.flow.StateFlow
* For now we just use this to display the deployment URL since we do not
* support creating environments from the plugin.
*/
class NewEnvironmentPage(context: CoderToolboxContext, deploymentURL: LocalizableString) :
CoderPage(deploymentURL) {
class NewEnvironmentPage(deploymentURL: LocalizableString) :
CoderPage(MutableStateFlow(deploymentURL)) {
override val fields: StateFlow<List<UiField>> = MutableStateFlow(emptyList())
}
3 changes: 3 additions & 0 deletions src/main/resources/localization/defaultMessages.po
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,7 @@ msgid "Error encountered while setting up Coder"
msgstr ""

msgid "Setting up Coder"
msgstr ""

msgid "Loading workspaces..."
msgstr ""
4 changes: 4 additions & 0 deletions src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
import com.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
Expand Down Expand Up @@ -114,6 +115,8 @@ class CoderRestClientTest {
object : ToolboxProxySettings {
override fun getProxy(): Proxy? = null
override fun getProxySelector(): ProxySelector? = null
override fun getProxyAuth(): ProxyAuth? = null

override fun addProxyChangeListener(listener: Runnable) {
}

Expand Down Expand Up @@ -579,6 +582,7 @@ class CoderRestClientTest {
}
}

override fun getProxyAuth(): ProxyAuth? = null
override fun addProxyChangeListener(listener: Runnable) {
}

Expand Down
Loading