irc-framework
Version:
A better IRC framework for node.js
446 lines (431 loc) • 18.7 kB
JavaScript
;
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);
});
};