http_proxy.ts 8.6 KB

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