| "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; |