Skip to content

Commit 5ba3f76

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/vscode/test-electron-2.4.1
2 parents 650f5c8 + 71f5ba5 commit 5ba3f76

13 files changed

+309
-80
lines changed

CHANGELOG.md

+34
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,46 @@
22

33
## Unreleased
44

5+
## [v1.4.0](https://github.com/coder/vscode-coder/releases/tag/v1.3.9) (2025-02-04)
6+
7+
- Recreate REST client after starting a workspace to ensure fresh TLS certificates.
8+
- Use `coder ssh` subcommand in place of `coder vscodessh`.
9+
10+
## [v1.3.10](https://github.com/coder/vscode-coder/releases/tag/v1.3.9) (2025-01-17)
11+
12+
- Fix bug where checking for overridden properties incorrectly converted host name pattern to regular expression.
13+
14+
## [v1.3.9](https://github.com/coder/vscode-coder/releases/tag/v1.3.9) (2024-12-12)
15+
16+
- Only show a login failure dialog for explicit logins (and not autologins).
17+
18+
## [v1.3.8](https://github.com/coder/vscode-coder/releases/tag/v1.3.8) (2024-12-06)
19+
20+
- When starting a workspace, shell out to the Coder binary instead of making an
21+
API call. This reduces drift between what the plugin does and the CLI does. As
22+
part of this, the `session_token` file was renamed to `session` since that is
23+
what the CLI expects.
24+
25+
## [v1.3.7](https://github.com/coder/vscode-coder/releases/tag/v1.3.7) (2024-11-04)
26+
27+
### Added
28+
29+
- New setting `coder.tlsAltHost` to configure an alternative hostname to use for
30+
TLS verification. This is useful when the hostname in the certificate does not
31+
match the hostname used to connect.
32+
33+
## [v1.3.6](https://github.com/coder/vscode-coder/releases/tag/v1.3.6) (2024-11-04)
34+
535
### Added
636

737
- Default URL setting that takes precedence over CODER_URL.
838
- Autologin setting that automatically initiates login when the extension
939
activates using either the default URL or CODER_URL.
1040

41+
### Changed
42+
43+
- When a client certificate and/or key is configured, skip token authentication.
44+
1145
## [v1.3.5](https://github.com/coder/vscode-coder/releases/tag/v1.3.5) (2024-10-16)
1246

1347
### Fixed

CONTRIBUTING.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ contains the `coder-vscode` prefix, and if so we delay activation to:
3434

3535
```text
3636
Host coder-vscode.dev.coder.com--*
37-
ProxyCommand "/tmp/coder" vscodessh --network-info-dir "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/net" --session-token-file "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com/session_token" --url-file "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com/url" %h
37+
ProxyCommand "/tmp/coder" --global-config "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com" ssh --stdio --network-info-dir "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/net" --ssh-host-prefix coder-vscode.dev.coder.com-- %h
3838
ConnectTimeout 0
3939
StrictHostKeyChecking no
4040
UserKnownHostsFile /dev/null
@@ -50,8 +50,8 @@ specified port. This port is printed to the `Remote - SSH` log file in the VS
5050
Code Output panel in the format `-> socksPort <port> ->`. We use this port to
5151
find the SSH process ID that is being used by the remote session.
5252

53-
The `vscodessh` subcommand on the `coder` binary periodically flushes its
54-
network information to `network-info-dir + "/" + process.ppid`. SSH executes
53+
The `ssh` subcommand on the `coder` binary periodically flushes its network
54+
information to `network-info-dir + "/" + process.ppid`. SSH executes
5555
`ProxyCommand`, which means the `process.ppid` will always be the matching SSH
5656
command.
5757

package.json

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"displayName": "Coder",
55
"description": "Open any workspace with a single click.",
66
"repository": "https://github.com/coder/vscode-coder",
7-
"version": "1.3.5",
7+
"version": "1.4.0",
88
"engines": {
99
"vscode": "^1.73.0"
1010
},
@@ -74,17 +74,22 @@
7474
"default": ""
7575
},
7676
"coder.tlsCertFile": {
77-
"markdownDescription": "Path to file for TLS client cert. When specified, token authorization will be skipped.",
77+
"markdownDescription": "Path to file for TLS client cert. When specified, token authorization will be skipped. `http.proxySupport` must be set to `on` or `off`, otherwise VS Code will override the proxy agent set by the plugin.",
7878
"type": "string",
7979
"default": ""
8080
},
8181
"coder.tlsKeyFile": {
82-
"markdownDescription": "Path to file for TLS client key. When specified, token authorization will be skipped.",
82+
"markdownDescription": "Path to file for TLS client key. When specified, token authorization will be skipped. `http.proxySupport` must be set to `on` or `off`, otherwise VS Code will override the proxy agent set by the plugin.",
8383
"type": "string",
8484
"default": ""
8585
},
8686
"coder.tlsCaFile": {
87-
"markdownDescription": "Path to file for TLS certificate authority.",
87+
"markdownDescription": "Path to file for TLS certificate authority. `http.proxySupport` must be set to `on` or `off`, otherwise VS Code will override the proxy agent set by the plugin.",
88+
"type": "string",
89+
"default": ""
90+
},
91+
"coder.tlsAltHost": {
92+
"markdownDescription": "Alternative hostname to use for TLS verification. This is useful when the hostname in the certificate does not match the hostname used to connect.",
8893
"type": "string",
8994
"default": ""
9095
},

src/api.ts

+57-16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { spawn } from "child_process"
12
import { Api } from "coder/site/src/api/api"
23
import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated"
34
import fs from "fs/promises"
@@ -31,6 +32,7 @@ async function createHttpAgent(): Promise<ProxyAgent> {
3132
const certFile = expandPath(String(cfg.get("coder.tlsCertFile") ?? "").trim())
3233
const keyFile = expandPath(String(cfg.get("coder.tlsKeyFile") ?? "").trim())
3334
const caFile = expandPath(String(cfg.get("coder.tlsCaFile") ?? "").trim())
35+
const altHost = expandPath(String(cfg.get("coder.tlsAltHost") ?? "").trim())
3436

3537
return new ProxyAgent({
3638
// Called each time a request is made.
@@ -41,6 +43,7 @@ async function createHttpAgent(): Promise<ProxyAgent> {
4143
cert: certFile === "" ? undefined : await fs.readFile(certFile),
4244
key: keyFile === "" ? undefined : await fs.readFile(keyFile),
4345
ca: caFile === "" ? undefined : await fs.readFile(caFile),
46+
servername: altHost === "" ? undefined : altHost,
4447
// rejectUnauthorized defaults to true, so we need to explicitly set it to
4548
// false if we want to allow self-signed certificates.
4649
rejectUnauthorized: !insecure,
@@ -66,7 +69,8 @@ async function getHttpAgent(): Promise<ProxyAgent> {
6669
e.affectsConfiguration("coder.insecure") ||
6770
e.affectsConfiguration("coder.tlsCertFile") ||
6871
e.affectsConfiguration("coder.tlsKeyFile") ||
69-
e.affectsConfiguration("coder.tlsCaFile")
72+
e.affectsConfiguration("coder.tlsCaFile") ||
73+
e.affectsConfiguration("coder.tlsAltHost")
7074
) {
7175
agent = createHttpAgent()
7276
}
@@ -119,29 +123,66 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s
119123
/**
120124
* Start or update a workspace and return the updated workspace.
121125
*/
122-
export async function startWorkspaceIfStoppedOrFailed(restClient: Api, workspace: Workspace): Promise<Workspace> {
123-
// If the workspace requires the latest active template version, we should attempt
124-
// to update that here.
125-
// TODO: If param set changes, what do we do??
126-
const versionID = workspace.template_require_active_version
127-
? // Use the latest template version
128-
workspace.template_active_version_id
129-
: // Default to not updating the workspace if not required.
130-
workspace.latest_build.template_version_id
131-
126+
export async function startWorkspaceIfStoppedOrFailed(
127+
restClient: Api,
128+
globalConfigDir: string,
129+
binPath: string,
130+
workspace: Workspace,
131+
writeEmitter: vscode.EventEmitter<string>,
132+
): Promise<Workspace> {
132133
// Before we start a workspace, we make an initial request to check it's not already started
133134
const updatedWorkspace = await restClient.getWorkspace(workspace.id)
134135

135136
if (!["stopped", "failed"].includes(updatedWorkspace.latest_build.status)) {
136137
return updatedWorkspace
137138
}
138139

139-
const latestBuild = await restClient.startWorkspace(updatedWorkspace.id, versionID)
140+
return new Promise((resolve, reject) => {
141+
const startArgs = [
142+
"--global-config",
143+
globalConfigDir,
144+
"start",
145+
"--yes",
146+
workspace.owner_name + "/" + workspace.name,
147+
]
148+
const startProcess = spawn(binPath, startArgs)
149+
150+
startProcess.stdout.on("data", (data: Buffer) => {
151+
data
152+
.toString()
153+
.split(/\r*\n/)
154+
.forEach((line: string) => {
155+
if (line !== "") {
156+
writeEmitter.fire(line.toString() + "\r\n")
157+
}
158+
})
159+
})
160+
161+
let capturedStderr = ""
162+
startProcess.stderr.on("data", (data: Buffer) => {
163+
data
164+
.toString()
165+
.split(/\r*\n/)
166+
.forEach((line: string) => {
167+
if (line !== "") {
168+
writeEmitter.fire(line.toString() + "\r\n")
169+
capturedStderr += line.toString() + "\n"
170+
}
171+
})
172+
})
140173

141-
return {
142-
...updatedWorkspace,
143-
latest_build: latestBuild,
144-
}
174+
startProcess.on("close", (code: number) => {
175+
if (code === 0) {
176+
resolve(restClient.getWorkspace(workspace.id))
177+
} else {
178+
let errorText = `"${startArgs.join(" ")}" exited with code ${code}`
179+
if (capturedStderr !== "") {
180+
errorText += `: ${capturedStderr}`
181+
}
182+
reject(new Error(errorText))
183+
}
184+
})
185+
})
145186
}
146187

147188
/**

src/commands.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export class Commands {
143143
const inputUrl = args[0]
144144
const inputToken = args[1]
145145
const inputLabel = args[2]
146+
const isAutologin = typeof args[3] === "undefined" ? false : Boolean(args[3])
146147

147148
const url = await this.maybeAskUrl(inputUrl)
148149
if (!url) {
@@ -155,7 +156,7 @@ export class Commands {
155156
const label = typeof inputLabel === "undefined" ? toSafeHost(url) : inputLabel
156157

157158
// Try to get a token from the user, if we need one, and their user.
158-
const res = await this.maybeAskToken(url, inputToken)
159+
const res = await this.maybeAskToken(url, inputToken, isAutologin)
159160
if (!res) {
160161
return // The user aborted, or unable to auth.
161162
}
@@ -202,7 +203,11 @@ export class Commands {
202203
* token. Null means the user aborted or we were unable to authenticate with
203204
* mTLS (in the latter case, an error notification will have been displayed).
204205
*/
205-
private async maybeAskToken(url: string, token: string): Promise<{ user: User; token: string } | null> {
206+
private async maybeAskToken(
207+
url: string,
208+
token: string,
209+
isAutologin: boolean,
210+
): Promise<{ user: User; token: string } | null> {
206211
const restClient = await makeCoderSdk(url, token, this.storage)
207212
if (!needToken()) {
208213
try {
@@ -212,11 +217,15 @@ export class Commands {
212217
return { token: "", user }
213218
} catch (err) {
214219
const message = getErrorMessage(err, "no response from the server")
215-
this.vscodeProposed.window.showErrorMessage("Failed to log in", {
216-
detail: message,
217-
modal: true,
218-
useCustom: true,
219-
})
220+
if (isAutologin) {
221+
this.storage.writeToCoderOutputChannel(`Failed to log in to Coder server: ${message}`)
222+
} else {
223+
this.vscodeProposed.window.showErrorMessage("Failed to log in to Coder server", {
224+
detail: message,
225+
modal: true,
226+
useCustom: true,
227+
})
228+
}
220229
// Invalid certificate, most likely.
221230
return null
222231
}
@@ -529,7 +538,7 @@ async function openWorkspace(
529538
// when opening a workspace unless explicitly specified.
530539
let remoteAuthority = `ssh-remote+${AuthorityPrefix}.${toSafeHost(baseUrl)}--${workspaceOwner}--${workspaceName}`
531540
if (workspaceAgent) {
532-
remoteAuthority += `--${workspaceAgent}`
541+
remoteAuthority += `.${workspaceAgent}`
533542
}
534543

535544
let newWindow = true

src/extension.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
221221
if (cfg.get("coder.autologin") === true) {
222222
const defaultUrl = cfg.get("coder.defaultUrl") || process.env.CODER_URL
223223
if (defaultUrl) {
224-
vscode.commands.executeCommand("coder.login", defaultUrl)
224+
vscode.commands.executeCommand("coder.login", defaultUrl, undefined, undefined, "true")
225225
}
226226
}
227227
}

src/featureSet.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as semver from "semver"
33
export type FeatureSet = {
44
vscodessh: boolean
55
proxyLogDirectory: boolean
6+
wildcardSSH: boolean
67
}
78

89
/**
@@ -21,5 +22,6 @@ export function featureSetForVersion(version: semver.SemVer | null): FeatureSet
2122
// If this check didn't exist, VS Code connections would fail on
2223
// older versions because of an unknown CLI argument.
2324
proxyLogDirectory: (version?.compare("2.3.3") || 0) > 0 || version?.prerelease[0] === "devel",
25+
wildcardSSH: (version?.compare("2.19.0") || 0) > 0 || version?.prerelease[0] === "devel",
2426
}
2527
}

0 commit comments

Comments
 (0)