Skip to content

Commit 8661aac

Browse files
clemyanarcanis
andauthored
Core: Use hpagent for proxies (#6779)
## What's the problem this PR addresses? Proxying with the `tunnel` package is... finnicky. I did not extensively investigate these but as a few examples of issues I encountered when I used it in the past: - Does not use the specified CA certificates, even for HTTPS over HTTP proxy - Does not use proxy after redirects Fixes #6568 Fixes #5244 Hard to tell definitively without reproductions but these should be also fixed: Fixes #3215 Fixes #2250 ## How did you fix it? Use `hpagent` instead. This by itself fixes most HTTPS over HTTP proxying issues. I have also reused HTTPS network settings (e.g. CA certs) for connecting to HTTPS proxies. Side note: `pem` needs openssl installed to work which makes testing this (and running other HTTPS-related test) difficult in some cases (e.g. on Windows). Should we use another library for HTTPS testing? ## Checklist <!--- Don't worry if you miss something, chores are automatically tested. --> <!--- This checklist exists to help you remember doing the chores when you submit a PR. --> <!--- Put an `x` in all the boxes that apply. --> - [x] I have read the [Contributing Guide](https://yarnpkg.com/advanced/contributing). <!-- See https://yarnpkg.com/advanced/contributing#preparing-your-pr-to-be-released for more details. --> <!-- Check with `yarn version check` and fix with `yarn version check -i` --> - [x] I have set the packages that need to be released for my changes to be effective. <!-- The "Testing chores" workflow validates that your PR follows our guidelines. --> <!-- If it doesn't pass, click on it to see details as to what your PR might be missing. --> - [x] I will check that all automated PR checks pass before the PR gets reviewed. --------- Co-authored-by: Maël Nison <[email protected]>
1 parent 2c69552 commit 8661aac

File tree

10 files changed

+436
-79
lines changed

10 files changed

+436
-79
lines changed

.pnp.cjs

Lines changed: 11 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.
Binary file not shown.

.yarn/versions/562a0ff0.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
releases:
2+
"@yarnpkg/cli": patch
3+
"@yarnpkg/core": patch
4+
"@yarnpkg/plugin-essentials": patch
5+
"@yarnpkg/plugin-git": patch
6+
"@yarnpkg/plugin-github": patch
7+
"@yarnpkg/plugin-http": patch
8+
"@yarnpkg/plugin-npm": patch
9+
"@yarnpkg/plugin-npm-cli": patch
10+
"@yarnpkg/plugin-typescript": patch
11+
12+
declined:
13+
- "@yarnpkg/plugin-compat"
14+
- "@yarnpkg/plugin-constraints"
15+
- "@yarnpkg/plugin-dlx"
16+
- "@yarnpkg/plugin-exec"
17+
- "@yarnpkg/plugin-file"
18+
- "@yarnpkg/plugin-init"
19+
- "@yarnpkg/plugin-interactive-tools"
20+
- "@yarnpkg/plugin-jsr"
21+
- "@yarnpkg/plugin-link"
22+
- "@yarnpkg/plugin-nm"
23+
- "@yarnpkg/plugin-pack"
24+
- "@yarnpkg/plugin-patch"
25+
- "@yarnpkg/plugin-pnp"
26+
- "@yarnpkg/plugin-pnpm"
27+
- "@yarnpkg/plugin-stage"
28+
- "@yarnpkg/plugin-version"
29+
- "@yarnpkg/plugin-workspace-tools"
30+
- "@yarnpkg/builder"
31+
- "@yarnpkg/doctor"
32+
- "@yarnpkg/extensions"
33+
- "@yarnpkg/nm"
34+
- "@yarnpkg/pnpify"
35+
- "@yarnpkg/sdks"

packages/acceptance-tests/pkg-tests-core/sources/utils/tests.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {IncomingMessage, ServerResponse} from 'http';
99
import http from 'http';
1010
import invariant from 'invariant';
1111
import {AddressInfo} from 'net';
12+
import net from 'net';
1213
import os from 'os';
1314
import pem from 'pem';
1415
import semver from 'semver';
@@ -808,6 +809,110 @@ export const startPackageServer = ({type}: {type: keyof typeof packageServerUrls
808809
});
809810
};
810811

812+
const proxyServerUrls: {
813+
http: Promise<string> | null;
814+
https: Promise<string> | null;
815+
} = {http: null, https: null};
816+
817+
export const startProxyServer = ({type = `http`}: {type?: keyof typeof proxyServerUrls} = {}): Promise<string> => {
818+
const serverUrl = proxyServerUrls[type];
819+
if (serverUrl !== null)
820+
return serverUrl;
821+
822+
const sendError = (res: ServerResponse, statusCode: number, errorMessage: string): void => {
823+
res.writeHead(statusCode);
824+
res.end(errorMessage);
825+
};
826+
827+
return proxyServerUrls[type] = new Promise((resolve, reject) => {
828+
const listener: http.RequestListener = (req, res) => {
829+
void (async () => {
830+
try {
831+
if (!req.url) {
832+
sendError(res, 400, `Missing URL in request`);
833+
return;
834+
}
835+
836+
const url = new URL(req.url);
837+
const options = {
838+
hostname: url.hostname,
839+
port: url.port || (url.protocol === `https:` ? 443 : 80),
840+
path: `${url.pathname}${url.search}`,
841+
method: req.method,
842+
headers: {...req.headers},
843+
};
844+
845+
delete options.headers[`proxy-authorization`];
846+
delete options.headers[`proxy-connection`];
847+
options.headers.connection = `close`;
848+
849+
const proxyReq = (url.protocol === `https:` ? https : http).request(options, proxyRes => {
850+
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
851+
proxyRes.pipe(res);
852+
});
853+
854+
req.pipe(proxyReq);
855+
856+
proxyReq.on(`error`, err => {
857+
sendError(res, 502, `Proxy error: ${err.message}`);
858+
});
859+
} catch (error) {
860+
sendError(res, 500, `Proxy server error: ${error.message}`);
861+
}
862+
})();
863+
};
864+
865+
(async () => {
866+
let server: https.Server | http.Server;
867+
868+
if (type === `https`) {
869+
const certs = await getHttpsCertificates();
870+
871+
server = https.createServer({
872+
cert: certs.server.certificate,
873+
key: certs.server.clientKey,
874+
ca: certs.ca.certificate,
875+
}, listener);
876+
} else {
877+
server = http.createServer(listener);
878+
}
879+
880+
// Handle the CONNECT method using the 'connect' event
881+
server.on(`connect`, (req, clientSocket, head) => {
882+
// Parse the target and establish the connection
883+
const [targetHost, targetPort] = (req.url || ``).split(`:`);
884+
const port = parseInt(targetPort) || 443;
885+
886+
const serverSocket = net.connect(port, targetHost, () => {
887+
clientSocket.write(
888+
`HTTP/1.1 200 Connection Established\r\n` +
889+
`\r\n`,
890+
);
891+
892+
serverSocket.write(head);
893+
894+
serverSocket.pipe(clientSocket);
895+
clientSocket.pipe(serverSocket);
896+
});
897+
898+
serverSocket.on(`error`, () => {
899+
clientSocket.end();
900+
});
901+
clientSocket.on(`error`, () => {
902+
serverSocket.end();
903+
});
904+
});
905+
906+
// We don't want the server to prevent the process from exiting
907+
server.unref();
908+
server.listen(() => {
909+
const {port} = server.address() as AddressInfo;
910+
resolve(`${type}://localhost:${port}`);
911+
});
912+
})();
913+
});
914+
};
915+
811916
export interface PackageDriver {
812917
(packageJson: Record<string, any>, subDefinition: Record<string, any> | RunFunction, fn?: RunFunction): any;
813918
getPackageManagerName: () => string;

0 commit comments

Comments
 (0)