diff --git a/src/remote.ts b/src/remote.ts index 2f2678cd..fbc31a51 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -19,7 +19,8 @@ import prettyBytes from "pretty-bytes" import * as semver from "semver" import * as vscode from "vscode" import * as ws from "ws" -import { SSHConfig, defaultSSHConfigResponse, mergeSSHConfigValues } from "./sshConfig" +import { SSHConfig, SSHValues, defaultSSHConfigResponse, mergeSSHConfigValues } from "./sshConfig" +import { sshSupportsSetEnv } from "./sshSupport" import { Storage } from "./storage" export class Remote { @@ -509,7 +510,7 @@ export class Remote { } const escape = (str: string): string => `"${str.replace(/"/g, '\\"')}"` - const sshValues = { + const sshValues: SSHValues = { Host: `${Remote.Prefix}*`, ProxyCommand: `${escape(binaryPath)} vscodessh --network-info-dir ${escape( this.storage.getNetworkInfoPath(), @@ -520,9 +521,11 @@ export class Remote { StrictHostKeyChecking: "no", UserKnownHostsFile: "/dev/null", LogLevel: "ERROR", + } + if (sshSupportsSetEnv()) { // This allows for tracking the number of extension // users connected to workspaces! - SetEnv: "CODER_SSH_SESSION_TYPE=vscode", + sshValues.SetEnv = " CODER_SSH_SESSION_TYPE=vscode" } await sshConfig.update(sshValues, sshConfigOverrides) diff --git a/src/sshConfig.ts b/src/sshConfig.ts index 63b55f51..7430659e 100644 --- a/src/sshConfig.ts +++ b/src/sshConfig.ts @@ -1,5 +1,4 @@ -import { SSHConfigResponse } from "coder/site/src/api/typesGenerated" -import { writeFile, readFile } from "fs/promises" +import { readFile, writeFile } from "fs/promises" import { ensureDir } from "fs-extra" import path from "path" @@ -9,13 +8,14 @@ interface Block { raw: string } -interface SSHValues { +export interface SSHValues { Host: string ProxyCommand: string ConnectTimeout: string StrictHostKeyChecking: string UserKnownHostsFile: string LogLevel: string + SetEnv?: string } // Interface for the file system to make it easier to test diff --git a/src/sshSupport.test.ts b/src/sshSupport.test.ts new file mode 100644 index 00000000..f4db7831 --- /dev/null +++ b/src/sshSupport.test.ts @@ -0,0 +1,18 @@ +import { it, expect } from "vitest" +import { sshSupportsSetEnv, sshVersionSupportsSetEnv } from "./sshSupport" + +const supports = { + "OpenSSH_8.9p1 Ubuntu-3ubuntu0.1, OpenSSL 3.0.2 15 Mar 2022": true, + "OpenSSH_7.6p1 Ubuntu-4ubuntu0.7, OpenSSL 1.0.2n 7 Dec 2017": false, + "OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017": false, +} + +Object.entries(supports).forEach(([version, expected]) => { + it(version, () => { + expect(sshVersionSupportsSetEnv(version)).toBe(expected) + }) +}) + +it("current shell supports ssh", () => { + expect(sshSupportsSetEnv()).toBeTruthy() +}) diff --git a/src/sshSupport.ts b/src/sshSupport.ts new file mode 100644 index 00000000..0100b04f --- /dev/null +++ b/src/sshSupport.ts @@ -0,0 +1,36 @@ +import * as childProcess from "child_process" + +export function sshSupportsSetEnv(): boolean { + try { + // Run `ssh -V` to get the version string. + const spawned = childProcess.spawnSync("ssh", ["-V"]) + // The version string outputs to stderr. + return sshVersionSupportsSetEnv(spawned.stderr.toString().trim()) + } catch (error) { + return false + } +} + +// sshVersionSupportsSetEnv ensures that the version string from the SSH +// command line supports the `SetEnv` directive. +// +// It was introduced in SSH 7.8 and not all versions support it. +export function sshVersionSupportsSetEnv(sshVersionString: string): boolean { + const match = sshVersionString.match(/OpenSSH_([\d.]+)[^,]*/) + if (match && match[1]) { + const installedVersion = match[1] + const parts = installedVersion.split(".") + if (parts.length < 2) { + return false + } + // 7.8 is the first version that supports SetEnv + if (Number.parseInt(parts[0], 10) < 7) { + return false + } + if (Number.parseInt(parts[1], 10) < 8) { + return false + } + return true + } + return false +}