├── .devcontainer ├── README.md ├── basic-auth-test │ ├── .vscode │ │ └── tasks.json │ ├── README.md │ ├── basic-auth-proxy │ │ ├── Dockerfile │ │ └── basic_auth.conf │ ├── devcontainer-lock.json │ ├── devcontainer.json │ ├── docker-compose.yml │ ├── pac-server.js │ ├── package.json │ └── test.pac ├── devcontainer-lock.json ├── devcontainer.json ├── docker-compose.yml ├── http-proxy-privoxy │ └── Dockerfile ├── https-proxy-test │ ├── .gitignore │ ├── .vscode │ │ └── tasks.json │ ├── README.md │ ├── devcontainer.json │ └── docker-compose.yml └── vscode │ ├── Dockerfile │ ├── install-vscode.sh │ └── settings.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CredScanSuppressions.json ├── LICENSE.md ├── README.md ├── SECURITY.md ├── azure-pipelines └── publish.yml ├── package-lock.json ├── package.json ├── src ├── agent.ts └── index.ts ├── tests ├── docker-compose.yml ├── test-client │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── direct.test.ts │ │ ├── proxy.test.ts │ │ ├── socket.test.ts │ │ ├── tls.test.ts │ │ └── utils.ts │ └── tsconfig.json ├── test-http-auth-proxy │ ├── Dockerfile │ └── basic_auth.conf ├── test-http-kerberos-proxy │ ├── Dockerfile │ └── setup.sh ├── test-https-proxy │ └── .gitignore ├── test-https-server │ ├── Dockerfile │ └── nginx.conf └── test-proxy-client │ ├── Dockerfile │ └── configure-kerberos-client.sh └── tsconfig.json /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # VS Code container with network proxy 2 | 3 | This is a Dev Container with VS Code Insiders installed and no direct internet connection. HTTP_PROXY and HTTPS_PROXY environment variables are set to the proxy server running in a separate container. 4 | 5 | When connecting from a local VS Code window use the following command in the integrated terminal to avoid connecting back to the local window. The Dev Containers extensions automatically forwards the X11 display if available locally. 6 | ``` 7 | env -i HTTP_PROXY="$HTTP_PROXY" HTTPS_PROXY="$HTTPS_PROXY" NO_PROXY="$NO_PROXY" HOME="$HOME" DISPLAY="$DISPLAY" bash -lic code-insiders 8 | ``` 9 | 10 | Or use the Dev Container CLI for tunnels: 11 | ```sh 12 | npm i -g @devcontainers/cli 13 | devcontainer up --workspace-folder . 14 | devcontainer exec --workspace-folder . code-insiders tunnel 15 | ``` 16 | -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "watch", 6 | "dependsOn": [ 7 | "npm: pac-server:run", 8 | "npm: proxy-1:access-log", 9 | "npm: proxy-2:access-log" 10 | ], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "runOptions": { 16 | "runOn": "folderOpen" 17 | } 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "pac-server:run", 22 | "group": "build", 23 | "isBackground": true, 24 | "label": "npm: pac-server:run", 25 | "presentation": { 26 | "group": "watch" 27 | } 28 | }, 29 | { 30 | "type": "npm", 31 | "script": "proxy-1:access-log", 32 | "group": "build", 33 | "isBackground": true, 34 | "label": "npm: proxy-1:access-log", 35 | "presentation": { 36 | "group": "watch" 37 | } 38 | }, 39 | { 40 | "type": "npm", 41 | "script": "proxy-2:access-log", 42 | "group": "build", 43 | "isBackground": true, 44 | "label": "npm: proxy-2:access-log", 45 | "presentation": { 46 | "group": "watch" 47 | } 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/README.md: -------------------------------------------------------------------------------- 1 | ## Basic Auth Test 2 | 3 | Use this dev container configuration with a different VS Code install than you want to test (e.g., use VS Code stable if you want to test VS Code Insiders). 4 | - `Dev Containers: Reopen in Container` > `Basic Auth Test`. 5 | - The dev container should show 3 log terminals: 1 for the proxy config (PAC), 2 for 2 proxies with different basic auth credentials. 6 | - Locally start VS Code to test with the PAC file's URL, e.g.: `code-insiders --proxy-pac-url=http://localhost:3333`. 7 | - Enter the credentials when asked: localhost:3111 uses user1 and pass1, localhost:3122 uses user2 and pass2. 8 | - Connections to *.github.com go through localhost:3111. 9 | - Connections to *.githubcopilot.com go through localhost:3122. 10 | - All other connections do not use a proxy. 11 | - Install GitHub Copilot Chat and use `Developer: GitHub Copilot Chat Diagnostics` to test connections with basic auth. 12 | - Verify in the log terminals of the dev container that the PAC file and the proxies are being used. 13 | 14 | Optional (from other test passes): 15 | - To further check extension support install https://marketplace.visualstudio.com/items?itemName=chrmarti.network-proxy-test. 16 | - Update `test.pac` to apply the proxies to different domains, e.g., `https://example.com` and `https://marketplace.visualstudio.com`. 17 | - Use `Network Proxy Test: Test Network Connection` to test. 18 | - Use `yarn proxy-1:passwd ` and `yarn proxy-2:passwd ` in the dev container to update passwords. 19 | - The 'Remember my credentials' option in the credentials dialog remembers the credentails across restarts. 20 | - Note that credentials are always remembered until VS Code is restarted (not just reloaded). If you did not check the 'Remember my credentials' option, you will be asked again after restarting VS Code. 21 | -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/basic-auth-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu/squid:latest 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y apache2-utils \ 5 | && rm -rf /var/lib/apt/lists/* 6 | 7 | COPY basic_auth.conf /etc/squid/conf.d/squid.acl.conf 8 | RUN sed -e '/^http_access/ s/^#*/#/' -i /etc/squid/conf.d/debian.conf 9 | 10 | ARG PROXY_USERNAME 11 | ARG PROXY_PASSWORD 12 | RUN htpasswd -bc /etc/squid/.htpasswd $PROXY_USERNAME $PROXY_PASSWORD 13 | -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/basic-auth-proxy/basic_auth.conf: -------------------------------------------------------------------------------- 1 | auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/.htpasswd 2 | auth_param basic children 5 3 | auth_param basic realm Squid Basic Authentication 4 | auth_param basic credentialsttl 5 hours 5 | acl password proxy_auth REQUIRED 6 | http_access allow password 7 | -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/devcontainer-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { 4 | "version": "1.5.0", 5 | "resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:20761bd733511c1995ee955682cc2778b0a2e556abf88e9b88490c3be3c80bbc", 6 | "integrity": "sha256:20761bd733511c1995ee955682cc2778b0a2e556abf88e9b88490c3be3c80bbc" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Basic Auth Test", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "devcontainer", 5 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/.devcontainer/basic-auth-test", 6 | "features": { 7 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} 8 | }, 9 | "overrideCommand": true 10 | } 11 | -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | devcontainer: 5 | image: mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm 6 | volumes: 7 | - ../../..:/workspaces 8 | ports: 9 | - "127.0.0.1:3333:3333" 10 | basic-auth-proxy-1: 11 | restart: always 12 | build: 13 | context: basic-auth-proxy 14 | dockerfile: Dockerfile 15 | args: 16 | # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="test credentials")] 17 | - PROXY_USERNAME=user1 18 | # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="test credentials")] 19 | - PROXY_PASSWORD=pass1 20 | ports: 21 | - "127.0.0.1:3111:3128" 22 | basic-auth-proxy-2: 23 | restart: always 24 | build: 25 | context: basic-auth-proxy 26 | dockerfile: Dockerfile 27 | args: 28 | # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="test credentials")] 29 | - PROXY_USERNAME=user2 30 | # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="test credentials")] 31 | - PROXY_PASSWORD=pass2 32 | ports: 33 | - "127.0.0.1:3122:3128" 34 | -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/pac-server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const server = http.createServer((req, res) => { 6 | console.log('Sending pac file...'); 7 | const filePath = path.join(__dirname, 'test.pac'); 8 | const stat = fs.statSync(filePath); 9 | 10 | res.writeHead(200, { 11 | 'Content-Type': 'application/x-ns-proxy-autoconfig', 12 | 'Content-Length': stat.size 13 | }); 14 | 15 | const readStream = fs.createReadStream(filePath); 16 | readStream.pipe(res); 17 | }); 18 | 19 | server.listen(3333, () => { 20 | console.log('Server is running on port 3333'); 21 | }); -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "proxy-1:passwd": "docker exec -it basic-auth-test-basic-auth-proxy-1-1 /bin/sh -c 'htpasswd -bc /etc/squid/.htpasswd user1 $0 && kill -9 $(cat /var/run/squid.pid)'", 4 | "proxy-2:passwd": "docker exec -it basic-auth-test-basic-auth-proxy-2-1 /bin/sh -c 'htpasswd -bc /etc/squid/.htpasswd user2 $0 && kill -9 $(cat /var/run/squid.pid)'", 5 | 6 | "pac-server:run": "node pac-server.js", 7 | "proxy-1:access-log": "while true; do docker exec -it basic-auth-test-basic-auth-proxy-1-1 tail -F /var/log/squid/access.log ; sleep 1 ; done", 8 | "proxy-2:access-log": "while true; do docker exec -it basic-auth-test-basic-auth-proxy-2-1 tail -F /var/log/squid/access.log ; sleep 1 ; done" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.devcontainer/basic-auth-test/test.pac: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | if (dnsDomainIs(host, "github.com")) 3 | return "PROXY localhost:3111"; 4 | if (dnsDomainIs(host, "githubcopilot.com")) 5 | return "PROXY localhost:3122"; 6 | return "DIRECT"; 7 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 4 | "version": "2.2.0", 5 | "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:36e51dcb08a87d73250ddb486a88f8755ba93c1bc6756bb32b7ecb38d7175622", 6 | "integrity": "sha256:36e51dcb08a87d73250ddb486a88f8755ba93c1bc6756bb32b7ecb38d7175622" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VS Code with Proxy", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "vscode", 5 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 6 | "features": { 7 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | vscode: 5 | build: vscode 6 | command: 7 | - /bin/sh 8 | - -c 9 | - | 10 | cat </etc/apt/apt.conf.d/proxy.conf 11 | Acquire::http::Proxy "$$HTTP_PROXY"; 12 | Acquire::https::Proxy "$$HTTPS_PROXY"; 13 | EOF 14 | cat <>/etc/wgetrc 15 | http_proxy=$$HTTP_PROXY 16 | https_proxy=$$HTTPS_PROXY 17 | EOF 18 | while [ ! -f /home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem ]; do sleep 1; done 19 | cp /home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt 20 | update-ca-certificates 21 | # https://chromium.googlesource.com/chromium/src/+/master/docs/linux/cert_management.md#linux-cert-management 22 | su -c 'mkdir -p "$$HOME/.pki/nssdb" && certutil -d "sql:$$HOME/.pki/nssdb" -A -t "C,," -n "mitmproxy CA" -i /home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem' node 23 | sleep inf 24 | privileged: true 25 | links: 26 | # - http-proxy-squid 27 | # - http-proxy-privoxy 28 | - http-proxy-mitmproxy 29 | # - dns-proxy 30 | environment: 31 | # - HTTP_PROXY=http://http-proxy-squid:3128 32 | # - HTTPS_PROXY=http://http-proxy-squid:3128 33 | # - HTTP_PROXY=http://http-proxy-privoxy:8118 34 | # - HTTPS_PROXY=http://http-proxy-privoxy:8118 35 | - HTTP_PROXY=http://http-proxy-mitmproxy:8080 36 | - HTTPS_PROXY=http://http-proxy-mitmproxy:8080 37 | - NO_PROXY=127.0.0.1,localhost 38 | volumes: 39 | - mitmproxy-ca:/home/mitmproxy/.mitmproxy 40 | - ../..:/workspaces 41 | # dns: 42 | # - 172.33.0.10 43 | networks: 44 | - no-internet-proxy 45 | - no-internet-dns 46 | # http-proxy-squid: 47 | # image: ubuntu/squid:latest 48 | # networks: 49 | # - no-internet-proxy 50 | # - internet 51 | # http-proxy-privoxy: 52 | # build: http-proxy-privoxy 53 | # networks: 54 | # - no-internet-proxy 55 | # - internet 56 | http-proxy-mitmproxy: 57 | image: mitmproxy/mitmproxy:latest 58 | command: mitmdump 59 | volumes: 60 | - mitmproxy-ca:/home/mitmproxy/.mitmproxy 61 | networks: 62 | - no-internet-proxy 63 | - internet 64 | # dns-proxy: 65 | # image: defreitas/dns-proxy-server:latest 66 | # networks: 67 | # no-internet-dns: 68 | # ipv4_address: 172.33.0.10 69 | # internet: 70 | 71 | networks: 72 | no-internet-proxy: 73 | driver: bridge 74 | internal: true 75 | no-internet-dns: 76 | ipam: 77 | config: 78 | - subnet: 172.33.0.0/16 79 | driver: bridge 80 | internal: true 81 | internet: 82 | driver: bridge 83 | 84 | volumes: 85 | mitmproxy-ca: 86 | -------------------------------------------------------------------------------- /.devcontainer/http-proxy-privoxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y \ 5 | privoxy \ 6 | iputils-ping \ 7 | dnsutils \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | RUN sed -e 's/^listen-address/# \0/' -i /etc/privoxy/config 11 | RUN echo "listen-address :8118" >>/etc/privoxy/config 12 | 13 | CMD /etc/init.d/privoxy start && sleep inf 14 | -------------------------------------------------------------------------------- /.devcontainer/https-proxy-test/.gitignore: -------------------------------------------------------------------------------- 1 | mitmproxy-config -------------------------------------------------------------------------------- /.devcontainer/https-proxy-test/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "mitmweb", 6 | "type": "shell", 7 | "command": "mitmweb --web-host '*'", 8 | "runOptions": { 9 | "runOn": "folderOpen" 10 | } 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.devcontainer/https-proxy-test/README.md: -------------------------------------------------------------------------------- 1 | ## HTTPS Proxy Test 2 | 3 | - `Dev Containers: Reopen in Container` > `HTTPS Proxy Test`. 4 | - First time: Install the certificate from `.devcontainer/https-proxy-test/mitmproxy-config` in the OS and restart VS Code. See https://docs.mitmproxy.org/stable/concepts-certificates/#installing-the-mitmproxy-ca-certificate-manually. 5 | - Add the user setting `"http.proxy": "https://localhost:8080"`. 6 | - Install GitHub Copilot Chat and use `Developer: GitHub Copilot Chat Diagnostics` to test connections with a HTTPS proxy. Use a second window to test connections from a local extension host. 7 | - Verify in the log terminal of the dev container that the proxy is being used. 8 | 9 | Note: Due to an issue in mitmproxy, Electron's `fetch` currently doesn't work. Add the user setting `"github.copilot.advanced.debug.useElectronFetcher": false` as a workaround. -------------------------------------------------------------------------------- /.devcontainer/https-proxy-test/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTTPS Proxy Test", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "devcontainer", 5 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/.devcontainer/https-proxy-test" 6 | } 7 | -------------------------------------------------------------------------------- /.devcontainer/https-proxy-test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | devcontainer: 5 | image: mitmproxy/mitmproxy 6 | volumes: 7 | - ../../..:/workspaces 8 | - ./mitmproxy-config:/root/.mitmproxy 9 | ports: 10 | - "127.0.0.1:8080:8080" 11 | - "127.0.0.1:8081:8081" 12 | command: sleep inf 13 | -------------------------------------------------------------------------------- /.devcontainer/vscode/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/typescript-node:16-bullseye 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y \ 5 | iputils-ping \ 6 | dnsutils \ 7 | libnss3-tools \ 8 | chromium \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | RUN wget "https://code.visualstudio.com/sha/download?build=insider&os=cli-alpine-$([ "`uname -m`" = "aarch64" ] && echo "arm64" || echo "x64")" -O- | tar -xz -C /usr/local/bin/ 12 | RUN mv /usr/local/bin/code-insiders /usr/local/bin/code-cli-insiders 13 | 14 | ADD install-vscode.sh /root/ 15 | RUN /root/install-vscode.sh 16 | ADD --chown=node:node ["settings.json", "/home/node/.config/Code - Insiders/User/settings.json"] 17 | 18 | CMD sleep inf 19 | -------------------------------------------------------------------------------- /.devcontainer/vscode/install-vscode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | apt update 4 | apt install -y wget gpg 5 | 6 | wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg 7 | install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg 8 | sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list' 9 | rm -f packages.microsoft.gpg 10 | 11 | apt update 12 | apt install -y code-insiders libsecret-1-dev libxkbfile-dev 13 | -------------------------------------------------------------------------------- /.devcontainer/vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "keyboard.dispatch": "keyCode" 3 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.bat eol=crlf 4 | *.cmd eol=crlf 5 | *.ps1 eol=lf 6 | *.sh eol=lf 7 | *.rtf -text 8 | **/*.json linguist-language=jsonc 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.pem 3 | out 4 | *.tgz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | *.pem 4 | tests 5 | tsconfig.json 6 | src 7 | *.tgz 8 | azure-pipelines 9 | .devcontainer 10 | .gitattributes 11 | CredScanSuppressions.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | Notable changes will be documented here. 3 | 4 | ## [0.32.0] 5 | - Check both system certificates settings for `fetch` ([microsoft/vscode-proxy-agent#66](https://github.com/microsoft/vscode-proxy-agent/pull/66)) 6 | 7 | ## [0.31.0] 8 | - Fix basic auth for fetch ([microsoft/vscode#239033](https://github.com/microsoft/vscode/issues/239033)) 9 | 10 | ## [0.30.0] 11 | - useHostProxy > isUseHostProxyEnabled() ([microsoft/vscode-copilot-release#3821](https://github.com/microsoft/vscode-copilot-release/issues/3821)) 12 | 13 | ## [0.29.0] 14 | - Update to undici 7.2.0 ([microsoft/vscode-proxy-agent#57](https://github.com/microsoft/vscode-proxy-agent/pull/57)) 15 | - Get options from unpatched agents ([microsoft/vscode-proxy-agent#58](https://github.com/microsoft/vscode-proxy-agent/pull/58)) 16 | 17 | ## [0.28.0] 18 | - Pass-through with socketPath ([microsoft/vscode#236423](https://github.com/microsoft/vscode/issues/236423)) 19 | 20 | ## [0.27.0] 21 | - Add system certificates to https proxy requests ([microsoft/vscode#235410](https://github.com/microsoft/vscode/issues/235410)) 22 | 23 | ## [0.26.0] 24 | - Move fetch patching to agent package ([microsoft/vscode#228697](https://github.com/microsoft/vscode/issues/228697)) 25 | 26 | ## [0.25.0] 27 | - Do not overwrite https.Agent certificates ([microsoft/vscode#234175](https://github.com/microsoft/vscode/issues/234175)) 28 | 29 | ## [0.24.0] 30 | - Skip keepAlive flag ([microsoft/vscode#228872](https://github.com/microsoft/vscode/issues/228872)) 31 | - Refactor for reuse with fetch ([microsoft/vscode#228697](https://github.com/microsoft/vscode/issues/228697)) 32 | 33 | ## [0.23.0] 34 | - Pass on keepAlive flag ([microsoft/vscode#173861](https://github.com/microsoft/vscode/issues/173861)) 35 | 36 | ## [0.22.0] 37 | - Set agent protocol ([microsoft/vscode-extension-test-runner#42](https://github.com/microsoft/vscode-extension-test-runner/issues/42)) 38 | 39 | ## [0.21.0] 40 | - Add NO_PROXY setting to be passed from config ([microsoft/vscode#211956](https://github.com/microsoft/vscode/issues/211956)) 41 | 42 | ## [0.20.0] 43 | - Update socks to avoid CVE-2024-29415 44 | 45 | ## [0.19.0] 46 | - Also check for /etc/ssl/ca-bundle.pem ([microsoft/vscode#203847](https://github.com/microsoft/vscode/issues/203847)) 47 | 48 | ## [0.18.0] 49 | - Async callback for additional certificates ([microsoft/vscode-remote-release#9176](https://github.com/microsoft/vscode-remote-release/issues/9176)) 50 | 51 | ## [0.17.0] 52 | - Add auth callback and Kerberos test setup ([microsoft/vscode#187456](https://github.com/microsoft/vscode/issues/187456)) 53 | 54 | ## [0.16.0] 55 | - Update dependencies. 56 | 57 | ## [0.15.0] 58 | - Skip expired certificates ([microsoft/vscode#184271](https://github.com/microsoft/vscode/issues/184271)) 59 | - Handle additional socks schemes ([microsoft/vscode#158669](https://github.com/microsoft/vscode/issues/158669)) 60 | - Ensure early writes are queued ([microsoft/vscode#185098](https://github.com/microsoft/vscode/issues/185098)) 61 | 62 | ## [0.14.1] 63 | - Load certificates in net.connect ([microsoft/vscode#185098](https://github.com/microsoft/vscode/issues/185098)) 64 | 65 | ## [0.14.0] 66 | - Load certificates in tls.connect ([microsoft/vscode#185098](https://github.com/microsoft/vscode/issues/185098)) 67 | 68 | ## [0.13.0] 69 | - Rename to @vscode/proxy-agent. 70 | 71 | ## [0.12.0] 72 | - Avoid buffer deprecation warning (fixes [microsoft/vscode#136874](https://github.com/microsoft/vscode/issues/136874)) 73 | 74 | ## [0.11.0] 75 | - Override original agent again (fixes [microsoft/vscode#117054](https://github.com/microsoft/vscode/issues/117054)) 76 | 77 | ## [0.10.0] 78 | - Do not override original agent (forward port [microsoft/vscode#120354](https://github.com/microsoft/vscode/issues/120354)) 79 | - Move vscode-windows-ca-certs dependency ([microsoft/vscode#120546](https://github.com/microsoft/vscode/issues/120546)) 80 | 81 | ## [0.9.0] 82 | - Copy and adapt pac-proxy-agent to catch up with latest dependencies and bug fixes. 83 | 84 | ## [0.8.2] 85 | - Do not override original agent (fixes [microsoft/vscode#120354](https://github.com/microsoft/vscode/issues/120354)) 86 | 87 | ## [0.8.0] 88 | - Align log level constants with VS Code. 89 | 90 | ## [0.7.0] 91 | - Override original agent (fixes [microsoft/vscode#117054](https://github.com/microsoft/vscode/issues/117054)) 92 | 93 | ## [0.6.0] 94 | - Use TypeScript. 95 | - Move proxy resolution from VS Code here. 96 | 97 | ## [0.5.2] 98 | - Handle false as the original proxy. 99 | - Update typings. 100 | 101 | ## [0.5.1] 102 | - Allow for newer patch versions of dependencies. 103 | 104 | ## [0.5.0] 105 | - Update to https-proxy-agent 2.2.3 (https://nodesecurity.io/advisories/1184) 106 | 107 | ## [0.4.0] 108 | - Fall back to original agent when provided in options. 109 | - Add default port to options. 110 | 111 | ## [0.3.0] 112 | - Forward request and options to `resolveProxy`. 113 | 114 | ## [0.2.0] 115 | - Fix missing servername for SNI ([#27](https://github.com/Microsoft/vscode/issues/64133)). 116 | 117 | ## [0.1.0] 118 | - Initial release -------------------------------------------------------------------------------- /CredScanSuppressions.json: -------------------------------------------------------------------------------- 1 | { 2 | "tool": "Credential Scanner", 3 | "suppressions": [ 4 | { 5 | "file": [ 6 | ".devcontainer/basic-auth-test/docker-compose.yml" 7 | ], 8 | "_justification": "These are dummy credentials for testing." 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Nathan Rajlich <nathan@tootallnate.net> 4 | Copyright (c) 2015 Félicien François <felicien@tweakstyle.com> 5 | Copyright (c) Microsoft Corporation. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VS Code Proxy Agent 2 | 3 | Adaption of the [electron-proxy-agent](https://github.com/felicienfrancois/node-electron-proxy-agent) for Visual Studio Code. 4 | 5 | ## Contributing 6 | 7 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 8 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 9 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 10 | 11 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 12 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 13 | provided by the bot. You will only need to do this once across all repos using our CLA. 14 | 15 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 16 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 17 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 18 | 19 | ## License 20 | [MIT](LICENSE.md) -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /azure-pipelines/publish.yml: -------------------------------------------------------------------------------- 1 | name: $(Date:yyyyMMdd)$(Rev:.r) 2 | 3 | trigger: 4 | branches: 5 | include: 6 | - main 7 | pr: none 8 | 9 | resources: 10 | repositories: 11 | - repository: templates 12 | type: github 13 | name: microsoft/vscode-engineering 14 | ref: main 15 | endpoint: Monaco 16 | 17 | parameters: 18 | - name: publishPackage 19 | displayName: 🚀 Publish vscode-proxy-agent 20 | type: boolean 21 | default: false 22 | 23 | extends: 24 | template: azure-pipelines/npm-package/pipeline.yml@templates 25 | parameters: 26 | npmPackages: 27 | - name: vscode-proxy-agent 28 | 29 | buildSteps: 30 | - script: npm ci 31 | displayName: Install dependencies 32 | - script: npm run compile 33 | displayName: Compile 34 | 35 | testPlatforms: {} 36 | publishPackage: ${{ parameters.publishPackage }} -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode/proxy-agent", 3 | "version": "0.32.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@vscode/proxy-agent", 9 | "version": "0.32.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@tootallnate/once": "^3.0.0", 13 | "agent-base": "^7.0.1", 14 | "debug": "^4.3.4", 15 | "http-proxy-agent": "^7.0.0", 16 | "https-proxy-agent": "^7.0.2", 17 | "socks-proxy-agent": "^8.0.1", 18 | "undici": "^7.2.0" 19 | }, 20 | "devDependencies": { 21 | "@types/debug": "^4.1.9", 22 | "@types/node": "^20.8.4", 23 | "typescript": "^5.2.2" 24 | }, 25 | "optionalDependencies": { 26 | "@vscode/windows-ca-certs": "^0.3.1" 27 | } 28 | }, 29 | "node_modules/@tootallnate/once": { 30 | "version": "3.0.0", 31 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-3.0.0.tgz", 32 | "integrity": "sha512-OAdBVB7rlwvLD+DiecSAyVKzKVmSfXbouCyM5I6wHGi4MGXIyFqErg1IvyJ7PI1e+GYZuZh7cCHV/c4LA8SKMw==", 33 | "engines": { 34 | "node": ">= 10" 35 | } 36 | }, 37 | "node_modules/@types/debug": { 38 | "version": "4.1.9", 39 | "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz", 40 | "integrity": "sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==", 41 | "dev": true, 42 | "dependencies": { 43 | "@types/ms": "*" 44 | } 45 | }, 46 | "node_modules/@types/ms": { 47 | "version": "0.7.31", 48 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", 49 | "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", 50 | "dev": true 51 | }, 52 | "node_modules/@types/node": { 53 | "version": "20.8.4", 54 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.4.tgz", 55 | "integrity": "sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==", 56 | "dev": true, 57 | "dependencies": { 58 | "undici-types": "~5.25.1" 59 | } 60 | }, 61 | "node_modules/@vscode/windows-ca-certs": { 62 | "version": "0.3.1", 63 | "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.1.tgz", 64 | "integrity": "sha512-1B6hZAsqg125wuMsXiKIFkBgKx/J7YR4RT/ccYGkWAToPU9MVa40PRe+evLFUmLPH6NmPohEPlCzZLbqgvHCcQ==", 65 | "hasInstallScript": true, 66 | "optional": true, 67 | "os": [ 68 | "win32" 69 | ], 70 | "dependencies": { 71 | "node-addon-api": "^3.0.2" 72 | } 73 | }, 74 | "node_modules/agent-base": { 75 | "version": "7.1.0", 76 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", 77 | "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", 78 | "dependencies": { 79 | "debug": "^4.3.4" 80 | }, 81 | "engines": { 82 | "node": ">= 14" 83 | } 84 | }, 85 | "node_modules/debug": { 86 | "version": "4.3.4", 87 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 88 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 89 | "dependencies": { 90 | "ms": "2.1.2" 91 | }, 92 | "engines": { 93 | "node": ">=6.0" 94 | }, 95 | "peerDependenciesMeta": { 96 | "supports-color": { 97 | "optional": true 98 | } 99 | } 100 | }, 101 | "node_modules/http-proxy-agent": { 102 | "version": "7.0.0", 103 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", 104 | "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", 105 | "dependencies": { 106 | "agent-base": "^7.1.0", 107 | "debug": "^4.3.4" 108 | }, 109 | "engines": { 110 | "node": ">= 14" 111 | } 112 | }, 113 | "node_modules/https-proxy-agent": { 114 | "version": "7.0.2", 115 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", 116 | "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", 117 | "dependencies": { 118 | "agent-base": "^7.0.2", 119 | "debug": "4" 120 | }, 121 | "engines": { 122 | "node": ">= 14" 123 | } 124 | }, 125 | "node_modules/ip-address": { 126 | "version": "9.0.5", 127 | "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", 128 | "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", 129 | "dependencies": { 130 | "jsbn": "1.1.0", 131 | "sprintf-js": "^1.1.3" 132 | }, 133 | "engines": { 134 | "node": ">= 12" 135 | } 136 | }, 137 | "node_modules/jsbn": { 138 | "version": "1.1.0", 139 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", 140 | "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" 141 | }, 142 | "node_modules/ms": { 143 | "version": "2.1.2", 144 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 145 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 146 | }, 147 | "node_modules/node-addon-api": { 148 | "version": "3.2.1", 149 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", 150 | "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", 151 | "optional": true 152 | }, 153 | "node_modules/smart-buffer": { 154 | "version": "4.2.0", 155 | "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", 156 | "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", 157 | "engines": { 158 | "node": ">= 6.0.0", 159 | "npm": ">= 3.0.0" 160 | } 161 | }, 162 | "node_modules/socks": { 163 | "version": "2.8.3", 164 | "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", 165 | "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", 166 | "dependencies": { 167 | "ip-address": "^9.0.5", 168 | "smart-buffer": "^4.2.0" 169 | }, 170 | "engines": { 171 | "node": ">= 10.0.0", 172 | "npm": ">= 3.0.0" 173 | } 174 | }, 175 | "node_modules/socks-proxy-agent": { 176 | "version": "8.0.1", 177 | "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", 178 | "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", 179 | "dependencies": { 180 | "agent-base": "^7.0.1", 181 | "debug": "^4.3.4", 182 | "socks": "^2.7.1" 183 | }, 184 | "engines": { 185 | "node": ">= 14" 186 | } 187 | }, 188 | "node_modules/sprintf-js": { 189 | "version": "1.1.3", 190 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", 191 | "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" 192 | }, 193 | "node_modules/typescript": { 194 | "version": "5.2.2", 195 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 196 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 197 | "dev": true, 198 | "bin": { 199 | "tsc": "bin/tsc", 200 | "tsserver": "bin/tsserver" 201 | }, 202 | "engines": { 203 | "node": ">=14.17" 204 | } 205 | }, 206 | "node_modules/undici": { 207 | "version": "7.5.0", 208 | "resolved": "https://registry.npmjs.org/undici/-/undici-7.5.0.tgz", 209 | "integrity": "sha512-NFQG741e8mJ0fLQk90xKxFdaSM7z4+IQpAgsFI36bCDY9Z2+aXXZjVy2uUksMouWfMI9+w5ejOq5zYYTBCQJDQ==", 210 | "license": "MIT", 211 | "engines": { 212 | "node": ">=20.18.1" 213 | } 214 | }, 215 | "node_modules/undici-types": { 216 | "version": "5.25.3", 217 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", 218 | "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", 219 | "dev": true 220 | } 221 | }, 222 | "dependencies": { 223 | "@tootallnate/once": { 224 | "version": "3.0.0", 225 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-3.0.0.tgz", 226 | "integrity": "sha512-OAdBVB7rlwvLD+DiecSAyVKzKVmSfXbouCyM5I6wHGi4MGXIyFqErg1IvyJ7PI1e+GYZuZh7cCHV/c4LA8SKMw==" 227 | }, 228 | "@types/debug": { 229 | "version": "4.1.9", 230 | "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz", 231 | "integrity": "sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==", 232 | "dev": true, 233 | "requires": { 234 | "@types/ms": "*" 235 | } 236 | }, 237 | "@types/ms": { 238 | "version": "0.7.31", 239 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", 240 | "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", 241 | "dev": true 242 | }, 243 | "@types/node": { 244 | "version": "20.8.4", 245 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.4.tgz", 246 | "integrity": "sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==", 247 | "dev": true, 248 | "requires": { 249 | "undici-types": "~5.25.1" 250 | } 251 | }, 252 | "@vscode/windows-ca-certs": { 253 | "version": "0.3.1", 254 | "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.1.tgz", 255 | "integrity": "sha512-1B6hZAsqg125wuMsXiKIFkBgKx/J7YR4RT/ccYGkWAToPU9MVa40PRe+evLFUmLPH6NmPohEPlCzZLbqgvHCcQ==", 256 | "optional": true, 257 | "requires": { 258 | "node-addon-api": "^3.0.2" 259 | } 260 | }, 261 | "agent-base": { 262 | "version": "7.1.0", 263 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", 264 | "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", 265 | "requires": { 266 | "debug": "^4.3.4" 267 | } 268 | }, 269 | "debug": { 270 | "version": "4.3.4", 271 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 272 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 273 | "requires": { 274 | "ms": "2.1.2" 275 | } 276 | }, 277 | "http-proxy-agent": { 278 | "version": "7.0.0", 279 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", 280 | "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", 281 | "requires": { 282 | "agent-base": "^7.1.0", 283 | "debug": "^4.3.4" 284 | } 285 | }, 286 | "https-proxy-agent": { 287 | "version": "7.0.2", 288 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", 289 | "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", 290 | "requires": { 291 | "agent-base": "^7.0.2", 292 | "debug": "4" 293 | } 294 | }, 295 | "ip-address": { 296 | "version": "9.0.5", 297 | "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", 298 | "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", 299 | "requires": { 300 | "jsbn": "1.1.0", 301 | "sprintf-js": "^1.1.3" 302 | } 303 | }, 304 | "jsbn": { 305 | "version": "1.1.0", 306 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", 307 | "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" 308 | }, 309 | "ms": { 310 | "version": "2.1.2", 311 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 312 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 313 | }, 314 | "node-addon-api": { 315 | "version": "3.2.1", 316 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", 317 | "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", 318 | "optional": true 319 | }, 320 | "smart-buffer": { 321 | "version": "4.2.0", 322 | "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", 323 | "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" 324 | }, 325 | "socks": { 326 | "version": "2.8.3", 327 | "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", 328 | "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", 329 | "requires": { 330 | "ip-address": "^9.0.5", 331 | "smart-buffer": "^4.2.0" 332 | } 333 | }, 334 | "socks-proxy-agent": { 335 | "version": "8.0.1", 336 | "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", 337 | "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", 338 | "requires": { 339 | "agent-base": "^7.0.1", 340 | "debug": "^4.3.4", 341 | "socks": "^2.7.1" 342 | } 343 | }, 344 | "sprintf-js": { 345 | "version": "1.1.3", 346 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", 347 | "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" 348 | }, 349 | "typescript": { 350 | "version": "5.2.2", 351 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 352 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 353 | "dev": true 354 | }, 355 | "undici": { 356 | "version": "7.5.0", 357 | "resolved": "https://registry.npmjs.org/undici/-/undici-7.5.0.tgz", 358 | "integrity": "sha512-NFQG741e8mJ0fLQk90xKxFdaSM7z4+IQpAgsFI36bCDY9Z2+aXXZjVy2uUksMouWfMI9+w5ejOq5zYYTBCQJDQ==" 359 | }, 360 | "undici-types": { 361 | "version": "5.25.3", 362 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", 363 | "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", 364 | "dev": true 365 | } 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode/proxy-agent", 3 | "version": "0.32.0", 4 | "description": "NodeJS http(s) agent implementation for VS Code", 5 | "main": "out/index.js", 6 | "types": "out/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/microsoft/vscode-proxy-agent.git" 10 | }, 11 | "keywords": [ 12 | "proxy", 13 | "agent", 14 | "http", 15 | "https", 16 | "socks", 17 | "request", 18 | "access" 19 | ], 20 | "authors": [ 21 | "Nathan Rajlich (http://n8.io/)", 22 | "Félicien François ", 23 | "Microsoft Corporation" 24 | ], 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/microsoft/vscode-proxy-agent/issues" 28 | }, 29 | "homepage": "https://github.com/microsoft/vscode-proxy-agent", 30 | "dependencies": { 31 | "@tootallnate/once": "^3.0.0", 32 | "agent-base": "^7.0.1", 33 | "debug": "^4.3.4", 34 | "http-proxy-agent": "^7.0.0", 35 | "https-proxy-agent": "^7.0.2", 36 | "socks-proxy-agent": "^8.0.1", 37 | "undici": "^7.2.0" 38 | }, 39 | "devDependencies": { 40 | "@types/debug": "^4.1.9", 41 | "@types/node": "^20.8.4", 42 | "typescript": "^5.2.2" 43 | }, 44 | "scripts": { 45 | "compile": "tsc -p ./", 46 | "watch": "tsc -watch -p ./" 47 | }, 48 | "optionalDependencies": { 49 | "@vscode/windows-ca-certs": "^0.3.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/agent.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import https from 'https'; 3 | import net from 'net'; 4 | import createDebug from 'debug'; 5 | import { Readable, Duplex } from 'stream'; 6 | import { format } from 'url'; 7 | import { HttpProxyAgent, HttpProxyAgentOptions } from 'http-proxy-agent'; 8 | import { HttpsProxyAgent, HttpsProxyAgentOptions } from 'https-proxy-agent'; 9 | import { SocksProxyAgent, SocksProxyAgentOptions } from 'socks-proxy-agent'; 10 | import { 11 | Agent, 12 | AgentConnectOpts, 13 | } from 'agent-base'; 14 | import EventEmitter from 'events'; 15 | 16 | const debug = createDebug('pac-proxy-agent'); 17 | 18 | type FindProxyForURL = (req: http.ClientRequest, opts: http.RequestOptions, url: string) => Promise; 19 | 20 | /** 21 | * The `PacProxyAgent` class. 22 | * 23 | * A few different "protocol" modes are supported (supported protocols are 24 | * backed by the `get-uri` module): 25 | * 26 | * - "pac+data", "data" - refers to an embedded "data:" URI 27 | * - "pac+file", "file" - refers to a local file 28 | * - "pac+ftp", "ftp" - refers to a file located on an FTP server 29 | * - "pac+http", "http" - refers to an HTTP endpoint 30 | * - "pac+https", "https" - refers to an HTTPS endpoint 31 | * 32 | * @api public 33 | */ 34 | export class PacProxyAgent extends Agent { 35 | resolver: FindProxyForURL; 36 | opts: PacProxyAgentOptions; 37 | addCAs: (opts: PacProxyAgentOptions) => Promise; 38 | casAdded = false; 39 | cache?: Readable; 40 | 41 | constructor(resolver: FindProxyForURL, opts: PacProxyAgentOptions = {}, addCAs: (opts: PacProxyAgentOptions) => Promise = async () => {}) { 42 | super(opts); 43 | debug('Creating PacProxyAgent with options %o', opts); 44 | 45 | this.resolver = resolver; 46 | this.opts = { ...opts }; 47 | this.addCAs = addCAs; 48 | this.cache = undefined; 49 | } 50 | 51 | /** 52 | * Called when the node-core HTTP client library is creating a new HTTP request. 53 | * 54 | * @api protected 55 | */ 56 | async connect(req: http.ClientRequest, opts: AgentConnectOpts): Promise { 57 | const { secureEndpoint } = opts; 58 | 59 | // Calculate the `url` parameter 60 | const defaultPort = secureEndpoint ? 443 : 80; 61 | let path = req.path; 62 | let search: string | null = null; 63 | const firstQuestion = path.indexOf('?'); 64 | if (firstQuestion !== -1) { 65 | search = path.substring(firstQuestion); 66 | path = path.substring(0, firstQuestion); 67 | } 68 | 69 | const urlOpts = { 70 | ...opts, 71 | protocol: secureEndpoint ? 'https:' : 'http:', 72 | pathname: path, 73 | search, 74 | 75 | // need to use `hostname` instead of `host` otherwise `port` is ignored 76 | hostname: opts.host, 77 | host: null, 78 | href: null, 79 | 80 | // set `port` to null when it is the protocol default port (80 / 443) 81 | port: defaultPort === opts.port ? null : opts.port 82 | }; 83 | const url = format(urlOpts); 84 | 85 | debug('url: %o', url); 86 | let result = await this.resolver(req, opts, url); 87 | 88 | const { proxy, url: proxyURL } = getProxyURLFromResolverResult(result); 89 | 90 | let agent: http.Agent | null = null; 91 | if (!proxyURL) { 92 | // Needed for SNI. 93 | const originalAgent = this.opts.originalAgent; 94 | const defaultAgent = secureEndpoint ? https.globalAgent : http.globalAgent; 95 | agent = originalAgent === false ? new (defaultAgent as any).constructor() : (originalAgent || defaultAgent) 96 | } else if (proxyURL.startsWith('socks')) { 97 | // Use a SOCKSv5h or SOCKSv4a proxy 98 | agent = new SocksProxyAgent(proxyURL); 99 | } else if (proxyURL.startsWith('http')) { 100 | // Use an HTTP or HTTPS proxy 101 | // http://dev.chromium.org/developers/design-documents/secure-web-proxy 102 | if (!this.casAdded && proxyURL.startsWith('https')) { 103 | debug('Adding CAs to proxy options'); 104 | this.casAdded = true; 105 | await this.addCAs(this.opts); 106 | } 107 | if (secureEndpoint) { 108 | agent = new HttpsProxyAgent2(proxyURL, this.opts); 109 | } else { 110 | agent = new HttpProxyAgent(proxyURL, this.opts); 111 | } 112 | } 113 | 114 | try { 115 | if (agent) { 116 | let s: Duplex | http.Agent; 117 | if (agent instanceof Agent) { 118 | s = await agent.connect(req, opts); 119 | } else { 120 | s = agent; 121 | } 122 | req.emit('proxy', { proxy, socket: s }); 123 | return s; 124 | } 125 | throw new Error(`Could not determine proxy type for: ${proxy}`); 126 | } catch (err) { 127 | debug('Got error for proxy %o: %o', proxy, err); 128 | req.emit('proxy', { proxy, error: err }); 129 | } 130 | 131 | throw new Error(`Failed to establish a socket connection to proxies: ${result}`); 132 | } 133 | } 134 | 135 | export function getProxyURLFromResolverResult(result: string | undefined) { 136 | // Default to "DIRECT" if a falsey value was returned (or nothing) 137 | if (!result) { 138 | return { proxy: 'DIRECT', url: undefined }; 139 | } 140 | 141 | const proxies = String(result) 142 | .trim() 143 | .split(/\s*;\s*/g) 144 | .filter(Boolean); 145 | 146 | for (const proxy of proxies) { 147 | const [type, target] = proxy.split(/\s+/); 148 | debug('Attempting to use proxy: %o', proxy); 149 | 150 | if (type === 'DIRECT') { 151 | return { proxy, url: undefined }; 152 | } else if (type === 'SOCKS' || type === 'SOCKS5') { 153 | // Use a SOCKSv5h proxy 154 | return { proxy, url: `socks://${target}` }; 155 | } else if (type === 'SOCKS4') { 156 | // Use a SOCKSv4a proxy 157 | return { proxy, url: `socks4a://${target}` }; 158 | } else if ( 159 | type === 'PROXY' || 160 | type === 'HTTP' || 161 | type === 'HTTPS' 162 | ) { 163 | // Use an HTTP or HTTPS proxy 164 | // http://dev.chromium.org/developers/design-documents/secure-web-proxy 165 | return { proxy, url: `${type === 'HTTPS' ? 'https' : 'http'}://${target}` }; 166 | } 167 | } 168 | return { proxy: 'DIRECT', url: undefined }; 169 | } 170 | 171 | type LookupProxyAuthorization = (proxyURL: string, proxyAuthenticate: string | string[] | undefined, state: Record) => Promise; 172 | 173 | type HttpsProxyAgentOptions2 = HttpsProxyAgentOptions & { lookupProxyAuthorization?: LookupProxyAuthorization }; 174 | 175 | interface ConnectResponse { 176 | statusCode: number; 177 | statusText: string; 178 | headers: http.IncomingHttpHeaders; 179 | } 180 | 181 | class HttpsProxyAgent2 extends HttpsProxyAgent { 182 | 183 | addHeaders: http.OutgoingHttpHeaders; 184 | lookupProxyAuthorization?: LookupProxyAuthorization; 185 | 186 | constructor(proxy: Uri | URL, opts: HttpsProxyAgentOptions2) { 187 | const addHeaders = {}; 188 | const origHeaders = opts?.headers; 189 | const agentOpts: HttpsProxyAgentOptions = { 190 | ...opts, 191 | headers: (): http.OutgoingHttpHeaders => { 192 | const headers = origHeaders 193 | ? typeof origHeaders === 'function' 194 | ? origHeaders() 195 | : origHeaders 196 | : {}; 197 | return { 198 | ...headers, 199 | ...addHeaders 200 | }; 201 | } 202 | }; 203 | super(proxy, agentOpts); 204 | this.addHeaders = addHeaders; 205 | this.lookupProxyAuthorization = opts.lookupProxyAuthorization; 206 | } 207 | 208 | async connect(req: http.ClientRequest, opts: AgentConnectOpts, state: Record = {}): Promise { 209 | const tmpReq = new EventEmitter(); 210 | let connect: ConnectResponse | undefined; 211 | tmpReq.once('proxyConnect', (_connect: ConnectResponse) => { 212 | connect = _connect; 213 | }); 214 | if (this.lookupProxyAuthorization && !this.addHeaders['Proxy-Authorization']) { 215 | try { 216 | const proxyAuthorization = await this.lookupProxyAuthorization(this.proxy.href, undefined, state); 217 | if (proxyAuthorization) { 218 | this.addHeaders['Proxy-Authorization'] = proxyAuthorization; 219 | } 220 | } catch (err) { 221 | req.emit('error', err); 222 | } 223 | } 224 | const s = await super.connect(tmpReq as any, opts); 225 | const proxyAuthenticate = connect?.headers['proxy-authenticate'] as string | string[] | undefined; 226 | if (this.lookupProxyAuthorization && connect?.statusCode === 407 && proxyAuthenticate) { 227 | try { 228 | const proxyAuthorization = await this.lookupProxyAuthorization(this.proxy.href, proxyAuthenticate, state); 229 | if (proxyAuthorization) { 230 | this.addHeaders['Proxy-Authorization'] = proxyAuthorization; 231 | tmpReq.removeAllListeners(); 232 | s.destroy(); 233 | return this.connect(req, opts, state); 234 | } 235 | } catch (err) { 236 | req.emit('error', err); 237 | } 238 | } 239 | req.once('socket', s => tmpReq.emit('socket', s)); 240 | return s; 241 | } 242 | } 243 | 244 | export function createPacProxyAgent( 245 | resolver: FindProxyForURL, 246 | opts?: PacProxyAgentOptions, 247 | addCAs?: (opts: PacProxyAgentOptions) => Promise, 248 | ): PacProxyAgent { 249 | if (!opts) { 250 | opts = {}; 251 | } 252 | 253 | if (typeof resolver !== 'function') { 254 | throw new TypeError('a resolve function must be specified!'); 255 | } 256 | 257 | return new PacProxyAgent(resolver, opts, addCAs); 258 | } 259 | type PacProxyAgentOptions = 260 | HttpProxyAgentOptions<''> & 261 | HttpsProxyAgentOptions2<''> & 262 | SocksProxyAgentOptions & { 263 | fallbackToDirect?: boolean; 264 | originalAgent?: false | http.Agent; 265 | _vscodeTestReplaceCaCerts?: boolean; 266 | } 267 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Nathan Rajlich, Félicien François, Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as net from 'net'; 7 | import * as http from 'http'; 8 | import type * as https from 'https'; 9 | import * as tls from 'tls'; 10 | import * as nodeurl from 'url'; 11 | import * as os from 'os'; 12 | import * as fs from 'fs'; 13 | import * as cp from 'child_process'; 14 | import * as crypto from 'crypto'; 15 | import * as undici from 'undici'; 16 | import * as stream from 'stream'; 17 | 18 | import { createPacProxyAgent, getProxyURLFromResolverResult, PacProxyAgent } from './agent'; 19 | import type { IncomingHttpHeaders } from 'undici/types/header'; 20 | 21 | export enum LogLevel { 22 | Trace, 23 | Debug, 24 | Info, 25 | Warning, 26 | Error, 27 | Critical, 28 | Off 29 | } 30 | 31 | export type ProxyResolveEvent = { 32 | count: number; 33 | duration: number; 34 | errorCount: number; 35 | cacheCount: number; 36 | cacheSize: number; 37 | cacheRolls: number; 38 | envCount: number; 39 | settingsCount: number; 40 | localhostCount: number; 41 | envNoProxyCount: number; 42 | configNoProxyCount: number; 43 | results: ConnectionResult[]; 44 | }; 45 | 46 | interface ConnectionResult { 47 | proxy: string; 48 | connection: string; 49 | code: string; 50 | count: number; 51 | } 52 | 53 | const maxCacheEntries = 5000; // Cache can grow twice that much due to 'oldCache'. 54 | 55 | export type LookupProxyAuthorization = (proxyURL: string, proxyAuthenticate: string | string[] | undefined, state: Record) => Promise; 56 | 57 | export interface Log { 58 | trace(message: string, ...args: any[]): void; 59 | debug(message: string, ...args: any[]): void; 60 | info(message: string, ...args: any[]): void; 61 | warn(message: string, ...args: any[]): void; 62 | error(message: string | Error, ...args: any[]): void; 63 | } 64 | 65 | export interface ProxyAgentParams { 66 | resolveProxy(url: string): Promise; 67 | getProxyURL: () => string | undefined, 68 | getProxySupport: () => ProxySupportSetting, 69 | getNoProxyConfig?: () => string[], 70 | isAdditionalFetchSupportEnabled: () => boolean, 71 | addCertificatesV1: () => boolean, 72 | addCertificatesV2: () => boolean, 73 | loadAdditionalCertificates(): Promise; 74 | lookupProxyAuthorization?: LookupProxyAuthorization; 75 | log: Log; 76 | getLogLevel(): LogLevel; 77 | proxyResolveTelemetry(event: ProxyResolveEvent): void; 78 | isUseHostProxyEnabled: () => boolean; 79 | env: NodeJS.ProcessEnv; 80 | } 81 | 82 | export function createProxyResolver(params: ProxyAgentParams) { 83 | const { getProxyURL, log, proxyResolveTelemetry: proxyResolverTelemetry, env } = params; 84 | let envProxy = proxyFromConfigURL(env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY); // Not standardized. 85 | 86 | let envNoProxy = noProxyFromEnv(env.no_proxy || env.NO_PROXY); // Not standardized. 87 | 88 | let cacheRolls = 0; 89 | let oldCache = new Map(); 90 | let cache = new Map(); 91 | function getCacheKey(url: nodeurl.UrlWithStringQuery) { 92 | // Expecting proxies to usually be the same per scheme://host:port. Assuming that for performance. 93 | return nodeurl.format({ ...url, ...{ pathname: undefined, search: undefined, hash: undefined } }); 94 | } 95 | function getCachedProxy(key: string) { 96 | let proxy = cache.get(key); 97 | if (proxy) { 98 | return proxy; 99 | } 100 | proxy = oldCache.get(key); 101 | if (proxy) { 102 | oldCache.delete(key); 103 | cacheProxy(key, proxy); 104 | } 105 | return proxy; 106 | } 107 | function cacheProxy(key: string, proxy: string) { 108 | cache.set(key, proxy); 109 | if (cache.size >= maxCacheEntries) { 110 | oldCache = cache; 111 | cache = new Map(); 112 | cacheRolls++; 113 | log.debug('ProxyResolver#cacheProxy cacheRolls', cacheRolls); 114 | } 115 | } 116 | 117 | let timeout: NodeJS.Timer | undefined; 118 | let count = 0; 119 | let duration = 0; 120 | let errorCount = 0; 121 | let cacheCount = 0; 122 | let envCount = 0; 123 | let settingsCount = 0; 124 | let localhostCount = 0; 125 | let envNoProxyCount = 0; 126 | let configNoProxyCount = 0; 127 | let results: ConnectionResult[] = []; 128 | function logEvent() { 129 | timeout = undefined; 130 | proxyResolverTelemetry({ count, duration, errorCount, cacheCount, cacheSize: cache.size, cacheRolls, envCount, settingsCount, localhostCount, envNoProxyCount, configNoProxyCount, results }); 131 | count = duration = errorCount = cacheCount = envCount = settingsCount = localhostCount = envNoProxyCount = configNoProxyCount = 0; 132 | results = []; 133 | } 134 | 135 | function resolveProxyWithRequest(flags: { useProxySettings: boolean, addCertificatesV1: boolean }, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) { 136 | if (!timeout) { 137 | timeout = setTimeout(logEvent, 10 * 60 * 1000); 138 | } 139 | 140 | const stackText = ''; // getLogLevel() === LogLevel.Trace ? '\n' + new Error('Error for stack trace').stack : ''; 141 | 142 | addCertificatesToOptionsV1(params, flags.addCertificatesV1, opts, () => { 143 | if (!flags.useProxySettings) { 144 | callback('DIRECT'); 145 | return; 146 | } 147 | useProxySettings(url, req, stackText, callback); 148 | }); 149 | } 150 | 151 | function useProxySettings(url: string, req: http.ClientRequest | undefined, stackText: string, callback: (proxy?: string) => void) { 152 | const parsedUrl = nodeurl.parse(url); // Coming from Node's URL, sticking with that. 153 | 154 | const hostname = parsedUrl.hostname; 155 | if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '::ffff:127.0.0.1') { 156 | localhostCount++; 157 | callback('DIRECT'); 158 | log.debug('ProxyResolver#resolveProxy localhost', url, 'DIRECT', stackText); 159 | return; 160 | } 161 | 162 | const secureEndpoint = parsedUrl.protocol === 'https:'; 163 | const defaultPort = secureEndpoint ? 443 : 80; 164 | 165 | // if there are any config entries present then env variables are ignored 166 | let noProxyConfig = params.getNoProxyConfig ? params.getNoProxyConfig() : []; 167 | if (noProxyConfig.length) { 168 | let configNoProxy = noProxyFromConfig(noProxyConfig); // Not standardized. 169 | if (typeof hostname === 'string' && configNoProxy(hostname, String(parsedUrl.port || defaultPort))) { 170 | configNoProxyCount++; 171 | callback('DIRECT'); 172 | log.debug('ProxyResolver#resolveProxy configNoProxy', url, 'DIRECT', stackText); 173 | return; 174 | } 175 | } else { 176 | if (typeof hostname === 'string' && envNoProxy(hostname, String(parsedUrl.port || defaultPort))) { 177 | envNoProxyCount++; 178 | callback('DIRECT'); 179 | log.debug('ProxyResolver#resolveProxy envNoProxy', url, 'DIRECT', stackText); 180 | return; 181 | } 182 | } 183 | 184 | let settingsProxy = proxyFromConfigURL(getProxyURL()); 185 | if (settingsProxy) { 186 | settingsCount++; 187 | callback(settingsProxy); 188 | log.debug('ProxyResolver#resolveProxy settings', url, settingsProxy, stackText); 189 | return; 190 | } 191 | 192 | if (envProxy) { 193 | envCount++; 194 | callback(envProxy); 195 | log.debug('ProxyResolver#resolveProxy env', url, envProxy, stackText); 196 | return; 197 | } 198 | 199 | if (!params.isUseHostProxyEnabled()) { 200 | callback('DIRECT'); 201 | log.debug('ProxyResolver#resolveProxy unconfigured', url, 'DIRECT', stackText); 202 | return; 203 | } 204 | 205 | const key = getCacheKey(parsedUrl); 206 | const proxy = getCachedProxy(key); 207 | if (proxy) { 208 | cacheCount++; 209 | if (req) { 210 | collectResult(results, proxy, secureEndpoint ? 'HTTPS' : 'HTTP', req); 211 | } 212 | callback(proxy); 213 | log.debug('ProxyResolver#resolveProxy cached', url, proxy, stackText); 214 | return; 215 | } 216 | 217 | const start = Date.now(); 218 | params.resolveProxy(url) // Use full URL to ensure it is an actually used one. 219 | .then(proxy => { 220 | if (proxy) { 221 | cacheProxy(key, proxy); 222 | if (req) { 223 | collectResult(results, proxy, secureEndpoint ? 'HTTPS' : 'HTTP', req); 224 | } 225 | } 226 | callback(proxy); 227 | log.debug('ProxyResolver#resolveProxy', url, proxy, stackText); 228 | }).then(() => { 229 | count++; 230 | duration = Date.now() - start + duration; 231 | }, err => { 232 | errorCount++; 233 | const fallback: string | undefined = cache.values().next().value; // fall back to any proxy (https://github.com/microsoft/vscode/issues/122825) 234 | callback(fallback); 235 | log.error('ProxyResolver#resolveProxy', fallback, toErrorMessage(err), stackText); 236 | }); 237 | } 238 | 239 | return { 240 | resolveProxyWithRequest, 241 | resolveProxyURL: (url: string) => new Promise((resolve, reject) => { 242 | useProxySettings(url, undefined, '', result => { 243 | try { 244 | resolve(getProxyURLFromResolverResult(result).url); 245 | } catch (err) { 246 | reject(err); 247 | } 248 | }); 249 | }), 250 | }; 251 | } 252 | 253 | function collectResult(results: ConnectionResult[], resolveProxy: string, connection: string, req: http.ClientRequest) { 254 | const proxy = resolveProxy ? String(resolveProxy).trim().split(/\s+/, 1)[0] : 'EMPTY'; 255 | req.on('response', res => { 256 | const code = `HTTP_${res.statusCode}`; 257 | const result = findOrCreateResult(results, proxy, connection, code); 258 | result.count++; 259 | }); 260 | req.on('error', err => { 261 | const code = err && typeof (err).code === 'string' && (err).code || 'UNKNOWN_ERROR'; 262 | const result = findOrCreateResult(results, proxy, connection, code); 263 | result.count++; 264 | }); 265 | } 266 | 267 | function findOrCreateResult(results: ConnectionResult[], proxy: string, connection: string, code: string): ConnectionResult { 268 | for (const result of results) { 269 | if (result.proxy === proxy && result.connection === connection && result.code === code) { 270 | return result; 271 | } 272 | } 273 | const result = { proxy, connection, code, count: 0 }; 274 | results.push(result); 275 | return result; 276 | } 277 | 278 | function proxyFromConfigURL(configURL: string | undefined) { 279 | if (!configURL) { 280 | return undefined; 281 | } 282 | const url = (configURL || '').trim(); 283 | const i = url.indexOf('://'); 284 | if (i === -1) { 285 | return undefined; 286 | } 287 | const scheme = url.substr(0, i).toLowerCase(); 288 | const proxy = url.substr(i + 3); 289 | if (scheme === 'http') { 290 | return 'PROXY ' + proxy; 291 | } else if (scheme === 'https') { 292 | return 'HTTPS ' + proxy; 293 | } else if (scheme === 'socks' || scheme === 'socks5' || scheme === 'socks5h') { 294 | return 'SOCKS ' + proxy; 295 | } else if (scheme === 'socks4' || scheme === 'socks4a') { 296 | return 'SOCKS4 ' + proxy; 297 | } 298 | return undefined; 299 | } 300 | 301 | function shouldBypassProxy(value: string[]) { 302 | if (value.includes("*")) { 303 | return () => true; 304 | } 305 | const filters = value 306 | .map(s => s.trim().split(':', 2)) 307 | .map(([name, port]) => ({ name, port })) 308 | .filter(filter => !!filter.name) 309 | .map(({ name, port }) => { 310 | const domain = name[0] === '.' ? name : `.${name}`; 311 | return { domain, port }; 312 | }); 313 | if (!filters.length) { 314 | return () => false; 315 | } 316 | return (hostname: string, port: string) => filters.some(({ domain, port: filterPort }) => { 317 | return `.${hostname.toLowerCase()}`.endsWith(domain) && (!filterPort || port === filterPort); 318 | }); 319 | } 320 | 321 | function noProxyFromEnv(envValue?: string) { 322 | const value = (envValue || '') 323 | .trim() 324 | .toLowerCase() 325 | .split(','); 326 | return shouldBypassProxy(value); 327 | } 328 | 329 | function noProxyFromConfig(noProxy: string[]) { 330 | const value = noProxy 331 | .map((item) => item.trim().toLowerCase()); 332 | return shouldBypassProxy(value); 333 | } 334 | 335 | export type ProxySupportSetting = 'override' | 'fallback' | 'on' | 'off'; 336 | 337 | export type ResolveProxyWithRequest = (flags: { useProxySettings: boolean, addCertificatesV1: boolean }, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) => void; 338 | 339 | export function createHttpPatch(params: ProxyAgentParams, originals: typeof http | typeof https, resolveProxy: ResolveProxyWithRequest) { 340 | return { 341 | get: patch(originals.get), 342 | request: patch(originals.request) 343 | }; 344 | 345 | function patch(original: typeof http.get) { 346 | function patched(url?: string | nodeurl.URL | null, options?: http.RequestOptions | null, callback?: (res: http.IncomingMessage) => void): http.ClientRequest { 347 | if (typeof url !== 'string' && !(url && (url).searchParams)) { 348 | callback = options; 349 | options = url; 350 | url = null; 351 | } 352 | if (typeof options === 'function') { 353 | callback = options; 354 | options = null; 355 | } 356 | options = options || {}; 357 | 358 | if (options.socketPath) { 359 | return original.apply(null, arguments as any); 360 | } 361 | 362 | const originalAgent = options.agent; 363 | if (originalAgent === true) { 364 | throw new Error('Unexpected agent option: true'); 365 | } 366 | const isHttps = (originals.globalAgent as any).protocol === 'https:'; 367 | const optionsPatched = originalAgent instanceof PacProxyAgent; 368 | const config = params.getProxySupport(); 369 | const useProxySettings = !optionsPatched && (config === 'override' || config === 'fallback' || (config === 'on' && originalAgent === undefined)); 370 | // If Agent.options.ca is set to undefined, it overwrites RequestOptions.ca. 371 | const originalOptionsCa = isHttps ? (options as https.RequestOptions).ca : undefined; 372 | const originalAgentCa = isHttps && originalAgent instanceof originals.Agent && (originalAgent as https.Agent).options && 'ca' in (originalAgent as https.Agent).options && (originalAgent as https.Agent).options.ca; 373 | const originalCa = originalAgentCa !== false ? originalAgentCa : originalOptionsCa; 374 | const addCertificatesV1 = !optionsPatched && params.addCertificatesV1() && isHttps && !originalCa; 375 | 376 | if (useProxySettings || addCertificatesV1) { 377 | if (url) { 378 | const parsed = typeof url === 'string' ? new nodeurl.URL(url) : url; 379 | const urlOptions = { 380 | protocol: parsed.protocol, 381 | hostname: parsed.hostname.lastIndexOf('[', 0) === 0 ? parsed.hostname.slice(1, -1) : parsed.hostname, 382 | port: parsed.port, 383 | path: `${parsed.pathname}${parsed.search}` 384 | }; 385 | if (parsed.username || parsed.password) { 386 | options.auth = `${parsed.username}:${parsed.password}`; 387 | } 388 | options = { ...urlOptions, ...options }; 389 | } else { 390 | options = { ...options }; 391 | } 392 | const resolveP = (req: http.ClientRequest, opts: http.RequestOptions, url: string): Promise => new Promise(resolve => resolveProxy({ useProxySettings, addCertificatesV1 }, req, opts, url, resolve)); 393 | const host = options.hostname || options.host; 394 | const isLocalhost = !host || host === 'localhost' || host === '127.0.0.1'; // Avoiding https://github.com/microsoft/vscode/issues/120354 395 | const agent = createPacProxyAgent(resolveP, { 396 | originalAgent: (!useProxySettings || isLocalhost || config === 'fallback') ? originalAgent : undefined, 397 | lookupProxyAuthorization: params.lookupProxyAuthorization, 398 | // keepAlive: ((originalAgent || originals.globalAgent) as { keepAlive?: boolean }).keepAlive, // Skipping due to https://github.com/microsoft/vscode/issues/228872. 399 | _vscodeTestReplaceCaCerts: (options as SecureContextOptionsPatch)._vscodeTestReplaceCaCerts, 400 | }, opts => new Promise(resolve => addCertificatesToOptionsV1(params, params.addCertificatesV1(), opts, resolve))); 401 | agent.protocol = isHttps ? 'https:' : 'http:'; 402 | options.agent = agent 403 | if (isHttps) { 404 | (options as https.RequestOptions).ca = originalCa; 405 | } 406 | return original(options, callback); 407 | } 408 | 409 | return original.apply(null, arguments as any); 410 | } 411 | return patched; 412 | } 413 | } 414 | 415 | export interface SecureContextOptionsPatch { 416 | _vscodeAdditionalCaCerts?: (string | Buffer)[]; 417 | _vscodeTestReplaceCaCerts?: boolean; 418 | } 419 | 420 | export function createNetPatch(params: ProxyAgentParams, originals: typeof net) { 421 | return { 422 | connect: patchNetConnect(params, originals.connect), 423 | }; 424 | } 425 | 426 | function patchNetConnect(params: ProxyAgentParams, original: typeof net.connect): typeof net.connect { 427 | function connect(options: net.NetConnectOpts, connectionListener?: () => void): net.Socket; 428 | function connect(port: number, host?: string, connectionListener?: () => void): net.Socket; 429 | function connect(path: string, connectionListener?: () => void): net.Socket; 430 | function connect(...args: any[]): net.Socket { 431 | if (params.getLogLevel() === LogLevel.Trace) { 432 | params.log.trace('ProxyResolver#net.connect', toLogString(args)); 433 | } 434 | if (!params.addCertificatesV2()) { 435 | return original.apply(null, arguments as any); 436 | } 437 | const socket = new net.Socket(); 438 | (socket as any).connecting = true; 439 | getOrLoadAdditionalCertificates(params) 440 | .then(() => { 441 | const options: net.NetConnectOpts | undefined = args.find(arg => arg && typeof arg === 'object'); 442 | if (options?.timeout) { 443 | socket.setTimeout(options.timeout); 444 | } 445 | socket.connect.apply(socket, arguments as any); 446 | }) 447 | .catch(err => { 448 | params.log.error('ProxyResolver#net.connect', toErrorMessage(err)); 449 | }); 450 | return socket; 451 | } 452 | return connect; 453 | } 454 | 455 | export function createTlsPatch(params: ProxyAgentParams, originals: typeof tls) { 456 | return { 457 | connect: patchTlsConnect(params, originals.connect), 458 | createSecureContext: patchCreateSecureContext(originals.createSecureContext), 459 | }; 460 | } 461 | 462 | function patchTlsConnect(params: ProxyAgentParams, original: typeof tls.connect): typeof tls.connect { 463 | function connect(options: tls.ConnectionOptions, secureConnectListener?: () => void): tls.TLSSocket; 464 | function connect(port: number, host?: string, options?: tls.ConnectionOptions, secureConnectListener?: () => void): tls.TLSSocket; 465 | function connect(port: number, options?: tls.ConnectionOptions, secureConnectListener?: () => void): tls.TLSSocket; 466 | function connect(...args: any[]): tls.TLSSocket { 467 | if (params.getLogLevel() === LogLevel.Trace) { 468 | params.log.trace('ProxyResolver#tls.connect', toLogString(args)); 469 | } 470 | let options: tls.ConnectionOptions | undefined = args.find(arg => arg && typeof arg === 'object'); 471 | if (!params.addCertificatesV2() || options?.ca) { 472 | return original.apply(null, arguments as any); 473 | } 474 | let secureConnectListener: (() => void) | undefined = args.find(arg => typeof arg === 'function'); 475 | if (!options) { 476 | options = {}; 477 | const listenerIndex = args.findIndex(arg => typeof arg === 'function'); 478 | if (listenerIndex !== -1) { 479 | args[listenerIndex - 1] = options; 480 | } else { 481 | args[2] = options; 482 | } 483 | } else { 484 | options = { 485 | ...options 486 | }; 487 | } 488 | const port = typeof args[0] === 'number' ? args[0] 489 | : typeof args[0] === 'string' && !isNaN(Number(args[0])) ? Number(args[0]) // E.g., http2 module passes port as string. 490 | : options.port; 491 | const host = typeof args[1] === 'string' ? args[1] : options.host; 492 | let tlsSocket: tls.TLSSocket; 493 | if (options.socket) { 494 | if (!options.secureContext) { 495 | options.secureContext = tls.createSecureContext(options); 496 | } 497 | if (!_certificates) { 498 | params.log.trace('ProxyResolver#tls.connect waiting for existing socket connect'); 499 | options.socket.once('connect' , () => { 500 | params.log.trace('ProxyResolver#tls.connect got existing socket connect - adding certs'); 501 | for (const cert of _certificates || []) { 502 | options!.secureContext!.context.addCACert(cert); 503 | } 504 | }); 505 | } else { 506 | params.log.trace('ProxyResolver#tls.connect existing socket already connected - adding certs'); 507 | for (const cert of _certificates) { 508 | options!.secureContext!.context.addCACert(cert); 509 | } 510 | } 511 | } else { 512 | if (!options.secureContext) { 513 | options.secureContext = tls.createSecureContext(options); 514 | } 515 | params.log.trace('ProxyResolver#tls.connect creating unconnected socket'); 516 | const socket = options.socket = new net.Socket(); 517 | (socket as any).connecting = true; 518 | getOrLoadAdditionalCertificates(params) 519 | .then(caCertificates => { 520 | params.log.trace('ProxyResolver#tls.connect adding certs before connecting socket'); 521 | for (const cert of caCertificates) { 522 | options!.secureContext!.context.addCACert(cert); 523 | } 524 | if (options?.timeout) { 525 | socket.setTimeout(options.timeout); 526 | socket.once('timeout', () => { 527 | tlsSocket.emit('timeout'); 528 | }); 529 | } 530 | socket.connect({ 531 | port: port!, 532 | host, 533 | ...options, 534 | }); 535 | }) 536 | .catch(err => { 537 | params.log.error('ProxyResolver#tls.connect', toErrorMessage(err)); 538 | }); 539 | } 540 | if (typeof args[1] === 'string') { 541 | tlsSocket = original(port!, host!, options, secureConnectListener); 542 | } else if (typeof args[0] === 'number' || typeof args[0] === 'string' && !isNaN(Number(args[0]))) { 543 | tlsSocket = original(port!, options, secureConnectListener); 544 | } else { 545 | tlsSocket = original(options, secureConnectListener); 546 | } 547 | return tlsSocket; 548 | } 549 | return connect; 550 | } 551 | 552 | function patchCreateSecureContext(original: typeof tls.createSecureContext): typeof tls.createSecureContext { 553 | return function (details?: tls.SecureContextOptions): ReturnType { 554 | const context = original.apply(null, arguments as any); 555 | const certs = (details as SecureContextOptionsPatch)?._vscodeAdditionalCaCerts; 556 | if (certs) { 557 | for (const cert of certs) { 558 | context.context.addCACert(cert); 559 | } 560 | } 561 | return context; 562 | }; 563 | } 564 | 565 | export function createFetchPatch(params: ProxyAgentParams, originalFetch: typeof globalThis.fetch, resolveProxyURL: (url: string) => Promise) { 566 | return async function patchedFetch(input: string | URL | Request, init?: RequestInit) { 567 | if (!params.isAdditionalFetchSupportEnabled()) { 568 | return originalFetch(input, init); 569 | } 570 | const agentOptions = getAgentOptions(init); 571 | if (agentOptions.socketPath) { 572 | return originalFetch(input, init); 573 | } 574 | const proxySupport = params.getProxySupport(); 575 | const doResolveProxy = proxySupport === 'override' || proxySupport === 'fallback' || (proxySupport === 'on' && ((init as any)?.dispatcher) === undefined); 576 | const addCerts = params.addCertificatesV1() || params.addCertificatesV2(); // There is no v2 for `fetch`, checking both settings. 577 | if (!doResolveProxy && !addCerts) { 578 | return originalFetch(input, init); 579 | } 580 | const urlString = typeof input === 'string' ? input : 'cache' in input ? input.url : input.toString(); 581 | const proxyURL = doResolveProxy ? await resolveProxyURL(urlString) : undefined; 582 | if (!proxyURL && !addCerts) { 583 | return originalFetch(input, init); 584 | } 585 | const systemCA = addCerts ? [...tls.rootCertificates, ...await getOrLoadAdditionalCertificates(params)] : undefined; 586 | const { allowH2 } = agentOptions; 587 | const requestCA = agentOptions.requestCA || systemCA; 588 | const proxyCA = agentOptions.proxyCA || systemCA; 589 | if (!proxyURL) { 590 | const modifiedInit = { 591 | ...init, 592 | dispatcher: new undici.Agent({ 593 | allowH2, 594 | connect: { ca: requestCA }, 595 | }) 596 | }; 597 | return originalFetch(input, modifiedInit); 598 | } 599 | 600 | const state: Record = {}; 601 | const proxyAuthorization = await params.lookupProxyAuthorization?.(proxyURL, undefined, state); 602 | const modifiedInit = { 603 | ...init, 604 | dispatcher: new undici.ProxyAgent({ 605 | uri: proxyURL, 606 | allowH2, 607 | headers: proxyAuthorization ? { 'Proxy-Authorization': proxyAuthorization } : undefined, 608 | requestTls: requestCA ? { allowH2, ca: requestCA } : { allowH2 }, 609 | proxyTls: proxyCA ? { allowH2, ca: proxyCA } : { allowH2 }, 610 | clientFactory: (origin: URL, opts: object): undici.Dispatcher => (new undici.Pool(origin, opts) as any).compose((dispatch: undici.Dispatcher['dispatch']) => { 611 | class ProxyAuthHandler extends undici.DecoratorHandler { 612 | constructor(private dispatch: undici.Dispatcher['dispatch'], private options: undici.Dispatcher.DispatchOptions, private handler: undici.Dispatcher.DispatchHandler) { 613 | super(handler); 614 | } 615 | onResponseError(controller: undici.Dispatcher.DispatchController, err: Error): void { 616 | if (!(err instanceof ProxyAuthError)) { 617 | return this.handler.onResponseError?.(controller, err); 618 | } 619 | (async () => { 620 | try { 621 | const proxyAuthorization = await params.lookupProxyAuthorization?.(proxyURL!, err.proxyAuthenticate, state); 622 | if (proxyAuthorization) { 623 | if (!this.options.headers) { 624 | this.options.headers = ['Proxy-Authorization', proxyAuthorization]; 625 | } else if (Array.isArray(this.options.headers)) { 626 | const i = this.options.headers.findIndex((value, index) => index % 2 === 0 && value.toLowerCase() === 'proxy-authorization'); 627 | if (i === -1) { 628 | this.options.headers.push('Proxy-Authorization', proxyAuthorization); 629 | } else { 630 | this.options.headers[i + 1] = proxyAuthorization; 631 | } 632 | } else if (typeof (this.options.headers as any)[Symbol.iterator] === 'function') { 633 | const headers = [...(this.options.headers as Iterable<[string, string | string[] | undefined]>)]; 634 | const i = headers.findIndex(value => value[0].toLowerCase() === 'proxy-authorization'); 635 | if (i === -1) { 636 | headers.push(['Proxy-Authorization', proxyAuthorization]); 637 | } else { 638 | headers[i][1] = proxyAuthorization; 639 | } 640 | this.options.headers = headers; 641 | } else { 642 | (this.options.headers as Record)['Proxy-Authorization'] = proxyAuthorization; 643 | } 644 | this.dispatch(this.options, this); 645 | } else { 646 | this.handler.onResponseError?.(controller, new undici.errors.RequestAbortedError(`Proxy response (407) ?.== 200 when HTTP Tunneling`)); // Mimick undici's behavior 647 | } 648 | } catch (err: any) { 649 | this.handler.onResponseError?.(controller, err); 650 | } 651 | })(); 652 | } 653 | onRequestUpgrade?(controller: undici.Dispatcher.DispatchController, statusCode: number, headers: IncomingHttpHeaders, socket: stream.Duplex): void { 654 | if (statusCode === 407 && headers) { 655 | let proxyAuthenticate: string | string[] | undefined; 656 | for (const header in headers) { 657 | if (header.toLowerCase() === 'proxy-authenticate') { 658 | proxyAuthenticate = headers[header]; 659 | break; 660 | } 661 | } 662 | if (proxyAuthenticate) { 663 | controller.abort(new ProxyAuthError(proxyAuthenticate)); 664 | return; 665 | } 666 | } 667 | this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket); 668 | } 669 | } 670 | return function proxyAuthDispatch(options: undici.Dispatcher.DispatchOptions, handler: undici.Dispatcher.DispatchHandler) { 671 | return dispatch(options, new ProxyAuthHandler(dispatch, options, handler)); 672 | }; 673 | }), 674 | }) 675 | }; 676 | return originalFetch(input, modifiedInit); 677 | }; 678 | } 679 | 680 | class ProxyAuthError extends Error { 681 | constructor(public proxyAuthenticate: string | string[]) { 682 | super('Proxy authentication required'); 683 | } 684 | } 685 | 686 | const agentOptions = Symbol('agentOptions'); 687 | const proxyAgentOptions = Symbol('proxyAgentOptions'); 688 | 689 | export function patchUndici(originalUndici: typeof undici) { 690 | const originalAgent = originalUndici.Agent; 691 | const patchedAgent = function PatchedAgent(opts?: undici.Agent.Options): undici.Agent { 692 | const agent = new originalAgent(opts); 693 | (agent as any)[agentOptions] = { 694 | ...opts, 695 | ...(opts?.connect && typeof opts?.connect === 'object' ? { connect: { ...opts.connect } } : undefined), 696 | }; 697 | return agent; 698 | }; 699 | patchedAgent.prototype = originalAgent.prototype; 700 | (originalUndici as any).Agent = patchedAgent; 701 | 702 | const originalProxyAgent = originalUndici.ProxyAgent; 703 | const patchedProxyAgent = function PatchedProxyAgent(opts: undici.ProxyAgent.Options | string): undici.ProxyAgent { 704 | const proxyAgent = new originalProxyAgent(opts); 705 | (proxyAgent as any)[proxyAgentOptions] = typeof opts === 'string' ? opts : { 706 | ...opts, 707 | ...(opts?.connect && typeof opts?.connect === 'object' ? { connect: { ...opts.connect } } : undefined), 708 | }; 709 | return proxyAgent; 710 | }; 711 | patchedProxyAgent.prototype = originalProxyAgent.prototype; 712 | (originalUndici as any).ProxyAgent = patchedProxyAgent; 713 | } 714 | 715 | function getAgentOptions(requestInit: RequestInit | undefined) { 716 | let allowH2: boolean | undefined; 717 | let requestCA: string | Buffer | Array | undefined; 718 | let proxyCA: string | Buffer | Array | undefined; 719 | let socketPath: string | undefined; 720 | const dispatcher: undici.Dispatcher = (requestInit as any)?.dispatcher; 721 | let originalAgentOptions: undici.Agent.Options | undefined = dispatcher && (dispatcher as any)[agentOptions]; 722 | if (dispatcher && !originalAgentOptions) { 723 | // Handles bundled extension code where undici does not get patched. 724 | const optionsSymbol = Object.getOwnPropertySymbols(dispatcher).find(s => s.description === 'options'); 725 | if (optionsSymbol) { 726 | originalAgentOptions = (dispatcher as any)[optionsSymbol]; 727 | } 728 | } 729 | if (originalAgentOptions && typeof originalAgentOptions === 'object') { 730 | allowH2 = originalAgentOptions.allowH2; 731 | if (originalAgentOptions.connect && typeof originalAgentOptions.connect === 'object') { 732 | requestCA = 'ca' in originalAgentOptions.connect && originalAgentOptions.connect.ca || undefined; 733 | socketPath = originalAgentOptions.connect.socketPath || undefined; 734 | } 735 | } 736 | let originalProxyAgentOptions: undici.ProxyAgent.Options | string | undefined = dispatcher && (dispatcher as any)[proxyAgentOptions]; 737 | if (dispatcher && !originalProxyAgentOptions) { 738 | // Handles bundled extension code where undici does not get patched. 739 | const proxyAgentSymbol = Object.getOwnPropertySymbols(dispatcher).find(s => s.description === 'proxy agent'); 740 | if (proxyAgentSymbol) { 741 | const proxyAgent = (dispatcher as any)[proxyAgentSymbol]; 742 | if (proxyAgent && typeof proxyAgent === 'object') { 743 | const optionsSymbol = Object.getOwnPropertySymbols(proxyAgent).find(s => s.description === 'options'); 744 | if (optionsSymbol) { 745 | originalProxyAgentOptions = (proxyAgent as any)[optionsSymbol]; 746 | } 747 | } 748 | } 749 | } 750 | if (originalProxyAgentOptions && typeof originalProxyAgentOptions === 'object') { 751 | allowH2 = originalProxyAgentOptions.allowH2; 752 | requestCA = originalProxyAgentOptions.requestTls && 'ca' in originalProxyAgentOptions.requestTls && originalProxyAgentOptions.requestTls.ca || undefined; 753 | proxyCA = originalProxyAgentOptions.proxyTls && 'ca' in originalProxyAgentOptions.proxyTls && originalProxyAgentOptions.proxyTls.ca || undefined; 754 | } 755 | return { allowH2, requestCA, proxyCA, socketPath }; 756 | } 757 | 758 | function addCertificatesToOptionsV1(params: ProxyAgentParams, addCertificatesV1: boolean, opts: http.RequestOptions | tls.ConnectionOptions, callback: () => void) { 759 | if (addCertificatesV1) { 760 | getOrLoadAdditionalCertificates(params) 761 | .then(caCertificates => { 762 | if ((opts as SecureContextOptionsPatch)._vscodeTestReplaceCaCerts) { 763 | (opts as https.RequestOptions).ca = caCertificates; 764 | } else { 765 | (opts as SecureContextOptionsPatch)._vscodeAdditionalCaCerts = caCertificates; 766 | } 767 | callback(); 768 | }) 769 | .catch(err => { 770 | params.log.error('ProxyResolver#addCertificatesV1', toErrorMessage(err)); 771 | }); 772 | } else { 773 | callback(); 774 | } 775 | } 776 | 777 | let _certificatesPromise: Promise | undefined; 778 | let _certificates: string[] | undefined; 779 | export async function getOrLoadAdditionalCertificates(params: ProxyAgentParams) { 780 | if (!_certificatesPromise) { 781 | _certificatesPromise = (async () => { 782 | return _certificates = await params.loadAdditionalCertificates(); 783 | })(); 784 | } 785 | return _certificatesPromise; 786 | } 787 | 788 | export interface CertificateParams { 789 | log: Log; 790 | } 791 | 792 | let _systemCertificatesPromise: Promise | undefined; 793 | export async function loadSystemCertificates(params: CertificateParams) { 794 | if (!_systemCertificatesPromise) { 795 | _systemCertificatesPromise = (async () => { 796 | try { 797 | const certs = await readSystemCertificates(); 798 | params.log.debug('ProxyResolver#loadSystemCertificates count', certs.length); 799 | const now = Date.now(); 800 | const filtered = certs 801 | .filter(cert => { 802 | try { 803 | const parsedCert = new crypto.X509Certificate(cert); 804 | const parsedDate = Date.parse(parsedCert.validTo); 805 | return isNaN(parsedDate) || parsedDate > now; 806 | } catch (err) { 807 | params.log.debug('ProxyResolver#loadSystemCertificates parse error', toErrorMessage(err)); 808 | return false; 809 | } 810 | }); 811 | params.log.debug('ProxyResolver#loadSystemCertificates count filtered', filtered.length); 812 | return filtered; 813 | } catch (err) { 814 | params.log.error('ProxyResolver#loadSystemCertificates error', toErrorMessage(err)); 815 | return []; 816 | } 817 | })(); 818 | } 819 | return _systemCertificatesPromise; 820 | } 821 | 822 | export function resetCaches() { 823 | _certificatesPromise = undefined; 824 | _certificates = undefined; 825 | _systemCertificatesPromise = undefined; 826 | } 827 | 828 | async function readSystemCertificates(): Promise { 829 | if (process.platform === 'win32') { 830 | return readWindowsCaCertificates(); 831 | } 832 | if (process.platform === 'darwin') { 833 | return readMacCaCertificates(); 834 | } 835 | if (process.platform === 'linux') { 836 | return readLinuxCaCertificates(); 837 | } 838 | return []; 839 | } 840 | 841 | async function readWindowsCaCertificates() { 842 | // @ts-ignore Windows only 843 | const winCA = await import('@vscode/windows-ca-certs'); 844 | 845 | let ders: any[] = []; 846 | const store = new winCA.Crypt32(); 847 | try { 848 | let der: any; 849 | while (der = store.next()) { 850 | ders.push(der); 851 | } 852 | } finally { 853 | store.done(); 854 | } 855 | 856 | const certs = new Set(ders.map(derToPem)); 857 | return Array.from(certs); 858 | } 859 | 860 | async function readMacCaCertificates() { 861 | const stdout = await new Promise((resolve, reject) => { 862 | const child = cp.spawn('/usr/bin/security', ['find-certificate', '-a', '-p']); 863 | const stdout: string[] = []; 864 | child.stdout.setEncoding('utf8'); 865 | child.stdout.on('data', str => stdout.push(str)); 866 | child.on('error', reject); 867 | child.on('exit', code => code ? reject(code) : resolve(stdout.join(''))); 868 | }); 869 | const certs = new Set(stdout.split(/(?=-----BEGIN CERTIFICATE-----)/g) 870 | .filter(pem => !!pem.length)); 871 | return Array.from(certs); 872 | } 873 | 874 | const linuxCaCertificatePaths = [ 875 | '/etc/ssl/certs/ca-certificates.crt', // Debian / Ubuntu / Alpine / Fedora 876 | '/etc/ssl/certs/ca-bundle.crt', // Fedora 877 | '/etc/ssl/ca-bundle.pem', // OpenSUSE 878 | ]; 879 | 880 | async function readLinuxCaCertificates() { 881 | for (const certPath of linuxCaCertificatePaths) { 882 | try { 883 | const content = await fs.promises.readFile(certPath, { encoding: 'utf8' }); 884 | const certs = new Set(content.split(/(?=-----BEGIN CERTIFICATE-----)/g) 885 | .filter(pem => !!pem.length)); 886 | return Array.from(certs); 887 | } catch (err: any) { 888 | if (err?.code !== 'ENOENT') { 889 | throw err; 890 | } 891 | } 892 | } 893 | return []; 894 | } 895 | 896 | function derToPem(blob: Buffer) { 897 | const lines = ['-----BEGIN CERTIFICATE-----']; 898 | const der = blob.toString('base64'); 899 | for (let i = 0; i < der.length; i += 64) { 900 | lines.push(der.substr(i, 64)); 901 | } 902 | lines.push('-----END CERTIFICATE-----', ''); 903 | return lines.join(os.EOL); 904 | } 905 | 906 | function toErrorMessage(err: any) { 907 | return err && (err.stack || err.message) || String(err); 908 | } 909 | 910 | export function toLogString(args: any[]) { 911 | return `[${args.map(arg => JSON.stringify(arg, (key, value) => { 912 | const t = typeof value; 913 | if (t === 'object') { 914 | if (key) { 915 | if ((key === 'ca' || key === '_vscodeAdditionalCaCerts') && Array.isArray(value)) { 916 | return `[${value.length} certs]`; 917 | } 918 | if (key === 'ca' && (typeof value === 'string' || Buffer.isBuffer(value))) { 919 | return `[${(value.toString().match(/-----BEGIN CERTIFICATE-----/g) || []).length} certs]`; 920 | } 921 | return !value || value.toString ? String(value) : Object.prototype.toString.call(value); 922 | } else { 923 | return value; 924 | } 925 | } 926 | if (t === 'function') { 927 | return `[Function: ${value.name}]`; 928 | } 929 | if (t === 'bigint') { 930 | return String(value); 931 | } 932 | if (t === 'string' && value.length > 25) { 933 | const len = `[${value.length} chars]`; 934 | return `${value.substr(0, 25 - len.length)}${len}`; 935 | } 936 | return value; 937 | })).join(', ')}]`; 938 | } 939 | 940 | /** 941 | * Certificates for testing. These are not automatically used, but can be added in 942 | * ProxyAgentParams#loadAdditionalCertificates(). This just provides a shared array 943 | * between production code and tests. 944 | */ 945 | export const testCertificates: string[] = []; 946 | -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | test-direct-client: 5 | image: node:20.18.1 6 | volumes: 7 | - ..:/repo 8 | networks: 9 | - test-servers 10 | working_dir: /repo/tests/test-client 11 | environment: 12 | - MOCHA_TESTS=src/direct.test.ts src/tls.test.ts src/socket.test.ts 13 | command: /bin/sh -c 'rm -rf /root/.npm && npm run test:watch' 14 | depends_on: 15 | test-https-server: 16 | condition: service_healthy 17 | test-proxy-client: 18 | image: test-proxy-client:latest 19 | build: test-proxy-client 20 | volumes: 21 | - ..:/repo 22 | - ./test-https-proxy/mitmproxy-config:/root/.mitmproxy 23 | networks: 24 | - test-proxies 25 | working_dir: /repo/tests/test-client 26 | environment: 27 | - MOCHA_TESTS=src/proxy.test.ts 28 | command: /bin/sh -c ' 29 | while [ ! -f /root/.mitmproxy/mitmproxy-ca-cert.pem ]; do sleep 1; done && 30 | cp /root/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt && 31 | update-ca-certificates && 32 | /usr/local/bin/configure-kerberos-client.sh && 33 | rm -rf /root/.npm && 34 | npm run test:watch' 35 | depends_on: 36 | test-http-proxy: 37 | condition: service_started 38 | test-http-auth-proxy: 39 | condition: service_started 40 | test-http-kerberos-proxy: 41 | condition: service_started 42 | test-https-proxy: 43 | condition: service_started 44 | test-http-proxy: 45 | image: ubuntu/squid:latest 46 | networks: 47 | - test-proxies 48 | - test-proxies-and-servers 49 | ports: 50 | - 3128 51 | depends_on: 52 | test-https-server: 53 | condition: service_healthy 54 | test-http-auth-proxy: 55 | image: test-http-auth-proxy:latest 56 | build: test-http-auth-proxy 57 | networks: 58 | - test-proxies 59 | - test-proxies-and-servers 60 | ports: 61 | - 3128 62 | depends_on: 63 | test-https-server: 64 | condition: service_healthy 65 | test-http-kerberos-proxy: 66 | image: test-http-kerberos-proxy:latest 67 | build: test-http-kerberos-proxy 68 | container_name: test-http-kerberos-proxy # needs to be configured to have a static name for the kerberos server hostname 69 | networks: 70 | - test-proxies 71 | - test-proxies-and-servers 72 | ports: 73 | - 80 74 | depends_on: 75 | test-https-server: 76 | condition: service_healthy 77 | test-https-proxy: 78 | image: mitmproxy/mitmproxy:latest 79 | # https://stackoverflow.com/q/61453754 80 | command: /bin/sh -c 'update-ca-certificates && mitmdump --set ssl_insecure=true' 81 | volumes: 82 | - ./test-https-proxy/mitmproxy-config:/root/.mitmproxy 83 | - ./test-https-server/ssl_cert.pem:/usr/local/share/ca-certificates/test-https-server.crt 84 | networks: 85 | - test-proxies 86 | - test-proxies-and-servers 87 | ports: 88 | - 8080 89 | depends_on: 90 | test-https-server: 91 | condition: service_healthy 92 | test-https-server: 93 | image: test-https-server:latest 94 | build: test-https-server 95 | volumes: 96 | - ./test-https-server:/etc/nginx 97 | networks: 98 | - test-servers 99 | - test-proxies-and-servers 100 | ports: 101 | - 443 102 | 103 | networks: 104 | test-proxies: 105 | driver: bridge 106 | internal: true 107 | test-proxies-and-servers: {} 108 | test-servers: {} 109 | -------------------------------------------------------------------------------- /tests/test-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "test.js", 6 | "scripts": { 7 | "test": "mocha -r ts-node/register $MOCHA_TESTS", 8 | "test:watch": "npm test -- -w --watch-extensions ts" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@types/kerberos": "^1.1.2", 15 | "@types/mocha": "5.2.5", 16 | "@types/node": "^20.8.4", 17 | "kerberos": "^2.0.1", 18 | "mocha": "10.8.2", 19 | "ts-node": "9.1.1", 20 | "typescript": "^5.2.2", 21 | "undici": "^7.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/test-client/src/direct.test.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as https from 'https'; 3 | import * as undici from 'undici'; 4 | import * as assert from 'assert'; 5 | import * as fs from 'fs'; 6 | import * as os from 'os'; 7 | import * as path from 'path'; 8 | import * as crypto from 'crypto'; 9 | import * as vpa from '../../..'; 10 | import { createPacProxyAgent } from '../../../src/agent'; 11 | import { testRequest, ca, directProxyAgentParams, unusedCa, directProxyAgentParamsV1, proxiedProxyAgentParamsV1 } from './utils'; 12 | 13 | describe('Direct client', function () { 14 | it('should work without agent', function () { 15 | return testRequest(https, { 16 | hostname: 'test-https-server', 17 | path: '/test-path', 18 | ca, 19 | }); 20 | }); 21 | it('should support SNI when not proxied', function () { 22 | return testRequest(https, { 23 | hostname: 'test-https-server', 24 | path: '/test-path', 25 | agent: createPacProxyAgent(async () => 'DIRECT'), 26 | ca, 27 | }); 28 | }); 29 | it('should omit default port in host header', function () { 30 | // https://github.com/Microsoft/vscode/issues/65118 31 | return testRequest(https, { 32 | hostname: 'test-https-server', 33 | path: '/test-path', 34 | agent: createPacProxyAgent(async () => 'DIRECT'), 35 | ca, 36 | }, { 37 | assertResult: result => { 38 | assert.strictEqual(result.headers.host, 'test-https-server'); 39 | } 40 | }); 41 | }); 42 | it('should fall back to original agent when not proxied', function () { 43 | // https://github.com/Microsoft/vscode/issues/68531 44 | let originalAgent = false; 45 | return testRequest(https, { 46 | hostname: 'test-https-server', 47 | path: '/test-path', 48 | agent: createPacProxyAgent(async () => 'DIRECT', { 49 | originalAgent: new class extends http.Agent { 50 | addRequest(req: any, opts: any): void { 51 | originalAgent = true; 52 | (https.globalAgent).addRequest(req, opts); 53 | } 54 | }() 55 | }), 56 | ca, 57 | }, { 58 | assertResult: () => { 59 | assert.ok(originalAgent); 60 | } 61 | }); 62 | }); 63 | it('should handle `false` as the original agent', function () { 64 | return testRequest(https, { 65 | hostname: 'test-https-server', 66 | path: '/test-path', 67 | agent: createPacProxyAgent(async () => 'DIRECT', { originalAgent: false }), 68 | ca, 69 | }); 70 | }); 71 | 72 | it('should override original agent', async function () { 73 | // https://github.com/microsoft/vscode/issues/117054 74 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParams); 75 | const patchedHttps: typeof https = { 76 | ...https, 77 | ...vpa.createHttpPatch(directProxyAgentParams, https, resolveProxy), 78 | } as any; 79 | let seen = false; 80 | await testRequest(patchedHttps, { 81 | hostname: 'test-https-server', 82 | path: '/test-path', 83 | agent: new class extends https.Agent { 84 | addRequest(req: any, opts: any): void { 85 | seen = true; 86 | (https.globalAgent).addRequest(req, opts); 87 | } 88 | }(), 89 | ca, 90 | }); 91 | assert.ok(!seen, 'Original agent called!'); 92 | }); 93 | it('should use original agent 1', async function () { 94 | // https://github.com/microsoft/vscode/issues/117054 avoiding https://github.com/microsoft/vscode/issues/120354 95 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParams); 96 | const patchedHttps: typeof https = { 97 | ...https, 98 | ...vpa.createHttpPatch(directProxyAgentParams, https, resolveProxy), 99 | } as any; 100 | let seen = false; 101 | await testRequest(patchedHttps, { 102 | hostname: '', 103 | path: '/test-path', 104 | agent: new class extends https.Agent { 105 | addRequest(req: any, opts: any): void { 106 | seen = true; 107 | (https.globalAgent).addRequest(req, opts); 108 | } 109 | }(), 110 | ca, 111 | }).catch(() => {}); // Connection failure expected. 112 | assert.ok(seen, 'Original agent not called!'); 113 | }); 114 | it('should use original agent 2', async function () { 115 | // https://github.com/microsoft/vscode/issues/117054 116 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParams); 117 | const patchedHttps: typeof https = { 118 | ...https, 119 | ...vpa.createHttpPatch({ 120 | ...directProxyAgentParams, 121 | getProxySupport: () => 'fallback', 122 | }, https, resolveProxy), 123 | } as any; 124 | let seen = false; 125 | await testRequest(patchedHttps, { 126 | hostname: 'test-https-server', 127 | path: '/test-path', 128 | agent: new class extends https.Agent { 129 | addRequest(req: any, opts: any): void { 130 | seen = true; 131 | (https.globalAgent).addRequest(req, opts); 132 | } 133 | }(), 134 | ca, 135 | }); 136 | assert.ok(seen, 'Original agent not called!'); 137 | }); 138 | it('should use original agent 3', async function () { 139 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParams); 140 | const patchedHttps: typeof https = { 141 | ...https, 142 | ...vpa.createHttpPatch({ 143 | ...directProxyAgentParams, 144 | getProxySupport: () => 'on', 145 | }, https, resolveProxy), 146 | } as any; 147 | let seen = false; 148 | await testRequest(patchedHttps, { 149 | hostname: 'test-https-server', 150 | path: '/test-path', 151 | agent: new class extends https.Agent { 152 | addRequest(req: any, opts: any): void { 153 | seen = true; 154 | (https.globalAgent).addRequest(req, opts); 155 | } 156 | }(), 157 | ca, 158 | }); 159 | assert.ok(seen, 'Original agent not called!'); 160 | }); 161 | it.skip('should reuse socket with agent', async function () { 162 | // Skipping due to https://github.com/microsoft/vscode/issues/228872. 163 | // https://github.com/microsoft/vscode/issues/173861 164 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParams); 165 | const patchedHttps: typeof https = { 166 | ...https, 167 | ...vpa.createHttpPatch(directProxyAgentParams, https, resolveProxy), 168 | } as any; 169 | await testRequest(patchedHttps, { 170 | hostname: 'test-https-server', 171 | path: '/test-path', 172 | ca, 173 | }); 174 | await testRequest(patchedHttps, { 175 | hostname: 'test-https-server', 176 | path: '/test-path', 177 | ca, 178 | }, { 179 | assertResult: (_, req) => { 180 | assert.strictEqual(req.reusedSocket, true); 181 | } 182 | }); 183 | }); 184 | 185 | it('should use system certificates', async function () { 186 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParamsV1); 187 | const patchedHttps: typeof https = { 188 | ...https, 189 | ...vpa.createHttpPatch(directProxyAgentParamsV1, https, resolveProxy), 190 | } as any; 191 | await testRequest(patchedHttps, { 192 | hostname: 'test-https-server', 193 | path: '/test-path', 194 | _vscodeTestReplaceCaCerts: true, 195 | }); 196 | }); 197 | it('should use ca request option', async function () { 198 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParamsV1); 199 | const patchedHttps: typeof https = { 200 | ...https, 201 | ...vpa.createHttpPatch(directProxyAgentParamsV1, https, resolveProxy), 202 | } as any; 203 | try { 204 | await testRequest(patchedHttps, { 205 | hostname: 'test-https-server', 206 | path: '/test-path', 207 | _vscodeTestReplaceCaCerts: true, 208 | ca: unusedCa, 209 | }); 210 | assert.fail('Expected to fail with self-signed certificate'); 211 | } catch (err: any) { 212 | assert.strictEqual(err?.message, 'self-signed certificate'); 213 | } 214 | }); 215 | it('should use ca agent option 1', async function () { 216 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParamsV1); 217 | const patchedHttps: typeof https = { 218 | ...https, 219 | ...vpa.createHttpPatch(directProxyAgentParamsV1, https, resolveProxy), 220 | } as any; 221 | try { 222 | await testRequest(patchedHttps, { 223 | hostname: 'test-https-server', 224 | path: '/test-path', 225 | _vscodeTestReplaceCaCerts: true, 226 | agent: new https.Agent({ ca: unusedCa }), 227 | }); 228 | assert.fail('Expected to fail with self-signed certificate'); 229 | } catch (err: any) { 230 | assert.strictEqual(err?.message, 'self-signed certificate'); 231 | } 232 | }); 233 | it('should use ca agent option 2', async function () { 234 | try { 235 | vpa.resetCaches(); // Allows loadAdditionalCertificates to run again. 236 | const params = { 237 | ...directProxyAgentParamsV1, 238 | loadAdditionalCertificates: async () => [ 239 | ...await vpa.loadSystemCertificates({ log: console }), 240 | ], 241 | }; 242 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(params); 243 | const patchedHttps: typeof https = { 244 | ...https, 245 | ...vpa.createHttpPatch(params, https, resolveProxy), 246 | } as any; 247 | await testRequest(patchedHttps, { 248 | hostname: 'test-https-server', 249 | path: '/test-path', 250 | _vscodeTestReplaceCaCerts: true, 251 | agent: new https.Agent({ ca }), 252 | }); 253 | } finally { 254 | vpa.resetCaches(); // Allows loadAdditionalCertificates to run again. 255 | } 256 | }); 257 | it('should prefer ca agent option', async function () { 258 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParamsV1); 259 | const patchedHttps: typeof https = { 260 | ...https, 261 | ...vpa.createHttpPatch(directProxyAgentParamsV1, https, resolveProxy), 262 | } as any; 263 | await testRequest(patchedHttps, { 264 | hostname: 'test-https-server', 265 | path: '/test-path', 266 | _vscodeTestReplaceCaCerts: true, 267 | ca: unusedCa, 268 | agent: new https.Agent({ ca: undefined }), 269 | }); 270 | }); 271 | 272 | it('should pass-through with socketPath (fetch)', async function () { 273 | // https://github.com/microsoft/vscode/issues/236423 274 | const server = http.createServer((_, res) => { 275 | res.writeHead(200, { 'Content-Type': 'application/json' }); 276 | res.end(JSON.stringify({ status: 'OK from socket path!' })); 277 | }); 278 | try { 279 | const socketPath = path.join(os.tmpdir(), `test-server-${crypto.randomUUID()}.sock`); 280 | await new Promise(resolve => server.listen(socketPath, resolve)); 281 | 282 | const { resolveProxyURL } = vpa.createProxyResolver(proxiedProxyAgentParamsV1); 283 | const patchedFetch = vpa.createFetchPatch(proxiedProxyAgentParamsV1, globalThis.fetch, resolveProxyURL); 284 | const patchedUndici = { ...undici }; 285 | vpa.patchUndici(patchedUndici); 286 | const res = await patchedFetch('http://localhost/test-path', { 287 | dispatcher: new patchedUndici.Agent({ 288 | connect: { 289 | socketPath, 290 | }, 291 | }) 292 | } as any); 293 | assert.strictEqual(res.status, 200); 294 | assert.strictEqual((await res.json()).status, 'OK from socket path!'); 295 | } finally { 296 | server.close(); 297 | } 298 | }); 299 | it('should pass-through allowH2 with patched undici (fetch)', async function () { 300 | const { resolveProxyURL } = vpa.createProxyResolver(directProxyAgentParamsV1); 301 | const patchedFetch = vpa.createFetchPatch(directProxyAgentParamsV1, globalThis.fetch, resolveProxyURL); 302 | const patchedUndici = { ...undici }; 303 | vpa.patchUndici(patchedUndici); 304 | const res = await patchedFetch('https://test-https-server/test-path', { 305 | dispatcher: new patchedUndici.Agent({ 306 | allowH2: true 307 | }) 308 | } as any); 309 | assert.strictEqual(res.status, 200); 310 | assert.strictEqual((await res.json()).status, 'OK HTTP2!'); 311 | }); 312 | it('should pass-through allowH2 with unpatched undici (fetch)', async function () { 313 | const { resolveProxyURL } = vpa.createProxyResolver(directProxyAgentParamsV1); 314 | const patchedFetch = vpa.createFetchPatch(directProxyAgentParamsV1, globalThis.fetch, resolveProxyURL); 315 | const res = await patchedFetch('https://test-https-server/test-path', { 316 | dispatcher: new undici.Agent({ 317 | allowH2: true 318 | }) 319 | } as any); 320 | assert.strictEqual(res.status, 200); 321 | assert.strictEqual((await res.json()).status, 'OK HTTP2!'); 322 | }); 323 | }); 324 | -------------------------------------------------------------------------------- /tests/test-client/src/proxy.test.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | import * as undici from 'undici'; 3 | import * as assert from 'assert'; 4 | import * as vpa from '../../..'; 5 | import { createPacProxyAgent } from '../../../src/agent'; 6 | import { testRequest, ca, unusedCa, proxiedProxyAgentParamsV1, tlsProxiedProxyAgentParamsV1, directProxyAgentParamsV1 } from './utils'; 7 | 8 | describe('Proxied client', function () { 9 | it('should use HTTP proxy for HTTPS connection', function () { 10 | return testRequest(https, { 11 | hostname: 'test-https-server', 12 | path: '/test-path', 13 | agent: createPacProxyAgent(async () => 'PROXY test-http-proxy:3128'), 14 | ca, 15 | }); 16 | }); 17 | 18 | it('should use HTTPS proxy for HTTPS connection', function () { 19 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(tlsProxiedProxyAgentParamsV1); 20 | const patchedHttps: typeof https = { 21 | ...https, 22 | ...vpa.createHttpPatch(tlsProxiedProxyAgentParamsV1, https, resolveProxy), 23 | } as any; 24 | return testRequest(patchedHttps, { 25 | hostname: 'test-https-server', 26 | path: '/test-path', 27 | _vscodeTestReplaceCaCerts: true, 28 | }); 29 | }); 30 | 31 | it('should use HTTPS proxy for HTTPS connection (fetch)', async function () { 32 | const { resolveProxyURL } = vpa.createProxyResolver(tlsProxiedProxyAgentParamsV1); 33 | const patchedFetch = vpa.createFetchPatch(tlsProxiedProxyAgentParamsV1, globalThis.fetch, resolveProxyURL); 34 | const res = await patchedFetch('https://test-https-server/test-path'); 35 | assert.strictEqual(res.status, 200); 36 | assert.strictEqual((await res.json()).status, 'OK!'); 37 | }); 38 | 39 | it('should support basic auth', function () { 40 | return testRequest(https, { 41 | hostname: 'test-https-server', 42 | path: '/test-path', 43 | agent: createPacProxyAgent(async () => 'PROXY foo:bar@test-http-auth-proxy:3128'), 44 | ca, 45 | }); 46 | }); 47 | 48 | it('should support basic auth (fetch)', async function () { 49 | const params: vpa.ProxyAgentParams = { 50 | ...directProxyAgentParamsV1, 51 | resolveProxy: async () => 'PROXY foo:bar@test-http-auth-proxy:3128', 52 | }; 53 | const { resolveProxyURL } = vpa.createProxyResolver(params); 54 | const patchedFetch = vpa.createFetchPatch(params, globalThis.fetch, resolveProxyURL); 55 | const res = await patchedFetch('https://test-https-server/test-path'); 56 | assert.strictEqual(res.status, 200); 57 | assert.strictEqual((await res.json()).status, 'OK!'); 58 | }); 59 | 60 | it('should fail with 407 when auth is missing', async function () { 61 | try { 62 | await testRequest(https, { 63 | hostname: 'test-https-server', 64 | path: '/test-path', 65 | agent: createPacProxyAgent(async () => 'PROXY test-http-auth-proxy:3128'), 66 | ca, 67 | }); 68 | } catch (err) { 69 | assert.strictEqual((err as any).statusCode, 407); 70 | return; 71 | } 72 | assert.fail('Should have failed'); 73 | }); 74 | 75 | it('should fail with 407 when auth is missing (fetch)', async function () { 76 | try { 77 | const params: vpa.ProxyAgentParams = { 78 | ...directProxyAgentParamsV1, 79 | resolveProxy: async () => 'PROXY test-http-auth-proxy:3128', 80 | }; 81 | const { resolveProxyURL } = vpa.createProxyResolver(params); 82 | const patchedFetch = vpa.createFetchPatch(params, globalThis.fetch, resolveProxyURL); 83 | await patchedFetch('https://test-https-server/test-path'); 84 | } catch (err: any) { 85 | assert.strictEqual(err?.cause?.cause?.message, 'Proxy response (407) ?.== 200 when HTTP Tunneling'); 86 | return; 87 | } 88 | assert.fail('Should have failed'); 89 | }); 90 | 91 | it('should call auth callback after 407', function () { 92 | return testRequest(https, { 93 | hostname: 'test-https-server', 94 | path: '/test-path', 95 | agent: createPacProxyAgent(async () => 'PROXY test-http-auth-proxy:3128', { 96 | async lookupProxyAuthorization(proxyURL, proxyAuthenticate) { 97 | assert.strictEqual(proxyURL, 'http://test-http-auth-proxy:3128/'); 98 | if (!proxyAuthenticate) { 99 | return; 100 | } 101 | assert.strictEqual(proxyAuthenticate, 'Basic realm="Squid Basic Authentication"'); 102 | return `Basic ${Buffer.from('foo:bar').toString('base64')}`; 103 | }, 104 | }), 105 | ca, 106 | }); 107 | }); 108 | 109 | it('should call auth callback after 407 (fetch)', async function () { 110 | const params: vpa.ProxyAgentParams = { 111 | ...directProxyAgentParamsV1, 112 | resolveProxy: async () => 'PROXY test-http-auth-proxy:3128', 113 | async lookupProxyAuthorization(proxyURL, proxyAuthenticate) { 114 | assert.strictEqual(proxyURL, 'http://test-http-auth-proxy:3128'); // TODO: Investigate why there is no trailing slash like with the https module. 115 | if (!proxyAuthenticate) { 116 | return; 117 | } 118 | assert.strictEqual(proxyAuthenticate, 'Basic realm="Squid Basic Authentication"'); 119 | return `Basic ${Buffer.from('foo:bar').toString('base64')}`; 120 | }, 121 | }; 122 | const { resolveProxyURL } = vpa.createProxyResolver(params); 123 | const patchedFetch = vpa.createFetchPatch(params, globalThis.fetch, resolveProxyURL); 124 | const res = await patchedFetch('https://test-https-server/test-path'); 125 | assert.strictEqual(res.status, 200); 126 | assert.strictEqual((await res.json()).status, 'OK!'); 127 | }); 128 | 129 | it('should call auth callback before request', function () { 130 | return testRequest(https, { 131 | hostname: 'test-https-server', 132 | path: '/test-path', 133 | agent: createPacProxyAgent(async () => 'PROXY test-http-auth-proxy:3128', { 134 | async lookupProxyAuthorization(proxyURL, proxyAuthenticate) { 135 | assert.strictEqual(proxyURL, 'http://test-http-auth-proxy:3128/'); 136 | assert.strictEqual(proxyAuthenticate, undefined); 137 | return `Basic ${Buffer.from('foo:bar').toString('base64')}`; 138 | }, 139 | }), 140 | ca, 141 | }); 142 | }); 143 | 144 | it('should call auth callback before request (fetch)', async function () { 145 | const params: vpa.ProxyAgentParams = { 146 | ...directProxyAgentParamsV1, 147 | resolveProxy: async () => 'PROXY test-http-auth-proxy:3128', 148 | async lookupProxyAuthorization(proxyURL, proxyAuthenticate) { 149 | assert.strictEqual(proxyURL, 'http://test-http-auth-proxy:3128'); // TODO: Investigate why there is no trailing slash like with the https module. 150 | assert.strictEqual(proxyAuthenticate, undefined); 151 | return `Basic ${Buffer.from('foo:bar').toString('base64')}`; 152 | }, 153 | }; 154 | const { resolveProxyURL } = vpa.createProxyResolver(params); 155 | const patchedFetch = vpa.createFetchPatch(params, globalThis.fetch, resolveProxyURL); 156 | const res = await patchedFetch('https://test-https-server/test-path'); 157 | assert.strictEqual(res.status, 200); 158 | assert.strictEqual((await res.json()).status, 'OK!'); 159 | }); 160 | 161 | it('should pass state around', async function () { 162 | let count = 0; 163 | await testRequest(https, { 164 | hostname: 'test-https-server', 165 | path: '/test-path', 166 | agent: createPacProxyAgent(async () => 'PROXY test-http-auth-proxy:3128', { 167 | async lookupProxyAuthorization(proxyURL, proxyAuthenticate, state: { count?: number }) { 168 | assert.strictEqual(proxyURL, 'http://test-http-auth-proxy:3128/'); 169 | assert.strictEqual(proxyAuthenticate, state.count ? 'Basic realm="Squid Basic Authentication"' : undefined); 170 | const credentials = state.count === 2 ? 'foo:bar' : 'foo:wrong'; 171 | count = state.count = (state.count || 0) + 1; 172 | return `Basic ${Buffer.from(credentials).toString('base64')}`; 173 | }, 174 | }), 175 | ca, 176 | }); 177 | assert.strictEqual(count, 3); 178 | }); 179 | 180 | it('should pass state around (fetch)', async function () { 181 | let count = 0; 182 | const params: vpa.ProxyAgentParams = { 183 | ...directProxyAgentParamsV1, 184 | resolveProxy: async () => 'PROXY test-http-auth-proxy:3128', 185 | async lookupProxyAuthorization(proxyURL, proxyAuthenticate, state: { count?: number }) { 186 | assert.strictEqual(proxyURL, 'http://test-http-auth-proxy:3128'); // TODO: Investigate why there is no trailing slash like with the https module. 187 | assert.strictEqual(proxyAuthenticate, state.count ? 'Basic realm="Squid Basic Authentication"' : undefined); 188 | const credentials = state.count === 2 ? 'foo:bar' : 'foo:wrong'; 189 | count = state.count = (state.count || 0) + 1; 190 | return `Basic ${Buffer.from(credentials).toString('base64')}`; 191 | }, 192 | }; 193 | const { resolveProxyURL } = vpa.createProxyResolver(params); 194 | const patchedFetch = vpa.createFetchPatch(params, globalThis.fetch, resolveProxyURL); 195 | const res = await patchedFetch('https://test-https-server/test-path'); 196 | assert.strictEqual(res.status, 200); 197 | assert.strictEqual((await res.json()).status, 'OK!'); 198 | assert.strictEqual(count, 3); 199 | }); 200 | 201 | it('should work with kerberos', function () { 202 | this.timeout(10000); 203 | const proxyAuthenticateCache = {}; 204 | return testRequest(https, { 205 | hostname: 'test-https-server', 206 | path: '/test-path', 207 | agent: createPacProxyAgent(async () => 'PROXY test-http-kerberos-proxy:80', { 208 | async lookupProxyAuthorization(proxyURL, proxyAuthenticate, state) { 209 | assert.strictEqual(proxyURL, 'http://test-http-kerberos-proxy/'); 210 | if (proxyAuthenticate) { 211 | assert.strictEqual(proxyAuthenticate, 'Negotiate'); 212 | } 213 | const log = { ...console, trace: console.log }; 214 | return lookupProxyAuthorization(log, log, proxyAuthenticateCache, true, proxyURL, proxyAuthenticate, state); 215 | }, 216 | }), 217 | ca, 218 | }); 219 | }); 220 | 221 | it('should work with kerberos (fetch)', async function () { 222 | this.timeout(10000); 223 | const proxyAuthenticateCache = {}; 224 | const params: vpa.ProxyAgentParams = { 225 | ...directProxyAgentParamsV1, 226 | resolveProxy: async () => 'PROXY test-http-kerberos-proxy:80', 227 | async lookupProxyAuthorization(proxyURL, proxyAuthenticate, state) { 228 | assert.strictEqual(proxyURL, 'http://test-http-kerberos-proxy:80'); // TODO: Investigate why there is no trailing slash like with the https module. 229 | if (proxyAuthenticate) { 230 | assert.strictEqual(proxyAuthenticate, 'Negotiate'); 231 | } 232 | const log = { ...console, trace: console.log }; 233 | return lookupProxyAuthorization(log, log, proxyAuthenticateCache, true, proxyURL, proxyAuthenticate, state); 234 | }, 235 | }; 236 | const { resolveProxyURL } = vpa.createProxyResolver(params); 237 | const patchedFetch = vpa.createFetchPatch(params, globalThis.fetch, resolveProxyURL); 238 | const res = await patchedFetch('https://test-https-server/test-path'); 239 | assert.strictEqual(res.status, 200); 240 | assert.strictEqual((await res.json()).status, 'OK!'); 241 | }); 242 | 243 | it('should use system certificates', async function () { 244 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(proxiedProxyAgentParamsV1); 245 | const patchedHttps: typeof https = { 246 | ...https, 247 | ...vpa.createHttpPatch(proxiedProxyAgentParamsV1, https, resolveProxy), 248 | } as any; 249 | await testRequest(patchedHttps, { 250 | hostname: 'test-https-server', 251 | path: '/test-path', 252 | _vscodeTestReplaceCaCerts: true, 253 | }); 254 | }); 255 | it('should use ca request option', async function () { 256 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(proxiedProxyAgentParamsV1); 257 | const patchedHttps: typeof https = { 258 | ...https, 259 | ...vpa.createHttpPatch(proxiedProxyAgentParamsV1, https, resolveProxy), 260 | } as any; 261 | try { 262 | await testRequest(patchedHttps, { 263 | hostname: 'test-https-server', 264 | path: '/test-path', 265 | _vscodeTestReplaceCaCerts: true, 266 | ca: unusedCa, 267 | }); 268 | assert.fail('Expected to fail with self-signed certificate'); 269 | } catch (err: any) { 270 | assert.strictEqual(err?.message, 'self-signed certificate'); 271 | } 272 | }); 273 | it('should use ca agent option 1', async function () { 274 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(proxiedProxyAgentParamsV1); 275 | const patchedHttps: typeof https = { 276 | ...https, 277 | ...vpa.createHttpPatch(proxiedProxyAgentParamsV1, https, resolveProxy), 278 | } as any; 279 | try { 280 | await testRequest(patchedHttps, { 281 | hostname: 'test-https-server', 282 | path: '/test-path', 283 | _vscodeTestReplaceCaCerts: true, 284 | agent: new https.Agent({ ca: unusedCa }), 285 | }); 286 | assert.fail('Expected to fail with self-signed certificate'); 287 | } catch (err: any) { 288 | assert.strictEqual(err?.message, 'self-signed certificate'); 289 | } 290 | }); 291 | it('should use ca agent option 2', async function () { 292 | try { 293 | vpa.resetCaches(); // Allows loadAdditionalCertificates to run again. 294 | const params = { 295 | ...proxiedProxyAgentParamsV1, 296 | loadAdditionalCertificates: async () => [ 297 | ...await vpa.loadSystemCertificates({ log: console }), 298 | ], 299 | }; 300 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(params); 301 | const patchedHttps: typeof https = { 302 | ...https, 303 | ...vpa.createHttpPatch(params, https, resolveProxy), 304 | } as any; 305 | await testRequest(patchedHttps, { 306 | hostname: 'test-https-server', 307 | path: '/test-path', 308 | _vscodeTestReplaceCaCerts: true, 309 | agent: new https.Agent({ ca }), 310 | }); 311 | } finally { 312 | vpa.resetCaches(); // Allows loadAdditionalCertificates to run again. 313 | } 314 | }); 315 | it('should prefer ca agent option', async function () { 316 | const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(proxiedProxyAgentParamsV1); 317 | const patchedHttps: typeof https = { 318 | ...https, 319 | ...vpa.createHttpPatch(proxiedProxyAgentParamsV1, https, resolveProxy), 320 | } as any; 321 | await testRequest(patchedHttps, { 322 | hostname: 'test-https-server', 323 | path: '/test-path', 324 | _vscodeTestReplaceCaCerts: true, 325 | ca: unusedCa, 326 | agent: new https.Agent({ ca: undefined }), 327 | }); 328 | }); 329 | 330 | it('should pass-through allowH2 with patched undici (fetch)', async function () { 331 | const { resolveProxyURL } = vpa.createProxyResolver(proxiedProxyAgentParamsV1); 332 | const patchedFetch = vpa.createFetchPatch(proxiedProxyAgentParamsV1, globalThis.fetch, resolveProxyURL); 333 | const patchedUndici = { ...undici }; 334 | vpa.patchUndici(patchedUndici); 335 | const res = await patchedFetch('https://test-https-server/test-path', { 336 | dispatcher: new patchedUndici.ProxyAgent({ 337 | uri: 'http://test-http-proxy:3128', 338 | allowH2: true 339 | }) 340 | } as any); 341 | assert.strictEqual(res.status, 200); 342 | assert.strictEqual((await res.json()).status, 'OK HTTP2!'); 343 | }); 344 | it('should pass-through allowH2 with unpatched undici (fetch)', async function () { 345 | const { resolveProxyURL } = vpa.createProxyResolver(proxiedProxyAgentParamsV1); 346 | const patchedFetch = vpa.createFetchPatch(proxiedProxyAgentParamsV1, globalThis.fetch, resolveProxyURL); 347 | const res = await patchedFetch('https://test-https-server/test-path', { 348 | dispatcher: new undici.ProxyAgent({ 349 | uri: 'http://test-http-proxy:3128', 350 | allowH2: true 351 | }) 352 | } as any); 353 | assert.strictEqual(res.status, 200); 354 | assert.strictEqual((await res.json()).status, 'OK HTTP2!'); 355 | }); 356 | }); 357 | 358 | // From microsoft/vscode's proxyResolver.ts: 359 | async function lookupProxyAuthorization( 360 | extHostLogService: Console, 361 | mainThreadTelemetry: Console, 362 | // configProvider: ExtHostConfigProvider, 363 | proxyAuthenticateCache: Record, 364 | isRemote: boolean, 365 | proxyURL: string, 366 | proxyAuthenticate: string | string[] | undefined, 367 | state: { kerberosRequested?: boolean } 368 | ): Promise { 369 | const cached = proxyAuthenticateCache[proxyURL]; 370 | if (proxyAuthenticate) { 371 | proxyAuthenticateCache[proxyURL] = proxyAuthenticate; 372 | } 373 | extHostLogService.trace('ProxyResolver#lookupProxyAuthorization callback', `proxyURL:${proxyURL}`, `proxyAuthenticate:${proxyAuthenticate}`, `proxyAuthenticateCache:${cached}`); 374 | const header = proxyAuthenticate || cached; 375 | const authenticate = Array.isArray(header) ? header : typeof header === 'string' ? [header] : []; 376 | sendTelemetry(mainThreadTelemetry, authenticate, isRemote); 377 | if (authenticate.some(a => /^(Negotiate|Kerberos)( |$)/i.test(a)) && !state.kerberosRequested) { 378 | try { 379 | state.kerberosRequested = true; 380 | const kerberos = await import('kerberos'); 381 | const url = new URL(proxyURL); 382 | const spn = /* configProvider.getConfiguration('http').get('proxyKerberosServicePrincipal') 383 | || */ (process.platform === 'win32' ? `HTTP/${url.hostname}` : `HTTP@${url.hostname}`); 384 | extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup', `proxyURL:${proxyURL}`, `spn:${spn}`); 385 | const client = await kerberos.initializeClient(spn); 386 | const response = await client.step(''); 387 | return 'Negotiate ' + response; 388 | } catch (err) { 389 | extHostLogService.error('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err); 390 | } 391 | } 392 | return undefined; 393 | } 394 | 395 | type ProxyAuthenticationClassification = { 396 | owner: 'chrmarti'; 397 | comment: 'Data about proxy authentication requests'; 398 | authenticationType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Type of the authentication requested' }; 399 | extensionHostType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of the extension host' }; 400 | }; 401 | 402 | type ProxyAuthenticationEvent = { 403 | authenticationType: string; 404 | extensionHostType: string; 405 | }; 406 | 407 | let telemetrySent = false; 408 | 409 | function sendTelemetry(mainThreadTelemetry: Console, authenticate: string[], isRemote: boolean) { 410 | if (telemetrySent || !authenticate.length) { 411 | return; 412 | } 413 | telemetrySent = true; 414 | 415 | mainThreadTelemetry.log('proxyAuthenticationRequest', { 416 | authenticationType: authenticate.map(a => a.split(' ')[0]).join(','), 417 | extensionHostType: isRemote ? 'remote' : 'local', 418 | }); 419 | } 420 | -------------------------------------------------------------------------------- /tests/test-client/src/socket.test.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net'; 2 | import * as tls from 'tls'; 3 | import * as dns from 'dns'; 4 | import { createNetPatch, createTlsPatch, toLogString } from '../../../src/index'; 5 | import { directProxyAgentParams } from './utils'; 6 | import * as assert from 'assert'; 7 | 8 | describe('Socket client', function () { 9 | it('net.connect() should work without delay', async () => { 10 | const netPatched = { 11 | ...net, 12 | ...createNetPatch(directProxyAgentParams, net), 13 | }; 14 | const socket = netPatched.connect(808, 'test-https-server'); 15 | const p = new Promise((resolve, reject) => { 16 | socket.on('error', reject); 17 | const chunks: Buffer[] = []; 18 | socket.on('data', chunk => chunks.push(chunk)); 19 | socket.on('end', () => { 20 | resolve(Buffer.concat(chunks).toString()); 21 | }); 22 | }); 23 | socket.write(`GET /test-path-unencrypted HTTP/1.1 24 | Host: test-http-server 25 | Connection: close 26 | 27 | `); 28 | const response = await p; 29 | assert.ok(response.startsWith('HTTP/1.1 200 OK'), `Unexpected response: ${response}`); 30 | }); 31 | 32 | it('tls.connect() should work without delay', async () => { 33 | const tlsPatched = { 34 | ...tls, 35 | ...createTlsPatch(directProxyAgentParams, tls), 36 | }; 37 | const socket = tlsPatched.connect({ 38 | host: 'test-https-server', 39 | port: 443, 40 | servername: 'test-https-server', // for SNI 41 | }); 42 | const p = new Promise((resolve, reject) => { 43 | socket.on('error', reject); 44 | const chunks: Buffer[] = []; 45 | socket.on('data', chunk => chunks.push(chunk)); 46 | socket.on('end', () => { 47 | resolve(Buffer.concat(chunks).toString()); 48 | }); 49 | }); 50 | socket.write(`GET /test-path HTTP/1.1 51 | Host: test-https-server 52 | Connection: close 53 | 54 | `); 55 | const response = await p; 56 | assert.ok(response.startsWith('HTTP/1.1 200 OK'), `Unexpected response: ${response}`); 57 | }); 58 | 59 | it('net.connect() should support timeout', async () => { 60 | const netPatched = { 61 | ...net, 62 | ...createNetPatch(directProxyAgentParams, net), 63 | }; 64 | const socket = netPatched.connect({ 65 | host: 'test-https-server', 66 | port: 808, 67 | timeout: 500, 68 | }); 69 | const timeout = new Promise((resolve, reject) => { 70 | socket.on('timeout', resolve); 71 | socket.on('error', reject); 72 | socket.on('end', reject); 73 | }); 74 | await Promise.race([timeout, new Promise((_, reject) => setTimeout(() => reject(new Error('no timeout event received')), 1000))]); 75 | }); 76 | 77 | it('tls.connect() should support timeout', async () => { 78 | const tlsPatched = { 79 | ...tls, 80 | ...createTlsPatch(directProxyAgentParams, tls), 81 | }; 82 | const socket = tlsPatched.connect({ 83 | host: 'test-https-server', 84 | port: 443, 85 | servername: 'test-https-server', // for SNI 86 | timeout: 500, 87 | }); 88 | const timeout = new Promise((resolve, reject) => { 89 | socket.on('timeout', resolve); 90 | socket.on('error', reject); 91 | socket.on('end', reject); 92 | }); 93 | await Promise.race([timeout, new Promise((_, reject) => setTimeout(() => reject(new Error('no timeout event received')), 1000))]); 94 | }); 95 | 96 | it('tls.connect() should support net.connect() options', async () => { 97 | const tlsPatched = { 98 | ...tls, 99 | ...createTlsPatch(directProxyAgentParams, tls), 100 | }; 101 | let lookupUsed = false; 102 | const socket = tlsPatched.connect(443, 'test-https-server', { 103 | servername: 'test-https-server', // for SNI 104 | lookup: (hostname, options, callback) => { 105 | lookupUsed = true; 106 | dns.lookup(hostname, options, callback); 107 | }, 108 | }); 109 | const p = new Promise((resolve, reject) => { 110 | socket.on('error', reject); 111 | const chunks: Buffer[] = []; 112 | socket.on('data', chunk => chunks.push(chunk)); 113 | socket.on('end', () => { 114 | resolve(Buffer.concat(chunks).toString()); 115 | }); 116 | }); 117 | socket.write(`GET /test-path HTTP/1.1 118 | Host: test-https-server 119 | Connection: close 120 | 121 | `); 122 | const response = await p; 123 | assert.ok(response.startsWith('HTTP/1.1 200 OK'), `Unexpected response: ${response}`); 124 | assert.ok(lookupUsed, 'lookup() was not used'); 125 | }); 126 | 127 | it('toLogString() should work', async () => { 128 | assert.strictEqual(toLogString([{ 129 | str: 'string', 130 | buf: Buffer.from('buffer'), 131 | obj: { a: 1 }, 132 | arr: [1, 2, 3], 133 | undef: undefined, 134 | null: null, 135 | bool: true, 136 | num: 1, 137 | sym: Symbol('test'), 138 | fn: () => {}, 139 | date: new Date(0), 140 | obj2: Object.create(null), 141 | }, () => {}]), '[{"str":"string","buf":"[object Object]","obj":"[object Object]","arr":"1,2,3","null":"null","bool":true,"num":1,"fn":"[Function: fn]","date":"1970-01-01T00:00:00.000Z","obj2":"[object Object]"}, "[Function: ]"]'); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /tests/test-client/src/tls.test.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net'; 2 | import * as tls from 'tls'; 3 | import { createNetPatch, createTlsPatch, resetCaches, SecureContextOptionsPatch } from '../../../src/index'; 4 | import { ca, directProxyAgentParams } from './utils'; 5 | 6 | describe('TLS patch', function () { 7 | beforeEach(() => { 8 | resetCaches(); 9 | }); 10 | it('should work without CA option v1', function (done) { 11 | const tlsPatched = { 12 | ...tls, 13 | ...createTlsPatch({ 14 | ...directProxyAgentParams, 15 | addCertificatesV1: () => true, 16 | addCertificatesV2: () => false, 17 | loadAdditionalCertificates: async () => [], 18 | }, tls), 19 | }; 20 | const options: tls.ConnectionOptions = { 21 | host: 'test-https-server', 22 | port: 443, 23 | servername: 'test-https-server', // for SNI 24 | }; 25 | (options as SecureContextOptionsPatch)._vscodeAdditionalCaCerts = ca.map(ca => ca.toString()); 26 | options.secureContext = tlsPatched.createSecureContext(options); // Needed here because we don't patch tls like in VS Code. 27 | const socket = tlsPatched.connect(options); 28 | socket.on('error', done); 29 | socket.on('secureConnect', () => { 30 | const { authorized, authorizationError } = socket; 31 | socket.destroy(); 32 | if (authorized) { 33 | done(); 34 | } else { 35 | done(authorizationError); 36 | } 37 | }); 38 | }); 39 | 40 | it('should work without CA option v2', function (done) { 41 | const tlsPatched = { 42 | ...tls, 43 | ...createTlsPatch(directProxyAgentParams, tls), 44 | }; 45 | const options: tls.ConnectionOptions = { 46 | host: 'test-https-server', 47 | port: 443, 48 | servername: 'test-https-server', // for SNI 49 | }; 50 | const socket = tlsPatched.connect(options); 51 | socket.on('error', done); 52 | socket.on('secureConnect', () => { 53 | const { authorized, authorizationError } = socket; 54 | socket.destroy(); 55 | if (authorized) { 56 | done(); 57 | } else { 58 | done(authorizationError); 59 | } 60 | }); 61 | }); 62 | 63 | it('should work with existing connected socket v2', function (done) { 64 | const netPatched = { 65 | ...net, 66 | ...createNetPatch(directProxyAgentParams, net), 67 | }; 68 | const tlsPatched = { 69 | ...tls, 70 | ...createTlsPatch(directProxyAgentParams, tls), 71 | }; 72 | const existingSocket = netPatched.connect(443, 'test-https-server'); 73 | existingSocket.on('connect', () => { 74 | const options: tls.ConnectionOptions = { 75 | socket: existingSocket, 76 | servername: 'test-https-server', // for SNI 77 | }; 78 | const socket = tlsPatched.connect(options); 79 | socket.on('error', done); 80 | socket.on('secureConnect', () => { 81 | const { authorized, authorizationError } = socket; 82 | socket.destroy(); 83 | if (authorized) { 84 | done(); 85 | } else { 86 | done(authorizationError); 87 | } 88 | }); 89 | }); 90 | }); 91 | 92 | it('should work with existing connecting socket v2', function (done) { 93 | const netPatched = { 94 | ...net, 95 | ...createNetPatch(directProxyAgentParams, net), 96 | }; 97 | const tlsPatched = { 98 | ...tls, 99 | ...createTlsPatch(directProxyAgentParams, tls), 100 | }; 101 | const existingSocket = netPatched.connect(443, 'test-https-server'); 102 | const options: tls.ConnectionOptions = { 103 | socket: existingSocket, 104 | servername: 'test-https-server', // for SNI 105 | }; 106 | const socket = tlsPatched.connect(options); 107 | socket.on('error', done); 108 | socket.on('secureConnect', () => { 109 | const { authorized, authorizationError } = socket; 110 | socket.destroy(); 111 | if (authorized) { 112 | done(); 113 | } else { 114 | done(authorizationError); 115 | } 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /tests/test-client/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as https from 'https'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import * as assert from 'assert'; 6 | 7 | import * as vpa from '../../..'; 8 | import { loadSystemCertificates } from '../../../src'; 9 | 10 | export const ca = [ 11 | fs.readFileSync(path.join(__dirname, '../../test-https-server/ssl_cert.pem')).toString(), 12 | fs.readFileSync(path.join(__dirname, '../../test-https-server/ssl_teapot_cert.pem')).toString(), 13 | ]; 14 | 15 | export const unusedCa = fs.readFileSync(path.join(__dirname, '../../test-https-server/ssl_unused_cert.pem')).toString(); 16 | 17 | export const directProxyAgentParams: vpa.ProxyAgentParams = { 18 | resolveProxy: async () => 'DIRECT', 19 | getProxyURL: () => undefined, 20 | getProxySupport: () => 'override', 21 | isAdditionalFetchSupportEnabled: () => true, 22 | addCertificatesV1: () => false, 23 | addCertificatesV2: () => true, 24 | log: console, 25 | getLogLevel: () => vpa.LogLevel.Trace, 26 | proxyResolveTelemetry: () => undefined, 27 | isUseHostProxyEnabled: () => true, 28 | loadAdditionalCertificates: async () => [ 29 | ...await loadSystemCertificates({ log: console }), 30 | ...ca, 31 | ], 32 | env: {}, 33 | }; 34 | 35 | export const directProxyAgentParamsV1: vpa.ProxyAgentParams = { 36 | ...directProxyAgentParams, 37 | addCertificatesV1: () => true, 38 | addCertificatesV2: () => false, 39 | }; 40 | 41 | export const proxiedProxyAgentParamsV1: vpa.ProxyAgentParams = { 42 | ...directProxyAgentParamsV1, 43 | resolveProxy: async () => 'PROXY test-http-proxy:3128', 44 | }; 45 | 46 | export const tlsProxiedProxyAgentParamsV1: vpa.ProxyAgentParams = { 47 | ...directProxyAgentParamsV1, 48 | resolveProxy: async () => 'HTTPS test-https-proxy:8080', 49 | }; 50 | 51 | export async function testRequest(client: C, options: C extends typeof https ? (https.RequestOptions & vpa.SecureContextOptionsPatch) : http.RequestOptions, testOptions: { assertResult?: (result: any, req: http.ClientRequest, res: http.IncomingMessage) => void; } = {}) { 52 | return new Promise((resolve, reject) => { 53 | const req = client.request(options, res => { 54 | if (!res.statusCode || res.statusCode < 200 || res.statusCode > 299) { 55 | const chunks: Buffer[] = []; 56 | res.on('data', chunk => chunks.push(chunk)); 57 | res.on('end', () => { 58 | const err = new Error(`Error status: ${res.statusCode} ${res.statusMessage} \n${Buffer.concat(chunks).toString()}`); 59 | (err as any).statusCode = res.statusCode; 60 | (err as any).statusMessage = res.statusMessage; 61 | reject(err); 62 | }); 63 | return; 64 | } 65 | let data = ''; 66 | res.setEncoding('utf8'); 67 | res.on('data', chunk => { 68 | data += chunk; 69 | }); 70 | res.on('end', () => { 71 | try { 72 | const result = JSON.parse(data); 73 | assert.equal(result.status, 'OK!'); 74 | if (testOptions.assertResult) { 75 | testOptions.assertResult(result, req, res); 76 | } 77 | resolve(); 78 | } catch (err: any) { 79 | err.message = `${err.message}: ${data}`; 80 | reject(err); 81 | } 82 | }); 83 | }); 84 | req.on('error', err => { 85 | reject(err); 86 | }); 87 | req.end(); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /tests/test-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "esModuleInterop": true, 6 | "strict": true, 7 | "resolveJsonModule": true 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /tests/test-http-auth-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu/squid:latest 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y apache2-utils \ 5 | && rm -rf /var/lib/apt/lists/* 6 | 7 | COPY basic_auth.conf /etc/squid/conf.d/squid.acl.conf 8 | RUN sed -e '/^http_access/ s/^#*/#/' -i /etc/squid/conf.d/debian.conf 9 | 10 | RUN htpasswd -bc /etc/squid/.htpasswd foo bar 11 | -------------------------------------------------------------------------------- /tests/test-http-auth-proxy/basic_auth.conf: -------------------------------------------------------------------------------- 1 | auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/.htpasswd 2 | auth_param basic children 5 3 | auth_param basic realm Squid Basic Authentication 4 | auth_param basic credentialsttl 5 hours 5 | acl password proxy_auth REQUIRED 6 | http_access allow password 7 | -------------------------------------------------------------------------------- /tests/test-http-kerberos-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ENV DEBIAN_FRONTEND="noninteractive" 4 | 5 | RUN apt-get update && \ 6 | apt-get -y -qq install \ 7 | python curl \ 8 | build-essential libkrb5-dev \ 9 | krb5-user krb5-kdc krb5-admin-server \ 10 | apache2 libapache2-mod-auth-gssapi && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | COPY setup.sh /usr/local/bin/setup.sh 14 | 15 | CMD ["/usr/local/bin/setup.sh"] 16 | -------------------------------------------------------------------------------- /tests/test-http-kerberos-proxy/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export KERBEROS_USERNAME="PlaceholderUsername" 4 | export KERBEROS_PASSWORD="Placeholder" 5 | export KERBEROS_REALM="vscode.proxy.test" 6 | export KERBEROS_PORT="80" 7 | export KERBEROS_HOSTNAME="test-http-kerberos-proxy.tests_test-proxies" 8 | 9 | set -o xtrace 10 | set -x 11 | 12 | echo "Setting up Kerberos config file at /etc/krb5.conf" 13 | cat > /etc/krb5.conf << EOL 14 | [libdefaults] 15 | default_realm = ${KERBEROS_REALM^^} 16 | dns_lookup_realm = false 17 | dns_lookup_kdc = false 18 | [realms] 19 | ${KERBEROS_REALM^^} = { 20 | kdc = $KERBEROS_HOSTNAME 21 | admin_server = $KERBEROS_HOSTNAME 22 | } 23 | [domain_realm] 24 | .$KERBEROS_REALM = ${KERBEROS_REALM^^} 25 | [logging] 26 | kdc = FILE:/var/log/krb5kdc.log 27 | admin_server = FILE:/var/log/kadmin.log 28 | default = FILE:/var/log/krb5lib.log 29 | EOL 30 | 31 | echo "Setting up kerberos ACL configuration at /etc/krb5kdc/kadm5.acl" 32 | mkdir -p /etc/krb5kdc 33 | echo -e "*/*@${KERBEROS_REALM^^}\t*" > /etc/krb5kdc/kadm5.acl 34 | 35 | echo "Creating KDC database" 36 | # krb5_newrealm returns non-0 return code as it is running in a container, ignore it for this command only 37 | set +e 38 | printf "$KERBEROS_PASSWORD\n$KERBEROS_PASSWORD" | krb5_newrealm 39 | set -e 40 | 41 | echo "Creating principals for tests" 42 | kadmin.local -q "addprinc -pw $KERBEROS_PASSWORD $KERBEROS_USERNAME" 43 | 44 | echo "Adding principal for Kerberos auth and creating keytabs" 45 | kadmin.local -q "addprinc -randkey HTTP/$KERBEROS_HOSTNAME" 46 | kadmin.local -q "ktadd -k /etc/krb5.keytab HTTP/$KERBEROS_HOSTNAME" 47 | 48 | chmod 777 /etc/krb5.keytab 49 | 50 | echo "Restarting Kerberos KDS service" 51 | service krb5-kdc restart 52 | 53 | echo "Add ServerName to Apache config" 54 | grep -q -F "ServerName $KERBEROS_HOSTNAME" /etc/apache2/apache2.conf || echo "ServerName $KERBEROS_HOSTNAME" >> /etc/apache2/apache2.conf 55 | 56 | echo "Deleting default virtual host files" 57 | rm /etc/apache2/sites-enabled/*.conf 58 | rm /etc/apache2/sites-available/*.conf 59 | 60 | echo "Create virtual host files" 61 | cat > /etc/apache2/sites-available/kerberos-proxy.conf << EOL 62 | 63 | ServerName $KERBEROS_HOSTNAME 64 | ServerAlias $KERBEROS_HOSTNAME 65 | 66 | ProxyRequests On 67 | ProxyPreserveHost On 68 | 69 | 70 | Order Deny,Allow 71 | Allow from all 72 | 73 | AuthType GSSAPI 74 | AuthName "GSSAPI Single Sign On Login" 75 | Require valid-user 76 | GssapiCredStore keytab:/etc/krb5.keytab 77 | 78 | 79 | 80 | EOL 81 | 82 | echo "Enabling virtual host site" 83 | a2ensite kerberos-proxy.conf 84 | 85 | echo "Enabling apache modules" 86 | a2enmod proxy 87 | a2enmod proxy_http 88 | a2enmod proxy_http2 89 | a2enmod proxy_connect 90 | a2enmod ssl 91 | a2enmod headers 92 | service apache2 restart 93 | 94 | echo "KERBEROS PROXY RUNNING" 95 | # show apache logs to keep container running 96 | tail -f /var/log/apache2/error.log -------------------------------------------------------------------------------- /tests/test-https-proxy/.gitignore: -------------------------------------------------------------------------------- 1 | mitmproxy-config -------------------------------------------------------------------------------- /tests/test-https-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y openssl \ 5 | && rm -rf /var/lib/apt/lists/* 6 | 7 | WORKDIR /etc/nginx 8 | 9 | CMD openssl req -nodes -x509 -newkey rsa:2048 -keyout ssl_key.pem -out ssl_cert.pem -days 365 -subj "/C=US/ST=Oregon/L=Portland/O=IT/CN=test-https-server" \ 10 | && openssl req -nodes -x509 -newkey rsa:2048 -keyout ssl_teapot_key.pem -out ssl_teapot_cert.pem -days 365 -subj "/C=US/ST=Oregon/L=Portland/O=IT/CN=test-teapot-server" \ 11 | && openssl req -nodes -x509 -newkey rsa:2048 -keyout ssl_unused_key.pem -out ssl_unused_cert.pem -days 365 -subj "/C=US/ST=Oregon/L=Portland/O=IT/CN=test-https-server" \ 12 | && nginx -g 'daemon off;' 13 | 14 | HEALTHCHECK --interval=1s --timeout=3s --retries=10 \ 15 | CMD curl -f -k https://test-https-server/test-path || exit 1 -------------------------------------------------------------------------------- /tests/test-https-server/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | # include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | 25 | ssl_protocols TLSv1.2 TLSv1.3; 26 | ssl_prefer_server_ciphers on; 27 | # https://cipherli.st (requires Node.js 10) 28 | # ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384; 29 | # Strong (requires Node.js 10) 30 | # ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256; 31 | # Compatibility: 32 | ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA; 33 | ssl_ecdh_curve secp384r1; 34 | ssl_session_cache none; 35 | ssl_session_tickets off; 36 | ssl_stapling on; 37 | ssl_stapling_verify on; 38 | add_header Strict-Transport-Security "max-age=63072000"; 39 | add_header X-Frame-Options DENY; 40 | add_header X-Content-Type-Options nosniff; 41 | add_header X-XSS-Protection "1; mode=block"; 42 | 43 | # Test SNI support. 44 | server { 45 | listen 443 ssl http2; 46 | server_name test-teapot-server; 47 | 48 | ssl_certificate ssl_teapot_cert.pem; 49 | ssl_certificate_key ssl_teapot_key.pem; 50 | 51 | location / { 52 | return 418; 53 | } 54 | } 55 | 56 | server { 57 | listen 443 ssl http2; 58 | server_name test-https-server; 59 | 60 | ssl_certificate ssl_cert.pem; 61 | ssl_certificate_key ssl_key.pem; 62 | 63 | location / { 64 | return 404; 65 | } 66 | 67 | location = /test-path { 68 | if ($http2) { 69 | return 200 '{ 70 | "status": "OK HTTP2!", 71 | "headers": { 72 | "host": "$http_host" 73 | } 74 | }'; 75 | } 76 | return 200 '{ 77 | "status": "OK!", 78 | "headers": { 79 | "host": "$http_host" 80 | } 81 | }'; 82 | add_header Content-Type application/json; 83 | } 84 | } 85 | 86 | server { 87 | listen 808; 88 | server_name test-http-server; 89 | 90 | location / { 91 | return 404; 92 | } 93 | 94 | location = /test-path-unencrypted { 95 | return 200 '{ 96 | "status": "OK!", 97 | "headers": { 98 | "host": "$http_host" 99 | } 100 | }'; 101 | add_header Content-Type application/json; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/test-proxy-client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.18.1 2 | 3 | ENV DEBIAN_FRONTEND="noninteractive" 4 | 5 | RUN apt-get update && apt-get -y -qq install \ 6 | curl krb5-user 7 | 8 | COPY ./configure-kerberos-client.sh /usr/local/bin/configure-kerberos-client.sh 9 | -------------------------------------------------------------------------------- /tests/test-proxy-client/configure-kerberos-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | export KERBEROS_USERNAME="PlaceholderUsername" 6 | export KERBEROS_PASSWORD="Placeholder" 7 | export KERBEROS_REALM="vscode.proxy.test" 8 | export KERBEROS_PORT="80" 9 | export KERBEROS_HOSTNAME="test-http-kerberos-proxy" 10 | 11 | echo "Setting up Kerberos config file at /etc/krb5.conf" 12 | cat > /etc/krb5.conf << EOL 13 | [libdefaults] 14 | default_realm = ${KERBEROS_REALM^^} 15 | dns_lookup_realm = false 16 | dns_lookup_kdc = false 17 | [realms] 18 | ${KERBEROS_REALM^^} = { 19 | kdc = $KERBEROS_HOSTNAME 20 | admin_server = $KERBEROS_HOSTNAME 21 | } 22 | [domain_realm] 23 | .$KERBEROS_REALM = ${KERBEROS_REALM^^} 24 | [logging] 25 | kdc = FILE:/var/log/krb5kdc.log 26 | admin_server = FILE:/var/log/kadmin.log 27 | default = FILE:/var/log/krb5lib.log 28 | EOL 29 | 30 | echo -n "$KERBEROS_PASSWORD" | kinit "$KERBEROS_USERNAME" 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "esModuleInterop": true, 6 | "outDir": "out", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "tests", 15 | "out" 16 | ] 17 | } --------------------------------------------------------------------------------