UNPKG

snowflake-sdk

Version:
1,653 lines (1,436 loc) 49.2 kB
/* * Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved. */ /* SnowflakeService state machine Preconnected - enter() - Preconnected - connect() - Connecting - request() - Connecting - destroy() - Preconnected Connecting - enter() - async operation - Connected if operation succeeds - Disconnected if network error (we need another PreConnected state) - Disconnected if operation fails connect() - error request() - enqueue destroy() - enqueue Connected - enter() - Connected connect() - error request() - async operation - Connected if operation succeeds - Connected if network error - Renewing if GS says session token has expired - Disconnected if GS says session token is invalid destroy() - async operation - Disconnected if operation succeeds - Connected if network error - Connected if operation fails Renewing - enter() - async operation - Connected if operation succeeds - Connected if network error - Disconnected if operation fails - connect() - error - request() - enqueue - destroy() - enqueue Disconnected - enter() - Disconnected - connect() - Disconnected - request() - Disconnected - destroy() - Disconnected */ const { v4: uuidv4 } = require('uuid'); const EventEmitter = require('events').EventEmitter; const Util = require('../util'); const Errors = require('../errors'); const ErrorCodes = Errors.codes; const Url = require('url'); const QueryString = require('querystring'); const Parameters = require('../parameters'); const GSErrors = require('../constants/gs_errors'); const QueryContextCache = require('../queryContextCache'); const Logger = require('../logger'); const GlobalConfig = require('../global_config'); const AuthenticationTypes = require('../authentication/authentication_types'); const AuthOkta = require('../authentication/auth_okta'); const AuthKeypair = require('../authentication/auth_keypair'); const Authenticator = require('../authentication/authentication'); function isRetryableNetworkError(err) { // anything other than REVOKED error can be retryable. return !Object.prototype.hasOwnProperty.call(err, 'cause') || err.cause === undefined || !Object.prototype.hasOwnProperty.call(err.cause, 'code') || ( err.cause.code !== ErrorCodes.ERR_OCSP_REVOKED && err.cause.code !== 'DEPTH_ZERO_SELF_SIGNED_CERT' && err.cause.code !== 'CERT_HAS_EXPIRED' && err.cause.code !== 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' && err.cause.code !== 'SELF_SIGNED_CERT_IN_CHAIN' ); } function isRetryableHttpError(err) { return Object.prototype.hasOwnProperty.call(err, 'response') && Util.isRetryableHttpError(err.response, false); } /** * Creates a new SnowflakeService instance. * * @param {Object} connectionConfig * @param {Object} httpClient * @param {Object} [config] * @constructor */ function SnowflakeService(connectionConfig, httpClient, config) { // validate input Errors.assertInternal(Util.isObject(connectionConfig)); Errors.assertInternal(Util.isObject(httpClient)); Errors.assertInternal(!Util.exists(config) || Util.isObject(config)); // if a config object was specified, verify // that it has all the information we need let tokenInfoConfig; if (Util.exists(config)) { Errors.assertInternal(Util.isObject(config)); Errors.assertInternal(Util.isObject(config.tokenInfo)); tokenInfoConfig = config.tokenInfo; } else if (connectionConfig.sessionToken) { tokenInfoConfig = { sessionToken: connectionConfig.sessionToken, masterToken: connectionConfig.masterToken || connectionConfig.sessionToken, sessionTokenExpirationTime: connectionConfig.sessionTokenExpirationTime || Date.now() + 1000 * 60 * 60 * 24, masterTokenExpirationTime: connectionConfig.masterTokenExpirationTime || Date.now() + 1000 * 60 * 60 * 24 }; } // create a new TokenInfo instance const tokenInfo = new TokenInfo(tokenInfoConfig); this.authenticator = Authenticator.getAuthenticator(connectionConfig, httpClient); // create state objects for all the different states we can be in const stateOptions = { snowflakeService: this, httpClient: httpClient, connectionConfig: connectionConfig, tokenInfo: tokenInfo }; const statePristine = new StatePristine(stateOptions); const stateConnecting = new StateConnecting(stateOptions); const stateConnected = new StateConnected(stateOptions); const stateRenewing = new StateRenewing(stateOptions); const stateDisconnected = new StateDisconnected(stateOptions); let currentState; /** * Transitions to a given state. * * @param {Object} state * @param {Object} [transitionContext] */ const transitionTo = function (state, transitionContext) { // this check is necessary to make sure we don't re-enter a transient state // like Renewing when we're already in it if (currentState !== state) { // if we have a current state, exit it; the null check is necessary // because the currentState is undefined at bootstrap time when we // transition to the first state if (currentState) { currentState.exit(); } // update the current state currentState = state; // enter the new state currentState.enter(transitionContext); } }; /** * Set the session id for the current SnowflakeService * @param sessionId */ this.setSessionId = function (sessionId) { this.sessionId = sessionId; }; /** * Get the session id. * @returns {number} */ this.getSessionId = function () { return this.sessionId; }; /** * Transitions to the Pristine state. * * {Object} [transitionContext] */ this.transitionToPristine = function (transitionContext) { transitionTo(statePristine, transitionContext); }; /** * Transitions to the Connecting state. * * {Object} [transitionContext] */ this.transitionToConnecting = function (transitionContext) { transitionTo(stateConnecting, transitionContext); }; /** * Transitions to the Connected state. * * {Object} [transitionContext] */ this.transitionToConnected = function (transitionContext) { transitionTo(stateConnected, transitionContext); }; /** * Transitions to the Renewing state. * * {Object} [transitionContext] */ this.transitionToRenewing = function (transitionContext) { transitionTo(stateRenewing, transitionContext); }; /** * Transitions to the Disconnected state. * * {Object} [transitionContext] */ this.transitionToDisconnected = function (transitionContext) { transitionTo(stateDisconnected, transitionContext); // clear the tokens because we're in a fatal state and we don't want the // tokens to be available via getConfig() anymore tokenInfo.clearTokens(); }; /** * Returns a configuration object that can be passed to the SnowflakeService * constructor to get an equivalent SnowflakeService object. * * @returns {Object} */ this.getConfig = function () { return { tokenInfo: tokenInfo.getConfig() }; }; /** * Establishes a connection to Snowflake. * * @param {Object} options */ this.connect = function (options) { new OperationConnect(options).validate().execute(); }; /** * Issues a connect-continue request to Snowflake. * * @param {Object} [options] */ this.continue = function (options) { new OperationContinue(options).validate().execute(); }; /** * Issues a generic request to Snowflake. * * @param {Object} options */ this.request = function (options) { new OperationRequest(options).validate().execute(); }; /** * Issues a generic async request to Snowflake. * * @param {Object} options */ this.requestAsync = async function (options) { return await new OperationRequest(options).validate().executeAsync(); }; /** * Terminates the current connection to Snowflake. * * @param {Object} options */ this.destroy = function (options) { this.clearCache(); new OperationDestroy(options).validate().execute(); }; /** * Creates a new OperationAbstract. * * @param {Object} options * @constructor */ function OperationAbstract(options) { this.options = options; } /** * Validates the operation options. * * @returns {Object} the operation. */ OperationAbstract.prototype.validate = function () { return this; }; /** * Executes the operation. */ OperationAbstract.prototype.execute = function () { }; /** * Creates a new OperationConnect. * * @param {Object} options * @constructor */ function OperationConnect(options) { OperationAbstract.apply(this, [options]); } Util.inherits(OperationConnect, OperationAbstract); /** * @inheritDoc */ OperationConnect.prototype.validate = function () { // verify that the options object contains a callback function const options = this.options; Errors.assertInternal( (Util.isObject(options) && Util.isFunction(options.callback))); return this; }; /** * @inheritDoc */ OperationConnect.prototype.execute = function () { currentState.connect(this.options); }; /** * Creates a new OperationContinue. * * @param {Object} options * @constructor */ function OperationContinue(options) { OperationAbstract.apply(this, [options]); } Util.inherits(OperationContinue, OperationAbstract); /** * @inheritDoc */ OperationContinue.prototype.validate = function () { // verify that the options contain a json object const options = this.options; Errors.assertInternal( Util.isObject(options) && Util.isObject(options.json)); return this; }; /** * @inheritDoc */ OperationContinue.prototype.execute = function () { currentState.continue(this.options); }; /** * Creates a new OperationRequest. * * @param {Object} options * @constructor */ function OperationRequest(options) { OperationAbstract.apply(this, [options]); } Util.inherits(OperationRequest, OperationAbstract); /** * @inheritDoc */ OperationRequest.prototype.validate = function () { // verify that the options object contains all the necessary information const options = this.options; Errors.assertInternal(Util.isObject(options)); Errors.assertInternal(Util.isString(options.method)); Errors.assertInternal( !Util.exists(options.headers) || Util.isObject(options.headers)); Errors.assertInternal(Util.isString(options.url)); Errors.assertInternal( !Util.exists(options.json) || Util.isObject(options.json)); return this; }; /** * @inheritDoc */ OperationRequest.prototype.execute = function () { currentState.request(this.options); }; /** * @inheritDoc */ OperationRequest.prototype.executeAsync = async function () { return await currentState.requestAsync(this.options); }; /** * Creates a new OperationDestroy. * * @param {Object} options * @constructor */ function OperationDestroy(options) { OperationAbstract.apply(this, [options]); } Util.inherits(OperationDestroy, OperationAbstract); /** * @inheritDoc */ OperationDestroy.prototype.validate = function () { // verify that the options object contains a callback function const options = this.options; Errors.assertInternal(Util.isObject(options) && Util.isFunction(options.callback)); return this; }; /** * @inheritDoc */ OperationDestroy.prototype.execute = function () { // delegate to current state currentState.destroy(this.options); }; /* All queued operations will be added to this array */ const operationQueue = []; /** * Appends a request operation to the queue. * * @param {Object} options */ this.enqueueRequest = function (options) { operationQueue.push(new OperationRequest(options)); }; /** * Appends a destroy operation to the queue. * * @param {Object} options */ this.enqueueDestroy = function (options) { operationQueue.push(new OperationDestroy(options)); }; /** * Executes all the operations in the queue. */ this.drainOperationQueue = function () { // execute all the operations in the queue for (let index = 0, length = operationQueue.length; index < length; index++) { operationQueue[index].execute(); } // empty the queue operationQueue.length = 0; }; this.isConnected = function () { return currentState === stateConnected || currentState === stateConnecting || currentState === stateRenewing; }; this.getServiceName = function () { return Parameters.getValue(Parameters.names.SERVICE_NAME); }; this.getClientSessionKeepAlive = function () { return Parameters.getValue(Parameters.names.CLIENT_SESSION_KEEP_ALIVE); }; this.getClientSessionKeepAliveHeartbeatFrequency = function () { return Parameters.getValue(Parameters.names.CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY); }; this.getJsTreatIntegerAsBigInt = function () { return Parameters.getValue(Parameters.names.JS_TREAT_INTEGER_AS_BIGINT); }; this.getAuthenticator = function () { return this.authenticator; }; // if we don't have any tokens, start out as pristine if (tokenInfo.isEmpty()) { this.transitionToPristine(); } else { // we're already connected this.transitionToConnected(); } /** * Issues a post request to Snowflake. * * @param {Object} options */ this.postAsync = function (options) { return new OperationRequest(options).validate().executeAsync(); }; this.getQueryContextDTO = function () { if (!this.qcc){ return; } return this.qcc.getQueryContextDTO(); }; this.deserializeQueryContext = function (data) { if (!this.qcc){ return; } this.qcc.deserializeQueryContext(data); }; this.clearCache = function () { if (!this.qcc){ return; } this.qcc.clearCache(); }; this.initializeQueryContextCache = function (size) { if (!connectionConfig.getDisableQueryContextCache()){ this.qcc = new QueryContextCache(size, this.getSessionId()); } else { Logger.getInstance().debug(`QueryContextCache initialization skipped as it is disabled for connection with sessionId: ${this.sessionId}`); } }; // testing purpose this.getQueryContextCacheSize = function () { if (!this.qcc){ return; } return this.qcc.getSize(); }; } Util.inherits(SnowflakeService, EventEmitter); module.exports = SnowflakeService; /////////////////////////////////////////////////////////////////////////// //// StateAbstract //// /////////////////////////////////////////////////////////////////////////// /** * Creates a new StateAbstract instance. * * @param {Object} options * @constructor */ function StateAbstract(options) { /** * Issues an http request to Snowflake. * * @param {Object} requestOptions * @param {Object} httpClient * * @returns {Object} the http request object. */ function sendHttpRequest(requestOptions, httpClient, auth) { const realRequestOptions = { method: requestOptions.method, headers: requestOptions.headers, url: requestOptions.absoluteUrl, gzip: requestOptions.gzip, json: requestOptions.json, callback: async function (err, response, body) { // if we got an error, wrap it into a network error if (err) { // if we're running in DEBUG loglevel, probably we want to see the full error instead Logger.getInstance().debug('Encountered an error when sending the request. Details: ' + JSON.stringify(err, Util.getCircularReplacer())); err = Errors.createNetworkError( ErrorCodes.ERR_SF_NETWORK_COULD_NOT_CONNECT, err); } else if (!response) { // empty response err = Errors.createUnexpectedContentError( ErrorCodes.ERR_SF_RESPONSE_NOT_JSON, '(EMPTY)'); } else if (Object.prototype.hasOwnProperty.call(response, 'statusCode') && response.statusCode !== 200) { // if we didn't get a 200, the request failed if (response.statusCode === 401 && response.body) { let innerCode; try { innerCode = JSON.parse(response.body).code; } catch (e) { err = Errors.createRequestFailedError( ErrorCodes.ERR_SF_RESPONSE_FAILURE, response); Logger.getInstance().debug('HTTP Error: %s', response.statusCode); } if (innerCode === '390104') { err = Errors.createRequestFailedError( ErrorCodes.ERR_SF_RESPONSE_INVALID_TOKEN, response); Logger.getInstance().debug('HTTP Error: %s', response.statusCode); } else { err = Errors.createRequestFailedError( ErrorCodes.ERR_SF_RESPONSE_FAILURE, response); Logger.getInstance().debug('HTTP Error: %s', response.statusCode); } } else { err = Errors.createRequestFailedError( ErrorCodes.ERR_SF_RESPONSE_FAILURE, response); Logger.getInstance().debug('HTTP Error: %s', response.statusCode); } } else { // if the response body is a non-empty string and the response is // supposed to contain json, try to json-parse the body if (Util.isString(body) && response.getResponseHeader('Content-Type') === 'application/json') { try { if (body.includes('smkId')) { body = Util.convertSmkIdToString(body); } body = JSON.parse(body); } catch (parseError) { // we expected to get json err = Errors.createUnexpectedContentError( ErrorCodes.ERR_SF_RESPONSE_NOT_JSON, response.body); } } // if we were able to successfully json-parse the body and the // success flag is false, the operation we tried to perform failed if (body && !body.success) { const data = body.data; if (body.code === GSErrors.code.ID_TOKEN_INVALID && data.authnMethod === 'TOKEN') { Logger.getInstance().debug('ID Token being used has expired. Reauthenticating'); const key = Util.buildCredentialCacheKey(connectionConfig.host, connectionConfig.username, AuthenticationTypes.ID_TOKEN_AUTHENTICATOR); await GlobalConfig.getCredentialManager().remove(key); await auth.reauthenticate(requestOptions.json); return httpClient.request(realRequestOptions); } err = Errors.createOperationFailedError( body.code, data, body.message, data && data.sqlState ? data.sqlState : undefined); } } // if we have an error, clear the body if (err) { body = undefined; } // if a callback was specified, invoke it if (Util.isFunction(requestOptions.callback)) { await requestOptions.callback.apply(requestOptions.scope, [err, body]); } } }; if (requestOptions.retry > 2) { const includeParam = requestOptions.url.includes('?'); realRequestOptions.url += (includeParam ? '&' : '?'); realRequestOptions.url += ('clientStartTime=' + requestOptions.startTime + '&' + 'retryCount=' + (requestOptions.retry - 1)); } return httpClient.request(realRequestOptions); } this.snowflakeService = options.snowflakeService; this.httpClient = options.httpClient; this.connectionConfig = options.connectionConfig; this.tokenInfo = options.tokenInfo; const connectionConfig = options.connectionConfig; const snowflakeService = options.snowflakeService; const httpClient = options.httpClient; /////////////////////////////////////////////////////////////////////////// //// Request //// /////////////////////////////////////////////////////////////////////////// /** * Creates a new Request. * * @param {Object} requestOptions * @constructor */ function Request(requestOptions) { this.requestOptions = requestOptions; } /** * Sends out the request. * * @returns {Object} the request that was issued. */ Request.prototype.sendAsync = async function () { // pre-process the request options this.preprocessOptions(this.requestOptions); const options = { method: this.requestOptions.method, headers: this.requestOptions.headers, url: this.requestOptions.absoluteUrl, json: this.requestOptions.json }; // issue the async http request return await httpClient.requestAsync(options); }; /** * Sends out the request. * * @returns {Object} the request that was issued. */ Request.prototype.send = function () { // pre-process the request options this.preprocessOptions(this.requestOptions); // issue the http request sendHttpRequest(this.requestOptions, httpClient, snowflakeService.getAuthenticator()); }; /** * Pre-processes the request options just before the request is sent. * * @param {Object} requestOptions */ Request.prototype.preprocessOptions = function (requestOptions) { // augment the headers with the default request headers requestOptions.headers = Util.apply(this.getDefaultReqHeaders(), requestOptions.headers || {}); if (Util.isLoginRequest(requestOptions.url)) { Util.apply(requestOptions.headers, { 'CLIENT_APP_VERSION': requestOptions.json.data.CLIENT_APP_VERSION, 'CLIENT_APP_ID': requestOptions.json.data.CLIENT_APP_ID, }); } // augment the options with the absolute url requestOptions.absoluteUrl = this.buildFullUrl(requestOptions.url); }; /** * Converts a relative url to an absolute url. * * @param {String} relativeUrl * * @returns {String} */ Request.prototype.buildFullUrl = function (relativeUrl) { return connectionConfig.accessUrl + relativeUrl; }; /** * Returns the default headers to send with every request. * * @returns {Object} */ Request.prototype.getDefaultReqHeaders = function () { return { 'Accept': 'application/json', 'Content-Type': 'application/json' }; }; /////////////////////////////////////////////////////////////////////////// //// SessionTokenRequest //// /////////////////////////////////////////////////////////////////////////// /** * @constructor */ function SessionTokenRequest() { Request.apply(this, arguments); } Util.inherits(SessionTokenRequest, Request); /** * @inheritDoc */ SessionTokenRequest.prototype.preprocessOptions = function (requestOptions) { // call super Request.prototype.preprocessOptions.apply(this, arguments); // add the current session token to the request headers requestOptions.headers = requestOptions.headers || {}; requestOptions.headers.Authorization = 'Snowflake Token="' + options.tokenInfo.getSessionToken() + '"'; if (Util.string.isNotNullOrEmpty( Parameters.getValue(Parameters.names.SERVICE_NAME))) { requestOptions.headers['X-Snowflake-Service'] = Parameters.getValue(Parameters.names.SERVICE_NAME); } }; /////////////////////////////////////////////////////////////////////////// //// MasterTokenRequest //// /////////////////////////////////////////////////////////////////////////// /** * @constructor */ function MasterTokenRequest() { Request.apply(this, arguments); } Util.inherits(MasterTokenRequest, Request); /** * @inheritDoc */ MasterTokenRequest.prototype.preprocessOptions = function (requestOptions) { // call super Request.prototype.preprocessOptions.apply(this, arguments); // add the current master token to the request headers requestOptions.headers = requestOptions.headers || {}; requestOptions.headers.Authorization = 'Snowflake Token="' + options.tokenInfo.getMasterToken() + '"'; }; /////////////////////////////////////////////////////////////////////////// //// UnauthenticatedRequest //// /////////////////////////////////////////////////////////////////////////// /** * Creates a new UnauthenticatedRequest. * * @constructor */ function UnauthenticatedRequest() { Request.apply(this, arguments); } Util.inherits(UnauthenticatedRequest, Request); /** * Creates a new SessionTokenRequest. * * @param {Object} requestOptions * * @returns {Object} */ this.createSessionTokenRequest = function (requestOptions) { return new SessionTokenRequest(requestOptions); }; /** * Creates a new MasterTokenRequest. * * @param {Object} requestOptions * * @returns {Object} */ this.createMasterTokenRequest = function (requestOptions) { return new MasterTokenRequest(requestOptions); }; /** * Creates a new UnauthenticatedRequest. * * @param {Object} requestOptions * * @returns {Object} */ this.createUnauthenticatedRequest = function (requestOptions) { return new UnauthenticatedRequest(requestOptions); }; } /** * Enters this state. * @abstract */ StateAbstract.prototype.enter = function () { }; /** * Exits this state. * @abstract */ StateAbstract.prototype.exit = function () { }; /** * Establishes a connection to Snowflake. * * @abstract */ StateAbstract.prototype.connect = function () { }; /** * Issues a connect-continue request to Snowflake. * * @abstract */ StateAbstract.prototype.continue = function () { }; /** * Issues a generic request to Snowflake. * * @abstract */ StateAbstract.prototype.request = function () { }; /** * Terminates the current connection to Snowflake. * * @abstract */ StateAbstract.prototype.destroy = function () { }; /////////////////////////////////////////////////////////////////////////// //// StatePristine //// /////////////////////////////////////////////////////////////////////////// function StatePristine() { StateAbstract.apply(this, arguments); } Util.inherits(StatePristine, StateAbstract); /** * @inheritDoc */ StatePristine.prototype.connect = function (options) { // transition to the Connecting state with the callback in the transition // context this.snowflakeService.transitionToConnecting( { options: options }); }; /** * @inheritDoc */ StatePristine.prototype.request = function (options) { const callback = options.callback; process.nextTick(function () { callback(Errors.createClientError( ErrorCodes.ERR_CONN_REQUEST_STATUS_PRISTINE)); }); }; /** * @inheritDoc */ StatePristine.prototype.destroy = function (options) { // we're still in the preconnected state so any // attempts to destroy should result in an error const callback = options.callback; process.nextTick(function () { callback(Errors.createClientError( ErrorCodes.ERR_CONN_DESTROY_STATUS_PRISTINE)); }); }; /////////////////////////////////////////////////////////////////////////// //// StateConnecting //// /////////////////////////////////////////////////////////////////////////// function StateConnecting() { StateAbstract.apply(this, arguments); } Util.inherits(StateConnecting, StateAbstract); /** * @inheritDoc */ StateConnecting.prototype.enter = function (context) { // save the context this.context = context; // initiate the connection process this.continue(); }; /** * @inheritDoc */ StateConnecting.prototype.exit = function () { // clear the context this.context = null; }; /** * @inheritDoc */ StateConnecting.prototype.connect = function (options) { // we're already connecting so any attempts // to connect should result in an error const callback = options.callback; process.nextTick(function () { callback(Errors.createClientError( ErrorCodes.ERR_CONN_CONNECT_STATUS_CONNECTING)); }); }; /** * @inheritDoc */ StateConnecting.prototype.continue = function () { const context = this.context; const err = context.options.err; let json = context.options.json; // if no json was specified, treat this as the first connect // and get the necessary information from connectionConfig if (!json) { json = { data: { ACCOUNT_NAME: this.connectionConfig.account, LOGIN_NAME: this.connectionConfig.username, PASSWORD: this.connectionConfig.password } }; } // extract the inflight context from the error and put it back in the json if (err && err.data && err.data.inFlightCtx) { json.inFlightCtx = err.data.inFlightCtx; } // initialize the json data if necessary json.data = json.data || {}; // add the client-app-id, client-app-version, and client-app-name const clientInfo = { CLIENT_APP_ID: this.connectionConfig.getClientType(), CLIENT_APP_VERSION: this.connectionConfig.getClientVersion(), }; // if we have some information about the client environment, add it as well const clientEnvironment = this.connectionConfig.getClientEnvironment(); if (Util.isObject(clientEnvironment)) { clientInfo.CLIENT_ENVIRONMENT = clientEnvironment; } const clientApplication = this.connectionConfig.getClientApplication(); if (Util.isString(clientApplication)) { clientEnvironment['APPLICATION'] = clientApplication; } const sessionParameters = { SESSION_PARAMETERS: {} }; if (Util.exists(this.connectionConfig.getClientSessionKeepAlive())) { sessionParameters.SESSION_PARAMETERS.CLIENT_SESSION_KEEP_ALIVE = this.connectionConfig.getClientSessionKeepAlive(); } if (Util.exists(this.connectionConfig.getClientSessionKeepAliveHeartbeatFrequency())) { sessionParameters.SESSION_PARAMETERS.CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY = this.connectionConfig.getClientSessionKeepAliveHeartbeatFrequency(); } if (Util.exists(this.connectionConfig.getJsTreatIntegerAsBigInt())) { sessionParameters.SESSION_PARAMETERS.JS_TREAT_INTEGER_AS_BIGINT = this.connectionConfig.getJsTreatIntegerAsBigInt(); } if (Util.exists(this.connectionConfig.getGcsUseDownscopedCredential())) { sessionParameters.SESSION_PARAMETERS.GCS_USE_DOWNSCOPED_CREDENTIAL = this.connectionConfig.getGcsUseDownscopedCredential(); } if (Util.exists(this.connectionConfig.getClientRequestMFAToken())) { sessionParameters.SESSION_PARAMETERS.CLIENT_REQUEST_MFA_TOKEN = this.connectionConfig.getClientRequestMFAToken(); } if (Util.exists(this.connectionConfig.getClientStoreTemporaryCredential())) { sessionParameters.SESSION_PARAMETERS.CLIENT_STORE_TEMPORARY_CREDENTIAL = this.connectionConfig.getClientStoreTemporaryCredential(); } Util.apply(json.data, clientInfo); Util.apply(json.data, sessionParameters); const connectionConfig = this.connectionConfig; const maxLoginRetries = connectionConfig.getRetrySfMaxLoginRetries(); const maxRetryTimeout = connectionConfig.getRetryTimeout(); const startTime = connectionConfig.accessUrl.startsWith('https://') ? Date.now() : 'FIXEDTIMESTAMP'; let numRetries = 0; let sleep = connectionConfig.getRetrySfStartingSleepTime(); let totalElapsedTime = 0; Logger.getInstance().debug('Total retryTimeout is for the retries = ' + maxRetryTimeout === 0 ? 'unlimited' : maxRetryTimeout); const parent = this; const requestCallback = async function (err, body) { // clear credential-related information connectionConfig.clearCredentials(); // if the request succeeded if (!err) { Errors.assertInternal(Util.exists(body)); Errors.assertInternal(Util.exists(body.data)); parent.snowflakeService.setSessionId(body.data.sessionId); Logger.getInstance().debug(`New session with id ${parent.snowflakeService.getSessionId()} initialized`); // update the parameters Parameters.update(body.data.parameters); // update all token-related information parent.tokenInfo.update(body.data); if (connectionConfig.getClientRequestMFAToken() && body.data.mfaToken) { const key = Util.buildCredentialCacheKey(connectionConfig.host, connectionConfig.username, AuthenticationTypes.USER_PWD_MFA_AUTHENTICATOR); await GlobalConfig.getCredentialManager().write(key, body.data.mfaToken); } if (connectionConfig.getClientStoreTemporaryCredential() && body.data.idToken) { const key = Util.buildCredentialCacheKey(connectionConfig.host, connectionConfig.username, AuthenticationTypes.ID_TOKEN_AUTHENTICATOR); await GlobalConfig.getCredentialManager().write(key, body.data.idToken); } // we're now connected parent.snowflakeService.transitionToConnected(); const qccSize = Parameters.getValue('QUERY_CONTEXT_CACHE_SIZE'); parent.snowflakeService.initializeQueryContextCache(qccSize); } else { if (Errors.isNetworkError(err) || Errors.isRequestFailedError(err)) { if (numRetries < maxLoginRetries && ( isRetryableNetworkError(err) || isRetryableHttpError(err)) && (maxRetryTimeout === 0 || totalElapsedTime < maxRetryTimeout)) { numRetries++; const jitter = Util.getJitteredSleepTime(numRetries, sleep, totalElapsedTime, maxRetryTimeout); sleep = jitter.sleep; totalElapsedTime = jitter.totalElapsedTime; if (sleep <= 0) { Logger.getInstance().debug('Reached out to the max Login Timeout'); parent.snowflakeService.transitionToDisconnected(); } const auth = parent.snowflakeService.getAuthenticator(); if (auth instanceof AuthOkta) { Logger.getInstance().debug('OKTA authentication requires token refresh.'); const retryOption = { totalElapsedTime, numRetries, }; auth.reauthenticate(context.options.json, retryOption).then(() => { numRetries = retryOption.numRetries; totalElapsedTime = retryOption.totalElapsedTime; setTimeout(sendRequest, sleep * 1000); return; }); } else { if (auth instanceof AuthKeypair) { Logger.getInstance().debug('AuthKeyPair authentication requires token refresh.'); await auth.reauthenticate(context.options.json); } setTimeout(sendRequest, sleep * 1000); return; } } else { Logger.getInstance().debug('Failed to all retries to SF.'); // we're now disconnected parent.snowflakeService.transitionToDisconnected(); } } else { // we're now disconnected parent.snowflakeService.transitionToDisconnected(); } } // invoke the transition-context callback that was passed to us by the // Pristine state on connect() if (Util.isFunction(context.options.callback)) { context.options.callback(err); } // all queued operations are now free to go parent.snowflakeService.drainOperationQueue(); }; // issue a login request const sendRequest = function () { const targetUrl = buildLoginUrl(connectionConfig); Logger.getInstance().debug( 'Contacting SF: %s, (%s/%s)', targetUrl, numRetries, maxLoginRetries); const request = parent.createUnauthenticatedRequest({ method: 'POST', url: targetUrl, json: json, scope: this, startTime: startTime, retry: numRetries, callback: requestCallback }); request.send(); }; sendRequest(); }; /** * Builds the url for a login request. * * @param connectionConfig * * @returns {*} */ function buildLoginUrl(connectionConfig) { const queryParams = [ { name: 'warehouse', value: connectionConfig.getWarehouse() }, { name: 'databaseName', value: connectionConfig.getDatabase() }, { name: 'schemaName', value: connectionConfig.getSchema() }, { name: 'roleName', value: connectionConfig.getRole() } ]; const queryStringObject = {}; if (!connectionConfig.isQaMode()) { // no requestId is attached to login-request in test mode. queryStringObject.requestId = uuidv4(); } for (let index = 0, length = queryParams.length; index < length; index++) { const queryParam = queryParams[index]; if (Util.string.isNotNullOrEmpty(queryParam.value)) { queryStringObject[queryParam.name] = queryParam.value; } } return Url.format( { pathname: '/session/v1/login-request', search: QueryString.stringify(queryStringObject) }); } /** * @inheritDoc */ StateConnecting.prototype.request = function (options) { // enqueue the request operation this.snowflakeService.enqueueRequest(options); }; /** * @inheritDoc */ StateConnecting.prototype.destroy = function (options) { // enqueue the destroy operation this.snowflakeService.enqueueDestroy(options); }; /////////////////////////////////////////////////////////////////////////// //// StateConnected //// /////////////////////////////////////////////////////////////////////////// function StateConnected() { StateAbstract.apply(this, arguments); } Util.inherits(StateConnected, StateAbstract); /** * @inheritDoc */ StateConnected.prototype.connect = function (options) { // we're already connected so any attempts // to connect should result in an error const callback = options.callback; process.nextTick(function () { callback(Errors.createClientError( ErrorCodes.ERR_CONN_CONNECT_STATUS_CONNECTED)); }); }; StateConnected.prototype.requestAsync = async function (options) { // create a session token request from the options and send out the request return await this.createSessionTokenRequest(options).sendAsync(); }; /** * @inheritDoc */ StateConnected.prototype.request = function (options) { const scopeOrig = options.scope; const callbackOrig = options.callback; // define our own scope and callback options.scope = this; options.callback = async function (err, body) { // if there was no error, invoke the callback if one was specified if (!err) { if (Util.isFunction(callbackOrig)) { await callbackOrig.apply(scopeOrig, [err, body]); } } else { // restore the original scope and callback to the options object because // we might need to repeat the request options.scope = scopeOrig; options.callback = callbackOrig; // if the session token has expired if (err.code === GSErrors.code.SESSION_TOKEN_EXPIRED) { // enqueue the request operation this.snowflakeService.enqueueRequest(options); // if a session token renewal isn't already in progress, issue a // request to renew the session token this.snowflakeService.transitionToRenewing(); } else if ((err.code === GSErrors.code.SESSION_TOKEN_INVALID) || (err.code === GSErrors.code.GONE_SESSION)) { // if the session token is invalid or it doesn't exist // enqueue the request operation this.snowflakeService.enqueueRequest(options); // we're disconnected this.snowflakeService.transitionToDisconnected(); // all queued operations are now free to go this.snowflakeService.drainOperationQueue(); // TODO: remember that a session renewal is no longer in progress // TODO: make sure the last session renewal did not time out } else { // it's a normal failure // if a callback was specified, invoke it if (Util.isFunction(callbackOrig)) { callbackOrig.apply(scopeOrig, [err, body]); } } } }; // create a session token request from the options and send out the request this.createSessionTokenRequest(options).send(); }; /** * @inheritDoc */ StateConnected.prototype.destroy = function (options) { const requestID = uuidv4(); // send out a session token request to terminate the current connection this.createSessionTokenRequest( { method: 'POST', url: `/session?delete=true&requestId=${requestID}`, scope: this, callback: function (err) { // if the destroy request succeeded or the session already expired, we're disconnected if (!err || err.code === GSErrors.code.GONE_SESSION || err.code === GSErrors.code.SESSION_TOKEN_EXPIRED) { err = undefined; this.snowflakeService.transitionToDisconnected(); } // invoke the original callback options.callback(err); } }).send(); }; /////////////////////////////////////////////////////////////////////////// //// StateRenewing //// /////////////////////////////////////////////////////////////////////////// function StateRenewing() { StateAbstract.apply(this, arguments); } Util.inherits(StateRenewing, StateAbstract); /** * @inheritDoc */ StateRenewing.prototype.enter = function () { // send out a master token request to renew the current session token this.createMasterTokenRequest( { method: 'POST', url: '/session/token-request', headers: { CLIENT_APP_ID: this.connectionConfig.getClientType(), CLIENT_APP_VERSION: this.connectionConfig.getClientVersion(), }, json: { 'REQUEST_TYPE': 'RENEW', 'oldSessionToken': this.tokenInfo.getSessionToken(), }, scope: this, callback: function (err, body) { // if the request succeeded if (!err) { // update the token information this.tokenInfo.update(body.data); // we're now connected again this.snowflakeService.transitionToConnected(); } else { // if the master token has expired, transition to the disconnected // state if (err.code === GSErrors.code.MASTER_TOKEN_EXPIRED) { this.snowflakeService.transitionToDisconnected(); } else if (Errors.isNetworkError(err)) { // go back to the connected state this.snowflakeService.transitionToConnected(); } else { // if the renewal failed for some other reason, we're // disconnected // TODO: what should our state be here? also disconnected? this.snowflakeService.transitionToDisconnected(); } } // all queued operations are now free to go this.snowflakeService.drainOperationQueue(); } }).send(); }; /** * @inheritDoc */ StateRenewing.prototype.connect = function (options) { // we're renewing the session token, which means we're connected, // so any attempts to connect should result in an error const callback = options.callback; process.nextTick(function () { callback(Errors.createClientError( ErrorCodes.ERR_CONN_CONNECT_STATUS_CONNECTED)); }); }; /** * @inheritDoc */ StateRenewing.prototype.request = function (options) { // enqueue the request operation this.snowflakeService.enqueueRequest(options); }; /** * @inheritDoc */ StateRenewing.prototype.destroy = function (options) { // enqueue the destroy operation this.snowflakeService.enqueueDestroy(options); }; /////////////////////////////////////////////////////////////////////////// //// StateDisconnected //// /////////////////////////////////////////////////////////////////////////// function StateDisconnected() { StateAbstract.apply(this, arguments); } Util.inherits(StateDisconnected, StateAbstract); /** * @inheritDoc */ StateDisconnected.prototype.connect = function (options) { // we're disconnected -- and fatally so -- so any // attempts to connect should result in an error const callback = options.callback; process.nextTick(function () { callback(Errors.createClientError( ErrorCodes.ERR_CONN_CONNECT_STATUS_DISCONNECTED)); }); }; /** * @inheritDoc */ StateDisconnected.prototype.request = function (options) { // we're disconnected, so any attempts to // send a request should result in an error const callback = options.callback; process.nextTick(function () { callback(Errors.createClientError( ErrorCodes.ERR_CONN_REQUEST_STATUS_DISCONNECTED, true)); }); }; /** * @inheritDoc */ StateDisconnected.prototype.destroy = function (options) { // we're already disconnected so any attempts // to destroy should result in an error const callback = options.callback; process.nextTick(function () { callback(Errors.createClientError( ErrorCodes.ERR_CONN_DESTROY_STATUS_DISCONNECTED)); }); }; /** * Creates a TokenInfo object that encapsulates all token-related information, * e.g. the master token, the session token, the tokens' expiration times, etc. * * @param {Object} [config] * * @constructor */ function TokenInfo(config) { let masterToken; let sessionToken; let masterTokenExpirationTime; let sessionTokenExpirationTime; if (Util.isObject(config)) { masterToken = config.masterToken; sessionToken = config.sessionToken; masterTokenExpirationTime = config.masterTokenExpirationTime; sessionTokenExpirationTime = config.sessionTokenExpirationTime; } /** * Returns true if no token-related information is available, false otherwise. * * @returns {Boolean} */ this.isEmpty = function () { return !Util.exists(masterToken) || !Util.exists(masterTokenExpirationTime) || !Util.exists(sessionToken) || !Util.exists(sessionTokenExpirationTime); }; /** * Clears all token-related information. */ this.clearTokens = function () { masterToken = undefined; masterTokenExpirationTime = undefined; sessionToken = undefined; sessionTokenExpirationTime = undefined; }; /** * Updates the tokens and their expiration times. * * @param {Object} data */ this.update = function (data) { masterToken = data.masterToken; sessionToken = data.token || data.sessionToken; const currentTime = new Date().getTime(); masterTokenExpirationTime = currentTime + 1000 * (data.masterValidityInSeconds || data.validityInSecondsMT); sessionTokenExpirationTime = currentTime + 1000 * (data.validityInSeconds || data.validityInSecondsST); }; /** * Returns the master token. * * @returns {String} */ this.getMasterToken = function () { return masterToken; }; /** * Returns the expiration time of the master token. * * @returns {Number} */ this.getMasterTokenExpirationTime = function () { return masterTokenExpirationTime; }; /** * Returns the session token. * * @returns {String} */ this.getSessionToken = function () { return sessionToken; }; /** * Returns the expiration time of the session token. * * @returns {Number} */ this.getSessionTokenExpirationTime = function () { return sessionTokenExpirationTime; }; /** * Returns a configuration object that can be passed to the TokenInfo * constructor to get an equivalent TokenInfo object. * * @returns {Object} */ this.getConfig = function () { return { masterToken: masterToken, masterTokenExpirationTime: masterTokenExpirationTime, sessionToken: sessionToken, sessionTokenExpirationTime: sessionTokenExpirationTime }; }; }