UNPKG

@zowe/imperative

Version:
1,002 lines 58.3 kB
"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