UNPKG

tedious

Version:

A TDS driver, for connecting to MS SQLServer databases.

1,398 lines (1,141 loc) 76.7 kB
"use strict"; const crypto = require('crypto'); const os = require('os'); // $FlowFixMe const constants = require('constants'); const _require = require('tls'), createSecureContext = _require.createSecureContext; const _require2 = require('adal-node'), AuthenticationContext = _require2.AuthenticationContext; const BulkLoad = require('./bulk-load'); const Debug = require('./debug'); const EventEmitter = require('events').EventEmitter; const InstanceLookup = require('./instance-lookup').InstanceLookup; const TransientErrorLookup = require('./transient-error-lookup.js').TransientErrorLookup; const TYPE = require('./packet').TYPE; const PreloginPayload = require('./prelogin-payload'); const Login7Payload = require('./login7-payload'); const NTLMResponsePayload = require('./ntlm-payload'); const Request = require('./request'); const RpcRequestPayload = require('./rpcrequest-payload'); const SqlBatchPayload = require('./sqlbatch-payload'); const MessageIO = require('./message-io'); const TokenStreamParser = require('./token/token-stream-parser').Parser; const Transaction = require('./transaction').Transaction; const ISOLATION_LEVEL = require('./transaction').ISOLATION_LEVEL; const ConnectionError = require('./errors').ConnectionError; const RequestError = require('./errors').RequestError; const Connector = require('./connector').Connector; const libraryName = require('./library').name; const versions = require('./tds-versions').versions; const _require3 = require('./ntlm'), createNTLMRequest = _require3.createNTLMRequest; // A rather basic state machine for managing a connection. // Implements something approximating s3.2.1. const KEEP_ALIVE_INITIAL_DELAY = 30 * 1000; const DEFAULT_CONNECT_TIMEOUT = 15 * 1000; const DEFAULT_CLIENT_REQUEST_TIMEOUT = 15 * 1000; const DEFAULT_CANCEL_TIMEOUT = 5 * 1000; const DEFAULT_CONNECT_RETRY_INTERVAL = 500; const DEFAULT_PACKET_SIZE = 4 * 1024; const DEFAULT_TEXTSIZE = '2147483647'; const DEFAULT_DATEFIRST = 7; const DEFAULT_PORT = 1433; const DEFAULT_TDS_VERSION = '7_4'; const DEFAULT_LANGUAGE = 'us_english'; const DEFAULT_DATEFORMAT = 'mdy'; class Connection extends EventEmitter { constructor(config) { super(); if (typeof config !== 'object' || config === null) { throw new TypeError('The "config" argument is required and must be of type Object.'); } if (typeof config.server !== 'string') { throw new TypeError('The "config.server" property is required and must be of type string.'); } this.fedAuthRequired = false; this.fedAuthInfoToken = undefined; let authentication; if (config.authentication !== undefined) { if (typeof config.authentication !== 'object' || config.authentication === null) { throw new TypeError('The "config.authentication" property must be of type Object.'); } const type = config.authentication.type; const options = config.authentication.options === undefined ? {} : config.authentication.options; if (typeof type !== 'string') { throw new TypeError('The "config.authentication.type" property must be of type string.'); } if (type !== 'default' && type !== 'ntlm' && type !== 'azure-active-directory-password' && type !== 'azure-active-directory-access-token') { throw new TypeError('The "type" property must one of "default", "ntlm", "azure-active-directory-password" or "azure-active-directory-access-token".'); } if (typeof options !== 'object' || options === null) { throw new TypeError('The "config.authentication.options" property must be of type object.'); } if (type === 'ntlm') { if (typeof options.domain !== 'string') { throw new TypeError('The "config.authentication.options.domain" property must be of type string.'); } if (options.userName !== undefined && typeof options.userName !== 'string') { throw new TypeError('The "config.authentication.options.userName" property must be of type string.'); } if (options.password !== undefined && typeof options.password !== 'string') { throw new TypeError('The "config.authentication.options.password" property must be of type string.'); } authentication = { type: 'ntlm', options: { userName: options.userName, password: options.password, domain: options.domain && options.domain.toUpperCase() } }; } else if (type === 'azure-active-directory-password') { if (options.userName !== undefined && typeof options.userName !== 'string') { throw new TypeError('The "config.authentication.options.userName" property must be of type string.'); } if (options.password !== undefined && typeof options.password !== 'string') { throw new TypeError('The "config.authentication.options.password" property must be of type string.'); } authentication = { type: 'azure-active-directory-password', options: { userName: options.userName, password: options.password } }; } else if (type === 'azure-active-directory-access-token') { if (typeof options.token !== 'string') { throw new TypeError('The "config.authentication.options.token" property must be of type string.'); } authentication = { type: 'azure-active-directory-password', options: { token: options.token } }; } else { if (options.userName !== undefined && typeof options.userName !== 'string') { throw new TypeError('The "config.authentication.options.userName" property must be of type string.'); } if (options.password !== undefined && typeof options.password !== 'string') { throw new TypeError('The "config.authentication.options.password" property must be of type string.'); } authentication = { type: 'default', options: { userName: options.userName, password: options.password } }; } } else { authentication = { type: 'default', options: { userName: undefined, password: undefined } }; } this.config = { server: config.server, authentication: authentication, options: { abortTransactionOnError: false, appName: undefined, camelCaseColumns: false, cancelTimeout: DEFAULT_CANCEL_TIMEOUT, columnNameReplacer: undefined, connectionRetryInterval: DEFAULT_CONNECT_RETRY_INTERVAL, connectTimeout: DEFAULT_CONNECT_TIMEOUT, connectionIsolationLevel: ISOLATION_LEVEL.READ_COMMITTED, cryptoCredentialsDetails: {}, database: undefined, datefirst: DEFAULT_DATEFIRST, dateFormat: DEFAULT_DATEFORMAT, debug: { data: false, packet: false, payload: false, token: false }, enableAnsiNull: true, enableAnsiNullDefault: true, enableAnsiPadding: true, enableAnsiWarnings: true, enableArithAbort: false, enableConcatNullYieldsNull: true, enableCursorCloseOnCommit: null, enableImplicitTransactions: false, enableNumericRoundabort: false, enableQuotedIdentifier: true, encrypt: false, fallbackToDefaultDb: false, instanceName: undefined, isolationLevel: ISOLATION_LEVEL.READ_COMMITTED, language: DEFAULT_LANGUAGE, localAddress: undefined, maxRetriesOnTransientErrors: 3, multiSubnetFailover: false, packetSize: DEFAULT_PACKET_SIZE, port: DEFAULT_PORT, readOnlyIntent: false, requestTimeout: DEFAULT_CLIENT_REQUEST_TIMEOUT, rowCollectionOnDone: false, rowCollectionOnRequestCompletion: false, tdsVersion: DEFAULT_TDS_VERSION, textsize: DEFAULT_TEXTSIZE, trustServerCertificate: true, useColumnNames: false, useUTC: true } }; if (config.options) { if (config.options.port && config.options.instanceName) { throw new Error('Port and instanceName are mutually exclusive, but ' + config.options.port + ' and ' + config.options.instanceName + ' provided'); } if (config.options.abortTransactionOnError !== undefined) { if (typeof config.options.abortTransactionOnError !== 'boolean' && config.options.abortTransactionOnError !== null) { throw new TypeError('The "config.options.abortTransactionOnError" property must be of type string or null.'); } this.config.options.abortTransactionOnError = config.options.abortTransactionOnError; } if (config.options.appName !== undefined) { if (typeof config.options.appName !== 'string') { throw new TypeError('The "config.options.appName" property must be of type string.'); } this.config.options.appName = config.options.appName; } if (config.options.camelCaseColumns !== undefined) { if (typeof config.options.camelCaseColumns !== 'boolean') { throw new TypeError('The "config.options.camelCaseColumns" property must be of type boolean.'); } this.config.options.camelCaseColumns = config.options.camelCaseColumns; } if (config.options.cancelTimeout !== undefined) { if (typeof config.options.cancelTimeout !== 'number') { throw new TypeError('The "config.options.cancelTimeout" property must be of type number.'); } this.config.options.cancelTimeout = config.options.cancelTimeout; } if (config.options.columnNameReplacer) { if (typeof config.options.columnNameReplacer !== 'function') { throw new TypeError('The "config.options.cancelTimeout" property must be of type function.'); } this.config.options.columnNameReplacer = config.options.columnNameReplacer; } if (config.options.connectTimeout !== undefined) { if (typeof config.options.connectTimeout !== 'number') { throw new TypeError('The "config.options.connectTimeout" property must be of type number.'); } this.config.options.connectTimeout = config.options.connectTimeout; } if (config.options.connectionIsolationLevel !== undefined) { this.config.options.connectionIsolationLevel = config.options.connectionIsolationLevel; } if (config.options.connectTimeout !== undefined) { if (typeof config.options.connectTimeout !== 'number') { throw new TypeError('The "config.options.connectTimeout" property must be of type number.'); } this.config.options.connectTimeout = config.options.connectTimeout; } if (config.options.cryptoCredentialsDetails !== undefined) { if (typeof config.options.cryptoCredentialsDetails !== 'object' || config.options.cryptoCredentialsDetails === null) { throw new TypeError('The "config.options.cryptoCredentialsDetails" property must be of type Object.'); } this.config.options.cryptoCredentialsDetails = config.options.cryptoCredentialsDetails; } if (config.options.database !== undefined) { if (typeof config.options.database !== 'string') { throw new TypeError('The "config.options.database" property must be of type string.'); } this.config.options.database = config.options.database; } if (config.options.datefirst !== undefined) { if (typeof config.options.datefirst !== 'number' && config.options.datefirst !== null) { throw new TypeError('The "config.options.datefirst" property must be of type number.'); } if (config.options.datefirst !== null && (config.options.datefirst < 1 || config.options.datefirst > 7)) { throw new RangeError('The "config.options.datefirst" property must be >= 1 and <= 7'); } this.config.options.datefirst = config.options.datefirst; } if (config.options.dateFormat !== undefined) { if (typeof config.options.dateFormat !== 'string' && config.options.dateFormat !== null) { throw new TypeError('The "config.options.dateFormat" property must be of type string or null.'); } this.config.options.dateFormat = config.options.dateFormat; } if (config.options.debug) { if (config.options.debug.data !== undefined) { if (typeof config.options.debug.data !== 'boolean') { throw new TypeError('The "config.options.debug.data" property must be of type boolean.'); } this.config.options.debug.data = config.options.debug.data; } if (config.options.debug.packet !== undefined) { if (typeof config.options.debug.packet !== 'boolean') { throw new TypeError('The "config.options.debug.packet" property must be of type boolean.'); } this.config.options.debug.packet = config.options.debug.packet; } if (config.options.debug.payload !== undefined) { if (typeof config.options.debug.payload !== 'boolean') { throw new TypeError('The "config.options.debug.payload" property must be of type boolean.'); } this.config.options.debug.payload = config.options.debug.payload; } if (config.options.debug.token !== undefined) { if (typeof config.options.debug.token !== 'boolean') { throw new TypeError('The "config.options.debug.token" property must be of type boolean.'); } this.config.options.debug.token = config.options.debug.token; } } if (config.options.enableAnsiNull !== undefined) { if (typeof config.options.enableAnsiNull !== 'boolean' && config.options.enableAnsiNull !== null) { throw new TypeError('The "config.options.enableAnsiNull" property must be of type boolean or null.'); } this.config.options.enableAnsiNull = config.options.enableAnsiNull; } if (config.options.enableAnsiNullDefault !== undefined) { if (typeof config.options.enableAnsiNullDefault !== 'boolean' && config.options.enableAnsiNullDefault !== null) { throw new TypeError('The "config.options.enableAnsiNullDefault" property must be of type boolean or null.'); } this.config.options.enableAnsiNullDefault = config.options.enableAnsiNullDefault; } if (config.options.enableAnsiPadding !== undefined) { if (typeof config.options.enableAnsiPadding !== 'boolean' && config.options.enableAnsiPadding !== null) { throw new TypeError('The "config.options.enableAnsiPadding" property must be of type boolean or null.'); } this.config.options.enableAnsiPadding = config.options.enableAnsiPadding; } if (config.options.enableAnsiWarnings !== undefined) { if (typeof config.options.enableAnsiWarnings !== 'boolean' && config.options.enableAnsiWarnings !== null) { throw new TypeError('The "config.options.enableAnsiWarnings" property must be of type boolean or null.'); } this.config.options.enableAnsiWarnings = config.options.enableAnsiWarnings; } if (config.options.enableArithAbort !== undefined) { if (typeof config.options.enableArithAbort !== 'boolean' && config.options.enableArithAbort !== null) { throw new TypeError('The "config.options.enableArithAbort" property must be of type boolean or null.'); } this.config.options.enableArithAbort = config.options.enableArithAbort; } if (config.options.enableConcatNullYieldsNull !== undefined) { if (typeof config.options.enableConcatNullYieldsNull !== 'boolean' && config.options.enableConcatNullYieldsNull !== null) { throw new TypeError('The "config.options.enableConcatNullYieldsNull" property must be of type boolean or null.'); } this.config.options.enableConcatNullYieldsNull = config.options.enableConcatNullYieldsNull; } if (config.options.enableCursorCloseOnCommit !== undefined) { if (typeof config.options.enableCursorCloseOnCommit !== 'boolean' && config.options.enableCursorCloseOnCommit !== null) { throw new TypeError('The "config.options.enableCursorCloseOnCommit" property must be of type boolean or null.'); } this.config.options.enableCursorCloseOnCommit = config.options.enableCursorCloseOnCommit; } if (config.options.enableImplicitTransactions !== undefined) { if (typeof config.options.enableImplicitTransactions !== 'boolean' && config.options.enableImplicitTransactions !== null) { throw new TypeError('The "config.options.enableImplicitTransactions" property must be of type boolean or null.'); } this.config.options.enableImplicitTransactions = config.options.enableImplicitTransactions; } if (config.options.enableNumericRoundabort !== undefined) { if (typeof config.options.enableNumericRoundabort !== 'boolean' && config.options.enableNumericRoundabort !== null) { throw new TypeError('The "config.options.enableNumericRoundabort" property must be of type boolean or null.'); } this.config.options.enableNumericRoundabort = config.options.enableNumericRoundabort; } if (config.options.enableQuotedIdentifier !== undefined) { if (typeof config.options.enableQuotedIdentifier !== 'boolean' && config.options.enableQuotedIdentifier !== null) { throw new TypeError('The "config.options.enableQuotedIdentifier" property must be of type boolean or null.'); } this.config.options.enableQuotedIdentifier = config.options.enableQuotedIdentifier; } if (config.options.encrypt !== undefined) { if (typeof config.options.encrypt !== 'boolean') { throw new TypeError('The "config.options.encrypt" property must be of type boolean.'); } this.config.options.encrypt = config.options.encrypt; } else { this.config.options.encrypt = true; } if (config.options.fallbackToDefaultDb !== undefined) { if (typeof config.options.fallbackToDefaultDb !== 'boolean') { throw new TypeError('The "config.options.fallbackToDefaultDb" property must be of type boolean.'); } this.config.options.fallbackToDefaultDb = config.options.fallbackToDefaultDb; } if (config.options.instanceName !== undefined) { if (typeof config.options.instanceName !== 'string') { throw new TypeError('The "config.options.instanceName" property must be of type string.'); } this.config.options.instanceName = config.options.instanceName; this.config.options.port = undefined; } if (config.options.isolationLevel !== undefined) { if (typeof config.options.isolationLevel !== 'number') { throw new TypeError('The "config.options.language" property must be of type numer.'); } this.config.options.isolationLevel = config.options.isolationLevel; } if (config.options.language !== undefined) { if (typeof config.options.language !== 'string' && config.options.language !== null) { throw new TypeError('The "config.options.language" property must be of type string or null.'); } this.config.options.language = config.options.language; } if (config.options.localAddress !== undefined) { if (typeof config.options.localAddress !== 'string') { throw new TypeError('The "config.options.localAddress" property must be of type string.'); } this.config.options.localAddress = config.options.localAddress; } if (config.options.multiSubnetFailover !== undefined) { if (typeof config.options.multiSubnetFailover !== 'boolean') { throw new TypeError('The "config.options.multiSubnetFailover" property must be of type boolean.'); } this.config.options.multiSubnetFailover = config.options.multiSubnetFailover; } if (config.options.packetSize !== undefined) { if (typeof config.options.packetSize !== 'number') { throw new TypeError('The "config.options.packetSize" property must be of type number.'); } this.config.options.packetSize = config.options.packetSize; } if (config.options.port !== undefined) { if (typeof config.options.port !== 'number') { throw new TypeError('The "config.options.port" property must be of type number.'); } if (config.options.port <= 0 || config.options.port >= 65536) { throw new RangeError('The "config.options.port" property must be > 0 and < 65536'); } this.config.options.port = config.options.port; this.config.options.instanceName = undefined; } if (config.options.readOnlyIntent !== undefined) { if (typeof config.options.readOnlyIntent !== 'boolean') { throw new TypeError('The "config.options.readOnlyIntent" property must be of type boolean.'); } this.config.options.readOnlyIntent = config.options.readOnlyIntent; } if (config.options.requestTimeout !== undefined) { if (typeof config.options.requestTimeout !== 'number') { throw new TypeError('The "config.options.requestTimeout" property must be of type number.'); } this.config.options.requestTimeout = config.options.requestTimeout; } if (config.options.maxRetriesOnTransientErrors !== undefined) { if (typeof config.options.maxRetriesOnTransientErrors !== 'number') { throw new TypeError('The "config.options.maxRetriesOnTransientErrors" property must be of type number.'); } if (config.options.maxRetriesOnTransientErrors < 0) { throw new TypeError('The "config.options.maxRetriesOnTransientErrors" property must be equal or greater than 0.'); } this.config.options.maxRetriesOnTransientErrors = config.options.maxRetriesOnTransientErrors; } if (config.options.connectionRetryInterval !== undefined) { if (typeof config.options.connectionRetryInterval !== 'number') { throw new TypeError('The "config.options.connectionRetryInterval" property must be of type number.'); } if (config.options.connectionRetryInterval <= 0) { throw new TypeError('The "config.options.connectionRetryInterval" property must be greater than 0.'); } this.config.options.connectionRetryInterval = config.options.connectionRetryInterval; } if (config.options.rowCollectionOnDone !== undefined) { if (typeof config.options.rowCollectionOnDone !== 'boolean') { throw new TypeError('The "config.options.rowCollectionOnDone" property must be of type boolean.'); } this.config.options.rowCollectionOnDone = config.options.rowCollectionOnDone; } if (config.options.rowCollectionOnRequestCompletion !== undefined) { if (typeof config.options.rowCollectionOnRequestCompletion !== 'boolean') { throw new TypeError('The "config.options.rowCollectionOnRequestCompletion" property must be of type boolean.'); } this.config.options.rowCollectionOnRequestCompletion = config.options.rowCollectionOnRequestCompletion; } if (config.options.tdsVersion !== undefined) { if (typeof config.options.tdsVersion !== 'string') { throw new TypeError('The "config.options.tdsVersion" property must be of type string.'); } this.config.options.tdsVersion = config.options.tdsVersion; } if (config.options.textsize !== undefined) { if (typeof config.options.textsize !== 'number' && config.options.textsize !== null) { throw new TypeError('The "config.options.textsize" property must be of type number or null.'); } this.config.options.textsize = config.options.textsize; } if (config.options.trustServerCertificate !== undefined) { if (typeof config.options.trustServerCertificate !== 'boolean') { throw new TypeError('The "config.options.trustServerCertificate" property must be of type boolean.'); } this.config.options.trustServerCertificate = config.options.trustServerCertificate; } if (config.options.useColumnNames !== undefined) { if (typeof config.options.useColumnNames !== 'boolean') { throw new TypeError('The "config.options.useColumnNames" property must be of type boolean.'); } this.config.options.useColumnNames = config.options.useColumnNames; } if (config.options.useUTC !== undefined) { if (typeof config.options.useUTC !== 'boolean') { throw new TypeError('The "config.options.useUTC" property must be of type boolean.'); } this.config.options.useUTC = config.options.useUTC; } } let credentialsDetails = this.config.options.cryptoCredentialsDetails; if (credentialsDetails.secureOptions === undefined) { // If the caller has not specified their own `secureOptions`, // we set `SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS` here. // Older SQL Server instances running on older Windows versions have // trouble with the BEAST workaround in OpenSSL. // As BEAST is a browser specific exploit, we can just disable this option here. credentialsDetails = Object.create(credentialsDetails, { secureOptions: { value: constants.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS } }); } this.secureContext = createSecureContext(credentialsDetails); this.createDebug(); this.createTokenStreamParser(); this.inTransaction = false; this.transactionDescriptors = [Buffer.from([0, 0, 0, 0, 0, 0, 0, 0])]; this.transitionTo(this.STATE.CONNECTING); if (this.config.options.tdsVersion < '7_2') { // 'beginTransaction', 'commitTransaction' and 'rollbackTransaction' // events are utilized to maintain inTransaction property state which in // turn is used in managing transactions. These events are only fired for // TDS version 7.2 and beyond. The properties below are used to emulate // equivalent behavior for TDS versions before 7.2. this.transactionDepth = 0; this.isSqlBatch = false; } this.curTransientRetryCount = 0; this.transientErrorLookup = new TransientErrorLookup(); this.cleanupTypeEnum = { NORMAL: 0, REDIRECT: 1, RETRY: 2 }; } close() { this.transitionTo(this.STATE.FINAL); } initialiseConnection() { this.connect(); this.createConnectTimer(); } cleanupConnection(cleanupTypeEnum) { if (!this.closed) { this.clearConnectTimer(); this.clearRequestTimer(); this.clearRetryTimer(); this.closeConnection(); if (cleanupTypeEnum === this.cleanupTypeEnum.REDIRECT) { this.emit('rerouting'); } else if (cleanupTypeEnum !== this.cleanupTypeEnum.RETRY) { process.nextTick(() => { this.emit('end'); }); } const request = this.request; if (request) { const err = RequestError('Connection closed before request completed.', 'ECLOSE'); request.callback(err); this.request = undefined; } this.closed = true; this.loggedIn = false; this.loginError = undefined; } } createDebug() { this.debug = new Debug(this.config.options.debug); this.debug.on('debug', message => { this.emit('debug', message); }); } createTokenStreamParser() { this.tokenStreamParser = new TokenStreamParser(this.debug, undefined, this.config.options); this.tokenStreamParser.on('infoMessage', token => { this.emit('infoMessage', token); }); this.tokenStreamParser.on('sspichallenge', token => { if (token.ntlmpacket) { this.ntlmpacket = token.ntlmpacket; this.ntlmpacketBuffer = token.ntlmpacketBuffer; } this.emit('sspichallenge', token); }); this.tokenStreamParser.on('errorMessage', token => { this.emit('errorMessage', token); if (this.loggedIn) { const request = this.request; if (request) { if (!request.canceled) { const error = new RequestError(token.message, 'EREQUEST'); error.number = token.number; error.state = token.state; error.class = token.class; error.serverName = token.serverName; error.procName = token.procName; error.lineNumber = token.lineNumber; request.error = error; } } } else { const error = ConnectionError(token.message, 'ELOGIN'); const isLoginErrorTransient = this.transientErrorLookup.isTransientError(token.number); if (isLoginErrorTransient && this.curTransientRetryCount !== this.config.options.maxRetriesOnTransientErrors) { error.isTransient = true; } this.loginError = error; } }); this.tokenStreamParser.on('databaseChange', token => { this.emit('databaseChange', token.newValue); }); this.tokenStreamParser.on('languageChange', token => { this.emit('languageChange', token.newValue); }); this.tokenStreamParser.on('charsetChange', token => { this.emit('charsetChange', token.newValue); }); this.tokenStreamParser.on('fedAuthInfo', token => { this.dispatchEvent('fedAuthInfo', token); }); this.tokenStreamParser.on('featureExtAck', token => { this.dispatchEvent('featureExtAck', token); }); this.tokenStreamParser.on('loginack', token => { if (!token.tdsVersion) { // unsupported TDS version this.loginError = ConnectionError('Server responded with unknown TDS version.', 'ETDS'); this.loggedIn = false; return; } if (!token.interface) { // unsupported interface this.loginError = ConnectionError('Server responded with unsupported interface.', 'EINTERFACENOTSUPP'); this.loggedIn = false; return; } // use negotiated version this.config.options.tdsVersion = token.tdsVersion; this.loggedIn = true; }); this.tokenStreamParser.on('routingChange', token => { this.routingData = token.newValue; this.dispatchEvent('routingChange'); }); this.tokenStreamParser.on('packetSizeChange', token => { this.messageIo.packetSize(token.newValue); }); // A new top-level transaction was started. This is not fired // for nested transactions. this.tokenStreamParser.on('beginTransaction', token => { this.transactionDescriptors.push(token.newValue); this.inTransaction = true; }); // A top-level transaction was committed. This is not fired // for nested transactions. this.tokenStreamParser.on('commitTransaction', () => { this.transactionDescriptors.length = 1; this.inTransaction = false; }); // A top-level transaction was rolled back. This is not fired // for nested transactions. This is also fired if a batch // aborting error happened that caused a rollback. this.tokenStreamParser.on('rollbackTransaction', () => { this.transactionDescriptors.length = 1; // An outermost transaction was rolled back. Reset the transaction counter this.inTransaction = false; this.emit('rollbackTransaction'); }); this.tokenStreamParser.on('columnMetadata', token => { const request = this.request; if (request) { if (!request.canceled) { let columns; if (this.config.options.useColumnNames) { columns = {}; for (let j = 0, len = token.columns.length; j < len; j++) { const col = token.columns[j]; if (columns[col.colName] == null) { columns[col.colName] = col; } } } else { columns = token.columns; } request.emit('columnMetadata', columns); } } else { this.emit('error', new Error("Received 'columnMetadata' when no sqlRequest is in progress")); this.close(); } }); this.tokenStreamParser.on('order', token => { const request = this.request; if (request) { if (!request.canceled) { request.emit('order', token.orderColumns); } } else { this.emit('error', new Error("Received 'order' when no sqlRequest is in progress")); this.close(); } }); this.tokenStreamParser.on('row', token => { const request = this.request; if (request) { if (!request.canceled) { if (this.config.options.rowCollectionOnRequestCompletion) { request.rows.push(token.columns); } if (this.config.options.rowCollectionOnDone) { request.rst.push(token.columns); } if (!(this.state === this.STATE.SENT_ATTENTION && request.paused)) { request.emit('row', token.columns); } } } else { this.emit('error', new Error("Received 'row' when no sqlRequest is in progress")); this.close(); } }); this.tokenStreamParser.on('returnStatus', token => { const request = this.request; if (request) { if (!request.canceled) { // Keep value for passing in 'doneProc' event. this.procReturnStatusValue = token.value; } } }); this.tokenStreamParser.on('returnValue', token => { const request = this.request; if (request) { if (!request.canceled) { this.request.emit('returnValue', token.paramName, token.value, token.metadata); } } }); this.tokenStreamParser.on('doneProc', token => { const request = this.request; if (request) { if (!request.canceled) { request.emit('doneProc', token.rowCount, token.more, this.procReturnStatusValue, request.rst); this.procReturnStatusValue = undefined; if (token.rowCount !== undefined) { request.rowCount += token.rowCount; } if (this.config.options.rowCollectionOnDone) { request.rst = []; } } } }); this.tokenStreamParser.on('doneInProc', token => { const request = this.request; if (request) { if (!request.canceled) { request.emit('doneInProc', token.rowCount, token.more, request.rst); if (token.rowCount !== undefined) { request.rowCount += token.rowCount; } if (this.config.options.rowCollectionOnDone) { request.rst = []; } } } }); this.tokenStreamParser.on('done', token => { const request = this.request; if (request) { if (token.attention) { this.dispatchEvent('attention'); } if (request.canceled) { // If we received a `DONE` token with `DONE_ERROR`, but no previous `ERROR` token, // We assume this is the indication that an in-flight request was canceled. if (token.sqlError && !request.error) { this.clearCancelTimer(); request.error = RequestError('Canceled.', 'ECANCEL'); } } else { if (token.sqlError && !request.error) { // check if the DONE_ERROR flags was set, but an ERROR token was not sent. request.error = RequestError('An unknown error has occurred.', 'UNKNOWN'); } request.emit('done', token.rowCount, token.more, request.rst); if (token.rowCount !== undefined) { request.rowCount += token.rowCount; } if (this.config.options.rowCollectionOnDone) { request.rst = []; } } } }); this.tokenStreamParser.on('endOfMessage', () => { // EOM pseudo token received if (this.state === this.STATE.SENT_CLIENT_REQUEST) { this.dispatchEvent('endOfMessageMarkerReceived'); } }); this.tokenStreamParser.on('resetConnection', () => { this.emit('resetConnection'); }); this.tokenStreamParser.on('tokenStreamError', error => { this.emit('error', error); this.close(); }); this.tokenStreamParser.on('drain', () => { // Bridge the release of backpressure from the token stream parser // transform to the packet stream transform. this.messageIo.resume(); }); } connect() { if (this.config.options.port) { return this.connectOnPort(this.config.options.port, this.config.options.multiSubnetFailover); } else { return new InstanceLookup().instanceLookup({ server: this.config.server, instanceName: this.config.options.instanceName, timeout: this.config.options.connectTimeout }, (message, port) => { if (this.state === this.STATE.FINAL) { return; } if (message) { this.emit('connect', ConnectionError(message, 'EINSTLOOKUP')); } else { this.connectOnPort(port, this.config.options.multiSubnetFailover); } }); } } connectOnPort(port, multiSubnetFailover) { const connectOpts = { host: this.routingData ? this.routingData.server : this.config.server, port: this.routingData ? this.routingData.port : port, localAddress: this.config.options.localAddress }; new Connector(connectOpts, multiSubnetFailover).execute((err, socket) => { if (err) { return this.socketError(err); } if (this.state === this.STATE.FINAL) { socket.destroy(); return; } this.socket = socket; this.socket.on('error', error => { this.socketError(error); }); this.socket.on('close', () => { this.socketClose(); }); this.socket.on('end', () => { this.socketEnd(); }); this.messageIo = new MessageIO(this.socket, this.config.options.packetSize, this.debug); this.messageIo.on('data', data => { this.dispatchEvent('data', data); }); this.messageIo.on('message', () => { this.dispatchEvent('message'); }); this.messageIo.on('secure', cleartext => { this.emit('secure', cleartext); }); this.socketConnect(); }); } closeConnection() { if (this.socket) { this.socket.destroy(); } } createConnectTimer() { this.connectTimer = setTimeout(() => { this.connectTimeout(); }, this.config.options.connectTimeout); } createCancelTimer() { this.clearCancelTimer(); const timeout = this.config.options.cancelTimeout; if (timeout > 0) { this.cancelTimer = setTimeout(() => { this.cancelTimeout(); }, timeout); } } createRequestTimer() { this.clearRequestTimer(); // release old timer, just to be safe const request = this.request; const timeout = request.timeout !== undefined ? request.timeout : this.config.options.requestTimeout; if (timeout) { this.requestTimer = setTimeout(() => { this.requestTimeout(); }, timeout); } } createRetryTimer() { this.clearRetryTimer(); this.retryTimer = setTimeout(() => { this.retryTimeout(); }, this.config.options.connectionRetryInterval); } connectTimeout() { const message = `Failed to connect to ${this.config.server}${this.config.options.port ? `:${this.config.options.port}` : `\\${this.config.options.instanceName}`} in ${this.config.options.connectTimeout}ms`; this.debug.log(message); this.emit('connect', ConnectionError(message, 'ETIMEOUT')); this.connectTimer = undefined; this.dispatchEvent('connectTimeout'); } cancelTimeout() { const message = `Failed to cancel request in ${this.config.options.cancelTimeout}ms`; this.debug.log(message); this.dispatchEvent('socketError', ConnectionError(message, 'ETIMEOUT')); } requestTimeout() { this.requestTimer = undefined; const request = this.request; request.cancel(); const timeout = request.timeout !== undefined ? request.timeout : this.config.options.requestTimeout; const message = 'Timeout: Request failed to complete in ' + timeout + 'ms'; request.error = RequestError(message, 'ETIMEOUT'); } retryTimeout() { this.retryTimer = undefined; this.emit('retry'); this.transitionTo(this.STATE.CONNECTING); } clearConnectTimer() { if (this.connectTimer) { clearTimeout(this.connectTimer); } } clearCancelTimer() { if (this.cancelTimer) { clearTimeout(this.cancelTimer); } } clearRequestTimer() { if (this.requestTimer) { clearTimeout(this.requestTimer); this.requestTimer = undefined; } } clearRetryTimer() { if (this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = undefined; } } transitionTo(newState) { if (this.state === newState) { this.debug.log('State is already ' + newState.name); return; } if (this.state && this.state.exit) { this.state.exit.call(this, newState); } this.debug.log('State change: ' + (this.state ? this.state.name : 'undefined') + ' -> ' + newState.name); this.state = newState; if (this.state.enter) { this.state.enter.apply(this); } } dispatchEvent(eventName, ...args) { if (this.state.events[eventName]) { this.state.events[eventName].apply(this, args); } else { this.emit('error', new Error(`No event '${eventName}' in state '${this.state.name}'`)); this.close(); } } socketError(error) { if (this.state === this.STATE.CONNECTING || this.state === this.STATE.SENT_TLSSSLNEGOTIATION) { const message = `Failed to connect to ${this.config.server}:${this.config.options.port} - ${error.message}`; this.debug.log(message); this.emit('connect', ConnectionError(message, 'ESOCKET')); } else { const message = `Connection lost - ${error.message}`; this.debug.log(message); this.emit('error', ConnectionError(message, 'ESOCKET')); } this.dispatchEvent('socketError', error); } socketConnect() { this.socket.setKeepAlive(true, KEEP_ALIVE_INITIAL_DELAY); this.closed = false; this.debug.log('connected to ' + this.config.server + ':' + this.config.options.port); this.dispatchEvent('socketConnect'); } socketEnd() { this.debug.log('socket ended'); if (this.state !== this.STATE.FINAL) { const error = new Error('socket hang up'); error.code = 'ECONNRESET'; this.socketError(error); } } socketClose() { this.debug.log('connection to ' + this.config.server + ':' + this.config.options.port + ' closed'); if (this.state === this.STATE.REROUTING) { this.debug.log('Rerouting to ' + this.routingData.server + ':' + this.routingData.port); this.dispatchEvent('reconnect'); } else if (this.state === this.STATE.TRANSIENT_FAILURE_RETRY) { const server = this.routingData ? this.routingData.server : this.server; const port = this.routingData ? this.routingData.port : this.config.options.port; this.debug.log('Retry after transient failure connecting to ' + server + ':' + port); this.dispatchEvent('retry'); } else { this.transitionTo(this.STATE.FINAL); } } sendPreLogin() { const payload = new PreloginPayload({ encrypt: this.config.options.encrypt }); this.messageIo.sendMessage(TYPE.PRELOGIN, payload.data); this.debug.payload(function () { return payload.toString(' '); }); } emptyMessageBuffer() { this.messageBuffer = Buffer.alloc(0); } addToMessageBuffer(data) { this.messageBuffer = Buffer.concat([this.messageBuffer, data]); } sendLogin7Packet() { const payload = new Login7Payload({ tdsVersion: versions[this.config.options.tdsVersion], packetSize: this.config.options.packetSize, clientProgVer: 0, clientPid: process.pid, connectionId: 0, clientTimeZone: new Date().getTimezoneOffset(), clientLcid: 0x00000409 }); const authentication = this.config.authentication; switch (authentication.type) { case 'azure-active-directory-password': payload.fedAuth = { type: 'ADAL', echo: this.fedAuthRequired, workflow: 'default' }; break; case 'azure-active-directory-access-token': payload.fedAuth = { type: 'SECURITYTOKEN', echo: this.fedAuthRequired, fedAuthToken: authentication.options.token }; break; case 'ntlm': payload.sspi = createNTLMRequest({ domain: authentication.options.domain }); break; default: payload.userName = authentication.options.userName; payload.password = authentication.options.password; } payload.hostname = os.hostname(); payload.serverName = this.routingData ? this.routingData.server : this.config.server; payload.appName = this.config.options.appName || 'Tedious'; payload.libraryName = libraryName; payload.language = this.config.options.language; payload.database = this.config.options.database; payload.clientId = Buffer.from([1, 2, 3, 4, 5, 6]); payload.readOnlyIntent = this.config.options.readOnlyIntent; payload.initDbFatal = !this.config.options.fallbackToDefaultDb; this.routingData = undefined; this.messageIo.sendMessage(TYPE.LOGIN7, payload.toBuffer()); this.debug.payload(function () { return payload.toString(' '); }); } sendFedAuthResponsePacket(tokenResponse) { const accessTokenLen = Buffer.byteLength(tokenResponse.accessToken, 'ucs2'); const data = Buffer.alloc(8 + accessTokenLen); let offset = 0; offset = data.writeUInt32LE(accessTokenLen + 4, offset); offset = data.writeUInt32LE(accessTokenLen, offset); data.write(tokenResponse.accessToken, offset, 'ucs2'); this.messageIo.sendMessage(TYPE.FEDAUTH_TOKEN, data); // sent the fedAuth token message, the rest is similar to standard login 7 this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); } // Returns false to apply backpressure. sendDataToTokenStreamParser(data) { return this.tokenStreamParser.addBuffer(data); } // This is an internal method that is called from Request.pause(). // It has to check whether the passed Request object represents the currently // active request, because the application might have called Request.pause() // on an old inactive Request object. pauseRequest(request) { if (this.isRequestActive(request)) { this.tokenStreamParser.pause(); } } // This is an internal method that is called from Request.resume(). resumeRequest(request) { if (this.isRequestActive(request)) { this.tokenStreamParser.resume(); } } // Returns true if the passed request is the currently active request of the connection. isRequestActive(request) { return request === this.request && this.state === this.STATE.SENT_CLIENT_REQUEST; } sendInitialSql() { const payload = new SqlBatchPayload(this.getInitialSql(), this.currentTransactionDescriptor(), this.config.options); return this.messageIo.sendMessage(TYPE.SQL_BATCH, payload.data); } getInitialSql() { const options = []; if (this.config.options.enableAnsiNull === true) { options.push('set ansi_nulls on'); } else if (this.config.options.enableAnsiNull === false) { options.push('set ansi_nulls off'); } if (this.config.options.enableAnsiNullDefault === true) { options.push('set ansi_null_dflt_on on'); } else if (this.config.options.enableAnsiNullDefault === false) { options.push('set ansi_null_dflt_on off'); } if (this.config.options.enableAnsiPadding === true) { options.push('set ansi_padding on'); } else if (this.config.options.enableAnsiPadding === false) { options.push('set ansi_padding off'); } if (this.config.options.enableAnsiWarnings === true) { options.push('set ansi_warnings on'); } else if (this.config.options.enableAnsiWarnings === false) { options.push('set ansi_warnings off'); } if (this.config.options.enableArithAbort === true) { options.push('set arithabort on'); } else if (this.config.options.enableArithAbort === false) { options.push('set arithabort off'); } if (this.config.options.enableConcatNullYieldsNull === true) { options.push('set concat_null_yields_null on'); } else if (this.config.options.enableArithAbort === false) { options.push('set concat_null_yields_null off'); } if (this.config.options.enableCursorCloseOnCommit === true) { options.push('set cursor_close_on_commit on'); } else if (this.config.options.enableCursorCloseOnCommit === false) { options.push('set cursor_close_on_commit off'); } if (this.config.options.datefirst !== null) { options.push(`set datefirst ${this.config.options.datefirst}`); } if (this.config.options.dateFormat !== null) { options.push(`set dateformat ${this.config.options.dateFormat}`); } if (this.config.options.enableImplicitTransactions === true) { options.push('set implicit_transactions o