Correctly triggering IRC socket close events
[KiwiIRC.git] / server / irc / connection.js
1 var net = require('net'),
2 tls = require('tls'),
3 util = require('util'),
4 _ = require('lodash'),
5 EventBinder = require('./eventbinder.js'),
6 IrcServer = require('./server.js'),
7 IrcChannel = require('./channel.js'),
8 IrcUser = require('./user.js'),
9 EE = require('../ee.js'),
10 Socks;
11
12
13 // Break the Node.js version down into usable parts
14 var version_values = process.version.substr(1).split('.').map(function (item) {
15 return parseInt(item, 10);
16 });
17
18 // If we have a suitable Nodejs version, bring int he socks functionality
19 if (version_values[1] >= 10) {
20 Socks = require('socksjs');
21 }
22
23 var IrcConnection = function (hostname, port, ssl, nick, user, pass, state) {
24 var that = this;
25
26 EE.call(this,{
27 wildcard: true,
28 delimiter: ' '
29 });
30 this.setMaxListeners(0);
31
32 // Socket state
33 this.connected = false;
34
35 // If registeration with the IRCd has completed
36 this.registered = false;
37
38 // If we are in the CAP negotiation stage
39 this.cap_negotiation = true;
40
41 // User information
42 this.nick = nick;
43 this.user = user; // Contains users real hostname and address
44 this.username = this.nick.replace(/[^0-9a-zA-Z\-_.\/]/, '');
45 this.password = pass;
46
47 // State object
48 this.state = state;
49
50 // IrcServer object
51 this.server = new IrcServer(this, hostname, port);
52
53 // IrcUser objects
54 this.irc_users = Object.create(null);
55
56 // TODO: use `this.nick` instead of `'*'` when using an IrcUser per nick
57 this.irc_users[this.nick] = new IrcUser(this, '*');
58
59 // IrcChannel objects
60 this.irc_channels = Object.create(null);
61
62 // IRC connection information
63 this.irc_host = {hostname: hostname, port: port};
64 this.ssl = !(!ssl);
65
66 // SOCKS proxy details
67 // TODO: Wildcard matching of hostnames and/or CIDR ranges of IP addresses
68 if ((global.config.socks_proxy && global.config.socks_proxy.enabled) && ((global.config.socks_proxy.all) || (_.contains(global.config.socks_proxy.proxy_hosts, this.irc_host.hostname)))) {
69 this.socks = {
70 host: global.config.socks_proxy.address,
71 port: global.config.socks_proxy.port,
72 user: global.config.socks_proxy.user,
73 pass: global.config.socks_proxy.pass
74 };
75 } else {
76 this.socks = false;
77 }
78
79 // Options sent by the IRCd
80 this.options = Object.create(null);
81 this.cap = {requested: [], enabled: []};
82
83 // Is SASL supported on the IRCd
84 this.sasl = false;
85
86 // Buffers for data sent from the IRCd
87 this.hold_last = false;
88 this.held_data = '';
89
90 this.applyIrcEvents();
91
92 // Call any modules before making the connection
93 global.modules.emit('irc connecting', {connection: this})
94 .done(function () {
95 that.connect();
96 });
97 };
98 util.inherits(IrcConnection, EE);
99
100 module.exports.IrcConnection = IrcConnection;
101
102
103
104 IrcConnection.prototype.applyIrcEvents = function () {
105 // Listen for events on the IRC connection
106 this.irc_events = {
107 'server * connect': onServerConnect,
108 'channel * join': onChannelJoin,
109
110 // TODO: uncomment when using an IrcUser per nick
111 //'user:*:privmsg': onUserPrivmsg,
112 'user * nick': onUserNick,
113 'channel * part': onUserParts,
114 'channel * quit': onUserParts,
115 'channel * kick': onUserKick
116 };
117
118 EventBinder.bindIrcEvents('', this.irc_events, this, this);
119 };
120
121
122 /**
123 * Start the connection to the IRCd
124 */
125 IrcConnection.prototype.connect = function () {
126 var that = this;
127 var socks;
128
129 // The socket connect event to listener for
130 var socket_connect_event_name = 'connect';
131
132
133 // Make sure we don't already have an open connection
134 this.disposeSocket();
135
136 // Are we connecting through a SOCKS proxy?
137 if (this.socks) {
138 this.socket = Socks.connect({
139 host: this.irc_host.hostname,
140 port: this.irc_host.port,
141 ssl: this.ssl,
142 rejectUnauthorized: global.config.reject_unauthorised_certificates
143 }, {host: this.socks.host,
144 port: this.socks.port,
145 user: this.socks.user,
146 pass: this.socks.pass
147 });
148
149 } else if (this.ssl) {
150 this.socket = tls.connect({
151 host: this.irc_host.hostname,
152 port: this.irc_host.port,
153 rejectUnauthorized: global.config.reject_unauthorised_certificates
154 });
155
156 socket_connect_event_name = 'secureConnect';
157
158 } else {
159 this.socket = net.connect({
160 host: this.irc_host.hostname,
161 port: this.irc_host.port
162 });
163 }
164
165 this.socket.on(socket_connect_event_name, function () {
166 that.connected = true;
167 socketConnectHandler.call(that);
168 });
169
170 this.socket.setEncoding('utf-8');
171
172 this.socket.on('error', function (event) {
173 that.emit('error', event);
174 });
175
176 this.socket.on('data', function () {
177 parse.apply(that, arguments);
178 });
179
180 this.socket.on('close', function (had_error) {
181 that.connected = false;
182 that.emit('close');
183
184 // Close the whole socket down
185 that.disposeSocket();
186 });
187 };
188
189 /**
190 * Send an event to the client
191 */
192 IrcConnection.prototype.clientEvent = function (event_name, data, callback) {
193 data.server = this.con_num;
194 this.state.sendIrcCommand(event_name, data, callback);
195 };
196
197 /**
198 * Write a line of data to the IRCd
199 */
200 IrcConnection.prototype.write = function (data, callback) {
201 this.socket.write(data + '\r\n', 'utf-8', callback);
202 };
203
204
205
206 /**
207 * Close the connection to the IRCd after sending one last line
208 */
209 IrcConnection.prototype.end = function (data, callback) {
210 if (data)
211 this.write(data);
212
213 this.socket.end();
214 };
215
216
217
218 /**
219 * Clean up this IrcConnection instance and any sockets
220 */
221 IrcConnection.prototype.dispose = function () {
222 var that = this;
223
224 _.each(this.irc_users, function (user) {
225 user.dispose();
226 });
227 _.each(this.irc_channels, function (chan) {
228 chan.dispose();
229 });
230 this.irc_users = undefined;
231 this.irc_channels = undefined;
232
233 this.server.dispose();
234 this.server = undefined;
235
236 EventBinder.unbindIrcEvents('', this.irc_events, this);
237
238 // If we're still connected, wait until the socket is closed before disposing
239 // so that all the events are still correctly triggered
240 if (this.socket && this.connected) {
241 this.socket.once('close', function() {
242 that.disposeSocket();
243 that.removeAllListeners();
244 });
245
246 this.socket.end();
247
248 } else {
249 this.disposeSocket();
250 this.removeAllListeners();
251 }
252 };
253
254
255
256 /**
257 * Clean up any sockets for this IrcConnection
258 */
259 IrcConnection.prototype.disposeSocket = function () {
260 if (this.socket) {
261 this.socket.end();
262 this.socket.removeAllListeners();
263 this.socket = null;
264 }
265 };
266
267
268
269 function onChannelJoin(event) {
270 var chan;
271
272 // Only deal with ourselves joining a channel
273 if (event.nick !== this.nick)
274 return;
275
276 // We should only ever get a JOIN command for a channel
277 // we're not already a member of.. but check we don't
278 // have this channel in case something went wrong somewhere
279 // at an earlier point
280 if (!this.irc_channels[event.channel]) {
281 chan = new IrcChannel(this, event.channel);
282 this.irc_channels[event.channel] = chan;
283 chan.irc_events.join.call(chan, event);
284 }
285 }
286
287
288 function onServerConnect(event) {
289 this.nick = event.nick;
290 }
291
292
293 function onUserPrivmsg(event) {
294 var user;
295
296 // Only deal with messages targetted to us
297 if (event.channel !== this.nick)
298 return;
299
300 if (!this.irc_users[event.nick]) {
301 user = new IrcUser(this, event.nick);
302 this.irc_users[event.nick] = user;
303 user.irc_events.privmsg.call(user, event);
304 }
305 }
306
307
308 function onUserNick(event) {
309 var user;
310
311 // Only deal with messages targetted to us
312 if (event.nick !== this.nick)
313 return;
314
315 this.nick = event.newnick;
316 }
317
318
319 function onUserParts(event) {
320 // Only deal with ourselves leaving a channel
321 if (event.nick !== this.nick)
322 return;
323
324 if (this.irc_channels[event.channel]) {
325 this.irc_channels[event.channel].dispose();
326 delete this.irc_channels[event.channel];
327 }
328 }
329
330 function onUserKick(event){
331 // Only deal with ourselves being kicked from a channel
332 if (event.kicked !== this.nick)
333 return;
334
335 if (this.irc_channels[event.channel]) {
336 this.irc_channels[event.channel].dispose();
337 delete this.irc_channels[event.channel];
338 }
339
340 }
341
342
343
344
345 /**
346 * Handle the socket connect event, starting the IRCd registration
347 */
348 var socketConnectHandler = function () {
349 var that = this,
350 connect_data;
351
352 // Build up data to be used for webirc/etc detection
353 connect_data = {
354 connection: this,
355
356 // Array of lines to be sent to the IRCd before anything else
357 prepend_data: []
358 };
359
360 // Let the webirc/etc detection modify any required parameters
361 connect_data = findWebIrc.call(this, connect_data);
362
363 global.modules.emit('irc authorize', connect_data).done(function () {
364 // Send any initial data for webirc/etc
365 if (connect_data.prepend_data) {
366 _.each(connect_data.prepend_data, function(data) {
367 that.write(data);
368 });
369 }
370
371 that.write('CAP LS');
372
373 if (that.password)
374 that.write('PASS ' + that.password);
375
376 that.write('NICK ' + that.nick);
377 that.write('USER ' + that.username + ' 0 0 :' + '[www.kiwiirc.com] ' + that.nick);
378
379 that.emit('connected');
380 });
381 };
382
383
384
385 /**
386 * Load any WEBIRC or alternative settings for this connection
387 * Called in scope of the IrcConnection instance
388 */
389 function findWebIrc(connect_data) {
390 var webirc_pass = global.config.webirc_pass,
391 ip_as_username = global.config.ip_as_username,
392 tmp;
393
394
395 // Do we have a WEBIRC password for this?
396 if (webirc_pass && webirc_pass[this.irc_host.hostname]) {
397 // Build the WEBIRC line to be sent before IRC registration
398 tmp = 'WEBIRC ' + webirc_pass[this.irc_host.hostname] + ' KiwiIRC ';
399 tmp += this.user.hostname + ' ' + this.user.address;
400
401 connect_data.prepend_data = [tmp];
402 }
403
404
405 // Check if we need to pass the users IP as its username/ident
406 if (ip_as_username && ip_as_username.indexOf(this.irc_host.hostname) > -1) {
407 // Get a hex value of the clients IP
408 this.username = this.user.address.split('.').map(function(i, idx){
409 var hex = parseInt(i, 10).toString(16);
410
411 // Pad out the hex value if it's a single char
412 if (hex.length === 1)
413 hex = '0' + hex;
414
415 return hex;
416 }).join('');
417
418 }
419
420 return connect_data;
421 }
422
423
424
425 /**
426 * The regex that parses a line of data from the IRCd
427 * Deviates from the RFC a little to support the '/' character now used in some
428 * IRCds
429 */
430 var parse_regex = /^(?:(?:(?:(@[^ ]+) )?):(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)!([^\x00\r\n\ ]+?)@?([a-z0-9\.\-:\/_]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
431
432 var parse = function (data) {
433 var i,
434 msg,
435 msg2,
436 trm,
437 j,
438 tags = [],
439 tag;
440
441 if (this.hold_last && this.held_data !== '') {
442 data = this.held_data + data;
443 this.hold_last = false;
444 this.held_data = '';
445 }
446
447 // If the last line is incomplete, hold it until we have more data
448 if (data.substr(-1) !== '\n') {
449 this.hold_last = true;
450 }
451
452 // Process our data line by line
453 data = data.split("\n");
454 for (i = 0; i < data.length; i++) {
455 if (!data[i]) break;
456
457 // If flagged to hold the last line, store it and move on
458 if (this.hold_last && (i === data.length - 1)) {
459 this.held_data = data[i];
460 break;
461 }
462
463 // Parse the complete line, removing any carriage returns
464 msg = parse_regex.exec(data[i].replace(/^\r+|\r+$/, ''));
465
466 if (msg) {
467 if (msg[1]) {
468 tags = msg[1].split(';');
469 for (j = 0; j < tags.length; j++) {
470 tag = tags[j].split('=');
471 tags[j] = {tag: tag[0], value: tag[1]};
472 }
473 }
474 msg = {
475 tags: tags,
476 prefix: msg[2],
477 nick: msg[3],
478 ident: msg[4],
479 hostname: msg[5] || '',
480 command: msg[6],
481 params: msg[7] || '',
482 trailing: (msg[8]) ? msg[8].trim() : ''
483 };
484 msg.params = msg.params.split(' ');
485 this.irc_commands.dispatch(msg.command.toUpperCase(), msg);
486 } else {
487 // The line was not parsed correctly, must be malformed
488 console.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, ''));
489 }
490 }
491 };