diff --git a/src/remote.ts b/src/remote.ts index dfdd54ee..af8c039c 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -20,7 +20,7 @@ import * as semver from "semver" import * as vscode from "vscode" import * as ws from "ws" import { SSHConfig, SSHValues, defaultSSHConfigResponse, mergeSSHConfigValues } from "./sshConfig" -import { sshSupportsSetEnv } from "./sshSupport" +import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport" import { Storage } from "./storage" export class Remote { @@ -411,7 +411,7 @@ export class Remote { // // If we didn't write to the SSH config file, connecting would fail with // "Host not found". - await this.updateSSHConfig() + await this.updateSSHConfig(authorityParts[1]) this.findSSHProcessID().then((pid) => { if (!pid) { @@ -440,7 +440,7 @@ export class Remote { // updateSSHConfig updates the SSH configuration with a wildcard that handles // all Coder entries. - private async updateSSHConfig() { + private async updateSSHConfig(hostName: string) { let deploymentSSHConfig = defaultSSHConfigResponse try { const deploymentConfig = await getDeploymentSSHConfig() @@ -528,6 +528,34 @@ export class Remote { } await sshConfig.update(sshValues, sshConfigOverrides) + + // A user can provide a "Host *" entry in their SSH config to add options + // to all hosts. We need to ensure that the options we set are not + // overridden by the user's config. + const computedProperties = computeSSHProperties(hostName, sshConfig.getRaw()) + const keysToMatch: Array = ["ProxyCommand", "UserKnownHostsFile", "StrictHostKeyChecking"] + for (let i = 0; i < keysToMatch.length; i++) { + const key = keysToMatch[i] + if (computedProperties[key] === sshValues[key]) { + continue + } + + const result = await this.vscodeProposed.window.showErrorMessage( + "Unexpected SSH Config Option", + { + useCustom: true, + modal: true, + 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!`, + }, + "Reload Window", + ) + if (result === "Reload Window") { + await this.reloadWindow() + } + await this.closeRemote() + } + + return sshConfig.getRaw() } // showNetworkUpdates finds the SSH process ID that is being used by this diff --git a/src/sshSupport.test.ts b/src/sshSupport.test.ts index 4761f6ec..dbcde14f 100644 --- a/src/sshSupport.test.ts +++ b/src/sshSupport.test.ts @@ -1,5 +1,5 @@ import { it, expect } from "vitest" -import { sshSupportsSetEnv, sshVersionSupportsSetEnv } from "./sshSupport" +import { computeSSHProperties, sshSupportsSetEnv, sshVersionSupportsSetEnv } from "./sshSupport" const supports = { "OpenSSH_8.9p1 Ubuntu-3ubuntu0.1, OpenSSL 3.0.2 15 Mar 2022": true, @@ -17,3 +17,23 @@ Object.entries(supports).forEach(([version, expected]) => { it("current shell supports ssh", () => { expect(sshSupportsSetEnv()).toBeTruthy() }) + +it("computes the config for a host", () => { + const properties = computeSSHProperties( + "coder-vscode--testing", + `Host * + StrictHostKeyChecking yes + +# --- START CODER VSCODE --- +Host coder-vscode--* + StrictHostKeyChecking no + Another=true +# --- END CODER VSCODE --- +`, + ) + + expect(properties).toEqual({ + Another: "true", + StrictHostKeyChecking: "yes", + }) +}) diff --git a/src/sshSupport.ts b/src/sshSupport.ts index 2121adad..5726070a 100644 --- a/src/sshSupport.ts +++ b/src/sshSupport.ts @@ -36,3 +36,55 @@ export function sshVersionSupportsSetEnv(sshVersionString: string): boolean { } return false } + +// computeSSHProperties accepts an SSH config and a host name and returns +// the properties that should be set for that host. +export function computeSSHProperties(host: string, config: string): Record { + let currentConfig: + | { + Host: string + properties: Record + } + | undefined + const configs: Array = [] + config.split("\n").forEach((line) => { + line = line.trim() + if (line === "") { + return + } + const [key, ...valueParts] = line.split(/\s+|=/) + if (key.startsWith("#")) { + // Ignore comments! + return + } + if (key === "Host") { + if (currentConfig) { + configs.push(currentConfig) + } + currentConfig = { + Host: valueParts.join(" "), + properties: {}, + } + return + } + if (!currentConfig) { + return + } + currentConfig.properties[key] = valueParts.join(" ") + }) + if (currentConfig) { + configs.push(currentConfig) + } + + const merged: Record = {} + configs.reverse().forEach((config) => { + if (!config) { + return + } + if (!new RegExp("^" + config?.Host.replace(/\*/g, ".*") + "$").test(host)) { + return + } + Object.assign(merged, config.properties) + }) + return merged +}