Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitacd0578

Browse files
authored
fix: support for downloading the CLI when proxy is configured (#177)
Until this commit, the CLI download manager relied on a separately configured HTTPclient that lacked proxy support, unlike the REST client which was refactored and modularized.Now we have the same support for proxy and a proper user agent and custom logging interceptor.
1 parent6ab431e commitacd0578

File tree

8 files changed

+192
-90
lines changed

8 files changed

+192
-90
lines changed

‎CHANGELOG.md‎

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

77
- workspaces status is now refresh every time Coder Toolbox becomes visible
88

9+
###Fixed
10+
11+
- support for downloading the CLI when proxy is configured
12+
913
##0.6.2 - 2025-08-14
1014

1115
###Changed

‎README.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,27 @@ mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode socks5
220220
>
221221
in:https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0
222222

223+
###Mitmproxy returns 502 Bad Gateway to the client
224+
225+
When running traffic through mitmproxy, you may encounter 502 Bad Gateway errors that mention HTTP/2 protocol error: *
226+
*Received header value surrounded by whitespace**.
227+
This happens because some upstream servers (including dev.coder.com) send back headers such as Content-Security-Policy
228+
with leading or trailing spaces.
229+
While browsers and many HTTP clients accept these headers, mitmproxy enforces the stricter HTTP/2 and HTTP/1.1 RFCs,
230+
which forbid whitespace around header values.
231+
As a result, mitmproxy rejects the response and surfaces a 502 to the client.
232+
233+
The workaround is to disable HTTP/2 in mitmproxy and force HTTP/1.1 on both the client and upstream sides. This avoids
234+
the strict header validation path and allows
235+
mitmproxy to pass responses through unchanged. You can do this by starting mitmproxy with:
236+
237+
```bash
238+
mitmproxy --set http2=false --set upstream_http_version=HTTP/1.1
239+
```
240+
241+
This ensures coder toolbox http client ↔ mitmproxy ↔ server connections all run over HTTP/1.1, preventing the whitespace
242+
error.
243+
223244
##Debugging and Reporting issues
224245

225246
Enabling debug logging is essential for diagnosing issues with the Toolbox plugin, especially when SSH

‎src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt‎

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import com.coder.toolbox.cli.gpg.GPGVerifier
1212
importcom.coder.toolbox.cli.gpg.VerificationResult
1313
importcom.coder.toolbox.cli.gpg.VerificationResult.Failed
1414
importcom.coder.toolbox.cli.gpg.VerificationResult.Invalid
15+
importcom.coder.toolbox.plugin.PluginManager
16+
importcom.coder.toolbox.sdk.CoderHttpClientBuilder
17+
importcom.coder.toolbox.sdk.interceptors.Interceptors
1518
importcom.coder.toolbox.sdk.v2.models.Workspace
1619
importcom.coder.toolbox.sdk.v2.models.WorkspaceAgent
1720
importcom.coder.toolbox.settings.SignatureFallbackStrategy.ALLOW
18-
importcom.coder.toolbox.util.CoderHostnameVerifier
1921
importcom.coder.toolbox.util.InvalidVersionException
2022
importcom.coder.toolbox.util.SemVer
21-
importcom.coder.toolbox.util.coderSocketFactory
22-
importcom.coder.toolbox.util.coderTrustManagers
2323
importcom.coder.toolbox.util.escape
2424
importcom.coder.toolbox.util.escapeSubcommand
2525
importcom.coder.toolbox.util.safeHost
@@ -29,15 +29,13 @@ import com.squareup.moshi.JsonDataException
2929
importcom.squareup.moshi.Moshi
3030
importkotlinx.coroutines.Dispatchers
3131
importkotlinx.coroutines.withContext
32-
importokhttp3.OkHttpClient
3332
importorg.zeroturnaround.exec.ProcessExecutor
3433
importretrofit2.Retrofit
3534
importjava.io.EOFException
3635
importjava.io.FileNotFoundException
3736
importjava.net.URL
3837
importjava.nio.file.Files
3938
importjava.nio.file.Path
40-
importjavax.net.ssl.X509TrustManager
4139

4240
/**
4341
* Version output from the CLI's version command.
@@ -148,13 +146,14 @@ class CoderCLIManager(
148146
val coderConfigPath:Path= context.settingsStore.dataDir(deploymentURL).resolve("config")
149147

150148
privatefuncreateDownloadService():CoderDownloadService {
151-
val okHttpClient=OkHttpClient.Builder()
152-
.sslSocketFactory(
153-
coderSocketFactory(context.settingsStore.tls),
154-
coderTrustManagers(context.settingsStore.tls.caPath)[0]asX509TrustManager
155-
)
156-
.hostnameVerifier(CoderHostnameVerifier(context.settingsStore.tls.altHostname))
157-
.build()
149+
val interceptors= buildList {
150+
add((Interceptors.userAgent(PluginManager.pluginInfo.version)))
151+
add(Interceptors.logging(context))
152+
}
153+
val okHttpClient=CoderHttpClientBuilder.build(
154+
context,
155+
interceptors
156+
)
158157

159158
val retrofit=Retrofit.Builder()
160159
.baseUrl(deploymentURL.toString())
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
packagecom.coder.toolbox.sdk
2+
3+
importcom.coder.toolbox.CoderToolboxContext
4+
importcom.coder.toolbox.util.CoderHostnameVerifier
5+
importcom.coder.toolbox.util.coderSocketFactory
6+
importcom.coder.toolbox.util.coderTrustManagers
7+
importcom.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth
8+
importokhttp3.Credentials
9+
importokhttp3.Interceptor
10+
importokhttp3.OkHttpClient
11+
importjavax.net.ssl.X509TrustManager
12+
13+
object CoderHttpClientBuilder {
14+
funbuild(
15+
context:CoderToolboxContext,
16+
interceptors:List<Interceptor>
17+
):OkHttpClient {
18+
val settings= context.settingsStore.readOnly()
19+
20+
val socketFactory= coderSocketFactory(settings.tls)
21+
val trustManagers= coderTrustManagers(settings.tls.caPath)
22+
var builder=OkHttpClient.Builder()
23+
24+
if (context.proxySettings.getProxy()!=null) {
25+
context.logger.info("proxy:${context.proxySettings.getProxy()}")
26+
builder.proxy(context.proxySettings.getProxy())
27+
}elseif (context.proxySettings.getProxySelector()!=null) {
28+
context.logger.info("proxy selector:${context.proxySettings.getProxySelector()}")
29+
builder.proxySelector(context.proxySettings.getProxySelector()!!)
30+
}
31+
32+
// Note: This handles only HTTP/HTTPS proxy authentication.
33+
// SOCKS5 proxy authentication is currently not supported due to limitations described in:
34+
// https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0
35+
builder.proxyAuthenticator { _, response->
36+
val proxyAuth= context.proxySettings.getProxyAuth()
37+
if (proxyAuth==null|| proxyAuth!isProxyAuth.Basic) {
38+
return@proxyAuthenticatornull
39+
}
40+
val credentials=Credentials.basic(proxyAuth.username, proxyAuth.password)
41+
response.request.newBuilder()
42+
.header("Proxy-Authorization", credentials)
43+
.build()
44+
}
45+
46+
builder.sslSocketFactory(socketFactory, trustManagers[0]asX509TrustManager)
47+
.hostnameVerifier(CoderHostnameVerifier(settings.tls.altHostname))
48+
.retryOnConnectionFailure(true)
49+
50+
interceptors.forEach { interceptor->
51+
builder.addInterceptor(interceptor)
52+
53+
}
54+
return builder.build()
55+
}
56+
}

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

Lines changed: 14 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.coder.toolbox.sdk.convertors.LoggingConverterFactory
77
importcom.coder.toolbox.sdk.convertors.OSConverter
88
importcom.coder.toolbox.sdk.convertors.UUIDConverter
99
importcom.coder.toolbox.sdk.ex.APIResponseException
10-
importcom.coder.toolbox.sdk.interceptors.LoggingInterceptor
10+
importcom.coder.toolbox.sdk.interceptors.Interceptors
1111
importcom.coder.toolbox.sdk.v2.CoderV2RestFacade
1212
importcom.coder.toolbox.sdk.v2.models.ApiErrorResponse
1313
importcom.coder.toolbox.sdk.v2.models.BuildInfo
@@ -21,23 +21,14 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceBuildReason
2121
importcom.coder.toolbox.sdk.v2.models.WorkspaceResource
2222
importcom.coder.toolbox.sdk.v2.models.WorkspaceStatus
2323
importcom.coder.toolbox.sdk.v2.models.WorkspaceTransition
24-
importcom.coder.toolbox.util.CoderHostnameVerifier
25-
importcom.coder.toolbox.util.coderSocketFactory
26-
importcom.coder.toolbox.util.coderTrustManagers
27-
importcom.coder.toolbox.util.getArch
28-
importcom.coder.toolbox.util.getHeaders
29-
importcom.coder.toolbox.util.getOS
30-
importcom.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth
3124
importcom.squareup.moshi.Moshi
32-
importokhttp3.Credentials
3325
importokhttp3.OkHttpClient
3426
importretrofit2.Response
3527
importretrofit2.Retrofit
3628
importretrofit2.converter.moshi.MoshiConverterFactory
3729
importjava.net.HttpURLConnection
3830
importjava.net.URL
3931
importjava.util.UUID
40-
importjavax.net.ssl.X509TrustManager
4132

4233
/**
4334
* An HTTP client that can make requests to the Coder API.
@@ -50,7 +41,6 @@ open class CoderRestClient(
5041
valtoken:String?,
5142
privatevalpluginVersion:String ="development",
5243
) {
53-
privateval settings= context.settingsStore.readOnly()
5444
privatelateinitvar moshi:Moshi
5545
privatelateinitvar httpClient:OkHttpClient
5646
privatelateinitvar retroRestClient:CoderV2RestFacade
@@ -70,69 +60,22 @@ open class CoderRestClient(
7060
.add(OSConverter())
7161
.add(UUIDConverter())
7262
.build()
73-
74-
val socketFactory= coderSocketFactory(settings.tls)
75-
val trustManagers= coderTrustManagers(settings.tls.caPath)
76-
var builder=OkHttpClient.Builder()
77-
78-
if (context.proxySettings.getProxy()!=null) {
79-
context.logger.info("proxy:${context.proxySettings.getProxy()}")
80-
builder.proxy(context.proxySettings.getProxy())
81-
}elseif (context.proxySettings.getProxySelector()!=null) {
82-
context.logger.info("proxy selector:${context.proxySettings.getProxySelector()}")
83-
builder.proxySelector(context.proxySettings.getProxySelector()!!)
84-
}
85-
86-
// Note: This handles only HTTP/HTTPS proxy authentication.
87-
// SOCKS5 proxy authentication is currently not supported due to limitations described in:
88-
// https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0
89-
builder.proxyAuthenticator { _, response->
90-
val proxyAuth= context.proxySettings.getProxyAuth()
91-
if (proxyAuth==null|| proxyAuth!isProxyAuth.Basic) {
92-
return@proxyAuthenticatornull
93-
}
94-
val credentials=Credentials.basic(proxyAuth.username, proxyAuth.password)
95-
response.request.newBuilder()
96-
.header("Proxy-Authorization", credentials)
97-
.build()
98-
}
99-
100-
if (context.settingsStore.requireTokenAuth) {
101-
if (token.isNullOrBlank()) {
102-
throwIllegalStateException("Token is required for$url deployment")
103-
}
104-
builder= builder.addInterceptor {
105-
it.proceed(
106-
it.request().newBuilder().addHeader("Coder-Session-Token", token).build()
107-
)
63+
val interceptors= buildList {
64+
if (context.settingsStore.requireTokenAuth) {
65+
if (token.isNullOrBlank()) {
66+
throwIllegalStateException("Token is required for$url deployment")
67+
}
68+
add(Interceptors.tokenAuth(token))
10869
}
70+
add((Interceptors.userAgent(pluginVersion)))
71+
add(Interceptors.externalHeaders(context, url))
72+
add(Interceptors.logging(context))
10973
}
11074

111-
httpClient=
112-
builder
113-
.sslSocketFactory(socketFactory, trustManagers[0]asX509TrustManager)
114-
.hostnameVerifier(CoderHostnameVerifier(settings.tls.altHostname))
115-
.retryOnConnectionFailure(true)
116-
.addInterceptor {
117-
it.proceed(
118-
it.request().newBuilder().addHeader(
119-
"User-Agent",
120-
"Coder Toolbox/$pluginVersion (${getOS()};${getArch()})",
121-
).build(),
122-
)
123-
}
124-
.addInterceptor {
125-
var request= it.request()
126-
val headers= getHeaders(url, settings.headerCommand)
127-
if (headers.isNotEmpty()) {
128-
val reqBuilder= request.newBuilder()
129-
headers.forEach { h-> reqBuilder.addHeader(h.key, h.value) }
130-
request= reqBuilder.build()
131-
}
132-
it.proceed(request)
133-
}
134-
.addInterceptor(LoggingInterceptor(context))
135-
.build()
75+
httpClient=CoderHttpClientBuilder.build(
76+
context,
77+
interceptors
78+
)
13679

13780
retroRestClient=
13881
Retrofit.Builder().baseUrl(url.toString()).client(httpClient)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
packagecom.coder.toolbox.sdk.interceptors
2+
3+
importcom.coder.toolbox.CoderToolboxContext
4+
importcom.coder.toolbox.util.getArch
5+
importcom.coder.toolbox.util.getHeaders
6+
importcom.coder.toolbox.util.getOS
7+
importokhttp3.Interceptor
8+
importjava.net.URL
9+
10+
/**
11+
* Factory of okhttp interceptors
12+
*/
13+
object Interceptors {
14+
15+
/**
16+
* Creates a token authentication interceptor
17+
*/
18+
funtokenAuth(token:String):Interceptor {
19+
returnInterceptor { chain->
20+
chain.proceed(
21+
chain.request().newBuilder()
22+
.addHeader("Coder-Session-Token", token)
23+
.build()
24+
)
25+
}
26+
}
27+
28+
/**
29+
* Creates a User-Agent header interceptor
30+
*/
31+
funuserAgent(pluginVersion:String):Interceptor {
32+
returnInterceptor { chain->
33+
chain.proceed(
34+
chain.request().newBuilder()
35+
.addHeader("User-Agent","Coder Toolbox/$pluginVersion (${getOS()};${getArch()})")
36+
.build()
37+
)
38+
}
39+
}
40+
41+
/**
42+
* Adds headers generated by executing a native command
43+
*/
44+
funexternalHeaders(context:CoderToolboxContext,url:URL):Interceptor {
45+
val settings= context.settingsStore.readOnly()
46+
returnInterceptor { chain->
47+
var request= chain.request()
48+
val headers= getHeaders(url, settings.headerCommand)
49+
if (headers.isNotEmpty()) {
50+
val reqBuilder= request.newBuilder()
51+
headers.forEach { h-> reqBuilder.addHeader(h.key, h.value) }
52+
request= reqBuilder.build()
53+
}
54+
chain.proceed(request)
55+
}
56+
}
57+
58+
/**
59+
* Creates a logging interceptor
60+
*/
61+
funlogging(context:CoderToolboxContext):Interceptor {
62+
returnLoggingInterceptor(context)
63+
}
64+
}

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
3535
importcom.jetbrains.toolbox.api.core.os.LocalDesktopManager
3636
importcom.jetbrains.toolbox.api.localization.LocalizableStringFactory
3737
importcom.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
38+
importcom.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth
3839
importcom.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
3940
importcom.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
4041
importcom.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
@@ -52,6 +53,8 @@ import org.zeroturnaround.exec.InvalidExitValueException
5253
importorg.zeroturnaround.exec.ProcessInitException
5354
importjava.net.HttpURLConnection
5455
importjava.net.InetSocketAddress
56+
importjava.net.Proxy
57+
importjava.net.ProxySelector
5558
importjava.net.URI
5659
importjava.net.URL
5760
importjava.nio.file.AccessDeniedException
@@ -87,8 +90,17 @@ internal class CoderCLIManagerTest {
8790
mockk<Logger>(relaxed=true)
8891
),
8992
mockk<CoderSecretsStore>(),
90-
mockk<ToolboxProxySettings>()
91-
)
93+
object:ToolboxProxySettings {
94+
overridefungetProxy():Proxy?=null
95+
overridefungetProxySelector():ProxySelector?=null
96+
overridefungetProxyAuth():ProxyAuth?=null
97+
98+
overridefunaddProxyChangeListener(listener:Runnable) {
99+
}
100+
101+
overridefunremoveProxyChangeListener(listener:Runnable) {
102+
}
103+
})
92104

93105
@BeforeTest
94106
funsetup() {
@@ -547,11 +559,10 @@ internal class CoderCLIManagerTest {
547559
context.logger,
548560
)
549561

550-
val ccm=
551-
CoderCLIManager(
552-
context.copy(settingsStore= settings),
553-
it.url?:URI.create("https://test.coder.invalid").toURL()
554-
)
562+
val ccm=CoderCLIManager(
563+
context.copy(settingsStore= settings),
564+
it.url?:URI.create("https://test.coder.invalid").toURL()
565+
)
555566

556567
val sshConfigPath=Path.of(settings.sshConfigPath)
557568
// Input is the configuration that we start with, if any.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp