ae0100e7f4af3d85fec4045335feaf7d6a2c977f
[KiwiIRC.git] / server / irc / connection.js
1 var net = require('net'),
2 tls = require('tls'),
3 util = require('util'),
4 _ = require('lodash'),
5 EventEmitter2 = require('eventemitter2').EventEmitter2,
6 EventBinder = require('./eventbinder.js'),
7 IrcServer = require('./server.js'),
8 IrcChannel = require('./channel.js'),
9 IrcUser = require('./user.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 EventEmitter2.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, EventEmitter2);
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 _.each(this.irc_users, function (user) {
223 user.dispose();
224 });
225 _.each(this.irc_channels, function (chan) {
226 chan.dispose();
227 });
228 this.irc_users = undefined;
229 this.irc_channels = undefined;
230
231 this.server.dispose();
232 this.server = undefined;
233
234 EventBinder.unbindIrcEvents('', this.irc_events, this);
235
236 this.disposeSocket();
237 this.removeAllListeners();
238 };
239
240
241
242 /**
243 * Clean up any sockets for this IrcConnection
244 */
245 IrcConnection.prototype.disposeSocket = function () {
246 if (this.socket) {
247 this.socket.end();
248 this.socket.removeAllListeners();
249 this.socket = null;
250 }
251 };
252
253
254
255 function 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
274 function onServerConnect(event) {
275 this.nick = event.nick;
276 }
277
278
279 function 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
294 function 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
305 function 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
316 function 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
328
329
330
331 /**
332 * Handle the socket connect event, starting the IRCd registration
333 */
334 var socketConnectHandler = function () {
335 var that = this,
336 connect_data;
337
338 // Build up data to be used for webirc/etc detection
339 connect_data = {
340 connection: this,
341
342 // Array of lines to be sent to the IRCd before anything else
343 prepend_data: []
344 };
345
346 // Let the webirc/etc detection modify any required parameters
347 connect_data = findWebIrc.call(this, connect_data);
348
349 global.modules.emit('irc authorize', connect_data).done(function () {
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
359 if (that.password)
360 that.write('PASS ' + that.password);
361
362 that.write('NICK ' + that.nick);
363 that.write('USER ' + that.username + ' 0 0 :' + '[www.kiwiirc.com] ' + that.nick);
364
365 that.emit('connected');
366 });
367 };
368
369
370
371 /**
372 * Load any WEBIRC or alternative settings for this connection
373 * Called in scope of the IrcConnection instance
374 */
375 function findWebIrc(connect_data) {
376 var webirc_pass = global.config.webirc_pass,
377 ip_as_username = global.config.ip_as_username,
378 tmp;
379
380
381 // Do we have a WEBIRC password for this?
382 if (webirc_pass && webirc_pass[this.irc_host.hostname]) {
383 // Build the WEBIRC line to be sent before IRC registration
384 tmp = 'WEBIRC ' + webirc_pass[this.irc_host.hostname] + ' KiwiIRC ';
385 tmp += this.user.hostname + ' ' + this.user.address;
386
387 connect_data.prepend_data = [tmp];
388 }
389
390
391 // Check if we need to pass the users IP as its username/ident
392 if (ip_as_username && ip_as_username.indexOf(this.irc_host.hostname) > -1) {
393 // Get a hex value of the clients IP
394 this.username = this.user.address.split('.').map(function(i, idx){
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;
402 }).join('');
403
404 }
405
406 return connect_data;
407 }
408
409
410
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 */
416 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;
417
418 var parse = function (data) {
419 var i,
420 msg,
421 msg2,
422 trm,
423 j,
424 tags = [],
425 tag;
426
427 if (this.hold_last && this.held_data !== '') {
428 data = this.held_data + data;
429 this.hold_last = false;
430 this.held_data = '';
431 }
432
433 // If the last line is incomplete, hold it until we have more data
434 if (data.substr(-1) !== '\n') {
435 this.hold_last = true;
436 }
437
438 // Process our data line by line
439 data = data.split("\n");
440 for (i = 0; i < data.length; i++) {
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+$/, ''));
451
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]};
458 }
459 }
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(' ');
471 this.irc_commands.dispatch(msg.command.toUpperCase(), msg);
472 } else {
473 // The line was not parsed correctly, must be malformed
474 console.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, ''));
475 }
476 }
477 };