Skip to content

pass headers to coder api #81

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

Closed
wants to merge 10 commits into from
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
/coverage/
*.vsix
yarn-error.log
.vscode/settings.json
21 changes: 20 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@
"scope": "machine",
"default": []
},
"coder.vpnHeader":{
"markdownDescription": "key/ value pair to add to coder",
"type": "object",
"properties": {
"headerName": {
"type": "string",
"description": "header name"
},
"token": {
"type": "string",
"description": "header value (optional)"
},
"tokenFile": {
"type":"string",
"description": "optional.Path to a file to retrieve the token from, if set, it will ignore the the require on startup "

}
}
},
"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",
Expand Down Expand Up @@ -246,4 +265,4 @@
"yaml": "^1.10.0",
"zod": "^3.21.4"
}
}
}
98 changes: 96 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"use strict"
import axios from "axios"
import axios, { AxiosResponse } from "axios"
import { getAuthenticatedUser } from "coder/site/src/api/api"
import { readFileSync } from "fs"
import * as https from "https"
import * as module from "module"
import * as os from "os"
import path from "path"
import * as vscode from "vscode"
import { Commands } from "./commands"
import { SelfSignedCertificateError } from "./error"
Expand Down Expand Up @@ -71,7 +74,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {

vscode.window.registerTreeDataProvider("myWorkspaces", myWorkspacesProvider)
vscode.window.registerTreeDataProvider("allWorkspaces", allWorkspacesProvider)

await initGlobalVpnHeaders(storage)
addAxiosInterceptor(storage)
getAuthenticatedUser()
.then(async (user) => {
if (user) {
Expand Down Expand Up @@ -172,3 +176,93 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
})
}
}
function addAxiosInterceptor(storage: Storage): void {
axios.interceptors.response.use(
(res) => {
if (isVpnTokenInvalid(res)) {
getVpnHeaderFromUser(
"seems like the Vpn Token provided is either invalid or expired, please provide a new token",
).then((token) => {
storage.setVpnHeaderToken(token)
})
} else {
return res
}
},
(error) => {
if (isVpnTokenInvalidOnError(error)) {
getVpnHeaderFromUser(
"seems like the Vpn Token provided is either invalid or expired, please provide a new token",
).then((token) => {
storage.setVpnHeaderToken(token)
})
// vscode.window.showErrorMessage(JSON.stringify("vpn token not valid, make sure you added a correct token"))
}
return error
},
)
}
async function initGlobalVpnHeaders(storage: Storage): Promise<void> {
//find if global vpn headers are needed
type VpnHeaderConfig = {
headerName: string
token: string
tokenFile: string
}
const vpnHeaderConf = vscode.workspace.getConfiguration("coder").get<VpnHeaderConfig>("vpnHeader")
if (!vpnHeaderConf) {
return
}
const { headerName, tokenFile, token } = vpnHeaderConf
if (!headerName) {
throw Error(
"vpn header name was not defined in extension setting, please make sure to set `coder.vpnHeader.headerName`",
)
}
const maybeVpnHeaderToken = (await storage.getVpnHeaderToken()) || token || readVpnHeaderTokenFromFile(tokenFile)
if (maybeVpnHeaderToken) {
storage.setVpnHeaderToken(maybeVpnHeaderToken)
axios.defaults.headers.common[headerName] = maybeVpnHeaderToken
} else {
//default to read global headers from user prompt
const vpnToken = await getVpnHeaderFromUser(
"you need to add your vpn access token to be able to run api calls to coder ",
)

if (vpnToken) {
storage.setVpnHeaderToken(vpnToken)
axios.defaults.headers.common[headerName] = vpnToken
} else {
throw Error(
"you must provide a vpn token, either by user prompt, path to file holding the token, or explicitly as conf argument ",
)
}
}
}

async function getVpnHeaderFromUser(message: string): Promise<string | undefined> {
return await vscode.window.showInputBox({
title: "VpnToken",
prompt: message,
placeHolder: "put your token here",
})
}

function readVpnHeaderTokenFromFile(filepath: string): string | undefined {
if (!filepath) {
return
}
if (filepath.startsWith("~")) {
return readFileSync(path.join(os.homedir(), filepath.slice(1)), "utf-8")
} else {
return readFileSync(filepath, "utf-8")
}
}
function isVpnTokenInvalid(res: AxiosResponse<any, any>): boolean {
//if token expired or missing the vpn will return 200 OK with the actual html page to get you to reauthenticate
// , this will result in "data" not being an object but a string containing the html
return typeof res.data !== "object"
}
function isVpnTokenInvalidOnError(error: any): boolean {
return error.isAxiosError && error.response.status === 403
}
14 changes: 12 additions & 2 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,13 +503,23 @@ export class Remote {
}

const escape = (str: string): string => `"${str.replace(/"/g, '\\"')}"`

const vpnTokenHeaderName = this.vscodeProposed.workspace
.getConfiguration()
.get<string>("coder.vpnHeader.headerName")
const VpnHeaderToken = await this.storage.getVpnHeaderToken()

const sshValues: SSHValues = {

Host: `${Remote.Prefix}*`,
ProxyCommand: `${escape(binaryPath)} vscodessh --network-info-dir ${escape(
// when running vscodessh command we get the following error "find workspace: invalid character '<' looking for beginning of value" which means that the header is not proppgate currectly
ProxyCommand: `${escape(
binaryPath,
)} --header="${vpnTokenHeaderName}=${VpnHeaderToken}" vscodessh --network-info-dir ${escape(
this.storage.getNetworkInfoPath(),
)} --session-token-file ${escape(this.storage.getSessionTokenPath())} --url-file ${escape(
this.storage.getURLPath(),
)} %h`,
)} %h`,
ConnectTimeout: "0",
StrictHostKeyChecking: "no",
UserKnownHostsFile: "/dev/null",
Expand Down
8 changes: 8 additions & 0 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export class Storage {
return undefined
}
}
public async setVpnHeaderToken(headerToken?: string): Promise<void> {
if (headerToken) {
this.secrets.store("vpnHeaderToken", headerToken)
}
}
public async getVpnHeaderToken(): Promise<string | undefined> {
return await this.secrets.get("vpnHeaderToken")
}

// getRemoteSSHLogPath returns the log path for the "Remote - SSH" output panel.
// There is no VS Code API to get the contents of an output panel. We use this
Expand Down
Loading