tedious
Version:
A TDS driver, for connecting to MS SQLServer databases.
1,398 lines (1,141 loc) • 76.7 kB
JavaScript
"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