Check the correct user for op status on a nick click
[KiwiIRC.git] / server / irc / connection.js
CommitLineData
cefa0900
JA
1var net = require('net'),
2 tls = require('tls'),
3 util = require('util'),
b156e01a 4 dns = require('dns'),
cefa0900 5 _ = require('lodash'),
4c25c0d7 6 EventBinder = require('./eventbinder.js'),
cefa0900
JA
7 IrcServer = require('./server.js'),
8 IrcChannel = require('./channel.js'),
9650b343 9 IrcUser = require('./user.js'),
2abb4615 10 EE = require('../ee.js'),
1a452a02 11 iconv = require('iconv-lite'),
55ccaf50 12 Socks;
32a09dc1
D
13
14
55ccaf50
JA
15// Break the Node.js version down into usable parts
16var version_values = process.version.substr(1).split('.').map(function (item) {
17 return parseInt(item, 10);
18});
9de636f9 19
55ccaf50 20// If we have a suitable Nodejs version, bring int he socks functionality
9de636f9 21if (version_values[1] >= 10) {
3b259b15 22 Socks = require('socksjs');
55ccaf50 23}
db8af19d 24
b09157de 25var IrcConnection = function (hostname, port, ssl, nick, user, pass, state) {
2a8e95d1 26 var that = this;
4c25c0d7 27
2abb4615 28 EE.call(this,{
cefa0900 29 wildcard: true,
d9285da9 30 delimiter: ' '
cefa0900 31 });
4c25c0d7 32 this.setMaxListeners(0);
32a09dc1 33
d1b3e8b3 34 // Set the first configured encoding as the default encoding
3efb4f33 35 this.encoding = global.config.default_encoding;
d1b3e8b3 36
db8af19d 37 // Socket state
63008d5e 38 this.connected = false;
db8af19d
D
39
40 // If registeration with the IRCd has completed
63008d5e 41 this.registered = false;
db8af19d
D
42
43 // If we are in the CAP negotiation stage
63008d5e 44 this.cap_negotiation = true;
db8af19d
D
45
46 // User information
63008d5e
D
47 this.nick = nick;
48 this.user = user; // Contains users real hostname and address
5973d01c 49 this.username = this.nick.replace(/[^0-9a-zA-Z\-_.\/]/, '');
db8af19d 50 this.password = pass;
32a09dc1 51
b09157de
JA
52 // State object
53 this.state = state;
32a09dc1 54
cefa0900
JA
55 // IrcServer object
56 this.server = new IrcServer(this, hostname, port);
32a09dc1 57
cefa0900
JA
58 // IrcUser objects
59 this.irc_users = Object.create(null);
efe9d487
D
60
61 // TODO: use `this.nick` instead of `'*'` when using an IrcUser per nick
62 this.irc_users[this.nick] = new IrcUser(this, '*');
63
cefa0900
JA
64 // IrcChannel objects
65 this.irc_channels = Object.create(null);
db8af19d
D
66
67 // IRC connection information
63008d5e
D
68 this.irc_host = {hostname: hostname, port: port};
69 this.ssl = !(!ssl);
32a09dc1 70
9650b343 71 // SOCKS proxy details
ac0b278c
JA
72 // TODO: Wildcard matching of hostnames and/or CIDR ranges of IP addresses
73 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)))) {
74 this.socks = {
75 host: global.config.socks_proxy.address,
76 port: global.config.socks_proxy.port,
77 user: global.config.socks_proxy.user,
78 pass: global.config.socks_proxy.pass
79 };
80 } else {
81 this.socks = false;
82 }
db8af19d
D
83
84 // Options sent by the IRCd
63008d5e
D
85 this.options = Object.create(null);
86 this.cap = {requested: [], enabled: []};
db8af19d
D
87
88 // Is SASL supported on the IRCd
63008d5e 89 this.sasl = false;
32a09dc1 90
db8af19d 91 // Buffers for data sent from the IRCd
63008d5e
D
92 this.hold_last = false;
93 this.held_data = '';
94
4c25c0d7 95 this.applyIrcEvents();
63008d5e 96};
2abb4615 97util.inherits(IrcConnection, EE);
63008d5e
D
98
99module.exports.IrcConnection = IrcConnection;
100
f9ff7686 101
db8af19d 102
4c25c0d7
D
103IrcConnection.prototype.applyIrcEvents = function () {
104 // Listen for events on the IRC connection
105 this.irc_events = {
d9285da9
JA
106 'server * connect': onServerConnect,
107 'channel * join': onChannelJoin,
df6d68a5
D
108
109 // TODO: uncomment when using an IrcUser per nick
110 //'user:*:privmsg': onUserPrivmsg,
d9285da9
JA
111 'user * nick': onUserNick,
112 'channel * part': onUserParts,
113 'channel * quit': onUserParts,
114 'channel * kick': onUserKick
4c25c0d7
D
115 };
116
117 EventBinder.bindIrcEvents('', this.irc_events, this, this);
118};
119
db8af19d
D
120
121/**
122 * Start the connection to the IRCd
123 */
63008d5e 124IrcConnection.prototype.connect = function () {
7496de01 125 var that = this;
63008d5e 126
db8af19d
D
127 // The socket connect event to listener for
128 var socket_connect_event_name = 'connect';
129
7496de01
D
130 // The destination address
131 var dest_addr = this.socks ?
132 this.socks.host :
133 this.irc_host.hostname;
db8af19d
D
134
135 // Make sure we don't already have an open connection
136 this.disposeSocket();
137
7496de01
D
138 // Get the IP family for the dest_addr (either socks or IRCd destination)
139 getConnectionFamily(dest_addr, function (err, family, host) {
140 var outgoing;
141
142 // Decide which net. interface to make the connection through
143 if (global.config.outgoing_address) {
144 if ((family === 'IPv6') && (global.config.outgoing_address.IPv6)) {
b156e01a
JA
145 outgoing = global.config.outgoing_address.IPv6;
146 } else {
7496de01 147 outgoing = global.config.outgoing_address.IPv4 || '0.0.0.0';
ddae94f5
D
148
149 // We don't have an IPv6 interface but dest_addr may still resolve to
150 // an IPv4 address. Reset `host` and try connecting anyway, letting it
151 // fail if an IPv4 resolved address is not found
152 host = dest_addr;
b156e01a 153 }
7496de01
D
154
155 } else {
156 // No config was found so use the default
157 outgoing = '0.0.0.0';
158 }
159
160 // Are we connecting through a SOCKS proxy?
161 if (this.socks) {
b156e01a 162 that.socket = Socks.connect({
ddae94f5 163 host: host,
b156e01a
JA
164 port: that.irc_host.port,
165 ssl: that.ssl,
166 rejectUnauthorized: global.config.reject_unauthorised_certificates
7496de01 167 }, {host: that.socks.host,
b156e01a
JA
168 port: that.socks.port,
169 user: that.socks.user,
170 pass: that.socks.pass,
171 localAddress: outgoing
172 });
173
7496de01
D
174 } else {
175 // No socks connection, connect directly to the IRCd
b156e01a
JA
176
177 if (that.ssl) {
178 that.socket = tls.connect({
ddae94f5 179 host: host,
b156e01a
JA
180 port: that.irc_host.port,
181 rejectUnauthorized: global.config.reject_unauthorised_certificates,
182 localAddress: outgoing
183 });
184
185 socket_connect_event_name = 'secureConnect';
186
187 } else {
188 that.socket = net.connect({
189 host: host,
190 port: that.irc_host.port,
191 localAddress: outgoing
192 });
193 }
7496de01 194 }
b156e01a 195
7496de01
D
196 // Apply the socket listeners
197 that.socket.on(socket_connect_event_name, function () {
b156e01a 198 that.connected = true;
cc3c5d04
D
199
200 // Make note of the port numbers for any identd lookups
201 // Nodejs < 0.9.6 has no socket.localPort so check this first
202 if (this.localPort) {
203 global.clients.port_pairs[this.localPort.toString() + '_' + this.remotePort.toString()] = that;
204 }
205
b156e01a
JA
206 socketConnectHandler.call(that);
207 });
32a09dc1 208
7496de01 209 that.socket.on('error', function (event) {
b156e01a
JA
210 that.emit('error', event);
211 });
32a09dc1 212
7496de01 213 that.socket.on('data', function () {
b156e01a
JA
214 parse.apply(that, arguments);
215 });
32a09dc1 216
7496de01 217 that.socket.on('close', function (had_error) {
b156e01a 218 that.connected = false;
cc3c5d04
D
219
220 // Remove this socket form the identd lookup
221 // Nodejs < 0.9.6 has no socket.localPort so check this first
222 if (this.localPort) {
223 delete global.clients.port_pairs[this.localPort.toString() + '_' + this.remotePort.toString()];
224 }
225
b156e01a 226 that.emit('close');
db8af19d 227
b156e01a
JA
228 // Close the whole socket down
229 that.disposeSocket();
230 });
7496de01 231 });
2a8e95d1 232};
2a8e95d1 233
3c91bff8
JA
234/**
235 * Send an event to the client
236 */
237IrcConnection.prototype.clientEvent = function (event_name, data, callback) {
238 data.server = this.con_num;
239 this.state.sendIrcCommand(event_name, data, callback);
240};
db8af19d
D
241
242/**
243 * Write a line of data to the IRCd
244 */
2a8e95d1 245IrcConnection.prototype.write = function (data, callback) {
1a452a02 246 //ENCODE string to encoding of the server
d1b3e8b3 247 encoded_buffer = iconv.encode(data + '\r\n', this.encoding);
1a452a02 248 this.socket.write(encoded_buffer);
2a8e95d1
D
249};
250
db8af19d
D
251
252
253/**
254 * Close the connection to the IRCd after sending one last line
255 */
2a8e95d1 256IrcConnection.prototype.end = function (data, callback) {
db8af19d
D
257 if (data)
258 this.write(data);
32a09dc1 259
db8af19d 260 this.socket.end();
2a8e95d1
D
261};
262
db8af19d
D
263
264
265/**
266 * Clean up this IrcConnection instance and any sockets
267 */
c08717da 268IrcConnection.prototype.dispose = function () {
5e211c3d
D
269 var that = this;
270
cefa0900
JA
271 _.each(this.irc_users, function (user) {
272 user.dispose();
273 });
274 _.each(this.irc_channels, function (chan) {
275 chan.dispose();
276 });
ebe178d6
D
277 this.irc_users = undefined;
278 this.irc_channels = undefined;
279
280 this.server.dispose();
281 this.server = undefined;
4c25c0d7
D
282
283 EventBinder.unbindIrcEvents('', this.irc_events, this);
284
5e211c3d
D
285 // If we're still connected, wait until the socket is closed before disposing
286 // so that all the events are still correctly triggered
287 if (this.socket && this.connected) {
288 this.socket.once('close', function() {
289 that.disposeSocket();
290 that.removeAllListeners();
291 });
292
293 this.socket.end();
294
295 } else {
296 this.disposeSocket();
297 this.removeAllListeners();
298 }
c08717da
D
299};
300
301
2a8e95d1 302
db8af19d
D
303/**
304 * Clean up any sockets for this IrcConnection
305 */
306IrcConnection.prototype.disposeSocket = function () {
307 if (this.socket) {
6b8fa7a6 308 this.socket.end();
db8af19d
D
309 this.socket.removeAllListeners();
310 this.socket = null;
311 }
2a8e95d1
D
312};
313
d1b3e8b3
VDF
314/**
315 * Set a new encoding for this connection
316 * Return true in case of success
317 */
318
319IrcConnection.prototype.setEncoding = function (encoding) {
3ddc135a
D
320 var encoded_test;
321
3efb4f33
VDF
322 try {
323 encoded_test = iconv.encode("TEST", encoding);
324 //This test is done to check if this encoding also supports
325 //the ASCII charset required by the IRC protocols
326 //(Avoid the use of base64 or incompatible encodings)
327 if (encoded_test == "TEST") {
328 this.encoding = encoding;
329 return true;
330 }
331 return false;
332 } catch (err) {
333 return false;
65c5a477 334 }
d1b3e8b3 335};
2a8e95d1 336
b156e01a
JA
337function getConnectionFamily(host, callback) {
338 if (net.isIP(host)) {
339 if (net.isIPv4(host)) {
340 setImmediate(callback, null, 'IPv4', host);
341 } else {
342 setImmediate(callback, null, 'IPv6', host);
343 }
344 } else {
345 dns.resolve6(host, function (err, addresses) {
346 if (!err) {
347 callback(null, 'IPv6', addresses[0]);
348 } else {
349 dns.resolve4(host, function (err, addresses) {
350 if (!err) {
351 callback(null, 'IPv4',addresses[0]);
352 } else {
353 callback(err);
354 }
355 });
356 }
357 });
358 }
359}
360
db8af19d 361
4c25c0d7
D
362function onChannelJoin(event) {
363 var chan;
364
365 // Only deal with ourselves joining a channel
366 if (event.nick !== this.nick)
367 return;
368
369 // We should only ever get a JOIN command for a channel
370 // we're not already a member of.. but check we don't
371 // have this channel in case something went wrong somewhere
372 // at an earlier point
373 if (!this.irc_channels[event.channel]) {
374 chan = new IrcChannel(this, event.channel);
375 this.irc_channels[event.channel] = chan;
376 chan.irc_events.join.call(chan, event);
377 }
378}
379
380
381function onServerConnect(event) {
382 this.nick = event.nick;
4c25c0d7
D
383}
384
385
386function onUserPrivmsg(event) {
387 var user;
388
389 // Only deal with messages targetted to us
390 if (event.channel !== this.nick)
391 return;
392
393 if (!this.irc_users[event.nick]) {
394 user = new IrcUser(this, event.nick);
395 this.irc_users[event.nick] = user;
396 user.irc_events.privmsg.call(user, event);
397 }
398}
399
400
9be602fc
D
401function onUserNick(event) {
402 var user;
403
404 // Only deal with messages targetted to us
405 if (event.nick !== this.nick)
406 return;
407
408 this.nick = event.newnick;
409}
410
411
4c25c0d7
D
412function onUserParts(event) {
413 // Only deal with ourselves leaving a channel
414 if (event.nick !== this.nick)
415 return;
416
417 if (this.irc_channels[event.channel]) {
418 this.irc_channels[event.channel].dispose();
419 delete this.irc_channels[event.channel];
420 }
421}
422
6d2df752
MC
423function onUserKick(event){
424 // Only deal with ourselves being kicked from a channel
425 if (event.kicked !== this.nick)
426 return;
427
428 if (this.irc_channels[event.channel]) {
429 this.irc_channels[event.channel].dispose();
430 delete this.irc_channels[event.channel];
431 }
432
433}
434
4c25c0d7
D
435
436
437
db8af19d
D
438/**
439 * Handle the socket connect event, starting the IRCd registration
440 */
441var socketConnectHandler = function () {
15fefff7
D
442 var that = this,
443 connect_data;
444
445 // Build up data to be used for webirc/etc detection
446 connect_data = {
bd7196e1
D
447 connection: this,
448
449 // Array of lines to be sent to the IRCd before anything else
450 prepend_data: []
15fefff7
D
451 };
452
453 // Let the webirc/etc detection modify any required parameters
266b5087 454 connect_data = findWebIrc.call(this, connect_data);
15fefff7 455
d9285da9 456 global.modules.emit('irc authorize', connect_data).done(function () {
bd7196e1
D
457 // Send any initial data for webirc/etc
458 if (connect_data.prepend_data) {
459 _.each(connect_data.prepend_data, function(data) {
460 that.write(data);
461 });
462 }
463
464 that.write('CAP LS');
465
db8af19d 466 if (that.password)
bd7196e1 467 that.write('PASS ' + that.password);
32a09dc1 468
bd7196e1
D
469 that.write('NICK ' + that.nick);
470 that.write('USER ' + that.username + ' 0 0 :' + '[www.kiwiirc.com] ' + that.nick);
32a09dc1 471
bd7196e1
D
472 that.emit('connected');
473 });
7dfe47c6 474};
2a8e95d1 475
15fefff7 476
db8af19d
D
477
478/**
479 * Load any WEBIRC or alternative settings for this connection
480 * Called in scope of the IrcConnection instance
481 */
15fefff7 482function findWebIrc(connect_data) {
db8af19d
D
483 var webirc_pass = global.config.webirc_pass,
484 ip_as_username = global.config.ip_as_username,
485 tmp;
486
15fefff7
D
487
488 // Do we have a WEBIRC password for this?
bd7196e1 489 if (webirc_pass && webirc_pass[this.irc_host.hostname]) {
db8af19d 490 // Build the WEBIRC line to be sent before IRC registration
bd7196e1
D
491 tmp = 'WEBIRC ' + webirc_pass[this.irc_host.hostname] + ' KiwiIRC ';
492 tmp += this.user.hostname + ' ' + this.user.address;
db8af19d 493
15fefff7
D
494 connect_data.prepend_data = [tmp];
495 }
496
497
498 // Check if we need to pass the users IP as its username/ident
bd7196e1 499 if (ip_as_username && ip_as_username.indexOf(this.irc_host.hostname) > -1) {
15fefff7 500 // Get a hex value of the clients IP
bd7196e1 501 this.username = this.user.address.split('.').map(function(i, idx){
32800bf5
D
502 var hex = parseInt(i, 10).toString(16);
503
504 // Pad out the hex value if it's a single char
505 if (hex.length === 1)
506 hex = '0' + hex;
507
508 return hex;
15fefff7
D
509 }).join('');
510
511 }
512
513 return connect_data;
514}
515
516
517
db8af19d
D
518/**
519 * The regex that parses a line of data from the IRCd
520 * Deviates from the RFC a little to support the '/' character now used in some
521 * IRCds
522 */
85e1be6c 523var parse_regex = /^(?:(?:(?:(@[^ ]+) )?):(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)!([^\x00\r\n\ ]+?)@?([a-z0-9\.\-:\/_]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
db8af19d 524
2a8e95d1
D
525var parse = function (data) {
526 var i,
527 msg,
16e717f5
JA
528 msg2,
529 trm,
530 j,
531 tags = [],
532 tag;
3ec786bc 533
1a452a02 534 //DECODE server encoding
d1b3e8b3 535 data = iconv.decode(data, this.encoding);
d1b3e8b3 536
db8af19d 537 if (this.hold_last && this.held_data !== '') {
2a8e95d1
D
538 data = this.held_data + data;
539 this.hold_last = false;
540 this.held_data = '';
541 }
db8af19d
D
542
543 // If the last line is incomplete, hold it until we have more data
2a8e95d1
D
544 if (data.substr(-1) !== '\n') {
545 this.hold_last = true;
546 }
db8af19d
D
547
548 // Process our data line by line
2a8e95d1
D
549 data = data.split("\n");
550 for (i = 0; i < data.length; i++) {
db8af19d
D
551 if (!data[i]) break;
552
553 // If flagged to hold the last line, store it and move on
554 if (this.hold_last && (i === data.length - 1)) {
555 this.held_data = data[i];
556 break;
557 }
1a452a02 558
db8af19d
D
559 // Parse the complete line, removing any carriage returns
560 msg = parse_regex.exec(data[i].replace(/^\r+|\r+$/, ''));
2a8e95d1 561
db8af19d
D
562 if (msg) {
563 if (msg[1]) {
564 tags = msg[1].split(';');
565 for (j = 0; j < tags.length; j++) {
566 tag = tags[j].split('=');
567 tags[j] = {tag: tag[0], value: tag[1]};
16e717f5 568 }
2a8e95d1 569 }
db8af19d
D
570 msg = {
571 tags: tags,
572 prefix: msg[2],
573 nick: msg[3],
574 ident: msg[4],
575 hostname: msg[5] || '',
576 command: msg[6],
577 params: msg[7] || '',
578 trailing: (msg[8]) ? msg[8].trim() : ''
579 };
580 msg.params = msg.params.split(' ');
3ec786bc 581 this.irc_commands.dispatch(msg.command.toUpperCase(), msg);
db8af19d 582 } else {
db8af19d
D
583 // The line was not parsed correctly, must be malformed
584 console.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, ''));
2a8e95d1
D
585 }
586 }
587};