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

Lines changed: 6 additions & 1 deletion
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

Lines changed: 13 additions & 1 deletion
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

Lines changed: 51 additions & 0 deletions
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

Lines changed: 75 additions & 19 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy