index.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const url = require("url");
  4. const http = require("http");
  5. const https = require("https");
  6. const pm = require("./proxy");
  7. let tunnel;
  8. var HttpCodes;
  9. (function (HttpCodes) {
  10. HttpCodes[HttpCodes["OK"] = 200] = "OK";
  11. HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices";
  12. HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently";
  13. HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved";
  14. HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther";
  15. HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified";
  16. HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy";
  17. HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy";
  18. HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect";
  19. HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect";
  20. HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest";
  21. HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized";
  22. HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired";
  23. HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden";
  24. HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound";
  25. HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed";
  26. HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable";
  27. HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired";
  28. HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout";
  29. HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict";
  30. HttpCodes[HttpCodes["Gone"] = 410] = "Gone";
  31. HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests";
  32. HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError";
  33. HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented";
  34. HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway";
  35. HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable";
  36. HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout";
  37. })(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {}));
  38. var Headers;
  39. (function (Headers) {
  40. Headers["Accept"] = "accept";
  41. Headers["ContentType"] = "content-type";
  42. })(Headers = exports.Headers || (exports.Headers = {}));
  43. var MediaTypes;
  44. (function (MediaTypes) {
  45. MediaTypes["ApplicationJson"] = "application/json";
  46. })(MediaTypes = exports.MediaTypes || (exports.MediaTypes = {}));
  47. /**
  48. * Returns the proxy URL, depending upon the supplied url and proxy environment variables.
  49. * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
  50. */
  51. function getProxyUrl(serverUrl) {
  52. let proxyUrl = pm.getProxyUrl(url.parse(serverUrl));
  53. return proxyUrl ? proxyUrl.href : '';
  54. }
  55. exports.getProxyUrl = getProxyUrl;
  56. const HttpRedirectCodes = [HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.SeeOther, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect];
  57. const HttpResponseRetryCodes = [HttpCodes.BadGateway, HttpCodes.ServiceUnavailable, HttpCodes.GatewayTimeout];
  58. const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD'];
  59. const ExponentialBackoffCeiling = 10;
  60. const ExponentialBackoffTimeSlice = 5;
  61. class HttpClientResponse {
  62. constructor(message) {
  63. this.message = message;
  64. }
  65. readBody() {
  66. return new Promise(async (resolve, reject) => {
  67. let output = Buffer.alloc(0);
  68. this.message.on('data', (chunk) => {
  69. output = Buffer.concat([output, chunk]);
  70. });
  71. this.message.on('end', () => {
  72. resolve(output.toString());
  73. });
  74. });
  75. }
  76. }
  77. exports.HttpClientResponse = HttpClientResponse;
  78. function isHttps(requestUrl) {
  79. let parsedUrl = url.parse(requestUrl);
  80. return parsedUrl.protocol === 'https:';
  81. }
  82. exports.isHttps = isHttps;
  83. class HttpClient {
  84. constructor(userAgent, handlers, requestOptions) {
  85. this._ignoreSslError = false;
  86. this._allowRedirects = true;
  87. this._allowRedirectDowngrade = false;
  88. this._maxRedirects = 50;
  89. this._allowRetries = false;
  90. this._maxRetries = 1;
  91. this._keepAlive = false;
  92. this._disposed = false;
  93. this.userAgent = userAgent;
  94. this.handlers = handlers || [];
  95. this.requestOptions = requestOptions;
  96. if (requestOptions) {
  97. if (requestOptions.ignoreSslError != null) {
  98. this._ignoreSslError = requestOptions.ignoreSslError;
  99. }
  100. this._socketTimeout = requestOptions.socketTimeout;
  101. if (requestOptions.allowRedirects != null) {
  102. this._allowRedirects = requestOptions.allowRedirects;
  103. }
  104. if (requestOptions.allowRedirectDowngrade != null) {
  105. this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade;
  106. }
  107. if (requestOptions.maxRedirects != null) {
  108. this._maxRedirects = Math.max(requestOptions.maxRedirects, 0);
  109. }
  110. if (requestOptions.keepAlive != null) {
  111. this._keepAlive = requestOptions.keepAlive;
  112. }
  113. if (requestOptions.allowRetries != null) {
  114. this._allowRetries = requestOptions.allowRetries;
  115. }
  116. if (requestOptions.maxRetries != null) {
  117. this._maxRetries = requestOptions.maxRetries;
  118. }
  119. }
  120. }
  121. options(requestUrl, additionalHeaders) {
  122. return this.request('OPTIONS', requestUrl, null, additionalHeaders || {});
  123. }
  124. get(requestUrl, additionalHeaders) {
  125. return this.request('GET', requestUrl, null, additionalHeaders || {});
  126. }
  127. del(requestUrl, additionalHeaders) {
  128. return this.request('DELETE', requestUrl, null, additionalHeaders || {});
  129. }
  130. post(requestUrl, data, additionalHeaders) {
  131. return this.request('POST', requestUrl, data, additionalHeaders || {});
  132. }
  133. patch(requestUrl, data, additionalHeaders) {
  134. return this.request('PATCH', requestUrl, data, additionalHeaders || {});
  135. }
  136. put(requestUrl, data, additionalHeaders) {
  137. return this.request('PUT', requestUrl, data, additionalHeaders || {});
  138. }
  139. head(requestUrl, additionalHeaders) {
  140. return this.request('HEAD', requestUrl, null, additionalHeaders || {});
  141. }
  142. sendStream(verb, requestUrl, stream, additionalHeaders) {
  143. return this.request(verb, requestUrl, stream, additionalHeaders);
  144. }
  145. /**
  146. * Gets a typed object from an endpoint
  147. * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise
  148. */
  149. async getJson(requestUrl, additionalHeaders = {}) {
  150. additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
  151. let res = await this.get(requestUrl, additionalHeaders);
  152. return this._processResponse(res, this.requestOptions);
  153. }
  154. async postJson(requestUrl, obj, additionalHeaders = {}) {
  155. let data = JSON.stringify(obj, null, 2);
  156. additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
  157. additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
  158. let res = await this.post(requestUrl, data, additionalHeaders);
  159. return this._processResponse(res, this.requestOptions);
  160. }
  161. async putJson(requestUrl, obj, additionalHeaders = {}) {
  162. let data = JSON.stringify(obj, null, 2);
  163. additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
  164. additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
  165. let res = await this.put(requestUrl, data, additionalHeaders);
  166. return this._processResponse(res, this.requestOptions);
  167. }
  168. async patchJson(requestUrl, obj, additionalHeaders = {}) {
  169. let data = JSON.stringify(obj, null, 2);
  170. additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson);
  171. additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson);
  172. let res = await this.patch(requestUrl, data, additionalHeaders);
  173. return this._processResponse(res, this.requestOptions);
  174. }
  175. /**
  176. * Makes a raw http request.
  177. * All other methods such as get, post, patch, and request ultimately call this.
  178. * Prefer get, del, post and patch
  179. */
  180. async request(verb, requestUrl, data, headers) {
  181. if (this._disposed) {
  182. throw new Error("Client has already been disposed.");
  183. }
  184. let parsedUrl = url.parse(requestUrl);
  185. let info = this._prepareRequest(verb, parsedUrl, headers);
  186. // Only perform retries on reads since writes may not be idempotent.
  187. let maxTries = (this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1) ? this._maxRetries + 1 : 1;
  188. let numTries = 0;
  189. let response;
  190. while (numTries < maxTries) {
  191. response = await this.requestRaw(info, data);
  192. // Check if it's an authentication challenge
  193. if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) {
  194. let authenticationHandler;
  195. for (let i = 0; i < this.handlers.length; i++) {
  196. if (this.handlers[i].canHandleAuthentication(response)) {
  197. authenticationHandler = this.handlers[i];
  198. break;
  199. }
  200. }
  201. if (authenticationHandler) {
  202. return authenticationHandler.handleAuthentication(this, info, data);
  203. }
  204. else {
  205. // We have received an unauthorized response but have no handlers to handle it.
  206. // Let the response return to the caller.
  207. return response;
  208. }
  209. }
  210. let redirectsRemaining = this._maxRedirects;
  211. while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1
  212. && this._allowRedirects
  213. && redirectsRemaining > 0) {
  214. const redirectUrl = response.message.headers["location"];
  215. if (!redirectUrl) {
  216. // if there's no location to redirect to, we won't
  217. break;
  218. }
  219. let parsedRedirectUrl = url.parse(redirectUrl);
  220. if (parsedUrl.protocol == 'https:' && parsedUrl.protocol != parsedRedirectUrl.protocol && !this._allowRedirectDowngrade) {
  221. throw new Error("Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.");
  222. }
  223. // we need to finish reading the response before reassigning response
  224. // which will leak the open socket.
  225. await response.readBody();
  226. // let's make the request with the new redirectUrl
  227. info = this._prepareRequest(verb, parsedRedirectUrl, headers);
  228. response = await this.requestRaw(info, data);
  229. redirectsRemaining--;
  230. }
  231. if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) {
  232. // If not a retry code, return immediately instead of retrying
  233. return response;
  234. }
  235. numTries += 1;
  236. if (numTries < maxTries) {
  237. await response.readBody();
  238. await this._performExponentialBackoff(numTries);
  239. }
  240. }
  241. return response;
  242. }
  243. /**
  244. * Needs to be called if keepAlive is set to true in request options.
  245. */
  246. dispose() {
  247. if (this._agent) {
  248. this._agent.destroy();
  249. }
  250. this._disposed = true;
  251. }
  252. /**
  253. * Raw request.
  254. * @param info
  255. * @param data
  256. */
  257. requestRaw(info, data) {
  258. return new Promise((resolve, reject) => {
  259. let callbackForResult = function (err, res) {
  260. if (err) {
  261. reject(err);
  262. }
  263. resolve(res);
  264. };
  265. this.requestRawWithCallback(info, data, callbackForResult);
  266. });
  267. }
  268. /**
  269. * Raw request with callback.
  270. * @param info
  271. * @param data
  272. * @param onResult
  273. */
  274. requestRawWithCallback(info, data, onResult) {
  275. let socket;
  276. if (typeof (data) === 'string') {
  277. info.options.headers["Content-Length"] = Buffer.byteLength(data, 'utf8');
  278. }
  279. let callbackCalled = false;
  280. let handleResult = (err, res) => {
  281. if (!callbackCalled) {
  282. callbackCalled = true;
  283. onResult(err, res);
  284. }
  285. };
  286. let req = info.httpModule.request(info.options, (msg) => {
  287. let res = new HttpClientResponse(msg);
  288. handleResult(null, res);
  289. });
  290. req.on('socket', (sock) => {
  291. socket = sock;
  292. });
  293. // If we ever get disconnected, we want the socket to timeout eventually
  294. req.setTimeout(this._socketTimeout || 3 * 60000, () => {
  295. if (socket) {
  296. socket.end();
  297. }
  298. handleResult(new Error('Request timeout: ' + info.options.path), null);
  299. });
  300. req.on('error', function (err) {
  301. // err has statusCode property
  302. // res should have headers
  303. handleResult(err, null);
  304. });
  305. if (data && typeof (data) === 'string') {
  306. req.write(data, 'utf8');
  307. }
  308. if (data && typeof (data) !== 'string') {
  309. data.on('close', function () {
  310. req.end();
  311. });
  312. data.pipe(req);
  313. }
  314. else {
  315. req.end();
  316. }
  317. }
  318. /**
  319. * Gets an http agent. This function is useful when you need an http agent that handles
  320. * routing through a proxy server - depending upon the url and proxy environment variables.
  321. * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com
  322. */
  323. getAgent(serverUrl) {
  324. let parsedUrl = url.parse(serverUrl);
  325. return this._getAgent(parsedUrl);
  326. }
  327. _prepareRequest(method, requestUrl, headers) {
  328. const info = {};
  329. info.parsedUrl = requestUrl;
  330. const usingSsl = info.parsedUrl.protocol === 'https:';
  331. info.httpModule = usingSsl ? https : http;
  332. const defaultPort = usingSsl ? 443 : 80;
  333. info.options = {};
  334. info.options.host = info.parsedUrl.hostname;
  335. info.options.port = info.parsedUrl.port ? parseInt(info.parsedUrl.port) : defaultPort;
  336. info.options.path = (info.parsedUrl.pathname || '') + (info.parsedUrl.search || '');
  337. info.options.method = method;
  338. info.options.headers = this._mergeHeaders(headers);
  339. if (this.userAgent != null) {
  340. info.options.headers["user-agent"] = this.userAgent;
  341. }
  342. info.options.agent = this._getAgent(info.parsedUrl);
  343. // gives handlers an opportunity to participate
  344. if (this.handlers) {
  345. this.handlers.forEach((handler) => {
  346. handler.prepareRequest(info.options);
  347. });
  348. }
  349. return info;
  350. }
  351. _mergeHeaders(headers) {
  352. const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {});
  353. if (this.requestOptions && this.requestOptions.headers) {
  354. return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers));
  355. }
  356. return lowercaseKeys(headers || {});
  357. }
  358. _getExistingOrDefaultHeader(additionalHeaders, header, _default) {
  359. const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {});
  360. let clientHeader;
  361. if (this.requestOptions && this.requestOptions.headers) {
  362. clientHeader = lowercaseKeys(this.requestOptions.headers)[header];
  363. }
  364. return additionalHeaders[header] || clientHeader || _default;
  365. }
  366. _getAgent(parsedUrl) {
  367. let agent;
  368. let proxyUrl = pm.getProxyUrl(parsedUrl);
  369. let useProxy = proxyUrl && proxyUrl.hostname;
  370. if (this._keepAlive && useProxy) {
  371. agent = this._proxyAgent;
  372. }
  373. if (this._keepAlive && !useProxy) {
  374. agent = this._agent;
  375. }
  376. // if agent is already assigned use that agent.
  377. if (!!agent) {
  378. return agent;
  379. }
  380. const usingSsl = parsedUrl.protocol === 'https:';
  381. let maxSockets = 100;
  382. if (!!this.requestOptions) {
  383. maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets;
  384. }
  385. if (useProxy) {
  386. // If using proxy, need tunnel
  387. if (!tunnel) {
  388. tunnel = require('tunnel');
  389. }
  390. const agentOptions = {
  391. maxSockets: maxSockets,
  392. keepAlive: this._keepAlive,
  393. proxy: {
  394. proxyAuth: proxyUrl.auth,
  395. host: proxyUrl.hostname,
  396. port: proxyUrl.port
  397. },
  398. };
  399. let tunnelAgent;
  400. const overHttps = proxyUrl.protocol === 'https:';
  401. if (usingSsl) {
  402. tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp;
  403. }
  404. else {
  405. tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp;
  406. }
  407. agent = tunnelAgent(agentOptions);
  408. this._proxyAgent = agent;
  409. }
  410. // if reusing agent across request and tunneling agent isn't assigned create a new agent
  411. if (this._keepAlive && !agent) {
  412. const options = { keepAlive: this._keepAlive, maxSockets: maxSockets };
  413. agent = usingSsl ? new https.Agent(options) : new http.Agent(options);
  414. this._agent = agent;
  415. }
  416. // if not using private agent and tunnel agent isn't setup then use global agent
  417. if (!agent) {
  418. agent = usingSsl ? https.globalAgent : http.globalAgent;
  419. }
  420. if (usingSsl && this._ignoreSslError) {
  421. // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
  422. // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
  423. // we have to cast it to any and change it directly
  424. agent.options = Object.assign(agent.options || {}, { rejectUnauthorized: false });
  425. }
  426. return agent;
  427. }
  428. _performExponentialBackoff(retryNumber) {
  429. retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber);
  430. const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber);
  431. return new Promise(resolve => setTimeout(() => resolve(), ms));
  432. }
  433. static dateTimeDeserializer(key, value) {
  434. if (typeof value === 'string') {
  435. let a = new Date(value);
  436. if (!isNaN(a.valueOf())) {
  437. return a;
  438. }
  439. }
  440. return value;
  441. }
  442. async _processResponse(res, options) {
  443. return new Promise(async (resolve, reject) => {
  444. const statusCode = res.message.statusCode;
  445. const response = {
  446. statusCode: statusCode,
  447. result: null,
  448. headers: {}
  449. };
  450. // not found leads to null obj returned
  451. if (statusCode == HttpCodes.NotFound) {
  452. resolve(response);
  453. }
  454. let obj;
  455. let contents;
  456. // get the result from the body
  457. try {
  458. contents = await res.readBody();
  459. if (contents && contents.length > 0) {
  460. if (options && options.deserializeDates) {
  461. obj = JSON.parse(contents, HttpClient.dateTimeDeserializer);
  462. }
  463. else {
  464. obj = JSON.parse(contents);
  465. }
  466. response.result = obj;
  467. }
  468. response.headers = res.message.headers;
  469. }
  470. catch (err) {
  471. // Invalid resource (contents not json); leaving result obj null
  472. }
  473. // note that 3xx redirects are handled by the http layer.
  474. if (statusCode > 299) {
  475. let msg;
  476. // if exception/error in body, attempt to get better error
  477. if (obj && obj.message) {
  478. msg = obj.message;
  479. }
  480. else if (contents && contents.length > 0) {
  481. // it may be the case that the exception is in the body message as string
  482. msg = contents;
  483. }
  484. else {
  485. msg = "Failed request: (" + statusCode + ")";
  486. }
  487. let err = new Error(msg);
  488. // attach statusCode and body obj (if available) to the error object
  489. err['statusCode'] = statusCode;
  490. if (response.result) {
  491. err['result'] = response.result;
  492. }
  493. reject(err);
  494. }
  495. else {
  496. resolve(response);
  497. }
  498. });
  499. }
  500. }
  501. exports.HttpClient = HttpClient;