Skip to content

Commit bcf5486

Browse files
committed
Add coder.proxyBypass setting
This works around VS Code having no support for no_proxy except through environment variables.
1 parent 81877df commit bcf5486

File tree

5 files changed

+171
-34
lines changed

5 files changed

+171
-34
lines changed

CHANGELOG.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22

33
## Unreleased
44

5+
## [v1.2.0](https://github.com/coder/vscode-coder/releases/tag/v1.2.0) (2024-06-21)
6+
7+
### Added
8+
9+
- New setting `coder.proxyBypass` which is the equivalent of `no_proxy`. This
10+
only takes effect if `http.proxySupport` is `on` or `off`, otherwise VS Code
11+
overrides the HTTP agent the plugin sets.
12+
513
## [v1.1.0](https://github.com/coder/vscode-coder/releases/tag/v1.1.0) (2024-06-17)
614

715
### Added
816

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

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

package.json

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"displayName": "Coder",
55
"description": "Open any workspace with a single click.",
66
"repository": "https://github.com/coder/vscode-coder",
7-
"version": "1.1.0",
7+
"version": "1.2.0",
88
"engines": {
99
"vscode": "^1.73.0"
1010
},
@@ -87,6 +87,11 @@
8787
"markdownDescription": "Path to file for TLS certificate authority",
8888
"type": "string",
8989
"default": ""
90+
},
91+
"coder.proxyBypass": {
92+
"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.",
93+
"type": "string",
94+
"default": ""
9095
}
9196
}
9297
},
@@ -249,7 +254,6 @@
249254
"@types/glob": "^7.1.3",
250255
"@types/ndjson": "^2.0.1",
251256
"@types/node": "^18.0.0",
252-
"@types/proxy-from-env": "^1.0.4",
253257
"@types/vscode": "^1.73.0",
254258
"@types/which": "^2.0.1",
255259
"@types/ws": "^8.5.10",
@@ -268,7 +272,6 @@
268272
"glob": "^7.1.6",
269273
"nyc": "^15.1.0",
270274
"prettier": "^3.2.5",
271-
"proxy-agent": "^6.4.0",
272275
"ts-loader": "^9.5.1",
273276
"tsc-watch": "^6.2.0",
274277
"typescript": "^5.4.5",
@@ -291,7 +294,7 @@
291294
"ndjson": "^2.0.0",
292295
"node-forge": "^1.3.1",
293296
"pretty-bytes": "^6.0.0",
294-
"proxy-from-env": "^1.1.0",
297+
"proxy-agent": "^6.4.0",
295298
"semver": "^7.6.0",
296299
"tar-fs": "^2.1.1",
297300
"ua-parser-js": "^1.0.37",

src/api.ts

+47-20
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,59 @@ import { Api } from "coder/site/src/api/api"
22
import fs from "fs/promises"
33
import * as os from "os"
44
import { ProxyAgent } from "proxy-agent"
5-
import { getProxyForUrl } from "proxy-from-env"
65
import * as vscode from "vscode"
76
import { CertificateError } from "./error"
7+
import { getProxyForUrl } from "./proxy"
88
import { Storage } from "./storage"
99

1010
// expandPath will expand ${userHome} in the input string.
11-
const expandPath = (input: string): string => {
11+
function expandPath(input: string): string {
1212
const userHome = os.homedir()
1313
return input.replace(/\${userHome}/g, userHome)
1414
}
1515

16+
async function createHttpAgent(): Promise<ProxyAgent> {
17+
const cfg = vscode.workspace.getConfiguration()
18+
const insecure = Boolean(cfg.get("coder.insecure"))
19+
const certFile = expandPath(String(cfg.get("coder.tlsCertFile") ?? "").trim())
20+
const keyFile = expandPath(String(cfg.get("coder.tlsKeyFile") ?? "").trim())
21+
const caFile = expandPath(String(cfg.get("coder.tlsCaFile") ?? "").trim())
22+
23+
return new ProxyAgent({
24+
// Called each time a request is made.
25+
getProxyForUrl: (url: string) => {
26+
const cfg = vscode.workspace.getConfiguration()
27+
return getProxyForUrl(url, cfg.get("http.proxy"), cfg.get("coder.proxyBypass"))
28+
},
29+
cert: certFile === "" ? undefined : await fs.readFile(certFile),
30+
key: keyFile === "" ? undefined : await fs.readFile(keyFile),
31+
ca: caFile === "" ? undefined : await fs.readFile(caFile),
32+
// rejectUnauthorized defaults to true, so we need to explicitly set it to
33+
// false if we want to allow self-signed certificates.
34+
rejectUnauthorized: !insecure,
35+
})
36+
}
37+
38+
let agent: Promise<ProxyAgent> | undefined = undefined
39+
async function getHttpAgent(): Promise<ProxyAgent> {
40+
if (!agent) {
41+
vscode.workspace.onDidChangeConfiguration((e) => {
42+
if (
43+
// http.proxy and coder.proxyBypass are read each time a request is
44+
// made, so no need to watch them.
45+
e.affectsConfiguration("coder.insecure") ||
46+
e.affectsConfiguration("coder.tlsCertFile") ||
47+
e.affectsConfiguration("coder.tlsKeyFile") ||
48+
e.affectsConfiguration("coder.tlsCaFile")
49+
) {
50+
agent = createHttpAgent()
51+
}
52+
})
53+
agent = createHttpAgent()
54+
}
55+
return agent
56+
}
57+
1658
/**
1759
* Create an sdk instance using the provided URL and token and hook it up to
1860
* configuration. The token may be undefined if some other form of
@@ -31,25 +73,10 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s
3173
config.headers[key] = value
3274
})
3375

34-
const cfg = vscode.workspace.getConfiguration()
35-
const insecure = Boolean(cfg.get("coder.insecure"))
36-
const certFile = expandPath(String(cfg.get("coder.tlsCertFile") ?? "").trim())
37-
const keyFile = expandPath(String(cfg.get("coder.tlsKeyFile") ?? "").trim())
38-
const caFile = expandPath(String(cfg.get("coder.tlsCaFile") ?? "").trim())
39-
4076
// Configure proxy and TLS.
41-
const agent = new ProxyAgent({
42-
// If the proxy setting exists, we always use it. Otherwise we follow the
43-
// standard environment variables (no_proxy, http_proxy, etc).
44-
getProxyForUrl: (url: string) => cfg.get("http.proxy") || getProxyForUrl(url),
45-
cert: certFile === "" ? undefined : await fs.readFile(certFile),
46-
key: keyFile === "" ? undefined : await fs.readFile(keyFile),
47-
ca: caFile === "" ? undefined : await fs.readFile(caFile),
48-
// rejectUnauthorized defaults to true, so we need to explicitly set it to
49-
// false if we want to allow self-signed certificates.
50-
rejectUnauthorized: !insecure,
51-
})
52-
77+
// Note that by default VS Code overrides the agent. To prevent this, set
78+
// `http.proxySupport` to `on` or `off`.
79+
const agent = await getHttpAgent()
5380
config.httpsAgent = agent
5481
config.httpAgent = agent
5582

src/proxy.ts

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// This file is copied from proxy-from-env with added support to use something
2+
// other than environment variables.
3+
4+
import { parse as parseUrl } from "url"
5+
6+
const DEFAULT_PORTS: Record<string, number> = {
7+
ftp: 21,
8+
gopher: 70,
9+
http: 80,
10+
https: 443,
11+
ws: 80,
12+
wss: 443,
13+
}
14+
15+
/**
16+
* @param {string|object} url - The URL, or the result from url.parse.
17+
* @return {string} The URL of the proxy that should handle the request to the
18+
* given URL. If no proxy is set, this will be an empty string.
19+
*/
20+
export function getProxyForUrl(
21+
url: string,
22+
httpProxy: string | null | undefined,
23+
noProxy: string | null | undefined,
24+
): string {
25+
const parsedUrl = typeof url === "string" ? parseUrl(url) : url || {}
26+
let proto = parsedUrl.protocol
27+
let hostname = parsedUrl.host
28+
const portRaw = parsedUrl.port
29+
if (typeof hostname !== "string" || !hostname || typeof proto !== "string") {
30+
return "" // Don't proxy URLs without a valid scheme or host.
31+
}
32+
33+
proto = proto.split(":", 1)[0]
34+
// Stripping ports in this way instead of using parsedUrl.hostname to make
35+
// sure that the brackets around IPv6 addresses are kept.
36+
hostname = hostname.replace(/:\d*$/, "")
37+
const port = (portRaw && parseInt(portRaw)) || DEFAULT_PORTS[proto] || 0
38+
if (!shouldProxy(hostname, port, noProxy)) {
39+
return "" // Don't proxy URLs that match NO_PROXY.
40+
}
41+
42+
let proxy =
43+
httpProxy ||
44+
getEnv("npm_config_" + proto + "_proxy") ||
45+
getEnv(proto + "_proxy") ||
46+
getEnv("npm_config_proxy") ||
47+
getEnv("all_proxy")
48+
if (proxy && proxy.indexOf("://") === -1) {
49+
// Missing scheme in proxy, default to the requested URL's scheme.
50+
proxy = proto + "://" + proxy
51+
}
52+
return proxy
53+
}
54+
55+
/**
56+
* Determines whether a given URL should be proxied.
57+
*
58+
* @param {string} hostname - The host name of the URL.
59+
* @param {number} port - The effective port of the URL.
60+
* @returns {boolean} Whether the given URL should be proxied.
61+
* @private
62+
*/
63+
function shouldProxy(hostname: string, port: number, noProxy: string | null | undefined): boolean {
64+
const NO_PROXY = (noProxy || getEnv("npm_config_no_proxy") || getEnv("no_proxy")).toLowerCase()
65+
if (!NO_PROXY) {
66+
return true // Always proxy if NO_PROXY is not set.
67+
}
68+
if (NO_PROXY === "*") {
69+
return false // Never proxy if wildcard is set.
70+
}
71+
72+
return NO_PROXY.split(/[,\s]/).every(function (proxy) {
73+
if (!proxy) {
74+
return true // Skip zero-length hosts.
75+
}
76+
const parsedProxy = proxy.match(/^(.+):(\d+)$/)
77+
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : proxy
78+
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0
79+
if (parsedProxyPort && parsedProxyPort !== port) {
80+
return true // Skip if ports don't match.
81+
}
82+
83+
if (!/^[.*]/.test(parsedProxyHostname)) {
84+
// No wildcards, so stop proxying if there is an exact match.
85+
return hostname !== parsedProxyHostname
86+
}
87+
88+
if (parsedProxyHostname.charAt(0) === "*") {
89+
// Remove leading wildcard.
90+
parsedProxyHostname = parsedProxyHostname.slice(1)
91+
}
92+
// Stop proxying if the hostname ends with the no_proxy host.
93+
return !hostname.endsWith(parsedProxyHostname)
94+
})
95+
}
96+
97+
/**
98+
* Get the value for an environment variable.
99+
*
100+
* @param {string} key - The name of the environment variable.
101+
* @return {string} The value of the environment variable.
102+
* @private
103+
*/
104+
function getEnv(key: string): string {
105+
return process.env[key.toLowerCase()] || process.env[key.toUpperCase()] || ""
106+
}

yarn.lock

-7
Original file line numberDiff line numberDiff line change
@@ -704,13 +704,6 @@
704704
dependencies:
705705
undici-types "~5.26.4"
706706

707-
"@types/proxy-from-env@^1.0.4":
708-
version "1.0.4"
709-
resolved "https://registry.yarnpkg.com/@types/proxy-from-env/-/proxy-from-env-1.0.4.tgz#0a0545768f2d6c16b81a84ffefb53b423807907c"
710-
integrity sha512-TPR9/bCZAr3V1eHN4G3LD3OLicdJjqX1QRXWuNcCYgE66f/K8jO2ZRtHxI2D9MbnuUP6+qiKSS8eUHp6TFHGCw==
711-
dependencies:
712-
"@types/node" "*"
713-
714707
"@types/semver@^7.5.0":
715708
version "7.5.3"
716709
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04"

0 commit comments

Comments
 (0)