"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"); | |
const util = require("./Util"); | |
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["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 = {})); | |
const HttpRedirectCodes = [HttpCodes.MovedPermanently, HttpCodes.ResourceMoved, HttpCodes.SeeOther, HttpCodes.TemporaryRedirect, HttpCodes.PermanentRedirect]; | |
const HttpResponseRetryCodes = [HttpCodes.BadGateway, HttpCodes.ServiceUnavailable, HttpCodes.GatewayTimeout]; | |
const NetworkRetryErrors = ['ECONNRESET', 'ENOTFOUND', 'ESOCKETTIMEDOUT', 'ETIMEDOUT', 'ECONNREFUSED']; | |
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 buffer = Buffer.alloc(0); | |
const encodingCharset = util.obtainContentCharset(this); | |
// Extract Encoding from header: 'content-encoding' | |
// Match `gzip`, `gzip, deflate` variations of GZIP encoding | |
const contentEncoding = this.message.headers['content-encoding'] || ''; | |
const isGzippedEncoded = new RegExp('(gzip$)|(gzip, *deflate)').test(contentEncoding); | |
this.message.on('data', function (data) { | |
const chunk = (typeof data === 'string') ? Buffer.from(data, encodingCharset) : data; | |
buffer = Buffer.concat([buffer, chunk]); | |
}).on('end', function () { | |
return __awaiter(this, void 0, void 0, function* () { | |
if (isGzippedEncoded) { // Process GZipped Response Body HERE | |
const gunzippedBody = yield util.decompressGzippedContent(buffer, encodingCharset); | |
resolve(gunzippedBody); | |
} | |
else { | |
resolve(buffer.toString(encodingCharset)); | |
} | |
}); | |
}).on('error', function (err) { | |
reject(err); | |
}); | |
})); | |
} | |
} | |
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["NO_PROXY"] = "NO_PROXY"; | |
})(EnvironmentVariables || (EnvironmentVariables = {})); | |
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 || []; | |
let no_proxy = process.env[EnvironmentVariables.NO_PROXY]; | |
if (no_proxy) { | |
this._httpProxyBypassHosts = []; | |
no_proxy.split(',').forEach(bypass => { | |
this._httpProxyBypassHosts.push(new RegExp(bypass, 'i')); | |
}); | |
} | |
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.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); | |
} | |
/** | |
* 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 parsedUrl = url.parse(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) { | |
try { | |
response = yield this.requestRaw(info, data); | |
} | |
catch (err) { | |
if (err && err.code && NetworkRetryErrors.indexOf(err.code) > -1) { | |
continue; | |
} | |
throw err; | |
} | |
// 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 = url.parse(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. | |
yield response.readBody(); | |
// let's make the request with the new redirectUrl | |
info = this._prepareRequest(verb, parsedRedirectUrl, 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; | |
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.destroy(); | |
} | |
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 = 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._isPresigned(url.format(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(parsedUrl) { | |
let agent; | |
let proxy = this._getProxy(parsedUrl); | |
let useProxy = proxy.proxyUrl && proxy.proxyUrl.hostname && !this._isMatchInBypassProxyList(parsedUrl); | |
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: { | |
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(parsedUrl) { | |
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 }; | |
} | |
_isMatchInBypassProxyList(parsedUrl) { | |
if (!this._httpProxyBypassHosts) { | |
return false; | |
} | |
let bypass = false; | |
this._httpProxyBypassHosts.forEach(bypassHost => { | |
if (bypassHost.test(parsedUrl.href)) { | |
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; |