Skip to content

feat: add handling for insecure requests #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
},
"scope": "machine",
"default": []
},
"coder.insecure": {
"markdownDescription": "If true, the extension will not verify the authenticity of the remote host. This is useful for self-signed certificates.",
"type": "boolean",
"default": false
}
}
},
Expand Down Expand Up @@ -241,4 +246,4 @@
"yaml": "^1.10.0",
"zod": "^3.21.4"
}
}
}
14 changes: 13 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getAuthenticatedUser, getWorkspaces, updateWorkspaceVersion } from "cod
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
import * as vscode from "vscode"
import { extractAgents } from "./api-helper"
import { SelfSignedCertificateError } from "./error"
import { Remote } from "./remote"
import { Storage } from "./storage"
import { OpenableTreeItem } from "./workspacesProvider"
Expand Down Expand Up @@ -61,6 +62,14 @@ export class Commands {
if (axios.isAxiosError(err) && err.response?.data) {
message = err.response.data.detail
}
if (err instanceof SelfSignedCertificateError) {
err.showInsecureNotification(this.storage)

return {
message: err.message,
severity: vscode.InputBoxValidationSeverity.Error,
}
}
return {
message: "Invalid session token! (" + message + ")",
severity: vscode.InputBoxValidationSeverity.Error,
Expand Down Expand Up @@ -189,7 +198,10 @@ export class Commands {
quickPick.items = items
quickPick.busy = false
})
.catch(() => {
.catch((ex) => {
if (ex instanceof SelfSignedCertificateError) {
ex.showInsecureNotification(this.storage)
}
return
})
})
Expand Down
51 changes: 51 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as fs from "fs/promises"
import * as jsonc from "jsonc-parser"
import * as vscode from "vscode"
import { Storage } from "./storage"

export class SelfSignedCertificateError extends Error {
public static Notification =
"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."
public static ActionAllowInsecure = "Allow Insecure"
public static ActionViewMoreDetails = "View More Details"

constructor(message: string) {
super(`Your Coder deployment is using a self-signed certificate: ${message}`)
}

public viewMoreDetails(): Thenable<boolean> {
return vscode.env.openExternal(vscode.Uri.parse("https://github.com/coder/vscode-coder/issues/105"))
}

// allowInsecure manually reads the settings file and updates the value of the
// "coder.insecure" property.
public async allowInsecure(storage: Storage): Promise<void> {
let settingsContent = "{}"
try {
settingsContent = await fs.readFile(storage.getUserSettingsPath(), "utf8")
} catch (ex) {
// Ignore! It's probably because the file doesn't exist.
}
const edits = jsonc.modify(settingsContent, ["coder.insecure"], true, {})
await fs.writeFile(storage.getUserSettingsPath(), jsonc.applyEdits(settingsContent, edits))

vscode.window.showInformationMessage(
'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.',
)
}

public async showInsecureNotification(storage: Storage): Promise<void> {
const value = await vscode.window.showErrorMessage(
SelfSignedCertificateError.Notification,
SelfSignedCertificateError.ActionAllowInsecure,
SelfSignedCertificateError.ActionViewMoreDetails,
)
if (value === SelfSignedCertificateError.ActionViewMoreDetails) {
await this.viewMoreDetails()
return
}
if (value === SelfSignedCertificateError.ActionAllowInsecure) {
return this.allowInsecure(storage)
}
}
}
94 changes: 75 additions & 19 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,67 @@
"use strict"
import axios from "axios"
import { getAuthenticatedUser } from "coder/site/src/api/api"
import * as https from "https"
import * as module from "module"
import * as vscode from "vscode"
import { Commands } from "./commands"
import { SelfSignedCertificateError } from "./error"
import { Remote } from "./remote"
import { Storage } from "./storage"
import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider"

export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
// The Remote SSH extension's proposed APIs are used to override
// the SSH host name in VS Code itself. It's visually unappealing
// having a lengthy name!
//
// This is janky, but that's alright since it provides such minimal
// functionality to the extension.
const remoteSSHExtension = vscode.extensions.getExtension("ms-vscode-remote.remote-ssh")
if (!remoteSSHExtension) {
throw new Error("Remote SSH extension not found")
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vscodeProposed: typeof vscode = (module as any)._load(
"vscode",
{
filename: remoteSSHExtension?.extensionPath,
},
false,
)

// updateInsecure is called on extension activation and when the insecure
// setting is changed. It updates the https agent to allow self-signed
// certificates if the insecure setting is true.
const applyInsecure = () => {
const insecure = Boolean(vscode.workspace.getConfiguration().get("coder.insecure"))

axios.defaults.httpsAgent = new https.Agent({
// rejectUnauthorized defaults to true, so we need to explicitly set it to false
// if we want to allow self-signed certificates.
rejectUnauthorized: !insecure,
})
}

axios.interceptors.response.use(
(r) => r,
(err) => {
if (err) {
const msg = err.toString() as string
if (msg.indexOf("unable to verify the first certificate") !== -1) {
throw new SelfSignedCertificateError(msg)
}
}

throw err
},
)

vscode.workspace.onDidChangeConfiguration((e) => {
e.affectsConfiguration("coder.insecure") && applyInsecure()
})
applyInsecure()

const output = vscode.window.createOutputChannel("Coder")
const storage = new Storage(output, ctx.globalState, ctx.secrets, ctx.globalStorageUri, ctx.logUri)
await storage.init()
Expand Down Expand Up @@ -62,25 +116,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
},
})

// The Remote SSH extension's proposed APIs are used to override
// the SSH host name in VS Code itself. It's visually unappealing
// having a lengthy name!
//
// This is janky, but that's alright since it provides such minimal
// functionality to the extension.
const remoteSSHExtension = vscode.extensions.getExtension("ms-vscode-remote.remote-ssh")
if (!remoteSSHExtension) {
throw new Error("Remote SSH extension not found")
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vscodeProposed: typeof vscode = (module as any)._load(
"vscode",
{
filename: remoteSSHExtension?.extensionPath,
},
false,
)

const commands = new Commands(vscodeProposed, storage)

vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
Expand Down Expand Up @@ -109,6 +144,27 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
try {
await remote.setup(vscodeProposed.env.remoteAuthority)
} catch (ex) {
if (ex instanceof SelfSignedCertificateError) {
const prompt = await vscodeProposed.window.showErrorMessage(
"Failed to open workspace",
{
detail: SelfSignedCertificateError.Notification,
modal: true,
useCustom: true,
},
SelfSignedCertificateError.ActionAllowInsecure,
SelfSignedCertificateError.ActionViewMoreDetails,
)
if (prompt === SelfSignedCertificateError.ActionAllowInsecure) {
await ex.allowInsecure(storage)
await remote.reloadWindow()
return
}
if (prompt === SelfSignedCertificateError.ActionViewMoreDetails) {
await ex.viewMoreDetails()
return
}
}
await vscodeProposed.window.showErrorMessage("Failed to open workspace", {
detail: (ex as string).toString(),
modal: true,
Expand Down
2 changes: 1 addition & 1 deletion src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ export class Remote {
}

// reloadWindow reloads the current window.
private async reloadWindow() {
public async reloadWindow() {
await vscode.commands.executeCommand("workbench.action.reloadWindow")
}

Expand Down
1 change: 1 addition & 0 deletions src/sshSupport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { computeSSHProperties, sshSupportsSetEnv, sshVersionSupportsSetEnv } fro

const supports = {
"OpenSSH_8.9p1 Ubuntu-3ubuntu0.1, OpenSSL 3.0.2 15 Mar 2022": true,
"OpenSSH_for_Windows_8.1p1, LibreSSL 3.0.2": true,
"OpenSSH_9.0p1, LibreSSL 3.3.6": 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,
Expand Down
2 changes: 1 addition & 1 deletion src/sshSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function sshSupportsSetEnv(): boolean {
//
// 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.]+)[^,]*/)
const match = sshVersionString.match(/OpenSSH.*_([\d.]+)[^,]*/)
if (match && match[1]) {
const installedVersion = match[1]
const parts = installedVersion.split(".")
Expand Down