From 747ffa1b25b834b3271a67c3cec421014dc2760d Mon Sep 17 00:00:00 2001 From: Nathan Erickson Date: Tue, 7 Apr 2026 13:04:56 -0400 Subject: [PATCH] feat: support disconnect reason, message, and language in client/server end() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Protocol.disconnect() to write the description message and language tag fields per RFC 4253 ยง11.1 instead of zero-filling them. Expose the new parameters through Client.end() and the server-side Connection.end() so callers can specify a disconnect reason code, a human-readable message, and an optional language tag. Add test for server disconnect with custom reason and message. --- lib/client.js | 5 +++-- lib/protocol/Protocol.js | 23 ++++++++++++++++++----- lib/server.js | 5 +++-- test/test-misc-client-server.js | 31 +++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/lib/client.js b/lib/client.js index 7291c2ce..8ae36768 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1176,9 +1176,10 @@ class Client extends EventEmitter { return this; } - end() { + end(reason = null, message = '', lang = '') { + reason = reason || DISCONNECT_REASON.BY_APPLICATION; if (this._sock && isWritable(this._sock)) { - this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION); + this._protocol.disconnect(reason, message, lang); this._sock.end(); } return this; diff --git a/lib/protocol/Protocol.js b/lib/protocol/Protocol.js index 73024881..2013b392 100644 --- a/lib/protocol/Protocol.js +++ b/lib/protocol/Protocol.js @@ -321,23 +321,36 @@ class Protocol { // Global // ------ - disconnect(reason) { - const pktLen = 1 + 4 + 4 + 4; + disconnect(reason, message = '', lang = '') { + const msgLen = Buffer.byteLength(message); + const langLen = Buffer.byteLength(lang); + const pktLen = 1 + 4 + 4 + msgLen + 4 + langLen; // We don't use _packetRW.write.* here because we need to make sure that // we always get a full packet allocated because this message can be sent // at any time -- even during a key exchange let p = this._packetRW.write.allocStartKEX; const packet = this._packetRW.write.alloc(pktLen, true); - const end = p + pktLen; if (!VALID_DISCONNECT_REASONS.has(reason)) reason = DISCONNECT_REASON.PROTOCOL_ERROR; packet[p] = MESSAGE.DISCONNECT; writeUInt32BE(packet, reason, ++p); - packet.fill(0, p += 4, end); + p += 4; + + writeUInt32BE(packet, msgLen, p); + p += 4; + if (msgLen > 0) { + packet.utf8Write(message, p, msgLen); + p += msgLen; + } + + writeUInt32BE(packet, langLen, p); + p += 4; + if (langLen > 0) + packet.utf8Write(lang, p, langLen); - this._debug && this._debug(`Outbound: Sending DISCONNECT (${reason})`); + this._debug && this._debug(`Outbound: Sending DISCONNECT (${reason}, message: ${message})`); sendPacket(this, this._packetRW.write.finalize(packet, true), true); } ping() { diff --git a/lib/server.js b/lib/server.js index 306d6584..bd09e59a 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1278,9 +1278,10 @@ class Client extends EventEmitter { } } - end() { + end(reason = null, message = '', lang = '') { + reason = reason || DISCONNECT_REASON.BY_APPLICATION; if (this._sock && isWritable(this._sock)) { - this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION); + this._protocol.disconnect(reason, message, lang); this._sock.end(); } return this; diff --git a/test/test-misc-client-server.js b/test/test-misc-client-server.js index 2dd5a29d..b1010d41 100644 --- a/test/test-misc-client-server.js +++ b/test/test-misc-client-server.js @@ -1458,3 +1458,34 @@ const setup = setupSimple.bind(undefined, debug); })); })); } + +{ + const { DISCONNECT_REASON } = require('../lib/protocol/constants.js'); + const { client, server } = setup_( + 'Server disconnect with custom reason and message', + { + client: clientCfg, + server: serverCfg, + noClientError: true, + }, + ); + + server.on('connection', mustCall((conn) => { + conn.on('authentication', mustCall((ctx) => { + ctx.accept(); + })).on('ready', mustCall(() => { + conn.end( + DISCONNECT_REASON.PROTOCOL_ERROR, + 'Too many authentication failures' + ); + })); + })); + + client.on('ready', mustCall(() => {})); + client.on('error', mustCall((err) => { + assert(err.code === DISCONNECT_REASON.PROTOCOL_ERROR, + `Expected reason ${DISCONNECT_REASON.PROTOCOL_ERROR}, got: ${err.code}`); + assert(err.message === 'Too many authentication failures', + `Expected custom message, got: ${err.message}`); + })); +}