Skip to content

Commit

Permalink
Refactor for reuse with fetch (microsoft/vscode#228697)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Nov 5, 2024
1 parent e2f7112 commit 918cc19
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 113 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Change Log
Notable changes will be documented here.

## [TBD]
## [0.24.0]
- Skip keepAlive flag ([microsoft/vscode#228872](https://github.com/microsoft/vscode/issues/228872))
- Refactor for reuse with fetch ([microsoft/vscode#228697](https://github.com/microsoft/vscode/issues/228697))

## [0.23.0]
- Pass on keepAlive flag ([microsoft/vscode#173861](https://github.com/microsoft/vscode/issues/173861))
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vscode/proxy-agent",
"version": "0.23.0",
"version": "0.24.0",
"description": "NodeJS http(s) agent implementation for VS Code",
"main": "out/index.js",
"types": "out/index.d.ts",
Expand Down
167 changes: 81 additions & 86 deletions src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ type FindProxyForURL = (req: http.ClientRequest, opts: http.RequestOptions, url:
*
* @api public
*/
class _PacProxyAgent extends Agent {
export class PacProxyAgent extends Agent {
resolver: FindProxyForURL;
opts: createPacProxyAgent.PacProxyAgentOptions;
opts: PacProxyAgentOptions;
cache?: Readable;

constructor(resolver: FindProxyForURL, opts: createPacProxyAgent.PacProxyAgentOptions = {}) {
constructor(resolver: FindProxyForURL, opts: PacProxyAgentOptions = {}) {
super(opts);
debug('Creating PacProxyAgent with options %o', opts);

Expand Down Expand Up @@ -82,79 +82,84 @@ class _PacProxyAgent extends Agent {
debug('url: %o', url);
let result = await this.resolver(req, opts, url);

// Default to "DIRECT" if a falsey value was returned (or nothing)
if (!result) {
result = 'DIRECT';
}

const proxies = String(result)
.trim()
.split(/\s*;\s*/g)
.filter(Boolean);

if (this.opts.fallbackToDirect && !proxies.includes('DIRECT')) {
proxies.push('DIRECT');
const { proxy, url: proxyURL } = getProxyURLFromResolverResult(result);

let agent: http.Agent | null = null;
if (!proxyURL) {
// Needed for SNI.
const originalAgent = this.opts.originalAgent;
const defaultAgent = secureEndpoint ? https.globalAgent : http.globalAgent;
agent = originalAgent === false ? new (defaultAgent as any).constructor() : (originalAgent || defaultAgent)
} else if (proxyURL.startsWith('socks')) {
// Use a SOCKSv5h or SOCKSv4a proxy
agent = new SocksProxyAgent(proxyURL);
} else if (proxyURL.startsWith('http')) {
// Use an HTTP or HTTPS proxy
// http://dev.chromium.org/developers/design-documents/secure-web-proxy
if (secureEndpoint) {
agent = new HttpsProxyAgent2(proxyURL, this.opts);
} else {
agent = new HttpProxyAgent(proxyURL, this.opts);
}
}

for (const proxy of proxies) {
let agent: http.Agent | null = null;
const [type, target] = proxy.split(/\s+/);
debug('Attempting to use proxy: %o', proxy);

if (type === 'DIRECT') {
// Needed for SNI.
const originalAgent = this.opts.originalAgent;
const defaultAgent = secureEndpoint ? https.globalAgent : http.globalAgent;
agent = originalAgent === false ? new (defaultAgent as any).constructor() : (originalAgent || defaultAgent)
} else if (type === 'SOCKS' || type === 'SOCKS5') {
// Use a SOCKSv5h proxy
agent = new SocksProxyAgent(`socks://${target}`);
} else if (type === 'SOCKS4') {
// Use a SOCKSv4a proxy
agent = new SocksProxyAgent(`socks4a://${target}`);
} else if (
type === 'PROXY' ||
type === 'HTTP' ||
type === 'HTTPS'
) {
// Use an HTTP or HTTPS proxy
// http://dev.chromium.org/developers/design-documents/secure-web-proxy
const proxyURL = `${
type === 'HTTPS' ? 'https' : 'http'
}://${target}`;
if (secureEndpoint) {
agent = new HttpsProxyAgent2(proxyURL, this.opts);
try {
if (agent) {
let s: Duplex | http.Agent;
if (agent instanceof Agent) {
s = await agent.connect(req, opts);
} else {
agent = new HttpProxyAgent(proxyURL, this.opts);
s = agent;
}
req.emit('proxy', { proxy, socket: s });
return s;
}

try {
if (agent) {
let s: Duplex | http.Agent;
if (agent instanceof Agent) {
s = await agent.connect(req, opts);
} else {
s = agent;
}
req.emit('proxy', { proxy, socket: s });
return s;
}
throw new Error(`Could not determine proxy type for: ${proxy}`);
} catch (err) {
debug('Got error for proxy %o: %o', proxy, err);
req.emit('proxy', { proxy, error: err });
}
throw new Error(`Could not determine proxy type for: ${proxy}`);
} catch (err) {
debug('Got error for proxy %o: %o', proxy, err);
req.emit('proxy', { proxy, error: err });
}

throw new Error(
`Failed to establish a socket connection to proxies: ${JSON.stringify(
proxies
)}`
);
throw new Error(`Failed to establish a socket connection to proxies: ${result}`);
}
}

export function getProxyURLFromResolverResult(result: string | undefined) {
// Default to "DIRECT" if a falsey value was returned (or nothing)
if (!result) {
return { proxy: 'DIRECT', url: undefined };
}

const proxies = String(result)
.trim()
.split(/\s*;\s*/g)
.filter(Boolean);

for (const proxy of proxies) {
const [type, target] = proxy.split(/\s+/);
debug('Attempting to use proxy: %o', proxy);

if (type === 'DIRECT') {
return { proxy, url: undefined };
} else if (type === 'SOCKS' || type === 'SOCKS5') {
// Use a SOCKSv5h proxy
return { proxy, url: `socks://${target}` };
} else if (type === 'SOCKS4') {
// Use a SOCKSv4a proxy
return { proxy, url: `socks4a://${target}` };
} else if (
type === 'PROXY' ||
type === 'HTTP' ||
type === 'HTTPS'
) {
// Use an HTTP or HTTPS proxy
// http://dev.chromium.org/developers/design-documents/secure-web-proxy
return { proxy, url: `${type === 'HTTPS' ? 'https' : 'http'}://${target}` };
}
}
return { proxy: 'DIRECT', url: undefined };
}

type LookupProxyAuthorization = (proxyURL: string, proxyAuthenticate: string | string[] | undefined, state: Record<string, any>) => Promise<string | undefined>;

type HttpsProxyAgentOptions2<Uri> = HttpsProxyAgentOptions<Uri> & { lookupProxyAuthorization?: LookupProxyAuthorization };
Expand Down Expand Up @@ -228,10 +233,10 @@ class HttpsProxyAgent2<Uri extends string> extends HttpsProxyAgent<Uri> {
}
}

function createPacProxyAgent(
export function createPacProxyAgent(
resolver: FindProxyForURL,
opts?: createPacProxyAgent.PacProxyAgentOptions
): _PacProxyAgent {
opts?: PacProxyAgentOptions
): PacProxyAgent {
if (!opts) {
opts = {};
}
Expand All @@ -240,22 +245,12 @@ function createPacProxyAgent(
throw new TypeError('a resolve function must be specified!');
}

return new _PacProxyAgent(resolver, opts);
return new PacProxyAgent(resolver, opts);
}

namespace createPacProxyAgent {
export type PacProxyAgentOptions =
HttpProxyAgentOptions<''> &
HttpsProxyAgentOptions2<''> &
SocksProxyAgentOptions & {
fallbackToDirect?: boolean;
originalAgent?: false | http.Agent;
}

export type PacProxyAgent = _PacProxyAgent;
export const PacProxyAgent = _PacProxyAgent;

createPacProxyAgent.prototype = _PacProxyAgent.prototype;
type PacProxyAgentOptions =
HttpProxyAgentOptions<''> &
HttpsProxyAgentOptions2<''> &
SocksProxyAgentOptions & {
fallbackToDirect?: boolean;
originalAgent?: false | http.Agent;
}

export = createPacProxyAgent;
51 changes: 33 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as fs from 'fs';
import * as cp from 'child_process';
import * as crypto from 'crypto';

import createPacProxyAgent, { PacProxyAgent } from './agent';
import { createPacProxyAgent, getProxyURLFromResolverResult, PacProxyAgent } from './agent';

export enum LogLevel {
Trace,
Expand Down Expand Up @@ -76,7 +76,7 @@ export interface ProxyAgentParams {
}

export function createProxyResolver(params: ProxyAgentParams) {
const { getProxyURL, log, getLogLevel, proxyResolveTelemetry: proxyResolverTelemetry, useHostProxy, env } = params;
const { getProxyURL, log, proxyResolveTelemetry: proxyResolverTelemetry, env } = params;
let envProxy = proxyFromConfigURL(env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY); // Not standardized.

let envNoProxy = noProxyFromEnv(env.no_proxy || env.NO_PROXY); // Not standardized.
Expand Down Expand Up @@ -128,25 +128,23 @@ export function createProxyResolver(params: ProxyAgentParams) {
results = [];
}

function resolveProxy(flags: { useProxySettings: boolean, addCertificatesV1: boolean }, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) {
function resolveProxyWithRequest(flags: { useProxySettings: boolean, addCertificatesV1: boolean }, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) {
if (!timeout) {
timeout = setTimeout(logEvent, 10 * 60 * 1000);
}

const stackText = ''; // getLogLevel() === LogLevel.Trace ? '\n' + new Error('Error for stack trace').stack : '';

addCertificatesV1(params, flags.addCertificatesV1, opts, () => {
useProxySettings(useHostProxy, flags.useProxySettings, req, opts, url, stackText, callback);
if (!flags.useProxySettings) {
callback('DIRECT');
return;
}
useProxySettings(url, req, stackText, callback);
});
}

function useProxySettings(useHostProxy: boolean, useProxySettings: boolean, req: http.ClientRequest, opts: http.RequestOptions, url: string, stackText: string, callback: (proxy?: string) => void) {

if (!useProxySettings) {
callback('DIRECT');
return;
}

function useProxySettings(url: string, req: http.ClientRequest | undefined, stackText: string, callback: (proxy?: string) => void) {
const parsedUrl = nodeurl.parse(url); // Coming from Node's URL, sticking with that.

const hostname = parsedUrl.hostname;
Expand All @@ -157,7 +155,7 @@ export function createProxyResolver(params: ProxyAgentParams) {
return;
}

const { secureEndpoint } = opts as any;
const secureEndpoint = parsedUrl.protocol === 'https:';
const defaultPort = secureEndpoint ? 443 : 80;

// if there are any config entries present then env variables are ignored
Expand Down Expand Up @@ -198,13 +196,15 @@ export function createProxyResolver(params: ProxyAgentParams) {
const proxy = getCachedProxy(key);
if (proxy) {
cacheCount++;
collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req);
if (req) {
collectResult(results, proxy, secureEndpoint ? 'HTTPS' : 'HTTP', req);
}
callback(proxy);
log.debug('ProxyResolver#resolveProxy cached', url, proxy, stackText);
return;
}

if (!useHostProxy) {
if (!params.useHostProxy) {
callback('DIRECT');
log.debug('ProxyResolver#resolveProxy unconfigured', url, 'DIRECT', stackText);
return;
Expand All @@ -215,7 +215,9 @@ export function createProxyResolver(params: ProxyAgentParams) {
.then(proxy => {
if (proxy) {
cacheProxy(key, proxy);
collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req);
if (req) {
collectResult(results, proxy, secureEndpoint ? 'HTTPS' : 'HTTP', req);
}
}
callback(proxy);
log.debug('ProxyResolver#resolveProxy', url, proxy, stackText);
Expand All @@ -230,7 +232,18 @@ export function createProxyResolver(params: ProxyAgentParams) {
});
}

return resolveProxy;
return {
resolveProxyWithRequest,
resolveProxyURL: (url: string) => new Promise<string | undefined>((resolve, reject) => {
useProxySettings(url, undefined, '', result => {
try {
resolve(getProxyURLFromResolverResult(result).url);
} catch (err) {
reject(err);
}
});
}),
};
}

function collectResult(results: ConnectionResult[], resolveProxy: string, connection: string, req: http.ClientRequest) {
Expand Down Expand Up @@ -317,7 +330,9 @@ function noProxyFromConfig(noProxy: string[]) {

export type ProxySupportSetting = 'override' | 'fallback' | 'on' | 'off';

export function createHttpPatch(params: ProxyAgentParams, originals: typeof http | typeof https, resolveProxy: ReturnType<typeof createProxyResolver>) {
export type ResolveProxyWithRequest = (flags: { useProxySettings: boolean, addCertificatesV1: boolean }, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) => void;

export function createHttpPatch(params: ProxyAgentParams, originals: typeof http | typeof https, resolveProxy: ResolveProxyWithRequest) {
return {
get: patch(originals.get),
request: patch(originals.request)
Expand Down Expand Up @@ -551,7 +566,7 @@ function addCertificatesV1(params: ProxyAgentParams, addCertificatesV1: boolean,

let _certificatesPromise: Promise<string[]> | undefined;
let _certificates: string[] | undefined;
async function getOrLoadAdditionalCertificates(params: ProxyAgentParams) {
export async function getOrLoadAdditionalCertificates(params: ProxyAgentParams) {
if (!_certificatesPromise) {
_certificatesPromise = (async () => {
return _certificates = await params.loadAdditionalCertificates();
Expand Down
Loading

0 comments on commit 918cc19

Please sign in to comment.