Skip to content

Commit fef2032

Browse files
committed
Add headers to API requests
1 parent 761303e commit fef2032

File tree

5 files changed

+121
-6
lines changed

5 files changed

+121
-6
lines changed

src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
150150
if (token == null) { // User aborted.
151151
throw IllegalArgumentException("Unable to connect to $deploymentURL, $TOKEN is missing")
152152
}
153-
val client = CoderRestClient(deploymentURL, token.first)
153+
val client = CoderRestClient(deploymentURL, token.first, settings.headerCommand)
154154
return try {
155155
Pair(client, client.me().username)
156156
} catch (ex: AuthenticationResponseException) {

src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.intellij.openapi.extensions.PluginId
2020
import com.intellij.openapi.util.SystemInfo
2121
import okhttp3.OkHttpClient
2222
import okhttp3.logging.HttpLoggingInterceptor
23+
import org.zeroturnaround.exec.ProcessExecutor
2324
import retrofit2.Retrofit
2425
import retrofit2.converter.gson.GsonConverterFactory
2526
import java.net.HttpURLConnection.HTTP_CREATED
@@ -41,16 +42,16 @@ class CoderRestClientService {
4142
*
4243
* @throws [AuthenticationResponseException] if authentication failed.
4344
*/
44-
fun initClientSession(url: URL, token: String): User {
45-
client = CoderRestClient(url, token)
45+
fun initClientSession(url: URL, token: String, headerCommand: String?): User {
46+
client = CoderRestClient(url, token, headerCommand)
4647
me = client.me()
4748
buildVersion = client.buildInfo().version
4849
isReady = true
4950
return me
5051
}
5152
}
5253

53-
class CoderRestClient(var url: URL, var token: String) {
54+
class CoderRestClient(var url: URL, var token: String, var headerCommand: String?) {
5455
private var httpClient: OkHttpClient
5556
private var retroRestClient: CoderV2RestFacade
5657

@@ -61,6 +62,16 @@ class CoderRestClient(var url: URL, var token: String) {
6162
httpClient = OkHttpClient.Builder()
6263
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("Coder-Session-Token", token).build()) }
6364
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("User-Agent", "Coder Gateway/${pluginVersion.version} (${SystemInfo.getOsNameAndVersion()}; ${SystemInfo.OS_ARCH})").build()) }
65+
.addInterceptor {
66+
var request = it.request()
67+
val headers = getHeaders(url, headerCommand)
68+
if (headers.size > 0) {
69+
val builder = request.newBuilder()
70+
headers.forEach { builder.addHeader(it.key, it.value) }
71+
request = builder.build()
72+
}
73+
it.proceed(request)
74+
}
6475
// this should always be last if we want to see previous interceptors logged
6576
.addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BASIC) })
6677
.build()
@@ -141,4 +152,45 @@ class CoderRestClient(var url: URL, var token: String) {
141152

142153
return buildResponse.body()!!
143154
}
155+
156+
companion object {
157+
private val newlineRegex = "\r?\n".toRegex()
158+
private val endingNewlineRegex = "\r?\n$".toRegex()
159+
160+
// TODO: This really only needs to be a private function, but
161+
// unfortunately it is not possible to test the client because it fails
162+
// on the plugin manager core call and I do not know how to fix it. So,
163+
// for now make this static and test it directly instead.
164+
@JvmStatic
165+
fun getHeaders(url: URL, headerCommand: String?): Map<String, String> {
166+
if (headerCommand.isNullOrBlank()) {
167+
return emptyMap()
168+
}
169+
val (shell, caller) = when (getOS()) {
170+
OS.WINDOWS -> Pair("cmd.exe", "/c")
171+
else -> Pair("sh", "-c")
172+
}
173+
return ProcessExecutor()
174+
.command(shell, caller, headerCommand)
175+
.environment("CODER_URL", url.toString())
176+
.exitValues(0)
177+
.readOutput(true)
178+
.execute()
179+
.outputUTF8()
180+
.replaceFirst(endingNewlineRegex, "")
181+
.split(newlineRegex)
182+
.associate {
183+
// Header names cannot be blank or contain whitespace and
184+
// the Coder CLI requires that there be an equals sign (the
185+
// value can be blank though). The second case is taken
186+
// care of by the destructure here, as it will throw if
187+
// there are not enough parts.
188+
val (name, value) = it.split("=", limit=2)
189+
if (name.contains(" ") || name == "") {
190+
throw Exception("\"$name\" is not a valid header name")
191+
}
192+
name to value
193+
}
194+
}
195+
}
144196
}

src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.coder.gateway.sdk.toURL
1313
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
1414
import com.coder.gateway.sdk.v2.models.toAgentModels
1515
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
16+
import com.coder.gateway.services.CoderSettingsState
1617
import com.coder.gateway.toWorkspaceParams
1718
import com.intellij.icons.AllIcons
1819
import com.intellij.ide.BrowserUtil
@@ -72,6 +73,7 @@ data class DeploymentInfo(
7273
)
7374

7475
class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: (Component) -> Unit) : GatewayRecentConnections, Disposable {
76+
private val settings: CoderSettingsState = service()
7577
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()
7678
private val cs = CoroutineScope(Dispatchers.Main)
7779

@@ -259,7 +261,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
259261
deployments[dir] ?: try {
260262
val url = Path.of(dir).resolve("url").readText()
261263
val token = Path.of(dir).resolve("session").readText()
262-
DeploymentInfo(CoderRestClient(url.toURL(), token))
264+
DeploymentInfo(CoderRestClient(url.toURL(), token, settings.headerCommand))
263265
} catch (e: Exception) {
264266
logger.error("Unable to create client from $dir", e)
265267
DeploymentInfo(error = "Error trying to read $dir: ${e.message}")

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
527527
*/
528528
private fun authenticate(url: URL, token: String) {
529529
logger.info("Authenticating to $url...")
530-
clientService.initClientSession(url, token)
530+
clientService.initClientSession(url, token, settings.headerCommand)
531531

532532
try {
533533
logger.info("Checking compatibility with Coder version ${clientService.buildVersion}...")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.coder.gateway.sdk
2+
3+
import spock.lang.*
4+
5+
@Unroll
6+
class CoderRestClientTest extends Specification {
7+
def "gets headers"() {
8+
expect:
9+
CoderRestClient.getHeaders(new URL("http://localhost"), command) == expected
10+
11+
where:
12+
command | expected
13+
null | [:]
14+
"" | [:]
15+
"printf 'foo=bar\nbaz=qux'" | ["foo": "bar", "baz": "qux"]
16+
"printf 'foo=bar\r\nbaz=qux'" | ["foo": "bar", "baz": "qux"]
17+
"printf 'foo=bar\r\n'" | ["foo": "bar"]
18+
"printf 'foo=bar'" | ["foo": "bar"]
19+
"printf 'foo=bar='" | ["foo": "bar="]
20+
"printf 'foo=bar=baz'" | ["foo": "bar=baz"]
21+
"printf 'foo='" | ["foo": ""]
22+
}
23+
24+
def "fails to get headers"() {
25+
when:
26+
CoderRestClient.getHeaders(new URL("http://localhost"), command)
27+
28+
then:
29+
thrown(Exception)
30+
31+
where:
32+
command << [
33+
"printf 'foo=bar\r\n\r\n'",
34+
"printf '\r\nfoo=bar'",
35+
"printf '=foo'",
36+
"printf 'foo'",
37+
"printf ' =foo'",
38+
"printf 'foo =bar'",
39+
"printf 'foo foo=bar'",
40+
"printf ''",
41+
"exit 1",
42+
]
43+
}
44+
45+
@IgnoreIf({ os.windows })
46+
def "has access to environment variables"() {
47+
expect:
48+
CoderRestClient.getHeaders(new URL("http://localhost"), "printf url=\$CODER_URL") == [
49+
"url": "http://localhost",
50+
]
51+
}
52+
53+
@Requires({ os.windows })
54+
def "has access to environment variables"() {
55+
expect:
56+
CoderRestClient.getHeaders(new URL("http://localhost"), "printf url=%CODER_URL%") == [
57+
"url": "http://localhost",
58+
]
59+
60+
}
61+
}

0 commit comments

Comments
 (0)