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 1 commit
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
Prev Previous commit
Next Next commit
Add support for the insecure property
  • Loading branch information
kylecarbs committed Jun 9, 2023
commit a61b7f00abae7acc43913a20fb9e2d8918c5d891
14 changes: 13 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { extractAgents } from "./api-helper"
import { Remote } from "./remote"
import { Storage } from "./storage"
import { OpenableTreeItem } from "./workspacesProvider"
import { SelfSignedCertificateError } from "./error"

export class Commands {
public constructor(private readonly vscodeProposed: typeof vscode, private readonly storage: Storage) {}
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
44 changes: 44 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as jsonc from "jsonc-parser"
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)
}
}
}
106 changes: 66 additions & 40 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,58 @@ import { Commands } from "./commands"
import { Remote } from "./remote"
import { Storage } from "./storage"
import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider"
import { SelfSignedCertificateError } from "./error"

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"))
console.log("updating insecure", 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 @@ -64,48 +114,8 @@ 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)

// 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 updateInsecure = () => {
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(undefined, (error) => {
// if (error)
return error
})

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

vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
Expand All @@ -132,6 +142,22 @@ 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