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