UNPKG

8.83 kBPlain TextView Raw
1/*
2 * Copyright 2019 gRPC authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18import { log } from './logging';
19import { LogVerbosity } from './constants';
20import { getDefaultAuthority } from './resolver';
21import { Socket } from 'net';
22import * as http from 'http';
23import * as tls from 'tls';
24import * as logging from './logging';
25import {
26 SubchannelAddress,
27 isTcpSubchannelAddress,
28 subchannelAddressToString,
29} from './subchannel-address';
30import { ChannelOptions } from './channel-options';
31import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser';
32import { URL } from 'url';
33
34const TRACER_NAME = 'proxy';
35
36function trace(text: string): void {
37 logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
38}
39
40interface ProxyInfo {
41 address?: string;
42 creds?: string;
43}
44
45function getProxyInfo(): ProxyInfo {
46 let proxyEnv = '';
47 let envVar = '';
48 /* Prefer using 'grpc_proxy'. Fallback on 'http_proxy' if it is not set.
49 * Also prefer using 'https_proxy' with fallback on 'http_proxy'. The
50 * fallback behavior can be removed if there's a demand for it.
51 */
52 if (process.env.grpc_proxy) {
53 envVar = 'grpc_proxy';
54 proxyEnv = process.env.grpc_proxy;
55 } else if (process.env.https_proxy) {
56 envVar = 'https_proxy';
57 proxyEnv = process.env.https_proxy;
58 } else if (process.env.http_proxy) {
59 envVar = 'http_proxy';
60 proxyEnv = process.env.http_proxy;
61 } else {
62 return {};
63 }
64 let proxyUrl: URL;
65 try {
66 proxyUrl = new URL(proxyEnv);
67 } catch (e) {
68 log(LogVerbosity.ERROR, `cannot parse value of "${envVar}" env var`);
69 return {};
70 }
71 if (proxyUrl.protocol !== 'http:') {
72 log(
73 LogVerbosity.ERROR,
74 `"${proxyUrl.protocol}" scheme not supported in proxy URI`
75 );
76 return {};
77 }
78 let userCred: string | null = null;
79 if (proxyUrl.username) {
80 if (proxyUrl.password) {
81 log(LogVerbosity.INFO, 'userinfo found in proxy URI');
82 userCred = `${proxyUrl.username}:${proxyUrl.password}`;
83 } else {
84 userCred = proxyUrl.username;
85 }
86 }
87 const hostname = proxyUrl.hostname;
88 let port = proxyUrl.port;
89 /* The proxy URL uses the scheme "http:", which has a default port number of
90 * 80. We need to set that explicitly here if it is omitted because otherwise
91 * it will use gRPC's default port 443. */
92 if (port === '') {
93 port = '80';
94 }
95 const result: ProxyInfo = {
96 address: `${hostname}:${port}`,
97 };
98 if (userCred) {
99 result.creds = userCred;
100 }
101 trace(
102 'Proxy server ' + result.address + ' set by environment variable ' + envVar
103 );
104 return result;
105}
106
107function getNoProxyHostList(): string[] {
108 /* Prefer using 'no_grpc_proxy'. Fallback on 'no_proxy' if it is not set. */
109 let noProxyStr: string | undefined = process.env.no_grpc_proxy;
110 let envVar = 'no_grpc_proxy';
111 if (!noProxyStr) {
112 noProxyStr = process.env.no_proxy;
113 envVar = 'no_proxy';
114 }
115 if (noProxyStr) {
116 trace('No proxy server list set by environment variable ' + envVar);
117 return noProxyStr.split(',');
118 } else {
119 return [];
120 }
121}
122
123export interface ProxyMapResult {
124 target: GrpcUri;
125 extraOptions: ChannelOptions;
126}
127
128export function mapProxyName(
129 target: GrpcUri,
130 options: ChannelOptions
131): ProxyMapResult {
132 const noProxyResult: ProxyMapResult = {
133 target: target,
134 extraOptions: {},
135 };
136 if ((options['grpc.enable_http_proxy'] ?? 1) === 0) {
137 return noProxyResult;
138 }
139 if (target.scheme === 'unix') {
140 return noProxyResult;
141 }
142 const proxyInfo = getProxyInfo();
143 if (!proxyInfo.address) {
144 return noProxyResult;
145 }
146 const hostPort = splitHostPort(target.path);
147 if (!hostPort) {
148 return noProxyResult;
149 }
150 const serverHost = hostPort.host;
151 for (const host of getNoProxyHostList()) {
152 if (host === serverHost) {
153 trace(
154 'Not using proxy for target in no_proxy list: ' + uriToString(target)
155 );
156 return noProxyResult;
157 }
158 }
159 const extraOptions: ChannelOptions = {
160 'grpc.http_connect_target': uriToString(target),
161 };
162 if (proxyInfo.creds) {
163 extraOptions['grpc.http_connect_creds'] = proxyInfo.creds;
164 }
165 return {
166 target: {
167 scheme: 'dns',
168 path: proxyInfo.address,
169 },
170 extraOptions: extraOptions,
171 };
172}
173
174export interface ProxyConnectionResult {
175 socket?: Socket;
176 realTarget?: GrpcUri;
177}
178
179export function getProxiedConnection(
180 address: SubchannelAddress,
181 channelOptions: ChannelOptions,
182 connectionOptions: tls.ConnectionOptions
183): Promise<ProxyConnectionResult> {
184 if (!('grpc.http_connect_target' in channelOptions)) {
185 return Promise.resolve<ProxyConnectionResult>({});
186 }
187 const realTarget = channelOptions['grpc.http_connect_target'] as string;
188 const parsedTarget = parseUri(realTarget);
189 if (parsedTarget === null) {
190 return Promise.resolve<ProxyConnectionResult>({});
191 }
192 const options: http.RequestOptions = {
193 method: 'CONNECT',
194 path: parsedTarget.path,
195 };
196 const headers: http.OutgoingHttpHeaders = {
197 Host: parsedTarget.path,
198 };
199 // Connect to the subchannel address as a proxy
200 if (isTcpSubchannelAddress(address)) {
201 options.host = address.host;
202 options.port = address.port;
203 } else {
204 options.socketPath = address.path;
205 }
206 if ('grpc.http_connect_creds' in channelOptions) {
207 headers['Proxy-Authorization'] =
208 'Basic ' +
209 Buffer.from(channelOptions['grpc.http_connect_creds'] as string).toString(
210 'base64'
211 );
212 }
213 options.headers = headers;
214 const proxyAddressString = subchannelAddressToString(address);
215 trace('Using proxy ' + proxyAddressString + ' to connect to ' + options.path);
216 return new Promise<ProxyConnectionResult>((resolve, reject) => {
217 const request = http.request(options);
218 request.once('connect', (res, socket, head) => {
219 request.removeAllListeners();
220 socket.removeAllListeners();
221 if (res.statusCode === 200) {
222 trace(
223 'Successfully connected to ' +
224 options.path +
225 ' through proxy ' +
226 proxyAddressString
227 );
228 if ('secureContext' in connectionOptions) {
229 /* The proxy is connecting to a TLS server, so upgrade this socket
230 * connection to a TLS connection.
231 * This is a workaround for https://github.com/nodejs/node/issues/32922
232 * See https://github.com/grpc/grpc-node/pull/1369 for more info. */
233 const targetPath = getDefaultAuthority(parsedTarget);
234 const hostPort = splitHostPort(targetPath);
235 const remoteHost = hostPort?.host ?? targetPath;
236
237 const cts = tls.connect(
238 {
239 host: remoteHost,
240 servername: remoteHost,
241 socket: socket,
242 ...connectionOptions,
243 },
244 () => {
245 trace(
246 'Successfully established a TLS connection to ' +
247 options.path +
248 ' through proxy ' +
249 proxyAddressString
250 );
251 resolve({ socket: cts, realTarget: parsedTarget });
252 }
253 );
254 cts.on('error', (error: Error) => {
255 trace(
256 'Failed to establish a TLS connection to ' +
257 options.path +
258 ' through proxy ' +
259 proxyAddressString +
260 ' with error ' +
261 error.message
262 );
263 reject();
264 });
265 } else {
266 trace(
267 'Successfully established a plaintext connection to ' +
268 options.path +
269 ' through proxy ' +
270 proxyAddressString
271 );
272 resolve({
273 socket,
274 realTarget: parsedTarget,
275 });
276 }
277 } else {
278 log(
279 LogVerbosity.ERROR,
280 'Failed to connect to ' +
281 options.path +
282 ' through proxy ' +
283 proxyAddressString +
284 ' with status ' +
285 res.statusCode
286 );
287 reject();
288 }
289 });
290 request.once('error', err => {
291 request.removeAllListeners();
292 log(
293 LogVerbosity.ERROR,
294 'Failed to connect to proxy ' +
295 proxyAddressString +
296 ' with error ' +
297 err.message
298 );
299 reject();
300 });
301 request.end();
302 });
303}
304
\No newline at end of file