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