123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- /*
- * Copyright 2019 gRPC authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
- import { log } from './logging';
- import { LogVerbosity } from './constants';
- import { getDefaultAuthority } from './resolver';
- import { Socket } from 'net';
- import * as http from 'http';
- import * as tls from 'tls';
- import * as logging from './logging';
- import {
- SubchannelAddress,
- isTcpSubchannelAddress,
- subchannelAddressToString,
- } from './subchannel-address';
- import { ChannelOptions } from './channel-options';
- import { GrpcUri, parseUri, splitHostPort, uriToString } from './uri-parser';
- import { URL } from 'url';
- const TRACER_NAME = 'proxy';
- function trace(text: string): void {
- logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
- }
- interface ProxyInfo {
- address?: string;
- creds?: string;
- }
- function getProxyInfo(): ProxyInfo {
- let proxyEnv = '';
- let envVar = '';
- /* Prefer using 'grpc_proxy'. Fallback on 'http_proxy' if it is not set.
- * Also prefer using 'https_proxy' with fallback on 'http_proxy'. The
- * fallback behavior can be removed if there's a demand for it.
- */
- if (process.env.grpc_proxy) {
- envVar = 'grpc_proxy';
- proxyEnv = process.env.grpc_proxy;
- } else if (process.env.https_proxy) {
- envVar = 'https_proxy';
- proxyEnv = process.env.https_proxy;
- } else if (process.env.http_proxy) {
- envVar = 'http_proxy';
- proxyEnv = process.env.http_proxy;
- } else {
- return {};
- }
- let proxyUrl: URL;
- try {
- proxyUrl = new URL(proxyEnv);
- } catch (e) {
- log(LogVerbosity.ERROR, `cannot parse value of "${envVar}" env var`);
- return {};
- }
- if (proxyUrl.protocol !== 'http:') {
- log(
- LogVerbosity.ERROR,
- `"${proxyUrl.protocol}" scheme not supported in proxy URI`
- );
- return {};
- }
- let userCred: string | null = null;
- if (proxyUrl.username) {
- if (proxyUrl.password) {
- log(LogVerbosity.INFO, 'userinfo found in proxy URI');
- userCred = `${proxyUrl.username}:${proxyUrl.password}`;
- } else {
- userCred = proxyUrl.username;
- }
- }
- const hostname = proxyUrl.hostname;
- let port = proxyUrl.port;
- /* The proxy URL uses the scheme "http:", which has a default port number of
- * 80. We need to set that explicitly here if it is omitted because otherwise
- * it will use gRPC's default port 443. */
- if (port === '') {
- port = '80';
- }
- const result: ProxyInfo = {
- address: `${hostname}:${port}`,
- };
- if (userCred) {
- result.creds = userCred;
- }
- trace(
- 'Proxy server ' + result.address + ' set by environment variable ' + envVar
- );
- return result;
- }
- function getNoProxyHostList(): string[] {
- /* Prefer using 'no_grpc_proxy'. Fallback on 'no_proxy' if it is not set. */
- let noProxyStr: string | undefined = process.env.no_grpc_proxy;
- let envVar = 'no_grpc_proxy';
- if (!noProxyStr) {
- noProxyStr = process.env.no_proxy;
- envVar = 'no_proxy';
- }
- if (noProxyStr) {
- trace('No proxy server list set by environment variable ' + envVar);
- return noProxyStr.split(',');
- } else {
- return [];
- }
- }
- export interface ProxyMapResult {
- target: GrpcUri;
- extraOptions: ChannelOptions;
- }
- export function mapProxyName(
- target: GrpcUri,
- options: ChannelOptions
- ): ProxyMapResult {
- const noProxyResult: ProxyMapResult = {
- target: target,
- extraOptions: {},
- };
- if ((options['grpc.enable_http_proxy'] ?? 1) === 0) {
- return noProxyResult;
- }
- if (target.scheme === 'unix') {
- return noProxyResult;
- }
- const proxyInfo = getProxyInfo();
- if (!proxyInfo.address) {
- return noProxyResult;
- }
- const hostPort = splitHostPort(target.path);
- if (!hostPort) {
- return noProxyResult;
- }
- const serverHost = hostPort.host;
- for (const host of getNoProxyHostList()) {
- if (host === serverHost) {
- trace(
- 'Not using proxy for target in no_proxy list: ' + uriToString(target)
- );
- return noProxyResult;
- }
- }
- const extraOptions: ChannelOptions = {
- 'grpc.http_connect_target': uriToString(target),
- };
- if (proxyInfo.creds) {
- extraOptions['grpc.http_connect_creds'] = proxyInfo.creds;
- }
- return {
- target: {
- scheme: 'dns',
- path: proxyInfo.address,
- },
- extraOptions: extraOptions,
- };
- }
- export interface ProxyConnectionResult {
- socket?: Socket;
- realTarget?: GrpcUri;
- }
- export function getProxiedConnection(
- address: SubchannelAddress,
- channelOptions: ChannelOptions,
- connectionOptions: tls.ConnectionOptions
- ): Promise<ProxyConnectionResult> {
- if (!('grpc.http_connect_target' in channelOptions)) {
- return Promise.resolve<ProxyConnectionResult>({});
- }
- const realTarget = channelOptions['grpc.http_connect_target'] as string;
- const parsedTarget = parseUri(realTarget);
- if (parsedTarget === null) {
- return Promise.resolve<ProxyConnectionResult>({});
- }
- const options: http.RequestOptions = {
- method: 'CONNECT',
- path: parsedTarget.path,
- };
- const headers: http.OutgoingHttpHeaders = {
- Host: parsedTarget.path,
- };
- // Connect to the subchannel address as a proxy
- if (isTcpSubchannelAddress(address)) {
- options.host = address.host;
- options.port = address.port;
- } else {
- options.socketPath = address.path;
- }
- if ('grpc.http_connect_creds' in channelOptions) {
- headers['Proxy-Authorization'] =
- 'Basic ' +
- Buffer.from(
- channelOptions['grpc.http_connect_creds'] as string
- ).toString('base64');
- }
- options.headers = headers
- const proxyAddressString = subchannelAddressToString(address);
- trace('Using proxy ' + proxyAddressString + ' to connect to ' + options.path);
- return new Promise<ProxyConnectionResult>((resolve, reject) => {
- const request = http.request(options);
- request.once('connect', (res, socket, head) => {
- request.removeAllListeners();
- socket.removeAllListeners();
- if (res.statusCode === 200) {
- trace(
- 'Successfully connected to ' +
- options.path +
- ' through proxy ' +
- proxyAddressString
- );
- if ('secureContext' in connectionOptions) {
- /* The proxy is connecting to a TLS server, so upgrade this socket
- * connection to a TLS connection.
- * This is a workaround for https://github.com/nodejs/node/issues/32922
- * See https://github.com/grpc/grpc-node/pull/1369 for more info. */
- const targetPath = getDefaultAuthority(parsedTarget);
- const hostPort = splitHostPort(targetPath);
- const remoteHost = hostPort?.host ?? targetPath;
- const cts = tls.connect(
- {
- host: remoteHost,
- servername: remoteHost,
- socket: socket,
- ...connectionOptions,
- },
- () => {
- trace(
- 'Successfully established a TLS connection to ' +
- options.path +
- ' through proxy ' +
- proxyAddressString
- );
- resolve({ socket: cts, realTarget: parsedTarget });
- }
- );
- cts.on('error', (error: Error) => {
- trace('Failed to establish a TLS connection to ' +
- options.path +
- ' through proxy ' +
- proxyAddressString +
- ' with error ' +
- error.message);
- reject();
- });
- } else {
- trace(
- 'Successfully established a plaintext connection to ' +
- options.path +
- ' through proxy ' +
- proxyAddressString
- );
- resolve({
- socket,
- realTarget: parsedTarget,
- });
- }
- } else {
- log(
- LogVerbosity.ERROR,
- 'Failed to connect to ' +
- options.path +
- ' through proxy ' +
- proxyAddressString +
- ' with status ' +
- res.statusCode
- );
- reject();
- }
- });
- request.once('error', (err) => {
- request.removeAllListeners();
- log(
- LogVerbosity.ERROR,
- 'Failed to connect to proxy ' +
- proxyAddressString +
- ' with error ' +
- err.message
- );
- reject();
- });
- request.end();
- });
- }
|