/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * This attempts to handle dealing with IRC services, which are a diverse set of * programs to automate and add features to IRCd. Often these services are seen * with the names NickServ, ChanServ, OperServ and MemoServ; but other services * do exist and are in use. * * Since the "protocol" behind services is really just text-based, human * readable messages, attempt to parse them, but always fall back to just * showing the message to the user if we're unsure what to do. * * Anope * https://www.anope.org/docgen/1.8/ */ import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { ircHandlerPriorities } from "resource:///modules/ircHandlerPriorities.sys.mjs"; /* * If a service is found, an extra field (serviceName) is added with the * "generic" service name (e.g. a bot which performs NickServ like functionality * will be mapped to NickServ). */ function ServiceMessage(aAccount, aMessage) { // This should be a property of the account or configurable somehow, it maps // from server specific service names to our generic service names (e.g. if // irc.foo.net has a service called bar, which acts as a NickServ, we would // map "bar": "NickServ"). Note that the keys of this map should be // normalized. let nicknameToServiceName = { chanserv: "ChanServ", infoserv: "InfoServ", nickserv: "NickServ", saslserv: "SaslServ", "freenode-connect": "freenode-connect", }; let nickname = aAccount.normalize(aMessage.origin); if (nicknameToServiceName.hasOwnProperty(nickname)) { aMessage.serviceName = nicknameToServiceName[nickname]; } return aMessage; } export var ircServices = { name: "IRC Services", priority: ircHandlerPriorities.HIGH_PRIORITY, isEnabled: () => true, sendIdentify(aAccount) { if ( aAccount.imAccount.password && aAccount.shouldAuthenticate && !aAccount.isAuthenticated ) { aAccount.sendMessage( "IDENTIFY", aAccount.imAccount.password, "IDENTIFY " ); } }, commands: { // If we automatically reply to a NOTICE message this does not abide by RFC // 2812. Oh well. NOTICE(ircMessage, ircHandlers) { if (!ircHandlers.hasServicesHandlers) { return false; } let message = ServiceMessage(this, ircMessage); // If no service was found, return early. if (!message.hasOwnProperty("serviceName")) { return false; } // If the name is recognized as a service name, add the service name field // and run it through the handlers. return ircHandlers.handleServicesMessage(this, message); }, NICK(aMessage) { let newNick = aMessage.params[0]; // We only auto-authenticate for the account nickname. if (this.normalize(newNick) != this.normalize(this._accountNickname)) { return false; } // If we're not identified already, try to identify. if (!this.isAuthenticated) { ircServices.sendIdentify(this); } // We always want the RFC 2812 handler to handle NICK, so return false. return false; }, "001": function (aMessage) { // RPL_WELCOME // If SASL authentication failed, attempt IDENTIFY. ircServices.sendIdentify(this); // We always want the RFC 2812 handler to handle 001, so return false. return false; }, 421(aMessage) { // ERR_UNKNOWNCOMMAND // :Unknown command // IDENTIFY failed, try NICKSERV IDENTIFY. if ( aMessage.params[1] == "IDENTIFY" && this.imAccount.password && this.shouldAuthenticate && !this.isAuthenticated ) { this.sendMessage( "NICKSERV", ["IDENTIFY", this.imAccount.password], "NICKSERV IDENTIFY " ); return true; } if (aMessage.params[1] == "NICKSERV") { this.WARN("NICKSERV command does not exist."); return true; } return false; }, }, }; export var servicesBase = { name: "IRC Services", priority: ircHandlerPriorities.DEFAULT_PRIORITY, isEnabled: () => true, commands: { ChanServ(aMessage) { // [] let channel = aMessage.params[1].split(" ", 1)[0]; if (!channel || channel[0] != "[" || channel.slice(-1)[0] != "]") { return false; } // Remove the [ and ]. channel = channel.slice(1, -1); // If it isn't a channel or doesn't exist, return early. if (!this.isMUCName(channel) || !this.conversations.has(channel)) { return false; } // Otherwise, display the message in that conversation. let params = { incoming: true }; if (aMessage.command == "NOTICE") { params.notification = true; } // The message starts after the channel name, plus [, ] and a space. let message = aMessage.params[1].slice(channel.length + 3); this.getConversation(channel).writeMessage( aMessage.origin, message, params ); return true; }, InfoServ(aMessage) { let text = aMessage.params[1]; // Show the message of the day in the server tab. if (text == "*** \u0002Message(s) of the Day\u0002 ***") { this._infoServMotd = [text]; return true; } else if (text == "*** \u0002End of Message(s) of the Day\u0002 ***") { if (this._showServerTab && this._infoServMotd) { this._infoServMotd.push(text); this.getConversation(aMessage.origin).writeMessage( aMessage.origin, this._infoServMotd.join("\n"), { incoming: true, } ); delete this._infoServMotd; } return true; } else if (this.hasOwnProperty("_infoServMotd")) { this._infoServMotd.push(text); return true; } return false; }, NickServ(message, ircHandlers) { // Since we feed the messages back through the system at the end of the // timeout when waiting for a log-in, we need to NOT try to handle them // here and let them fall through to the default handler. if (this.isHandlingQueuedMessages) { return false; } let text = message.params[1]; // If we have a queue of messages, we're waiting for authentication. if (this.nickservMessageQueue) { if ( text == "Password accepted - you are now recognized." || // Anope. text.startsWith("You are now identified for \x02") ) { // Atheme. // Password successfully accepted by NickServ, don't display the // queued messages. this.LOG("Successfully authenticated with NickServ."); this.isAuthenticated = true; clearTimeout(this.nickservAuthTimeout); delete this.nickservAuthTimeout; delete this.nickservMessageQueue; } else { // Queue any other messages that occur during the timeout so they // appear in the proper order. this.nickservMessageQueue.push(message); } return true; } // NickServ wants us to identify. if ( text == "This nick is owned by someone else. Please choose another." || // Anope. text == "This nickname is registered and protected. If it is your" || // Anope (SECURE enabled). text == "This nickname is registered. Please choose a different nickname, or identify via \x02/msg NickServ identify \x02." ) { // Atheme. this.LOG("Authentication requested by NickServ."); // Wait one second before showing the message to the user (giving the // the server time to process the log-in). this.nickservMessageQueue = [message]; this.nickservAuthTimeout = setTimeout( function () { this.isHandlingQueuedMessages = true; this.nickservMessageQueue.every(aMessage => ircHandlers.handleMessage(this, aMessage) ); delete this.isHandlingQueuedMessages; delete this.nickservMessageQueue; }.bind(this), 10000 ); return true; } if ( !this.isAuthenticated && (text == "You are already identified." || // Anope. text.startsWith("You are already logged in as \x02")) ) { // Atheme. // Do not show the message if caused by the automatic reauthentication. this.isAuthenticated = true; return true; } return false; }, /** * Ignore useless messages from SaslServ (unless showing of server messages * is enabled). * * @param {object} aMessage The IRC message object. * @returns {boolean} True if the message was handled, false if it should be * processed by another handler. */ SaslServ(aMessage) { // If the user would like to see server messages, fall through to the // standard handler. if (this._showServerTab) { return false; } // Only ignore the message notifying of last login. let text = aMessage.params[1]; return text.startsWith("Last login from: "); }, /* * freenode sends some annoying messages on start-up from a freenode-connect * bot. Only show these if the user wants to see server messages. See bug * 1521761. */ "freenode-connect": function (aMessage) { // If the user would like to see server messages, fall through to the // standard handler. if (this._showServerTab) { return false; } // Only ignore the message notifying of scanning (and include additional // checking of the hostname). return ( aMessage.host.startsWith("freenode/utility-bot/") && aMessage.params[1].includes( "connections will be scanned for vulnerabilities" ) ); }, }, };