Skip to content

Commit 4d1bdc4

Browse files
authored
Merge pull request #1356 from murgatroid99/grpc-js_proxy_support_take_2
grpc-js: Interact with proxies properly
2 parents 5566f1d + e73c962 commit 4d1bdc4

File tree

5 files changed

+119
-77
lines changed

5 files changed

+119
-77
lines changed

Diff for: packages/grpc-js/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@grpc/grpc-js",
3-
"version": "0.8.0",
3+
"version": "0.8.1",
44
"description": "gRPC Library for Node - pure JS implementation",
55
"homepage": "https://grpc.io/",
66
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",

Diff for: packages/grpc-js/src/channel.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ServiceConfig, validateServiceConfig } from './service-config';
3838
import { trace, log } from './logging';
3939
import { SubchannelAddress } from './subchannel';
4040
import { MaxMessageSizeFilterFactory } from './max-message-size-filter';
41+
import { mapProxyName } from './http_proxy';
4142

4243
export enum ConnectivityState {
4344
CONNECTING,
@@ -163,6 +164,14 @@ export class ChannelImplementation implements Channel {
163164
);
164165
}
165166
}
167+
if (this.options['grpc.default_authority']) {
168+
this.defaultAuthority = this.options['grpc.default_authority'] as string;
169+
} else {
170+
this.defaultAuthority = getDefaultAuthority(target);
171+
}
172+
const proxyMapResult = mapProxyName(target, options);
173+
this.target = proxyMapResult.target;
174+
this.options = Object.assign({}, this.options, proxyMapResult.extraOptions);
166175
/* The global boolean parameter to getSubchannelPool has the inverse meaning to what
167176
* the grpc.use_local_subchannel_pool channel option means. */
168177
this.subchannelPool = getSubchannelPool(
@@ -207,7 +216,7 @@ export class ChannelImplementation implements Channel {
207216
);
208217
}
209218
this.resolvingLoadBalancer = new ResolvingLoadBalancer(
210-
target,
219+
this.target,
211220
channelControlHelper,
212221
defaultServiceConfig
213222
);
@@ -217,12 +226,6 @@ export class ChannelImplementation implements Channel {
217226
new MaxMessageSizeFilterFactory(this.options),
218227
new CompressionFilterFactory(this),
219228
]);
220-
// TODO(murgatroid99): Add more centralized handling of channel options
221-
if (this.options['grpc.default_authority']) {
222-
this.defaultAuthority = this.options['grpc.default_authority'] as string;
223-
} else {
224-
this.defaultAuthority = getDefaultAuthority(target);
225-
}
226229
}
227230

228231
/**

Diff for: packages/grpc-js/src/http_proxy.ts

+78-46
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ import { parseTarget } from './resolver-dns';
2222
import { Socket } from 'net';
2323
import * as http from 'http';
2424
import * as logging from './logging';
25-
import { SubchannelAddress, isTcpSubchannelAddress } from './subchannel';
25+
import {
26+
SubchannelAddress,
27+
isTcpSubchannelAddress,
28+
subchannelAddressToString,
29+
} from './subchannel';
30+
import { ChannelOptions } from './channel-options';
2631

2732
const TRACER_NAME = 'proxy';
2833

@@ -89,8 +94,6 @@ function getProxyInfo(): ProxyInfo {
8994
return result;
9095
}
9196

92-
const PROXY_INFO = getProxyInfo();
93-
9497
function getNoProxyHostList(): string[] {
9598
/* Prefer using 'no_grpc_proxy'. Fallback on 'no_proxy' if it is not set. */
9699
let noProxyStr: string | undefined = process.env.no_grpc_proxy;
@@ -107,81 +110,109 @@ function getNoProxyHostList(): string[] {
107110
}
108111
}
109112

110-
const NO_PROXY_HOSTS = getNoProxyHostList();
113+
export interface ProxyMapResult {
114+
target: string;
115+
extraOptions: ChannelOptions;
116+
}
111117

112-
export function shouldUseProxy(target: string): boolean {
113-
if (!PROXY_INFO.address) {
114-
return false;
118+
export function mapProxyName(
119+
target: string,
120+
options: ChannelOptions
121+
): ProxyMapResult {
122+
const noProxyResult: ProxyMapResult = {
123+
target: target,
124+
extraOptions: {},
125+
};
126+
const proxyInfo = getProxyInfo();
127+
if (!proxyInfo.address) {
128+
return noProxyResult;
115129
}
116-
let serverHost: string;
117130
const parsedTarget = parseTarget(target);
118-
if (parsedTarget) {
119-
serverHost = parsedTarget.host;
120-
} else {
121-
return false;
131+
if (!parsedTarget) {
132+
return noProxyResult;
122133
}
123-
for (const host of NO_PROXY_HOSTS) {
134+
const serverHost = parsedTarget.host;
135+
for (const host of getNoProxyHostList()) {
124136
if (host === serverHost) {
125137
trace('Not using proxy for target in no_proxy list: ' + target);
126-
return false;
138+
return noProxyResult;
127139
}
128140
}
129-
return true;
141+
const extraOptions: ChannelOptions = {
142+
'grpc.http_connect_target': target,
143+
};
144+
if (proxyInfo.creds) {
145+
extraOptions['grpc.http_connect_creds'] = proxyInfo.creds;
146+
}
147+
return {
148+
target: `dns:${proxyInfo.address}`,
149+
extraOptions: extraOptions,
150+
};
151+
}
152+
153+
export interface ProxyConnectionResult {
154+
socket?: Socket;
155+
realTarget?: string;
130156
}
131157

132158
export function getProxiedConnection(
133-
target: string,
134-
subchannelAddress: SubchannelAddress
135-
): Promise<Socket> {
136-
if (
137-
!(
138-
PROXY_INFO.address &&
139-
shouldUseProxy(target) &&
140-
isTcpSubchannelAddress(subchannelAddress)
141-
)
142-
) {
143-
return Promise.reject<Socket>();
159+
address: SubchannelAddress,
160+
channelOptions: ChannelOptions
161+
): Promise<ProxyConnectionResult> {
162+
if (!('grpc.http_connect_target' in channelOptions)) {
163+
return Promise.resolve<ProxyConnectionResult>({});
144164
}
145-
const subchannelAddressPathString = `${subchannelAddress.host}:${subchannelAddress.port}`;
146-
trace(
147-
'Using proxy ' +
148-
PROXY_INFO.address +
149-
' to connect to ' +
150-
target +
151-
' at ' +
152-
subchannelAddress
153-
);
165+
const realTarget = channelOptions['grpc.http_connect_target'] as string;
166+
const parsedTarget = parseTarget(realTarget)!;
154167
const options: http.RequestOptions = {
155168
method: 'CONNECT',
156-
host: PROXY_INFO.address,
157-
path: subchannelAddressPathString,
158169
};
159-
if (PROXY_INFO.creds) {
170+
// Connect to the subchannel address as a proxy
171+
if (isTcpSubchannelAddress(address)) {
172+
options.host = address.host;
173+
options.port = address.port;
174+
} else {
175+
options.socketPath = address.path;
176+
}
177+
if (parsedTarget.port === undefined) {
178+
options.path = parsedTarget.host;
179+
} else {
180+
options.path = `${parsedTarget.host}:${parsedTarget.port}`;
181+
}
182+
if ('grpc.http_connect_creds' in channelOptions) {
160183
options.headers = {
161184
'Proxy-Authorization':
162-
'Basic ' + Buffer.from(PROXY_INFO.creds).toString('base64'),
185+
'Basic ' +
186+
Buffer.from(
187+
channelOptions['grpc.http_connect_creds'] as string
188+
).toString('base64'),
163189
};
164190
}
165-
return new Promise<Socket>((resolve, reject) => {
191+
const proxyAddressString = subchannelAddressToString(address);
192+
trace('Using proxy ' + proxyAddressString + ' to connect to ' + options.path);
193+
return new Promise<ProxyConnectionResult>((resolve, reject) => {
166194
const request = http.request(options);
167195
request.once('connect', (res, socket, head) => {
168196
request.removeAllListeners();
169197
socket.removeAllListeners();
170198
if (res.statusCode === 200) {
171199
trace(
172200
'Successfully connected to ' +
173-
subchannelAddress +
201+
options.path +
174202
' through proxy ' +
175-
PROXY_INFO.address
203+
proxyAddressString
176204
);
177-
resolve(socket);
205+
resolve({
206+
socket,
207+
realTarget,
208+
});
178209
} else {
179210
log(
180211
LogVerbosity.ERROR,
181212
'Failed to connect to ' +
182-
subchannelAddress +
213+
options.path +
183214
' through proxy ' +
184-
PROXY_INFO.address +
215+
proxyAddressString +
185216
' with status ' +
186217
res.statusCode
187218
);
@@ -193,11 +224,12 @@ export function getProxiedConnection(
193224
log(
194225
LogVerbosity.ERROR,
195226
'Failed to connect to proxy ' +
196-
PROXY_INFO.address +
227+
proxyAddressString +
197228
' with error ' +
198229
err.message
199230
);
200231
reject();
201232
});
233+
request.end();
202234
});
203235
}

Diff for: packages/grpc-js/src/resolver-dns.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,15 @@ class DnsResolver implements Resolver {
171171
});
172172
return;
173173
}
174-
if (this.dnsHostname !== null) {
174+
if (this.dnsHostname === null) {
175+
setImmediate(() => {
176+
this.listener.onError({
177+
code: Status.UNAVAILABLE,
178+
details: `Failed to parse DNS address ${this.target}`,
179+
metadata: new Metadata(),
180+
});
181+
});
182+
} else {
175183
/* We clear out latestLookupResult here to ensure that it contains the
176184
* latest result since the last time we started resolving. That way, the
177185
* TXT resolution handler can use it, but only if it finishes second. We

Diff for: packages/grpc-js/src/subchannel.ts

+21-22
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { BackoffTimeout, BackoffOptions } from './backoff-timeout';
2626
import { getDefaultAuthority } from './resolver';
2727
import * as logging from './logging';
2828
import { LogVerbosity } from './constants';
29-
import { shouldUseProxy, getProxiedConnection } from './http_proxy';
29+
import { getProxiedConnection, ProxyConnectionResult } from './http_proxy';
3030
import * as net from 'net';
3131

3232
const clientVersion = require('../../package.json').version;
@@ -278,7 +278,7 @@ export class Subchannel {
278278
clearTimeout(this.keepaliveTimeoutId);
279279
}
280280

281-
private createSession(socket?: net.Socket) {
281+
private createSession(proxyConnectionResult: ProxyConnectionResult) {
282282
let connectionOptions: http2.SecureClientSessionOptions =
283283
this.credentials._getConnectionOptions() || {};
284284
let addressScheme = 'http://';
@@ -299,16 +299,16 @@ export class Subchannel {
299299
};
300300
connectionOptions.servername = sslTargetNameOverride;
301301
}
302-
if (socket) {
303-
connectionOptions.socket = socket;
302+
if (proxyConnectionResult.socket) {
303+
connectionOptions.socket = proxyConnectionResult.socket;
304304
}
305305
} else {
306306
/* In all but the most recent versions of Node, http2.connect does not use
307307
* the options when establishing plaintext connections, so we need to
308308
* establish that connection explicitly. */
309309
connectionOptions.createConnection = (authority, option) => {
310-
if (socket) {
311-
return socket;
310+
if (proxyConnectionResult.socket) {
311+
return proxyConnectionResult.socket;
312312
} else {
313313
/* net.NetConnectOpts is declared in a way that is more restrictive
314314
* than what net.connect will actually accept, so we use the type
@@ -339,7 +339,10 @@ export class Subchannel {
339339
* determines whether the connection will be established over TLS or not.
340340
*/
341341
const session = http2.connect(
342-
addressScheme + getDefaultAuthority(this.channelTarget),
342+
addressScheme +
343+
getDefaultAuthority(
344+
proxyConnectionResult.realTarget ?? this.channelTarget
345+
),
343346
connectionOptions
344347
);
345348
this.session = session;
@@ -409,21 +412,17 @@ export class Subchannel {
409412
}
410413

411414
private startConnectingInternal() {
412-
if (shouldUseProxy(this.channelTarget)) {
413-
getProxiedConnection(this.channelTarget, this.subchannelAddress).then(
414-
(socket) => {
415-
this.createSession(socket);
416-
},
417-
(reason) => {
418-
this.transitionToState(
419-
[ConnectivityState.CONNECTING],
420-
ConnectivityState.TRANSIENT_FAILURE
421-
);
422-
}
423-
);
424-
} else {
425-
this.createSession();
426-
}
415+
getProxiedConnection(this.subchannelAddress, this.options).then(
416+
(result) => {
417+
this.createSession(result);
418+
},
419+
(reason) => {
420+
this.transitionToState(
421+
[ConnectivityState.CONNECTING],
422+
ConnectivityState.TRANSIENT_FAILURE
423+
);
424+
}
425+
);
427426
}
428427

429428
/**

0 commit comments

Comments
 (0)