@zowe/imperative
Version:
framework for building configurable CLIs
1,002 lines • 58.3 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");
const censor_1 = require("../../../censor");
/**
* 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);
}
/**
* Getter for the request queue, if one exists.
* Must be implemented at the end Rest Client level to be static.
* @memberof AbstractRestClient
* @returns {Queue | undefined}
*/
get requestQueue() {
return undefined; // Must be implemented
}
/**
* Wrap the request for a request into a request queue.
* Use the standard implementation if there is no request queue.
* @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) {
if (this.requestQueue) {
let requestPool = undefined;
if (this.session.ISession.hostname) {
requestPool = this.session.ISession.hostname;
if (this.session.ISession.port) {
requestPool += ":" + this.session.ISession.port.toString();
}
}
return this.requestQueue.enqueue(this._request.bind(this, options), requestPool);
}
return this._request(options);
}
/**
* 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);
// form a header from scrtData and place the header into the options.reqHeaders
this.addScrtHeader(options);
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 we held back a CR from the previous chunk, prepend it to current chunk
if (this.mNormalizeRequestNewlines) {
if (heldByte != null) {
data = Buffer.concat([heldByte, data]);
heldByte = undefined;
}
this.log.debug("Normalizing new lines in request chunk to \\n");
data = io_1.IO.processNewlines(data, this.lastByteReceivedUpload, true);
// If chunk ends with CR, hold it back until we see the next chunk
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
if (data.byteLength > 0 && data[data.byteLength - 1] === 13) {
heldByte = Buffer.from([data[data.byteLength - 1]]);
data = data.subarray(0, data.byteLength - 1);
this.log.debug("Holding back CR byte at chunk boundary");
}
}
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++;
}
}
if (data.byteLength > 0) {
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(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;
}
/**
* Adds any existing SCRT data as an SCRT header to the REST request.
* If no SCRT data has been supplied, this function does nothing.
* If errors exist in the SCRT data, this function just logs an error,
* so that the primary function of the REST request can still be accomplished.
*
* @private
* @param {IRestOptions} restReqOpts
* The options for the current REST request.
* Used to get the path and request headers.
* @memberof AbstractRestClient
*/
addScrtHeader(restReqOpts) {
let scrtData = null;
if (restReqOpts.resource.startsWith("/zosmf/")) {
logger_1.Logger.getImperativeLogger().debug("addScrtHeader: SCRT headers are NOT sent to z/OSMF.");
return;
}
// When scrtData exists in an environment variable, use it instead of scrtData in session
scrtData = this.getScrtFromEnv();
if (scrtData === null) {
// try to get SCRT data that was stored in the session by a Zowe client app
if (!Object.hasOwn(this.mSession.ISession, "scrtData")) {
// when no ScrtData is supplied, we have nothing to do.
return;
}
scrtData = this.mSession.ISession.scrtData;
}
const scrtHeaderVal = this.formScrtHeaderVal(scrtData);
if (scrtHeaderVal !== null) {
// we have a header to add to this request
restReqOpts.reqHeaders.push({
"Zowe-SCRT-client-feature": scrtHeaderVal
});
}
}
/**
* Get SCRT data from an environment variable. An environment variable is
* to be used exclusively by orchestrator apps (like an Ansible collection),
* which do not make direct REST requests. Instead, they run Zowe CLI commands
* to perform their actions. When SCRT data is set in an environment variable,
* the SCRT data in the session is ignored.
*
* @returns {IScrtData}
* A string containing a properly formatted value for a Zowe-SCRT-client-feature header.
* If SCRT data cannot be found, null is returned.
*/
getScrtFromEnv() {
const scrtEnvNm = "ZOWE_SCRT_CLIENT_FEATURE";
let scrtData = null;
const scrtStr = process.env[scrtEnvNm];
if (!utilities_1.TextUtils.hasNonBlankValue(scrtStr)) {
return null;
}
// parse featureName from the env var value
const enclosingQuote = "['\"]";
const valWithinQuotes = "([^'\"]+)";
const valAfterEquals = ` *= *${enclosingQuote}${valWithinQuotes}${enclosingQuote}`;
let envValRegEx = new RegExp("featureName" + valAfterEquals);
let envVal = scrtStr.match(envValRegEx);
if (envVal === null) {
logger_1.Logger.getImperativeLogger().error(`getScrtFromEnv: Required property = 'featureName' was not supplied in ` +
`environment variable '${scrtEnvNm}'. Value: ${scrtStr}`);
return null;
}
scrtData = { "featureName": envVal[1] };
// parse productId from the env var value
envValRegEx = new RegExp("productId" + valAfterEquals);
envVal = scrtStr.match(envValRegEx);
if (envVal !== null) {
scrtData.productId = envVal[1];
}
// parse productVersion from the env var value
envValRegEx = new RegExp("productVersion" + valAfterEquals);
envVal = scrtStr.match(envValRegEx);
if (envVal !== null) {
scrtData.productVersion = envVal[1];
}
return scrtData;
}
/**
* Form an HTTP header containing the SCRT data.
*
* @returns {IScrtData}
* A string containing a properly formatted value for a Zowe-SCRT-client-feature header.
* If no SCRT data has been set, null is returned.
*/
formScrtHeaderVal(scrtData) {
const funName = "formScrtHeaderVal:";
if (!this.isScrtValid(scrtData)) {
logger_1.Logger.getImperativeLogger().error(`${funName} No SCRT header is created when SCRT data is invalid.`);
return null;
}
// escape any escape characters and any single or double quotes in SCRT property values
let scrtHeaderVal = "";
for (const [scrtPropName, scrtPropVal] of Object.entries(scrtData)) {
const valToUse = String(scrtPropVal)
.trim()
.replaceAll("\\", "\\\\") // backslash to double backslash
.replaceAll('"', '\\"') // doublequote to backslash doublequote
.replaceAll("'", "\\'"); // singlequote to backslash singlequote
if (scrtHeaderVal.length > 0) {
scrtHeaderVal += ", ";
}
scrtHeaderVal += `${scrtPropName}='${valToUse}'`;
}
if (scrtHeaderVal.length === 0) {
// this should not occur since scrtData was validated, but we want a safety net
logger_1.Logger.getImperativeLogger().error(`${funName} Failed to form an SCRT header value. Length of header value is zero. ` +
`scrtData = ` + JSON.stringify(scrtData, null, 2));
return null;
}
return scrtHeaderVal;
}
/**
* Validates the supplied SCRT data.
*
* @param {IScrtData} scrtData - The data items required for SCRT reporting
*
* @returns {boolean} True if scrtData contains valid data. False otherwise.
*/
isScrtValid(scrtData) {
const funName = "isScrtValid:";
const FEAT_NAME_KEYWORD = "featureName";
const MAX_FEAT_NAME_LEN = 48;
const PROD_ID_KEYWORD = "productId";
const MAX_PROD_ID_LEN = 8;
const PROD_VER_KEYWORD = "productVersion";
const MAX_VER_LEN = 8;
const PROD_VER_SEG_KEYWORDS = ["version", "release", "modLevel"];
const MAX_VER_SEG_LEN = 2;
if (!scrtData) {
logger_1.Logger.getImperativeLogger().error(`${funName} The supplied scrtData is null or undefined.`);
return false;
}
for (const scrtPropNm of Object.keys(scrtData)) {
if (scrtPropNm === FEAT_NAME_KEYWORD || scrtPropNm === PROD_ID_KEYWORD ||
scrtPropNm === PROD_VER_KEYWORD) {
if (/^[\x20-\x7E]*$/.test(scrtData[scrtPropNm]) === false) {
logger_1.Logger.getImperativeLogger().error(`${funName} The SCRT property '${scrtPropNm}' ` +
`contains non-printable ASCII characters = '${scrtData[scrtPropNm]}'`);
return false;
}
}
else {
logger_1.Logger.getImperativeLogger().error(`${funName} The non-SCRT property = '${scrtPropNm}' ` +
`will not be placed in an SCRT header.`);
delete scrtData[scrtPropNm];
}
}
// featureName is a required property
if (!scrtData[FEAT_NAME_KEYWORD]) {
logger_1.Logger.getImperativeLogger().error(`${funName} ${FEAT_NAME_KEYWORD} is null or undefined.`);
return false;
}
if (!utilities_1.TextUtils.hasNonBlankValue(scrtData[FEAT_NAME_KEYWORD])) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${FEAT_NAME_KEYWORD}' is blank.`);
return false;
}
if (scrtData[FEAT_NAME_KEYWORD].length > MAX_FEAT_NAME_LEN) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${FEAT_NAME_KEYWORD}' is longer than ` +
`${MAX_FEAT_NAME_LEN} bytes. Value = '${scrtData[FEAT_NAME_KEYWORD]}'`);
return false;
}
// productId is an optional property
if (Object.hasOwn(scrtData, PROD_ID_KEYWORD)) {
if (scrtData[PROD_ID_KEYWORD] === null) {
logger_1.Logger.getImperativeLogger().error(`${funName} ${PROD_ID_KEYWORD} is null.`);
return false;
}
else {
if (!utilities_1.TextUtils.hasNonBlankValue(scrtData[PROD_ID_KEYWORD])) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${PROD_ID_KEYWORD}' is blank.`);
return false;
}
if (scrtData[PROD_ID_KEYWORD].length > MAX_PROD_ID_LEN) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${PROD_ID_KEYWORD}' is longer than ` +
`${MAX_PROD_ID_LEN} bytes. Value = '${scrtData[PROD_ID_KEYWORD]}'`);
return false;
}
}
}
// productVersion is an optional property
if (Object.hasOwn(scrtData, PROD_VER_KEYWORD)) {
if (scrtData[PROD_VER_KEYWORD] === null) {
logger_1.Logger.getImperativeLogger().error(`${funName} ${PROD_VER_KEYWORD} is null.`);
return false;
}
else {
if (!utilities_1.TextUtils.hasNonBlankValue(scrtData[PROD_VER_KEYWORD])) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${PROD_VER_KEYWORD}' is blank.`);
return false;
}
if (scrtData[PROD_VER_KEYWORD].length > MAX_VER_LEN) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${PROD_VER_KEYWORD}' is longer than ` +
`${MAX_VER_LEN} bytes. Value = '${scrtData[PROD_VER_KEYWORD]}'`);
return false;
}
}
// validate each segment of the version
const numOfVerSegs = 3;
const verSegments = scrtData[PROD_VER_KEYWORD].split(".");
if (verSegments.length !== numOfVerSegs) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${PROD_VER_KEYWORD}' is not formatted as vv.rr.mm ` +
`Value = '${scrtData[PROD_VER_KEYWORD]}'`);
return false;
}
for (let segInx = 0; segInx <= numOfVerSegs - 1; segInx++) {
// validate the length of each segment of the version
if (verSegments[segInx].length > MAX_VER_SEG_LEN) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${PROD_VER_SEG_KEYWORDS[segInx]}' is longer than ` +
`${MAX_VER_SEG_LEN} bytes. ` +
`Value = '${verSegments[segInx]}'`);
return false;
}
if (!utilities_1.TextUtils.hasNonBlankValue(verSegments[segInx])) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${PROD_VER_SEG_KEYWORDS[segInx]}' is blank.`);
return false;
}
// validate that each segment of the version is numeric
if (Number.isNaN(Number(verSegments[segInx]))) {
logger_1.Logger.getImperativeLogger().error(`${funName} '${PROD_VER_SEG_KEYWORDS[segInx]}' is not a numeric ` +
`value = '${verSegments[segInx]}'`);
return false;
}
}
}
return true;
}
/**
* 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;
}
const logMessage = "Using cookie authentication with token" +
(censor_1.Censor.isSecureValue("tokenType") ? "" : ` type ${this.session.ISession.tokenType}`);
this.log.trace(logMessage);
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;
this.mDecodeStream = 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.mDecodeStream = 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) {
var _a;
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
((_a = this.mDecodeStream) !== null && _a !== void 0 ? _a : 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() {
var _a;
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.on("finish", requestEnd);
((_a = this.mDecodeStream) !== null && _a !== void 0 ? _a : this.mResponseStream).end();
}
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