Skip to content

Commit

Permalink
feat(proxy): support no_proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Dec 24, 2023
1 parent 742d27e commit 2dbbec0
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 27 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ Node.js has no built-in support for HTTP Proxies for fetch (see [nodejs/undici#1

This package bundles a compact and simple proxy-supported solution for both Node.js versions without native fetch using [HTTP Agent](https://github.com/TooTallNate/proxy-agents/tree/main/packages/proxy-agent) and versions with native fetch using [Undici Proxy Agent](https://undici.nodejs.org/#/docs/api/ProxyAgent).

By default, `https_proxy`, `http_proxy`, `HTTPS_PROXY`, and `HTTP_PROXY` environment variables will be checked and used (in order) for the proxy and if not any of them are set, the proxy will be disabled.
By default, `https_proxy`, `http_proxy`, `HTTPS_PROXY`, and `HTTP_PROXY` environment variables will be checked and used (in order) for the proxy and if not any of them are set, the proxy will be disabled. You can override it using `url` option passed to `createFetch` and `createProxy` utils.

By default, `no_proxy` and `NO_PROXY` environment variables will be checked and used for the (comma seperated) list of hosts to ignore proxy for. You can override it using `noProxy` option passed to `createFetch` and `createProxy` utils. The enties staring with a dot will be used to check domain and also any subdomain.

> [!NOTE]
> Using export conditions, this utility adds proxy support for Node.js and for other runtimes, it will simply return native fetch.
Expand Down
18 changes: 17 additions & 1 deletion lib/proxy.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@ import type * as http from "node:http";
import type * as https from "node:https";
import type * as undici from "undici";

export type ProxyOptions = { url?: string };
export type ProxyOptions = {
/**
* HTTP(s) Proxy URL
*
* Default is read from `https_proxy`, `http_proxy`, `HTTPS_PROXY` or `HTTP_PROXY` environment variables
* */
url?: string;

/**
* List of hosts to skip proxy for (comma seperated or array of strings)
*
* Default is read from `no_proxy` or `NO_PROXY` environment variables
*
* Hots starting with a leading dot like `.foo.com` are also matched against domain and all its subdomains like `bar.foo.com`
*/
noProxy?: string | string[];
};

export declare const createProxy: (opts?: ProxyOptions) => {
agent: http.Agent | https.Agent | undefined;
Expand Down
86 changes: 61 additions & 25 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as http from "node:http";
import * as https from "node:https";
import { URL } from "node:url";
import { ProxyAgent as UndiciProxyAgent } from "undici";
import { Agent as _UndiciAgent, ProxyAgent as _UndiciProxyAgent } from "undici";
import { Agent, AgentConnectOpts } from "agent-base";
import { HttpProxyAgent } from "http-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent";
Expand All @@ -23,10 +23,16 @@ export function createProxy(opts: ProxyOptions = {}) {
};
}

const nodeAgent = new NodeProxyAgent({ uri });
const _noProxy = opts.noProxy || process.env.no_proxy || process.env.NO_PROXY;
const noProxy = typeof _noProxy === "string" ? _noProxy.split(",") : _noProxy;

const nodeAgent = new NodeProxyAgent({ uri, noProxy });

// https://undici.nodejs.org/#/docs/api/ProxyAgent
const undiciAgent = new UndiciProxyAgent({ uri });
const undiciAgent = new UndiciProxyAgent({
uri,
noProxy,
});

return {
agent: nodeAgent,
Expand All @@ -45,9 +51,47 @@ export const fetch = createFetch({});
// Utils
// ----------------------------------------------

export function debug(...args: any[]) {
if (process.env.debug) {
debug("[node-fetch-native] [proxy]", ...args);
function debug(...args: any[]) {
if (process.env.DEBUG) {
console.debug("[node-fetch-native] [proxy]", ...args);
}
}

function bypassProxy(host: string, noProxy: string[]) {
if (!noProxy) {
return false;
}
for (const _host of noProxy) {
if (_host === host || (_host[0] === "." && host.endsWith(_host.slice(1)))) {
return true;
}
}
return false;
}

// ----------------------------------------------
// Undici Agent
// ----------------------------------------------

// https://github.com/nodejs/undici/blob/main/lib/proxy-agent.js

class UndiciProxyAgent extends _UndiciProxyAgent {
_agent: _UndiciAgent;

constructor(
private _options: _UndiciProxyAgent.Options & { noProxy: string[] },
) {
super(_options);
this._agent = new _UndiciAgent();
}

dispatch(options, handler): boolean {
const hostname = new URL(options.origin).hostname;
if (bypassProxy(hostname, this._options.noProxy)) {
debug(`Bypassing proxy for: ${hostname}`);
return this._agent.dispatch(options, handler);
}
return super.dispatch(options, handler);
}
}

Expand All @@ -73,15 +117,14 @@ function isValidProtocol(v: string): v is ValidProtocol {
return (PROTOCOLS as readonly string[]).includes(v);
}

export class NodeProxyAgent extends Agent {
class NodeProxyAgent extends Agent {
cache: Map<string, Agent> = new Map();

httpAgent: http.Agent;
httpsAgent: http.Agent;

constructor(private proxyOptions: { uri: string }) {
constructor(private _options: { uri: string; noProxy: string[] }) {
super({});
debug("Creating new ProxyAgent instance: %o", proxyOptions);
this.httpAgent = new http.Agent({});
this.httpsAgent = new https.Agent({});
}
Expand All @@ -94,33 +137,26 @@ export class NodeProxyAgent extends Agent {
? (isWebSocket ? "wss:" : "https:")
: (isWebSocket ? "ws:" : "http:");

const host = req.getHeader("host");
const url = new URL(req.path, `${protocol}//${host}`).href;
const proxy = this.proxyOptions.uri;
const host = req.getHeader("host") as string;

if (!proxy) {
debug("Proxy not enabled for URL: %o", url);
if (bypassProxy(host, this._options.noProxy)) {
return opts.secureEndpoint ? this.httpsAgent : this.httpAgent;
}

debug("Request URL: %o", url);
debug("Proxy URL: %o", proxy);

// Attempt to get a cached `http.Agent` instance first
const cacheKey = `${protocol}+${proxy}`;
const cacheKey = `${protocol}+${this._options.uri}`;
let agent = this.cache.get(cacheKey);
if (agent) {
debug("Cache hit for proxy URL: %o", proxy);
} else {
const proxyUrl = new URL(proxy);
if (!agent) {
const proxyUrl = new URL(this._options.uri);
const proxyProto = proxyUrl.protocol.replace(":", "");
if (!isValidProtocol(proxyProto)) {
throw new Error(`Unsupported protocol for proxy URL: ${proxy}`);
throw new Error(
`Unsupported protocol for proxy URL: ${this._options.uri}`,
);
}
const Ctor =
proxies[proxyProto][opts.secureEndpoint || isWebSocket ? 1 : 0];
// @ts-expect-error meh…
agent = new Ctor(proxy, this.connectOpts);
agent = new (Ctor as any)(this._options.uri, this._options);
this.cache.set(cacheKey, agent);
}

Expand Down

0 comments on commit 2dbbec0

Please sign in to comment.