Skip to content

Commit a53b8db

Browse files
authored
feat: add handling for insecure requests (#106)
1 parent 9b936cd commit a53b8db

File tree

7 files changed

+148
-23
lines changed

7 files changed

+148
-23
lines changed

package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@
4242
},
4343
"scope": "machine",
4444
"default": []
45+
},
46+
"coder.insecure": {
47+
"markdownDescription": "If true, the extension will not verify the authenticity of the remote host. This is useful for self-signed certificates.",
48+
"type": "boolean",
49+
"default": false
4550
}
4651
}
4752
},
@@ -241,4 +246,4 @@
241246
"yaml": "^1.10.0",
242247
"zod": "^3.21.4"
243248
}
244-
}
249+
}

src/commands.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getAuthenticatedUser, getWorkspaces, updateWorkspaceVersion } from "cod
33
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
44
import * as vscode from "vscode"
55
import { extractAgents } from "./api-helper"
6+
import { SelfSignedCertificateError } from "./error"
67
import { Remote } from "./remote"
78
import { Storage } from "./storage"
89
import { OpenableTreeItem } from "./workspacesProvider"
@@ -61,6 +62,14 @@ export class Commands {
6162
if (axios.isAxiosError(err) && err.response?.data) {
6263
message = err.response.data.detail
6364
}
65+
if (err instanceof SelfSignedCertificateError) {
66+
err.showInsecureNotification(this.storage)
67+
68+
return {
69+
message: err.message,
70+
severity: vscode.InputBoxValidationSeverity.Error,
71+
}
72+
}
6473
return {
6574
message: "Invalid session token! (" + message + ")",
6675
severity: vscode.InputBoxValidationSeverity.Error,
@@ -189,7 +198,10 @@ export class Commands {
189198
quickPick.items = items
190199
quickPick.busy = false
191200
})
192-
.catch(() => {
201+
.catch((ex) => {
202+
if (ex instanceof SelfSignedCertificateError) {
203+
ex.showInsecureNotification(this.storage)
204+
}
193205
return
194206
})
195207
})

src/error.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as fs from "fs/promises"
2+
import * as jsonc from "jsonc-parser"
3+
import * as vscode from "vscode"
4+
import { Storage } from "./storage"
5+
6+
export class SelfSignedCertificateError extends Error {
7+
public static Notification =
8+
"Your Coder deployment is using a self-signed certificate. VS Code uses a version of Electron that does not support registering self-signed intermediate certificates with extensions."
9+
public static ActionAllowInsecure = "Allow Insecure"
10+
public static ActionViewMoreDetails = "View More Details"
11+
12+
constructor(message: string) {
13+
super(`Your Coder deployment is using a self-signed certificate: ${message}`)
14+
}
15+
16+
public viewMoreDetails(): Thenable<boolean> {
17+
return vscode.env.openExternal(vscode.Uri.parse("https://github.com/coder/vscode-coder/issues/105"))
18+
}
19+
20+
// allowInsecure manually reads the settings file and updates the value of the
21+
// "coder.insecure" property.
22+
public async allowInsecure(storage: Storage): Promise<void> {
23+
let settingsContent = "{}"
24+
try {
25+
settingsContent = await fs.readFile(storage.getUserSettingsPath(), "utf8")
26+
} catch (ex) {
27+
// Ignore! It's probably because the file doesn't exist.
28+
}
29+
const edits = jsonc.modify(settingsContent, ["coder.insecure"], true, {})
30+
await fs.writeFile(storage.getUserSettingsPath(), jsonc.applyEdits(settingsContent, edits))
31+
32+
vscode.window.showInformationMessage(
33+
'The Coder extension will no longer verify TLS on HTTPS requests. You can change this at any time with the "coder.insecure" property in your VS Code settings.',
34+
)
35+
}
36+
37+
public async showInsecureNotification(storage: Storage): Promise<void> {
38+
const value = await vscode.window.showErrorMessage(
39+
SelfSignedCertificateError.Notification,
40+
SelfSignedCertificateError.ActionAllowInsecure,
41+
SelfSignedCertificateError.ActionViewMoreDetails,
42+
)
43+
if (value === SelfSignedCertificateError.ActionViewMoreDetails) {
44+
await this.viewMoreDetails()
45+
return
46+
}
47+
if (value === SelfSignedCertificateError.ActionAllowInsecure) {
48+
return this.allowInsecure(storage)
49+
}
50+
}
51+
}

src/extension.ts

+75-19
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,67 @@
11
"use strict"
2+
import axios from "axios"
23
import { getAuthenticatedUser } from "coder/site/src/api/api"
4+
import * as https from "https"
35
import * as module from "module"
46
import * as vscode from "vscode"
57
import { Commands } from "./commands"
8+
import { SelfSignedCertificateError } from "./error"
69
import { Remote } from "./remote"
710
import { Storage } from "./storage"
811
import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider"
912

1013
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
14+
// The Remote SSH extension's proposed APIs are used to override
15+
// the SSH host name in VS Code itself. It's visually unappealing
16+
// having a lengthy name!
17+
//
18+
// This is janky, but that's alright since it provides such minimal
19+
// functionality to the extension.
20+
const remoteSSHExtension = vscode.extensions.getExtension("ms-vscode-remote.remote-ssh")
21+
if (!remoteSSHExtension) {
22+
throw new Error("Remote SSH extension not found")
23+
}
24+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25+
const vscodeProposed: typeof vscode = (module as any)._load(
26+
"vscode",
27+
{
28+
filename: remoteSSHExtension?.extensionPath,
29+
},
30+
false,
31+
)
32+
33+
// updateInsecure is called on extension activation and when the insecure
34+
// setting is changed. It updates the https agent to allow self-signed
35+
// certificates if the insecure setting is true.
36+
const applyInsecure = () => {
37+
const insecure = Boolean(vscode.workspace.getConfiguration().get("coder.insecure"))
38+
39+
axios.defaults.httpsAgent = new https.Agent({
40+
// rejectUnauthorized defaults to true, so we need to explicitly set it to false
41+
// if we want to allow self-signed certificates.
42+
rejectUnauthorized: !insecure,
43+
})
44+
}
45+
46+
axios.interceptors.response.use(
47+
(r) => r,
48+
(err) => {
49+
if (err) {
50+
const msg = err.toString() as string
51+
if (msg.indexOf("unable to verify the first certificate") !== -1) {
52+
throw new SelfSignedCertificateError(msg)
53+
}
54+
}
55+
56+
throw err
57+
},
58+
)
59+
60+
vscode.workspace.onDidChangeConfiguration((e) => {
61+
e.affectsConfiguration("coder.insecure") && applyInsecure()
62+
})
63+
applyInsecure()
64+
1165
const output = vscode.window.createOutputChannel("Coder")
1266
const storage = new Storage(output, ctx.globalState, ctx.secrets, ctx.globalStorageUri, ctx.logUri)
1367
await storage.init()
@@ -62,25 +116,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
62116
},
63117
})
64118

65-
// The Remote SSH extension's proposed APIs are used to override
66-
// the SSH host name in VS Code itself. It's visually unappealing
67-
// having a lengthy name!
68-
//
69-
// This is janky, but that's alright since it provides such minimal
70-
// functionality to the extension.
71-
const remoteSSHExtension = vscode.extensions.getExtension("ms-vscode-remote.remote-ssh")
72-
if (!remoteSSHExtension) {
73-
throw new Error("Remote SSH extension not found")
74-
}
75-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
76-
const vscodeProposed: typeof vscode = (module as any)._load(
77-
"vscode",
78-
{
79-
filename: remoteSSHExtension?.extensionPath,
80-
},
81-
false,
82-
)
83-
84119
const commands = new Commands(vscodeProposed, storage)
85120

86121
vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
@@ -109,6 +144,27 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
109144
try {
110145
await remote.setup(vscodeProposed.env.remoteAuthority)
111146
} catch (ex) {
147+
if (ex instanceof SelfSignedCertificateError) {
148+
const prompt = await vscodeProposed.window.showErrorMessage(
149+
"Failed to open workspace",
150+
{
151+
detail: SelfSignedCertificateError.Notification,
152+
modal: true,
153+
useCustom: true,
154+
},
155+
SelfSignedCertificateError.ActionAllowInsecure,
156+
SelfSignedCertificateError.ActionViewMoreDetails,
157+
)
158+
if (prompt === SelfSignedCertificateError.ActionAllowInsecure) {
159+
await ex.allowInsecure(storage)
160+
await remote.reloadWindow()
161+
return
162+
}
163+
if (prompt === SelfSignedCertificateError.ActionViewMoreDetails) {
164+
await ex.viewMoreDetails()
165+
return
166+
}
167+
}
112168
await vscodeProposed.window.showErrorMessage("Failed to open workspace", {
113169
detail: (ex as string).toString(),
114170
modal: true,

src/remote.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ export class Remote {
696696
}
697697

698698
// reloadWindow reloads the current window.
699-
private async reloadWindow() {
699+
public async reloadWindow() {
700700
await vscode.commands.executeCommand("workbench.action.reloadWindow")
701701
}
702702

src/sshSupport.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { computeSSHProperties, sshSupportsSetEnv, sshVersionSupportsSetEnv } fro
33

44
const supports = {
55
"OpenSSH_8.9p1 Ubuntu-3ubuntu0.1, OpenSSL 3.0.2 15 Mar 2022": true,
6+
"OpenSSH_for_Windows_8.1p1, LibreSSL 3.0.2": true,
67
"OpenSSH_9.0p1, LibreSSL 3.3.6": true,
78
"OpenSSH_7.6p1 Ubuntu-4ubuntu0.7, OpenSSL 1.0.2n 7 Dec 2017": false,
89
"OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017": false,

src/sshSupport.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function sshSupportsSetEnv(): boolean {
1616
//
1717
// It was introduced in SSH 7.8 and not all versions support it.
1818
export function sshVersionSupportsSetEnv(sshVersionString: string): boolean {
19-
const match = sshVersionString.match(/OpenSSH_([\d.]+)[^,]*/)
19+
const match = sshVersionString.match(/OpenSSH.*_([\d.]+)[^,]*/)
2020
if (match && match[1]) {
2121
const installedVersion = match[1]
2222
const parts = installedVersion.split(".")

0 commit comments

Comments
 (0)