| "use strict"; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| const http = require("http"); | |
| const https = require("https"); | |
| const pm = require("./proxy"); | |
| 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["TooManyRequests"] = 429] = "TooManyRequests"; | |
| 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 = {})); | |
| var Headers; | |
| (function (Headers) { | |
| Headers["Accept"] = "accept"; | |
| Headers["ContentType"] = "content-type"; | |
| })(Headers = exports.Headers || (exports.Headers = {})); | |
| var MediaTypes; | |
| (function (MediaTypes) { | |
| MediaTypes["ApplicationJson"] = "application/json"; | |
| })(MediaTypes = exports.MediaTypes || (exports.MediaTypes = {})); | |
| /** | |
| * Returns the proxy URL, depending upon the supplied url and proxy environment variables. | |
| * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com | |
| */ | |
| function getProxyUrl(serverUrl) { | |
| let proxyUrl = pm.getProxyUrl(new URL(serverUrl)); | |
| return proxyUrl ? proxyUrl.href : ''; | |
| } | |
| exports.getProxyUrl = getProxyUrl; | |
| 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 HttpClientError extends Error { | |
| constructor(message, statusCode) { | |
| super(message); | |
| this.name = 'HttpClientError'; | |
| this.statusCode = statusCode; | |
| Object.setPrototypeOf(this, HttpClientError.prototype); | |
| } | |
| } | |
| exports.HttpClientError = HttpClientError; | |
| class HttpClientResponse { | |
| constructor(message) { | |
| this.message = message; | |
| } | |
| readBody() { | |
| return new Promise(async (resolve, reject) => { | |
| let output = Buffer.alloc(0); | |
| this.message.on('data', (chunk) => { | |
| output = Buffer.concat([output, chunk]); | |
| }); | |
| this.message.on('end', () => { | |
| resolve(output.toString()); | |
| }); | |
| }); | |
| } | |
| } | |
| exports.HttpClientResponse = HttpClientResponse; | |
| function isHttps(requestUrl) { | |
| let parsedUrl = new URL(requestUrl); | |
| return parsedUrl.protocol === 'https:'; | |
| } | |
| exports.isHttps = isHttps; | |
| class HttpClient { | |
| constructor(userAgent, handlers, requestOptions) { | |
| this._ignoreSslError = false; | |
| this._allowRedirects = true; | |
| this._allowRedirectDowngrade = false; | |
| 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; | |
| if (requestOptions.allowRedirects != null) { | |
| this._allowRedirects = requestOptions.allowRedirects; | |
| } | |
| if (requestOptions.allowRedirectDowngrade != null) { | |
| this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; | |
| } | |
| 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); | |
| } | |
| /** | |
| * Gets a typed object from an endpoint | |
| * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise | |
| */ | |
| async getJson(requestUrl, additionalHeaders = {}) { | |
| additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); | |
| let res = await this.get(requestUrl, additionalHeaders); | |
| return this._processResponse(res, this.requestOptions); | |
| } | |
| async postJson(requestUrl, obj, additionalHeaders = {}) { | |
| let data = JSON.stringify(obj, null, 2); | |
| additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); | |
| additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); | |
| let res = await this.post(requestUrl, data, additionalHeaders); | |
| return this._processResponse(res, this.requestOptions); | |
| } | |
| async putJson(requestUrl, obj, additionalHeaders = {}) { | |
| let data = JSON.stringify(obj, null, 2); | |
| additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); | |
| additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); | |
| let res = await this.put(requestUrl, data, additionalHeaders); | |
| return this._processResponse(res, this.requestOptions); | |
| } | |
| async patchJson(requestUrl, obj, additionalHeaders = {}) { | |
| let data = JSON.stringify(obj, null, 2); | |
| additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); | |
| additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); | |
| let res = await this.patch(requestUrl, data, additionalHeaders); | |
| return this._processResponse(res, this.requestOptions); | |
| } | |
| /** | |
| * Makes a raw http request. | |
| * All other methods such as get, post, patch, and request ultimately call this. | |
| * Prefer get, del, post and patch | |
| */ | |
| async request(verb, requestUrl, data, headers) { | |
| if (this._disposed) { | |
| throw new Error('Client has already been disposed.'); | |
| } | |
| let parsedUrl = new URL(requestUrl); | |
| let info = this._prepareRequest(verb, parsedUrl, 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 = await 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; | |
| } | |
| let parsedRedirectUrl = new URL(redirectUrl); | |
| if (parsedUrl.protocol == 'https:' && | |
| parsedUrl.protocol != parsedRedirectUrl.protocol && | |
| !this._allowRedirectDowngrade) { | |
| 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.'); | |
| } | |
| // we need to finish reading the response before reassigning response | |
| // which will leak the open socket. | |
| await response.readBody(); | |
| // strip authorization header if redirected to a different hostname | |
| if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { | |
| for (let header in headers) { | |
| // header names are case insensitive | |
| if (header.toLowerCase() === 'authorization') { | |
| delete headers[header]; | |
| } | |
| } | |
| } | |
| // let's make the request with the new redirectUrl | |
| info = this._prepareRequest(verb, parsedRedirectUrl, headers); | |
| response = await 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) { | |
| await response.readBody(); | |
| await 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; | |
| 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(); | |
| } | |
| } | |
| /** | |
| * Gets an http agent. This function is useful when you need an http agent that handles | |
| * routing through a proxy server - depending upon the url and proxy environment variables. | |
| * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com | |
| */ | |
| getAgent(serverUrl) { | |
| let parsedUrl = new URL(serverUrl); | |
| return this._getAgent(parsedUrl); | |
| } | |
| _prepareRequest(method, requestUrl, headers) { | |
| const info = {}; | |
| info.parsedUrl = 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); | |
| if (this.userAgent != null) { | |
| info.options.headers['user-agent'] = this.userAgent; | |
| } | |
| info.options.agent = this._getAgent(info.parsedUrl); | |
| // gives handlers an opportunity to participate | |
| if (this.handlers) { | |
| this.handlers.forEach(handler => { | |
| handler.prepareRequest(info.options); | |
| }); | |
| } | |
| return info; | |
| } | |
| _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 || {}); | |
| } | |
| _getExistingOrDefaultHeader(additionalHeaders, header, _default) { | |
| const lowercaseKeys = obj => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); | |
| let clientHeader; | |
| if (this.requestOptions && this.requestOptions.headers) { | |
| clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; | |
| } | |
| return additionalHeaders[header] || clientHeader || _default; | |
| } | |
| _getAgent(parsedUrl) { | |
| let agent; | |
| let proxyUrl = pm.getProxyUrl(parsedUrl); | |
| let useProxy = proxyUrl && proxyUrl.hostname; | |
| 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; | |
| } | |
| 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: { | |
| ...((proxyUrl.username || proxyUrl.password) && { | |
| proxyAuth: `${proxyUrl.username}:${proxyUrl.password}` | |
| }), | |
| host: proxyUrl.hostname, | |
| port: proxyUrl.port | |
| } | |
| }; | |
| let tunnelAgent; | |
| const overHttps = 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 | |
| }); | |
| } | |
| return agent; | |
| } | |
| _performExponentialBackoff(retryNumber) { | |
| retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); | |
| const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); | |
| return new Promise(resolve => setTimeout(() => resolve(), ms)); | |
| } | |
| static dateTimeDeserializer(key, value) { | |
| if (typeof value === 'string') { | |
| let a = new Date(value); | |
| if (!isNaN(a.valueOf())) { | |
| return a; | |
| } | |
| } | |
| return value; | |
| } | |
| async _processResponse(res, options) { | |
| return new Promise(async (resolve, reject) => { | |
| const statusCode = res.message.statusCode; | |
| const response = { | |
| statusCode: statusCode, | |
| result: null, | |
| headers: {} | |
| }; | |
| // not found leads to null obj returned | |
| if (statusCode == HttpCodes.NotFound) { | |
| resolve(response); | |
| } | |
| let obj; | |
| let contents; | |
| // get the result from the body | |
| try { | |
| contents = await res.readBody(); | |
| if (contents && contents.length > 0) { | |
| if (options && options.deserializeDates) { | |
| obj = JSON.parse(contents, HttpClient.dateTimeDeserializer); | |
| } | |
| else { | |
| obj = JSON.parse(contents); | |
| } | |
| response.result = obj; | |
| } | |
| response.headers = res.message.headers; | |
| } | |
| catch (err) { | |
| // Invalid resource (contents not json); leaving result obj null | |
| } | |
| // note that 3xx redirects are handled by the http layer. | |
| if (statusCode > 299) { | |
| let msg; | |
| // if exception/error in body, attempt to get better error | |
| if (obj && obj.message) { | |
| msg = obj.message; | |
| } | |
| else if (contents && contents.length > 0) { | |
| // it may be the case that the exception is in the body message as string | |
| msg = contents; | |
| } | |
| else { | |
| msg = 'Failed request: (' + statusCode + ')'; | |
| } | |
| let err = new HttpClientError(msg, statusCode); | |
| err.result = response.result; | |
| reject(err); | |
| } | |
| else { | |
| resolve(response); | |
| } | |
| }); | |
| } | |
| } | |
| exports.HttpClient = HttpClient; |