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