Gateway.newConnection()
[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
JA
10 Socks;
11
12
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,
28 delimiter: ':'
29 });
4c25c0d7 30 this.setMaxListeners(0);
63008d5e 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;
b09157de
JA
46
47 // State object
48 this.state = state;
cefa0900
JA
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);
db8af19d
D
58
59 // IRC connection information
63008d5e
D
60 this.irc_host = {hostname: hostname, port: port};
61 this.ssl = !(!ssl);
9650b343
JA
62
63 // SOCKS proxy details
ac0b278c
JA
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 }
db8af19d
D
75
76 // Options sent by the IRCd
63008d5e
D
77 this.options = Object.create(null);
78 this.cap = {requested: [], enabled: []};
db8af19d
D
79
80 // Is SASL supported on the IRCd
63008d5e
D
81 this.sasl = false;
82
db8af19d 83 // Buffers for data sent from the IRCd
63008d5e
D
84 this.hold_last = false;
85 this.held_data = '';
86
4c25c0d7 87 this.applyIrcEvents();
db8af19d
D
88
89 // Call any modules before making the connection
90 global.modules.emit('irc:connecting', {connection: this})
91 .done(function () {
92 that.connect();
93 });
63008d5e 94};
cefa0900 95util.inherits(IrcConnection, EventEmitter2);
63008d5e
D
96
97module.exports.IrcConnection = IrcConnection;
98
f9ff7686 99
db8af19d 100
4c25c0d7
D
101IrcConnection.prototype.applyIrcEvents = function () {
102 // Listen for events on the IRC connection
103 this.irc_events = {
104 'server:*:connect': onServerConnect,
105 'channel:*:join': onChannelJoin,
df6d68a5
D
106
107 // TODO: uncomment when using an IrcUser per nick
108 //'user:*:privmsg': onUserPrivmsg,
9be602fc 109 'user:*:nick': onUserNick,
4c25c0d7
D
110 'channel:*:part': onUserParts,
111 'channel:*:quit': onUserParts,
112 'channel:*:kick': onUserParts
113 };
114
115 EventBinder.bindIrcEvents('', this.irc_events, this, this);
116};
117
db8af19d
D
118
119/**
120 * Start the connection to the IRCd
121 */
63008d5e
D
122IrcConnection.prototype.connect = function () {
123 var that = this;
9650b343 124 var socks;
63008d5e 125
db8af19d
D
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
9650b343
JA
133 // Are we connecting through a SOCKS proxy?
134 if (this.socks) {
57c22370 135 this.socket = Socks.connect({
9650b343 136 host: this.irc_host.hostname,
63008d5e 137 port: this.irc_host.port,
57c22370
JA
138 ssl: this.ssl,
139 rejectUnauthorized: global.config.reject_unauthorised_certificates
9650b343
JA
140 }, {host: this.socks.host,
141 port: this.socks.port,
142 user: this.socks.user,
143 pass: this.socks.pass
144 });
145
57c22370
JA
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
36108ca9 151 });
57c22370
JA
152
153 socket_connect_event_name = 'secureConnect';
154
2a8e95d1 155 } else {
57c22370
JA
156 this.socket = net.connect({
157 host: this.irc_host.hostname,
158 port: this.irc_host.port
2a8e95d1
D
159 });
160 }
57c22370
JA
161
162 this.socket.on(socket_connect_event_name, function () {
163 that.connected = true;
164 socketConnectHandler.call(that);
165 });
9650b343 166
db8af19d 167 this.socket.setEncoding('utf-8');
9650b343 168
d2d91c10
D
169 this.socket.on('error', function (event) {
170 that.emit('error', event);
db8af19d 171
2a8e95d1
D
172 });
173
2a8e95d1
D
174 this.socket.on('data', function () {
175 parse.apply(that, arguments);
176 });
177
db8af19d
D
178 this.socket.on('close', function (had_error) {
179 that.connected = false;
2a8e95d1 180 that.emit('close');
db8af19d
D
181
182 // Close the whole socket down
183 that.disposeSocket();
2a8e95d1 184 });
2a8e95d1 185};
2a8e95d1 186
3c91bff8
JA
187/**
188 * Send an event to the client
189 */
190IrcConnection.prototype.clientEvent = function (event_name, data, callback) {
191 data.server = this.con_num;
192 this.state.sendIrcCommand(event_name, data, callback);
193};
db8af19d
D
194
195/**
196 * Write a line of data to the IRCd
197 */
2a8e95d1 198IrcConnection.prototype.write = function (data, callback) {
db8af19d 199 this.socket.write(data + '\r\n', 'utf-8', callback);
2a8e95d1
D
200};
201
db8af19d
D
202
203
204/**
205 * Close the connection to the IRCd after sending one last line
206 */
2a8e95d1 207IrcConnection.prototype.end = function (data, callback) {
db8af19d
D
208 if (data)
209 this.write(data);
210
211 this.socket.end();
2a8e95d1
D
212};
213
db8af19d
D
214
215
216/**
217 * Clean up this IrcConnection instance and any sockets
218 */
c08717da 219IrcConnection.prototype.dispose = function () {
cefa0900
JA
220 _.each(this.irc_users, function (user) {
221 user.dispose();
222 });
223 _.each(this.irc_channels, function (chan) {
224 chan.dispose();
225 });
ebe178d6
D
226 this.irc_users = undefined;
227 this.irc_channels = undefined;
228
229 this.server.dispose();
230 this.server = undefined;
4c25c0d7
D
231
232 EventBinder.unbindIrcEvents('', this.irc_events, this);
233
db8af19d 234 this.disposeSocket();
c08717da
D
235 this.removeAllListeners();
236};
237
238
2a8e95d1 239
db8af19d
D
240/**
241 * Clean up any sockets for this IrcConnection
242 */
243IrcConnection.prototype.disposeSocket = function () {
244 if (this.socket) {
6b8fa7a6 245 this.socket.end();
db8af19d
D
246 this.socket.removeAllListeners();
247 this.socket = null;
248 }
2a8e95d1
D
249};
250
251
db8af19d 252
4c25c0d7
D
253function 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
272function onServerConnect(event) {
273 this.nick = event.nick;
df6d68a5
D
274
275 // TODO: use `event.nick` instead of `'*'` when using an IrcUser per nick
276 this.irc_users[event.nick] = new IrcUser(this, '*');
4c25c0d7
D
277}
278
279
280function 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
9be602fc
D
295function 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
4c25c0d7
D
306function 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
db8af19d
D
320/**
321 * Handle the socket connect event, starting the IRCd registration
322 */
323var socketConnectHandler = function () {
15fefff7
D
324 var that = this,
325 connect_data;
326
327 // Build up data to be used for webirc/etc detection
328 connect_data = {
bd7196e1
D
329 connection: this,
330
331 // Array of lines to be sent to the IRCd before anything else
332 prepend_data: []
15fefff7
D
333 };
334
335 // Let the webirc/etc detection modify any required parameters
266b5087 336 connect_data = findWebIrc.call(this, connect_data);
15fefff7 337
bd7196e1
D
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
db8af19d 348 if (that.password)
bd7196e1 349 that.write('PASS ' + that.password);
db8af19d 350
bd7196e1
D
351 that.write('NICK ' + that.nick);
352 that.write('USER ' + that.username + ' 0 0 :' + '[www.kiwiirc.com] ' + that.nick);
353
bd7196e1
D
354 that.emit('connected');
355 });
7dfe47c6 356};
2a8e95d1 357
15fefff7 358
db8af19d
D
359
360/**
361 * Load any WEBIRC or alternative settings for this connection
362 * Called in scope of the IrcConnection instance
363 */
15fefff7 364function findWebIrc(connect_data) {
db8af19d
D
365 var webirc_pass = global.config.webirc_pass,
366 ip_as_username = global.config.ip_as_username,
367 tmp;
368
15fefff7
D
369
370 // Do we have a WEBIRC password for this?
bd7196e1 371 if (webirc_pass && webirc_pass[this.irc_host.hostname]) {
db8af19d 372 // Build the WEBIRC line to be sent before IRC registration
bd7196e1
D
373 tmp = 'WEBIRC ' + webirc_pass[this.irc_host.hostname] + ' KiwiIRC ';
374 tmp += this.user.hostname + ' ' + this.user.address;
db8af19d 375
15fefff7
D
376 connect_data.prepend_data = [tmp];
377 }
378
379
380 // Check if we need to pass the users IP as its username/ident
bd7196e1 381 if (ip_as_username && ip_as_username.indexOf(this.irc_host.hostname) > -1) {
15fefff7 382 // Get a hex value of the clients IP
bd7196e1 383 this.username = this.user.address.split('.').map(function(i, idx){
15fefff7
D
384 return parseInt(i, 10).toString(16);
385 }).join('');
386
387 }
388
389 return connect_data;
390}
391
392
393
db8af19d
D
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 */
44bf687c 399var parse_regex = /^(?:(?:(?:(@[^ ]+) )?):(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/_]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
db8af19d 400
2a8e95d1
D
401var parse = function (data) {
402 var i,
403 msg,
16e717f5
JA
404 msg2,
405 trm,
406 j,
407 tags = [],
408 tag;
2a8e95d1 409
db8af19d 410 if (this.hold_last && this.held_data !== '') {
2a8e95d1
D
411 data = this.held_data + data;
412 this.hold_last = false;
413 this.held_data = '';
414 }
db8af19d
D
415
416 // If the last line is incomplete, hold it until we have more data
2a8e95d1
D
417 if (data.substr(-1) !== '\n') {
418 this.hold_last = true;
419 }
db8af19d
D
420
421 // Process our data line by line
2a8e95d1
D
422 data = data.split("\n");
423 for (i = 0; i < data.length; i++) {
db8af19d
D
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+$/, ''));
2a8e95d1 434
db8af19d
D
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]};
16e717f5 441 }
2a8e95d1 442 }
db8af19d
D
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+$/, ''));
2a8e95d1
D
461 }
462 }
463};