diff --git a/package.json b/package.json index 1b0829fa..7c63f0b6 100644 --- a/package.json +++ b/package.json @@ -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 } } }, @@ -241,4 +246,4 @@ "yaml": "^1.10.0", "zod": "^3.21.4" } -} +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 23c03d11..079aac20 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -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" @@ -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, @@ -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 }) }) diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 00000000..4c247836 --- /dev/null +++ b/src/error.ts @@ -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 { + 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 { + 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 { + 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) + } + } +} diff --git a/src/extension.ts b/src/extension.ts index e88928d6..b5aaf62c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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 { + // 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() @@ -62,25 +116,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { }, }) - // 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)) @@ -109,6 +144,27 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { 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, diff --git a/src/remote.ts b/src/remote.ts index c97430e1..5d2f1134 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -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") } diff --git a/src/sshSupport.test.ts b/src/sshSupport.test.ts index dbcde14f..8bf2ac58 100644 --- a/src/sshSupport.test.ts +++ b/src/sshSupport.test.ts @@ -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, diff --git a/src/sshSupport.ts b/src/sshSupport.ts index 5726070a..12b65dd3 100644 --- a/src/sshSupport.ts +++ b/src/sshSupport.ts @@ -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(".") 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