Skip to content

Commit

Permalink
Add coder.proxyBypass setting
Browse files Browse the repository at this point in the history
This works around VS Code having no support for no_proxy except through
environment variables.
  • Loading branch information
code-asher committed Jun 21, 2024
1 parent 81877df commit 009bca7
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 34 deletions.
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

## Unreleased

## [v1.2.0](https://github.com/coder/vscode-coder/releases/tag/v1.2.0) (2024-06-21)

### Added

- New setting `coder.proxyBypass` which is the equivalent of `no_proxy`. This
only takes effect if `http.proxySupport` is `on` or `off`, otherwise VS Code
overrides the HTTP agent the plugin sets.

## [v1.1.0](https://github.com/coder/vscode-coder/releases/tag/v1.1.0) (2024-06-17)

### Added

- Workspace and agent statuses now show in the sidebar. These are updated every
five seconds.
- Support http.proxy setting and proxy environment variables. This only takes
effect if http.proxySupport is `off` or `on`, otherwise VS Code overrides the
HTTP agent the plugin sets.
- Support `http.proxy` setting and proxy environment variables. These only take
effect if `http.proxySupport` is `on` or `off`, otherwise VS Code overrides
the HTTP agent the plugin sets.

## [v1.0.2](https://github.com/coder/vscode-coder/releases/tag/v1.0.2) (2024-06-12)

Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"displayName": "Coder",
"description": "Open any workspace with a single click.",
"repository": "https://github.com/coder/vscode-coder",
"version": "1.1.0",
"version": "1.2.0",
"engines": {
"vscode": "^1.73.0"
},
Expand Down Expand Up @@ -87,6 +87,11 @@
"markdownDescription": "Path to file for TLS certificate authority",
"type": "string",
"default": ""
},
"coder.proxyBypass": {
"markdownDescription": "If not set, will inherit from the `no_proxy` or `NO_PROXY` environment variables. `http.proxySupport` must be set to `on` or `off`, otherwise VS Code will override the proxy agent set by the plugin.",
"type": "string",
"default": ""
}
}
},
Expand Down Expand Up @@ -249,7 +254,6 @@
"@types/glob": "^7.1.3",
"@types/ndjson": "^2.0.1",
"@types/node": "^18.0.0",
"@types/proxy-from-env": "^1.0.4",
"@types/vscode": "^1.73.0",
"@types/which": "^2.0.1",
"@types/ws": "^8.5.10",
Expand All @@ -268,7 +272,6 @@
"glob": "^7.1.6",
"nyc": "^15.1.0",
"prettier": "^3.2.5",
"proxy-agent": "^6.4.0",
"ts-loader": "^9.5.1",
"tsc-watch": "^6.2.0",
"typescript": "^5.4.5",
Expand All @@ -291,7 +294,7 @@
"ndjson": "^2.0.0",
"node-forge": "^1.3.1",
"pretty-bytes": "^6.0.0",
"proxy-from-env": "^1.1.0",
"proxy-agent": "^6.4.0",
"semver": "^7.6.0",
"tar-fs": "^2.1.1",
"ua-parser-js": "^1.0.37",
Expand Down
67 changes: 47 additions & 20 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,59 @@ import { Api } from "coder/site/src/api/api"
import fs from "fs/promises"
import * as os from "os"
import { ProxyAgent } from "proxy-agent"
import { getProxyForUrl } from "proxy-from-env"
import * as vscode from "vscode"
import { CertificateError } from "./error"
import { getProxyForUrl } from "./proxy"
import { Storage } from "./storage"

// expandPath will expand ${userHome} in the input string.
const expandPath = (input: string): string => {
function expandPath(input: string): string {
const userHome = os.homedir()
return input.replace(/\${userHome}/g, userHome)
}

async function createHttpAgent(): Promise<ProxyAgent> {
const cfg = vscode.workspace.getConfiguration()
const insecure = Boolean(cfg.get("coder.insecure"))
const certFile = expandPath(String(cfg.get("coder.tlsCertFile") ?? "").trim())
const keyFile = expandPath(String(cfg.get("coder.tlsKeyFile") ?? "").trim())
const caFile = expandPath(String(cfg.get("coder.tlsCaFile") ?? "").trim())

return new ProxyAgent({
// Called each time a request is made.
getProxyForUrl: (url: string) => {
const cfg = vscode.workspace.getConfiguration()
return getProxyForUrl(url, cfg.get("http.proxy"), cfg.get("coder.proxyBypass"))
},
cert: certFile === "" ? undefined : await fs.readFile(certFile),
key: keyFile === "" ? undefined : await fs.readFile(keyFile),
ca: caFile === "" ? undefined : await fs.readFile(caFile),
// rejectUnauthorized defaults to true, so we need to explicitly set it to
// false if we want to allow self-signed certificates.
rejectUnauthorized: !insecure,
})
}

let agent: Promise<ProxyAgent> | undefined = undefined
async function getHttpAgent(): Promise<ProxyAgent> {
if (!agent) {
vscode.workspace.onDidChangeConfiguration((e) => {
if (
// http.proxy and coder.proxyBypass are read each time a request is
// made, so no need to watch them.
e.affectsConfiguration("coder.insecure") ||
e.affectsConfiguration("coder.tlsCertFile") ||
e.affectsConfiguration("coder.tlsKeyFile") ||
e.affectsConfiguration("coder.tlsCaFile")
) {
agent = createHttpAgent()
}
})
agent = createHttpAgent()
}
return agent
}

/**
* Create an sdk instance using the provided URL and token and hook it up to
* configuration. The token may be undefined if some other form of
Expand All @@ -31,25 +73,10 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s
config.headers[key] = value
})

const cfg = vscode.workspace.getConfiguration()
const insecure = Boolean(cfg.get("coder.insecure"))
const certFile = expandPath(String(cfg.get("coder.tlsCertFile") ?? "").trim())
const keyFile = expandPath(String(cfg.get("coder.tlsKeyFile") ?? "").trim())
const caFile = expandPath(String(cfg.get("coder.tlsCaFile") ?? "").trim())

// Configure proxy and TLS.
const agent = new ProxyAgent({
// If the proxy setting exists, we always use it. Otherwise we follow the
// standard environment variables (no_proxy, http_proxy, etc).
getProxyForUrl: (url: string) => cfg.get("http.proxy") || getProxyForUrl(url),
cert: certFile === "" ? undefined : await fs.readFile(certFile),
key: keyFile === "" ? undefined : await fs.readFile(keyFile),
ca: caFile === "" ? undefined : await fs.readFile(caFile),
// rejectUnauthorized defaults to true, so we need to explicitly set it to
// false if we want to allow self-signed certificates.
rejectUnauthorized: !insecure,
})

// Note that by default VS Code overrides the agent. To prevent this, set
// `http.proxySupport` to `on` or `off`.
const agent = await getHttpAgent()
config.httpsAgent = agent
config.httpAgent = agent

Expand Down
106 changes: 106 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// This file is copied from proxy-from-env with added support to use something
// other than environment variables.

import { parse as parseUrl } from "url"

const DEFAULT_PORTS: Record<string, number> = {
ftp: 21,
gopher: 70,
http: 80,
https: 443,
ws: 80,
wss: 443,
}

/**
* @param {string|object} url - The URL, or the result from url.parse.
* @return {string} The URL of the proxy that should handle the request to the
* given URL. If no proxy is set, this will be an empty string.
*/
export function getProxyForUrl(
url: string,
httpProxy: string | null | undefined,
noProxy: string | null | undefined,
): string {
const parsedUrl = typeof url === "string" ? parseUrl(url) : url || {}
let proto = parsedUrl.protocol
let hostname = parsedUrl.host
const portRaw = parsedUrl.port
if (typeof hostname !== "string" || !hostname || typeof proto !== "string") {
return "" // Don't proxy URLs without a valid scheme or host.
}

proto = proto.split(":", 1)[0]
// Stripping ports in this way instead of using parsedUrl.hostname to make
// sure that the brackets around IPv6 addresses are kept.
hostname = hostname.replace(/:\d*$/, "")
const port = (portRaw && parseInt(portRaw)) || DEFAULT_PORTS[proto] || 0
if (!shouldProxy(hostname, port, noProxy)) {
return "" // Don't proxy URLs that match NO_PROXY.
}

let proxy =
httpProxy ||
getEnv("npm_config_" + proto + "_proxy") ||
getEnv(proto + "_proxy") ||
getEnv("npm_config_proxy") ||
getEnv("all_proxy")
if (proxy && proxy.indexOf("://") === -1) {
// Missing scheme in proxy, default to the requested URL's scheme.
proxy = proto + "://" + proxy
}
return proxy
}

/**
* Determines whether a given URL should be proxied.
*
* @param {string} hostname - The host name of the URL.
* @param {number} port - The effective port of the URL.
* @returns {boolean} Whether the given URL should be proxied.
* @private
*/
function shouldProxy(hostname: string, port: number, noProxy: string | null | undefined): boolean {
const NO_PROXY = noProxy || (getEnv("npm_config_no_proxy") || getEnv("no_proxy")).toLowerCase()
if (!NO_PROXY) {
return true // Always proxy if NO_PROXY is not set.
}
if (NO_PROXY === "*") {
return false // Never proxy if wildcard is set.
}

return NO_PROXY.split(/[,\s]/).every(function (proxy) {
if (!proxy) {
return true // Skip zero-length hosts.
}
const parsedProxy = proxy.match(/^(.+):(\d+)$/)
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : proxy
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0
if (parsedProxyPort && parsedProxyPort !== port) {
return true // Skip if ports don't match.
}

if (!/^[.*]/.test(parsedProxyHostname)) {
// No wildcards, so stop proxying if there is an exact match.
return hostname !== parsedProxyHostname
}

if (parsedProxyHostname.charAt(0) === "*") {
// Remove leading wildcard.
parsedProxyHostname = parsedProxyHostname.slice(1)
}
// Stop proxying if the hostname ends with the no_proxy host.
return !hostname.endsWith(parsedProxyHostname)
})
}

/**
* Get the value for an environment variable.
*
* @param {string} key - The name of the environment variable.
* @return {string} The value of the environment variable.
* @private
*/
function getEnv(key: string): string {
return process.env[key.toLowerCase()] || process.env[key.toUpperCase()] || ""
}
7 changes: 0 additions & 7 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -704,13 +704,6 @@
dependencies:
undici-types "~5.26.4"

"@types/proxy-from-env@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/proxy-from-env/-/proxy-from-env-1.0.4.tgz#0a0545768f2d6c16b81a84ffefb53b423807907c"
integrity sha512-TPR9/bCZAr3V1eHN4G3LD3OLicdJjqX1QRXWuNcCYgE66f/K8jO2ZRtHxI2D9MbnuUP6+qiKSS8eUHp6TFHGCw==
dependencies:
"@types/node" "*"

"@types/semver@^7.5.0":
version "7.5.3"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04"
Expand Down

0 comments on commit 009bca7

Please sign in to comment.