@zowe/imperative
Version:
framework for building configurable CLIs
925 lines • 44.5 kB
JavaScript
"use strict";
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AbstractRestClient = void 0;
const util_1 = require("util");
const logger_1 = require("../../../logger");
const error_1 = require("../../../error");
const AbstractSession_1 = require("../session/AbstractSession");
const AuthOrder_1 = require("../session/AuthOrder");
const https = require("https");
const http = require("http");
const fs_1 = require("fs");
const Headers_1 = require("./Headers");
const RestConstants_1 = require("./RestConstants");
const ImperativeExpect_1 = require("../../../expect/src/ImperativeExpect");
const path = require("path");
const IRestClientError_1 = require("./doc/IRestClientError");
const RestClientError_1 = require("./RestClientError");
const io_1 = require("../../../io");
const operations_1 = require("../../../operations");
const utilities_1 = require("../../../utilities");
const SessConstants = require("../session/SessConstants");
const CompressionUtils_1 = require("./CompressionUtils");
const ProxySettings_1 = require("./ProxySettings");
const EnvironmentalVariableSettings_1 = require("../../../imperative/src/env/EnvironmentalVariableSettings");
/**
* Class to handle http(s) requests, build headers, collect data, report status codes, and header responses
* and passes control to session object for maintaining connection information (tokens, checking for timeout, etc...)
* @export
* @abstract
* @class AbstractRestClient
*/
class AbstractRestClient {
/**
* Creates an instance of AbstractRestClient.
* @param {AbstractSession} mSession - representing connection to this api
* @param topDefaultAuth
* The type of authentication used as the top selection in a default
* authentication order (when the user has not specified authOrder).
* The default value of AUTH_TYPE_TOKEN maintains backward compatibility
* with previous releases.
* @memberof AbstractRestClient
*/
constructor(mSession, topDefaultAuth = SessConstants.AUTH_TYPE_TOKEN) {
this.mSession = mSession;
/**
* Contains REST chucks
* @private
* @type {Buffer[]}
* @memberof AbstractRestClient
*/
this.mChunks = [];
/**
* Contains buffered data after all REST chucks are received
* @private
* @type {Buffer}
* @memberof AbstractRestClient
*/
this.mData = Buffer.from([]);
/**
* Bytes received from the server response so far
* @private
* @type {ITaskWithStatus}
* @memberof AbstractRestClient
*/
this.mBytesReceived = 0;
/**
* Whether or not to try and decode any encoded response
* @private
* @type {boolean}
* @memberof AbstractRestClient
*/
this.mDecode = true;
/**
* Last byte received when response is being streamed
* @private
* @type {number}
* @memberof AbstractRestClient
*/
this.lastByteReceived = 0;
/**
* Last byte received when upload is being streamed
* @private
* @type {number}
* @memberof AbstractRestClient
*/
this.lastByteReceivedUpload = 0;
ImperativeExpect_1.ImperativeExpect.toNotBeNullOrUndefined(mSession);
this.mLogger = logger_1.Logger.getImperativeLogger();
this.mIsJson = false;
// When a user specifies an authentication order, it will always be used.
// When a user does NOT specify an authentication order, the following call
// to cacheDefaultAuthOrder will set a default order in a way that avoids a
// breaking change to historical behavior.
//
// Note that the RestClient class (and by extension, the ZosmfRestClient class)
// overrides the default order to place basic auth at the top.
// Our Zowe client APIs use those two classes, and expect basic authentication
// to be placed at the top.
//
// Consumers who extend their own class directly from AbstractRestClient
// will have (by default) a token at the top of the authentication order.
//
// The current best practice for consumers of any of these APIs is to let the
// default order maintain backward compatibility. If your app needs a credential
// that conflicts with credentials required by other profiles/services, you should
// instruct your end users to specify an 'authOrder' property in the profile
// related to your app within their zowe.config.json file.
AuthOrder_1.AuthOrder.cacheDefaultAuthOrder(mSession.ISession, topDefaultAuth);
// Ensure that no other creds are in the session.
AuthOrder_1.AuthOrder.putTopAuthInSession(mSession.ISession);
}
/**
* Perform the actual http REST call with appropriate user input
* @param {IRestOptions} options
* @returns {Promise<string>}
* @throws if the request gets a status code outside of the 200 range
* or other connection problems occur (e.g. connection refused)
* @memberof AbstractRestClient
*/
request(options) {
return new Promise((resolve, reject) => {
// save for logging
this.mResource = options.resource;
this.mRequest = options.request;
this.mReqHeaders = options.reqHeaders;
this.mWriteData = options.writeData;
this.mRequestStream = options.requestStream;
this.mResponseStream = options.responseStream;
this.mNormalizeRequestNewlines = options.normalizeRequestNewLines;
this.mNormalizeResponseNewlines = options.normalizeResponseNewLines;
this.mTask = options.task;
// got a new promise
this.mResolve = resolve;
this.mReject = reject;
ImperativeExpect_1.ImperativeExpect.toBeDefinedAndNonBlank(options.resource, "resource");
ImperativeExpect_1.ImperativeExpect.toBeDefinedAndNonBlank(options.request, "request");
ImperativeExpect_1.ImperativeExpect.toBeEqual(options.requestStream != null && options.writeData != null, false, "You cannot specify both writeData and writeStream");
// putTopAuthInSession was originally done in the RestClient constructor.
// As a safety net to ensure that no logic has placed a different cred in the
// session since then, we again place only the top cred in the session.
AuthOrder_1.AuthOrder.putTopAuthInSession(this.session.ISession);
const buildOptions = this.buildOptions(options.resource, options.request, options.reqHeaders);
/**
* Perform the actual http request
*/
let clientRequest;
if (this.session.ISession.protocol === SessConstants.HTTPS_PROTOCOL) {
clientRequest = https.request(buildOptions, this.requestHandler.bind(this));
// try {
// clientRequest = https.request(buildOptions, this.requestHandler.bind(this));
// } catch (err) {
// if (err.message === "mac verify failure") {
// throw new ImperativeError({
// msg: "Failed to decrypt PFX file - verify your certificate passphrase is correct.",
// causeErrors: err,
// additionalDetails: err.message,
// stack: err.stack
// });
// } else { throw err; }
// }
}
else if (this.session.ISession.protocol === SessConstants.HTTP_PROTOCOL) {
clientRequest = http.request(buildOptions, this.requestHandler.bind(this));
}
/**
* For a REST request which includes writing raw data to the http server,
* write the data via http request.
*/
if (options.writeData != null) {
this.log.debug("will write data for request");
/**
* If the data is JSON, translate to text before writing
*/
if (this.mIsJson) {
this.log.debug("writing JSON for request");
this.log.trace("JSON body: %s", JSON.stringify(options.writeData));
clientRequest.write(JSON.stringify(options.writeData));
}
else {
clientRequest.write(options.writeData);
}
}
// Set up the request timeout
if (this.mSession.ISession.requestCompletionTimeout && this.mSession.ISession.requestCompletionTimeout > 0) {
clientRequest.setTimeout(this.mSession.ISession.requestCompletionTimeout);
}
clientRequest.on("timeout", () => {
var _a, _b;
if (clientRequest.socket.connecting) {
// We timed out. Destroy the request.
clientRequest.destroy(new Error("Connection timed out. Check the host, port, and firewall rules."));
}
else if (this.mSession.ISession.requestCompletionTimeout && this.mSession.ISession.requestCompletionTimeout > 0) {
(_b = (_a = this.mSession.ISession).requestCompletionTimeoutCallback) === null || _b === void 0 ? void 0 : _b.call(_a);
clientRequest.destroy(new error_1.ImperativeError({ msg: IRestClientError_1.completionTimeoutErrorMessage }));
}
});
/**
* Invoke any onError method whenever an error occurs on writing
*/
clientRequest.on("error", (errorResponse) => {
// Handle the HTTP 1.1 Keep-Alive race condition
if (errorResponse.code === "ECONNRESET" && clientRequest.reusedSocket) {
this.request(options).then((response) => {
resolve(response);
}).catch((err) => {
reject(err);
});
}
else if (errorResponse instanceof error_1.ImperativeError && errorResponse.message === IRestClientError_1.completionTimeoutErrorMessage) {
reject(this.populateError({
msg: "HTTP request timed out after connecting.",
causeErrors: errorResponse,
source: "timeout"
}));
}
else {
reject(this.populateError({
msg: "Failed to send an HTTP request.",
causeErrors: errorResponse,
source: "client"
}));
}
});
if (options.requestStream != null) {
// if the user requested streaming write of data to the request,
// write the data chunk by chunk to the server
let bytesUploaded = 0;
let heldByte;
options.requestStream.on("data", (data) => {
this.log.debug("Writing data chunk of length %d from requestStream to clientRequest", data.byteLength);
if (this.mNormalizeRequestNewlines) {
this.log.debug("Normalizing new lines in request chunk to \\n");
data = io_1.IO.processNewlines(data, this.lastByteReceivedUpload);
}
if (this.mTask != null) {
bytesUploaded += data.byteLength;
this.mTask.statusMessage = utilities_1.TextUtils.formatMessage("Uploading %d B", bytesUploaded);
if (this.mTask.percentComplete < operations_1.TaskProgress.NINETY_PERCENT) {
// we don't know how far along we are but increment the percentage to
// show we are making progress
this.mTask.percentComplete++;
}
}
clientRequest.write(data);
this.lastByteReceivedUpload = data[data.byteLength - 1];
});
options.requestStream.on("error", (streamError) => {
this.log.error("Error encountered reading requestStream: " + streamError);
reject(this.populateError({
msg: "Error reading requestStream",
causeErrors: streamError,
source: "client"
}));
});
options.requestStream.on("end", () => {
if (heldByte != null) {
clientRequest.write(Buffer.from(heldByte));
heldByte = undefined;
}
this.log.debug("Finished reading requestStream");
this.lastByteReceivedUpload = 0;
// finish the request
clientRequest.end();
});
}
else {
// otherwise we're done with the request
clientRequest.end();
}
// A request-for-token is completed after a REST request completes.
// Remove the request-for-token so it is not used on future requests
// with this same session.
AuthOrder_1.AuthOrder.removeRequestForToken(this.session.ISession);
});
}
/**
* Append specific headers for all requests by overriding this implementation
* @protected
* @param {(any[] | undefined)} headers - list of headers
* @returns {any[]} - completed list of headers
* @memberof AbstractRestClient
*/
appendHeaders(headers) {
if (headers == null) {
return [];
}
else {
return headers;
}
}
/**
* Process and customize errors encountered in your client.
* This is called any time an error is thrown from a failed Rest request using this client.
* error before receiving any response body from the API.
* You can use this, for example, to set the error tag for you client or add additional
* details to the error message.
* If you return null or undefined, Imperative will use the default error generated
* for your failed request.
* @protected
* @param {IImperativeError} error - the error encountered by the client
* @memberof AbstractRestClient
* @returns {IImperativeError} processedError - the error with the fields set the way you want them
*/
processError(_error) {
this.log.debug("Default stub for processError was called for rest client %s - processError was not overwritten", this.constructor.name);
return undefined; // do nothing by default
}
/**
* Build http(s) options based upon session settings and request.
* @private
* @param {string} resource - URI for this request
* @param {string} request - REST request type GET|PUT|POST|DELETE
* @param {any[]} reqHeaders - option headers to include with request
* @returns {IHTTPSOptions} - completed options object
* @throws {ImperativeError} - if the hostname is invalid or credentials are not passed to a session that requires auth
* @memberof AbstractRestClient
*/
buildOptions(resource, request, reqHeaders) {
var _a, _b, _c, _d;
var _e, _f;
this.validateRestHostname(this.session.ISession.hostname);
if (utilities_1.ImperativeConfig.instance.envVariablePrefix) {
const envValues = EnvironmentalVariableSettings_1.EnvironmentalVariableSettings.read(utilities_1.ImperativeConfig.instance.envVariablePrefix);
const socketConnectTimeout = (_a = envValues.socketConnectTimeout) === null || _a === void 0 ? void 0 : _a.value;
const requestCompletionTimeout = (_b = envValues.requestCompletionTimeout) === null || _b === void 0 ? void 0 : _b.value;
(_c = (_e = this.session.ISession).socketConnectTimeout) !== null && _c !== void 0 ? _c : (_e.socketConnectTimeout = isNaN(Number(socketConnectTimeout)) ? undefined : Number(socketConnectTimeout));
if (this.session.ISession.socketConnectTimeout != null) {
logger_1.Logger.getImperativeLogger().info("Setting socket connection timeout ms: " + String(this.mSession.ISession.socketConnectTimeout));
}
(_d = (_f = this.session.ISession).requestCompletionTimeout) !== null && _d !== void 0 ? _d : (_f.requestCompletionTimeout = isNaN(Number(requestCompletionTimeout)) ? undefined : Number(requestCompletionTimeout));
if (this.session.ISession.requestCompletionTimeout != null) {
logger_1.Logger.getImperativeLogger().info("Setting request completion timeout ms: " + String(this.mSession.ISession.requestCompletionTimeout));
}
}
/**
* HTTPS REST request options
*/
let options = {
headers: {},
hostname: this.session.ISession.hostname,
method: request,
/* Posix.join forces forward-slash delimiter on Windows.
* Path join is ok for just the resource part of the URL.
* We also eliminate any whitespace typos at the beginning
* or end of basePath or resource.
*/
path: path.posix.join(path.posix.sep, this.session.ISession.basePath.trim(), resource.trim()),
port: this.session.ISession.port,
rejectUnauthorized: this.session.ISession.rejectUnauthorized,
// Timeout after failing to connect for 60 seconds, or sooner if specified
timeout: this.session.ISession.socketConnectTimeout
};
// NOTE(Kelosky): This cannot be set for http requests
// options.agent = new https.Agent({secureProtocol: this.session.ISession.secureProtocol});
const proxyUrl = ProxySettings_1.ProxySettings.getSystemProxyUrl(this.session.ISession);
if (proxyUrl) {
if (ProxySettings_1.ProxySettings.matchesNoProxySettings(this.session.ISession)) {
this.mLogger.info(`Proxy setting "${proxyUrl.href}" will not be used as hostname was found listed under "no_proxy" setting.`);
}
else {
this.mLogger.info(`Using the following proxy setting for the request: ${proxyUrl.href}`);
options.agent = ProxySettings_1.ProxySettings.getProxyAgent(this.session.ISession);
}
}
// NOTE(Kelosky): we can bring certificate implementation back whenever we port tests and
// convert for imperative usage
/**
* Allow our session's defined identity validator run
*/
if (this.session.ISession.checkServerIdentity) {
this.log.trace("Check Server Identity Disabled (Allowing Mismatched Domains)");
options.checkServerIdentity = this.session.ISession.checkServerIdentity;
}
/**
* Place the credentials for the desired authentication type (based on our
* order of precedence) into the session options.
*/
let credsAreSet = false;
for (const nextAuthType of AuthOrder_1.AuthOrder.getAuthOrder(this.session.ISession)) {
if (nextAuthType === SessConstants.AUTH_TYPE_TOKEN) {
credsAreSet || (credsAreSet = this.setTokenAuth(options));
}
else if (nextAuthType === SessConstants.AUTH_TYPE_BASIC) {
credsAreSet || (credsAreSet = this.setPasswordAuth(options));
}
else if (nextAuthType === SessConstants.AUTH_TYPE_BEARER) {
credsAreSet || (credsAreSet = this.setBearerAuth(options));
}
else if (nextAuthType === SessConstants.AUTH_TYPE_CERT_PEM) {
credsAreSet || (credsAreSet = this.setCertPemAuth(options));
}
if (credsAreSet) {
break;
}
/* The following commented code was left as a place-holder for adding support
* for PFX certificates. The commented code was added when the order of credentials
* was specified using hard-coded logic. We now use the AuthOrder class to specify
* the order. When adding support for PFX certs, move this logic into a new function
* (with a name like setCertPfxAuth). Some conditional logic may have to be reversed
* in that function. See other such functions for an example. Add a new else-if
* clause above to call the new setCertPfxAuth function.
*/
// else if (this.session.ISession.type === SessConstants.AUTH_TYPE_CERT_PFX) {
// this.log.trace("Using PFX Certificate authentication");
// try {
// options.pfx = readFileSync(this.session.ISession.cert);
// } catch (err) {
// throw new ImperativeError({
// msg: "Certificate authentication failed when trying to read files.",
// causeErrors: err,
// additionalDetails: err.message,
// });
// }
// options.passphrase = this.session.ISession.passphrase;
// }
}
/* There is probably a better way report this kind of problem and a better message,
* but we do it this way to maintain backward compatibility.
*/
if (!credsAreSet && this.session.ISession.type !== SessConstants.AUTH_TYPE_NONE) {
throw new error_1.ImperativeError({ msg: "No credentials for a BASIC or TOKEN type of session." });
}
// for all headers passed into this request, append them to our options object
reqHeaders = this.appendHeaders(reqHeaders);
options = this.appendInputHeaders(options, reqHeaders);
// set transfer flags
this.setTransferFlags(options.headers);
const logResource = path.posix.join(path.posix.sep, this.session.ISession.basePath == null ? "" : this.session.ISession.basePath, resource);
this.log.trace("Rest request: %s %s:%s%s %s", request, this.session.ISession.hostname, this.session.ISession.port, logResource, this.session.ISession.user ? "as user " + this.session.ISession.user : "");
return options;
}
/**
* Set token auth into our REST request authentication options
* if a token value is specified in the session supplied to this class.
*
* @private
* @param {any} restOptionsToSet
* The set of REST request options into which the credentials will be set.
* @returns True if this function sets authentication options. False otherwise.
* @memberof AbstractRestClient
*/
setTokenAuth(restOptionsToSet) {
if (!(this.session.ISession.type === SessConstants.AUTH_TYPE_TOKEN)) {
return false;
}
if (!this.session.ISession.tokenValue) {
return false;
}
this.log.trace("Using cookie authentication with token %s", this.session.ISession.tokenValue);
const headerKeys = Object.keys(Headers_1.Headers.COOKIE_AUTHORIZATION);
const authentication = `${this.session.ISession.tokenType}=${this.session.ISession.tokenValue}`;
headerKeys.forEach((property) => {
restOptionsToSet.headers[property] = authentication;
});
return true;
}
/**
* Set user and password auth (A.K.A basic authentication) into our
* REST request authentication options if user and password values
* are specified in the session supplied to this class.
*
* @private
* @param {any} restOptionsToSet
* The set of REST request options into which the credentials will be set.
* @returns True if this function sets authentication options. False otherwise.
* @memberof AbstractRestClient
*/
setPasswordAuth(restOptionsToSet) {
var _a;
/* When logging into APIML, our desired auth type is token. However to
* get that token, we login to APIML with user and password (basic auth).
* So, we accept either auth type when setting basic auth creds.
*/
if (this.session.ISession.type !== SessConstants.AUTH_TYPE_BASIC &&
this.session.ISession.type !== SessConstants.AUTH_TYPE_TOKEN) {
return false;
}
if (!this.session.ISession.base64EncodedAuth &&
!(this.session.ISession.user && this.session.ISession.password)) {
return false;
}
this.log.trace("Using basic authentication");
const headerKeys = Object.keys(Headers_1.Headers.BASIC_AUTHORIZATION);
const authentication = AbstractSession_1.AbstractSession.BASIC_PREFIX + ((_a = this.session.ISession.base64EncodedAuth) !== null && _a !== void 0 ? _a : AbstractSession_1.AbstractSession.getBase64Auth(this.session.ISession.user, this.session.ISession.password));
headerKeys.forEach((property) => {
restOptionsToSet.headers[property] = authentication;
});
return true;
}
/**
* Set bearer auth token into our REST request authentication options.
*
* @private
* @param {any} restOptionsToSet
* The set of REST request options into which the credentials will be set.
* @returns True if this function sets authentication options. False otherwise.
* @memberof AbstractRestClient
*/
setBearerAuth(restOptionsToSet) {
if (!(this.session.ISession.type === SessConstants.AUTH_TYPE_BEARER)) {
return false;
}
if (!this.session.ISession.tokenValue) {
return false;
}
this.log.trace("Using bearer authentication");
const headerKeys = Object.keys(Headers_1.Headers.BASIC_AUTHORIZATION);
const authentication = AbstractSession_1.AbstractSession.BEARER_PREFIX + this.session.ISession.tokenValue;
headerKeys.forEach((property) => {
restOptionsToSet.headers[property] = authentication;
});
return true;
}
/**
* Set a PEM certificate auth into our REST request authentication options.
*
* @private
* @param {any} restOptionsToSet
* The set of REST request options into which the credentials will be set.
* @returns True if this function sets authentication options. False otherwise.
* @memberof AbstractRestClient
*/
setCertPemAuth(restOptionsToSet) {
if (!(this.session.ISession.type === SessConstants.AUTH_TYPE_CERT_PEM)) {
return false;
}
this.log.trace("Using PEM Certificate authentication");
try {
restOptionsToSet.cert = (0, fs_1.readFileSync)(this.session.ISession.cert);
restOptionsToSet.key = (0, fs_1.readFileSync)(this.session.ISession.certKey);
}
catch (err) {
throw new error_1.ImperativeError({
msg: "Failed to open one or more PEM certificate files, the file(s) did not exist.",
causeErrors: err,
additionalDetails: err.message,
});
}
return true;
}
/**
* Callback from http(s).request
* @private
* @param {*} res - https response
* @memberof AbstractRestClient
*/
requestHandler(res) {
this.mResponse = res;
this.mContentEncoding = null;
if (this.response.headers != null) {
// This is not ideal, but is the only way to avoid introducing a breaking change.
if (this.session.ISession.type === SessConstants.AUTH_TYPE_TOKEN || this.session.ISession.storeCookie === true) {
if (RestConstants_1.RestConstants.PROP_COOKIE in this.response.headers) {
this.session.storeCookie(this.response.headers[RestConstants_1.RestConstants.PROP_COOKIE]);
}
}
const getHeaderCaseInsensitive = (key) => {
var _a;
return (_a = this.response.headers[key]) !== null && _a !== void 0 ? _a : this.response.headers[key.toLowerCase()];
};
const tempLength = getHeaderCaseInsensitive(Headers_1.Headers.CONTENT_LENGTH);
if (tempLength != null) {
this.mContentLength = tempLength;
this.log.debug("Content length of response is: " + this.mContentLength);
}
const tempEncoding = getHeaderCaseInsensitive(Headers_1.Headers.CONTENT_ENCODING);
if (typeof tempEncoding === "string" && Headers_1.Headers.CONTENT_ENCODING_TYPES.find((x) => x === tempEncoding)) {
this.log.debug("Content encoding of response is: " + tempEncoding);
if (this.mDecode) {
this.mContentEncoding = tempEncoding;
this.log.debug("Using encoding: " + this.mContentEncoding);
}
}
}
if (this.mResponseStream != null) {
this.mResponseStream.on("error", (streamError) => {
this.mReject(streamError instanceof error_1.ImperativeError ? streamError : this.populateError({
msg: "Error writing to responseStream",
causeErrors: streamError,
source: "client"
}));
});
if (this.mContentEncoding != null) {
this.log.debug("Adding decompression transform to response stream");
try {
this.mResponseStream = CompressionUtils_1.CompressionUtils.decompressStream(this.mResponseStream, this.mContentEncoding, this.mNormalizeResponseNewlines);
}
catch (err) {
this.mReject(err);
}
}
}
/**
* Invoke any onData method whenever data becomes available
*/
res.on("data", (dataResponse) => {
this.onData(dataResponse);
});
/**
* Invoke any onEnd method whenever all response data has been received
*/
res.on("end", () => {
this.onEnd();
});
}
/**
* Method to accumulate and buffer http request response data until our
* onEnd method is invoked, at which point all response data has been accounted for.
* NOTE(Kelosky): this method may be invoked multiple times.
* @private
* @param {Buffer} respData - any datatype and content
* @memberof AbstractRestClient
*/
onData(respData) {
this.log.trace("Data chunk received...");
this.mBytesReceived += respData.byteLength;
if (this.requestFailure || this.mResponseStream == null) {
// buffer the data if we are not streaming
// or if we encountered an error, since the rest client
// relies on any JSON error to be in the this.dataString field
this.mChunks.push(respData);
}
else {
this.log.debug("Streaming data chunk of length " + respData.length + " to response stream");
if (this.mNormalizeResponseNewlines && this.mContentEncoding == null) {
this.log.debug("Normalizing new lines in data chunk to operating system appropriate line endings");
respData = io_1.IO.processNewlines(respData, this.lastByteReceived);
}
if (this.mTask != null) {
// update the progress task if provided by the requester
if (this.mContentLength != null) {
this.mTask.percentComplete = Math.floor(operations_1.TaskProgress.ONE_HUNDRED_PERCENT *
(this.mBytesReceived / this.mContentLength));
this.mTask.statusMessage = utilities_1.TextUtils.formatMessage("Downloading %d of %d B", this.mBytesReceived, this.mContentLength);
}
else {
this.mTask.statusMessage = utilities_1.TextUtils.formatMessage("Downloaded %d of ? B", this.mBytesReceived);
if (this.mTask.percentComplete < operations_1.TaskProgress.NINETY_PERCENT) {
// we don't know how far along we are but increment the percentage to
// show that we are making progress
this.mTask.percentComplete++;
}
}
}
// write the chunk to the response stream if requested
this.mResponseStream.write(respData);
this.lastByteReceived = respData[respData.byteLength - 1];
}
}
/**
* Method that must be implemented to extend the IRestClient class. This is the client specific implementation
* for what action to perform after all response data has been collected.
* @private
* @memberof AbstractRestClient
*/
onEnd() {
this.log.debug("onEnd() called for rest client %s", this.constructor.name);
// Concatenate the chunks, then toss the pieces
this.mData = Buffer.concat(this.mChunks);
this.mChunks = [];
if (this.mTask != null) {
this.mTask.percentComplete = operations_1.TaskProgress.ONE_HUNDRED_PERCENT;
this.mTask.stageName = operations_1.TaskStage.COMPLETE;
}
if (this.mContentEncoding != null && this.mData.length > 0) {
this.log.debug("Decompressing encoded response");
try {
this.mData = CompressionUtils_1.CompressionUtils.decompressBuffer(this.mData, this.mContentEncoding);
}
catch (err) {
this.mReject(err);
}
}
const requestEnd = () => {
if (this.requestFailure) {
// Reject the promise with an error
const httpStatus = this.response == null ? undefined : this.response.statusCode;
this.mReject(this.populateError({
msg: "Rest API failure with HTTP(S) status " + httpStatus,
causeErrors: this.dataString,
source: "http"
}));
}
else {
this.mResolve(this.dataString);
}
};
if (this.mResponseStream != null) {
this.log.debug("Ending response stream");
this.mResponseStream.end(requestEnd);
}
else {
requestEnd();
}
}
/**
* Construct a throwable rest client error with all "relevant" diagnostic information.
* The caller should have the session, so not all input fields are present on the error
* response. Only the set required to understand "what may have gone wrong".
*
* The "exit" point for the implementation error override will also be called here. The
* implementation can choose to transform the IImperativeError details however they see
* fit.
*
* @param {IRestClientError} error - The base request error. It is expected to already have msg,
* causeErrors, and the error source pre-populated.
* @param {*} [nodeClientError] - If the source is a node http client error (meaning the request
* did not make it to the remote system) this parameter should be
* populated.
* @returns {RestClientError} - The error that can be thrown or rejected.
*/
populateError(error, nodeClientError) {
// Final error object parameters
let finalError = error;
// extract the status code
const httpStatus = this.response == null ? undefined : this.response.statusCode;
// start off by coercing the request details to string in case an error is encountered trying
// to stringify / inspect them
let headerDetails = this.mReqHeaders + "";
let payloadDetails = this.mWriteData + "";
try {
headerDetails = JSON.stringify(this.mReqHeaders);
payloadDetails = (0, util_1.inspect)(this.mWriteData, { depth: null });
}
catch (stringifyError) {
this.log.error("Error encountered trying to parse details for REST request error:\n %s", (0, util_1.inspect)(stringifyError, { depth: null }));
}
// Populate the "relevant" fields - caller will have the session, so
// no need to duplicate "everything" here, just host/port for easy diagnosis
// Since IRestClientError inherits an errorCode from IImperativeError,
// also put the httpStatus in the errorCode.
finalError.errorCode = httpStatus;
finalError.protocol = this.mSession.ISession.protocol;
finalError.port = this.mSession.ISession.port;
finalError.host = this.mSession.ISession.hostname;
finalError.basePath = this.mSession.ISession.basePath;
finalError.httpStatus = httpStatus;
finalError.errno = nodeClientError != null ? nodeClientError.errno : undefined;
finalError.syscall = nodeClientError != null ? nodeClientError.syscall : undefined;
finalError.payload = this.mWriteData;
finalError.headers = this.mReqHeaders;
finalError.resource = this.mResource;
finalError.request = this.mRequest;
// Construct a formatted details message
let detailMessage;
if (finalError.source === "client") {
detailMessage =
`HTTP(S) client encountered an error. Request could not be initiated to host.\n` +
`Review connection details (host, port) and ensure correctness.`;
}
else if (finalError.source === "timeout") {
detailMessage = `HTTP(S) client encountered an error. Request timed out.`;
}
else {
detailMessage =
`Received HTTP(S) error ${finalError.httpStatus} = ${http.STATUS_CODES[finalError.httpStatus]}.`;
}
let availCredsMsg = Object.keys(this.mSession.ISession._authCache.availableCreds).toString();
if (availCredsMsg.length === 0) {
availCredsMsg = "No credentials were supplied";
}
detailMessage += "\n" +
"\nProtocol: " + finalError.protocol +
"\nHost: " + finalError.host +
"\nPort: " + finalError.port +
"\nBase Path: " + finalError.basePath +
"\nResource: " + finalError.resource +
"\nRequest: " + finalError.request +
"\nHeaders: " + headerDetails +
"\nPayload: " + payloadDetails +
"\nAuth type: " + this.mSession.ISession.type +
"\nAuth order: " + this.mSession.ISession.authTypeOrder +
"\nAvailable creds: " + availCredsMsg +
"\nAllow Unauth Cert: " + !this.mSession.ISession.rejectUnauthorized;
finalError.additionalDetails = detailMessage;
// Allow implementation to modify the error as necessary
// TODO - this is probably no longer necessary after adding the custom
// TODO - error object, but it is left for compatibility.
const processedError = this.processError(error);
if (processedError != null) {
this.log.debug("Error was processed by overridden processError method in RestClient %s", this.constructor.name);
finalError = Object.assign(Object.assign({}, finalError), processedError);
}
// Return the error object
return new RestClientError_1.RestClientError(finalError);
}
/**
* Appends output headers to the http(s) request
* @private
* @param {IHTTPSOptions} options - partially populated options objects
* @param {any[]} [reqHeaders] - input headers for request on outgoing request
* @returns {IHTTPSOptions} - with populated headers
* @memberof AbstractRestClient
*/
appendInputHeaders(options, reqHeaders) {
this.log.trace("appendInputHeaders called with options on rest client %s", JSON.stringify(options), this.constructor.name);
if (reqHeaders && reqHeaders.length > 0) {
reqHeaders.forEach((reqHeader) => {
const requestHeaderKeys = Object.keys(reqHeader);
requestHeaderKeys.forEach((property) => {
options.headers[property] = reqHeader[property];
});
});
}
return options;
}
/**
* Determine whether we should stringify or leave writable data alone
* @private
* @param {http.OutgoingHttpHeaders} headers - options containing populated headers
* @memberof AbstractRestClient
*/
setTransferFlags(headers) {
if (headers[Headers_1.Headers.CONTENT_TYPE] != null) {
const contentType = headers[Headers_1.Headers.CONTENT_TYPE];
if (contentType === Headers_1.Headers.APPLICATION_JSON[Headers_1.Headers.CONTENT_TYPE]) {
this.mIsJson = true;
}
else if (contentType === Headers_1.Headers.OCTET_STREAM[Headers_1.Headers.CONTENT_TYPE]) {
this.log.debug("Found octet-stream header in request. Will write in binary mode");
}
}
}
/**
* Determine if the hostname parameter is valid before attempting the request
*
* @private
* @param {String} hostname - the hostname to check
* @memberof AbstractRestClient
* @throws {ImperativeError} - if the hostname is invalid
*/
validateRestHostname(hostname) {
if (!hostname) {
throw new error_1.ImperativeError({ msg: "The hostname is required." });
}
else if (URL.canParse(hostname)) {
throw new error_1.ImperativeError({ msg: "The hostname should not contain the protocol." });
}
}
/**
* Return whether or not a REST request was successful by HTTP status code
* @readonly
* @type {boolean}
* @memberof AbstractRestClient
*/
get requestSuccess() {
if (this.response == null) {
return false;
}
else {
return this.response.statusCode >= RestConstants_1.RestConstants.HTTP_STATUS_200 &&
this.response.statusCode < RestConstants_1.RestConstants.HTTP_STATUS_300;
}
}
/**
* Return whether or not a REST request was successful by HTTP status code
* @readonly
* @type {boolean}
* @memberof AbstractRestClient
*/
get requestFailure() {
return !this.requestSuccess;
}
/**
* Return http(s) response body as a buffer
* @readonly
* @type {Buffer}
* @memberof AbstractRestClient
*/
get data() {
return this.mData;
}
/**
* Return http(s) response body as a string
* @readonly
* @type {string}
* @memberof AbstractRestClient
*/
get dataString() {
if (this.data == null) {
return undefined;
}
return this.data.toString("utf8");
}
/**
* Return http(s) response object
* @readonly
* @type {*}
* @memberof AbstractRestClient
*/
get response() {
return this.mResponse;
}
/**
* Return this session object
* @readonly
* @type {Session}
* @memberof AbstractRestClient
*/
get session() {
return this.mSession;
}
/**
* Return the logger object for ease of reference
* @readonly
* @type {Logger}
* @memberof AbstractRestClient
*/
get log() {
return this.mLogger;
}
}
exports.AbstractRestClient = AbstractRestClient;
//# sourceMappingURL=AbstractRestClient.js.map