Skip to content

Commit 4c37680

Browse files
authored
feat: validate ssh properties before launching workspaces (#96)
1 parent c0a1871 commit 4c37680

File tree

3 files changed

+104
-4
lines changed

3 files changed

+104
-4
lines changed

src/remote.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import * as semver from "semver"
2020
import * as vscode from "vscode"
2121
import * as ws from "ws"
2222
import { SSHConfig, SSHValues, defaultSSHConfigResponse, mergeSSHConfigValues } from "./sshConfig"
23-
import { sshSupportsSetEnv } from "./sshSupport"
23+
import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"
2424
import { Storage } from "./storage"
2525

2626
export class Remote {
@@ -411,7 +411,7 @@ export class Remote {
411411
//
412412
// If we didn't write to the SSH config file, connecting would fail with
413413
// "Host not found".
414-
await this.updateSSHConfig()
414+
await this.updateSSHConfig(authorityParts[1])
415415

416416
this.findSSHProcessID().then((pid) => {
417417
if (!pid) {
@@ -440,7 +440,7 @@ export class Remote {
440440

441441
// updateSSHConfig updates the SSH configuration with a wildcard that handles
442442
// all Coder entries.
443-
private async updateSSHConfig() {
443+
private async updateSSHConfig(hostName: string) {
444444
let deploymentSSHConfig = defaultSSHConfigResponse
445445
try {
446446
const deploymentConfig = await getDeploymentSSHConfig()
@@ -528,6 +528,34 @@ export class Remote {
528528
}
529529

530530
await sshConfig.update(sshValues, sshConfigOverrides)
531+
532+
// A user can provide a "Host *" entry in their SSH config to add options
533+
// to all hosts. We need to ensure that the options we set are not
534+
// overridden by the user's config.
535+
const computedProperties = computeSSHProperties(hostName, sshConfig.getRaw())
536+
const keysToMatch: Array<keyof SSHValues> = ["ProxyCommand", "UserKnownHostsFile", "StrictHostKeyChecking"]
537+
for (let i = 0; i < keysToMatch.length; i++) {
538+
const key = keysToMatch[i]
539+
if (computedProperties[key] === sshValues[key]) {
540+
continue
541+
}
542+
543+
const result = await this.vscodeProposed.window.showErrorMessage(
544+
"Unexpected SSH Config Option",
545+
{
546+
useCustom: true,
547+
modal: true,
548+
detail: `Your SSH config is overriding the "${key}" property to "${computedProperties[key]}" when it expected "${sshValues[key]}" for the "${hostName}" host. Please fix this and try again!`,
549+
},
550+
"Reload Window",
551+
)
552+
if (result === "Reload Window") {
553+
await this.reloadWindow()
554+
}
555+
await this.closeRemote()
556+
}
557+
558+
return sshConfig.getRaw()
531559
}
532560

533561
// showNetworkUpdates finds the SSH process ID that is being used by this

src/sshSupport.test.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { it, expect } from "vitest"
2-
import { sshSupportsSetEnv, sshVersionSupportsSetEnv } from "./sshSupport"
2+
import { computeSSHProperties, sshSupportsSetEnv, sshVersionSupportsSetEnv } from "./sshSupport"
33

44
const supports = {
55
"OpenSSH_8.9p1 Ubuntu-3ubuntu0.1, OpenSSL 3.0.2 15 Mar 2022": true,
@@ -17,3 +17,23 @@ Object.entries(supports).forEach(([version, expected]) => {
1717
it("current shell supports ssh", () => {
1818
expect(sshSupportsSetEnv()).toBeTruthy()
1919
})
20+
21+
it("computes the config for a host", () => {
22+
const properties = computeSSHProperties(
23+
"coder-vscode--testing",
24+
`Host *
25+
StrictHostKeyChecking yes
26+
27+
# --- START CODER VSCODE ---
28+
Host coder-vscode--*
29+
StrictHostKeyChecking no
30+
Another=true
31+
# --- END CODER VSCODE ---
32+
`,
33+
)
34+
35+
expect(properties).toEqual({
36+
Another: "true",
37+
StrictHostKeyChecking: "yes",
38+
})
39+
})

src/sshSupport.ts

+52
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,55 @@ export function sshVersionSupportsSetEnv(sshVersionString: string): boolean {
3636
}
3737
return false
3838
}
39+
40+
// computeSSHProperties accepts an SSH config and a host name and returns
41+
// the properties that should be set for that host.
42+
export function computeSSHProperties(host: string, config: string): Record<string, string> {
43+
let currentConfig:
44+
| {
45+
Host: string
46+
properties: Record<string, string>
47+
}
48+
| undefined
49+
const configs: Array<typeof currentConfig> = []
50+
config.split("\n").forEach((line) => {
51+
line = line.trim()
52+
if (line === "") {
53+
return
54+
}
55+
const [key, ...valueParts] = line.split(/\s+|=/)
56+
if (key.startsWith("#")) {
57+
// Ignore comments!
58+
return
59+
}
60+
if (key === "Host") {
61+
if (currentConfig) {
62+
configs.push(currentConfig)
63+
}
64+
currentConfig = {
65+
Host: valueParts.join(" "),
66+
properties: {},
67+
}
68+
return
69+
}
70+
if (!currentConfig) {
71+
return
72+
}
73+
currentConfig.properties[key] = valueParts.join(" ")
74+
})
75+
if (currentConfig) {
76+
configs.push(currentConfig)
77+
}
78+
79+
const merged: Record<string, string> = {}
80+
configs.reverse().forEach((config) => {
81+
if (!config) {
82+
return
83+
}
84+
if (!new RegExp("^" + config?.Host.replace(/\*/g, ".*") + "$").test(host)) {
85+
return
86+
}
87+
Object.assign(merged, config.properties)
88+
})
89+
return merged
90+
}

0 commit comments

Comments
 (0)