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