"use strict"; // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const url = require("url"); const http = require("http"); const https = require("https"); let fs; let tunnel; var HttpCodes; (function (HttpCodes) { HttpCodes[HttpCodes["OK"] = 200] = "OK"; HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; })(HttpCodes = exports.HttpCodes || (exports.HttpCodes = {})); const HttpRedirectCodes = [HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.SeeOther, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect]; const HttpResponseRetryCodes = [HttpCodes.BadGateway, HttpCodes.ServiceUnavailable, HttpCodes.GatewayTimeout]; const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; const ExponentialBackoffCeiling = 10; const ExponentialBackoffTimeSlice = 5; class HttpClientResponse { constructor(message) { this.message = message; } readBody() { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { let output = ''; this.message.on('data', (chunk) => { output += chunk; }); this.message.on('end', () => { resolve(output); }); })); } } exports.HttpClientResponse = HttpClientResponse; function isHttps(requestUrl) { let parsedUrl = url.parse(requestUrl); return parsedUrl.protocol === 'https:'; } exports.isHttps = isHttps; var EnvironmentVariables; (function (EnvironmentVariables) { EnvironmentVariables["HTTP_PROXY"] = "HTTP_PROXY"; EnvironmentVariables["HTTPS_PROXY"] = "HTTPS_PROXY"; })(EnvironmentVariables || (EnvironmentVariables = {})); class HttpClient { constructor(userAgent, handlers, requestOptions) { this._ignoreSslError = false; this._allowRedirects = true; this._maxRedirects = 50; this._allowRetries = false; this._maxRetries = 1; this._keepAlive = false; this._disposed = false; this.userAgent = userAgent; this.handlers = handlers || []; this.requestOptions = requestOptions; if (requestOptions) { if (requestOptions.ignoreSslError != null) { this._ignoreSslError = requestOptions.ignoreSslError; } this._socketTimeout = requestOptions.socketTimeout; this._httpProxy = requestOptions.proxy; if (requestOptions.proxy && requestOptions.proxy.proxyBypassHosts) { this._httpProxyBypassHosts = []; requestOptions.proxy.proxyBypassHosts.forEach(bypass => { this._httpProxyBypassHosts.push(new RegExp(bypass, 'i')); }); } this._certConfig = requestOptions.cert; if (this._certConfig) { // If using cert, need fs fs = require('fs'); // cache the cert content into memory, so we don't have to read it from disk every time if (this._certConfig.caFile && fs.existsSync(this._certConfig.caFile)) { this._ca = fs.readFileSync(this._certConfig.caFile, 'utf8'); } if (this._certConfig.certFile && fs.existsSync(this._certConfig.certFile)) { this._cert = fs.readFileSync(this._certConfig.certFile, 'utf8'); } if (this._certConfig.keyFile && fs.existsSync(this._certConfig.keyFile)) { this._key = fs.readFileSync(this._certConfig.keyFile, 'utf8'); } } if (requestOptions.allowRedirects != null) { this._allowRedirects = requestOptions.allowRedirects; } if (requestOptions.maxRedirects != null) { this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); } if (requestOptions.keepAlive != null) { this._keepAlive = requestOptions.keepAlive; } if (requestOptions.allowRetries != null) { this._allowRetries = requestOptions.allowRetries; } if (requestOptions.maxRetries != null) { this._maxRetries = requestOptions.maxRetries; } } } options(requestUrl, additionalHeaders) { return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); } get(requestUrl, additionalHeaders) { return this.request('GET', requestUrl, null, additionalHeaders || {}); } del(requestUrl, additionalHeaders) { return this.request('DELETE', requestUrl, null, additionalHeaders || {}); } post(requestUrl, data, additionalHeaders) { return this.request('POST', requestUrl, data, additionalHeaders || {}); } patch(requestUrl, data, additionalHeaders) { return this.request('PATCH', requestUrl, data, additionalHeaders || {}); } put(requestUrl, data, additionalHeaders) { return this.request('PUT', requestUrl, data, additionalHeaders || {}); } head(requestUrl, additionalHeaders) { return this.request('HEAD', requestUrl, null, additionalHeaders || {}); } sendStream(verb, requestUrl, stream, additionalHeaders) { return this.request(verb, requestUrl, stream, additionalHeaders); } /** * Makes a raw http request. * All other methods such as get, post, patch, and request ultimately call this. * Prefer get, del, post and patch */ request(verb, requestUrl, data, headers) { return __awaiter(this, void 0, void 0, function* () { if (this._disposed) { throw new Error("Client has already been disposed."); } let info = this._prepareRequest(verb, requestUrl, headers); // Only perform retries on reads since writes may not be idempotent. let maxTries = (this._allowRetries && RetryableHttpVerbs.indexOf(verb) != -1) ? this._maxRetries + 1 : 1; let numTries = 0; let response; while (numTries < maxTries) { response = yield this.requestRaw(info, data); // Check if it's an authentication challenge if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) { let authenticationHandler; for (let i = 0; i < this.handlers.length; i++) { if (this.handlers[i].canHandleAuthentication(response)) { authenticationHandler = this.handlers[i]; break; } } if (authenticationHandler) { return authenticationHandler.handleAuthentication(this, info, data); } else { // We have received an unauthorized response but have no handlers to handle it. // Let the response return to the caller. return response; } } let redirectsRemaining = this._maxRedirects; while (HttpRedirectCodes.indexOf(response.message.statusCode) != -1 && this._allowRedirects && redirectsRemaining > 0) { const redirectUrl = response.message.headers["location"]; if (!redirectUrl) { // if there's no location to redirect to, we won't break; } // we need to finish reading the response before reassigning response // which will leak the open socket. yield response.readBody(); // let's make the request with the new redirectUrl info = this._prepareRequest(verb, redirectUrl, headers); response = yield this.requestRaw(info, data); redirectsRemaining--; } if (HttpResponseRetryCodes.indexOf(response.message.statusCode) == -1) { // If not a retry code, return immediately instead of retrying return response; } numTries += 1; if (numTries < maxTries) { yield response.readBody(); yield this._performExponentialBackoff(numTries); } } return response; }); } /** * Needs to be called if keepAlive is set to true in request options. */ dispose() { if (this._agent) { this._agent.destroy(); } this._disposed = true; } /** * Raw request. * @param info * @param data */ requestRaw(info, data) { return new Promise((resolve, reject) => { let callbackForResult = function (err, res) { if (err) { reject(err); } resolve(res); }; this.requestRawWithCallback(info, data, callbackForResult); }); } /** * Raw request with callback. * @param info * @param data * @param onResult */ requestRawWithCallback(info, data, onResult) { let socket; let isDataString = typeof (data) === 'string'; if (typeof (data) === 'string') { info.options.headers["Content-Length"] = Buffer.byteLength(data, 'utf8'); } let callbackCalled = false; let handleResult = (err, res) => { if (!callbackCalled) { callbackCalled = true; onResult(err, res); } }; let req = info.httpModule.request(info.options, (msg) => { let res = new HttpClientResponse(msg); handleResult(null, res); }); req.on('socket', (sock) => { socket = sock; }); // If we ever get disconnected, we want the socket to timeout eventually req.setTimeout(this._socketTimeout || 3 * 60000, () => { if (socket) { socket.end(); } handleResult(new Error('Request timeout: ' + info.options.path), null); }); req.on('error', function (err) { // err has statusCode property // res should have headers handleResult(err, null); }); if (data && typeof (data) === 'string') { req.write(data, 'utf8'); } if (data && typeof (data) !== 'string') { data.on('close', function () { req.end(); }); data.pipe(req); } else { req.end(); } } _prepareRequest(method, requestUrl, headers) { const info = {}; info.parsedUrl = url.parse(requestUrl); const usingSsl = info.parsedUrl.protocol === 'https:'; info.httpModule = usingSsl ? https : http; const defaultPort = usingSsl ? 443 : 80; info.options = {}; info.options.host = info.parsedUrl.hostname; info.options.port = info.parsedUrl.port ? parseInt(info.parsedUrl.port) : defaultPort; info.options.path = (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); info.options.method = method; info.options.headers = this._mergeHeaders(headers); info.options.headers["user-agent"] = this.userAgent; info.options.agent = this._getAgent(requestUrl); // gives handlers an opportunity to participate if (this.handlers && !this._isPresigned(requestUrl)) { this.handlers.forEach((handler) => { handler.prepareRequest(info.options); }); } return info; } _isPresigned(requestUrl) { if (this.requestOptions && this.requestOptions.presignedUrlPatterns) { const patterns = this.requestOptions.presignedUrlPatterns; for (let i = 0; i < patterns.length; i++) { if (requestUrl.match(patterns[i])) { return true; } } } return false; } _mergeHeaders(headers) { const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => (c[k.toLowerCase()] = obj[k], c), {}); if (this.requestOptions && this.requestOptions.headers) { return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers)); } return lowercaseKeys(headers || {}); } _getAgent(requestUrl) { let agent; let proxy = this._getProxy(requestUrl); let useProxy = proxy.proxyUrl && proxy.proxyUrl.hostname && !this._isBypassProxy(requestUrl); if (this._keepAlive && useProxy) { agent = this._proxyAgent; } if (this._keepAlive && !useProxy) { agent = this._agent; } // if agent is already assigned use that agent. if (!!agent) { return agent; } let parsedUrl = url.parse(requestUrl); const usingSsl = parsedUrl.protocol === 'https:'; let maxSockets = 100; if (!!this.requestOptions) { maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; } if (useProxy) { // If using proxy, need tunnel if (!tunnel) { tunnel = require('tunnel'); } const agentOptions = { maxSockets: maxSockets, keepAlive: this._keepAlive, proxy: { proxyAuth: proxy.proxyAuth, host: proxy.proxyUrl.hostname, port: proxy.proxyUrl.port }, }; let tunnelAgent; const overHttps = proxy.proxyUrl.protocol === 'https:'; if (usingSsl) { tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; } else { tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; } agent = tunnelAgent(agentOptions); this._proxyAgent = agent; } // if reusing agent across request and tunneling agent isn't assigned create a new agent if (this._keepAlive && !agent) { const options = { keepAlive: this._keepAlive, maxSockets: maxSockets }; agent = usingSsl ? new https.Agent(options) : new http.Agent(options); this._agent = agent; } // if not using private agent and tunnel agent isn't setup then use global agent if (!agent) { agent = usingSsl ? https.globalAgent : http.globalAgent; } if (usingSsl && this._ignoreSslError) { // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options // we have to cast it to any and change it directly agent.options = Object.assign(agent.options || {}, { rejectUnauthorized: false }); } if (usingSsl && this._certConfig) { agent.options = Object.assign(agent.options || {}, { ca: this._ca, cert: this._cert, key: this._key, passphrase: this._certConfig.passphrase }); } return agent; } _getProxy(requestUrl) { const parsedUrl = url.parse(requestUrl); let usingSsl = parsedUrl.protocol === 'https:'; let proxyConfig = this._httpProxy; // fallback to http_proxy and https_proxy env let https_proxy = process.env[EnvironmentVariables.HTTPS_PROXY]; let http_proxy = process.env[EnvironmentVariables.HTTP_PROXY]; if (!proxyConfig) { if (https_proxy && usingSsl) { proxyConfig = { proxyUrl: https_proxy }; } else if (http_proxy) { proxyConfig = { proxyUrl: http_proxy }; } } let proxyUrl; let proxyAuth; if (proxyConfig) { if (proxyConfig.proxyUrl.length > 0) { proxyUrl = url.parse(proxyConfig.proxyUrl); } if (proxyConfig.proxyUsername || proxyConfig.proxyPassword) { proxyAuth = proxyConfig.proxyUsername + ":" + proxyConfig.proxyPassword; } } return { proxyUrl: proxyUrl, proxyAuth: proxyAuth }; } _isBypassProxy(requestUrl) { if (!this._httpProxyBypassHosts) { return false; } let bypass = false; this._httpProxyBypassHosts.forEach(bypassHost => { if (bypassHost.test(requestUrl)) { bypass = true; } }); return bypass; } _performExponentialBackoff(retryNumber) { retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); return new Promise(resolve => setTimeout(() => resolve(), ms)); } } exports.HttpClient = HttpClient;