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