UNPKG

irc-framework

Version:
446 lines (431 loc) 18.7 kB
'use strict'; require("core-js/modules/es.symbol.js"); require("core-js/modules/es.symbol.description.js"); require("core-js/modules/es.symbol.iterator.js"); require("core-js/modules/es.array.from.js"); require("core-js/modules/es.array.is-array.js"); require("core-js/modules/es.array.iterator.js"); require("core-js/modules/es.array.slice.js"); require("core-js/modules/es.string.iterator.js"); require("core-js/modules/web.dom-collections.iterator.js"); require("core-js/modules/es.array.concat.js"); require("core-js/modules/es.array.filter.js"); require("core-js/modules/es.array.includes.js"); require("core-js/modules/es.array.index-of.js"); require("core-js/modules/es.array.join.js"); require("core-js/modules/es.array.map.js"); require("core-js/modules/es.date.now.js"); require("core-js/modules/es.date.to-string.js"); require("core-js/modules/es.function.name.js"); require("core-js/modules/es.object.create.js"); require("core-js/modules/es.object.to-string.js"); require("core-js/modules/es.parse-int.js"); require("core-js/modules/es.regexp.exec.js"); require("core-js/modules/es.regexp.to-string.js"); require("core-js/modules/es.string.includes.js"); require("core-js/modules/es.string.match.js"); require("core-js/modules/es.string.replace.js"); function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } var Helpers = require('../../helpers'); var _ = { intersection: require('lodash/intersection'), difference: require('lodash/difference'), each: require('lodash/each'), uniq: require('lodash/uniq') }; var handlers = { RPL_WELCOME: function RPL_WELCOME(command, handler) { var nick = command.params[0]; // Get the server name so we know which messages are by the server in future handler.network.server = command.prefix; handler.network.cap.negotiating = false; // We can't use the time given here as ZNC actually replays the time when it first connects // to an IRC server, not now(). Send a PING so that we can get a reliable time from PONG if (handler.network.cap.isEnabled('server-time')) { // Ping to try get a server-time in its response as soon as possible handler.connection.write('PING ' + Date.now()); } handler.emit('registered', { nick: nick, tags: command.tags }); }, RPL_YOURHOST: function RPL_YOURHOST(command, handler) { // Your host is ircd.network.org, running version InspIRCd-2.0 var param = command.params[1] || ''; var m = param.match(/running version (.*)$/); if (!m) { handler.network.ircd = ''; } else { handler.network.ircd = m[1]; } }, RPL_ISUPPORT: function RPL_ISUPPORT(command, handler) { var options = command.params; var i; var option; var matches; var j; for (i = 1; i < options.length; i++) { option = Helpers.splitOnce(options[i], '='); option[0] = option[0].toUpperCase(); // https://datatracker.ietf.org/doc/html/draft-brocklesby-irc-isupport-03 // 2. Protocol outline [page 4] if (option[1]) { option[1] = option[1].replace(/\\x([0-9A-Fa-f]{2})/g, function (match, hex) { return String.fromCharCode(parseInt(hex, 16)); }); } handler.network.options[option[0]] = typeof option[1] !== 'undefined' ? option[1] : true; if (option[0] === 'PREFIX') { matches = /\(([^)]*)\)(.*)/.exec(option[1]); if (matches && matches.length === 3) { handler.network.options.PREFIX = []; for (j = 0; j < matches[2].length; j++) { handler.network.options.PREFIX.push({ symbol: matches[2].charAt(j), mode: matches[1].charAt(j) }); } } else if (option[1] === '') { handler.network.options.PREFIX = []; } } else if (option[0] === 'CHANTYPES') { handler.network.options.CHANTYPES = handler.network.options.CHANTYPES.split(''); } else if (option[0] === 'STATUSMSG') { handler.network.options.STATUSMSG = handler.network.options.STATUSMSG.split(''); } else if (option[0] === 'CHANMODES') { handler.network.options.CHANMODES = option[1].split(','); } else if (option[0] === 'CASEMAPPING') { handler.network.options.CASEMAPPING = option[1]; } else if (option[0] === 'CLIENTTAGDENY') { // https://ircv3.net/specs/extensions/message-tags#rpl_isupport-tokens handler.network.options.CLIENTTAGDENY = option[1].split(',').filter(function (f) { return !!f; }); } else if (option[0] === 'NETWORK') { handler.network.name = option[1]; } else if (option[0] === 'NAMESX' && !handler.network.cap.isEnabled('multi-prefix')) { // Tell the server to send us all user modes in NAMES reply, not just // the highest one handler.connection.write('PROTOCTL NAMESX'); } } handler.emit('server options', { options: handler.network.options, cap: handler.network.cap.enabled, tags: command.tags }); }, CAP: function CAP(command, handler) { var request_caps = []; var capability_values = Object.create(null); // TODO: capability modifiers // i.e. - for disable, ~ for requires ACK, = for sticky var capabilities = command.params[command.params.length - 1].replace(/(?:^| )[-~=]/, '').split(' ').filter(function (cap) { return !!cap; }).map(function (cap) { // CAPs in 3.2 may be in the form of CAP=VAL. So seperate those out var 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; } var cap_name = cap.substr(0, sep); var 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; }); // Which capabilities we want to enable var want = ['cap-notify', 'batch', 'multi-prefix', 'message-tags', 'draft/message-tags-0.2', 'away-notify', 'invite-notify', 'account-notify', 'account-tag', 'server-time', 'userhost-in-names', 'extended-join', 'znc.in/server-time-iso', 'znc.in/server-time']; // Optional CAPs depending on settings var saslAuth = getSaslAuth(handler); if (saslAuth || handler.connection.options.sasl_mechanism === 'EXTERNAL') { want.push('sasl'); } if (handler.connection.options.enable_chghost) { want.push('chghost'); } if (handler.connection.options.enable_setname) { want.push('setname'); } if (handler.connection.options.enable_echomessage) { want.push('echo-message'); } want = _.uniq(want.concat(handler.request_extra_caps)); switch (command.params[1]) { case 'LS': // Compute which of the available capabilities we want and request them request_caps = _.intersection(capabilities, want); if (request_caps.length > 0) { handler.network.cap.requested = handler.network.cap.requested.concat(request_caps); } // CAP 3.2 multline support. Only send our CAP requests on the last CAP LS // line which will not have * set for params[2] if (command.params[2] !== '*') { if (handler.network.cap.requested.length > 0) { handler.network.cap.negotiating = true; handler.connection.write('CAP REQ :' + handler.network.cap.requested.join(' ')); } else { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } } break; case 'ACK': if (capabilities.length > 0) { // Update list of enabled capabilities handler.network.cap.enabled = _.uniq(handler.network.cap.enabled.concat(capabilities)); // Update list of capabilities we would like to have but that aren't enabled handler.network.cap.requested = _.difference(handler.network.cap.requested, capabilities); } if (handler.network.cap.negotiating) { var authenticating = false; if (handler.network.cap.isEnabled('sasl')) { var options_mechanism = handler.connection.options.sasl_mechanism; var wanted_mechanism = typeof options_mechanism === 'string' ? options_mechanism.toUpperCase() : 'PLAIN'; var mechanisms = handler.network.cap.available.get('sasl'); var 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 { // The client requested an SASL mechanism that is not supported by SASL v3.2 // emit 'sasl failed' with reason 'unsupported_mechanism' and disconnect if requested handleSaslFail(handler, 'unsupported_mechanism'); } } else if (saslAuth || handler.connection.options.sasl_mechanism === 'EXTERNAL') { // The client provided an account for SASL auth but the server did not offer the SASL cap // emit 'sasl failed' with reason 'capability_missing' and disconnect if requested handleSaslFail(handler, 'capability_missing'); } 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; } } break; case 'NAK': if (capabilities.length > 0) { handler.network.cap.requested = _.difference(handler.network.cap.requested, capabilities); } // If all of our requested CAPs have been handled, end CAP negotiation if (handler.network.cap.negotiating && handler.network.cap.requested.length === 0) { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } break; case 'LIST': // should we do anything here? break; case 'NEW': // Request any new CAPs that we want but haven't already enabled request_caps = []; for (var i = 0; i < capabilities.length; i++) { var cap = capabilities[i]; if (want.indexOf(cap) > -1 && request_caps.indexOf(cap) === -1 && !handler.network.cap.isEnabled(cap)) { handler.network.cap.requested.push(cap); request_caps.push(cap); } } handler.connection.write('CAP REQ :' + request_caps.join(' ')); break; case 'DEL': // Update list of enabled capabilities handler.network.cap.enabled = _.difference(handler.network.cap.enabled, capabilities); var _iterator = _createForOfIteratorHelper(capabilities), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var cap_name = _step.value; handler.network.cap.available["delete"](cap_name); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } break; } handler.emit('cap ' + command.params[1].toLowerCase(), { command: command.params[1], capabilities: capability_values // for backward-compatibility }); }, AUTHENTICATE: function AUTHENTICATE(command, handler) { if (command.params[0] !== '+') { if (handler.network.cap.negotiating) { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } return; } // Send blank authenticate for EXTERNAL mechanism if (handler.connection.options.sasl_mechanism === 'EXTERNAL') { handler.connection.write('AUTHENTICATE +'); return; } var saslAuth = getSaslAuth(handler); var auth_str = saslAuth.account + '\0' + saslAuth.account + '\0' + saslAuth.password; var b = Buffer.from(auth_str, 'utf8'); var b64 = b.toString('base64'); // https://ircv3.net/specs/extensions/sasl-3.1#the-authenticate-command var singleAuthCommandLength = 400; var sliceOffset = 0; while (b64.length > sliceOffset) { handler.connection.write('AUTHENTICATE ' + b64.substr(sliceOffset, singleAuthCommandLength)); sliceOffset += singleAuthCommandLength; } if (b64.length === sliceOffset) { handler.connection.write('AUTHENTICATE +'); } }, RPL_LOGGEDIN: function RPL_LOGGEDIN(command, handler) { if (handler.network.cap.negotiating === true) { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } var mask = Helpers.parseMask(command.params[1]); // Check if we have a server-time var time = command.getServerTime(); handler.emit('loggedin', { nick: command.params[0], ident: mask.user, hostname: mask.host, account: command.params[2], time: time, tags: command.tags }); handler.emit('account', { nick: command.params[0], ident: mask.user, hostname: mask.host, account: command.params[2], time: time, tags: command.tags }); }, RPL_LOGGEDOUT: function RPL_LOGGEDOUT(command, handler) { var mask = Helpers.parseMask(command.params[1]); // Check if we have a server-time var time = command.getServerTime(); handler.emit('loggedout', { nick: command.params[0], ident: mask.user, hostname: mask.host, account: false, time: time, tags: command.tags }); handler.emit('account', { nick: command.params[0], ident: mask.user, hostname: mask.host, account: false, time: time, tags: command.tags }); }, RPL_SASLLOGGEDIN: function RPL_SASLLOGGEDIN(command, handler) { if (handler.network.cap.negotiating) { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } }, ERR_NICKLOCKED: function ERR_NICKLOCKED(command, handler) { // SASL Authentication responded that the nick is locked // emit 'sasl failed' with reason 'nick_locked' and disconnect if requested handleSaslFail(handler, 'nick_locked', command); if (handler.network.cap.negotiating) { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } }, ERR_SASLFAIL: function ERR_SASLFAIL(command, handler) { // SASL Authentication responded with failure // emit 'sasl failed' with reason 'fail' and disconnect if requested handleSaslFail(handler, 'fail', command); if (handler.network.cap.negotiating) { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } }, ERR_SASLTOOLONG: function ERR_SASLTOOLONG(command, handler) { // SASL Authentication responded that the AUTHENTICATE command was too long // this should never happen as the library handles splitting // emit 'sasl failed' with reason 'too_long' and disconnect if requested handleSaslFail(handler, 'too_long', command); if (handler.network.cap.negotiating) { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } }, ERR_SASLABORTED: function ERR_SASLABORTED(command, handler) { if (handler.network.cap.negotiating) { handler.connection.write('CAP END'); handler.network.cap.negotiating = false; } }, ERR_SASLALREADYAUTHED: function ERR_SASLALREADYAUTHED(command, handler) { // noop } }; /** * Only use the nick+password combo if an account has not been specifically given. * If an account:{account,password} has been given, use it for SASL auth. */ function getSaslAuth(handler) { var options = handler.connection.options; if (options.account && options.account.account) { // An account username has been given, use it for SASL auth return { account: options.account.account, password: options.account.password || '' }; } else if (options.account) { // An account object existed but without auth credentials return null; } else if (options.password) { // No account credentials found but we have a server password. Also use it for SASL // for ease of use return { account: options.nick, password: options.password }; } return null; } function handleSaslFail(handler, reason, command) { var event = { reason: reason }; if (command) { var time = command.getServerTime(); event.message = command.params[command.params.length - 1]; event.nick = command.params[0]; event.time = time; event.tags = command.tags; } handler.emit('sasl failed', event); var sasl_disconnect_on_fail = handler.connection.options.sasl_disconnect_on_fail; if (sasl_disconnect_on_fail && handler.network.cap.negotiating) { handler.connection.end(); } } module.exports = function AddCommandHandlers(command_controller) { _.each(handlers, function (handler, handler_command) { command_controller.addHandler(handler_command, handler); }); };