Skip to content

Commit

Permalink
feat: add createGlobalProxyAgent
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Jun 21, 2019
1 parent e253caf commit 5ce7163
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 228 deletions.
15 changes: 15 additions & 0 deletions README.md
Expand Up @@ -93,6 +93,21 @@ if (MAJOR_NODEJS_VERSION >= 10) {

```

### Setup proxy using `createGlobalProxyAgent`

If you do not want to use `global.GLOBAL_AGENT` variable, then you can use `createGlobalProxyAgent` to instantiate a controlled instance of `global-agent`, e.g.

```js
import {
createGlobalProxyAgent
} from 'global-agent';

const globalProxyAgent = createGlobalProxyAgent();

```

Unlike `bootstrap` routine, `createGlobalProxyAgent` factory does not create `global.GLOBAL_AGENT` variable and does not guard against multiple initializations of `global-agent`. The result object of `createGlobalProxyAgent` is equivalent to `global.GLOBAL_AGENT`.

### Runtime configuration

`global-agent/bootstrap` script copies `process.env.GLOBAL_AGENT_HTTP_PROXY` value to `global.GLOBAL_AGENT.HTTP_PROXY` and continues to use the latter variable.
Expand Down
145 changes: 145 additions & 0 deletions src/factories/createGlobalProxyAgent.js
@@ -0,0 +1,145 @@
// @flow

import EventEmitter from 'events';
import http from 'http';
import https from 'https';
import semver from 'semver';
import Logger from '../Logger';
import {
HttpProxyAgent,
HttpsProxyAgent
} from '../classes';
import {
UnexpectedStateError
} from '../errors';
import {
bindHttpMethod,
isUrlMatchingNoProxy,
parseProxyUrl
} from '../utilities';
import type {
ProxyAgentConfigurationInputType,
ProxyAgentConfigurationType
} from '../types';
import createProxyController from './createProxyController';

const defaultConfigurationInput = {
environmentVariableNamespace: undefined
};

const log = Logger.child({
namespace: 'createGlobalProxyAgent'
});

const createConfiguration = (configurationInput: ProxyAgentConfigurationInputType): ProxyAgentConfigurationType => {
// eslint-disable-next-line no-process-env
const DEFAULT_ENVIRONMENT_VARIABLE_NAMESPACE = typeof process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE === 'string' ? process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE : 'GLOBAL_AGENT_';

return {
...configurationInput,
environmentVariableNamespace: typeof configurationInput.environmentVariableNamespace === 'string' ? configurationInput.environmentVariableNamespace : DEFAULT_ENVIRONMENT_VARIABLE_NAMESPACE
};
};

export default (configurationInput: ProxyAgentConfigurationInputType = defaultConfigurationInput) => {
const configuration = createConfiguration(configurationInput);

const proxyController = createProxyController();

// eslint-disable-next-line no-process-env
proxyController.HTTP_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTP_PROXY'] || null;

// eslint-disable-next-line no-process-env
proxyController.HTTPS_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTPS_PROXY'] || null;

// eslint-disable-next-line no-process-env
proxyController.NO_PROXY = process.env[configuration.environmentVariableNamespace + 'NO_PROXY'] || null;

log.info({
configuration: proxyController
}, 'global agent has been initialized');

const isProxyConfigured = (getProxy) => {
return () => {
return getProxy();
};
};

const mustUrlUseProxy = (getProxy) => {
return (url) => {
if (!getProxy()) {
return false;
}

if (!proxyController.NO_PROXY) {
return true;
}

return !isUrlMatchingNoProxy(url, proxyController.NO_PROXY);
};
};

const getUrlProxy = (getProxy) => {
return () => {
const proxy = getProxy();
if (!proxy) {
throw new UnexpectedStateError('HTTP(S) proxy must be configured.');
}

return parseProxyUrl(proxy);
};
};

const eventEmitter = new EventEmitter();

const getHttpProxy = () => {
return proxyController.HTTP_PROXY;
};

const httpAgent = new HttpProxyAgent(
isProxyConfigured(getHttpProxy),
mustUrlUseProxy(getHttpProxy),
getUrlProxy(getHttpProxy),
http.globalAgent,
eventEmitter
);

const getHttpsProxy = () => {
return proxyController.HTTPS_PROXY || proxyController.HTTP_PROXY;
};

const httpsAgent = new HttpsProxyAgent(
isProxyConfigured(getHttpsProxy),
mustUrlUseProxy(getHttpsProxy),
getUrlProxy(getHttpsProxy),
https.globalAgent,
eventEmitter
);

// Overriding globalAgent was added in v11.7.
// @see https://nodejs.org/uk/blog/release/v11.7.0/
if (semver.gte(process.version, 'v11.7.0')) {
// @see https://github.com/facebook/flow/issues/7670
// $FlowFixMe
http.globalAgent = httpAgent;

// $FlowFixMe
https.globalAgent = httpsAgent;
} else if (semver.gte(process.version, 'v10')) {
// $FlowFixMe
http.get = bindHttpMethod(http.get, httpAgent);

// $FlowFixMe
http.request = bindHttpMethod(http.request, httpAgent);

// $FlowFixMe
https.get = bindHttpMethod(https.get, httpsAgent);

// $FlowFixMe
https.request = bindHttpMethod(https.request, httpsAgent);
} else {
log.warn('attempt to initialize global-agent in unsupported Node.js version was ignored');
}

return proxyController;
};
@@ -1,25 +1,33 @@
// @flow

import Events from 'events';
import Logger from '../Logger';

type ProxyControllerType = {|
eventEmitter: Events | null,
HTTP_PROXY: string | null,
HTTPS_PROXY: string | null,
NO_PROXY: string | null
|};

const log = Logger.child({
namespace: 'createGlobalAgentGlobal'
namespace: 'createProxyController'
});

const KNOWN_PROPERTY_NAMES = [
'bootstrapped',
'eventEmitter',
'HTTP_PROXY',
'HTTPS_PROXY',
'NO_PROXY'
];

export default () => {
export default (): ProxyControllerType => {
// eslint-disable-next-line fp/no-proxy
return new Proxy({
bootstrapped: false,
HTTP_PROXY: '',
HTTPS_PROXY: '',
NO_PROXY: ''
eventEmitter: null,
HTTP_PROXY: null,
HTTPS_PROXY: null,
NO_PROXY: null
}, {
set: (subject, name, value) => {
if (!KNOWN_PROPERTY_NAMES.includes(name)) {
Expand Down
3 changes: 2 additions & 1 deletion src/factories/index.js
@@ -1,3 +1,4 @@
// @flow

export {default as createGlobalAgentGlobal} from './createGlobalAgentGlobal';
export {default as createGlobalProxyAgent} from './createGlobalProxyAgent';
export {default as createProxyController} from './createProxyController';
1 change: 1 addition & 0 deletions src/index.js
@@ -1,3 +1,4 @@
// @flow

export {bootstrap} from './routines';
export {createGlobalProxyAgent} from './factories';
148 changes: 10 additions & 138 deletions src/routines/bootstrap.js
@@ -1,153 +1,25 @@
// @flow

import EventEmitter from 'events';
import http from 'http';
import https from 'https';
import semver from 'semver';
import Logger from '../Logger';
import {
HttpProxyAgent,
HttpsProxyAgent
} from '../classes';
import {
UnexpectedStateError
} from '../errors';
import {
createGlobalAgentGlobal
createGlobalProxyAgent
} from '../factories';
import {
bindHttpMethod,
isUrlMatchingNoProxy,
parseProxyUrl
} from '../utilities';

type ConfigurationInputType = {|
+environmentVariableNamespace?: string
|};

type ConfigurationType = {|
+environmentVariableNamespace: string
|};

const defaultConfigurationInput = {
environmentVariableNamespace: undefined
};
import type {
ProxyAgentConfigurationInputType
} from '../types';

const log = Logger.child({
namespace: 'bootstrap'
});

const createConfiguration = (configurationInput: ConfigurationInputType): ConfigurationType => {
// eslint-disable-next-line no-process-env
const DEFAULT_ENVIRONMENT_VARIABLE_NAMESPACE = typeof process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE === 'string' ? process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE : 'GLOBAL_AGENT_';
export default (configurationInput?: ProxyAgentConfigurationInputType): boolean => {
if (global.GLOBAL_AGENT) {
log.warn('found global.GLOBAL_AGENT; second attempt to bootstrap global-agent was ignored');

return {
...configurationInput,
environmentVariableNamespace: typeof configurationInput.environmentVariableNamespace === 'string' ? configurationInput.environmentVariableNamespace : DEFAULT_ENVIRONMENT_VARIABLE_NAMESPACE
};
};

export default (configurationInput: ConfigurationInputType = defaultConfigurationInput) => {
const configuration = createConfiguration(configurationInput);

global.GLOBAL_AGENT = global.GLOBAL_AGENT || createGlobalAgentGlobal();

if (global.GLOBAL_AGENT.bootstrapped) {
log.warn('found global.globalAgent; second attempt to bootstrap global-agent was ignored');

return;
return false;
}

global.GLOBAL_AGENT.bootstrapped = true;

// eslint-disable-next-line no-process-env
global.GLOBAL_AGENT.HTTP_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTP_PROXY'] || null;

// eslint-disable-next-line no-process-env
global.GLOBAL_AGENT.HTTPS_PROXY = process.env[configuration.environmentVariableNamespace + 'HTTPS_PROXY'] || null;

// eslint-disable-next-line no-process-env
global.GLOBAL_AGENT.NO_PROXY = process.env[configuration.environmentVariableNamespace + 'NO_PROXY'] || null;
global.GLOBAL_AGENT = createGlobalProxyAgent(configurationInput);

log.info({
configuration: global.GLOBAL_AGENT
}, 'global agent has been initialized');

const isProxyConfigured = (getProxy) => {
return () => {
return getProxy();
};
};

const mustUrlUseProxy = (getProxy) => {
return (url) => {
if (!getProxy()) {
return false;
}

if (!global.GLOBAL_AGENT.NO_PROXY) {
return true;
}

return !isUrlMatchingNoProxy(url, global.GLOBAL_AGENT.NO_PROXY);
};
};

const getUrlProxy = (getProxy) => {
return () => {
const proxy = getProxy();
if (!proxy) {
throw new UnexpectedStateError('HTTP(S) proxy must be configured.');
}

return parseProxyUrl(proxy);
};
};

const eventEmitter = new EventEmitter();

const getHttpProxy = () => {
return global.GLOBAL_AGENT.HTTP_PROXY;
};
const httpAgent = new HttpProxyAgent(
isProxyConfigured(getHttpProxy),
mustUrlUseProxy(getHttpProxy),
getUrlProxy(getHttpProxy),
http.globalAgent,
eventEmitter
);

const getHttpsProxy = () => {
return global.GLOBAL_AGENT.HTTPS_PROXY || global.GLOBAL_AGENT.HTTP_PROXY;
};
const httpsAgent = new HttpsProxyAgent(
isProxyConfigured(getHttpsProxy),
mustUrlUseProxy(getHttpsProxy),
getUrlProxy(getHttpsProxy),
https.globalAgent,
eventEmitter
);

// Overriding globalAgent was added in v11.7.
// @see https://nodejs.org/uk/blog/release/v11.7.0/
if (semver.gte(process.version, 'v11.7.0')) {
// @see https://github.com/facebook/flow/issues/7670
// $FlowFixMe
http.globalAgent = httpAgent;

// $FlowFixMe
https.globalAgent = httpsAgent;
} else {
// $FlowFixMe
http.get = bindHttpMethod(http.get, httpAgent);

// $FlowFixMe
http.request = bindHttpMethod(http.request, httpAgent);

// $FlowFixMe
https.get = bindHttpMethod(https.get, httpsAgent);

// $FlowFixMe
https.request = bindHttpMethod(https.request, httpsAgent);
}
return true;
};
8 changes: 8 additions & 0 deletions src/types.js
Expand Up @@ -32,3 +32,11 @@ export type IsProxyConfiguredMethodType = () => boolean;
export type MustUrlUseProxyMethodType = (url: string) => boolean;
export type GetUrlProxyMethodType = (url: string) => ProxyConfigurationType;
export type ProtocolType = 'http:' | 'https:';

export type ProxyAgentConfigurationInputType = {|
+environmentVariableNamespace?: string
|};

export type ProxyAgentConfigurationType = {|
+environmentVariableNamespace: string
|};

0 comments on commit 5ce7163

Please sign in to comment.