@tramvai/module-http-proxy-agent 1.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # HttpProxyAgent
2
+
3
+ Enable support for http_proxy, https_proxy and no_proxy env variables
4
+
5
+ ## Installation
6
+
7
+ You need to install `@tramvai/module-http-proxy-agent`
8
+
9
+ ```bash
10
+ yarn add @tramvai/module-http-proxy-agent
11
+ ```
12
+
13
+ And connect in the project
14
+
15
+ ```tsx
16
+ import { createApp } from '@tramvai/core';
17
+ import { HttpProxyAgentModule } from '@tramvai/module-http-proxy-agent';
18
+
19
+ createApp({
20
+ name: 'tincoin',
21
+ modules: [ HttpProxyAgentModule ],
22
+ });
23
+ ```
24
+
25
+ ## Environment variables
26
+
27
+ - `HTTP_PROXY` - proxy url for HTTP requests
28
+ - `http_proxy` - see `HTTP_PROXY`
29
+
30
+ - `HTTPS_PROXY` - proxy url for HTTPS requests
31
+ - `https_proxy` - see `HTTPS_PROXY`
32
+
33
+ - `NO_PROXY` - list of urls patterns for which proxying is disabled
34
+ - `no_proxy` - see `NO_PROXY`
35
+
36
+ ## Explanation
37
+
38
+ `HttpProxyAgentModule` mokeypatch standard NodeJS [https.Agent](https://nodejs.org/dist/latest-v16.x/docs/api/https.html#class-httpsagent) for supporting connections via forwarding proxy, if some of mentioned above env variables are presented.
39
+
40
+ Some `NO_PROXY` env specification and examples [available here](https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/#standardizing-no_proxy)
41
+
42
+ Source code forked from [node-keepalive-proxy-agent](https://github.com/mknj/node-keepalive-proxy-agent)
43
+
44
+ ## Limitations
45
+
46
+ `HttpProxyAgentModule` [support only HTTPS requests](https://github.com/mknj/node-keepalive-proxy-agent/issues/28)
@@ -0,0 +1,34 @@
1
+ /// <reference types="node" />
2
+ import type { AgentOptions } from 'https';
3
+ import type { Socket } from 'net';
4
+ import type { LOGGER_TOKEN } from '@tramvai/tokens-common';
5
+ declare type Proxy = {
6
+ hostname: string;
7
+ port: number;
8
+ auth?: string;
9
+ };
10
+ export interface HttpsProxyAgentOptions extends AgentOptions {
11
+ proxy?: Proxy;
12
+ logger: ReturnType<typeof LOGGER_TOKEN>;
13
+ }
14
+ export interface ConnectOptions {
15
+ protocol: string;
16
+ host: string;
17
+ port: number;
18
+ hostname: string;
19
+ search: string;
20
+ query: string;
21
+ pathname: string;
22
+ path: string;
23
+ href: string;
24
+ method: string;
25
+ headers: Record<string, string[]>;
26
+ socket: Socket;
27
+ }
28
+ /**
29
+ * Fork of https://github.com/mknj/node-keepalive-proxy-agent with monkeypatching and no_proxy support
30
+ */
31
+ export declare const addProxyToHttpsAgent: ({ logger }: {
32
+ logger: ReturnType<typeof LOGGER_TOKEN>;
33
+ }) => void;
34
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Support specification proposal from https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/#the-lowest-common-denominator
3
+ */
4
+ export declare const matchNoProxy: ({ noProxy, hostname, }: {
5
+ noProxy: string;
6
+ hostname: string;
7
+ }) => boolean;
@@ -0,0 +1,2 @@
1
+ export declare class HttpProxyAgentModule {
2
+ }
package/lib/browser.js ADDED
@@ -0,0 +1,13 @@
1
+ import { __decorate } from 'tslib';
2
+ import { Module } from '@tramvai/core';
3
+
4
+ let HttpProxyAgentModule = class HttpProxyAgentModule {
5
+ };
6
+ HttpProxyAgentModule = __decorate([
7
+ Module({
8
+ imports: [],
9
+ providers: [],
10
+ })
11
+ ], HttpProxyAgentModule);
12
+
13
+ export { HttpProxyAgentModule };
@@ -0,0 +1,2 @@
1
+ export declare class HttpProxyAgentModule {
2
+ }
@@ -0,0 +1,168 @@
1
+ import { __decorate } from 'tslib';
2
+ import { Module, commandLineListTokens } from '@tramvai/core';
3
+ import { LOGGER_TOKEN } from '@tramvai/tokens-common';
4
+ import https from 'https';
5
+ import net from 'net';
6
+ import url from 'url';
7
+
8
+ const httpProxyEnabled = () => {
9
+ const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env;
10
+ return !!(http_proxy || HTTP_PROXY || https_proxy || HTTPS_PROXY || no_proxy || NO_PROXY);
11
+ };
12
+ const getHttpsProxy = () => {
13
+ const { https_proxy, HTTPS_PROXY } = process.env;
14
+ return https_proxy || HTTPS_PROXY || '';
15
+ };
16
+ const getNoProxy = () => {
17
+ const { no_proxy, NO_PROXY } = process.env;
18
+ return no_proxy || NO_PROXY || '';
19
+ };
20
+
21
+ const RULES_SEPARATOR = ',';
22
+ const HOST_PORT_SEPARATOR = ':';
23
+ const WILDCARD = '*';
24
+ /**
25
+ * Support specification proposal from https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/#the-lowest-common-denominator
26
+ */
27
+ const matchNoProxy = ({ noProxy, hostname, }) => {
28
+ const rules = noProxy.split(RULES_SEPARATOR).filter(Boolean);
29
+ for (const rule of rules) {
30
+ // @todo: respect port when matching
31
+ const [ruleHost, _rulePort] = rule.split(HOST_PORT_SEPARATOR);
32
+ // Wildcard (*) match all hosts
33
+ if (ruleHost === WILDCARD) {
34
+ return true;
35
+ }
36
+ // Strip leading dots (.) and wildcard with dot (*.) for backward compatibility
37
+ const ruleHostWithoutLeadingDot = ruleHost.replace(/^\*?\./, '');
38
+ const matchRegex = new RegExp(`${ruleHostWithoutLeadingDot}$`);
39
+ /**
40
+ * @example
41
+ * 'localhost'.match(/localhost$/)
42
+ * 'api.test.com'.match(/test.com$/)
43
+ */
44
+ if (hostname.match(matchRegex)) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ };
50
+
51
+ /* eslint-disable no-param-reassign */
52
+ /**
53
+ * Fork of https://github.com/mknj/node-keepalive-proxy-agent with monkeypatching and no_proxy support
54
+ */
55
+ const addProxyToHttpsAgent = ({ logger }) => {
56
+ const httpsProxyEnv = getHttpsProxy();
57
+ const noProxyEnv = getNoProxy();
58
+ const noProxyMatchResults = {};
59
+ if (!httpsProxyEnv) {
60
+ return;
61
+ }
62
+ const parsedProxy = new url.URL(httpsProxyEnv);
63
+ const proxy = { hostname: parsedProxy.hostname, port: parsedProxy.port };
64
+ logger.debug({
65
+ event: 'parsed proxy',
66
+ proxy,
67
+ });
68
+ // @ts-expect-error
69
+ const originalCreateConnection = https.Agent.prototype.createConnection;
70
+ // @ts-expect-error
71
+ https.Agent.prototype.createConnection = createConnection;
72
+ function createConnection(options, cb) {
73
+ const { hostname, href, method, headers } = options;
74
+ const connectionMustBeProxied = !noProxyEnv || !matchNoProxyWithCache({ hostname });
75
+ if (connectionMustBeProxied) {
76
+ logger.debug({
77
+ event: 'proxy connection',
78
+ connection: {
79
+ href,
80
+ method,
81
+ headers,
82
+ },
83
+ });
84
+ createConnectionHttpsAfterHttp.call(this, options, cb);
85
+ }
86
+ else {
87
+ cb(null, originalCreateConnection.call(this, options));
88
+ }
89
+ }
90
+ function createConnectionHttpsAfterHttp(options, cb) {
91
+ const proxySocket = net.connect(+proxy.port, proxy.hostname);
92
+ const errorListener = (error) => {
93
+ proxySocket.destroy();
94
+ cb(error);
95
+ };
96
+ proxySocket.once('error', errorListener);
97
+ let response = '';
98
+ const dataListener = (data) => {
99
+ response += data.toString();
100
+ if (!response.endsWith('\r\n\r\n')) {
101
+ // response not completed yet
102
+ return;
103
+ }
104
+ proxySocket.removeListener('error', errorListener);
105
+ proxySocket.removeListener('data', dataListener);
106
+ const m = response.match(/^HTTP\/1.\d (\d*)/);
107
+ if (m == null || m[1] == null) {
108
+ proxySocket.destroy();
109
+ return cb(new Error(response.trim()));
110
+ }
111
+ if (m[1] !== '200') {
112
+ proxySocket.destroy();
113
+ return cb(new Error(m[0]));
114
+ }
115
+ // tell super function to use our proxy socket
116
+ options.socket = proxySocket;
117
+ cb(null, originalCreateConnection.call(this, options));
118
+ };
119
+ proxySocket.on('data', dataListener);
120
+ let host = options.hostname;
121
+ if (!host) {
122
+ host = options.host;
123
+ }
124
+ // https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.6
125
+ let cmd = `CONNECT ${host}:${options.port} HTTP/1.1\r\n`;
126
+ cmd += `Host: ${host}:${options.port}\r\n`;
127
+ cmd += '\r\n';
128
+ proxySocket.write(cmd);
129
+ }
130
+ function matchNoProxyWithCache({ hostname }) {
131
+ if (hostname in noProxyMatchResults) {
132
+ return noProxyMatchResults[hostname];
133
+ }
134
+ const noProxy = noProxyEnv;
135
+ const result = matchNoProxy({ noProxy, hostname });
136
+ noProxyMatchResults[hostname] = result;
137
+ return result;
138
+ }
139
+ };
140
+ /* eslint-enable no-param-reassign */
141
+
142
+ let HttpProxyAgentModule = class HttpProxyAgentModule {
143
+ };
144
+ HttpProxyAgentModule = __decorate([
145
+ Module({
146
+ imports: [],
147
+ providers: [
148
+ httpProxyEnabled() && {
149
+ provide: commandLineListTokens.init,
150
+ multi: true,
151
+ useFactory: ({ loggerFactory }) => function addHttpsProxy() {
152
+ const logger = loggerFactory('http-proxy-agent');
153
+ logger.debug({
154
+ event: 'proxy agent enabled',
155
+ proxyEnv: getHttpsProxy(),
156
+ noProxyEnv: getNoProxy(),
157
+ });
158
+ addProxyToHttpsAgent({ logger });
159
+ },
160
+ deps: {
161
+ loggerFactory: LOGGER_TOKEN,
162
+ },
163
+ },
164
+ ].filter(Boolean),
165
+ })
166
+ ], HttpProxyAgentModule);
167
+
168
+ export { HttpProxyAgentModule };
package/lib/server.js ADDED
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var tslib = require('tslib');
6
+ var core = require('@tramvai/core');
7
+ var tokensCommon = require('@tramvai/tokens-common');
8
+ var https = require('https');
9
+ var net = require('net');
10
+ var url = require('url');
11
+
12
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
13
+
14
+ var https__default = /*#__PURE__*/_interopDefaultLegacy(https);
15
+ var net__default = /*#__PURE__*/_interopDefaultLegacy(net);
16
+ var url__default = /*#__PURE__*/_interopDefaultLegacy(url);
17
+
18
+ const httpProxyEnabled = () => {
19
+ const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY } = process.env;
20
+ return !!(http_proxy || HTTP_PROXY || https_proxy || HTTPS_PROXY || no_proxy || NO_PROXY);
21
+ };
22
+ const getHttpsProxy = () => {
23
+ const { https_proxy, HTTPS_PROXY } = process.env;
24
+ return https_proxy || HTTPS_PROXY || '';
25
+ };
26
+ const getNoProxy = () => {
27
+ const { no_proxy, NO_PROXY } = process.env;
28
+ return no_proxy || NO_PROXY || '';
29
+ };
30
+
31
+ const RULES_SEPARATOR = ',';
32
+ const HOST_PORT_SEPARATOR = ':';
33
+ const WILDCARD = '*';
34
+ /**
35
+ * Support specification proposal from https://about.gitlab.com/blog/2021/01/27/we-need-to-talk-no-proxy/#the-lowest-common-denominator
36
+ */
37
+ const matchNoProxy = ({ noProxy, hostname, }) => {
38
+ const rules = noProxy.split(RULES_SEPARATOR).filter(Boolean);
39
+ for (const rule of rules) {
40
+ // @todo: respect port when matching
41
+ const [ruleHost, _rulePort] = rule.split(HOST_PORT_SEPARATOR);
42
+ // Wildcard (*) match all hosts
43
+ if (ruleHost === WILDCARD) {
44
+ return true;
45
+ }
46
+ // Strip leading dots (.) and wildcard with dot (*.) for backward compatibility
47
+ const ruleHostWithoutLeadingDot = ruleHost.replace(/^\*?\./, '');
48
+ const matchRegex = new RegExp(`${ruleHostWithoutLeadingDot}$`);
49
+ /**
50
+ * @example
51
+ * 'localhost'.match(/localhost$/)
52
+ * 'api.test.com'.match(/test.com$/)
53
+ */
54
+ if (hostname.match(matchRegex)) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ };
60
+
61
+ /* eslint-disable no-param-reassign */
62
+ /**
63
+ * Fork of https://github.com/mknj/node-keepalive-proxy-agent with monkeypatching and no_proxy support
64
+ */
65
+ const addProxyToHttpsAgent = ({ logger }) => {
66
+ const httpsProxyEnv = getHttpsProxy();
67
+ const noProxyEnv = getNoProxy();
68
+ const noProxyMatchResults = {};
69
+ if (!httpsProxyEnv) {
70
+ return;
71
+ }
72
+ const parsedProxy = new url__default["default"].URL(httpsProxyEnv);
73
+ const proxy = { hostname: parsedProxy.hostname, port: parsedProxy.port };
74
+ logger.debug({
75
+ event: 'parsed proxy',
76
+ proxy,
77
+ });
78
+ // @ts-expect-error
79
+ const originalCreateConnection = https__default["default"].Agent.prototype.createConnection;
80
+ // @ts-expect-error
81
+ https__default["default"].Agent.prototype.createConnection = createConnection;
82
+ function createConnection(options, cb) {
83
+ const { hostname, href, method, headers } = options;
84
+ const connectionMustBeProxied = !noProxyEnv || !matchNoProxyWithCache({ hostname });
85
+ if (connectionMustBeProxied) {
86
+ logger.debug({
87
+ event: 'proxy connection',
88
+ connection: {
89
+ href,
90
+ method,
91
+ headers,
92
+ },
93
+ });
94
+ createConnectionHttpsAfterHttp.call(this, options, cb);
95
+ }
96
+ else {
97
+ cb(null, originalCreateConnection.call(this, options));
98
+ }
99
+ }
100
+ function createConnectionHttpsAfterHttp(options, cb) {
101
+ const proxySocket = net__default["default"].connect(+proxy.port, proxy.hostname);
102
+ const errorListener = (error) => {
103
+ proxySocket.destroy();
104
+ cb(error);
105
+ };
106
+ proxySocket.once('error', errorListener);
107
+ let response = '';
108
+ const dataListener = (data) => {
109
+ response += data.toString();
110
+ if (!response.endsWith('\r\n\r\n')) {
111
+ // response not completed yet
112
+ return;
113
+ }
114
+ proxySocket.removeListener('error', errorListener);
115
+ proxySocket.removeListener('data', dataListener);
116
+ const m = response.match(/^HTTP\/1.\d (\d*)/);
117
+ if (m == null || m[1] == null) {
118
+ proxySocket.destroy();
119
+ return cb(new Error(response.trim()));
120
+ }
121
+ if (m[1] !== '200') {
122
+ proxySocket.destroy();
123
+ return cb(new Error(m[0]));
124
+ }
125
+ // tell super function to use our proxy socket
126
+ options.socket = proxySocket;
127
+ cb(null, originalCreateConnection.call(this, options));
128
+ };
129
+ proxySocket.on('data', dataListener);
130
+ let host = options.hostname;
131
+ if (!host) {
132
+ host = options.host;
133
+ }
134
+ // https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.6
135
+ let cmd = `CONNECT ${host}:${options.port} HTTP/1.1\r\n`;
136
+ cmd += `Host: ${host}:${options.port}\r\n`;
137
+ cmd += '\r\n';
138
+ proxySocket.write(cmd);
139
+ }
140
+ function matchNoProxyWithCache({ hostname }) {
141
+ if (hostname in noProxyMatchResults) {
142
+ return noProxyMatchResults[hostname];
143
+ }
144
+ const noProxy = noProxyEnv;
145
+ const result = matchNoProxy({ noProxy, hostname });
146
+ noProxyMatchResults[hostname] = result;
147
+ return result;
148
+ }
149
+ };
150
+ /* eslint-enable no-param-reassign */
151
+
152
+ exports.HttpProxyAgentModule = class HttpProxyAgentModule {
153
+ };
154
+ exports.HttpProxyAgentModule = tslib.__decorate([
155
+ core.Module({
156
+ imports: [],
157
+ providers: [
158
+ httpProxyEnabled() && {
159
+ provide: core.commandLineListTokens.init,
160
+ multi: true,
161
+ useFactory: ({ loggerFactory }) => function addHttpsProxy() {
162
+ const logger = loggerFactory('http-proxy-agent');
163
+ logger.debug({
164
+ event: 'proxy agent enabled',
165
+ proxyEnv: getHttpsProxy(),
166
+ noProxyEnv: getNoProxy(),
167
+ });
168
+ addProxyToHttpsAgent({ logger });
169
+ },
170
+ deps: {
171
+ loggerFactory: tokensCommon.LOGGER_TOKEN,
172
+ },
173
+ },
174
+ ].filter(Boolean),
175
+ })
176
+ ], exports.HttpProxyAgentModule);
@@ -0,0 +1,3 @@
1
+ export declare const httpProxyEnabled: () => boolean;
2
+ export declare const getHttpsProxy: () => string;
3
+ export declare const getNoProxy: () => string;
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@tramvai/module-http-proxy-agent",
3
+ "version": "1.55.0",
4
+ "description": "Enable support for http_proxy, https_proxy and no_proxy env variables",
5
+ "browser": "lib/browser.js",
6
+ "main": "lib/server.js",
7
+ "module": "lib/server.es.js",
8
+ "typings": "lib/server.d.ts",
9
+ "files": [
10
+ "lib"
11
+ ],
12
+ "sideEffects": false,
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git@github.com:Tinkoff/tramvai.git"
16
+ },
17
+ "license": "Apache-2.0",
18
+ "scripts": {
19
+ "build": "tramvai-build --for-publish",
20
+ "watch": "tsc -w",
21
+ "build-for-publish": "true"
22
+ },
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org/"
25
+ },
26
+ "dependencies": {
27
+ "@tramvai/tokens-common": "1.55.0"
28
+ },
29
+ "devDependencies": {},
30
+ "peerDependencies": {
31
+ "@tramvai/core": "1.55.0",
32
+ "@tinkoff/dippy": "0.7.37",
33
+ "tslib": "^2.0.3"
34
+ }
35
+ }