UNPKG

@zowe/imperative

Version:
925 lines 44.5 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"); /** * 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