Topicbar usability improvements
[KiwiIRC.git] / server / app.js
CommitLineData
94423dfc
D
1/*jslint sloppy: true, continue: true, forin: true, regexp: true, undef: false, node: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
2/*globals kiwi_root */
3var tls = null,
4 net = null,
5 http = null,
6 node_static = null,
7 https = null,
8 fs = null,
9 url = null,
10 dns = null,
11 crypto = null,
12 events = null,
13 util = null,
14 ws = null,
15 jsp = null,
16 pro = null,
17 _ = null,
18 starttls = null,
19 kiwi = null;
20
21var file_server;
22
23this.init = function (objs) {
24 tls = objs.tls;
25 net = objs.net;
26 http = objs.http;
27 https = objs.https;
28 node_static = objs.node_static;
29 fs = objs.fs;
30 url = objs.url;
31 dns = objs.dns;
32 crypto = objs.crypto;
33 events = objs.events;
34 util = objs.util;
35 ws = objs.ws;
36 jsp = objs.jsp;
37 pro = objs.pro;
38 _ = objs._;
39 starttls = objs.starttls;
40 kiwi = require('./kiwi.js');
41
42 util.inherits(this.IRCConnection, events.EventEmitter);
43
44 file_server = new StaticFileServer();
45};
46
47
48
49
50
51
52/*
53 * Some process changes
54 */
55this.setTitle = function () {
56 process.title = 'kiwiirc';
57};
58
59this.changeUser = function () {
60 if (typeof kiwi.config.group !== 'undefined' && kiwi.config.group !== '') {
61 try {
62 process.setgid(kiwi.config.group);
63 } catch (err) {
64 kiwi.log('Failed to set gid: ' + err);
65 process.exit();
66 }
67 }
68
69 if (typeof kiwi.config.user !== 'undefined' && kiwi.config.user !== '') {
70 try {
71 process.setuid(kiwi.config.user);
72 } catch (e) {
73 kiwi.log('Failed to set uid: ' + e);
74 process.exit();
75 }
76 }
77};
78
79
80
81function StaticFileServer(public_html) {
82 public_html = public_html || 'client/';
83 this.file_server = new node_static.Server(public_html);
84}
85
86StaticFileServer.prototype.serve = function (request, response) {
6e1c802e
D
87 // The incoming requests root directory (ie. /kiwiclient/)
88 var root_path = kiwi.config.http_base_path || '/client',
89 root_path_regex = root_path.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
90
91 // Any asset request to head into the asset dir
92 request.url = request.url.replace(root_path + '/assets/', '/assets/');
93
94423dfc 94 // Any requests for /client to load the index file
6e1c802e 95 if (request.url.match(new RegExp('^' + root_path_regex, 'i'))) {
94423dfc
D
96 request.url = '/';
97 }
98
a8ce117f
D
99 // If a forwarded-for header is found, switch the source address
100 if (request.headers['x-forwarded-for']) {
101 // Check we're connecting from a whitelisted proxy
102 if (!kiwi.config.http_proxies
103 || kiwi.config.http_proxies.indexOf(request.connection.remoteAddress) < 0)
104 {
105 console.log('Unlisted proxy:', request.connection.remoteAddress);
106 response.writeHead(503);
107 response.end();
108 return;
109 }
110
111 // We're sent from a whitelisted proxy, replace the hosts
112 request.connection.remoteAddress = request.headers['x-forwarded-for'];
113 }
114
94423dfc
D
115 this.file_server.serve(request, response, function (err) {
116 if (err) {
117 response.writeHead(err.status, err.headers);
118 response.end();
119 }
120 });
121};
122
123
124
125
126
127
128
129
130var ircNumerics = {
131 RPL_WELCOME: '001',
132 RPL_MYINFO: '004',
133 RPL_ISUPPORT: '005',
134 RPL_WHOISUSER: '311',
135 RPL_WHOISSERVER: '312',
136 RPL_WHOISOPERATOR: '313',
137 RPL_WHOISIDLE: '317',
138 RPL_ENDOFWHOIS: '318',
139 RPL_WHOISCHANNELS: '319',
140 RPL_LISTSTART: '321',
141 RPL_LIST: '322',
142 RPL_LISTEND: '323',
143 RPL_NOTOPIC: '331',
144 RPL_TOPIC: '332',
145 RPL_TOPICWHOTIME: '333',
146 RPL_NAMEREPLY: '353',
147 RPL_ENDOFNAMES: '366',
148 RPL_BANLIST: '367',
149 RPL_ENDOFBANLIST: '368',
150 RPL_MOTD: '372',
151 RPL_MOTDSTART: '375',
152 RPL_ENDOFMOTD: '376',
153 RPL_WHOISMODES: '379',
154 ERR_NOSUCHNICK: '401',
155 ERR_CANNOTSENDTOCHAN: '404',
156 ERR_TOOMANYCHANNELS: '405',
157 ERR_NICKNAMEINUSE: '433',
158 ERR_USERNOTINCHANNEL: '441',
159 ERR_NOTONCHANNEL: '442',
160 ERR_NOTREGISTERED: '451',
161 ERR_LINKCHANNEL: '470',
162 ERR_CHANNELISFULL: '471',
163 ERR_INVITEONLYCHAN: '473',
164 ERR_BANNEDFROMCHAN: '474',
165 ERR_BADCHANNELKEY: '475',
166 ERR_CHANOPRIVSNEEDED: '482',
167 RPL_STARTTLS: '670'
168};
169
170this.bindIRCCommands = function (irc_connection, websocket) {
171 var bound_events = [],
172 bindCommand = function (command, listener) {
173 command = 'irc_' + command;
174 irc_connection.on(command, listener);
175 bound_events.push({"command": command, "listener": listener});
176 };
177
178 bindCommand('PING', function (msg) {
179 websocket.sendServerLine('PONG ' + msg.trailing);
180 });
181
182 bindCommand(ircNumerics.RPL_WELCOME, function (msg) {
183 if (irc_connection.IRC.CAP.negotiating) {
184 irc_connection.IRC.CAP.negotiating = false;
185 irc_connection.IRC.CAP.enabled = [];
186 irc_connection.IRC.CAP.requested = [];
187 irc_connection.IRC.registered = true;
188 }
189 var nick = msg.params.split(' ')[0];
190 websocket.sendClientEvent('connect', {connected: true, host: null, nick: nick});
191 });
192
193 bindCommand(ircNumerics.RPL_ISUPPORT, function (msg) {
194 var opts = msg.params.split(" "),
195 opt,
196 i,
197 j,
198 regex,
199 matches;
200 for (i = 0; i < opts.length; i++) {
201 opt = opts[i].split("=", 2);
202 opt[0] = opt[0].toUpperCase();
203 irc_connection.IRC.options[opt[0]] = (typeof opt[1] !== 'undefined') ? opt[1] : true;
204 if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES', 'NAMESX'], opt[0])) {
205 if (opt[0] === 'PREFIX') {
206 regex = /\(([^)]*)\)(.*)/;
207 matches = regex.exec(opt[1]);
208 if ((matches) && (matches.length === 3)) {
209 irc_connection.IRC.options[opt[0]] = [];
210 for (j = 0; j < matches[2].length; j++) {
211 irc_connection.IRC.options[opt[0]].push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
212 }
213
214 }
215 } else if (opt[0] === 'CHANTYPES') {
216 irc_connection.IRC.options.CHANTYPES = irc_connection.IRC.options.CHANTYPES.split('');
217 } else if (opt[0] === 'CHANMODES') {
218 irc_connection.IRC.options.CHANMODES = option[1].split(',');
219 } else if (opt[0] === 'NAMESX') {
220 websocket.sendServerLine('PROTOCTL NAMESX');
221 }
222 }
223 }
224
225 websocket.sendClientEvent('options', {server: '', "options": irc_connection.IRC.options});
226 });
227
228 bindCommand(ircNumerics.RPL_ENDOFWHOIS, function (msg) {
229 websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: true});
230 });
231
232 bindCommand(ircNumerics.RPL_WHOISUSER, function (msg) {
233 websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
234 });
235
236 bindCommand(ircNumerics.RPL_WHOISSERVER, function (msg) {
237 websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
238 });
239
240 bindCommand(ircNumerics.RPL_WHOISOPERATOR, function (msg) {
241 websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
242 });
243
244 bindCommand(ircNumerics.RPL_WHOISCHANNELS, function (msg) {
245 websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
246 });
247
248 bindCommand(ircNumerics.RPL_WHOISMODES, function (msg) {
249 websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing, end: false});
250 });
251
252 bindCommand(ircNumerics.RPL_LISTSTART, function (msg) {
253 websocket.sendClientEvent('list_start', {server: ''});
254 websocket.kiwi.buffer.list = [];
255 });
256
257 bindCommand(ircNumerics.RPL_LISTEND, function (msg) {
258 if (websocket.kiwi.buffer.list.length > 0) {
259 websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) {
260 return channel.num_users;
261 });
262 websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list});
263 websocket.kiwi.buffer.list = [];
264 }
265 websocket.sendClientEvent('list_end', {server: ''});
266 });
267
268 bindCommand(ircNumerics.RPL_LIST, function (msg) {
269 var parts, channel, num_users, topic;
270
271 parts = msg.params.split(' ');
272 channel = parts[1];
273 num_users = parts[2];
274 topic = msg.trailing;
275
276 //websocket.sendClientEvent('list_channel', {
277 websocket.kiwi.buffer.list.push({
278 server: '',
279 channel: channel,
280 topic: topic,
281 //modes: modes,
282 num_users: parseInt(num_users, 10)
283 });
284
285 if (websocket.kiwi.buffer.list.length > 200) {
286 websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) {
287 return channel.num_users;
288 });
289 websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list});
290 websocket.kiwi.buffer.list = [];
291 }
292 });
293
294 bindCommand(ircNumerics.RPL_WHOISIDLE, function (msg) {
295 var params = msg.params.split(" ", 4),
296 rtn = {server: '', nick: params[1], idle: params[2]};
297 if (params[3]) {
298 rtn.logon = params[3];
299 }
300 websocket.sendClientEvent('whois', rtn);
301 });
302
303 bindCommand(ircNumerics.RPL_MOTD, function (msg) {
304 websocket.kiwi.buffer.motd += msg.trailing + '\n';
305 });
306
307 bindCommand(ircNumerics.RPL_MOTDSTART, function (msg) {
308 websocket.kiwi.buffer.motd = '';
309 });
310
311 bindCommand(ircNumerics.RPL_ENDOFMOTD, function (msg) {
312 websocket.sendClientEvent('motd', {server: '', 'msg': websocket.kiwi.buffer.motd});
313 });
314
315 bindCommand(ircNumerics.RPL_NAMEREPLY, function (msg) {
316 var params = msg.params.split(" "),
317 chan = params[2],
318 users = msg.trailing.split(" "),
319 nicklist = [],
320 i = 0;
321
322 _.each(users, function (user) {
323 var j, k, modes = [];
324 for (j = 0; j < user.length; j++) {
325 for (k = 0; k < irc_connection.IRC.options.PREFIX.length; k++) {
326 if (user.charAt(j) === irc_connection.IRC.options.PREFIX[k].symbol) {
327 modes.push(irc_connection.IRC.options.PREFIX[k].mode);
328 }
329 }
330 }
331 nicklist.push({nick: user, modes: modes});
332 if (i++ >= 50) {
333 websocket.sendClientEvent('userlist', {server: '', 'users': nicklist, channel: chan});
334 nicklist = [];
335 i = 0;
336 }
337 });
338 if (i > 0) {
339 websocket.sendClientEvent('userlist', {server: '', "users": nicklist, channel: chan});
340 } else {
341 kiwi.log("oops");
342 }
343 });
344
345 bindCommand(ircNumerics.RPL_ENDOFNAMES, function (msg) {
346 websocket.sendClientEvent('userlist_end', {server: '', channel: msg.params.split(" ")[1]});
347 });
348
349 bindCommand(ircNumerics.ERR_LINKCHANNEL, function (msg) {
350 var params = msg.params.split(" ");
351 websocket.sendClientEvent('channel_redirect', {from: params[1], to: params[2]});
352 });
353
354 bindCommand(ircNumerics.ERR_NOSUCHNICK, function (msg) {
355 websocket.sendClientEvent('irc_error', {error: 'no_such_nick', nick: msg.params.split(" ")[1], reason: msg.trailing});
356 });
357
358 bindCommand(ircNumerics.RPL_BANLIST, function (msg) {
359 var params = msg.params.split(" ");
360 kiwi.log(params);
361 websocket.sendClientEvent('banlist', {server: '', channel: params[1], banned: params[2], banned_by: params[3], banned_at: params[4]});
362 });
363
364 bindCommand(ircNumerics.RPL_ENDOFBANLIST, function (msg) {
365 websocket.sendClientEvent('banlist_end', {server: '', channel: msg.params.split(" ")[1]});
366 });
367
368 bindCommand('JOIN', function (msg) {
369 var channel;
370
371 // Some BNC's send malformed JOIN causing the channel to be as a
372 // parameter instead of trailing.
373 if (typeof msg.trailing === 'string' && msg.trailing !== '') {
374 channel = msg.trailing;
375 } else if (typeof msg.params === 'string' && msg.params !== '') {
376 channel = msg.params;
377 }
378
379 websocket.sendClientEvent('join', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: channel});
380 if (msg.nick === irc_connection.IRC.nick) {
381 websocket.sendServerLine('NAMES ' + msg.trailing);
382 }
383 });
384
385 bindCommand('PART', function (msg) {
386 websocket.sendClientEvent('part', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
387 });
388
389 bindCommand('KICK', function (msg) {
390 var params = msg.params.split(" ");
391 websocket.sendClientEvent('kick', {kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
392 });
393
394 bindCommand('QUIT', function (msg) {
395 websocket.sendClientEvent('quit', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
396 });
397
398 bindCommand('NOTICE', function (msg) {
399 if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) {
400 // It's a CTCP response
401 websocket.sendClientEvent('ctcp_response', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)});
402 } else {
403 websocket.sendClientEvent('notice', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, target: msg.params.trim(), msg: msg.trailing});
404 }
405 });
406
407 bindCommand('NICK', function (msg) {
408 websocket.sendClientEvent('nick', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
409 });
410
411 bindCommand('TOPIC', function (msg) {
412 var obj = {nick: msg.nick, channel: msg.params, topic: msg.trailing};
413 websocket.sendClientEvent('topic', obj);
414 });
415
416 bindCommand(ircNumerics.RPL_TOPIC, function (msg) {
417 var obj = {nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing};
418 websocket.sendClientEvent('topic', obj);
419 });
420
421 bindCommand(ircNumerics.RPL_NOTOPIC, function (msg) {
422 var obj = {nick: '', channel: msg.params.split(" ")[1], topic: ''};
423 websocket.sendClientEvent('topic', obj);
424 });
425
426 bindCommand(ircNumerics.RPL_TOPICWHOTIME, function (msg) {
427 var parts = msg.params.split(' '),
428 nick = parts[2],
429 channel = parts[1],
430 when = parts[3],
431 obj = {nick: nick, channel: channel, when: when};
432 websocket.sendClientEvent('topicsetby', obj);
433 });
434
435 bindCommand('MODE', function (command) {
436 /*
437 var opts = msg.params.split(" "),
438 params = {nick: msg.nick};
439
440 switch (opts.length) {
441 case 1:
442 params.effected_nick = opts[0];
443 params.mode = msg.trailing;
444 break;
445 case 2:
446 params.channel = opts[0];
447 params.mode = opts[1];
448 break;
449 default:
450 params.channel = opts[0];
451 params.mode = opts[1];
452 params.effected_nick = opts[2];
453 break;
454 }
455 websocket.sendClientEvent('mode', params);
456 */
457 command.params = command.params.split(" ");
458 var chanmodes = irc_connection.IRC.options.CHANMODES,
459 prefixes = irc_connection.IRC.options.PREFIX,
460 always_param = chanmodes[0].concat(chanmodes[1]),
461 modes = [],
462 has_param, i, j, add;
463
464 prefixes = _.reduce(prefixes, function (list, prefix) {
465 list.push(prefix.mode);
466 return list;
467 }, []);
468 always_param = always_param.split('').concat(prefixes);
469
470 has_param = function (mode, add) {
471 if (_.find(always_param, function (m) {
472 return m === mode;
473 })) {
474 return true;
475 } else if (add && _.find(chanmodes[2].split(''), function (m) {
476 return m === mode;
477 })) {
478 return true;
479 } else {
480 return false;
481 }
482 };
483
484 if (!command.params[1]) {
485 command.params[1] = command.trailing;
486 }
487 j = 0;
488 for (i = 0; i < command.params[1].length; i++) {
489 switch (command.params[1][i]) {
490 case '+':
491 add = true;
492 break;
493 case '-':
494 add = false;
495 break;
496 default:
497 if (has_param(command.params[1][i], add)) {
498 modes.push({mode: (add ? '+' : '-') + command.params[1][i], param: command.params[2 + j]});
499 j++;
500 } else {
501 modes.push({mode: (add ? '+' : '-') + command.params[1][i], param: null});
502 }
503 }
504 }
505
506 websocket.sendClientEvent('mode', {
507 target: command.params[0],
508 nick: command.nick || command.prefix || '',
509 modes: modes
510 });
511 });
512
513 bindCommand('PRIVMSG', function (msg) {
514 var tmp, namespace, obj;
515 if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) {
516 // It's a CTCP request
517 if (msg.trailing.substr(1, 6) === 'ACTION') {
518 websocket.sendClientEvent('action', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(7, msg.trailing.length - 2)});
519 } else if (msg.trailing.substr(1, 4) === 'KIWI') {
520 tmp = msg.trailing.substr(6, msg.trailing.length - 2);
521 namespace = tmp.split(' ', 1)[0];
522 websocket.sendClientEvent('kiwi', {namespace: namespace, data: tmp.substr(namespace.length + 1)});
523
524 } else if (msg.trailing.substr(1, 7) === 'VERSION') {
525 irc_connection.write('NOTICE ' + msg.nick + ' :' + String.fromCharCode(1) + 'VERSION KiwiIRC' + String.fromCharCode(1) + '\r\n');
526 } else {
527 websocket.sendClientEvent('ctcp_request', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)});
528 }
529 } else {
530 obj = {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing};
531 websocket.sendClientEvent('msg', obj);
532 }
533 });
534
535 bindCommand('CAP', function (msg) {
536 var caps = kiwi.config.cap_options,
537 options = msg.trailing.split(" "),
538 opts;
539
540 switch (_.last(msg.params.split(" "))) {
541 case 'LS':
542 opts = '';
543 _.each(_.intersect(caps, options), function (cap) {
544 if (opts !== '') {
545 opts += " ";
546 }
547 opts += cap;
548 irc_connection.IRC.CAP.requested.push(cap);
549 });
550 if (opts.length > 0) {
551 websocket.sendServerLine('CAP REQ :' + opts);
552 } else {
553 websocket.sendServerLine('CAP END');
554 }
555 // TLS is special
556 /*if (_.include(options, 'tls')) {
557 websocket.sendServerLine('STARTTLS');
558 ircSocket.IRC.CAP.requested.push('tls');
559 }*/
560 break;
561 case 'ACK':
562 _.each(options, function (cap) {
563 irc_connection.IRC.CAP.enabled.push(cap);
564 });
565 if (_.last(msg.params.split(" ")) !== '*') {
566 irc_connection.IRC.CAP.requested = [];
567 irc_connection.IRC.CAP.negotiating = false;
568 websocket.sendServerLine('CAP END');
569 }
570 break;
571 case 'NAK':
572 irc_connection.IRC.CAP.requested = [];
573 irc_connection.IRC.CAP.negotiating = false;
574 websocket.sendServerLine('CAP END');
575 break;
576 }
577 });
578 /*case ircNumerics.RPL_STARTTLS:
579 try {
580 IRC = ircSocket.IRC;
581 listeners = ircSocket.listeners('data');
582 ircSocket.removeAllListeners('data');
583 ssl_socket = starttls(ircSocket, {}, function () {
584 ssl_socket.on("data", function (data) {
585 ircSocketDataHandler(data, websocket, ssl_socket);
586 });
587 ircSocket = ssl_socket;
588 ircSocket.IRC = IRC;
589 _.each(listeners, function (listener) {
590 ircSocket.addListener('data', listener);
591 });
592 });
593 //log(ircSocket);
594 } catch (e) {
595 kiwi.log(e);
596 }
597 break;*/
598 bindCommand(ircNumerics.ERR_CANNOTSENDTOCHAN, function (msg) {
599 websocket.sendClientEvent('irc_error', {error: 'cannot_send_to_chan', channel: msg.params.split(" ")[1], reason: msg.trailing});
600 });
601
602 bindCommand(ircNumerics.ERR_TOOMANYCHANNELS, function (msg) {
603 websocket.sendClientEvent('irc_error', {error: 'too_many_channels', channel: msg.params.split(" ")[1], reason: msg.trailing});
604 });
605
606 bindCommand(ircNumerics.ERR_USERNOTINCHANNEL, function (msg) {
607 var params = msg.params.split(" ");
608 websocket.sendClientEvent('irc_error', {error: 'user_not_in_channel', nick: params[0], channel: params[1], reason: msg.trainling});
609 });
610
611 bindCommand(ircNumerics.ERR_NOTONCHANNEL, function (msg) {
612 websocket.sendClientEvent('irc_error', {error: 'not_on_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
613 });
614
615 bindCommand(ircNumerics.ERR_CHANNELISFULL, function (msg) {
616 websocket.sendClientEvent('irc_error', {error: 'channel_is_full', channel: msg.params.split(" ")[1], reason: msg.trailing});
617 });
618
619 bindCommand(ircNumerics.ERR_INVITEONLYCHAN, function (msg) {
620 websocket.sendClientEvent('irc_error', {error: 'invite_only_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
621 });
622
623 bindCommand(ircNumerics.ERR_BANNEDFROMCHAN, function (msg) {
624 websocket.sendClientEvent('irc_error', {error: 'banned_from_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
625 });
626
627 bindCommand(ircNumerics.ERR_BADCHANNELKEY, function (msg) {
628 websocket.sendClientEvent('irc_error', {error: 'bad_channel_key', channel: msg.params.split(" ")[1], reason: msg.trailing});
629 });
630
631 bindCommand(ircNumerics.ERR_CHANOPRIVSNEEDED, function (msg) {
632 websocket.sendClientEvent('irc_error', {error: 'chanop_privs_needed', channel: msg.params.split(" ")[1], reason: msg.trailing});
633 });
634
635 bindCommand(ircNumerics.ERR_NICKNAMEINUSE, function (msg) {
636 websocket.sendClientEvent('irc_error', {error: 'nickname_in_use', nick: _.last(msg.params.split(" ")), reason: msg.trailing});
637 });
638
639 bindCommand('ERROR', function (msg) {
640 irc_connection.end();
641 websocket.sendClientEvent('irc_error', {error: 'error', reason: msg.trailing});
642 websocket.disconnect();
643 });
644
645 bindCommand(ircNumerics.ERR_NOTREGISTERED, function (msg) {
646 if (irc_connection.IRC.registered) {
647 kiwi.log('Kiwi thinks user is registered, but the IRC server thinks differently');
648 }
649 });
650
651 return bound_events;
652};
653
654this.rebindIRCCommands = function () {
655 _.each(kiwi.connections, function (con) {
656 _.each(con.sockets, function (sock) {
657 sock.ircConnection.rebindIRCCommands();
658 });
659 });
660};
661
662
663this.httpHandler = function (request, response) {
664 var uri, subs;
665
666 uri = url.parse(request.url, true);
667 subs = uri.pathname.substr(0, 4);
668
669 if (uri.pathname.substr(0, 10) === '/socket.io') {
670 return;
671 } else {
672 file_server.serve(request, response);
673 }
674};
675
676
677
678
679this.websocketListen = function (servers, handler) {
680 if (kiwi.httpServers.length > 0) {
681 _.each(kiwi.httpServers, function (hs) {
682 hs.close();
683 });
684 kiwi.httpsServers = [];
685 }
686
687 _.each(servers, function (server) {
688 var hs, opts;
689 if (server.secure === true) {
690 // Start some SSL server up
691 opts = {
692 key: fs.readFileSync(__dirname + '/' + server.ssl_key),
693 cert: fs.readFileSync(__dirname + '/' + server.ssl_cert)
694 };
695
696 // Do we have an intermediate certificate?
697 if (typeof server.ssl_ca !== 'undefined') {
698 opts.ca = fs.readFileSync(__dirname + '/' + server.ssl_ca);
699 }
700
701 hs = https.createServer(opts, handler);
702 kiwi.io.push(ws.listen(hs, {secure: true}));
703 hs.listen(server.port, server.address);
704 kiwi.log('Listening on ' + server.address + ':' + server.port.toString() + ' with SSL');
705 } else {
706 // Start some plain-text server up
707 hs = http.createServer(handler);
708 kiwi.io.push(ws.listen(hs, {secure: false}));
709 hs.listen(server.port, server.address);
710 kiwi.log('Listening on ' + server.address + ':' + server.port.toString() + ' without SSL');
711 }
712
713 kiwi.httpServers.push(hs);
714 });
715
716 _.each(kiwi.io, function (io) {
717 io.set('log level', 1);
718 io.enable('browser client minification');
719 io.enable('browser client etag');
720 io.set('transports', kiwi.config.transports);
721
722 io.of('/kiwi').authorization(function (handshakeData, callback) {
723 var address = handshakeData.address.address;
da6a8f89
D
724
725 // If a forwarded-for header is found, switch the source address
726 if (handshakeData.headers['x-forwarded-for']) {
727 // Check we're connecting from a whitelisted proxy
728 if (!kiwi.config.http_proxies
729 || kiwi.config.http_proxies.indexOf(address) < 0)
730 {
731 console.log('Unlisted proxy:', address);
732 websocket.disconnect();
733 return;
734 }
735
736 // We're sent from a whitelisted proxy, replace the hosts
737 address = handshakeData.headers['x-forwarded-for'];
738 }
739
740
94423dfc
D
741 if (typeof kiwi.connections[address] === 'undefined') {
742 kiwi.connections[address] = {count: 0, sockets: []};
743 }
744 callback(null, true);
745 }).on('connection', kiwi.websocketConnection);
746 io.of('/kiwi').on('error', console.log);
747 });
748};
749
750
751
752
753
754
755this.websocketConnection = function (websocket) {
da6a8f89
D
756 var con, address;
757
758 address = websocket.handshake.address.address;
759
760 // If a forwarded-for header is found, switch the source address
761 if (websocket.handshake.headers['x-forwarded-for']) {
762 // Check we're connecting from a whitelisted proxy
763 if (!kiwi.config.http_proxies
764 || kiwi.config.http_proxies.indexOf(address) < 0)
765 {
766 console.log('Unlisted proxy:', address);
767 websocket.disconnect();
768 return;
769 }
770
771 // We're sent from a whitelisted proxy, replace the hosts
772 address = websocket.handshake.headers['x-forwarded-for'];
773 }
774
775 kiwi.log('New connection! ' + address);
776
777 websocket.kiwi = {address: address, buffer: {list: []}};
94423dfc
D
778 con = kiwi.connections[websocket.kiwi.address];
779
780 if (con.count >= kiwi.config.max_client_conns) {
781 websocket.emit('too_many_connections');
782 websocket.disconnect();
783 } else {
784 con.count += 1;
785 con.sockets.push(websocket);
786
787 websocket.sendClientEvent = function (event_name, data) {
788 var ev = kiwi.kiwi_mod.run(event_name, data, {websocket: this});
789 if (ev === null) {
790 return;
791 }
792
793 //data.event = event_name;
794 websocket.emit('irc', {command:event_name, data:data});
795 };
796
797 websocket.sendServerLine = function (data, eol, callback) {
798 if ((arguments.length < 3) && (typeof eol === 'function')) {
799 callback = eol;
800 }
801 eol = (typeof eol !== 'string') ? '\r\n' : eol;
802
803 try {
804 websocket.ircConnection.write(data + eol, 'utf-8', callback);
805 } catch (e) { }
806 };
807
808 websocket.on('kiwi', kiwi.websocketKiwiMessage);
809 websocket.on('irc', kiwi.websocketMessage);
810 websocket.on('disconnect', kiwi.websocketDisconnect);
811 websocket.on('error', console.log);
812 }
813};
814
815
816this.IRCConnection = function (websocket, nick, host, port, ssl, password, callback) {
817 var ircSocket,
818 that = this,
819 regex,
820 onConnectHandler,
821 bound_events;
822
823 events.EventEmitter.call(this);
824
825 onConnectHandler = function () {
7c7590ee
D
826 // Inspired by:
827 // <dux0r> y4ry65yer56ytr
828 var realname = '[www.kiwiirc.com] ' + nick;
829 var username = nick.replace(/[^0-9a-zA-Z\-_.]/, '');
2feec7c4 830
94423dfc
D
831 that.IRC.nick = nick;
832 // Send the login data
833 dns.reverse(websocket.kiwi.address, function (err, domains) {
834 websocket.kiwi.hostname = (err) ? websocket.kiwi.address : _.first(domains);
2feec7c4
D
835
836 // Check if we need to pass the users IP as its realname
7c7590ee
D
837 if (kiwi.config.ip_as_username && kiwi.config.ip_as_username.indexOf(host) > -1) {
838 // Get a hex value of the clients IP
839 username = websocket.kiwi.address.split('.').map(function(i, idx){
840 return parseInt(i, 10).toString(16);
841 }).join('');
2feec7c4
D
842 }
843
844 // Do we have a WEBIRC password for this?
94423dfc
D
845 if ((kiwi.config.webirc) && (kiwi.config.webirc_pass[host])) {
846 websocket.sendServerLine('WEBIRC ' + kiwi.config.webirc_pass[host] + ' KiwiIRC ' + websocket.kiwi.hostname + ' ' + websocket.kiwi.address);
847 }
2feec7c4
D
848
849 // If the user specified a password
94423dfc
D
850 if (password) {
851 websocket.sendServerLine('PASS ' + password);
852 }
2feec7c4 853
94423dfc
D
854 websocket.sendServerLine('CAP LS');
855 websocket.sendServerLine('NICK ' + nick);
7c7590ee
D
856 websocket.sendServerLine('USER ' + username + ' 0 0 :' + realname);
857 console.log('Username:', username, 'Realname:', realname);
94423dfc
D
858
859 that.connected = true;
860 that.emit('connect');
861 });
862 };
863
864 if (!ssl) {
865 ircSocket = net.createConnection(port, host);
866 ircSocket.on('connect', onConnectHandler);
867 } else {
868 ircSocket = tls.connect(port, host, {}, onConnectHandler);
869 }
870
871 ircSocket.setEncoding('utf-8');
872 this.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}, registered: false};
873
874 this.on('error', function (e) {
875 if (that.IRC.registered) {
876 websocket.emit('disconnect');
877 } else {
878 websocket.emit('error', e.message);
879 }
880 });
881
882 ircSocket.on('error', function (e) {
883 that.connected = false;
884 that.emit('error', e);
885 that.destroySoon();
886 });
887
888 if (typeof callback === 'function') {
889 this.on('connect', callback);
890 }
891
892 regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
893 ircSocket.holdLast = false;
894 ircSocket.held = '';
895 ircSocket.on('data', function (data) {
896 var i, msg;
897 if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
898 data = ircSocket.held + data;
899 ircSocket.holdLast = false;
900 ircSocket.held = '';
901 }
902 if (data.substr(-1) !== '\n') {
903 ircSocket.holdLast = true;
904 }
905 data = data.split("\n");
906 for (i = 0; i < data.length; i++) {
907 if (data[i]) {
908 if ((ircSocket.holdLast) && (i === data.length - 1)) {
909 ircSocket.held = data[i];
910 break;
911 }
912
913 // We have a complete line of data, parse it!
914 msg = regex.exec(data[i].replace(/^\r+|\r+$/, ''));
915 if (msg) {
916 msg = {
917 prefix: msg[1],
918 nick: msg[2],
919 ident: msg[3],
920 hostname: msg[4] || '',
921 command: msg[5],
922 params: msg[6] || '',
923 trailing: (msg[7]) ? msg[7].trim() : ''
924 };
925 that.emit('irc_' + msg.command.toUpperCase(), msg);
926 if (that.listeners('irc_' + msg.command.toUpperCase()).length < 1) {
927 kiwi.log("Unknown command (" + String(msg.command).toUpperCase() + ")");
928 }
929 } else {
930 kiwi.log("Malformed IRC line: " + data[i].replace(/^\r+|\r+$/, ''));
931 }
932 }
933 }
934 });
935
936 if (callback) {
937 ircSocket.on('connect', callback);
938 }
939
940 ircSocket.on('end', function () {
941 that.connected = false;
942 that.emit('disconnect', false);
943 });
944
945 ircSocket.on('close', function (had_error) {
946 that.connected = false;
947 that.emit('disconnect', had_error);
948 });
949
950 ircSocket.on('timeout', function () {
951 ircSocket.destroy();
952 that.connected = false;
953 that.emit('error', {message: 'Connection timed out'});
954 });
955
956 ircSocket.on('drain', function () {
957 that.emit('drain');
958 });
959
960 this.write = function (data, encoding, callback) {
961 ircSocket.write(data, encoding, callback);
962 };
963
964 this.end = function (data, encoding, callback) {
965 that.connected = false;
966 ircSocket.end(data, encoding, callback);
967 };
968
969 this.destroySoon = function () {
970 ircSocket.destroySoon();
971 };
972
973 bound_events = kiwi.bindIRCCommands(this, websocket);
974
975 this.rebindIRCCommands = function () {
976 _.each(bound_events, function (event) {
977 that.removeListener(event.command, event.listener);
978 });
979 bound_events = kiwi.bindIRCCommands(that, websocket);
980 };
981
982 that.on('error', console.log);
983
984};
985
986
987
988this.websocketKiwiMessage = function (websocket, msg, callback) {
989 websocket.ircConnection = new kiwi.IRCConnection(websocket, msg.nick, msg.hostname, msg.port, msg.ssl, msg.password, callback);
990};
991
992
993this.websocketMessage = function (websocket, msg, callback) {
994 var args, obj, channels, keys;
995 //try {
996 if ((callback) && (typeof (callback) !== 'function')) {
997 callback = null;
998 }
999 try {
1000 msg.data = JSON.parse(msg.data);
1001 } catch (e) {
1002 kiwi.log('[app.websocketMessage] JSON parsing error ' + msg.data);
1003 return;
1004 }
1005 args = msg.data.args;
1006 switch (msg.data.method) {
1007 case 'privmsg':
1008 if ((args.target) && (args.msg)) {
1009 obj = kiwi.kiwi_mod.run('msgsend', args, {websocket: websocket});
1010 if (obj !== null) {
1011 websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + args.msg, callback);
1012 }
1013 }
1014 break;
1015 case 'ctcp':
1016 if ((args.target) && (args.type)) {
1017 if (args.request) {
1018 websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + String.fromCharCode(1) + args.type.toUpperCase() + ' ' + args.params + String.fromCharCode(1), callback);
1019 } else {
1020 websocket.sendServerLine('NOTICE ' + args.target + ' :' + String.fromCharCode(1) + args.type.toUpperCase() + ' ' + args.params + String.fromCharCode(1), callback);
1021 }
1022 }
1023 break;
1024
1025 case 'raw':
1026 websocket.sendServerLine(args.data, callback);
1027 break;
1028
1029 case 'join':
1030 if (args.channel) {
1031 channels = args.channel.split(",");
1032 keys = (args.key) ? args.key.split(",") : [];
1033 _.each(channels, function (chan, index) {
1034 websocket.sendServerLine('JOIN ' + chan + ' ' + (keys[index] || ''), callback);
1035 });
1036 }
1037 break;
1038
1039 case 'part':
1040 if (args.channel) {
1041 _.each(args.channel.split(","), function (chan) {
1042 websocket.sendServerLine('PART ' + chan, callback);
1043 });
1044 }
1045 break;
1046
1047 case 'topic':
1048 if (args.channel) {
1049 if (args.topic) {
1050 websocket.sendServerLine('TOPIC ' + args.channel + ' :' + args.topic, callback);
1051 } else {
1052 websocket.sendServerLine('TOPIC ' + args.channel, callback);
1053 }
1054 }
1055 break;
1056
1057 case 'kick':
1058 if ((args.channel) && (args.nick)) {
1059 websocket.sendServerLine('KICK ' + args.channel + ' ' + args.nick + ':' + args.reason, callback);
1060 }
1061 break;
1062
1063 case 'quit':
1064 websocket.ircConnection.end('QUIT :' + args.message + '\r\n');
1065 websocket.sentQUIT = true;
1066 websocket.ircConnection.destroySoon();
1067 websocket.disconnect();
1068 break;
1069
1070 case 'notice':
1071 if ((args.target) && (args.msg)) {
1072 websocket.sendServerLine('NOTICE ' + args.target + ' :' + args.msg, callback);
1073 }
1074 break;
1075
1076 case 'mode':
1077 if ((args.target) && (args.mode)) {
1078 websocket.sendServerLine('MODE ' + args.target + ' ' + args.mode + ' ' + args.params, callback);
1079 }
1080 break;
1081
1082 case 'nick':
1083 if (args.nick) {
1084 websocket.sendServerLine('NICK ' + args.nick, callback);
1085 }
1086 break;
1087
1088 case 'kiwi':
1089 if ((args.target) && (args.data)) {
1090 websocket.sendServerLine('PRIVMSG ' + args.target + ': ' + String.fromCharCode(1) + 'KIWI ' + args.data + String.fromCharCode(1), callback);
1091 }
1092 break;
1093 default:
1094 }
1095 //} catch (e) {
1096 // kiwi.log("Caught error (app.websocketMessage): " + e);
1097 //}
1098};
1099
1100
1101
1102this.websocketDisconnect = function (websocket) {
1103 var con;
1104
1105 if ((!websocket.sentQUIT) && (websocket.ircConnection.connected)) {
1106 try {
1107 websocket.ircConnection.end('QUIT :' + kiwi.config.quit_message + '\r\n');
1108 websocket.sentQUIT = true;
1109 websocket.ircConnection.destroySoon();
1110 } catch (e) {
1111 }
1112 }
1113 con = kiwi.connections[websocket.kiwi.address];
1114 con.count -= 1;
1115 con.sockets = _.reject(con.sockets, function (sock) {
1116 return sock === websocket;
1117 });
1118};
1119
1120
1121
1122
1123
1124
1125this.rehash = function () {
1126 var changes, i,
1127 reload_config = kiwi.loadConfig();
1128
1129 // If loading the new config errored out, dont attempt any changes
1130 if (reload_config === false) {
1131 return false;
1132 }
1133
1134 // We just want the settings that have been changed
1135 changes = reload_config[1];
1136
1137 if (Object.keys(changes).length !== 0) {
1138 kiwi.log('%s config changes: \n', Object.keys(changes).length, changes);
1139 for (i in changes) {
1140 switch (i) {
1141 case 'servers':
1142 kiwi.websocketListen(kiwi.config.servers, kiwi.httpHandler);
1143 delete changes.ports;
1144 delete changes.bind_address;
1145 delete changes.ssl_key;
1146 delete changes.ssl_cert;
1147 break;
1148 case 'user':
1149 case 'group':
1150 kiwi.changeUser();
1151 delete changes.user;
1152 delete changes.group;
1153 break;
1154 case 'module_dir':
1155 case 'modules':
1156 kiwi.kiwi_mod.loadModules(kiwi_root, kiwi.config);
1157 kiwi.kiwi_mod.printMods();
1158 delete changes.module_dir;
1159 delete changes.modules;
1160 break;
1161 }
1162 }
1163 }
1164
1165 // Also clear the kiwi.cached javascript and HTML
1166 if (kiwi.config.handle_http) {
1167 kiwi.cache = {alljs: '', html: []};
1168 }
1169
1170 return true;
1171};
1172
1173
1174
1175
1176
1177/*
1178 * KiwiIRC controlling via STDIN
1179 */
1180this.manageControll = function (data) {
1181 var parts = data.toString().trim().split(' '),
1182 connections_cnt = 0,
1183 i;
1184 switch (parts[0]) {
1185 case 'rehash':
1186 kiwi.log('Rehashing...');
1187 kiwi.log(kiwi.rehash() ? 'Rehash complete' : 'Rehash failed');
1188 break;
1189
1190 case 'recode':
1191 kiwi.log('Recoding...');
1192 kiwi.log(kiwi.recode() ? 'Recode complete' : 'Recode failed');
1193 break;
1194
1195 case 'mod':
1196 if (parts[1] === 'reload') {
1197 if (!parts[2]) {
1198 kiwi.log('Usage: mod reload module_name');
1199 return;
1200 }
1201
1202 kiwi.log('Reloading module (' + parts[2] + ')..');
1203 kiwi.kiwi_mod.reloadModule(parts[2]);
1204 } else if (parts[1] === 'list') {
1205 kiwi.kiwi_mod.printMods();
1206 }
1207 break;
1208
1209 case 'cache':
1210 if (parts[1] === 'clear') {
1211 kiwi.cache.html = {};
1212 kiwi.cache.alljs = '';
1213 kiwi.log('HTML cache cleared');
1214 }
1215 break;
1216
1217 case 'status':
1218 for (i in kiwi.connections) {
1219 connections_cnt = connections_cnt + parseInt(kiwi.connections[i].count, 10);
1220 }
1221 kiwi.log(connections_cnt.toString() + ' connected clients');
1222 break;
1223
1224 default:
1225 kiwi.log('Unknown command \'' + parts[0] + '\'');
1226 }
1227};