Skip to content

impl: verify cli signature #562

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

Merged
merged 11 commits into from
Jul 25, 2025
Merged
Prev Previous commit
Next Next commit
fix: download the correct CLI signature for Windows
The signature for windows CLI follows the format: coder-windows-amd64.exe.asc
Currently it is coded to coder-windows-amd64.asc which means the plugin
always fail to find any signature for windows cli
  • Loading branch information
fioan89 committed Jul 22, 2025
commit 8f5e5596b8737e7d2e1a8866131fbca7a8c58e88
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ dependencies {
// required by the unit tests
testImplementation(kotlin("test-junit5"))
testImplementation("io.mockk:mockk:1.13.12")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
// required by IntelliJ test framework
testImplementation("junit:junit:4.13.2")

Expand Down
68 changes: 24 additions & 44 deletions src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.coder.gateway.settings

import com.coder.gateway.cli.gpg.VerificationResult
import com.coder.gateway.util.Arch
import com.coder.gateway.util.OS
import com.coder.gateway.util.expand
Expand All @@ -9,7 +8,6 @@ import com.coder.gateway.util.getOS
import com.coder.gateway.util.safeHost
import com.coder.gateway.util.toURL
import com.coder.gateway.util.withPath
import com.github.weisj.jsvg.B
import com.intellij.openapi.diagnostic.Logger
import java.net.URL
import java.nio.file.Files
Expand Down Expand Up @@ -168,6 +166,11 @@ open class CoderSettings(
val fallbackOnCoderForSignatures: Boolean
get() = state.fallbackOnCoderForSignatures

/**
* Default CLI binary name based on OS and architecture
*/
val defaultCliBinaryNameByOsAndArch: String get() = getCoderCLIForOS(getOS(), getArch())

/**
* Default CLI signature name based on OS and architecture
*/
Expand Down Expand Up @@ -281,9 +284,8 @@ open class CoderSettings(
*/
fun binSource(url: URL): URL {
state.binarySource.let {
val binaryName = getCoderCLIForOS(getOS(), getArch())
return if (it.isBlank()) {
url.withPath("/bin/$binaryName")
url.withPath("/bin/$defaultCliBinaryNameByOsAndArch")
} else {
logger.info("Using binary source override $it")
try {
Expand Down Expand Up @@ -393,44 +395,19 @@ open class CoderSettings(
}

/**
* Return the name of the binary (with extension) for the provided OS and architecture.
* Returns the name of the binary (with extension) for the provided OS and architecture.
*/
private fun getCoderCLIForOS(
os: OS?,
arch: Arch?,
): String {
private fun getCoderCLIForOS(os: OS?, arch: Arch?): String {
logger.debug("Resolving binary for $os $arch")
return buildCoderFileName(os, arch)
}

/**
* Return the name of the signature file (.asc) for the provided OS and architecture.
*/
private fun getCoderSignatureForOS(
os: OS?,
arch: Arch?,
): String {
logger.debug("Resolving signature for $os $arch")
return buildCoderFileName(os, arch, true)
}

/**
* Build the coder file name based on OS, architecture, and whether it's a signature file.
*/
private fun buildCoderFileName(
os: OS?,
arch: Arch?,
isSignature: Boolean = false
): String {
if (os == null) {
logger.error("Could not resolve client OS and architecture, defaulting to WINDOWS AMD64")
return if (isSignature) "coder-windows-amd64.asc" else "coder-windows-amd64.exe"
}

val osName = when (os) {
OS.WINDOWS -> "windows"
OS.LINUX -> "linux"
OS.MAC -> "darwin"
val (osName, extension) = when (os) {
OS.WINDOWS -> "windows" to ".exe"
OS.LINUX -> "linux" to ""
OS.MAC -> "darwin" to ""
null -> {
logger.error("Could not resolve client OS and architecture, defaulting to WINDOWS AMD64")
return "coder-windows-amd64.exe"
}
}

val archName = when (arch) {
Expand All @@ -440,14 +417,17 @@ open class CoderSettings(
else -> "amd64" // default fallback
}

val extension = if (isSignature) ".asc" else when (os) {
OS.WINDOWS -> ".exe"
OS.LINUX, OS.MAC -> ""
}

return "coder-$osName-$archName$extension"
}

/**
* Returns the name of the signature file (.asc) for the provided OS and architecture.
*/
private fun getCoderSignatureForOS(os: OS?, arch: Arch?): String {
logger.debug("Resolving signature for $os $arch")
return "${getCoderCLIForOS(os, arch)}.asc"
}

companion object {
val logger = Logger.getInstance(CoderSettings::class.java.simpleName)
}
Expand Down
11 changes: 6 additions & 5 deletions src/main/kotlin/com/coder/gateway/util/OS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import java.util.Locale

fun getOS(): OS? = OS.from(System.getProperty("os.name"))

fun getArch(): Arch? = Arch.from(System.getProperty("os.arch").lowercase(Locale.getDefault()))
fun getArch(): Arch? = Arch.from(System.getProperty("os.arch")?.lowercase(Locale.getDefault()))

enum class OS {
WINDOWS,
LINUX,
MAC,
;
MAC;

companion object {
fun from(os: String): OS? = when {
fun from(os: String?): OS? = when {
os.isNullOrBlank() -> null
os.contains("win", true) -> {
WINDOWS
}
Expand All @@ -38,7 +38,8 @@ enum class Arch {
;

companion object {
fun from(arch: String): Arch? = when {
fun from(arch: String?): Arch? = when {
arch.isNullOrBlank() -> null
arch.contains("amd64", true) || arch.contains("x86_64", true) -> AMD64
arch.contains("arm64", true) || arch.contains("aarch64", true) -> ARM64
arch.contains("armv7", true) -> ARMV7
Expand Down
142 changes: 107 additions & 35 deletions src/test/kotlin/com/coder/gateway/settings/CoderSettingsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,36 @@ package com.coder.gateway.settings
import com.coder.gateway.util.OS
import com.coder.gateway.util.getOS
import com.coder.gateway.util.withPath
import org.junit.jupiter.api.Assertions
import java.net.URL
import java.nio.file.Path
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

internal class CoderSettingsTest {
private var originalOsName: String? = null
private var originalOsArch: String? = null

private lateinit var store: CoderSettings

@BeforeTest
fun setUp() {
originalOsName = System.getProperty("os.name")
originalOsArch = System.getProperty("os.arch")
store = CoderSettings(CoderSettingsState())
System.setProperty("intellij.testFramework.rethrow.logged.errors", "false")
}

@AfterTest
fun tearDown() {
System.setProperty("os.name", originalOsName)
System.setProperty("os.arch", originalOsArch)
}

@Test
fun testExpands() {
val state = CoderSettingsState()
Expand All @@ -35,13 +57,13 @@ internal class CoderSettingsTest {
CoderSettings(
state,
env =
Environment(
mapOf(
"LOCALAPPDATA" to "/tmp/coder-gateway-test/localappdata",
"HOME" to "/tmp/coder-gateway-test/home",
"XDG_DATA_HOME" to "/tmp/coder-gateway-test/xdg-data",
Environment(
mapOf(
"LOCALAPPDATA" to "/tmp/coder-gateway-test/localappdata",
"HOME" to "/tmp/coder-gateway-test/home",
"XDG_DATA_HOME" to "/tmp/coder-gateway-test/xdg-data",
),
),
),
)
var expected =
when (getOS()) {
Expand All @@ -59,12 +81,12 @@ internal class CoderSettingsTest {
CoderSettings(
state,
env =
Environment(
mapOf(
"XDG_DATA_HOME" to "",
"HOME" to "/tmp/coder-gateway-test/home",
Environment(
mapOf(
"XDG_DATA_HOME" to "",
"HOME" to "/tmp/coder-gateway-test/home",
),
),
),
)
expected = "/tmp/coder-gateway-test/home/.local/share/coder-gateway/localhost"

Expand All @@ -78,13 +100,13 @@ internal class CoderSettingsTest {
CoderSettings(
state,
env =
Environment(
mapOf(
"LOCALAPPDATA" to "/ignore",
"HOME" to "/ignore",
"XDG_DATA_HOME" to "/ignore",
Environment(
mapOf(
"LOCALAPPDATA" to "/ignore",
"HOME" to "/ignore",
"XDG_DATA_HOME" to "/ignore",
),
),
),
)
expected = "/tmp/coder-gateway-test/data-dir/localhost"
assertEquals(Path.of(expected).toAbsolutePath(), settings.dataDir(url))
Expand Down Expand Up @@ -131,13 +153,13 @@ internal class CoderSettingsTest {
CoderSettings(
state,
env =
Environment(
mapOf(
"APPDATA" to "/tmp/coder-gateway-test/cli-appdata",
"HOME" to "/tmp/coder-gateway-test/cli-home",
"XDG_CONFIG_HOME" to "/tmp/coder-gateway-test/cli-xdg-config",
Environment(
mapOf(
"APPDATA" to "/tmp/coder-gateway-test/cli-appdata",
"HOME" to "/tmp/coder-gateway-test/cli-home",
"XDG_CONFIG_HOME" to "/tmp/coder-gateway-test/cli-xdg-config",
),
),
),
)
var expected =
when (getOS()) {
Expand All @@ -153,12 +175,12 @@ internal class CoderSettingsTest {
CoderSettings(
state,
env =
Environment(
mapOf(
"XDG_CONFIG_HOME" to "",
"HOME" to "/tmp/coder-gateway-test/cli-home",
Environment(
mapOf(
"XDG_CONFIG_HOME" to "",
"HOME" to "/tmp/coder-gateway-test/cli-home",
),
),
),
)
expected = "/tmp/coder-gateway-test/cli-home/.config/coderv2"
assertEquals(Path.of(expected), settings.coderConfigDir)
Expand All @@ -169,14 +191,14 @@ internal class CoderSettingsTest {
CoderSettings(
state,
env =
Environment(
mapOf(
"CODER_CONFIG_DIR" to "/tmp/coder-gateway-test/coder-config-dir",
"APPDATA" to "/ignore",
"HOME" to "/ignore",
"XDG_CONFIG_HOME" to "/ignore",
Environment(
mapOf(
"CODER_CONFIG_DIR" to "/tmp/coder-gateway-test/coder-config-dir",
"APPDATA" to "/ignore",
"HOME" to "/ignore",
"XDG_CONFIG_HOME" to "/ignore",
),
),
),
)
expected = "/tmp/coder-gateway-test/coder-config-dir"
assertEquals(Path.of(expected), settings.coderConfigDir)
Expand Down Expand Up @@ -402,4 +424,54 @@ internal class CoderSettingsTest {
assertEquals(true, settings.ignoreSetupFailure)
assertEquals("test ssh log directory", settings.sshLogDirectory)
}


@Test
fun `Default CLI and signature for Windows AMD64`() =
assertBinaryAndSignature("Windows 10", "amd64", "coder-windows-amd64.exe", "coder-windows-amd64.exe.asc")

@Test
fun `Default CLI and signature for Windows ARM64`() =
assertBinaryAndSignature("Windows 10", "aarch64", "coder-windows-arm64.exe", "coder-windows-arm64.exe.asc")

@Test
fun `Default CLI and signature for Linux AMD64`() =
assertBinaryAndSignature("Linux", "x86_64", "coder-linux-amd64", "coder-linux-amd64.asc")

@Test
fun `Default CLI and signature for Linux ARM64`() =
assertBinaryAndSignature("Linux", "aarch64", "coder-linux-arm64", "coder-linux-arm64.asc")

@Test
fun `Default CLI and signature for Linux ARMV7`() =
assertBinaryAndSignature("Linux", "armv7l", "coder-linux-armv7", "coder-linux-armv7.asc")

@Test
fun `Default CLI and signature for Mac AMD64`() =
assertBinaryAndSignature("Mac OS X", "x86_64", "coder-darwin-amd64", "coder-darwin-amd64.asc")

@Test
fun `Default CLI and signature for Mac ARM64`() =
assertBinaryAndSignature("Mac OS X", "aarch64", "coder-darwin-arm64", "coder-darwin-arm64.asc")

@Test
fun `Default CLI and signature for unknown OS and Arch`() =
assertBinaryAndSignature(null, null, "coder-windows-amd64.exe", "coder-windows-amd64.exe.asc")

@Test
fun `Default CLI and signature for unknown Arch fallback on Linux`() =
assertBinaryAndSignature("Linux", "mips64", "coder-linux-amd64", "coder-linux-amd64.asc")

private fun assertBinaryAndSignature(
osName: String?,
arch: String?,
expectedBinary: String,
expectedSignature: String
) {
if (osName == null) System.clearProperty("os.name") else System.setProperty("os.name", osName)
if (arch == null) System.clearProperty("os.arch") else System.setProperty("os.arch", arch)

Assertions.assertEquals(expectedBinary, store.defaultCliBinaryNameByOsAndArch)
Assertions.assertEquals(expectedSignature, store.defaultSignatureNameByOsAndArch)
}
}
Loading