SOCKS proxy conf and integration
[KiwiIRC.git] / server / irc / connection.js
... / ...
CommitLineData
1var 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 SocksConnection = require('../socks.js');
11
12
13var IrcConnection = function (hostname, port, ssl, nick, user, pass, state) {
14 var that = this;
15
16 EventEmitter2.call(this,{
17 wildcard: true,
18 delimiter: ':'
19 });
20 this.setMaxListeners(0);
21
22 // Socket state
23 this.connected = false;
24
25 // If registeration with the IRCd has completed
26 this.registered = false;
27
28 // If we are in the CAP negotiation stage
29 this.cap_negotiation = true;
30
31 // User information
32 this.nick = nick;
33 this.user = user; // Contains users real hostname and address
34 this.username = this.nick.replace(/[^0-9a-zA-Z\-_.]/, '');
35 this.password = pass;
36
37 // State object
38 this.state = state;
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);
48
49 // IRC connection information
50 this.irc_host = {hostname: hostname, port: port};
51 this.ssl = !(!ssl);
52
53 // SOCKS proxy details
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 }
65
66 // Options sent by the IRCd
67 this.options = Object.create(null);
68 this.cap = {requested: [], enabled: []};
69
70 // Is SASL supported on the IRCd
71 this.sasl = false;
72
73 // Buffers for data sent from the IRCd
74 this.hold_last = false;
75 this.held_data = '';
76
77 this.applyIrcEvents();
78
79 // Call any modules before making the connection
80 global.modules.emit('irc:connecting', {connection: this})
81 .done(function () {
82 that.connect();
83 });
84};
85util.inherits(IrcConnection, EventEmitter2);
86
87module.exports.IrcConnection = IrcConnection;
88
89
90
91IrcConnection.prototype.applyIrcEvents = function () {
92 // Listen for events on the IRC connection
93 this.irc_events = {
94 'server:*:connect': onServerConnect,
95 'channel:*:join': onChannelJoin,
96
97 // TODO: uncomment when using an IrcUser per nick
98 //'user:*:privmsg': onUserPrivmsg,
99 'user:*:nick': onUserNick,
100 'channel:*:part': onUserParts,
101 'channel:*:quit': onUserParts,
102 'channel:*:kick': onUserParts
103 };
104
105 EventBinder.bindIrcEvents('', this.irc_events, this, this);
106};
107
108
109/**
110 * Start the connection to the IRCd
111 */
112IrcConnection.prototype.connect = function () {
113 var that = this;
114 var socks;
115
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
123 // Are we connecting through a SOCKS proxy?
124 if (this.socks) {
125 socks = new SocksConnection({
126 host: this.irc_host.hostname,
127 port: this.irc_host.port,
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
135 socks.on('connect', function (socket) {
136 that.socket = socket;
137 setupSocket.call(that);
138 that.connected = true;
139 socketConnectHandler.call(that);
140 });
141 } else {
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;
161 socketConnectHandler.call(that);
162 });
163
164 setupSocket.call(this);
165 }
166};
167
168/**
169 * Set the socket's encoding and add event handlers
170 */
171var setupSocket = function () {
172 var that = this;
173
174 this.socket.setEncoding('utf-8');
175
176 this.socket.on('error', function (event) {
177 that.emit('error', event);
178
179 });
180
181 this.socket.on('data', function () {
182 parse.apply(that, arguments);
183 });
184
185 this.socket.on('close', function (had_error) {
186 that.connected = false;
187 that.emit('close');
188
189 // Close the whole socket down
190 that.disposeSocket();
191 });
192};
193
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};
201
202/**
203 * Write a line of data to the IRCd
204 */
205IrcConnection.prototype.write = function (data, callback) {
206 this.socket.write(data + '\r\n', 'utf-8', callback);
207};
208
209
210
211/**
212 * Close the connection to the IRCd after sending one last line
213 */
214IrcConnection.prototype.end = function (data, callback) {
215 if (data)
216 this.write(data);
217
218 this.socket.end();
219};
220
221
222
223/**
224 * Clean up this IrcConnection instance and any sockets
225 */
226IrcConnection.prototype.dispose = function () {
227 _.each(this.irc_users, function (user) {
228 user.dispose();
229 });
230 _.each(this.irc_channels, function (chan) {
231 chan.dispose();
232 });
233 this.irc_users = undefined;
234 this.irc_channels = undefined;
235
236 this.server.dispose();
237 this.server = undefined;
238
239 EventBinder.unbindIrcEvents('', this.irc_events, this);
240
241 this.disposeSocket();
242 this.removeAllListeners();
243};
244
245
246
247/**
248 * Clean up any sockets for this IrcConnection
249 */
250IrcConnection.prototype.disposeSocket = function () {
251 if (this.socket) {
252 this.socket.end();
253 this.socket.removeAllListeners();
254 this.socket = null;
255 }
256};
257
258
259
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;
281
282 // TODO: use `event.nick` instead of `'*'` when using an IrcUser per nick
283 this.irc_users[event.nick] = new IrcUser(this, '*');
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
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
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
327/**
328 * Handle the socket connect event, starting the IRCd registration
329 */
330var socketConnectHandler = function () {
331 var that = this,
332 connect_data;
333
334 // Build up data to be used for webirc/etc detection
335 connect_data = {
336 connection: this,
337
338 // Array of lines to be sent to the IRCd before anything else
339 prepend_data: []
340 };
341
342 // Let the webirc/etc detection modify any required parameters
343 connect_data = findWebIrc.call(this, connect_data);
344
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
355 if (that.password)
356 that.write('PASS ' + that.password);
357
358 that.write('NICK ' + that.nick);
359 that.write('USER ' + that.username + ' 0 0 :' + '[www.kiwiirc.com] ' + that.nick);
360
361 that.emit('connected');
362 });
363};
364
365
366
367/**
368 * Load any WEBIRC or alternative settings for this connection
369 * Called in scope of the IrcConnection instance
370 */
371function findWebIrc(connect_data) {
372 var webirc_pass = global.config.webirc_pass,
373 ip_as_username = global.config.ip_as_username,
374 tmp;
375
376
377 // Do we have a WEBIRC password for this?
378 if (webirc_pass && webirc_pass[this.irc_host.hostname]) {
379 // Build the WEBIRC line to be sent before IRC registration
380 tmp = 'WEBIRC ' + webirc_pass[this.irc_host.hostname] + ' KiwiIRC ';
381 tmp += this.user.hostname + ' ' + this.user.address;
382
383 connect_data.prepend_data = [tmp];
384 }
385
386
387 // Check if we need to pass the users IP as its username/ident
388 if (ip_as_username && ip_as_username.indexOf(this.irc_host.hostname) > -1) {
389 // Get a hex value of the clients IP
390 this.username = this.user.address.split('.').map(function(i, idx){
391 return parseInt(i, 10).toString(16);
392 }).join('');
393
394 }
395
396 return connect_data;
397}
398
399
400
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
408var parse = function (data) {
409 var i,
410 msg,
411 msg2,
412 trm,
413 j,
414 tags = [],
415 tag;
416
417 if (this.hold_last && this.held_data !== '') {
418 data = this.held_data + data;
419 this.hold_last = false;
420 this.held_data = '';
421 }
422
423 // If the last line is incomplete, hold it until we have more data
424 if (data.substr(-1) !== '\n') {
425 this.hold_last = true;
426 }
427
428 // Process our data line by line
429 data = data.split("\n");
430 for (i = 0; i < data.length; i++) {
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+$/, ''));
441
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]};
448 }
449 }
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+$/, ''));
468 }
469 }
470};