Skip to content

Commit acf58d3

Browse files
timfishAbhiPrasad
andauthored
feat(node): Update and vendor https-proxy-agent (#10088)
Closes #9199 This PR vendors the `https-proxy-agent` code and in the process updates to v7.0.0. `https-proxy-agent` is our last remaining cjs-only dependency so this is required for #10046. This removes the following dependencies: - `[email protected]` - `[email protected]` - `[email protected]` - `[email protected]` The vendored code has been modified to use the Sentry logger rather than `debug`. Initially, rather than modify the vendored code substantially just to pass our tight lint rules, I've disabled a few of the less important lints that would make it particularly tricky to pull in upstream bug fixes: ```ts /* eslint-disable @typescript-eslint/explicit-member-accessibility */ /* eslint-disable @typescript-eslint/member-ordering */ /* eslint-disable jsdoc/require-jsdoc */ ``` ## Min supported Node version `https-proxy-agent` has a `@types/[email protected]` dev dependency but apart from adding an import for `URL`, I can't find anything that would stop it working on older versions of node and there is nothing in the changelogs that would suggest the min supported version has changed. --------- Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent 8c7b5b5 commit acf58d3

File tree

9 files changed

+593
-8
lines changed

9 files changed

+593
-8
lines changed

packages/node/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
"@sentry-internal/tracing": "7.93.0",
3333
"@sentry/core": "7.93.0",
3434
"@sentry/types": "7.93.0",
35-
"@sentry/utils": "7.93.0",
36-
"https-proxy-agent": "^5.0.0"
35+
"@sentry/utils": "7.93.0"
3736
},
3837
"devDependencies": {
3938
"@types/cookie": "0.5.2",

packages/node/src/proxy/base.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7
3+
* With the following licence:
4+
*
5+
* (The MIT License)
6+
*
7+
* Copyright (c) 2013 Nathan Rajlich <[email protected]>*
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining
10+
* a copy of this software and associated documentation files (the
11+
* 'Software'), to deal in the Software without restriction, including
12+
* without limitation the rights to use, copy, modify, merge, publish,
13+
* distribute, sublicense, and/or sell copies of the Software, and to
14+
* permit persons to whom the Software is furnished to do so, subject to
15+
* the following conditions:*
16+
*
17+
* The above copyright notice and this permission notice shall be
18+
* included in all copies or substantial portions of the Software.*
19+
*
20+
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
21+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27+
*/
28+
29+
/* eslint-disable @typescript-eslint/explicit-member-accessibility */
30+
/* eslint-disable @typescript-eslint/member-ordering */
31+
/* eslint-disable jsdoc/require-jsdoc */
32+
import * as http from 'http';
33+
import type * as net from 'net';
34+
import type { Duplex } from 'stream';
35+
import type * as tls from 'tls';
36+
37+
export * from './helpers';
38+
39+
interface HttpConnectOpts extends net.TcpNetConnectOpts {
40+
secureEndpoint: false;
41+
protocol?: string;
42+
}
43+
44+
interface HttpsConnectOpts extends tls.ConnectionOptions {
45+
secureEndpoint: true;
46+
protocol?: string;
47+
port: number;
48+
}
49+
50+
export type AgentConnectOpts = HttpConnectOpts | HttpsConnectOpts;
51+
52+
const INTERNAL = Symbol('AgentBaseInternalState');
53+
54+
interface InternalState {
55+
defaultPort?: number;
56+
protocol?: string;
57+
currentSocket?: Duplex;
58+
}
59+
60+
export abstract class Agent extends http.Agent {
61+
private [INTERNAL]: InternalState;
62+
63+
// Set by `http.Agent` - missing from `@types/node`
64+
options!: Partial<net.TcpNetConnectOpts & tls.ConnectionOptions>;
65+
keepAlive!: boolean;
66+
67+
constructor(opts?: http.AgentOptions) {
68+
super(opts);
69+
this[INTERNAL] = {};
70+
}
71+
72+
abstract connect(
73+
req: http.ClientRequest,
74+
options: AgentConnectOpts,
75+
): Promise<Duplex | http.Agent> | Duplex | http.Agent;
76+
77+
/**
78+
* Determine whether this is an `http` or `https` request.
79+
*/
80+
isSecureEndpoint(options?: AgentConnectOpts): boolean {
81+
if (options) {
82+
// First check the `secureEndpoint` property explicitly, since this
83+
// means that a parent `Agent` is "passing through" to this instance.
84+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
85+
if (typeof (options as any).secureEndpoint === 'boolean') {
86+
return options.secureEndpoint;
87+
}
88+
89+
// If no explicit `secure` endpoint, check if `protocol` property is
90+
// set. This will usually be the case since using a full string URL
91+
// or `URL` instance should be the most common usage.
92+
if (typeof options.protocol === 'string') {
93+
return options.protocol === 'https:';
94+
}
95+
}
96+
97+
// Finally, if no `protocol` property was set, then fall back to
98+
// checking the stack trace of the current call stack, and try to
99+
// detect the "https" module.
100+
const { stack } = new Error();
101+
if (typeof stack !== 'string') return false;
102+
return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
103+
}
104+
105+
createSocket(req: http.ClientRequest, options: AgentConnectOpts, cb: (err: Error | null, s?: Duplex) => void): void {
106+
const connectOpts = {
107+
...options,
108+
secureEndpoint: this.isSecureEndpoint(options),
109+
};
110+
Promise.resolve()
111+
.then(() => this.connect(req, connectOpts))
112+
.then(socket => {
113+
if (socket instanceof http.Agent) {
114+
// @ts-expect-error `addRequest()` isn't defined in `@types/node`
115+
return socket.addRequest(req, connectOpts);
116+
}
117+
this[INTERNAL].currentSocket = socket;
118+
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
119+
super.createSocket(req, options, cb);
120+
}, cb);
121+
}
122+
123+
createConnection(): Duplex {
124+
const socket = this[INTERNAL].currentSocket;
125+
this[INTERNAL].currentSocket = undefined;
126+
if (!socket) {
127+
throw new Error('No socket was returned in the `connect()` function');
128+
}
129+
return socket;
130+
}
131+
132+
get defaultPort(): number {
133+
return this[INTERNAL].defaultPort ?? (this.protocol === 'https:' ? 443 : 80);
134+
}
135+
136+
set defaultPort(v: number) {
137+
if (this[INTERNAL]) {
138+
this[INTERNAL].defaultPort = v;
139+
}
140+
}
141+
142+
get protocol(): string {
143+
return this[INTERNAL].protocol ?? (this.isSecureEndpoint() ? 'https:' : 'http:');
144+
}
145+
146+
set protocol(v: string) {
147+
if (this[INTERNAL]) {
148+
this[INTERNAL].protocol = v;
149+
}
150+
}
151+
}

packages/node/src/proxy/helpers.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* This code was originally forked from https://github.com/TooTallNate/proxy-agents/tree/b133295fd16f6475578b6b15bd9b4e33ecb0d0b7
3+
* With the following licence:
4+
*
5+
* (The MIT License)
6+
*
7+
* Copyright (c) 2013 Nathan Rajlich <[email protected]>*
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining
10+
* a copy of this software and associated documentation files (the
11+
* 'Software'), to deal in the Software without restriction, including
12+
* without limitation the rights to use, copy, modify, merge, publish,
13+
* distribute, sublicense, and/or sell copies of the Software, and to
14+
* permit persons to whom the Software is furnished to do so, subject to
15+
* the following conditions:*
16+
*
17+
* The above copyright notice and this permission notice shall be
18+
* included in all copies or substantial portions of the Software.*
19+
*
20+
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
21+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27+
*/
28+
29+
/* eslint-disable jsdoc/require-jsdoc */
30+
import * as http from 'http';
31+
import * as https from 'https';
32+
import type { Readable } from 'stream';
33+
// TODO (v8): Remove this when Node < 12 is no longer supported
34+
import type { URL } from 'url';
35+
36+
export type ThenableRequest = http.ClientRequest & {
37+
then: Promise<http.IncomingMessage>['then'];
38+
};
39+
40+
export async function toBuffer(stream: Readable): Promise<Buffer> {
41+
let length = 0;
42+
const chunks: Buffer[] = [];
43+
for await (const chunk of stream) {
44+
length += (chunk as Buffer).length;
45+
chunks.push(chunk);
46+
}
47+
return Buffer.concat(chunks, length);
48+
}
49+
50+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51+
export async function json(stream: Readable): Promise<any> {
52+
const buf = await toBuffer(stream);
53+
const str = buf.toString('utf8');
54+
try {
55+
return JSON.parse(str);
56+
} catch (_err: unknown) {
57+
const err = _err as Error;
58+
err.message += ` (input: ${str})`;
59+
throw err;
60+
}
61+
}
62+
63+
export function req(url: string | URL, opts: https.RequestOptions = {}): ThenableRequest {
64+
const href = typeof url === 'string' ? url : url.href;
65+
const req = (href.startsWith('https:') ? https : http).request(url, opts) as ThenableRequest;
66+
const promise = new Promise<http.IncomingMessage>((resolve, reject) => {
67+
req.once('response', resolve).once('error', reject).end() as unknown as ThenableRequest;
68+
});
69+
req.then = promise.then.bind(promise);
70+
return req;
71+
}

0 commit comments

Comments
 (0)