From a2697c944efce407a6fd85b876f83907362eead4 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Tue, 13 Dec 2022 17:17:49 +0100 Subject: [PATCH] Store capability values from CAP v3.2 handshakes + honor SASL v3.2 mechanism lists (#322) * Store capability values in 'CAP LS 302' negotiations. So it can be retrieved later (eg. to know what SASL mechanisms are available). https://ircv3.net/specs/extensions/capability-negotiation.html#the-cap-ls-subcommand * Honor SASL v3.2 mechanism lists From https://ircv3.net/specs/extensions/sasl-3.2#usage : > Clients SHOULD pick a mechanism present in the CAP LS reply they get from the server and attempt to use that mechanism for authentication after they request the sasl capability. * Update handler.network.cap.available on CAP NEW/DEL * Clear available caps when connecting && cleanup sasl code Co-authored-by: ItsOnlyBinary --- src/client.js | 1 + src/commands/handlers/registration.js | 33 ++++++++++++++++++++++----- src/networkinfo.js | 1 + 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index e1eec6a3..296f694d 100644 --- a/src/client.js +++ b/src/client.js @@ -112,6 +112,7 @@ module.exports = class IrcClient extends EventEmitter { client.network.cap.negotiating = false; client.network.cap.requested = []; client.network.cap.enabled = []; + client.network.cap.available.clear(); client.command_handler.resetCache(); }); diff --git a/src/commands/handlers/registration.js b/src/commands/handlers/registration.js index b9a53aed..4c7d75e5 100644 --- a/src/commands/handlers/registration.js +++ b/src/commands/handlers/registration.js @@ -109,11 +109,15 @@ const handlers = { const capabilities = command.params[command.params.length - 1] .replace(/(?:^| )[-~=]/, '') .split(' ') + .filter((cap) => !!cap) .map(function(cap) { // CAPs in 3.2 may be in the form of CAP=VAL. So seperate those out const sep = cap.indexOf('='); if (sep === -1) { capability_values[cap] = ''; + if (command.params[1] === 'LS' || command.params[1] === 'NEW') { + handler.network.cap.available.set(cap, ''); + } return cap; } @@ -121,6 +125,9 @@ const handlers = { const cap_value = cap.substr(sep + 1); capability_values[cap_name] = cap_value; + if (command.params[1] === 'LS' || command.params[1] === 'NEW') { + handler.network.cap.available.set(cap_name, cap_value); + } return cap_name; }); @@ -191,13 +198,24 @@ const handlers = { ); } if (handler.network.cap.negotiating) { + let authenticating = false; if (handler.network.cap.isEnabled('sasl')) { - if (typeof handler.connection.options.sasl_mechanism === 'string') { - handler.connection.write('AUTHENTICATE ' + handler.connection.options.sasl_mechanism); - } else { - handler.connection.write('AUTHENTICATE PLAIN'); + const options_mechanism = handler.connection.options.sasl_mechanism; + const wanted_mechanism = (typeof options_mechanism === 'string') ? + options_mechanism.toUpperCase() : + 'PLAIN'; + + const mechanisms = handler.network.cap.available.get('sasl'); + const valid_mechanisms = mechanisms.toUpperCase().split(','); + if ( + !mechanisms || // SASL v3.1 + valid_mechanisms.includes(wanted_mechanism) // SASL v3.2 + ) { + handler.connection.write('AUTHENTICATE ' + wanted_mechanism); + authenticating = true; } - } else if (handler.network.cap.requested.length === 0) { + } + if (!authenticating && handler.network.cap.requested.length === 0) { // If all of our requested CAPs have been handled, end CAP negotiation handler.connection.write('CAP END'); handler.network.cap.negotiating = false; @@ -244,12 +262,15 @@ const handlers = { handler.network.cap.enabled, capabilities ); + for (const cap_name of capabilities) { + handler.network.cap.available.delete(cap_name); + } break; } handler.emit('cap ' + command.params[1].toLowerCase(), { command: command.params[1], - capabilities: capability_values, + capabilities: capability_values, // for backward-compatibility }); }, diff --git a/src/networkinfo.js b/src/networkinfo.js index 14bea74d..75d29b70 100644 --- a/src/networkinfo.js +++ b/src/networkinfo.js @@ -108,6 +108,7 @@ function NetworkInfo() { negotiating: false, requested: [], enabled: [], + available: new Map(), isEnabled: function(cap_name) { return this.enabled.indexOf(cap_name) > -1; }