1
0

http_proxy.ts 9.2 KB

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