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