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