From 0a0fb70589d1fd2dfc66910c44efaa19dac96788 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 13:06:31 +0000 Subject: [PATCH 01/44] refactor: remove `enter` event of `FINAL` state --- src/connection.ts | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 704386af9..a0362819c 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1909,6 +1909,7 @@ class Connection extends EventEmitter { */ close() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } /** @@ -2312,6 +2313,7 @@ class Connection extends EventEmitter { this.dispatchEvent('retry'); } else { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } @@ -3215,9 +3217,11 @@ Connection.prototype.STATE = { events: { socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3288,9 +3292,11 @@ Connection.prototype.STATE = { events: { socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3304,9 +3310,11 @@ Connection.prototype.STATE = { }, socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, reconnect: function() { this.transitionTo(this.STATE.CONNECTING); @@ -3324,9 +3332,11 @@ Connection.prototype.STATE = { }, socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, retry: function() { this.createRetryTimer(); @@ -3338,9 +3348,11 @@ Connection.prototype.STATE = { events: { socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3374,10 +3386,12 @@ Connection.prototype.STATE = { } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } else { this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } })().catch((err) => { process.nextTick(() => { @@ -3388,9 +3402,11 @@ Connection.prototype.STATE = { events: { socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3440,14 +3456,17 @@ Connection.prototype.STATE = { return this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); } else { this.emit('connect', this.loginError); - return this.transitionTo(this.STATE.FINAL); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + return; } } else { this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); - return this.transitionTo(this.STATE.FINAL); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + return; } } - })().catch((err) => { process.nextTick(() => { throw err; @@ -3457,9 +3476,11 @@ Connection.prototype.STATE = { events: { socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3531,6 +3552,7 @@ Connection.prototype.STATE = { [new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'), err]); this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); return; } @@ -3545,10 +3567,12 @@ Connection.prototype.STATE = { } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } else { this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } })().catch((err) => { @@ -3560,9 +3584,11 @@ Connection.prototype.STATE = { events: { socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3592,9 +3618,11 @@ Connection.prototype.STATE = { events: { socketError: function socketError() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); }, connectTimeout: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3603,6 +3631,7 @@ Connection.prototype.STATE = { events: { socketError: function() { this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3694,6 +3723,7 @@ Connection.prototype.STATE = { const sqlRequest = this.request!; this.request = undefined; this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); sqlRequest.callback(err); } @@ -3742,6 +3772,7 @@ Connection.prototype.STATE = { this.request = undefined; this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); sqlRequest.callback(err); } @@ -3749,9 +3780,6 @@ Connection.prototype.STATE = { }, FINAL: { name: 'Final', - enter: function() { - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, events: { connectTimeout: function() { // Do nothing, as the timer should be cleaned up. From 48e6720a17d6a4728361aeca5d9ccca6b3ba748c Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 13:29:06 +0000 Subject: [PATCH 02/44] refactor: extract `enter` event logic from `SENT_LOGIN7_WITH_STANDARD_LOGIN` state --- src/connection.ts | 194 +++++++++++++++++++++++----------------------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index a0362819c..4019b6385 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2007,6 +2007,9 @@ class Connection extends EventEmitter { this.sendPreLogin(); this.transitionTo(this.STATE.SENT_PRELOGIN); + this.sentPrelogin().catch((err) => { + process.nextTick(() => { throw err; }); + }); } wrapWithTls(socket: net.Socket): Promise { @@ -3192,6 +3195,99 @@ class Connection extends EventEmitter { return 'read committed'; } } + + async sentPrelogin() { + let messageBuffer = Buffer.alloc(0); + + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } + + for await (const data of message) { + messageBuffer = Buffer.concat([messageBuffer, data]); + } + + const preloginPayload = new PreloginPayload(messageBuffer); + this.debug.payload(function() { + return preloginPayload.toString(' '); + }); + + if (preloginPayload.fedAuthRequired === 1) { + this.fedAuthRequired = true; + } + if ('strict' !== this.config.options.encrypt && (preloginPayload.encryptionString === 'ON' || preloginPayload.encryptionString === 'REQ')) { + if (!this.config.options.encrypt) { + this.emit('connect', new ConnectionError("Server requires encryption, set 'encrypt' config option to true.", 'EENCRYPT')); + return this.close(); + } + + try { + this.transitionTo(this.STATE.SENT_TLSSSLNEGOTIATION); + await this.messageIo.startTls(this.secureContextOptions, this.config.options.serverName ? this.config.options.serverName : this.routingData?.server ?? this.config.server, this.config.options.trustServerCertificate); + } catch (err: any) { + return this.socketError(err); + } + } + + this.sendLogin7Packet(); + + const { authentication } = this.config; + + switch (authentication.type) { + case 'azure-active-directory-password': + case 'azure-active-directory-msi-vm': + case 'azure-active-directory-msi-app-service': + case 'azure-active-directory-service-principal-secret': + case 'azure-active-directory-default': + this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); + break; + case 'ntlm': + this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); + break; + default: + this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); + break; + } + } + + async sentLogin7WithStandardLogin() { + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } + + const handler = new Login7TokenHandler(this); + const tokenStreamParser = this.createTokenStreamParser(message, handler); + + await once(tokenStreamParser, 'end'); + + if (handler.loginAckReceived) { + if (handler.routingData) { + this.routingData = handler.routingData; + this.transitionTo(this.STATE.REROUTING); + } else { + this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); + } + } else if (this.loginError) { + if (isTransientError(this.loginError)) { + this.debug.log('Initiating retry on transient error'); + this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + } else { + this.emit('connect', this.loginError); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + } + } else { + this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + } + } } function isTransientError(error: AggregateError | ConnectionError): boolean { @@ -3227,68 +3323,6 @@ Connection.prototype.STATE = { }, SENT_PRELOGIN: { name: 'SentPrelogin', - enter: function() { - (async () => { - let messageBuffer = Buffer.alloc(0); - - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } - - for await (const data of message) { - messageBuffer = Buffer.concat([messageBuffer, data]); - } - - const preloginPayload = new PreloginPayload(messageBuffer); - this.debug.payload(function() { - return preloginPayload.toString(' '); - }); - - if (preloginPayload.fedAuthRequired === 1) { - this.fedAuthRequired = true; - } - if ('strict' !== this.config.options.encrypt && (preloginPayload.encryptionString === 'ON' || preloginPayload.encryptionString === 'REQ')) { - if (!this.config.options.encrypt) { - this.emit('connect', new ConnectionError("Server requires encryption, set 'encrypt' config option to true.", 'EENCRYPT')); - return this.close(); - } - - try { - this.transitionTo(this.STATE.SENT_TLSSSLNEGOTIATION); - await this.messageIo.startTls(this.secureContextOptions, this.config.options.serverName ? this.config.options.serverName : this.routingData?.server ?? this.config.server, this.config.options.trustServerCertificate); - } catch (err: any) { - return this.socketError(err); - } - } - - this.sendLogin7Packet(); - - const { authentication } = this.config; - - switch (authentication.type) { - case 'azure-active-directory-password': - case 'azure-active-directory-msi-vm': - case 'azure-active-directory-msi-app-service': - case 'azure-active-directory-service-principal-secret': - case 'azure-active-directory-default': - this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); - break; - case 'ntlm': - this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); - break; - default: - this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); - break; - } - })().catch((err) => { - process.nextTick(() => { - throw err; - }); - }); - }, events: { socketError: function() { this.transitionTo(this.STATE.FINAL); @@ -3359,41 +3393,7 @@ Connection.prototype.STATE = { SENT_LOGIN7_WITH_STANDARD_LOGIN: { name: 'SentLogin7WithStandardLogin', enter: function() { - (async () => { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } - - const handler = new Login7TokenHandler(this); - const tokenStreamParser = this.createTokenStreamParser(message, handler); - - await once(tokenStreamParser, 'end'); - - if (handler.loginAckReceived) { - if (handler.routingData) { - this.routingData = handler.routingData; - this.transitionTo(this.STATE.REROUTING); - } else { - this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); - } - } else if (this.loginError) { - if (isTransientError(this.loginError)) { - this.debug.log('Initiating retry on transient error'); - this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); - } else { - this.emit('connect', this.loginError); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - } - } else { - this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - } - })().catch((err) => { + this.sentLogin7WithStandardLogin().catch((err) => { process.nextTick(() => { throw err; }); From 41ab30a9e9891d70a21cf05ea1a9ec84921f8b4d Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 13:43:13 +0000 Subject: [PATCH 03/44] refactor: extract more `enter` event logic from `SENT_LOGIN7` states --- src/connection.ts | 322 +++++++++++++++++++++++----------------------- 1 file changed, 163 insertions(+), 159 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 4019b6385..7ce3706d2 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3288,6 +3288,166 @@ class Connection extends EventEmitter { this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } + + async sentLogin7WithNtlm() { + while (true) { + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } + + const handler = new Login7TokenHandler(this); + const tokenStreamParser = this.createTokenStreamParser(message, handler); + + await once(tokenStreamParser, 'end'); + + if (handler.loginAckReceived) { + if (handler.routingData) { + this.routingData = handler.routingData; + return this.transitionTo(this.STATE.REROUTING); + } else { + return this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); + } + } else if (this.ntlmpacket) { + const authentication = this.config.authentication as NtlmAuthentication; + + const payload = new NTLMResponsePayload({ + domain: authentication.options.domain, + userName: authentication.options.userName, + password: authentication.options.password, + ntlmpacket: this.ntlmpacket + }); + + this.messageIo.sendMessage(TYPE.NTLMAUTH_PKT, payload.data); + this.debug.payload(function() { + return payload.toString(' '); + }); + + this.ntlmpacket = undefined; + } else if (this.loginError) { + if (isTransientError(this.loginError)) { + this.debug.log('Initiating retry on transient error'); + return this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + } else { + this.emit('connect', this.loginError); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + return; + } + } else { + this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + return; + } + } + } + + async sentLogin7WithFedauth() { + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } + + const handler = new Login7TokenHandler(this); + const tokenStreamParser = this.createTokenStreamParser(message, handler); + await once(tokenStreamParser, 'end'); + if (handler.loginAckReceived) { + if (handler.routingData) { + this.routingData = handler.routingData; + this.transitionTo(this.STATE.REROUTING); + } else { + this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); + } + + return; + } + + const fedAuthInfoToken = handler.fedAuthInfoToken; + + if (fedAuthInfoToken && fedAuthInfoToken.stsurl && fedAuthInfoToken.spn) { + const authentication = this.config.authentication as AzureActiveDirectoryPasswordAuthentication | AzureActiveDirectoryMsiVmAuthentication | AzureActiveDirectoryMsiAppServiceAuthentication | AzureActiveDirectoryServicePrincipalSecret | AzureActiveDirectoryDefaultAuthentication; + const tokenScope = new URL('/.default', fedAuthInfoToken.spn).toString(); + + let credentials; + + switch (authentication.type) { + case 'azure-active-directory-password': + credentials = new UsernamePasswordCredential( + authentication.options.tenantId ?? 'common', + authentication.options.clientId, + authentication.options.userName, + authentication.options.password + ); + break; + case 'azure-active-directory-msi-vm': + case 'azure-active-directory-msi-app-service': + const msiArgs = authentication.options.clientId ? [authentication.options.clientId, {}] : [{}]; + credentials = new ManagedIdentityCredential(...msiArgs); + break; + case 'azure-active-directory-default': + const args = authentication.options.clientId ? { managedIdentityClientId: authentication.options.clientId } : {}; + credentials = new DefaultAzureCredential(args); + break; + case 'azure-active-directory-service-principal-secret': + credentials = new ClientSecretCredential( + authentication.options.tenantId, + authentication.options.clientId, + authentication.options.clientSecret + ); + break; + } + + let tokenResponse; + try { + tokenResponse = await credentials.getToken(tokenScope); + } catch (err) { + this.loginError = new AggregateError( + [new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'), err]); + this.emit('connect', this.loginError); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + return; + } + + + const token = tokenResponse.token; + this.sendFedAuthTokenMessage(token); + + } else if (this.loginError) { + if (isTransientError(this.loginError)) { + this.debug.log('Initiating retry on transient error'); + this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + } else { + this.emit('connect', this.loginError); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + } + } else { + this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + } + } + + async loggedInSendingInitialSql() { + this.sendInitialSql(); + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } + const tokenStreamParser = this.createTokenStreamParser(message, new InitialSqlTokenHandler(this)); + await once(tokenStreamParser, 'end'); + + this.transitionTo(this.STATE.LOGGED_IN); + this.processedInitialSql(); + } } function isTransientError(error: AggregateError | ConnectionError): boolean { @@ -3413,61 +3573,7 @@ Connection.prototype.STATE = { SENT_LOGIN7_WITH_NTLM: { name: 'SentLogin7WithNTLMLogin', enter: function() { - (async () => { - while (true) { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } - - const handler = new Login7TokenHandler(this); - const tokenStreamParser = this.createTokenStreamParser(message, handler); - - await once(tokenStreamParser, 'end'); - - if (handler.loginAckReceived) { - if (handler.routingData) { - this.routingData = handler.routingData; - return this.transitionTo(this.STATE.REROUTING); - } else { - return this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); - } - } else if (this.ntlmpacket) { - const authentication = this.config.authentication as NtlmAuthentication; - - const payload = new NTLMResponsePayload({ - domain: authentication.options.domain, - userName: authentication.options.userName, - password: authentication.options.password, - ntlmpacket: this.ntlmpacket - }); - - this.messageIo.sendMessage(TYPE.NTLMAUTH_PKT, payload.data); - this.debug.payload(function() { - return payload.toString(' '); - }); - - this.ntlmpacket = undefined; - } else if (this.loginError) { - if (isTransientError(this.loginError)) { - this.debug.log('Initiating retry on transient error'); - return this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); - } else { - this.emit('connect', this.loginError); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - return; - } - } else { - this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - return; - } - } - })().catch((err) => { + this.sentLogin7WithNtlm().catch((err) => { process.nextTick(() => { throw err; }); @@ -3487,95 +3593,7 @@ Connection.prototype.STATE = { SENT_LOGIN7_WITH_FEDAUTH: { name: 'SentLogin7Withfedauth', enter: function() { - (async () => { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } - - const handler = new Login7TokenHandler(this); - const tokenStreamParser = this.createTokenStreamParser(message, handler); - await once(tokenStreamParser, 'end'); - if (handler.loginAckReceived) { - if (handler.routingData) { - this.routingData = handler.routingData; - this.transitionTo(this.STATE.REROUTING); - } else { - this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); - } - - return; - } - - const fedAuthInfoToken = handler.fedAuthInfoToken; - - if (fedAuthInfoToken && fedAuthInfoToken.stsurl && fedAuthInfoToken.spn) { - const authentication = this.config.authentication as AzureActiveDirectoryPasswordAuthentication | AzureActiveDirectoryMsiVmAuthentication | AzureActiveDirectoryMsiAppServiceAuthentication | AzureActiveDirectoryServicePrincipalSecret | AzureActiveDirectoryDefaultAuthentication; - const tokenScope = new URL('/.default', fedAuthInfoToken.spn).toString(); - - let credentials; - - switch (authentication.type) { - case 'azure-active-directory-password': - credentials = new UsernamePasswordCredential( - authentication.options.tenantId ?? 'common', - authentication.options.clientId, - authentication.options.userName, - authentication.options.password - ); - break; - case 'azure-active-directory-msi-vm': - case 'azure-active-directory-msi-app-service': - const msiArgs = authentication.options.clientId ? [authentication.options.clientId, {}] : [{}]; - credentials = new ManagedIdentityCredential(...msiArgs); - break; - case 'azure-active-directory-default': - const args = authentication.options.clientId ? { managedIdentityClientId: authentication.options.clientId } : {}; - credentials = new DefaultAzureCredential(args); - break; - case 'azure-active-directory-service-principal-secret': - credentials = new ClientSecretCredential( - authentication.options.tenantId, - authentication.options.clientId, - authentication.options.clientSecret - ); - break; - } - - let tokenResponse; - try { - tokenResponse = await credentials.getToken(tokenScope); - } catch (err) { - this.loginError = new AggregateError( - [new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'), err]); - this.emit('connect', this.loginError); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - return; - } - - - const token = tokenResponse.token; - this.sendFedAuthTokenMessage(token); - - } else if (this.loginError) { - if (isTransientError(this.loginError)) { - this.debug.log('Initiating retry on transient error'); - this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); - } else { - this.emit('connect', this.loginError); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - } - } else { - this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - } - - })().catch((err) => { + this.sentLogin7WithFedauth().catch((err) => { process.nextTick(() => { throw err; }); @@ -3595,21 +3613,7 @@ Connection.prototype.STATE = { LOGGED_IN_SENDING_INITIAL_SQL: { name: 'LoggedInSendingInitialSql', enter: function() { - (async () => { - this.sendInitialSql(); - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } - const tokenStreamParser = this.createTokenStreamParser(message, new InitialSqlTokenHandler(this)); - await once(tokenStreamParser, 'end'); - - this.transitionTo(this.STATE.LOGGED_IN); - this.processedInitialSql(); - - })().catch((err) => { + this.loggedInSendingInitialSql().catch((err) => { process.nextTick(() => { throw err; }); From bbb3875b249cea75ac702256f7207b04a8c6ee56 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 13:45:11 +0000 Subject: [PATCH 04/44] refactor: remove `enter` event for `CONNECTING` state --- src/connection.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 7ce3706d2..88223b339 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1769,6 +1769,7 @@ class Connection extends EventEmitter { } this.transitionTo(this.STATE.CONNECTING); + this.initialiseConnection(); } /** @@ -2175,6 +2176,7 @@ class Connection extends EventEmitter { this.retryTimer = undefined; this.emit('retry'); this.transitionTo(this.STATE.CONNECTING); + this.initialiseConnection(); } /** @@ -3467,9 +3469,6 @@ Connection.prototype.STATE = { }, CONNECTING: { name: 'Connecting', - enter: function() { - this.initialiseConnection(); - }, events: { socketError: function() { this.transitionTo(this.STATE.FINAL); @@ -3512,6 +3511,7 @@ Connection.prototype.STATE = { }, reconnect: function() { this.transitionTo(this.STATE.CONNECTING); + this.initialiseConnection(); } } }, From 738dc6d711ab99dbacf65db0599ca6b924359ed7 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 13:47:12 +0000 Subject: [PATCH 05/44] refactor: remove `enter` event for `REROUTING` state --- src/connection.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 88223b339..2317bfe0b 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3272,6 +3272,7 @@ class Connection extends EventEmitter { if (handler.routingData) { this.routingData = handler.routingData; this.transitionTo(this.STATE.REROUTING); + this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); } @@ -3308,7 +3309,9 @@ class Connection extends EventEmitter { if (handler.loginAckReceived) { if (handler.routingData) { this.routingData = handler.routingData; - return this.transitionTo(this.STATE.REROUTING); + this.transitionTo(this.STATE.REROUTING); + this.cleanupConnection(CLEANUP_TYPE.REDIRECT); + return; } else { return this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); } @@ -3362,6 +3365,7 @@ class Connection extends EventEmitter { if (handler.routingData) { this.routingData = handler.routingData; this.transitionTo(this.STATE.REROUTING); + this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); } @@ -3495,9 +3499,6 @@ Connection.prototype.STATE = { }, REROUTING: { name: 'ReRouting', - enter: function() { - this.cleanupConnection(CLEANUP_TYPE.REDIRECT); - }, events: { message: function() { }, From 8f7808c9fa856178d69dd943a8c028643f6952b8 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 14:37:05 +0000 Subject: [PATCH 06/44] refactor: remove `enter` event for `LOGGED_IN_SENDING_INITIAL_SQL` state --- src/connection.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 2317bfe0b..4f5351558 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3275,6 +3275,9 @@ class Connection extends EventEmitter { this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); + this.loggedInSendingInitialSql().catch((err) => { + process.nextTick(() => { throw err; }); + }); } } else if (this.loginError) { if (isTransientError(this.loginError)) { @@ -3313,7 +3316,11 @@ class Connection extends EventEmitter { this.cleanupConnection(CLEANUP_TYPE.REDIRECT); return; } else { - return this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); + this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); + this.loggedInSendingInitialSql().catch((err) => { + process.nextTick(() => { throw err; }); + }); + return; } } else if (this.ntlmpacket) { const authentication = this.config.authentication as NtlmAuthentication; @@ -3613,13 +3620,6 @@ Connection.prototype.STATE = { }, LOGGED_IN_SENDING_INITIAL_SQL: { name: 'LoggedInSendingInitialSql', - enter: function() { - this.loggedInSendingInitialSql().catch((err) => { - process.nextTick(() => { - throw err; - }); - }); - }, events: { socketError: function socketError() { this.transitionTo(this.STATE.FINAL); From df40cee6d1be42bd03ce7d0b51975886f5634087 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 14:38:29 +0000 Subject: [PATCH 07/44] refactor: remove `enter` event for `SENT_LOGIN7_WITH_STANDARD_LOGIN` state --- src/connection.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 4f5351558..b682cf0ac 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2425,6 +2425,9 @@ class Connection extends EventEmitter { 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); + this.sentLogin7WithStandardLogin().catch((err) => { + process.nextTick(() => { throw err; }); + }); } /** @@ -3251,6 +3254,9 @@ class Connection extends EventEmitter { break; default: this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); + this.sentLogin7WithStandardLogin().catch((err) => { + process.nextTick(() => { throw err; }); + }); break; } } @@ -3560,13 +3566,6 @@ Connection.prototype.STATE = { }, SENT_LOGIN7_WITH_STANDARD_LOGIN: { name: 'SentLogin7WithStandardLogin', - enter: function() { - this.sentLogin7WithStandardLogin().catch((err) => { - process.nextTick(() => { - throw err; - }); - }); - }, events: { socketError: function() { this.transitionTo(this.STATE.FINAL); From c3e6123ecde5478bf7b442992ed6670f3bd1b03c Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 14:53:48 +0000 Subject: [PATCH 08/44] refactor: remove `enter` event for `SENT_LOGIN7_WITH_NTLM` state --- src/connection.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index b682cf0ac..893c74c5e 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3251,6 +3251,9 @@ class Connection extends EventEmitter { break; case 'ntlm': this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); + this.sentLogin7WithNtlm().catch((err) => { + process.nextTick(() => { throw err; }); + }); break; default: this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); @@ -3579,13 +3582,6 @@ Connection.prototype.STATE = { }, SENT_LOGIN7_WITH_NTLM: { name: 'SentLogin7WithNTLMLogin', - enter: function() { - this.sentLogin7WithNtlm().catch((err) => { - process.nextTick(() => { - throw err; - }); - }); - }, events: { socketError: function() { this.transitionTo(this.STATE.FINAL); From 9bdb533919677e0cb9ea68ce8274c7c96dd63e9c Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 14:54:07 +0000 Subject: [PATCH 09/44] refactor: remove `enter` event for `SENT_LOGIN7_WITH_FEDAUTH` state --- src/connection.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 893c74c5e..91da26f73 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3248,6 +3248,9 @@ class Connection extends EventEmitter { case 'azure-active-directory-service-principal-secret': case 'azure-active-directory-default': this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); + this.sentLogin7WithFedauth().catch((err) => { + process.nextTick(() => { throw err; }); + }); break; case 'ntlm': this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); @@ -3595,13 +3598,6 @@ Connection.prototype.STATE = { }, SENT_LOGIN7_WITH_FEDAUTH: { name: 'SentLogin7Withfedauth', - enter: function() { - this.sentLogin7WithFedauth().catch((err) => { - process.nextTick(() => { - throw err; - }); - }); - }, events: { socketError: function() { this.transitionTo(this.STATE.FINAL); From 60cd91c484fc2085e35d9a10be0c2e81682fb2ce Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 15:03:07 +0000 Subject: [PATCH 10/44] refactor: move logic around --- src/connection.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 91da26f73..01fb35423 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2423,11 +2423,6 @@ class Connection extends EventEmitter { offset = data.writeUInt32LE(accessTokenLen, offset); data.write(token, 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); - this.sentLogin7WithStandardLogin().catch((err) => { - process.nextTick(() => { throw err; }); - }); } /** @@ -3442,7 +3437,11 @@ class Connection extends EventEmitter { const token = tokenResponse.token; this.sendFedAuthTokenMessage(token); - + // sent the fedAuth token message, the rest is similar to standard login 7 + this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); + this.sentLogin7WithStandardLogin().catch((err) => { + process.nextTick(() => { throw err; }); + }); } else if (this.loginError) { if (isTransientError(this.loginError)) { this.debug.log('Initiating retry on transient error'); From 59dfb254d80c61721e556ee75b8316893c3e576d Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 15:05:35 +0000 Subject: [PATCH 11/44] fixup: fix missing `loggedInSendingInitialSql` call --- src/connection.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/connection.ts b/src/connection.ts index 01fb35423..44f4d9984 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3382,6 +3382,9 @@ class Connection extends EventEmitter { this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); + this.loggedInSendingInitialSql().catch((err) => { + process.nextTick(() => { throw err; }); + }); } return; From ab6da057bb8aa1e2df4ecd91f21e37e4c698aff5 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 15:06:35 +0000 Subject: [PATCH 12/44] refactor: remove `reconnect` event from `REROUTING` state --- src/connection.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 44f4d9984..82fafe798 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2309,7 +2309,8 @@ class Connection extends EventEmitter { if (this.state === this.STATE.REROUTING) { this.debug.log('Rerouting to ' + this.routingData!.server + ':' + this.routingData!.port); - this.dispatchEvent('reconnect'); + this.transitionTo(this.STATE.CONNECTING); + this.initialiseConnection(); } else if (this.state === this.STATE.TRANSIENT_FAILURE_RETRY) { const server = this.routingData ? this.routingData.server : this.config.server; const port = this.routingData ? this.routingData.port : this.config.options.port; @@ -3530,10 +3531,6 @@ Connection.prototype.STATE = { connectTimeout: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - reconnect: function() { - this.transitionTo(this.STATE.CONNECTING); - this.initialiseConnection(); } } }, From ee3449817e36d9ff35c6120f11bcf503640c7ccd Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 15:09:32 +0000 Subject: [PATCH 13/44] refactor: remove `enter` event from `TRANSIENT_FAILURE_RETRY` state --- src/connection.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 82fafe798..acab418f2 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3291,6 +3291,8 @@ class Connection extends EventEmitter { if (isTransientError(this.loginError)) { this.debug.log('Initiating retry on transient error'); this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + this.curTransientRetryCount++; + this.cleanupConnection(CLEANUP_TYPE.RETRY); } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3349,7 +3351,10 @@ class Connection extends EventEmitter { } else if (this.loginError) { if (isTransientError(this.loginError)) { this.debug.log('Initiating retry on transient error'); - return this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + this.curTransientRetryCount++; + this.cleanupConnection(CLEANUP_TYPE.RETRY); + return; } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3450,6 +3455,8 @@ class Connection extends EventEmitter { if (isTransientError(this.loginError)) { this.debug.log('Initiating retry on transient error'); this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + this.curTransientRetryCount++; + this.cleanupConnection(CLEANUP_TYPE.RETRY); } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3536,10 +3543,6 @@ Connection.prototype.STATE = { }, TRANSIENT_FAILURE_RETRY: { name: 'TRANSIENT_FAILURE_RETRY', - enter: function() { - this.curTransientRetryCount++; - this.cleanupConnection(CLEANUP_TYPE.RETRY); - }, events: { message: function() { }, From a8233a3caee9d1d7590804750b6d669232da18be Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 15:42:21 +0000 Subject: [PATCH 14/44] refactor: remove `retry` event in `TRANSIENT_FAILURE_RETRY` state --- src/connection.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index acab418f2..ffe4fb3cb 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2316,7 +2316,7 @@ class Connection extends EventEmitter { 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'); + this.createRetryTimer(); } else { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); @@ -3553,9 +3553,6 @@ Connection.prototype.STATE = { connectTimeout: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - retry: function() { - this.createRetryTimer(); } } }, From 9d87e1c68572d5c007e846b20f0ccf227f2e7465 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 15:43:46 +0000 Subject: [PATCH 15/44] refactor: move logic around --- src/connection.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index ffe4fb3cb..cd399be70 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1992,7 +1992,7 @@ class Connection extends EventEmitter { return new TokenStreamParser(message, this.debug, handler, this.config.options); } - socketHandlingForSendPreLogin(socket: net.Socket) { + performSocketSetup(socket: net.Socket) { socket.on('error', (error) => { this.socketError(error); }); socket.on('close', () => { this.socketClose(); }); socket.on('end', () => { this.socketEnd(); }); @@ -2005,12 +2005,6 @@ class Connection extends EventEmitter { this.closed = false; this.debug.log('connected to ' + this.config.server + ':' + this.config.options.port); - - this.sendPreLogin(); - this.transitionTo(this.STATE.SENT_PRELOGIN); - this.sentPrelogin().catch((err) => { - process.nextTick(() => { throw err; }); - }); } wrapWithTls(socket: net.Socket): Promise { @@ -2060,7 +2054,13 @@ class Connection extends EventEmitter { } } - this.socketHandlingForSendPreLogin(socket); + this.performSocketSetup(socket); + + this.sendPreLogin(); + this.transitionTo(this.STATE.SENT_PRELOGIN); + this.sentPrelogin().catch((err) => { + process.nextTick(() => { throw err; }); + }); })().catch((err) => { this.clearConnectTimer(); From f3ecdd9ce0ce6177afd3dd1af6e7b8b4ac9162c7 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 15:45:19 +0000 Subject: [PATCH 16/44] refactor: rename method --- src/connection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index cd399be70..ad7091352 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2058,7 +2058,7 @@ class Connection extends EventEmitter { this.sendPreLogin(); this.transitionTo(this.STATE.SENT_PRELOGIN); - this.sentPrelogin().catch((err) => { + this.handlePreloginResponse().catch((err) => { process.nextTick(() => { throw err; }); }); })().catch((err) => { @@ -3197,7 +3197,7 @@ class Connection extends EventEmitter { } } - async sentPrelogin() { + async handlePreloginResponse() { let messageBuffer = Buffer.alloc(0); let message; From 3b8a64d63cbc59e10a020b339dbea1d47a811f1a Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 15:53:34 +0000 Subject: [PATCH 17/44] refactor: pass the connect timeout signal along the different connection steps --- src/connection.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index ad7091352..cd3a97380 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2058,7 +2058,7 @@ class Connection extends EventEmitter { this.sendPreLogin(); this.transitionTo(this.STATE.SENT_PRELOGIN); - this.handlePreloginResponse().catch((err) => { + this.handlePreloginResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); })().catch((err) => { @@ -3197,7 +3197,7 @@ class Connection extends EventEmitter { } } - async handlePreloginResponse() { + async handlePreloginResponse(signal: AbortSignal) { let messageBuffer = Buffer.alloc(0); let message; @@ -3244,26 +3244,26 @@ class Connection extends EventEmitter { case 'azure-active-directory-service-principal-secret': case 'azure-active-directory-default': this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); - this.sentLogin7WithFedauth().catch((err) => { + this.sentLogin7WithFedauth(signal).catch((err) => { process.nextTick(() => { throw err; }); }); break; case 'ntlm': this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); - this.sentLogin7WithNtlm().catch((err) => { + this.sentLogin7WithNtlm(signal).catch((err) => { process.nextTick(() => { throw err; }); }); break; default: this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); - this.sentLogin7WithStandardLogin().catch((err) => { + this.sentLogin7WithStandardLogin(signal).catch((err) => { process.nextTick(() => { throw err; }); }); break; } } - async sentLogin7WithStandardLogin() { + async sentLogin7WithStandardLogin(signal: AbortSignal) { let message; try { message = await this.messageIo.readMessage(); @@ -3305,7 +3305,7 @@ class Connection extends EventEmitter { } } - async sentLogin7WithNtlm() { + async sentLogin7WithNtlm(signal: AbortSignal) { while (true) { let message; try { @@ -3370,7 +3370,7 @@ class Connection extends EventEmitter { } } - async sentLogin7WithFedauth() { + async sentLogin7WithFedauth(signal: AbortSignal) { let message; try { message = await this.messageIo.readMessage(); @@ -3448,7 +3448,7 @@ class Connection extends EventEmitter { this.sendFedAuthTokenMessage(token); // sent the fedAuth token message, the rest is similar to standard login 7 this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); - this.sentLogin7WithStandardLogin().catch((err) => { + this.sentLogin7WithStandardLogin(signal).catch((err) => { process.nextTick(() => { throw err; }); }); } else if (this.loginError) { From 34e0c268543cb6a75f1d80e719645eb0987c99e6 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 16:51:12 +0000 Subject: [PATCH 18/44] refactor: rename methods --- src/connection.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index cd3a97380..6a8c8c071 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3244,26 +3244,26 @@ class Connection extends EventEmitter { case 'azure-active-directory-service-principal-secret': case 'azure-active-directory-default': this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); - this.sentLogin7WithFedauth(signal).catch((err) => { + this.handleLogin7WithFedauthResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); break; case 'ntlm': this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); - this.sentLogin7WithNtlm(signal).catch((err) => { + this.handleLogin7WithNtlmResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); break; default: this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); - this.sentLogin7WithStandardLogin(signal).catch((err) => { + this.handleLogin7WithStandardLoginResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); break; } } - async sentLogin7WithStandardLogin(signal: AbortSignal) { + async handleLogin7WithStandardLoginResponse(signal: AbortSignal) { let message; try { message = await this.messageIo.readMessage(); @@ -3283,7 +3283,7 @@ class Connection extends EventEmitter { this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); - this.loggedInSendingInitialSql().catch((err) => { + this.loggedInSendingInitialSql(signal).catch((err) => { process.nextTick(() => { throw err; }); }); } @@ -3305,7 +3305,7 @@ class Connection extends EventEmitter { } } - async sentLogin7WithNtlm(signal: AbortSignal) { + async handleLogin7WithNtlmResponse(signal: AbortSignal) { while (true) { let message; try { @@ -3327,7 +3327,7 @@ class Connection extends EventEmitter { return; } else { this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); - this.loggedInSendingInitialSql().catch((err) => { + this.loggedInSendingInitialSql(signal).catch((err) => { process.nextTick(() => { throw err; }); }); return; @@ -3370,7 +3370,7 @@ class Connection extends EventEmitter { } } - async sentLogin7WithFedauth(signal: AbortSignal) { + async handleLogin7WithFedauthResponse(signal: AbortSignal) { let message; try { message = await this.messageIo.readMessage(); @@ -3388,7 +3388,7 @@ class Connection extends EventEmitter { this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); - this.loggedInSendingInitialSql().catch((err) => { + this.loggedInSendingInitialSql(signal).catch((err) => { process.nextTick(() => { throw err; }); }); } @@ -3448,7 +3448,7 @@ class Connection extends EventEmitter { this.sendFedAuthTokenMessage(token); // sent the fedAuth token message, the rest is similar to standard login 7 this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); - this.sentLogin7WithStandardLogin(signal).catch((err) => { + this.handleLogin7WithStandardLoginResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); } else if (this.loginError) { @@ -3469,8 +3469,9 @@ class Connection extends EventEmitter { } } - async loggedInSendingInitialSql() { + async loggedInSendingInitialSql(signal: AbortSignal) { this.sendInitialSql(); + let message; try { message = await this.messageIo.readMessage(); From 723b6933fa969dde92a86fde4f74473984b2dcb1 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 19:26:47 +0000 Subject: [PATCH 19/44] refactor: further code cleanup --- src/connection.ts | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 6a8c8c071..0ae30c3d6 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2430,6 +2430,7 @@ class Connection extends EventEmitter { * @private */ sendInitialSql() { + this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); const payload = new SqlBatchPayload(this.getInitialSql(), this.currentTransactionDescriptor(), this.config.options); const message = new Message({ type: TYPE.SQL_BATCH }); @@ -2532,14 +2533,6 @@ class Connection extends EventEmitter { return options.join('\n'); } - /** - * @private - */ - processedInitialSql() { - this.clearConnectTimer(); - this.emit('connect'); - } - /** * Execute the SQL batch represented by [[Request]]. * There is no param support, and unlike [[Request.execSql]], @@ -3243,19 +3236,16 @@ class Connection extends EventEmitter { case 'azure-active-directory-msi-app-service': case 'azure-active-directory-service-principal-secret': case 'azure-active-directory-default': - this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); this.handleLogin7WithFedauthResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); break; case 'ntlm': - this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); this.handleLogin7WithNtlmResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); break; default: - this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); this.handleLogin7WithStandardLoginResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); @@ -3264,6 +3254,8 @@ class Connection extends EventEmitter { } async handleLogin7WithStandardLoginResponse(signal: AbortSignal) { + this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); + let message; try { message = await this.messageIo.readMessage(); @@ -3282,7 +3274,6 @@ class Connection extends EventEmitter { this.transitionTo(this.STATE.REROUTING); this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { - this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); this.loggedInSendingInitialSql(signal).catch((err) => { process.nextTick(() => { throw err; }); }); @@ -3306,6 +3297,8 @@ class Connection extends EventEmitter { } async handleLogin7WithNtlmResponse(signal: AbortSignal) { + this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); + while (true) { let message; try { @@ -3326,7 +3319,6 @@ class Connection extends EventEmitter { this.cleanupConnection(CLEANUP_TYPE.REDIRECT); return; } else { - this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); this.loggedInSendingInitialSql(signal).catch((err) => { process.nextTick(() => { throw err; }); }); @@ -3371,6 +3363,8 @@ class Connection extends EventEmitter { } async handleLogin7WithFedauthResponse(signal: AbortSignal) { + this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); + let message; try { message = await this.messageIo.readMessage(); @@ -3387,7 +3381,6 @@ class Connection extends EventEmitter { this.transitionTo(this.STATE.REROUTING); this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { - this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); this.loggedInSendingInitialSql(signal).catch((err) => { process.nextTick(() => { throw err; }); }); @@ -3447,7 +3440,6 @@ class Connection extends EventEmitter { const token = tokenResponse.token; this.sendFedAuthTokenMessage(token); // sent the fedAuth token message, the rest is similar to standard login 7 - this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); this.handleLogin7WithStandardLoginResponse(signal).catch((err) => { process.nextTick(() => { throw err; }); }); @@ -3471,7 +3463,14 @@ class Connection extends EventEmitter { async loggedInSendingInitialSql(signal: AbortSignal) { this.sendInitialSql(); + this.handleInitialSqlResponse(signal); + + this.transitionTo(this.STATE.LOGGED_IN); + this.clearConnectTimer(); + this.emit('connect'); + } + async handleInitialSqlResponse(signal: AbortSignal) { let message; try { message = await this.messageIo.readMessage(); @@ -3480,9 +3479,6 @@ class Connection extends EventEmitter { } const tokenStreamParser = this.createTokenStreamParser(message, new InitialSqlTokenHandler(this)); await once(tokenStreamParser, 'end'); - - this.transitionTo(this.STATE.LOGGED_IN); - this.processedInitialSql(); } } From 5e820514ec4b81a3dca8e56895ba8a5e7905a0c6 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 19:30:12 +0000 Subject: [PATCH 20/44] fixup: add missing await call --- src/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection.ts b/src/connection.ts index 0ae30c3d6..f8446bde0 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3463,7 +3463,7 @@ class Connection extends EventEmitter { async loggedInSendingInitialSql(signal: AbortSignal) { this.sendInitialSql(); - this.handleInitialSqlResponse(signal); + await this.handleInitialSqlResponse(signal); this.transitionTo(this.STATE.LOGGED_IN); this.clearConnectTimer(); From 3434c39ce592912709830990753940fbb25e0004 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 19:30:41 +0000 Subject: [PATCH 21/44] refactor: await on `loggedInSendingInitialSql` --- src/connection.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index f8446bde0..916175b51 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3274,9 +3274,7 @@ class Connection extends EventEmitter { this.transitionTo(this.STATE.REROUTING); this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { - this.loggedInSendingInitialSql(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); + return await this.loggedInSendingInitialSql(signal); } } else if (this.loginError) { if (isTransientError(this.loginError)) { @@ -3319,10 +3317,7 @@ class Connection extends EventEmitter { this.cleanupConnection(CLEANUP_TYPE.REDIRECT); return; } else { - this.loggedInSendingInitialSql(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); - return; + return await this.loggedInSendingInitialSql(signal); } } else if (this.ntlmpacket) { const authentication = this.config.authentication as NtlmAuthentication; @@ -3381,9 +3376,7 @@ class Connection extends EventEmitter { this.transitionTo(this.STATE.REROUTING); this.cleanupConnection(CLEANUP_TYPE.REDIRECT); } else { - this.loggedInSendingInitialSql(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); + return await this.loggedInSendingInitialSql(signal); } return; From 043a238252492ffa3966f143646a81974ba3e045 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 21:31:35 +0000 Subject: [PATCH 22/44] refactor: make more use of `async`/`await` --- src/connection.ts | 68 ++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 916175b51..0b7cea158 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1919,30 +1919,37 @@ class Connection extends EventEmitter { initialiseConnection() { const signal = this.createConnectTimer(); - if (this.config.options.port) { - return this.connectOnPort(this.config.options.port, this.config.options.multiSubnetFailover, signal, this.config.options.connector); - } else { - return instanceLookup({ - server: this.config.server, - instanceName: this.config.options.instanceName!, - timeout: this.config.options.connectTimeout, - signal: signal - }).then((port) => { - process.nextTick(() => { - this.connectOnPort(port, this.config.options.multiSubnetFailover, signal, this.config.options.connector); + this.establishConnection(signal).catch((err) => { + process.nextTick(() => { throw err; }); + }); + } + + async establishConnection(signal: AbortSignal) { + let port = this.config.options.port; + + if (!port) { + try { + port = await instanceLookup({ + server: this.config.server, + instanceName: this.config.options.instanceName!, + timeout: this.config.options.connectTimeout, + signal: signal }); - }, (err) => { + } catch (err: any) { this.clearConnectTimer(); + if (err.name === 'AbortError') { // Ignore the AbortError for now, this is still handled by the connectTimer firing return; } - process.nextTick(() => { + return process.nextTick(() => { this.emit('connect', new ConnectionError(err.message, 'EINSTLOOKUP')); }); - }); + } } + + await this.connectOnPort(port, this.config.options.multiSubnetFailover, signal, this.config.options.connector); } /** @@ -2031,7 +2038,7 @@ class Connection extends EventEmitter { }); } - connectOnPort(port: number, multiSubnetFailover: boolean, signal: AbortSignal, customConnector?: () => Promise) { + async connectOnPort(port: number, multiSubnetFailover: boolean, signal: AbortSignal, customConnector?: () => Promise) { const connectOpts = { host: this.routingData ? this.routingData.server : this.config.server, port: this.routingData ? this.routingData.port : port, @@ -2040,7 +2047,7 @@ class Connection extends EventEmitter { const connect = customConnector || (multiSubnetFailover ? connectInParallel : connectInSequence); - (async () => { + try { let socket = await connect(connectOpts, dns.lookup, signal); if (this.config.options.encrypt === 'strict') { @@ -2058,10 +2065,8 @@ class Connection extends EventEmitter { this.sendPreLogin(); this.transitionTo(this.STATE.SENT_PRELOGIN); - this.handlePreloginResponse(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); - })().catch((err) => { + return await this.handlePreloginResponse(signal); + } catch (err: any) { this.clearConnectTimer(); if (err.name === 'AbortError') { @@ -2069,7 +2074,7 @@ class Connection extends EventEmitter { } process.nextTick(() => { this.socketError(err); }); - }); + } } /** @@ -3236,20 +3241,13 @@ class Connection extends EventEmitter { case 'azure-active-directory-msi-app-service': case 'azure-active-directory-service-principal-secret': case 'azure-active-directory-default': - this.handleLogin7WithFedauthResponse(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); - break; + return await this.handleLogin7WithFedauthResponse(signal); + case 'ntlm': - this.handleLogin7WithNtlmResponse(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); - break; + return await this.handleLogin7WithNtlmResponse(signal); + default: - this.handleLogin7WithStandardLoginResponse(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); - break; + return await this.handleLogin7WithStandardLoginResponse(signal); } } @@ -3433,9 +3431,7 @@ class Connection extends EventEmitter { const token = tokenResponse.token; this.sendFedAuthTokenMessage(token); // sent the fedAuth token message, the rest is similar to standard login 7 - this.handleLogin7WithStandardLoginResponse(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); + return await this.handleLogin7WithStandardLoginResponse(signal); } else if (this.loginError) { if (isTransientError(this.loginError)) { this.debug.log('Initiating retry on transient error'); From 5a1fe2858ac7c872f330a4c10868316c7ec9cafa Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 21:32:09 +0000 Subject: [PATCH 23/44] refactor: remove `connectTimeout` event --- src/connection.ts | 43 +++---------------------------------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 0b7cea158..f7e9a7b93 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2150,7 +2150,9 @@ class Connection extends EventEmitter { this.debug.log(message); this.emit('connect', new ConnectionError(message, 'ETIMEOUT')); this.connectTimer = undefined; - this.dispatchEvent('connectTimeout'); + + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); } /** @@ -3492,10 +3494,6 @@ Connection.prototype.STATE = { socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3505,10 +3503,6 @@ Connection.prototype.STATE = { socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3520,10 +3514,6 @@ Connection.prototype.STATE = { socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3535,10 +3525,6 @@ Connection.prototype.STATE = { socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3548,10 +3534,6 @@ Connection.prototype.STATE = { socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3561,10 +3543,6 @@ Connection.prototype.STATE = { socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3574,10 +3552,6 @@ Connection.prototype.STATE = { socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3587,10 +3561,6 @@ Connection.prototype.STATE = { socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3600,10 +3570,6 @@ Connection.prototype.STATE = { socketError: function socketError() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - }, - connectTimeout: function() { - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); } } }, @@ -3762,9 +3728,6 @@ Connection.prototype.STATE = { FINAL: { name: 'Final', events: { - connectTimeout: function() { - // Do nothing, as the timer should be cleaned up. - }, message: function() { // Do nothing }, From 021c7d9cca49aa3b08f712de6008f5343c66e39a Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 21:39:33 +0000 Subject: [PATCH 24/44] refactor: extract reroute handling --- src/connection.ts | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index f7e9a7b93..4dc2a700a 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2314,10 +2314,7 @@ class Connection extends EventEmitter { 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.transitionTo(this.STATE.CONNECTING); - this.initialiseConnection(); } else if (this.state === this.STATE.TRANSIENT_FAILURE_RETRY) { const server = this.routingData ? this.routingData.server : this.config.server; const port = this.routingData ? this.routingData.port : this.config.options.port; @@ -3271,8 +3268,7 @@ class Connection extends EventEmitter { if (handler.loginAckReceived) { if (handler.routingData) { this.routingData = handler.routingData; - this.transitionTo(this.STATE.REROUTING); - this.cleanupConnection(CLEANUP_TYPE.REDIRECT); + return await this.handleRerouting(); } else { return await this.loggedInSendingInitialSql(signal); } @@ -3313,9 +3309,7 @@ class Connection extends EventEmitter { if (handler.loginAckReceived) { if (handler.routingData) { this.routingData = handler.routingData; - this.transitionTo(this.STATE.REROUTING); - this.cleanupConnection(CLEANUP_TYPE.REDIRECT); - return; + return await this.handleRerouting(); } else { return await this.loggedInSendingInitialSql(signal); } @@ -3373,13 +3367,10 @@ class Connection extends EventEmitter { if (handler.loginAckReceived) { if (handler.routingData) { this.routingData = handler.routingData; - this.transitionTo(this.STATE.REROUTING); - this.cleanupConnection(CLEANUP_TYPE.REDIRECT); + return await this.handleRerouting(); } else { return await this.loggedInSendingInitialSql(signal); } - - return; } const fedAuthInfoToken = handler.fedAuthInfoToken; @@ -3471,6 +3462,25 @@ class Connection extends EventEmitter { const tokenStreamParser = this.createTokenStreamParser(message, new InitialSqlTokenHandler(this)); await once(tokenStreamParser, 'end'); } + + async handleRerouting() { + this.transitionTo(this.STATE.REROUTING); + + this.clearConnectTimer(); + this.closeConnection(); + + this.emit('rerouting'); + + this.socket!.destroy(); + this.socket = undefined; + this.closed = true; + this.loginError = undefined; + + this.debug.log('Rerouting to ' + this.routingData!.server + ':' + this.routingData!.port); + + this.transitionTo(this.STATE.CONNECTING); + this.initialiseConnection(); + } } function isTransientError(error: AggregateError | ConnectionError): boolean { From d1dd53bdda2090e39cf0121ab535404fa8004f5a Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 21:42:21 +0000 Subject: [PATCH 25/44] refactor: extract retry handling --- src/connection.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 4dc2a700a..8b78c19b4 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3274,10 +3274,7 @@ class Connection extends EventEmitter { } } else if (this.loginError) { if (isTransientError(this.loginError)) { - this.debug.log('Initiating retry on transient error'); - this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); - this.curTransientRetryCount++; - this.cleanupConnection(CLEANUP_TYPE.RETRY); + return await this.handleRetry(); } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3331,11 +3328,7 @@ class Connection extends EventEmitter { this.ntlmpacket = undefined; } else if (this.loginError) { if (isTransientError(this.loginError)) { - this.debug.log('Initiating retry on transient error'); - this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); - this.curTransientRetryCount++; - this.cleanupConnection(CLEANUP_TYPE.RETRY); - return; + return await this.handleRetry(); } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3427,10 +3420,7 @@ class Connection extends EventEmitter { return await this.handleLogin7WithStandardLoginResponse(signal); } else if (this.loginError) { if (isTransientError(this.loginError)) { - this.debug.log('Initiating retry on transient error'); - this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); - this.curTransientRetryCount++; - this.cleanupConnection(CLEANUP_TYPE.RETRY); + return await this.handleRetry(); } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3481,6 +3471,13 @@ class Connection extends EventEmitter { this.transitionTo(this.STATE.CONNECTING); this.initialiseConnection(); } + + async handleRetry() { + this.debug.log('Initiating retry on transient error'); + this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + this.curTransientRetryCount++; + this.cleanupConnection(CLEANUP_TYPE.RETRY); + } } function isTransientError(error: AggregateError | ConnectionError): boolean { From 0d9ca141aa9a205f66657f3434e14444f2ce6718 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 21:46:12 +0000 Subject: [PATCH 26/44] refactor: further retry cleanup --- src/connection.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 8b78c19b4..7447deec5 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2316,11 +2316,7 @@ class Connection extends EventEmitter { if (this.state === this.STATE.REROUTING) { } else if (this.state === this.STATE.TRANSIENT_FAILURE_RETRY) { - const server = this.routingData ? this.routingData.server : this.config.server; - const port = this.routingData ? this.routingData.port : this.config.options.port; - this.debug.log('Retry after transient failure connecting to ' + server + ':' + port); - this.createRetryTimer(); } else { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); @@ -3461,7 +3457,6 @@ class Connection extends EventEmitter { this.emit('rerouting'); - this.socket!.destroy(); this.socket = undefined; this.closed = true; this.loginError = undefined; @@ -3476,7 +3471,20 @@ class Connection extends EventEmitter { this.debug.log('Initiating retry on transient error'); this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); this.curTransientRetryCount++; - this.cleanupConnection(CLEANUP_TYPE.RETRY); + + this.clearConnectTimer(); + this.clearRetryTimer(); + this.closeConnection(); + + this.socket = undefined; + this.closed = true; + this.loginError = undefined; + + const server = this.routingData ? this.routingData.server : this.config.server; + const port = this.routingData ? this.routingData.port : this.config.options.port; + this.debug.log('Retry after transient failure connecting to ' + server + ':' + port); + + this.createRetryTimer(); } } From 1052b370d17eab70134380334dcb9a6d3ef5ec55 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 21:50:43 +0000 Subject: [PATCH 27/44] refactor: get rid of the retry timeout property --- src/connection.ts | 45 ++++++++------------------------------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 7447deec5..0c6e82a0c 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1018,10 +1018,6 @@ class Connection extends EventEmitter { * @private */ requestTimer: undefined | NodeJS.Timeout; - /** - * @private - */ - retryTimer: undefined | NodeJS.Timeout; /** * @private @@ -1959,7 +1955,6 @@ class Connection extends EventEmitter { if (!this.closed) { this.clearConnectTimer(); this.clearRequestTimer(); - this.clearRetryTimer(); this.closeConnection(); if (cleanupType === CLEANUP_TYPE.REDIRECT) { this.emit('rerouting'); @@ -2125,16 +2120,6 @@ class Connection extends EventEmitter { } } - /** - * @private - */ - createRetryTimer() { - this.clearRetryTimer(); - this.retryTimer = setTimeout(() => { - this.retryTimeout(); - }, this.config.options.connectionRetryInterval); - } - /** * @private */ @@ -2176,16 +2161,6 @@ class Connection extends EventEmitter { request.error = new RequestError(message, 'ETIMEOUT'); } - /** - * @private - */ - retryTimeout() { - this.retryTimer = undefined; - this.emit('retry'); - this.transitionTo(this.STATE.CONNECTING); - this.initialiseConnection(); - } - /** * @private */ @@ -2216,16 +2191,6 @@ class Connection extends EventEmitter { } } - /** - * @private - */ - clearRetryTimer() { - if (this.retryTimer) { - clearTimeout(this.retryTimer); - this.retryTimer = undefined; - } - } - /** * @private */ @@ -3473,7 +3438,6 @@ class Connection extends EventEmitter { this.curTransientRetryCount++; this.clearConnectTimer(); - this.clearRetryTimer(); this.closeConnection(); this.socket = undefined; @@ -3484,7 +3448,14 @@ class Connection extends EventEmitter { const port = this.routingData ? this.routingData.port : this.config.options.port; this.debug.log('Retry after transient failure connecting to ' + server + ':' + port); - this.createRetryTimer(); + await new Promise((resolve, _reject) => { + setTimeout(resolve, this.config.options.connectionRetryInterval); + }); + + this.emit('retry'); + + this.transitionTo(this.STATE.CONNECTING); + this.initialiseConnection(); } } From 2662fcbf6fd419aa457e9e7ab57ddcaf5526952f Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 21:52:14 +0000 Subject: [PATCH 28/44] refactor: add signal parameters --- src/connection.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 0c6e82a0c..0860bab98 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3229,13 +3229,13 @@ class Connection extends EventEmitter { if (handler.loginAckReceived) { if (handler.routingData) { this.routingData = handler.routingData; - return await this.handleRerouting(); + return await this.handleRerouting(signal); } else { return await this.loggedInSendingInitialSql(signal); } } else if (this.loginError) { if (isTransientError(this.loginError)) { - return await this.handleRetry(); + return await this.handleRetry(signal); } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3267,7 +3267,7 @@ class Connection extends EventEmitter { if (handler.loginAckReceived) { if (handler.routingData) { this.routingData = handler.routingData; - return await this.handleRerouting(); + return await this.handleRerouting(signal); } else { return await this.loggedInSendingInitialSql(signal); } @@ -3289,7 +3289,7 @@ class Connection extends EventEmitter { this.ntlmpacket = undefined; } else if (this.loginError) { if (isTransientError(this.loginError)) { - return await this.handleRetry(); + return await this.handleRetry(signal); } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3321,7 +3321,7 @@ class Connection extends EventEmitter { if (handler.loginAckReceived) { if (handler.routingData) { this.routingData = handler.routingData; - return await this.handleRerouting(); + return await this.handleRerouting(signal); } else { return await this.loggedInSendingInitialSql(signal); } @@ -3381,7 +3381,7 @@ class Connection extends EventEmitter { return await this.handleLogin7WithStandardLoginResponse(signal); } else if (this.loginError) { if (isTransientError(this.loginError)) { - return await this.handleRetry(); + return await this.handleRetry(signal); } else { this.emit('connect', this.loginError); this.transitionTo(this.STATE.FINAL); @@ -3414,7 +3414,7 @@ class Connection extends EventEmitter { await once(tokenStreamParser, 'end'); } - async handleRerouting() { + async handleRerouting(signal: AbortSignal) { this.transitionTo(this.STATE.REROUTING); this.clearConnectTimer(); @@ -3432,7 +3432,7 @@ class Connection extends EventEmitter { this.initialiseConnection(); } - async handleRetry() { + async handleRetry(signal: AbortSignal) { this.debug.log('Initiating retry on transient error'); this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); this.curTransientRetryCount++; From 07edcb05b3f9d646e4b761c40db604302a45342c Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 22:02:38 +0000 Subject: [PATCH 29/44] refactor: extract error wrapping --- src/connection.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 0860bab98..d87149874 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2241,7 +2241,21 @@ class Connection extends EventEmitter { /** * @private */ - socketError(error: Error) { + socketError(err: Error) { + if (this.state === this.STATE.CONNECTING || this.state === this.STATE.SENT_TLSSSLNEGOTIATION) { + const wrappedErr = this.wrapSocketError(err); + this.debug.log(wrappedErr.message); + this.emit('connect', wrappedErr); + } else { + const wrappedErr = this.wrapSocketError(err); + this.debug.log(wrappedErr.message); + this.emit('error', wrappedErr); + } + + this.dispatchEvent('socketError', err); + } + + wrapSocketError(err: Error) { if (this.state === this.STATE.CONNECTING || this.state === this.STATE.SENT_TLSSSLNEGOTIATION) { const hostPostfix = this.config.options.port ? `:${this.config.options.port}` : `\\${this.config.options.instanceName}`; // If we have routing data stored, this connection has been redirected @@ -2250,15 +2264,12 @@ class Connection extends EventEmitter { // Grab the target host from the connection configration, and from a redirect message // otherwise, leave the message empty. const routingMessage = this.routingData ? ` (redirected from ${this.config.server}${hostPostfix})` : ''; - const message = `Failed to connect to ${server}${port}${routingMessage} - ${error.message}`; - this.debug.log(message); - this.emit('connect', new ConnectionError(message, 'ESOCKET')); + const message = `Failed to connect to ${server}${port}${routingMessage} - ${err.message}`; + return new ConnectionError(message, 'ESOCKET'); } else { - const message = `Connection lost - ${error.message}`; - this.debug.log(message); - this.emit('error', new ConnectionError(message, 'ESOCKET')); + const message = `Connection lost - ${err.message}`; + return new ConnectionError(message, 'ESOCKET'); } - this.dispatchEvent('socketError', error); } /** From 068ac762bf831b4273d8c1700e39c461d8d05e04 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 22:20:39 +0000 Subject: [PATCH 30/44] refactor: simplify error handling during connection open --- src/connection.ts | 70 +++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index d87149874..387fdcfd3 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2043,16 +2043,22 @@ class Connection extends EventEmitter { const connect = customConnector || (multiSubnetFailover ? connectInParallel : connectInSequence); try { - let socket = await connect(connectOpts, dns.lookup, signal); + let socket; + + try { + socket = await connect(connectOpts, dns.lookup, signal); + } catch (err: any) { + throw this.wrapSocketError(err); + } if (this.config.options.encrypt === 'strict') { try { // Wrap the socket with TLS for TDS 8.0 socket = await this.wrapWithTls(socket); - } catch (err) { + } catch (err: any) { socket.end(); - throw err; + throw this.wrapSocketError(err); } } @@ -2068,7 +2074,12 @@ class Connection extends EventEmitter { return; } - process.nextTick(() => { this.socketError(err); }); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + + process.nextTick(() => { + this.emit('connect', err); + }); } } @@ -3173,7 +3184,7 @@ class Connection extends EventEmitter { try { message = await this.messageIo.readMessage(); } catch (err: any) { - return this.socketError(err); + throw this.wrapSocketError(err); } for await (const data of message) { @@ -3190,15 +3201,14 @@ class Connection extends EventEmitter { } if ('strict' !== this.config.options.encrypt && (preloginPayload.encryptionString === 'ON' || preloginPayload.encryptionString === 'REQ')) { if (!this.config.options.encrypt) { - this.emit('connect', new ConnectionError("Server requires encryption, set 'encrypt' config option to true.", 'EENCRYPT')); - return this.close(); + throw new ConnectionError("Server requires encryption, set 'encrypt' config option to true.", 'EENCRYPT'); } try { this.transitionTo(this.STATE.SENT_TLSSSLNEGOTIATION); await this.messageIo.startTls(this.secureContextOptions, this.config.options.serverName ? this.config.options.serverName : this.routingData?.server ?? this.config.server, this.config.options.trustServerCertificate); } catch (err: any) { - return this.socketError(err); + throw this.wrapSocketError(err); } } @@ -3229,7 +3239,7 @@ class Connection extends EventEmitter { try { message = await this.messageIo.readMessage(); } catch (err: any) { - return this.socketError(err); + throw this.wrapSocketError(err); } const handler = new Login7TokenHandler(this); @@ -3248,14 +3258,10 @@ class Connection extends EventEmitter { if (isTransientError(this.loginError)) { return await this.handleRetry(signal); } else { - this.emit('connect', this.loginError); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); + throw this.loginError; } } else { - this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); + throw new ConnectionError('Login failed.', 'ELOGIN'); } } @@ -3267,7 +3273,7 @@ class Connection extends EventEmitter { try { message = await this.messageIo.readMessage(); } catch (err: any) { - return this.socketError(err); + throw this.wrapSocketError(err); } const handler = new Login7TokenHandler(this); @@ -3302,16 +3308,10 @@ class Connection extends EventEmitter { if (isTransientError(this.loginError)) { return await this.handleRetry(signal); } else { - this.emit('connect', this.loginError); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - return; + throw this.loginError; } } else { - this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - return; + throw new ConnectionError('Login failed.', 'ELOGIN'); } } } @@ -3323,7 +3323,7 @@ class Connection extends EventEmitter { try { message = await this.messageIo.readMessage(); } catch (err: any) { - return this.socketError(err); + throw this.wrapSocketError(err); } const handler = new Login7TokenHandler(this); @@ -3377,31 +3377,22 @@ class Connection extends EventEmitter { try { tokenResponse = await credentials.getToken(tokenScope); } catch (err) { - this.loginError = new AggregateError( - [new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'), err]); - this.emit('connect', this.loginError); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - return; + throw new AggregateError([new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'), err]); } - const token = tokenResponse.token; this.sendFedAuthTokenMessage(token); + // sent the fedAuth token message, the rest is similar to standard login 7 return await this.handleLogin7WithStandardLoginResponse(signal); } else if (this.loginError) { if (isTransientError(this.loginError)) { return await this.handleRetry(signal); } else { - this.emit('connect', this.loginError); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); + throw this.loginError; } } else { - this.emit('connect', new ConnectionError('Login failed.', 'ELOGIN')); - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); + throw new ConnectionError('Login failed.', 'ELOGIN'); } } @@ -3419,8 +3410,9 @@ class Connection extends EventEmitter { try { message = await this.messageIo.readMessage(); } catch (err: any) { - return this.socketError(err); + throw this.wrapSocketError(err); } + const tokenStreamParser = this.createTokenStreamParser(message, new InitialSqlTokenHandler(this)); await once(tokenStreamParser, 'end'); } From adf490b9d84621362af58a8b175a0826586c6c98 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 25 Jul 2023 22:37:45 +0000 Subject: [PATCH 31/44] fixup: wait for connections to be closed --- src/connection.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/connection.ts b/src/connection.ts index 387fdcfd3..b38bb8af8 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3422,6 +3422,7 @@ class Connection extends EventEmitter { this.clearConnectTimer(); this.closeConnection(); + await once(this.socket!, 'close'); this.emit('rerouting'); @@ -3442,6 +3443,7 @@ class Connection extends EventEmitter { this.clearConnectTimer(); this.closeConnection(); + await once(this.socket!, 'close'); this.socket = undefined; this.closed = true; From 8ad7774e8d9652859ec3e03afa889a71ccf62fa5 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Tue, 19 Sep 2023 16:59:03 +0000 Subject: [PATCH 32/44] Further cleanups. --- src/connection.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 437197d24..81b40e133 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -408,10 +408,6 @@ interface State { exit?(this: Connection, newState: State): void; events: { socketError?(this: Connection, err: Error): void; - connectTimeout?(this: Connection): void; - message?(this: Connection, message: Message): void; - retry?(this: Connection): void; - reconnect?(this: Connection): void; }; } @@ -3531,8 +3527,6 @@ Connection.prototype.STATE = { REROUTING: { name: 'ReRouting', events: { - message: function() { - }, socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); @@ -3542,8 +3536,6 @@ Connection.prototype.STATE = { TRANSIENT_FAILURE_RETRY: { name: 'TRANSIENT_FAILURE_RETRY', events: { - message: function() { - }, socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); @@ -3589,7 +3581,7 @@ Connection.prototype.STATE = { LOGGED_IN_SENDING_INITIAL_SQL: { name: 'LoggedInSendingInitialSql', events: { - socketError: function socketError() { + socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); } @@ -3750,9 +3742,6 @@ Connection.prototype.STATE = { FINAL: { name: 'Final', events: { - message: function() { - // Do nothing - }, socketError: function() { // Do nothing } From 14329d81ee9d55e27b6a78f974e2533f2a3248c9 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Wed, 20 Sep 2023 16:03:35 +0000 Subject: [PATCH 33/44] Convert `makeRequest` internals to `async`/`await`. --- src/connection.ts | 84 ++++++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 30 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 81b40e133..6d4a45316 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3100,49 +3100,73 @@ class Connection extends EventEmitter { request.rows! = []; request.rst! = []; - const onCancel = () => { - payloadStream.unpipe(message); - payloadStream.destroy(new RequestError('Canceled.', 'ECANCEL')); - - // set the ignore bit and end the message. - message.ignore = true; - message.end(); - - if (request instanceof Request && request.paused) { - // resume the request if it was paused so we can read the remaining tokens - request.resume(); - } - }; - - request.once('cancel', onCancel); - this.createRequestTimer(); const message = new Message({ type: packetType, resetConnection: this.resetConnectionOnNextRequest }); this.messageIo.outgoingMessageStream.write(message); this.transitionTo(this.STATE.SENT_CLIENT_REQUEST); - message.once('finish', () => { - request.removeListener('cancel', onCancel); + const payloadStream = Readable.from(payload); + + (async () => { + const onCancel = () => { + payloadStream.destroy(new RequestError('Canceled.', 'ECANCEL')); + }; + request.once('cancel', onCancel); + + // Cleanup for onCancel + try { + // Handle errors coming from payloadStream + try { + for await (const chunk of payloadStream) { + if (message.write(chunk) === false) { + // Wait for the message to drain, or the request to be cancelled. + await new Promise((resolve) => { + const onDrain = () => { + request.removeListener('cancel', onCancel); + message.removeListener('drain', onDrain); + + resolve(); + }; + + const onCancel = () => { + request.removeListener('cancel', onCancel); + message.removeListener('drain', onDrain); + + resolve(); + }; + + message.once('drain', onDrain); + request.once('cancel', onCancel); + }); + } + } + } catch (err: any) { + request.error ??= err; + message.ignore = true; + + if (request instanceof Request && request.paused) { + // resume the request if it was paused so we can read the remaining tokens + request.resume(); + } + } + + message.end(); + } finally { + request.removeListener('cancel', onCancel); + } + request.once('cancel', this._cancelAfterRequestSent); this.resetConnectionOnNextRequest = false; this.debug.payload(function() { return payload!.toString(' '); }); + })().catch((err) => { + process.nextTick(() => { + throw err; + }); }); - - const payloadStream = Readable.from(payload); - payloadStream.once('error', (error) => { - payloadStream.unpipe(message); - - // Only set a request error if no error was set yet. - request.error ??= error; - - message.ignore = true; - message.end(); - }); - payloadStream.pipe(message); } } From 9b2be0b8e4a178b55109f61297544bf983da218d Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Wed, 20 Sep 2023 16:15:49 +0000 Subject: [PATCH 34/44] move response handling into `makeRequest` --- src/connection.ts | 154 +++++++++++++++++++++++----------------------- 1 file changed, 76 insertions(+), 78 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 6d4a45316..453d4557c 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3162,6 +3162,82 @@ class Connection extends EventEmitter { this.debug.payload(function() { return payload!.toString(' '); }); + + { + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } + // request timer is stopped on first data package + this.clearRequestTimer(); + + const tokenStreamParser = this.createTokenStreamParser(message, new RequestTokenHandler(this, this.request!)); + + // If the request was canceled and we have a `cancelTimer` + // defined, we send a attention message after the + // request message was fully sent off. + // + // We already started consuming the current message + // (but all the token handlers should be no-ops), and + // need to ensure the next message is handled by the + // `SENT_ATTENTION` state. + if (this.request?.canceled && this.cancelTimer) { + return this.transitionTo(this.STATE.SENT_ATTENTION); + } + + const onResume = () => { + tokenStreamParser.resume(); + }; + const onPause = () => { + tokenStreamParser.pause(); + + this.request?.once('resume', onResume); + }; + + this.request?.on('pause', onPause); + + if (this.request instanceof Request && this.request.paused) { + onPause(); + } + + const onCancel = () => { + tokenStreamParser.removeListener('end', onEndOfMessage); + + if (this.request instanceof Request && this.request.paused) { + // resume the request if it was paused so we can read the remaining tokens + this.request.resume(); + } + + this.request?.removeListener('pause', onPause); + this.request?.removeListener('resume', onResume); + + // The `_cancelAfterRequestSent` callback will have sent a + // attention message, so now we need to also switch to + // the `SENT_ATTENTION` state to make sure the attention ack + // message is processed correctly. + this.transitionTo(this.STATE.SENT_ATTENTION); + }; + + const onEndOfMessage = () => { + this.request?.removeListener('cancel', this._cancelAfterRequestSent); + this.request?.removeListener('cancel', onCancel); + this.request?.removeListener('pause', onPause); + this.request?.removeListener('resume', onResume); + + this.transitionTo(this.STATE.LOGGED_IN); + const sqlRequest = this.request as Request; + this.request = undefined; + if (this.config.options.tdsVersion < '7_2' && sqlRequest.error && this.isSqlBatch) { + this.inTransaction = false; + } + sqlRequest.callback(sqlRequest.error, sqlRequest.rowCount, sqlRequest.rows); + }; + + tokenStreamParser.once('end', onEndOfMessage); + this.request?.once('cancel', onCancel); + } })().catch((err) => { process.nextTick(() => { throw err; @@ -3622,84 +3698,6 @@ Connection.prototype.STATE = { }, SENT_CLIENT_REQUEST: { name: 'SentClientRequest', - enter: function() { - (async () => { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } - // request timer is stopped on first data package - this.clearRequestTimer(); - - const tokenStreamParser = this.createTokenStreamParser(message, new RequestTokenHandler(this, this.request!)); - - // If the request was canceled and we have a `cancelTimer` - // defined, we send a attention message after the - // request message was fully sent off. - // - // We already started consuming the current message - // (but all the token handlers should be no-ops), and - // need to ensure the next message is handled by the - // `SENT_ATTENTION` state. - if (this.request?.canceled && this.cancelTimer) { - return this.transitionTo(this.STATE.SENT_ATTENTION); - } - - const onResume = () => { - tokenStreamParser.resume(); - }; - const onPause = () => { - tokenStreamParser.pause(); - - this.request?.once('resume', onResume); - }; - - this.request?.on('pause', onPause); - - if (this.request instanceof Request && this.request.paused) { - onPause(); - } - - const onCancel = () => { - tokenStreamParser.removeListener('end', onEndOfMessage); - - if (this.request instanceof Request && this.request.paused) { - // resume the request if it was paused so we can read the remaining tokens - this.request.resume(); - } - - this.request?.removeListener('pause', onPause); - this.request?.removeListener('resume', onResume); - - // The `_cancelAfterRequestSent` callback will have sent a - // attention message, so now we need to also switch to - // the `SENT_ATTENTION` state to make sure the attention ack - // message is processed correctly. - this.transitionTo(this.STATE.SENT_ATTENTION); - }; - - const onEndOfMessage = () => { - this.request?.removeListener('cancel', this._cancelAfterRequestSent); - this.request?.removeListener('cancel', onCancel); - this.request?.removeListener('pause', onPause); - this.request?.removeListener('resume', onResume); - - this.transitionTo(this.STATE.LOGGED_IN); - const sqlRequest = this.request as Request; - this.request = undefined; - if (this.config.options.tdsVersion < '7_2' && sqlRequest.error && this.isSqlBatch) { - this.inTransaction = false; - } - sqlRequest.callback(sqlRequest.error, sqlRequest.rowCount, sqlRequest.rows); - }; - - tokenStreamParser.once('end', onEndOfMessage); - this.request?.once('cancel', onCancel); - })(); - - }, exit: function(nextState) { this.clearRequestTimer(); }, From a8361311e471201fca764ca2e9423579c83b678b Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Wed, 20 Sep 2023 16:20:17 +0000 Subject: [PATCH 35/44] Cleanup response handling code to use scope local `request`. --- src/connection.ts | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 453d4557c..db600134f 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3173,7 +3173,7 @@ class Connection extends EventEmitter { // request timer is stopped on first data package this.clearRequestTimer(); - const tokenStreamParser = this.createTokenStreamParser(message, new RequestTokenHandler(this, this.request!)); + const tokenStreamParser = this.createTokenStreamParser(message, new RequestTokenHandler(this, request)); // If the request was canceled and we have a `cancelTimer` // defined, we send a attention message after the @@ -3183,7 +3183,7 @@ class Connection extends EventEmitter { // (but all the token handlers should be no-ops), and // need to ensure the next message is handled by the // `SENT_ATTENTION` state. - if (this.request?.canceled && this.cancelTimer) { + if (request.canceled && this.cancelTimer) { return this.transitionTo(this.STATE.SENT_ATTENTION); } @@ -3193,25 +3193,25 @@ class Connection extends EventEmitter { const onPause = () => { tokenStreamParser.pause(); - this.request?.once('resume', onResume); + request.once('resume', onResume); }; - this.request?.on('pause', onPause); + request.on('pause', onPause); - if (this.request instanceof Request && this.request.paused) { + if (request instanceof Request && request.paused) { onPause(); } const onCancel = () => { tokenStreamParser.removeListener('end', onEndOfMessage); - if (this.request instanceof Request && this.request.paused) { + if (request instanceof Request && request.paused) { // resume the request if it was paused so we can read the remaining tokens - this.request.resume(); + request.resume(); } - this.request?.removeListener('pause', onPause); - this.request?.removeListener('resume', onResume); + request.removeListener('pause', onPause); + request.removeListener('resume', onResume); // The `_cancelAfterRequestSent` callback will have sent a // attention message, so now we need to also switch to @@ -3221,22 +3221,21 @@ class Connection extends EventEmitter { }; const onEndOfMessage = () => { - this.request?.removeListener('cancel', this._cancelAfterRequestSent); - this.request?.removeListener('cancel', onCancel); - this.request?.removeListener('pause', onPause); - this.request?.removeListener('resume', onResume); + request.removeListener('cancel', this._cancelAfterRequestSent); + request.removeListener('cancel', onCancel); + request.removeListener('pause', onPause); + request.removeListener('resume', onResume); this.transitionTo(this.STATE.LOGGED_IN); - const sqlRequest = this.request as Request; this.request = undefined; - if (this.config.options.tdsVersion < '7_2' && sqlRequest.error && this.isSqlBatch) { + if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { this.inTransaction = false; } - sqlRequest.callback(sqlRequest.error, sqlRequest.rowCount, sqlRequest.rows); + request.callback(request.error, request.rowCount, request.rows); }; tokenStreamParser.once('end', onEndOfMessage); - this.request?.once('cancel', onCancel); + request.once('cancel', onCancel); } })().catch((err) => { process.nextTick(() => { From 40f7c7a4e2aa29b9bcf3eae4ced04990de9650eb Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Wed, 20 Sep 2023 16:25:49 +0000 Subject: [PATCH 36/44] Remove `_cancelAfterRequestSent` connection property --- src/connection.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index db600134f..ccf793e96 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1015,11 +1015,6 @@ class Connection extends EventEmitter { */ requestTimer: undefined | NodeJS.Timeout; - /** - * @private - */ - _cancelAfterRequestSent: () => void; - /** * @private */ @@ -1733,11 +1728,6 @@ class Connection extends EventEmitter { this.transientErrorLookup = new TransientErrorLookup(); this.state = this.STATE.INITIALIZED; - - this._cancelAfterRequestSent = () => { - this.messageIo.sendMessage(TYPE.ATTENTION); - this.createCancelTimer(); - }; } connect(connectListener?: (err?: Error) => void) { @@ -3156,7 +3146,11 @@ class Connection extends EventEmitter { request.removeListener('cancel', onCancel); } - request.once('cancel', this._cancelAfterRequestSent); + const onCancelAfterRequestSent = () => { + this.messageIo.sendMessage(TYPE.ATTENTION); + this.createCancelTimer(); + }; + request.once('cancel', onCancelAfterRequestSent); this.resetConnectionOnNextRequest = false; this.debug.payload(function() { @@ -3221,7 +3215,7 @@ class Connection extends EventEmitter { }; const onEndOfMessage = () => { - request.removeListener('cancel', this._cancelAfterRequestSent); + request.removeListener('cancel', onCancelAfterRequestSent); request.removeListener('cancel', onCancel); request.removeListener('pause', onPause); request.removeListener('resume', onResume); From ada474c3d2c874ce6d205d2cc9a0999255643e36 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Wed, 20 Sep 2023 18:37:53 +0000 Subject: [PATCH 37/44] Drop `TokenStreamParser` wrapper. --- src/connection.ts | 139 ++++++++++-------- src/token/token-stream-parser.ts | 46 ------ ...m-parser-test.js => stream-parser-test.js} | 36 +++-- 3 files changed, 99 insertions(+), 122 deletions(-) delete mode 100644 src/token/token-stream-parser.ts rename test/unit/token/{token-stream-parser-test.js => stream-parser-test.js} (54%) diff --git a/src/connection.ts b/src/connection.ts index ccf793e96..7f2fdb59e 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -29,7 +29,7 @@ import Request from './request'; import RpcRequestPayload from './rpcrequest-payload'; import SqlBatchPayload from './sqlbatch-payload'; import MessageIO from './message-io'; -import { Parser as TokenStreamParser } from './token/token-stream-parser'; +import TokenStreamParser from './token/stream-parser'; import { Transaction, ISOLATION_LEVEL, assertValidIsolationLevel } from './transaction'; import { ConnectionError, RequestError } from './errors'; import { connectInParallel, connectInSequence } from './connector'; @@ -1976,8 +1976,8 @@ class Connection extends EventEmitter { /** * @private */ - createTokenStreamParser(message: Message, handler: TokenHandler) { - return new TokenStreamParser(message, this.debug, handler, this.config.options); + createTokenStreamParser(message: Message) { + return TokenStreamParser.parseTokens(message, this.debug, this.config.options); } performSocketSetup(socket: net.Socket) { @@ -3167,69 +3167,83 @@ class Connection extends EventEmitter { // request timer is stopped on first data package this.clearRequestTimer(); - const tokenStreamParser = this.createTokenStreamParser(message, new RequestTokenHandler(this, request)); + const onCancel = () => { + // The `_cancelAfterRequestSent` callback will have sent a + // attention message, so now we need to also switch to + // the `SENT_ATTENTION` state to make sure the attention ack + // message is processed correctly. + this.transitionTo(this.STATE.SENT_ATTENTION); + }; + request.once('cancel', onCancel); - // If the request was canceled and we have a `cancelTimer` - // defined, we send a attention message after the - // request message was fully sent off. - // - // We already started consuming the current message - // (but all the token handlers should be no-ops), and - // need to ensure the next message is handled by the - // `SENT_ATTENTION` state. - if (request.canceled && this.cancelTimer) { - return this.transitionTo(this.STATE.SENT_ATTENTION); - } + if (!request.canceled && request instanceof Request && request.paused) { + await new Promise((resolve, _) => { + const onResume = () => { + request.removeListener('cancel', onCancel); + request.removeListener('resume', onResume); - const onResume = () => { - tokenStreamParser.resume(); - }; - const onPause = () => { - tokenStreamParser.pause(); + resolve(); + }; - request.once('resume', onResume); - }; + const onCancel = () => { + request.removeListener('cancel', onCancel); + request.removeListener('resume', onResume); - request.on('pause', onPause); + resolve(); + }; - if (request instanceof Request && request.paused) { - onPause(); + request.on('cancel', onCancel); + request.on('resume', onResume); + }); } - const onCancel = () => { - tokenStreamParser.removeListener('end', onEndOfMessage); + const handler = new RequestTokenHandler(this, request); + for await (const token of this.createTokenStreamParser(message)) { + if (!request.canceled && request instanceof Request && request.paused) { + await new Promise((resolve, _) => { + const onResume = () => { + request.removeListener('cancel', onCancel); + request.removeListener('resume', onResume); - if (request instanceof Request && request.paused) { - // resume the request if it was paused so we can read the remaining tokens - request.resume(); - } + resolve(); + }; - request.removeListener('pause', onPause); - request.removeListener('resume', onResume); + const onCancel = () => { + request.removeListener('cancel', onCancel); + request.removeListener('resume', onResume); - // The `_cancelAfterRequestSent` callback will have sent a - // attention message, so now we need to also switch to - // the `SENT_ATTENTION` state to make sure the attention ack - // message is processed correctly. - this.transitionTo(this.STATE.SENT_ATTENTION); - }; + resolve(); + }; + + request.on('cancel', onCancel); + request.on('resume', onResume); + }); + } - const onEndOfMessage = () => { - request.removeListener('cancel', onCancelAfterRequestSent); - request.removeListener('cancel', onCancel); - request.removeListener('pause', onPause); - request.removeListener('resume', onResume); + handler[token.handlerName](token as any); + } + + request.removeListener('cancel', onCancelAfterRequestSent); + request.removeListener('cancel', onCancel); + // If the request was canceled and we have a `cancelTimer` + // defined, we send a attention message after the + // request message was fully sent off. + // + // We already started consuming the current message + // (but all the token handlers should be no-ops), and + // need to ensure the next message is handled by the + // `SENT_ATTENTION` state. + if (request.canceled && this.cancelTimer) { + return this.transitionTo(this.STATE.SENT_ATTENTION); + } else { this.transitionTo(this.STATE.LOGGED_IN); this.request = undefined; if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { this.inTransaction = false; } request.callback(request.error, request.rowCount, request.rows); - }; - - tokenStreamParser.once('end', onEndOfMessage); - request.once('cancel', onCancel); + } } })().catch((err) => { process.nextTick(() => { @@ -3363,9 +3377,9 @@ class Connection extends EventEmitter { } const handler = new Login7TokenHandler(this); - const tokenStreamParser = this.createTokenStreamParser(message, handler); - - await once(tokenStreamParser, 'end'); + for await (const token of this.createTokenStreamParser(message)) { + handler[token.handlerName](token as any); + } if (handler.loginAckReceived) { if (handler.routingData) { @@ -3397,9 +3411,9 @@ class Connection extends EventEmitter { } const handler = new Login7TokenHandler(this); - const tokenStreamParser = this.createTokenStreamParser(message, handler); - - await once(tokenStreamParser, 'end'); + for await (const token of this.createTokenStreamParser(message)) { + handler[token.handlerName](token as any); + } if (handler.loginAckReceived) { if (handler.routingData) { @@ -3447,8 +3461,10 @@ class Connection extends EventEmitter { } const handler = new Login7TokenHandler(this); - const tokenStreamParser = this.createTokenStreamParser(message, handler); - await once(tokenStreamParser, 'end'); + for await (const token of this.createTokenStreamParser(message)) { + handler[token.handlerName](token as any); + } + if (handler.loginAckReceived) { if (handler.routingData) { this.routingData = handler.routingData; @@ -3533,8 +3549,10 @@ class Connection extends EventEmitter { throw this.wrapSocketError(err); } - const tokenStreamParser = this.createTokenStreamParser(message, new InitialSqlTokenHandler(this)); - await once(tokenStreamParser, 'end'); + const handler = new InitialSqlTokenHandler(this); + for await (const token of this.createTokenStreamParser(message)) { + handler[token.handlerName](token as any); + } } async handleRerouting(signal: AbortSignal) { @@ -3717,9 +3735,10 @@ Connection.prototype.STATE = { } const handler = new AttentionTokenHandler(this, this.request!); - const tokenStreamParser = this.createTokenStreamParser(message, handler); + for await (const token of this.createTokenStreamParser(message)) { + handler[token.handlerName](token as any); + } - await once(tokenStreamParser, 'end'); // 3.2.5.7 Sent Attention State // Discard any data contained in the response, until we receive the attention response if (handler.attentionReceived) { diff --git a/src/token/token-stream-parser.ts b/src/token/token-stream-parser.ts deleted file mode 100644 index 8fc6c8017..000000000 --- a/src/token/token-stream-parser.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { EventEmitter } from 'events'; -import StreamParser, { type ParserOptions } from './stream-parser'; -import Debug from '../debug'; -import { Token } from './token'; -import { Readable } from 'stream'; -import Message from '../message'; -import { TokenHandler } from './handler'; - -export class Parser extends EventEmitter { - debug: Debug; - options: ParserOptions; - parser: Readable; - - constructor(message: Message, debug: Debug, handler: TokenHandler, options: ParserOptions) { - super(); - - this.debug = debug; - this.options = options; - - this.parser = Readable.from(StreamParser.parseTokens(message, this.debug, this.options)); - this.parser.on('data', (token: Token) => { - handler[token.handlerName as keyof TokenHandler](token as any); - }); - - this.parser.on('drain', () => { - this.emit('drain'); - }); - - this.parser.on('end', () => { - this.emit('end'); - }); - } - - declare on: ( - ((event: 'end', listener: () => void) => this) & - ((event: string | symbol, listener: (...args: any[]) => void) => this) - ); - - pause() { - return this.parser.pause(); - } - - resume() { - return this.parser.resume(); - } -} diff --git a/test/unit/token/token-stream-parser-test.js b/test/unit/token/stream-parser-test.js similarity index 54% rename from test/unit/token/token-stream-parser-test.js rename to test/unit/token/stream-parser-test.js index 4aa9dc664..168ae8d52 100644 --- a/test/unit/token/token-stream-parser-test.js +++ b/test/unit/token/stream-parser-test.js @@ -1,5 +1,5 @@ var Debug = require('../../../src/debug'); -var Parser = require('../../../src/token/token-stream-parser').Parser; +var Parser = require('../../../src/token/stream-parser'); var TYPE = require('../../../src/token/token').TYPE; var WritableTrackingBuffer = require('../../../src/tracking-buffer/writable-tracking-buffer'); const assert = require('chai').assert; @@ -25,28 +25,32 @@ function createDbChangeBuffer() { return buffer.data; } -describe('Token Stream Parser', () => { - it('should envChange', (done) => { +describe('Token Stream Parser', function() { + it('should envChange', async function() { const buffer = createDbChangeBuffer(); - const parser = new Parser([buffer], debug, { - onDatabaseChange: function(token) { - assert.isOk(token); - } - }); + const parser = Parser.parseTokens([buffer], debug, {}, []); - parser.on('end', done); + const tokens = []; + for await (const token of parser) { + tokens.push(token); + } + + assert.lengthOf(tokens, 1); + assert.strictEqual(tokens[0].name, 'ENVCHANGE'); }); - it('should split token across buffers', (done) => { + it('should split token across buffers', async function() { const buffer = createDbChangeBuffer(); - const parser = new Parser([buffer.slice(0, 6), buffer.slice(6)], debug, { - onDatabaseChange: function(token) { - assert.isOk(token); - } - }); + const parser = Parser.parseTokens([buffer.slice(0, 6), buffer.slice(6)], debug, {}, []); + + const tokens = []; + for await (const token of parser) { + tokens.push(token); + } - parser.on('end', done); + assert.lengthOf(tokens, 1); + assert.strictEqual(tokens[0].name, 'ENVCHANGE'); }); }); From b846990506a4159bc2dbc39e21622fb3993b9dea Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Thu, 21 Sep 2023 12:53:03 +0000 Subject: [PATCH 38/44] Move attention signal handling into `makeRequest`. --- src/connection.ts | 200 +++++++++++++++++++++++----------------------- 1 file changed, 99 insertions(+), 101 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 7f2fdb59e..d694e6c5f 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -405,7 +405,6 @@ interface KeyStoreProviderMap { interface State { name: string; enter?(this: Connection): void; - exit?(this: Connection, newState: State): void; events: { socketError?(this: Connection, err: Error): void; }; @@ -2228,10 +2227,6 @@ class Connection extends EventEmitter { 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; @@ -3146,59 +3141,52 @@ class Connection extends EventEmitter { request.removeListener('cancel', onCancel); } - const onCancelAfterRequestSent = () => { - this.messageIo.sendMessage(TYPE.ATTENTION); - this.createCancelTimer(); - }; - request.once('cancel', onCancelAfterRequestSent); - this.resetConnectionOnNextRequest = false; this.debug.payload(function() { return payload!.toString(' '); }); - { + if (request.canceled) { let message; try { message = await this.messageIo.readMessage(); } catch (err: any) { return this.socketError(err); + } finally { + // request timer is stopped on first data package (or error) + this.clearRequestTimer(); } - // request timer is stopped on first data package - this.clearRequestTimer(); - - const onCancel = () => { - // The `_cancelAfterRequestSent` callback will have sent a - // attention message, so now we need to also switch to - // the `SENT_ATTENTION` state to make sure the attention ack - // message is processed correctly. - this.transitionTo(this.STATE.SENT_ATTENTION); - }; - request.once('cancel', onCancel); - - if (!request.canceled && request instanceof Request && request.paused) { - await new Promise((resolve, _) => { - const onResume = () => { - request.removeListener('cancel', onCancel); - request.removeListener('resume', onResume); - resolve(); - }; - - const onCancel = () => { - request.removeListener('cancel', onCancel); - request.removeListener('resume', onResume); + const handler = new RequestTokenHandler(this, request); + for await (const token of this.createTokenStreamParser(message)) { + handler[token.handlerName](token as any); + } - resolve(); - }; + this.transitionTo(this.STATE.LOGGED_IN); - request.on('cancel', onCancel); - request.on('resume', onResume); - }); + this.request = undefined; + if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { + this.inTransaction = false; } + request.callback(request.error, request.rowCount, request.rows); + } else { + const onCancelAfterRequestSent = () => { + this.messageIo.sendMessage(TYPE.ATTENTION); + this.createCancelTimer(); + }; + request.once('cancel', onCancelAfterRequestSent); + + { + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } finally { + // request timer is stopped on first data package (or error) + this.clearRequestTimer(); + } - const handler = new RequestTokenHandler(this, request); - for await (const token of this.createTokenStreamParser(message)) { if (!request.canceled && request instanceof Request && request.paused) { await new Promise((resolve, _) => { const onResume = () => { @@ -3220,29 +3208,78 @@ class Connection extends EventEmitter { }); } - handler[token.handlerName](token as any); - } + const handler = new RequestTokenHandler(this, request); + for await (const token of this.createTokenStreamParser(message)) { + if (!request.canceled && request instanceof Request && request.paused) { + await new Promise((resolve, _) => { + const onResume = () => { + request.removeListener('cancel', onCancel); + request.removeListener('resume', onResume); - request.removeListener('cancel', onCancelAfterRequestSent); - request.removeListener('cancel', onCancel); + resolve(); + }; + + const onCancel = () => { + request.removeListener('cancel', onCancel); + request.removeListener('resume', onResume); + + resolve(); + }; + + request.on('cancel', onCancel); + request.on('resume', onResume); + }); + } + + handler[token.handlerName](token as any); + } + + request.removeListener('cancel', onCancelAfterRequestSent); + + // If the request was canceled and we have a `cancelTimer` + // defined, we send a attention message after the + // request message was fully sent off. + // + // We already started consuming the current message + // (but all the token handlers should be no-ops), and + // need to ensure the next message is handled by the + // `SENT_ATTENTION` state. + if (request.canceled) { + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } + + const handler = new AttentionTokenHandler(this, this.request!); + for await (const token of this.createTokenStreamParser(message)) { + handler[token.handlerName](token as any); + } + + // 3.2.5.7 Sent Attention State + // Discard any data contained in the response, until we receive the attention response + if (handler.attentionReceived) { + this.clearCancelTimer(); - // If the request was canceled and we have a `cancelTimer` - // defined, we send a attention message after the - // request message was fully sent off. - // - // We already started consuming the current message - // (but all the token handlers should be no-ops), and - // need to ensure the next message is handled by the - // `SENT_ATTENTION` state. - if (request.canceled && this.cancelTimer) { - return this.transitionTo(this.STATE.SENT_ATTENTION); - } else { - this.transitionTo(this.STATE.LOGGED_IN); - this.request = undefined; - if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { - this.inTransaction = false; + this.request = undefined; + this.transitionTo(this.STATE.LOGGED_IN); + + if (request.error && request.error instanceof RequestError && request.error.code === 'ETIMEOUT') { + request.callback(request.error); + } else { + request.callback(new RequestError('Canceled.', 'ECANCEL')); + } + } + } else { + this.transitionTo(this.STATE.LOGGED_IN); + + this.request = undefined; + if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { + this.inTransaction = false; + } + request.callback(request.error, request.rowCount, request.rows); } - request.callback(request.error, request.rowCount, request.rows); } } })().catch((err) => { @@ -3709,9 +3746,6 @@ Connection.prototype.STATE = { }, SENT_CLIENT_REQUEST: { name: 'SentClientRequest', - exit: function(nextState) { - this.clearRequestTimer(); - }, events: { socketError: function(err) { const sqlRequest = this.request!; @@ -3725,42 +3759,6 @@ Connection.prototype.STATE = { }, SENT_ATTENTION: { name: 'SentAttention', - enter: function() { - (async () => { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } - - const handler = new AttentionTokenHandler(this, this.request!); - for await (const token of this.createTokenStreamParser(message)) { - handler[token.handlerName](token as any); - } - - // 3.2.5.7 Sent Attention State - // Discard any data contained in the response, until we receive the attention response - if (handler.attentionReceived) { - this.clearCancelTimer(); - - const sqlRequest = this.request!; - this.request = undefined; - this.transitionTo(this.STATE.LOGGED_IN); - - if (sqlRequest.error && sqlRequest.error instanceof RequestError && sqlRequest.error.code === 'ETIMEOUT') { - sqlRequest.callback(sqlRequest.error); - } else { - sqlRequest.callback(new RequestError('Canceled.', 'ECANCEL')); - } - } - - })().catch((err) => { - process.nextTick(() => { - throw err; - }); - }); - }, events: { socketError: function(err) { const sqlRequest = this.request!; From d4b9646fb74b202f4b753c158ee22e4bbe9ac513 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Thu, 21 Sep 2023 12:53:35 +0000 Subject: [PATCH 39/44] Remove `enter` state handler. --- src/connection.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index d694e6c5f..e684ea211 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -404,7 +404,6 @@ interface KeyStoreProviderMap { */ interface State { name: string; - enter?(this: Connection): void; events: { socketError?(this: Connection, err: Error): void; }; @@ -2229,10 +2228,6 @@ class Connection extends EventEmitter { 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); - } } /** From 93dcc031c7bfb034a2e847cd890d9e8fb5366d08 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Thu, 21 Sep 2023 12:58:51 +0000 Subject: [PATCH 40/44] Ensure `cancel` handler gets cleaned up. --- src/connection.ts | 78 +++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index e684ea211..d44058c12 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3171,7 +3171,7 @@ class Connection extends EventEmitter { }; request.once('cancel', onCancelAfterRequestSent); - { + try { let message; try { message = await this.messageIo.readMessage(); @@ -3228,53 +3228,53 @@ class Connection extends EventEmitter { handler[token.handlerName](token as any); } - + } finally { request.removeListener('cancel', onCancelAfterRequestSent); + } - // If the request was canceled and we have a `cancelTimer` - // defined, we send a attention message after the - // request message was fully sent off. - // - // We already started consuming the current message - // (but all the token handlers should be no-ops), and - // need to ensure the next message is handled by the - // `SENT_ATTENTION` state. - if (request.canceled) { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } - - const handler = new AttentionTokenHandler(this, this.request!); - for await (const token of this.createTokenStreamParser(message)) { - handler[token.handlerName](token as any); - } + // If the request was canceled and we have a `cancelTimer` + // defined, we send a attention message after the + // request message was fully sent off. + // + // We already started consuming the current message + // (but all the token handlers should be no-ops), and + // need to ensure the next message is handled by the + // `SENT_ATTENTION` state. + if (request.canceled) { + let message; + try { + message = await this.messageIo.readMessage(); + } catch (err: any) { + return this.socketError(err); + } - // 3.2.5.7 Sent Attention State - // Discard any data contained in the response, until we receive the attention response - if (handler.attentionReceived) { - this.clearCancelTimer(); + const handler = new AttentionTokenHandler(this, this.request!); + for await (const token of this.createTokenStreamParser(message)) { + handler[token.handlerName](token as any); + } - this.request = undefined; - this.transitionTo(this.STATE.LOGGED_IN); + // 3.2.5.7 Sent Attention State + // Discard any data contained in the response, until we receive the attention response + if (handler.attentionReceived) { + this.clearCancelTimer(); - if (request.error && request.error instanceof RequestError && request.error.code === 'ETIMEOUT') { - request.callback(request.error); - } else { - request.callback(new RequestError('Canceled.', 'ECANCEL')); - } - } - } else { + this.request = undefined; this.transitionTo(this.STATE.LOGGED_IN); - this.request = undefined; - if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { - this.inTransaction = false; + if (request.error && request.error instanceof RequestError && request.error.code === 'ETIMEOUT') { + request.callback(request.error); + } else { + request.callback(new RequestError('Canceled.', 'ECANCEL')); } - request.callback(request.error, request.rowCount, request.rows); } + } else { + this.transitionTo(this.STATE.LOGGED_IN); + + this.request = undefined; + if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { + this.inTransaction = false; + } + request.callback(request.error, request.rowCount, request.rows); } } })().catch((err) => { From 85680180bc85c51686843ad8f3a3cac067d76ad3 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Thu, 21 Sep 2023 13:01:23 +0000 Subject: [PATCH 41/44] Reduce duplicated logic. --- src/connection.ts | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index d44058c12..e0b61a911 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3182,8 +3182,8 @@ class Connection extends EventEmitter { this.clearRequestTimer(); } - if (!request.canceled && request instanceof Request && request.paused) { - await new Promise((resolve, _) => { + const onCancelOrResume = async () => { + return await new Promise((resolve, _) => { const onResume = () => { request.removeListener('cancel', onCancel); request.removeListener('resume', onResume); @@ -3201,29 +3201,16 @@ class Connection extends EventEmitter { request.on('cancel', onCancel); request.on('resume', onResume); }); + }; + + if (!request.canceled && request instanceof Request && request.paused) { + await onCancelOrResume(); } const handler = new RequestTokenHandler(this, request); for await (const token of this.createTokenStreamParser(message)) { if (!request.canceled && request instanceof Request && request.paused) { - await new Promise((resolve, _) => { - const onResume = () => { - request.removeListener('cancel', onCancel); - request.removeListener('resume', onResume); - - resolve(); - }; - - const onCancel = () => { - request.removeListener('cancel', onCancel); - request.removeListener('resume', onResume); - - resolve(); - }; - - request.on('cancel', onCancel); - request.on('resume', onResume); - }); + await onCancelOrResume(); } handler[token.handlerName](token as any); From 4c86e2d4a163e03741f540c96803140fd84549de Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Thu, 21 Sep 2023 15:20:46 +0000 Subject: [PATCH 42/44] Use `request` local var. --- src/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/connection.ts b/src/connection.ts index e0b61a911..89cb8c22c 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3235,7 +3235,7 @@ class Connection extends EventEmitter { return this.socketError(err); } - const handler = new AttentionTokenHandler(this, this.request!); + const handler = new AttentionTokenHandler(this, request); for await (const token of this.createTokenStreamParser(message)) { handler[token.handlerName](token as any); } From 335313c9753600ecd66c10e5a928b9f98475c515 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Mon, 25 Sep 2023 09:35:40 +0000 Subject: [PATCH 43/44] Fix benchmark deprecation message. --- benchmarks/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/common.js b/benchmarks/common.js index 67b1b84d9..3111ee755 100644 --- a/benchmarks/common.js +++ b/benchmarks/common.js @@ -65,7 +65,7 @@ function Benchmark(fn, configs, options) { for (let i = 0; i < length; i++) { const entry = entries[i]; - const stats = this._gcStats[entry.kind]; + const stats = this._gcStats[entry.detail.kind]; if (stats) { stats.count += 1; From 08c2a039f9d08baaf794ab73b7cff0fe62024847 Mon Sep 17 00:00:00 2001 From: Arthur Schreiber Date: Wed, 27 Sep 2023 09:27:22 +0000 Subject: [PATCH 44/44] massive refactoring changes. --- src/connection.ts | 914 ++++++++---------- src/connector.ts | 23 +- src/message-io.ts | 350 +++++-- src/token/handler.ts | 10 +- src/token/stream-parser.ts | 18 +- test/integration/connection-test.js | 7 - .../integration/invalid-packet-stream-test.js | 2 +- test/integration/socket-error-test.js | 16 +- test/integration/transactions-test.js | 14 +- test/unit/connection-retry-test.js | 4 + test/unit/connection-test.ts | 76 +- test/unit/connector-test.js | 9 +- test/unit/message-io-test.ts | 430 +++++++- test/unit/rerouting-test.js | 8 +- 14 files changed, 1221 insertions(+), 660 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index 89cb8c22c..f63cf705b 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -35,7 +35,6 @@ import { ConnectionError, RequestError } from './errors'; import { connectInParallel, connectInSequence } from './connector'; import { name as libraryName } from './library'; import { versions } from './tds-versions'; -import Message from './message'; import { type Metadata } from './metadata-parser'; import { createNTLMRequest } from './ntlm'; import { ColumnEncryptionAzureKeyVaultProvider } from './always-encrypted/keystore-provider-azure-key-vault'; @@ -49,7 +48,7 @@ import Procedures from './special-stored-procedure'; import AggregateError from 'es-aggregate-error'; import { version } from '../package.json'; import { URL } from 'url'; -import { AttentionTokenHandler, InitialSqlTokenHandler, Login7TokenHandler, RequestTokenHandler, TokenHandler } from './token/handler'; +import { AttentionTokenHandler, InitialSqlTokenHandler, Login7TokenHandler, RequestTokenHandler } from './token/handler'; type BeginTransactionCallback = /** @@ -326,10 +325,6 @@ interface DefaultAuthentication { }; } -interface ErrorWithCode extends Error { - code?: string; -} - interface InternalConnectionConfig { server: string; authentication: DefaultAuthentication | NtlmAuthentication | AzureActiveDirectoryPasswordAuthentication | AzureActiveDirectoryMsiAppServiceAuthentication | AzureActiveDirectoryMsiVmAuthentication | AzureActiveDirectoryAccessTokenAuthentication | AzureActiveDirectoryServicePrincipalSecret | AzureActiveDirectoryDefaultAuthentication; @@ -1008,10 +1003,6 @@ class Connection extends EventEmitter { * @private */ cancelTimer: undefined | NodeJS.Timeout; - /** - * @private - */ - requestTimer: undefined | NodeJS.Timeout; /** * @private @@ -1749,7 +1740,9 @@ class Connection extends EventEmitter { } this.transitionTo(this.STATE.CONNECTING); - this.initialiseConnection(); + this.initialiseConnection().catch((err) => { + process.nextTick(() => { throw err; }); + }); } /** @@ -1889,6 +1882,7 @@ class Connection extends EventEmitter { * The [[Event_end]] will be emitted once the connection has been closed. */ close() { + this.socket?.end(); this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); } @@ -1896,12 +1890,38 @@ class Connection extends EventEmitter { /** * @private */ - initialiseConnection() { - const signal = this.createConnectTimer(); + async initialiseConnection() { + const controller = new AbortController(); + const connectTimer = setTimeout(() => { + const hostPostfix = this.config.options.port ? `:${this.config.options.port}` : `\\${this.config.options.instanceName}`; + // If we have routing data stored, this connection has been redirected + const server = this.routingData ? this.routingData.server : this.config.server; + const port = this.routingData ? `:${this.routingData.port}` : hostPostfix; + // Grab the target host from the connection configration, and from a redirect message + // otherwise, leave the message empty. + const routingMessage = this.routingData ? ` (redirected from ${this.config.server}${hostPostfix})` : ''; + const message = `Failed to connect to ${server}${port}${routingMessage} in ${this.config.options.connectTimeout}ms`; - this.establishConnection(signal).catch((err) => { - process.nextTick(() => { throw err; }); - }); + this.debug.log(message); + + const error = new ConnectionError(message, 'ETIMEOUT'); + + controller.abort(error); + + // TODO: Get rid of this call. + this.socket?.destroy(error); + }, this.config.options.connectTimeout); + + try { + await this.establishConnection(controller.signal); + } catch (err: any) { + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + + process.nextTick(() => { this.emit('connect', err); }); + } finally { + clearTimeout(connectTimer); + } } async establishConnection(signal: AbortSignal) { @@ -1916,20 +1936,11 @@ class Connection extends EventEmitter { signal: signal }); } catch (err: any) { - this.clearConnectTimer(); - - if (signal.aborted) { - // Ignore the AbortError for now, this is still handled by the connectTimer firing - return; - } - - return process.nextTick(() => { - this.emit('connect', new ConnectionError(err.message, 'EINSTLOOKUP')); - }); + throw new ConnectionError(err.message, 'EINSTLOOKUP'); } } - await this.connectOnPort(port, this.config.options.multiSubnetFailover, signal, this.config.options.connector); + await this.performConnection(this.config.server, port, signal); } /** @@ -1937,8 +1948,6 @@ class Connection extends EventEmitter { */ cleanupConnection(cleanupType: typeof CLEANUP_TYPE[keyof typeof CLEANUP_TYPE]) { if (!this.closed) { - this.clearConnectTimer(); - this.clearRequestTimer(); this.closeConnection(); if (cleanupType === CLEANUP_TYPE.REDIRECT) { this.emit('rerouting'); @@ -1948,13 +1957,6 @@ class Connection extends EventEmitter { }); } - const request = this.request; - if (request) { - const err = new RequestError('Connection closed before request completed.', 'ECLOSE'); - request.callback(err); - this.request = undefined; - } - this.closed = true; this.loginError = undefined; } @@ -1974,14 +1976,11 @@ class Connection extends EventEmitter { /** * @private */ - createTokenStreamParser(message: Message) { + createTokenStreamParser(message: AsyncIterable) { return TokenStreamParser.parseTokens(message, this.debug, this.config.options); } performSocketSetup(socket: net.Socket) { - socket.on('error', (error) => { this.socketError(error); }); - socket.on('close', () => { this.socketClose(); }); - socket.on('end', () => { this.socketEnd(); }); socket.setKeepAlive(true, KEEP_ALIVE_INITIAL_DELAY); this.messageIo = new MessageIO(socket, this.config.options.packetSize, this.debug); @@ -1993,10 +1992,10 @@ class Connection extends EventEmitter { this.debug.log('connected to ' + this.config.server + ':' + this.config.options.port); } - wrapWithTls(socket: net.Socket, signal: AbortSignal): Promise { + async wrapWithTls(socket: net.Socket, signal: AbortSignal): Promise { signal.throwIfAborted(); - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const secureContext = tls.createSecureContext(this.secureContextOptions); // If connect to an ip address directly, // need to set the servername to an empty string @@ -2013,6 +2012,8 @@ class Connection extends EventEmitter { const encryptsocket = tls.connect(encryptOptions); const onAbort = () => { + console.log('onAbort', signal.reason); + encryptsocket.removeListener('error', onError); encryptsocket.removeListener('connect', onConnect); @@ -2022,6 +2023,8 @@ class Connection extends EventEmitter { }; const onError = (err: Error) => { + console.log('onError'); + signal.removeEventListener('abort', onAbort); encryptsocket.removeListener('error', onError); @@ -2048,54 +2051,142 @@ class Connection extends EventEmitter { }); } - async connectOnPort(port: number, multiSubnetFailover: boolean, signal: AbortSignal, customConnector?: () => Promise) { - const connectOpts = { - host: this.routingData ? this.routingData.server : this.config.server, - port: this.routingData ? this.routingData.port : port, - localAddress: this.config.options.localAddress - }; + async performConnection(server: string, port: number, signal: AbortSignal) { + await this.performConnectionWithRetry(server, port, signal); - const connect = customConnector || (multiSubnetFailover ? connectInParallel : connectInSequence); + // Now that the connection has been established, send the initial SQL setup statement. + await this.sendInitialSql(signal); + await this.handleInitialSqlResponse(signal); - try { - let socket; + // Now that we're connected, add an error handler + this.socket!.on('error', (error) => { this.socketError(error); }); + + this.transitionTo(this.STATE.LOGGED_IN); + process.nextTick(() => { this.emit('connect'); }); + } + + async performConnectionWithRetry(server: string, port: number, signal: AbortSignal): Promise { + signal.throwIfAborted(); + let retryCount = 0; + while (true) { try { - socket = await connect(connectOpts, dns.lookup, signal); + return await this.performConnectionWithReroutingData(server, port, signal); } catch (err: any) { - throw this.wrapSocketError(err); + if (!err.isTransient || retryCount === this.config.options.maxRetriesOnTransientErrors) { + throw err; + } } - if (this.config.options.encrypt === 'strict') { - try { - // Wrap the socket with TLS for TDS 8.0 - socket = await this.wrapWithTls(socket, signal); - } catch (err: any) { - socket.end(); + this.closed = true; + this.loginError = undefined; - throw this.wrapSocketError(err); - } - } + this.curTransientRetryCount++; + + this.debug.log('Initiating retry on transient error'); + this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); + this.debug.log('Retry after transient failure connecting to ' + server + ':' + port); + + // TODO: handle connect signal firing here + await new Promise((resolve, _reject) => { + setTimeout(resolve, this.config.options.connectionRetryInterval); + }); - this.performSocketSetup(socket); + // TODO: Remove this event + process.nextTick(() => { this.emit('retry'); }); - this.sendPreLogin(); - this.transitionTo(this.STATE.SENT_PRELOGIN); - return await this.handlePreloginResponse(signal); + // Wait for `retry` event to have been handled + await Promise.resolve(); + + this.transitionTo(this.STATE.CONNECTING); + + retryCount += 1; + } + } + + async performConnectionWithReroutingData(server: string, port: number, signal: AbortSignal): Promise { + this.socket = await this.connectOnPort(server, port, this.config.options.multiSubnetFailover, signal, this.config.options.connector); + + let routingData; + try { + await this.sendPreLogin(); + await this.handlePreloginResponse(signal); + + await this.sendLogin7Packet(); + routingData = await this.handleLogin7Response(signal); } catch (err: any) { - this.clearConnectTimer(); + // cleanup the socket + this.socket.destroy(); + this.socket = undefined; + throw err; + } - if (signal.aborted) { - return; + if (!routingData) { + return; + } + + this.socket.destroy(); + this.socket = undefined; + + this.routingData = routingData; + + this.transitionTo(this.STATE.REROUTING); + this.debug.log('Rerouting to ' + this.routingData!.server + ':' + this.routingData!.port); + + // TODO: Remove this event + process.nextTick(() => { this.emit('rerouting'); }); + + // Wait for `rerouting` event to have been handled + await Promise.resolve(); + + this.closed = true; + this.loginError = undefined; + + this.transitionTo(this.STATE.CONNECTING); + + return await this.performConnectionWithReroutingData(routingData.server, routingData.port, signal); + } + + async connectOnPort(server: string, port: number, multiSubnetFailover: boolean, signal: AbortSignal, customConnector?: () => Promise) { + signal.throwIfAborted(); + + const connectOpts = { + host: server, + port: port, + localAddress: this.config.options.localAddress + }; + + const connect = customConnector || (multiSubnetFailover ? connectInParallel : connectInSequence); + + let socket; + try { + socket = await connect(connectOpts, dns.lookup, signal); + } catch (err: any) { + if (err instanceof ConnectionError) { + throw err; } - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); + throw this.wrapSocketError(err); + } - process.nextTick(() => { - this.emit('connect', err); - }); + if (this.config.options.encrypt === 'strict') { + try { + // Wrap the socket with TLS for TDS 8.0 + socket = await this.wrapWithTls(socket, signal); + } catch (err: any) { + socket.end(); + + if (err instanceof ConnectionError) { + throw err; + } + + throw this.wrapSocketError(err); + } } + + this.performSocketSetup(socket); + + return socket; } /** @@ -2107,18 +2198,6 @@ class Connection extends EventEmitter { } } - /** - * @private - */ - createConnectTimer() { - const controller = new AbortController(); - this.connectTimer = setTimeout(() => { - controller.abort(); - this.connectTimeout(); - }, this.config.options.connectTimeout); - return controller.signal; - } - /** * @private */ @@ -2132,40 +2211,6 @@ class Connection extends EventEmitter { } } - /** - * @private - */ - createRequestTimer() { - this.clearRequestTimer(); // release old timer, just to be safe - const request = this.request as Request; - const timeout = (request.timeout !== undefined) ? request.timeout : this.config.options.requestTimeout; - if (timeout) { - this.requestTimer = setTimeout(() => { - this.requestTimeout(); - }, timeout); - } - } - - /** - * @private - */ - connectTimeout() { - const hostPostfix = this.config.options.port ? `:${this.config.options.port}` : `\\${this.config.options.instanceName}`; - // If we have routing data stored, this connection has been redirected - const server = this.routingData ? this.routingData.server : this.config.server; - const port = this.routingData ? `:${this.routingData.port}` : hostPostfix; - // Grab the target host from the connection configration, and from a redirect message - // otherwise, leave the message empty. - const routingMessage = this.routingData ? ` (redirected from ${this.config.server}${hostPostfix})` : ''; - const message = `Failed to connect to ${server}${port}${routingMessage} in ${this.config.options.connectTimeout}ms`; - this.debug.log(message); - this.emit('connect', new ConnectionError(message, 'ETIMEOUT')); - this.connectTimer = undefined; - - this.transitionTo(this.STATE.FINAL); - this.cleanupConnection(CLEANUP_TYPE.NORMAL); - } - /** * @private */ @@ -2175,28 +2220,6 @@ class Connection extends EventEmitter { this.dispatchEvent('socketError', new ConnectionError(message, 'ETIMEOUT')); } - /** - * @private - */ - 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 = new RequestError(message, 'ETIMEOUT'); - } - - /** - * @private - */ - clearConnectTimer() { - if (this.connectTimer) { - clearTimeout(this.connectTimer); - this.connectTimer = undefined; - } - } - /** * @private */ @@ -2207,16 +2230,6 @@ class Connection extends EventEmitter { } } - /** - * @private - */ - clearRequestTimer() { - if (this.requestTimer) { - clearTimeout(this.requestTimer); - this.requestTimer = undefined; - } - } - /** * @private */ @@ -2230,19 +2243,6 @@ class Connection extends EventEmitter { this.state = newState; } - /** - * @private - */ - getEventHandler(eventName: T): NonNullable { - const handler = this.state.events[eventName]; - - if (!handler) { - throw new Error(`No event '${eventName}' in state '${this.state.name}'`); - } - - return handler!; - } - /** * @private */ @@ -2282,11 +2282,16 @@ class Connection extends EventEmitter { // Grab the target host from the connection configration, and from a redirect message // otherwise, leave the message empty. const routingMessage = this.routingData ? ` (redirected from ${this.config.server}${hostPostfix})` : ''; - const message = `Failed to connect to ${server}${port}${routingMessage} - ${err.message}`; - return new ConnectionError(message, 'ESOCKET'); + const message = `Failed to connect to ${server}${port}${routingMessage}`; + + const error = new ConnectionError(message, 'ESOCKET'); + error.cause = err; + return error; } else { - const message = `Connection lost - ${err.message}`; - return new ConnectionError(message, 'ESOCKET'); + const message = 'Connection lost'; + const error = new ConnectionError(message, 'ESOCKET'); + error.cause = err; + return error; } } @@ -2294,12 +2299,12 @@ class Connection extends EventEmitter { * @private */ socketEnd() { - this.debug.log('socket ended'); - if (this.state !== this.STATE.FINAL) { - const error: ErrorWithCode = new Error('socket hang up'); - error.code = 'ECONNRESET'; - this.socketError(error); - } + // this.debug.log('socket ended'); + // if (this.state !== this.STATE.FINAL) { + // const error: ErrorWithCode = new Error('socket hang up'); + // error.code = 'ECONNRESET'; + // this.socketError(error); + // } } /** @@ -2320,7 +2325,7 @@ class Connection extends EventEmitter { /** * @private */ - sendPreLogin() { + async sendPreLogin() { const [, major, minor, build] = /^(\d+)\.(\d+)\.(\d+)/.exec(version) ?? ['0.0.0', '0', '0', '0']; const payload = new PreloginPayload({ // If encrypt setting is set to 'strict', then we should have already done the encryption before calling @@ -2330,16 +2335,17 @@ class Connection extends EventEmitter { version: { major: Number(major), minor: Number(minor), build: Number(build), subbuild: 0 } }); - this.messageIo.sendMessage(TYPE.PRELOGIN, payload.data); + await this.messageIo.writeMessage(TYPE.PRELOGIN, [payload.data]); this.debug.payload(function() { return payload.toString(' '); }); + this.transitionTo(this.STATE.SENT_PRELOGIN); } /** * @private */ - sendLogin7Packet() { + async sendLogin7Packet() { const payload = new Login7Payload({ tdsVersion: versions[this.config.options.tdsVersion], packetSize: this.config.options.packetSize, @@ -2400,7 +2406,7 @@ class Connection extends EventEmitter { payload.initDbFatal = !this.config.options.fallbackToDefaultDb; this.routingData = undefined; - this.messageIo.sendMessage(TYPE.LOGIN7, payload.toBuffer()); + await this.messageIo.writeMessage(TYPE.LOGIN7, [payload.toBuffer()]); this.debug.payload(function() { return payload.toString(' '); @@ -2410,26 +2416,32 @@ class Connection extends EventEmitter { /** * @private */ - sendFedAuthTokenMessage(token: string) { + async sendFedAuthTokenMessage(token: string, signal: AbortSignal) { + signal.throwIfAborted(); + const accessTokenLen = Buffer.byteLength(token, 'ucs2'); + const data = Buffer.alloc(8 + accessTokenLen); let offset = 0; offset = data.writeUInt32LE(accessTokenLen + 4, offset); offset = data.writeUInt32LE(accessTokenLen, offset); data.write(token, offset, 'ucs2'); - this.messageIo.sendMessage(TYPE.FEDAUTH_TOKEN, data); + + // TODO: handle `signal` + await this.messageIo.writeMessage(TYPE.FEDAUTH_TOKEN, [data]); } /** * @private */ - sendInitialSql() { + async sendInitialSql(signal: AbortSignal) { + signal.throwIfAborted(); + this.transitionTo(this.STATE.LOGGED_IN_SENDING_INITIAL_SQL); const payload = new SqlBatchPayload(this.getInitialSql(), this.currentTransactionDescriptor(), this.config.options); - const message = new Message({ type: TYPE.SQL_BATCH }); - this.messageIo.outgoingMessageStream.write(message); - Readable.from(payload).pipe(message); + // TODO: handle `signal` + await this.messageIo.writeMessage(TYPE.SQL_BATCH, payload); } /** @@ -3062,7 +3074,9 @@ class Connection extends EventEmitter { if (this.state !== this.STATE.LOGGED_IN) { const message = 'Requests can only be made in the ' + this.STATE.LOGGED_IN.name + ' state, not the ' + this.state.name + ' state'; this.debug.log(message); - request.callback(new RequestError(message, 'EINVALIDSTATE')); + process.nextTick(() => { + request.callback(new RequestError(message, 'EINVALIDSTATE')); + }); } else if (request.canceled) { process.nextTick(() => { request.callback(new RequestError('Canceled.', 'ECANCEL')); @@ -3080,189 +3094,205 @@ class Connection extends EventEmitter { request.rows! = []; request.rst! = []; - this.createRequestTimer(); - - const message = new Message({ type: packetType, resetConnection: this.resetConnectionOnNextRequest }); - this.messageIo.outgoingMessageStream.write(message); this.transitionTo(this.STATE.SENT_CLIENT_REQUEST); const payloadStream = Readable.from(payload); (async () => { - const onCancel = () => { - payloadStream.destroy(new RequestError('Canceled.', 'ECANCEL')); - }; - request.once('cancel', onCancel); + const request = this.request as Request; + const timeout = (request.timeout !== undefined) ? request.timeout : this.config.options.requestTimeout; + + let requestTimer; + if (timeout) { + requestTimer = setTimeout(() => { + request.cancel(); + const message = `Timeout: Request failed to complete in ${timeout}ms`; + request.error = new RequestError(message, 'ETIMEOUT'); + }, timeout); + } - // Cleanup for onCancel try { - // Handle errors coming from payloadStream - try { - for await (const chunk of payloadStream) { - if (message.write(chunk) === false) { - // Wait for the message to drain, or the request to be cancelled. - await new Promise((resolve) => { - const onDrain = () => { - request.removeListener('cancel', onCancel); - message.removeListener('drain', onDrain); + const onCancel = () => { + payloadStream.destroy(new RequestError('Canceled.', 'ECANCEL')); + }; + request.once('cancel', onCancel); - resolve(); - }; + // Cleanup for onCancel + try { + // Handle errors coming from payloadStream + try { + await this.messageIo.writeMessage(packetType, payloadStream, this.resetConnectionOnNextRequest); + } catch (err: any) { + request.error ??= err; - const onCancel = () => { - request.removeListener('cancel', onCancel); - message.removeListener('drain', onDrain); + if (request instanceof Request && request.paused) { + // resume the request if it was paused so we can read the remaining tokens + request.resume(); + } - resolve(); - }; + // TODO: Handle this better? Maybe through an AbortSignal to `.writeMessage`? + if (!this.socket?.readable) { + this.request = undefined; + this.transitionTo(this.STATE.FINAL); - message.once('drain', onDrain); - request.once('cancel', onCancel); - }); + process.nextTick(() => { request.callback(request.error); }); + return; } } - } catch (err: any) { - request.error ??= err; - message.ignore = true; - - if (request instanceof Request && request.paused) { - // resume the request if it was paused so we can read the remaining tokens - request.resume(); - } - } - - message.end(); - } finally { - request.removeListener('cancel', onCancel); - } - - this.resetConnectionOnNextRequest = false; - this.debug.payload(function() { - return payload!.toString(' '); - }); - - if (request.canceled) { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); } finally { - // request timer is stopped on first data package (or error) - this.clearRequestTimer(); + request.removeListener('cancel', onCancel); } - const handler = new RequestTokenHandler(this, request); - for await (const token of this.createTokenStreamParser(message)) { - handler[token.handlerName](token as any); - } + this.resetConnectionOnNextRequest = false; + this.debug.payload(function() { + return payload!.toString(' '); + }); + + if (request.canceled) { + const handler = new RequestTokenHandler(this, request); + for await (const token of this.createTokenStreamParser(this.messageIo.readMessage())) { + // request timer is stopped on first data package + clearTimeout(requestTimer); - this.transitionTo(this.STATE.LOGGED_IN); + handler[token.handlerName](token as any); + } - this.request = undefined; - if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { - this.inTransaction = false; - } - request.callback(request.error, request.rowCount, request.rows); - } else { - const onCancelAfterRequestSent = () => { - this.messageIo.sendMessage(TYPE.ATTENTION); - this.createCancelTimer(); - }; - request.once('cancel', onCancelAfterRequestSent); + this.transitionTo(this.STATE.LOGGED_IN); - try { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } finally { - // request timer is stopped on first data package (or error) - this.clearRequestTimer(); + this.request = undefined; + if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { + this.inTransaction = false; } - const onCancelOrResume = async () => { - return await new Promise((resolve, _) => { - const onResume = () => { - request.removeListener('cancel', onCancel); - request.removeListener('resume', onResume); + process.nextTick(() => { + request.callback(request.error, request.rowCount, request.rows); + }); + } else { + const onCancelAfterRequestSent = async () => { + this.createCancelTimer(); + await this.messageIo.writeMessage(TYPE.ATTENTION, []); + }; + request.once('cancel', onCancelAfterRequestSent); - resolve(); - }; + try { + const onCancelOrResume = async () => { + return await new Promise((resolve, _) => { + const onResume = () => { + request.removeListener('cancel', onCancel); + request.removeListener('resume', onResume); - const onCancel = () => { - request.removeListener('cancel', onCancel); - request.removeListener('resume', onResume); + resolve(); + }; - resolve(); - }; + const onCancel = () => { + request.removeListener('cancel', onCancel); + request.removeListener('resume', onResume); - request.on('cancel', onCancel); - request.on('resume', onResume); - }); - }; + resolve(); + }; - if (!request.canceled && request instanceof Request && request.paused) { - await onCancelOrResume(); - } + request.on('cancel', onCancel); + request.on('resume', onResume); + }); + }; - const handler = new RequestTokenHandler(this, request); - for await (const token of this.createTokenStreamParser(message)) { if (!request.canceled && request instanceof Request && request.paused) { await onCancelOrResume(); } - handler[token.handlerName](token as any); - } - } finally { - request.removeListener('cancel', onCancelAfterRequestSent); - } + const handler = new RequestTokenHandler(this, request); - // If the request was canceled and we have a `cancelTimer` - // defined, we send a attention message after the - // request message was fully sent off. - // - // We already started consuming the current message - // (but all the token handlers should be no-ops), and - // need to ensure the next message is handled by the - // `SENT_ATTENTION` state. - if (request.canceled) { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - return this.socketError(err); - } + try { + for await (const token of this.createTokenStreamParser(this.messageIo.readMessage())) { + // request timer is stopped on first data package + clearTimeout(requestTimer); - const handler = new AttentionTokenHandler(this, request); - for await (const token of this.createTokenStreamParser(message)) { - handler[token.handlerName](token as any); - } + if (!request.canceled && request instanceof Request && request.paused) { + await onCancelOrResume(); + } - // 3.2.5.7 Sent Attention State - // Discard any data contained in the response, until we receive the attention response - if (handler.attentionReceived) { - this.clearCancelTimer(); + handler[token.handlerName](token as any); + } + } catch (err: any) { + const error = new RequestError('Connection closed before request completed.', 'ECLOSE'); + error.cause = err; - this.request = undefined; - this.transitionTo(this.STATE.LOGGED_IN); + this.request = undefined; + request.error = error; - if (request.error && request.error instanceof RequestError && request.error.code === 'ETIMEOUT') { - request.callback(request.error); - } else { - request.callback(new RequestError('Canceled.', 'ECANCEL')); + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + + process.nextTick(() => { request.callback(request.error); }); + return; } + } finally { + request.removeListener('cancel', onCancelAfterRequestSent); } - } else { - this.transitionTo(this.STATE.LOGGED_IN); - this.request = undefined; - if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { - this.inTransaction = false; + // If the request was canceled and we have a `cancelTimer` + // defined, we send a attention message after the + // request message was fully sent off. + // + // We already started consuming the current message + // (but all the token handlers should be no-ops), and + // need to ensure the next message is handled by the + // `SENT_ATTENTION` state. + if (request.canceled) { + const handler = new AttentionTokenHandler(this, request); + + try { + for await (const token of this.createTokenStreamParser(this.messageIo.readMessage())) { + handler[token.handlerName](token as any); + } + } catch (err: any) { + this.clearCancelTimer(); + + const error = new RequestError('Connection closed before request completed.', 'ECLOSE'); + error.cause = err; + + this.request = undefined; + request.error = error; + + this.transitionTo(this.STATE.FINAL); + this.cleanupConnection(CLEANUP_TYPE.NORMAL); + + process.nextTick(() => { request.callback(request.error); }); + return; + } + + // 3.2.5.7 Sent Attention State + // Discard any data contained in the response, until we receive the attention response + if (handler.attentionReceived) { + this.clearCancelTimer(); + + this.request = undefined; + this.transitionTo(this.STATE.LOGGED_IN); + + if (request.error && request.error instanceof RequestError && request.error.code === 'ETIMEOUT') { + process.nextTick(() => { + request.callback(request.error); + }); + } else { + process.nextTick(() => { + request.callback(new RequestError('Canceled.', 'ECANCEL')); + }); + } + } + } else { + this.transitionTo(this.STATE.LOGGED_IN); + + this.request = undefined; + if (this.config.options.tdsVersion < '7_2' && request.error && this.isSqlBatch) { + this.inTransaction = false; + } + process.nextTick(() => { + request.callback(request.error, request.rowCount, request.rows); + }); } - request.callback(request.error, request.rowCount, request.rows); } + } finally { + clearTimeout(requestTimer); } })().catch((err) => { process.nextTick(() => { @@ -3331,16 +3361,10 @@ class Connection extends EventEmitter { } async handlePreloginResponse(signal: AbortSignal) { - let messageBuffer = Buffer.alloc(0); - - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - throw this.wrapSocketError(err); - } + signal.throwIfAborted(); - for await (const data of message) { + let messageBuffer = Buffer.alloc(0); + for await (const data of this.messageIo.readMessage()) { messageBuffer = Buffer.concat([messageBuffer, data]); } @@ -3352,6 +3376,7 @@ class Connection extends EventEmitter { if (preloginPayload.fedAuthRequired === 1) { this.fedAuthRequired = true; } + if ('strict' !== this.config.options.encrypt && (preloginPayload.encryptionString === 'ON' || preloginPayload.encryptionString === 'REQ')) { if (!this.config.options.encrypt) { throw new ConnectionError("Server requires encryption, set 'encrypt' config option to true.", 'EENCRYPT'); @@ -3364,83 +3389,38 @@ class Connection extends EventEmitter { throw this.wrapSocketError(err); } } - - this.sendLogin7Packet(); - - const { authentication } = this.config; - - switch (authentication.type) { - case 'azure-active-directory-password': - case 'azure-active-directory-msi-vm': - case 'azure-active-directory-msi-app-service': - case 'azure-active-directory-service-principal-secret': - case 'azure-active-directory-default': - return await this.handleLogin7WithFedauthResponse(signal); - - case 'ntlm': - return await this.handleLogin7WithNtlmResponse(signal); - - default: - return await this.handleLogin7WithStandardLoginResponse(signal); - } } - async handleLogin7WithStandardLoginResponse(signal: AbortSignal) { + async handleLogin7WithStandardLoginResponse(signal: AbortSignal): Promise { + signal.throwIfAborted(); this.transitionTo(this.STATE.SENT_LOGIN7_WITH_STANDARD_LOGIN); - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - throw this.wrapSocketError(err); - } - const handler = new Login7TokenHandler(this); - for await (const token of this.createTokenStreamParser(message)) { + // TODO: handle `signal` + for await (const token of this.createTokenStreamParser(this.messageIo.readMessage())) { handler[token.handlerName](token as any); } if (handler.loginAckReceived) { - if (handler.routingData) { - this.routingData = handler.routingData; - return await this.handleRerouting(signal); - } else { - return await this.loggedInSendingInitialSql(signal); - } - } else if (this.loginError) { - if (isTransientError(this.loginError)) { - return await this.handleRetry(signal); - } else { - throw this.loginError; - } - } else { - throw new ConnectionError('Login failed.', 'ELOGIN'); + return handler.routingData; } + + throw this.loginError ?? new ConnectionError('Login failed.', 'ELOGIN'); } - async handleLogin7WithNtlmResponse(signal: AbortSignal) { + async handleLogin7WithNtlmResponse(signal: AbortSignal): Promise { + signal.throwIfAborted(); this.transitionTo(this.STATE.SENT_LOGIN7_WITH_NTLM); while (true) { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - throw this.wrapSocketError(err); - } - const handler = new Login7TokenHandler(this); - for await (const token of this.createTokenStreamParser(message)) { + // TODO: handle `signal` + for await (const token of this.createTokenStreamParser(this.messageIo.readMessage())) { handler[token.handlerName](token as any); } if (handler.loginAckReceived) { - if (handler.routingData) { - this.routingData = handler.routingData; - return await this.handleRerouting(signal); - } else { - return await this.loggedInSendingInitialSql(signal); - } + return handler.routingData; } else if (this.ntlmpacket) { const authentication = this.config.authentication as NtlmAuthentication; @@ -3451,46 +3431,53 @@ class Connection extends EventEmitter { ntlmpacket: this.ntlmpacket }); - this.messageIo.sendMessage(TYPE.NTLMAUTH_PKT, payload.data); + // TODO: handle `signal` + await this.messageIo.writeMessage(TYPE.NTLMAUTH_PKT, [payload.data]); this.debug.payload(function() { return payload.toString(' '); }); this.ntlmpacket = undefined; - } else if (this.loginError) { - if (isTransientError(this.loginError)) { - return await this.handleRetry(signal); - } else { - throw this.loginError; - } - } else { - throw new ConnectionError('Login failed.', 'ELOGIN'); } + + throw this.loginError ?? new ConnectionError('Login failed.', 'ELOGIN'); } } - async handleLogin7WithFedauthResponse(signal: AbortSignal) { - this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); + async handleLogin7Response(signal: AbortSignal): Promise { + signal.throwIfAborted(); - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - throw this.wrapSocketError(err); + const { authentication } = this.config; + + switch (authentication.type) { + case 'azure-active-directory-password': + case 'azure-active-directory-msi-vm': + case 'azure-active-directory-msi-app-service': + case 'azure-active-directory-service-principal-secret': + case 'azure-active-directory-default': + return await this.handleLogin7WithFedauthResponse(signal); + + case 'ntlm': + return await this.handleLogin7WithNtlmResponse(signal); + + default: + return await this.handleLogin7WithStandardLoginResponse(signal); } + } + + async handleLogin7WithFedauthResponse(signal: AbortSignal): Promise { + signal.throwIfAborted(); + + this.transitionTo(this.STATE.SENT_LOGIN7_WITH_FEDAUTH); const handler = new Login7TokenHandler(this); - for await (const token of this.createTokenStreamParser(message)) { + // TODO: Handle signal + for await (const token of this.createTokenStreamParser(this.messageIo.readMessage())) { handler[token.handlerName](token as any); } if (handler.loginAckReceived) { - if (handler.routingData) { - this.routingData = handler.routingData; - return await this.handleRerouting(signal); - } else { - return await this.loggedInSendingInitialSql(signal); - } + return handler.routingData; } const fedAuthInfoToken = handler.fedAuthInfoToken; @@ -3530,95 +3517,33 @@ class Connection extends EventEmitter { let tokenResponse; try { + // TODO: Handle `signal` tokenResponse = await credentials.getToken(tokenScope); } catch (err) { - throw new AggregateError([new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'), err]); + const error = new ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH'); + error.cause = err; + throw error; } const token = tokenResponse.token; - this.sendFedAuthTokenMessage(token); + await this.sendFedAuthTokenMessage(token, signal); // sent the fedAuth token message, the rest is similar to standard login 7 return await this.handleLogin7WithStandardLoginResponse(signal); - } else if (this.loginError) { - if (isTransientError(this.loginError)) { - return await this.handleRetry(signal); - } else { - throw this.loginError; - } - } else { - throw new ConnectionError('Login failed.', 'ELOGIN'); } - } - - async loggedInSendingInitialSql(signal: AbortSignal) { - this.sendInitialSql(); - await this.handleInitialSqlResponse(signal); - this.transitionTo(this.STATE.LOGGED_IN); - this.clearConnectTimer(); - this.emit('connect'); + throw this.loginError ?? new ConnectionError('Login failed.', 'ELOGIN'); } async handleInitialSqlResponse(signal: AbortSignal) { - let message; - try { - message = await this.messageIo.readMessage(); - } catch (err: any) { - throw this.wrapSocketError(err); - } + signal.throwIfAborted(); const handler = new InitialSqlTokenHandler(this); - for await (const token of this.createTokenStreamParser(message)) { + // TODO: handle `signal` + for await (const token of this.createTokenStreamParser(this.messageIo.readMessage())) { handler[token.handlerName](token as any); } } - - async handleRerouting(signal: AbortSignal) { - this.transitionTo(this.STATE.REROUTING); - - this.clearConnectTimer(); - this.closeConnection(); - await once(this.socket!, 'close'); - - this.emit('rerouting'); - - this.socket = undefined; - this.closed = true; - this.loginError = undefined; - - this.debug.log('Rerouting to ' + this.routingData!.server + ':' + this.routingData!.port); - - this.transitionTo(this.STATE.CONNECTING); - this.initialiseConnection(); - } - - async handleRetry(signal: AbortSignal) { - this.debug.log('Initiating retry on transient error'); - this.transitionTo(this.STATE.TRANSIENT_FAILURE_RETRY); - this.curTransientRetryCount++; - - this.clearConnectTimer(); - this.closeConnection(); - await once(this.socket!, 'close'); - - this.socket = undefined; - this.closed = true; - this.loginError = undefined; - - const server = this.routingData ? this.routingData.server : this.config.server; - const port = this.routingData ? this.routingData.port : this.config.options.port; - this.debug.log('Retry after transient failure connecting to ' + server + ':' + port); - - await new Promise((resolve, _reject) => { - setTimeout(resolve, this.config.options.connectionRetryInterval); - }); - - this.emit('retry'); - - this.transitionTo(this.STATE.CONNECTING); - this.initialiseConnection(); - } } function isTransientError(error: AggregateError | ConnectionError): boolean { @@ -3729,27 +3654,18 @@ Connection.prototype.STATE = { SENT_CLIENT_REQUEST: { name: 'SentClientRequest', events: { - socketError: function(err) { - const sqlRequest = this.request!; - this.request = undefined; + socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - - sqlRequest.callback(err); } } }, SENT_ATTENTION: { name: 'SentAttention', events: { - socketError: function(err) { - const sqlRequest = this.request!; - this.request = undefined; - + socketError: function() { this.transitionTo(this.STATE.FINAL); this.cleanupConnection(CLEANUP_TYPE.NORMAL); - - sqlRequest.callback(err); } } }, diff --git a/src/connector.ts b/src/connector.ts index fa1b9f4e3..99ca6be3d 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -3,16 +3,13 @@ import dns, { type LookupAddress } from 'dns'; import * as punycode from 'punycode'; import { AbortSignal } from 'node-abort-controller'; -import AbortError from './errors/abort-error'; import AggregateError from 'es-aggregate-error'; type LookupFunction = (hostname: string, options: dns.LookupAllOptions, callback: (err: NodeJS.ErrnoException | null, addresses: dns.LookupAddress[]) => void) => void; export async function connectInParallel(options: { host: string, port: number, localAddress?: string | undefined }, lookup: LookupFunction, signal: AbortSignal) { - if (signal.aborted) { - throw new AbortError(); - } + signal.throwIfAborted(); const addresses = await lookupAllAddresses(options.host, lookup, signal); @@ -64,7 +61,7 @@ export async function connectInParallel(options: { host: string, port: number, l socket.destroy(); } - reject(new AbortError()); + reject(signal.reason); }; for (let i = 0, len = addresses.length; i < len; i++) { @@ -83,9 +80,7 @@ export async function connectInParallel(options: { host: string, port: number, l } export async function connectInSequence(options: { host: string, port: number, localAddress?: string | undefined }, lookup: LookupFunction, signal: AbortSignal) { - if (signal.aborted) { - throw new AbortError(); - } + signal.throwIfAborted(); const errors: any[] = []; const addresses = await lookupAllAddresses(options.host, lookup, signal); @@ -105,7 +100,7 @@ export async function connectInSequence(options: { host: string, port: number, l socket.destroy(); - reject(new AbortError()); + reject(signal.reason); }; const onError = (err: Error) => { @@ -134,9 +129,7 @@ export async function connectInSequence(options: { host: string, port: number, l socket.on('connect', onConnect); }); } catch (err) { - if (err instanceof Error && err.name === 'AbortError') { - throw err; - } + signal.throwIfAborted(); errors.push(err); @@ -151,9 +144,7 @@ export async function connectInSequence(options: { host: string, port: number, l * Look up all addresses for the given hostname. */ export async function lookupAllAddresses(host: string, lookup: LookupFunction, signal: AbortSignal): Promise { - if (signal.aborted) { - throw new AbortError(); - } + signal.throwIfAborted(); if (net.isIPv6(host)) { return [{ address: host, family: 6 }]; @@ -162,7 +153,7 @@ export async function lookupAllAddresses(host: string, lookup: LookupFunction, s } else { return await new Promise((resolve, reject) => { const onAbort = () => { - reject(new AbortError()); + reject(signal.reason); }; signal.addEventListener('abort', onAbort); diff --git a/src/message-io.ts b/src/message-io.ts index 3c3ee39f0..851010bab 100644 --- a/src/message-io.ts +++ b/src/message-io.ts @@ -1,17 +1,28 @@ import DuplexPair from 'native-duplexpair'; -import { Duplex } from 'stream'; +import { Duplex, Readable, Writable } from 'stream'; import * as tls from 'tls'; import { Socket } from 'net'; import { EventEmitter } from 'events'; import Debug from './debug'; -import Message from './message'; -import { TYPE } from './packet'; +import { HEADER_LENGTH, Packet, TYPE } from './packet'; -import IncomingMessageStream from './incoming-message-stream'; -import OutgoingMessageStream from './outgoing-message-stream'; +import { BufferList } from 'bl'; +import { ConnectionError } from './errors'; + +function withResolvers() { + let resolve: (value: T | PromiseLike) => void; + let reject: (reason?: any) => void; + + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { resolve: resolve!, reject: reject!, promise }; +} class MessageIO extends EventEmitter { socket: Socket; @@ -19,15 +30,12 @@ class MessageIO extends EventEmitter { tlsNegotiationComplete: boolean; - private incomingMessageStream: IncomingMessageStream; - outgoingMessageStream: OutgoingMessageStream; - securePair?: { cleartext: tls.TLSSocket; encrypted: Duplex; }; - incomingMessageIterator: AsyncIterableIterator; + private _packetSize: number; constructor(socket: Socket, packetSize: number, debug: Debug) { super(); @@ -35,40 +43,32 @@ class MessageIO extends EventEmitter { this.socket = socket; this.debug = debug; - this.tlsNegotiationComplete = false; - - this.incomingMessageStream = new IncomingMessageStream(this.debug); - this.incomingMessageIterator = this.incomingMessageStream[Symbol.asyncIterator](); + this._packetSize = packetSize; - this.outgoingMessageStream = new OutgoingMessageStream(this.debug, { packetSize: packetSize }); + this.tlsNegotiationComplete = false; + } - this.socket.pipe(this.incomingMessageStream); - this.outgoingMessageStream.pipe(this.socket); + get packetSize(): number { + return this._packetSize; } - packetSize(...args: [number]) { - if (args.length > 0) { - const packetSize = args[0]; - this.debug.log('Packet size changed from ' + this.outgoingMessageStream.packetSize + ' to ' + packetSize); - this.outgoingMessageStream.packetSize = packetSize; - } + set packetSize(value: number) { + this._packetSize = value; if (this.securePair) { - this.securePair.cleartext.setMaxSendFragment(this.outgoingMessageStream.packetSize); + this.securePair.cleartext.setMaxSendFragment(this._packetSize); } - - return this.outgoingMessageStream.packetSize; } // Negotiate TLS encryption. - startTls(credentialsDetails: tls.SecureContextOptions, hostname: string, trustServerCertificate: boolean) { + async startTls(credentialsDetails: tls.SecureContextOptions, hostname: string, trustServerCertificate: boolean) { if (!credentialsDetails.maxVersion || !['TLSv1.2', 'TLSv1.1', 'TLSv1'].includes(credentialsDetails.maxVersion)) { credentialsDetails.maxVersion = 'TLSv1.2'; } const secureContext = tls.createSecureContext(credentialsDetails); - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const duplexpair = new DuplexPair(); const securePair = this.securePair = { cleartext: tls.connect({ @@ -91,6 +91,7 @@ class MessageIO extends EventEmitter { this.socket.destroy(err); }); + const cipher = securePair.cleartext.getCipher(); if (cipher) { this.debug.log('TLS negotiated (' + cipher.name + ', ' + cipher.version + ')'); @@ -98,16 +99,18 @@ class MessageIO extends EventEmitter { this.emit('secure', securePair.cleartext); - securePair.cleartext.setMaxSendFragment(this.outgoingMessageStream.packetSize); - - this.outgoingMessageStream.unpipe(this.socket); - this.socket.unpipe(this.incomingMessageStream); + securePair.cleartext.setMaxSendFragment(this._packetSize); this.socket.pipe(securePair.encrypted); - securePair.encrypted.pipe(this.socket); + this.socket.once('error', (err) => { + securePair.cleartext.destroy(err); + }); + this.socket.once('close', () => { + securePair.cleartext.destroy(); + }); + - securePair.cleartext.pipe(this.incomingMessageStream); - this.outgoingMessageStream.pipe(securePair.cleartext); + securePair.encrypted.pipe(this.socket); this.tlsNegotiationComplete = true; @@ -126,33 +129,30 @@ class MessageIO extends EventEmitter { }; const onReadable = () => { - // When there is handshake data on the encryped stream of the secure pair, - // we wrap it into a `PRELOGIN` message and send it to the server. - // - // For each `PRELOGIN` message we sent we get back exactly one response message - // that contains the server's handshake response data. - const message = new Message({ type: TYPE.PRELOGIN, resetConnection: false }); - - let chunk; - while (chunk = securePair.encrypted.read()) { - message.write(chunk); - } - this.outgoingMessageStream.write(message); - message.end(); + (async () => { + // When there is handshake data on the encryped stream of the secure pair, + // we wrap it into a `PRELOGIN` message and send it to the server. + // + // For each `PRELOGIN` message we sent we get back exactly one response message + // that contains the server's handshake response data. + const chunks: Buffer[] = []; + let chunk; + while ((chunk = securePair.encrypted.read()) !== null) { + chunks.push(chunk); + } - this.readMessage().then(async (response) => { - // Setup readable handler for the next round of handshaking. - // If we encounter a `secureConnect` on the cleartext side - // of the secure pair, the `readable` handler is cleared - // and no further handshake handling will happen. - securePair.encrypted.once('readable', onReadable); + await this.writeMessage(TYPE.PRELOGIN, Readable.from(chunks)); - for await (const data of response) { + for await (const chunk of this.readMessage()) { // We feed the server's handshake response back into the // encrypted end of the secure pair. - securePair.encrypted.write(data); + securePair.encrypted.write(chunk); + } + + if (!this.tlsNegotiationComplete) { + securePair.encrypted.once('readable', onReadable); } - }).catch(onError); + })().catch(onError); }; securePair.cleartext.once('error', onError); @@ -161,26 +161,240 @@ class MessageIO extends EventEmitter { }); } - // TODO listen for 'drain' event when socket.write returns false. - // TODO implement incomplete request cancelation (2.2.1.6) - sendMessage(packetType: number, data?: Buffer, resetConnection?: boolean) { - const message = new Message({ type: packetType, resetConnection: resetConnection }); - message.end(data); - this.outgoingMessageStream.write(message); - return message; + async writeMessage(type: number, payload: AsyncIterable | Iterable, resetConnection = false) { + const stream = this.tlsNegotiationComplete ? this.securePair!.cleartext : this.socket; + + return await MessageIO.writeMessage(stream, this.debug, this._packetSize, type, payload, resetConnection); + } + + static async writeMessage(stream: Writable, debug: Debug, packetSize: number, type: number, payload: AsyncIterable | Iterable, resetConnection = false) { + const bl = new BufferList(); + const length = packetSize - HEADER_LENGTH; + let packetNumber = 0; + + const iterator = (payload as AsyncIterable)[Symbol.asyncIterator] ? (payload as AsyncIterable)[Symbol.asyncIterator]() : (payload as Iterable)[Symbol.iterator](); + + while (true) { + const { resolve, reject, promise } = withResolvers>(); + + stream.once('error', reject); + + try { + Promise.resolve(iterator.next()).then(resolve, reject); + const result = await promise; + + if (result.done) { + break; + } + + bl.append(result.value); + } catch (err: any) { + // If the stream is still writable, the error came from + // the payload. We will end the message with the ignore flag set. + if (stream.writable) { + const packet = new Packet(type); + packet.packetId(packetNumber += 1); + packet.resetConnection(resetConnection); + packet.last(true); + packet.ignore(true); + + debug.packet('Sent', packet); + debug.data(packet); + + if (stream.write(packet.buffer) === false) { + await new Promise((resolve, reject) => { + const onError = (err: Error) => { + stream.removeListener('drain', onDrain); + + reject(err); + }; + + const onDrain = () => { + stream.removeListener('error', onError); + + resolve(); + }; + + stream.once('drain', onDrain); + stream.once('error', onError); + }); + } + } + + throw err; + } finally { + stream.removeListener('error', reject); + } + + while (bl.length > length) { + const data = bl.slice(0, length); + bl.consume(length); + + // TODO: Get rid of creating `Packet` instances here. + const packet = new Packet(type); + packet.packetId(packetNumber += 1); + packet.resetConnection(resetConnection); + packet.addData(data); + + debug.packet('Sent', packet); + debug.data(packet); + + if (stream.write(packet.buffer) === false) { + await new Promise((resolve, reject) => { + const onError = (err: Error) => { + stream.removeListener('drain', onDrain); + + reject(err); + }; + + const onDrain = () => { + stream.removeListener('error', onError); + + resolve(); + }; + + stream.once('drain', onDrain); + stream.once('error', onError); + }); + } + } + } + + const data = bl.slice(); + bl.consume(data.length); + + // TODO: Get rid of creating `Packet` instances here. + const packet = new Packet(type); + packet.packetId(packetNumber += 1); + packet.resetConnection(resetConnection); + packet.last(true); + packet.ignore(false); + packet.addData(data); + + debug.packet('Sent', packet); + debug.data(packet); + + if (stream.write(packet.buffer) === false) { + await new Promise((resolve, reject) => { + const onError = (err: Error) => { + stream.removeListener('drain', onDrain); + + reject(err); + }; + + const onDrain = () => { + stream.removeListener('error', onError); + + resolve(); + }; + + stream.once('drain', onDrain); + stream.once('error', onError); + }); + } } /** * Read the next incoming message from the socket. + * + * Returns a generator that yields `Buffer`s of the incoming message. + * + * If there's an error on the stream (e.g. connection is closed unexpectedly), + * this will throw an error. */ - async readMessage(): Promise { - const result = await this.incomingMessageIterator.next(); + readMessage(): AsyncGenerator { + const stream = this.tlsNegotiationComplete ? this.securePair!.cleartext : this.socket; + return MessageIO.readMessage(stream, this.debug); + } - if (result.done) { - throw new Error('unexpected end of message stream'); + static async *readMessage(stream: Readable, debug: Debug) { + if (!stream.readable) { + throw new Error('Premature close'); } - return result.value; + const bl = new BufferList(); + + let error; + const onError = (err: Error) => { + error = err; + }; + + const onClose = () => { + error ??= new Error('Premature close'); + }; + + stream.once('error', onError); + stream.once('close', onClose); + + try { + while (true) { + const { promise, resolve, reject } = withResolvers(); + + stream.addListener('close', resolve); + stream.addListener('readable', resolve); + stream.addListener('error', reject); + + try { + await promise; + } finally { + stream.removeListener('close', resolve); + stream.removeListener('readable', resolve); + stream.removeListener('error', reject); + } + + // Did the stream error while we waited for it to become readable? + if (error) { + throw error; + } + + let chunk: Buffer; + while ((chunk = stream.read()) !== null) { + bl.append(chunk); + + // The packet header is always 8 bytes of length. + while (bl.length >= HEADER_LENGTH) { + // Get the full packet length + const length = bl.readUInt16BE(2); + if (length < HEADER_LENGTH) { + throw new ConnectionError('Unable to process incoming packet'); + } + + if (bl.length >= length) { + const data = bl.slice(0, length); + bl.consume(length); + + // TODO: Get rid of creating `Packet` instances here. + const packet = new Packet(data); + debug.packet('Received', packet); + debug.data(packet); + + yield packet.data(); + + // Did the stream error while we yielded? + if (error) { + throw error; + } + + if (packet.isLast()) { + // This was the last packet. Is there any data left in the buffer? + // If there is, this might be coming from the next message (e.g. a response to a `ATTENTION` + // message sent from the client while reading an incoming response). + // + // Put any remaining bytes back on the stream so we can read them on the next `readMessage` call. + if (bl.length) { + stream.unshift(bl.slice()); + } + + return; + } + } + } + } + } + } finally { + stream.removeListener('close', onClose); + stream.removeListener('error', onError); + } } } diff --git a/src/token/handler.ts b/src/token/handler.ts index ccd1bb74e..5619997c6 100644 --- a/src/token/handler.ts +++ b/src/token/handler.ts @@ -181,7 +181,7 @@ export class InitialSqlTokenHandler extends TokenHandler { } onPacketSizeChange(token: PacketSizeEnvChangeToken) { - this.connection.messageIo.packetSize(token.newValue); + this.connection.messageIo.packetSize = token.newValue; } onBeginTransaction(token: BeginTransactionEnvChangeToken) { @@ -266,9 +266,7 @@ export class Login7TokenHandler extends TokenHandler { this.connection.emit('errorMessage', token); const error = new ConnectionError(token.message, 'ELOGIN'); - - const isLoginErrorTransient = this.connection.transientErrorLookup.isTransientError(token.number); - if (isLoginErrorTransient && this.connection.curTransientRetryCount !== this.connection.config.options.maxRetriesOnTransientErrors) { + if (this.connection.transientErrorLookup.isTransientError(token.number)) { error.isTransient = true; } @@ -355,7 +353,7 @@ export class Login7TokenHandler extends TokenHandler { } onPacketSizeChange(token: PacketSizeEnvChangeToken) { - this.connection.messageIo.packetSize(token.newValue); + this.connection.messageIo.packetSize = token.newValue; } onDatabaseMirroringPartner(token: DatabaseMirroringPartnerEnvChangeToken) { @@ -421,7 +419,7 @@ export class RequestTokenHandler extends TokenHandler { } onPacketSizeChange(token: PacketSizeEnvChangeToken) { - this.connection.messageIo.packetSize(token.newValue); + this.connection.messageIo.packetSize = token.newValue; } onBeginTransaction(token: BeginTransactionEnvChangeToken) { diff --git a/src/token/stream-parser.ts b/src/token/stream-parser.ts index 4b39aa23b..85e2e8224 100644 --- a/src/token/stream-parser.ts +++ b/src/token/stream-parser.ts @@ -47,10 +47,14 @@ class StreamBuffer { this.position = 0; } - async waitForChunk() { + async waitForChunk(errorOnDone = true) { const result = await this.iterator.next(); if (result.done) { - throw new Error('unexpected end of data'); + if (errorOnDone) { + throw new Error('unexpected end of data'); + } + + return; } if (this.position === this.buffer.length) { @@ -81,14 +85,10 @@ class Parser { parser.colMetadata = colMetadata; while (true) { - try { - await streamBuffer.waitForChunk(); - } catch (err: unknown) { - if (streamBuffer.position === streamBuffer.buffer.length) { - return; - } + await streamBuffer.waitForChunk(false); - throw err; + if (streamBuffer.position === streamBuffer.buffer.length) { + return; } if (parser.suspended) { diff --git a/test/integration/connection-test.js b/test/integration/connection-test.js index 17bf5ef40..0b21ba204 100644 --- a/test/integration/connection-test.js +++ b/test/integration/connection-test.js @@ -293,7 +293,6 @@ describe('Initiate Connect Test', function() { try { assert.instanceOf(err, ConnectionError); assert.strictEqual(/** @type {ConnectionError} */(err).code, 'ESOCKET'); - assert.strictEqual(connection.connectTimer, undefined); done(); } catch (e) { done(e); @@ -302,8 +301,6 @@ describe('Initiate Connect Test', function() { connection.on('error', done); connection.connect(); - - assert.isOk(connection.connectTimer); }); it('should clear timeouts when failing to connect to named instance', function(done) { @@ -322,15 +319,12 @@ describe('Initiate Connect Test', function() { connection.on('connect', (err) => { assert.instanceOf(err, ConnectionError); assert.strictEqual(/** @type {ConnectionError} */(err).code, 'EINSTLOOKUP'); - assert.strictEqual(connection.connectTimer, undefined); done(); }); connection.on('error', done); connection.connect(); - - assert.isOk(connection.connectTimer); }); it('should fail if no cipher can be negotiated', function(done) { @@ -467,7 +461,6 @@ describe('Initiate Connect Test', function() { }); }); - describe('Ntlm Test', function() { /** * @enum {number} diff --git a/test/integration/invalid-packet-stream-test.js b/test/integration/invalid-packet-stream-test.js index e1e28ce26..039434ea3 100644 --- a/test/integration/invalid-packet-stream-test.js +++ b/test/integration/invalid-packet-stream-test.js @@ -66,7 +66,7 @@ describe('Connecting to a server that sends invalid packet data', function() { connection.connect((err) => { assert.instanceOf(err, ConnectionError); - assert.equal(/** @type {ConnectionError} */(err).message, 'Connection lost - Unable to process incoming packet'); + assert.equal(/** @type {ConnectionError} */(err).message, 'Unable to process incoming packet'); done(); }); diff --git a/test/integration/socket-error-test.js b/test/integration/socket-error-test.js index 27ea2073a..9566a2d71 100644 --- a/test/integration/socket-error-test.js +++ b/test/integration/socket-error-test.js @@ -57,7 +57,7 @@ describe('A `error` on the network socket', function() { connection.execSql(request); process.nextTick(() => { - /** @type {Socket} */(connection.socket).emit('error', socketError); + /** @type {Socket} */(connection.socket).destroy(socketError); }); }); @@ -73,11 +73,11 @@ describe('A `error` on the network socket', function() { connection.execSql(request); process.nextTick(() => { - /** @type {Socket} */(connection.socket).emit('error', socketError); + /** @type {Socket} */(connection.socket).destroy(socketError); }); }); - it('calls the request completion callback before emitting the `end` event', function(done) { + it('calls the request completion callback after emitting the `end` event', function(done) { const socketError = new Error('socket error'); connection.on('error', () => {}); @@ -87,17 +87,13 @@ describe('A `error` on the network socket', function() { }); const request = new Request('WAITFOR 00:00:30', function(err) { - assert.strictEqual(endEmitted, false); - - process.nextTick(() => { - assert.strictEqual(endEmitted, true); - done(); - }); + assert.strictEqual(endEmitted, true); + done(); }); connection.execSql(request); process.nextTick(() => { - /** @type {Socket} */(connection.socket).emit('error', socketError); + /** @type {Socket} */(connection.socket).destroy(socketError); }); }); }); diff --git a/test/integration/transactions-test.js b/test/integration/transactions-test.js index 578b6e1f0..ebd5204ca 100644 --- a/test/integration/transactions-test.js +++ b/test/integration/transactions-test.js @@ -4,6 +4,7 @@ const Transaction = require('../../src/transaction'); const fs = require('fs'); const async = require('async'); const { debugOptionsFromEnv } = require('../helpers/debug-options-from-env'); +const { ConnectionError } = require('../../src/errors'); const assert = require('chai').assert; const config = JSON.parse( @@ -466,15 +467,22 @@ describe('Transactions Test', function() { }); it('should test transaction helper socket error', function(done) { + const expectedError = new Error('socket error'); const connection = new Connection(config); connection.on('end', function(info) { done(); }); connection.on('error', function(err) { - assert.ok(~err.message.indexOf('socket error')); + assert.instanceOf(err, ConnectionError); + assert.strictEqual(err.message, 'Connection lost'); + assert.strictEqual(err.cause, expectedError); }); // connection.on('errorMessage', (error) => console.log("#{error.number} : #{error.message}")) - // connection.on('debug', (message) => console.log(message) if (debug)) + if (process.env.TEDIOUS_DEBUG) { + connection.on('debug', (message) => { + console.log(message); + }); + } connection.connect(function(err) { connection.transaction(function(err, outerDone) { @@ -492,7 +500,7 @@ describe('Transactions Test', function() { }); connection.execSql(request); - connection.socket.emit('error', new Error('socket error')); + connection.socket.destroy(expectedError); }); }); }); diff --git a/test/unit/connection-retry-test.js b/test/unit/connection-retry-test.js index 56800db73..4f3c547bd 100644 --- a/test/unit/connection-retry-test.js +++ b/test/unit/connection-retry-test.js @@ -7,6 +7,7 @@ const OutgoingMessageStream = require('../../src/outgoing-message-stream'); const Debug = require('../../src/debug'); const PreloginPayload = require('../../src/prelogin-payload'); const Message = require('../../src/message'); +const { debugOptionsFromEnv } = require('../helpers/debug-options-from-env'); function buildLoginAckToken() { const progname = 'Tedious SQL Server'; @@ -386,9 +387,12 @@ describe('Automatic Connection Retry', function() { maxRetriesOnTransientErrors: 5, connectTimeout: 200, connectionRetryInterval: 50, + debug: debugOptionsFromEnv() } }); + connection.on('debug', console.log); + connection.connect((err) => { connection.close(); diff --git a/test/unit/connection-test.ts b/test/unit/connection-test.ts index e4a4d8cb6..2daa025e5 100644 --- a/test/unit/connection-test.ts +++ b/test/unit/connection-test.ts @@ -1,6 +1,8 @@ import * as net from 'net'; import { Connection, ConnectionError } from '../../src/tedious'; import { assert } from 'chai'; +import MessageIO from '../../src/message-io'; +import Debug from '../../src/debug'; describe('Using `strict` encryption', function() { let server: net.Server; @@ -16,8 +18,6 @@ describe('Using `strict` encryption', function() { it('does not throw an unhandled exception if the tls handshake fails', function(done) { server.on('connection', (connection) => { - console.log('incoming connection'); - connection.on('data', () => { // Ignore all incoming data }); @@ -36,8 +36,12 @@ describe('Using `strict` encryption', function() { }); connection.connect((err) => { + console.log(err); + assert.instanceOf(err, Error); - assert.include(err!.message, 'Client network socket disconnected before secure TLS connection was established'); + + assert.instanceOf(err?.cause, Error); + assert.include((err!.cause as Error).message, 'Client network socket disconnected before secure TLS connection was established'); done(); }); @@ -75,3 +79,69 @@ describe('Using `strict` encryption', function() { }); }); }); + +describe('Connection error handling', function() { + describe('handles unexpected network issues', async function() { + let server: net.Server; + let _connections: net.Socket[]; + + beforeEach(function(done) { + _connections = []; + server = net.createServer(); + server.listen(0, '127.0.0.1', done); + }); + + afterEach(function(done) { + _connections.forEach((connection) => { + connection.destroy(); + }); + + server.close(done); + }); + + it('signals an error', function(done) { + let connectionCount = 0; + + server.on('connection', async (connection) => { + connectionCount++; + + const debug = new Debug(); + + try { + // PRELOGIN + { + const chunks = []; + for await (const data of MessageIO.readMessage(connection, debug)) { + chunks.push(data); + } + + connection.destroy(); + } + } catch (err) { + console.log(err); + } finally { + connection.end(); + } + }); + + const connection = new Connection({ + server: (server.address() as net.AddressInfo).address, + options: { + port: (server.address() as net.AddressInfo).port, + encrypt: false, + maxRetriesOnTransientErrors: 5 + } + }); + + connection.connect((err) => { + connection.close(); + + console.log(err); + assert.instanceOf(err, Error); + assert.strictEqual(1, connectionCount); + + done(); + }); + }); + }); +}); diff --git a/test/unit/connector-test.js b/test/unit/connector-test.js index 973b8c06e..162f5c999 100644 --- a/test/unit/connector-test.js +++ b/test/unit/connector-test.js @@ -10,6 +10,7 @@ const { connectInParallel, connectInSequence } = require('../../src/connector'); +const { default: AbortError } = require('../../src/errors/abort-error'); describe('lookupAllAddresses', function() { it('test IDN Server name', async function() { @@ -242,7 +243,7 @@ describe('connectInSequence', function() { it('will immediately abort when called with an aborted signal', async function() { const controller = new AbortController(); - controller.abort(); + controller.abort(new AbortError()); mitm.on('connect', () => { assert.fail('no connections expected'); @@ -277,7 +278,7 @@ describe('connectInSequence', function() { attemptedSockets.push(socket); process.nextTick(() => { - controller.abort(); + controller.abort(new AbortError()); }); }); @@ -451,7 +452,7 @@ describe('connectInParallel', function() { it('will immediately abort when called with an aborted signal', async function() { const controller = new AbortController(); - controller.abort(); + controller.abort(new AbortError()); mitm.on('connect', () => { assert.fail('no connections expected'); @@ -486,7 +487,7 @@ describe('connectInParallel', function() { attemptedSockets.push(socket); process.nextTick(() => { - controller.abort(); + controller.abort(new AbortError()); }); }); diff --git a/test/unit/message-io-test.ts b/test/unit/message-io-test.ts index b7f94c632..5acd5847a 100644 --- a/test/unit/message-io-test.ts +++ b/test/unit/message-io-test.ts @@ -5,18 +5,397 @@ import { promisify } from 'util'; import DuplexPair from 'native-duplexpair'; import { TLSSocket } from 'tls'; import { readFileSync } from 'fs'; -import { Duplex } from 'stream'; +import { Duplex, PassThrough, Readable } from 'stream'; import Debug from '../../src/debug'; import MessageIO from '../../src/message-io'; -import Message from '../../src/message'; import { Packet, TYPE } from '../../src/packet'; +import { BufferListStream } from 'bl'; const packetType = 2; const packetSize = 8 + 4; const delay = promisify(setTimeout); +describe('MessageIO.readMessage', function() { + it('can read single packet message from the given readable', async function() { + const expectedData = Buffer.from('foobar'); + + const stream = Readable.from(function*() { + { + const packet = new Packet(TYPE.PRELOGIN); + packet.last(true); + packet.addData(expectedData); + yield packet.buffer; + } + }()); + + const debug = new Debug(); + + for await (const chunk of MessageIO.readMessage(stream, debug)) { + assert.deepEqual(chunk, expectedData); + } + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); + + it('can read multi packet message from the given readable', async function() { + const expectedData = [ Buffer.from('foo'), Buffer.from('bar') ]; + + const stream = Readable.from(function*() { + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(expectedData[0]); + yield packet.buffer; + } + + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(expectedData[1]); + packet.last(true); + yield packet.buffer; + } + }()); + + const debug = new Debug(); + + const chunks = []; + for await (const chunk of MessageIO.readMessage(stream, debug)) { + chunks.push(chunk); + } + + assert.deepEqual(chunks, expectedData); + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); + + it('can read multi packet message in a single chunk from the given readable', async function() { + const expectedData = [Buffer.from('foo'), Buffer.from('bar')]; + + const stream = Readable.from(function*() { + const chunks = []; + + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(expectedData[0]); + chunks.push(packet.buffer); + } + + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(expectedData[1]); + packet.last(true); + chunks.push(packet.buffer); + } + + yield Buffer.concat(chunks); + }()); + + const debug = new Debug(); + + const chunks = []; + for await (const chunk of MessageIO.readMessage(stream, debug)) { + chunks.push(chunk); + } + + assert.deepEqual(chunks, expectedData); + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); + + it('can read single packet messages in a single chunk from the given readable', async function() { + const expectedData = [Buffer.from('foo'), Buffer.from('bar')]; + + const stream = Readable.from(function*() { + const chunks = []; + + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(expectedData[0]); + packet.last(true); + chunks.push(packet.buffer); + } + + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(expectedData[1]); + packet.last(true); + chunks.push(packet.buffer); + } + + yield Buffer.concat(chunks); + }()); + + const debug = new Debug(); + + { + const chunks = []; + for await (const chunk of MessageIO.readMessage(stream, debug)) { + chunks.push(chunk); + } + assert.deepEqual(chunks, [expectedData[0]]); + } + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + + { + const chunks = []; + for await (const chunk of MessageIO.readMessage(stream, debug)) { + chunks.push(chunk); + } + assert.deepEqual(chunks, [expectedData[1]]); + } + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); + + it('handles streams that close unexpectedly', async function() { + const stream = new Readable({ + read: () => {} + }); + + const debug = new Debug(); + + let hadError = false; + try { + await Promise.all([ + (async () => { + for await (const { } of MessageIO.readMessage(stream, debug)) { } + })(), + (async () => { + await delay(1); + // End the stream + stream.push(null); + })(), + ]); + } catch (err: any) { + hadError = true; + assert.strictEqual(err.message, 'Premature close'); + } + + assert.strictEqual(hadError, true); + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); + + it('throws an error when given a stream that can not be read from', async function() { + const stream = new Readable({ read: () => { } }); + stream.destroy(); + + const debug = new Debug(); + + let hadError = false; + try { + for await (const { } of MessageIO.readMessage(stream, debug)) { } + } catch (err: any) { + hadError = true; + assert.strictEqual(err.message, 'Premature close'); + } + + assert.strictEqual(hadError, true); + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); + + it('handles errors on the stream during reading', async function() { + const stream = Readable.from(function *() { + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(Buffer.alloc(100)); + yield packet.buffer; + } + + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(Buffer.alloc(100)); + yield packet.buffer; + } + }()); + + const debug = new Debug(); + const expectedErr = new Error('some error'); + + let hadError = false; + try { + for await (const {} of MessageIO.readMessage(stream, debug)) { + stream.destroy(expectedErr); + } + } catch (err: any) { + hadError = true; + assert.strictEqual(err, expectedErr); + } + + assert.strictEqual(hadError, true); + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); + + it('handles errors on the stream during reading (on the next tick)', async function() { + const stream = Readable.from(function*() { + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(Buffer.alloc(100)); + yield packet.buffer; + } + + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(Buffer.alloc(100)); + yield packet.buffer; + } + }()); + + const debug = new Debug(); + const expectedErr = new Error('some error'); + + let hadError = false; + try { + for await (const { } of MessageIO.readMessage(stream, debug)) { + process.nextTick(() => { + stream.destroy(expectedErr); + }); + + await delay(5); + } + } catch (err: any) { + hadError = true; + assert.strictEqual(err, expectedErr); + } + + assert.strictEqual(hadError, true); + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); + + it('throws an error if the streams ends before receiving the last packet', async function() { + const stream = Readable.from(function*() { + { + const packet = new Packet(TYPE.PRELOGIN); + packet.addData(Buffer.alloc(100)); + yield packet.buffer; + } + }()); + + const debug = new Debug(); + + let hadError = false; + try { + for await (const { } of MessageIO.readMessage(stream, debug)) { } + } catch (err: any) { + hadError = true; + assert.strictEqual(err.message, 'Premature close'); + } + + assert.strictEqual(hadError, true); + + assert.strictEqual(stream.listenerCount('error'), 0); + assert.strictEqual(stream.listenerCount('readable'), 0); + }); +}); + +describe('MessageIO.writeMessage', function() { + it('can write a payload as a set of packages to the given stream', async function() { + const bl = new BufferListStream() as any as Duplex; + + const debug = new Debug(); + const data = Buffer.from('foobar'); + + await MessageIO.writeMessage(bl, debug, 4096, TYPE.PRELOGIN, [data]); + + const packet = new Packet(bl.read()); + assert.strictEqual(packet.type(), TYPE.PRELOGIN); + assert.strictEqual(packet.isLast(), true); + assert.deepEqual(packet.data(), data); + }); + + it('can handle the payload erroring unexpectedly', async function() { + const bl = new BufferListStream() as any as Duplex; + + const debug = new Debug(); + const data = Buffer.from('foobar'); + + const expectedError = new Error('unexpected error'); + const payload = (function*() { + yield data; + + throw expectedError; + })(); + + try { + await MessageIO.writeMessage(bl, debug, 4096, TYPE.PRELOGIN, payload); + } catch (err: any) { + assert.strictEqual(err, expectedError); + } + + const packet = new Packet(bl.read()); + assert.strictEqual(packet.type(), TYPE.PRELOGIN); + assert.strictEqual(packet.statusAsString(), 'EOM IGNORE'); + assert.strictEqual(packet.isLast(), true); + assert.deepEqual(packet.data(), Buffer.alloc(0)); + }); + + it('can handle the target stream erroring while waiting for payload data', async function() { + const passthrough = new PassThrough({ objectMode: true }); + + const debug = new Debug(); + const data = Buffer.alloc(passthrough.writableHighWaterMark + 1); + + const expectedError = new Error('unexpected error'); + const payload = (async function*() { + yield data; + + await delay(1); + passthrough.destroy(expectedError); + await delay(1); + + yield data; + })(); + + try { + await MessageIO.writeMessage(passthrough, debug, 4096, TYPE.PRELOGIN, payload); + } catch (err: any) { + assert.strictEqual(err, expectedError); + } + + assert.isNull(passthrough.read()); + }); + + it('can handle the target stream erroring while waiting for a drain event', async function() { + const passthrough = new PassThrough({ writableHighWaterMark: 16, readableHighWaterMark: 16 }); + + const debug = new Debug(); + + const data = Buffer.alloc(passthrough.writableHighWaterMark * 2); + + const expectedError = new Error('unexpected error'); + const payload = (async function*() { + delay(10).then(() => { + passthrough.destroy(expectedError); + }); + + yield data; + })(); + + let hadError = false; + try { + await MessageIO.writeMessage(passthrough, debug, 16, TYPE.PRELOGIN, payload); + } catch (err: any) { + hadError = true; + assert.strictEqual(err, expectedError); + } + assert.isTrue(hadError); + }); +}); + describe('MessageIO', function() { let server: Server; let serverConnection: Socket; @@ -56,7 +435,7 @@ describe('MessageIO', function() { server.close(done); }); - describe('#sendMessage', function() { + describe('#writeMessage', function() { it('sends data that is smaller than the current packet length', async function() { const payload = Buffer.from([1, 2, 3]); @@ -79,7 +458,7 @@ describe('MessageIO', function() { // Client side (async () => { const io = new MessageIO(clientConnection, packetSize, debug); - io.sendMessage(packetType, payload); + await io.writeMessage(packetType, Readable.from([payload])); })() ]); }); @@ -106,7 +485,7 @@ describe('MessageIO', function() { // Client side (async () => { const io = new MessageIO(clientConnection, packetSize, debug); - io.sendMessage(packetType, payload); + await io.writeMessage(packetType, Readable.from([payload])); })() ]); }); @@ -134,7 +513,7 @@ describe('MessageIO', function() { // Client side (async () => { const io = new MessageIO(clientConnection, packetSize, debug); - io.sendMessage(packetType, payload); + await io.writeMessage(packetType, Readable.from([payload])); })() ]); }); @@ -158,11 +537,8 @@ describe('MessageIO', function() { (async () => { const io = new MessageIO(clientConnection, packetSize, debug); - const message = await io.readMessage(); - assert.instanceOf(message, Message); - const chunks = []; - for await (const chunk of message) { + for await (const chunk of io.readMessage()) { chunks.push(chunk); } @@ -189,11 +565,8 @@ describe('MessageIO', function() { (async () => { const io = new MessageIO(clientConnection, packetSize, debug); - const message = await io.readMessage(); - assert.instanceOf(message, Message); - const chunks = []; - for await (const chunk of message) { + for await (const chunk of io.readMessage()) { chunks.push(chunk); } @@ -228,11 +601,8 @@ describe('MessageIO', function() { (async () => { const io = new MessageIO(clientConnection, packetSize, debug); - const message = await io.readMessage(); - assert.instanceOf(message, Message); - const receivedData: Buffer[] = []; - for await (const chunk of message) { + for await (const chunk of io.readMessage()) { receivedData.push(chunk); } @@ -274,11 +644,8 @@ describe('MessageIO', function() { (async () => { const io = new MessageIO(clientConnection, packetSize, debug); - const message = await io.readMessage(); - assert.instanceOf(message, Message); - const receivedData: Buffer[] = []; - for await (const chunk of message) { + for await (const chunk of io.readMessage()) { receivedData.push(chunk); } @@ -321,18 +688,15 @@ describe('MessageIO', function() { (async () => { const io = new MessageIO(clientConnection, packetSize, debug); - const message = await io.readMessage(); - assert.instanceOf(message, Message); - const receivedData: Buffer[] = []; - for await (const chunk of message) { + for await (const chunk of io.readMessage()) { receivedData.push(chunk); } // The data of the individual packages gets merged together by the buffering happening // inside the `IncomingMessageStream`. We don't actually care about this, so it's // okay if this changes. - assert.deepEqual(receivedData, [ payload ]); + assert.deepEqual(Buffer.concat(receivedData), payload); })() ]); }); @@ -395,7 +759,7 @@ describe('MessageIO', function() { chunks.push(chunk); } - io.sendMessage(TYPE.PRELOGIN, Buffer.concat(chunks)); + await io.writeMessage(TYPE.PRELOGIN, Readable.from(chunks)); } { @@ -412,7 +776,7 @@ describe('MessageIO', function() { chunks.push(chunk); } - io.sendMessage(TYPE.PRELOGIN, Buffer.concat(chunks)); + await io.writeMessage(TYPE.PRELOGIN, Readable.from(chunks)); } // Verify that server side was successful at this point @@ -432,7 +796,7 @@ describe('MessageIO', function() { await io.startTls({}, 'localhost', true); // Send a request (via TLS) - io.sendMessage(TYPE.LOGIN7, payload); + await io.writeMessage(TYPE.LOGIN7, Readable.from([payload])); // Receive response (via TLS) const message = await io.readMessage(); @@ -467,7 +831,7 @@ describe('MessageIO', function() { chunks.push(chunk); } - io.sendMessage(TYPE.PRELOGIN, Buffer.concat(chunks)); + await io.writeMessage(TYPE.PRELOGIN, Readable.from(chunks)); } { @@ -484,7 +848,7 @@ describe('MessageIO', function() { chunks.push(chunk); } - io.sendMessage(TYPE.PRELOGIN, Buffer.concat(chunks)); + await io.writeMessage(TYPE.PRELOGIN, Readable.from(chunks)); } // Verify that server side was successful at this point @@ -589,7 +953,7 @@ describe('MessageIO', function() { chunks.push(chunk); } - io.sendMessage(TYPE.PRELOGIN, Buffer.concat(chunks)); + await io.writeMessage(TYPE.PRELOGIN, Readable.from(chunks)); } })() ]); diff --git a/test/unit/rerouting-test.js b/test/unit/rerouting-test.js index 22d3cf3cd..5f3250503 100644 --- a/test/unit/rerouting-test.js +++ b/test/unit/rerouting-test.js @@ -8,6 +8,7 @@ const Debug = require('../../src/debug'); const PreloginPayload = require('../../src/prelogin-payload'); const Message = require('../../src/message'); const WritableTrackingBuffer = require('../../src/tracking-buffer/writable-tracking-buffer'); +const { debugOptionsFromEnv } = require('../helpers/debug-options-from-env'); function buildRoutingEnvChangeToken(hostname, port) { const valueBuffer = new WritableTrackingBuffer(0); @@ -444,10 +445,15 @@ describe('Connecting to a server that sends a re-routing information', function( server: routingServer.address().address, options: { port: routingServer.address().port, - encrypt: false + encrypt: false, + debug: debugOptionsFromEnv() } }); + if (process.env.TEDIOUS_DEBUG) { + connection.on('debug', console.log); + } + try { await new Promise((resolve, reject) => { connection.connect((err) => {