summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/irc/ircServices.sys.mjs
blob: 4f39bda237888e37b4da7b9c73213c55c40d80b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/* 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 <password not logged>"
      );
    }
  },

  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
      // <command> :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 <password not logged>"
        );
        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) {
      // [<channel name>] <message>
      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 <password>\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"
        )
      );
    },
  },
};