Merge branch 'master' of github.com:prawnsalad/KiwiIRC
[KiwiIRC.git] / node / app.js
CommitLineData
80c584e7 1/*jslint sloppy: true, continue: true, forin: true, regexp: true, undef: false, node: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
2fc64ec2 2/*globals kiwi_root */
fd779420
D
3var tls = null;
4var net = null;
5var http = null;
6var https = null;
7var fs = null;
8var url = null;
9var dns = null;
10var crypto = null;
11var ws = null;
12var jsp = null;
13var pro = null;
14var _ = null;
15var starttls = null;
16var kiwi = null;
17
18this.init = function (objs) {
19 tls = objs.tls;
20 net = objs.net;
21 http = objs.http;
22 https = objs.https;
23 fs = objs.fs;
24 url = objs.url;
25 dns = objs.dns;
26 crypto = objs.crypto;
27 ws = objs.ws;
28 jsp = objs.jsp;
29 pro = objs.pro;
30 _ = objs._;
31 starttls = objs.starttls;
32 kiwi = require('./kiwi.js');
2fc64ec2 33};
fd779420
D
34
35
36
37
38
39
40/*
41 * Some process changes
42 */
43this.setTitle = function () {
44 process.title = 'kiwiirc';
2fc64ec2 45};
fd779420
D
46
47this.changeUser = function () {
48 if (typeof kiwi.config.group !== 'undefined' && kiwi.config.group !== '') {
49 try {
50 process.setgid(kiwi.config.group);
51 } catch (err) {
52 console.log('Failed to set gid: ' + err);
53 process.exit();
54 }
55 }
56
57 if (typeof kiwi.config.user !== 'undefined' && kiwi.config.user !== '') {
58 try {
59 process.setuid(kiwi.config.user);
60 } catch (e) {
61 console.log('Failed to set uid: ' + e);
62 process.exit();
63 }
64 }
65};
66
67
68
69
70
71
72
73
74
75var ircNumerics = {
76 RPL_WELCOME: '001',
337c9866 77 RPL_MYINFO: '004',
fd779420
D
78 RPL_ISUPPORT: '005',
79 RPL_WHOISUSER: '311',
80 RPL_WHOISSERVER: '312',
81 RPL_WHOISOPERATOR: '313',
82 RPL_WHOISIDLE: '317',
83 RPL_ENDOFWHOIS: '318',
84 RPL_WHOISCHANNELS: '319',
b10afa2d
D
85 RPL_LISTSTART: '321',
86 RPL_LIST: '322',
87 RPL_LISTEND: '323',
fd779420
D
88 RPL_NOTOPIC: '331',
89 RPL_TOPIC: '332',
90 RPL_NAMEREPLY: '353',
91 RPL_ENDOFNAMES: '366',
92 RPL_MOTD: '372',
93 RPL_WHOISMODES: '379',
94 ERR_NOSUCHNICK: '401',
95 ERR_CANNOTSENDTOCHAN: '404',
96 ERR_TOOMANYCHANNELS: '405',
97 ERR_NICKNAMEINUSE: '433',
98 ERR_USERNOTINCHANNEL: '441',
99 ERR_NOTONCHANNEL: '442',
100 ERR_NOTREGISTERED: '451',
101 ERR_LINKCHANNEL: '470',
102 ERR_CHANNELISFULL: '471',
103 ERR_INVITEONLYCHAN: '473',
104 ERR_BANNEDFROMCHAN: '474',
105 ERR_BADCHANNELKEY: '475',
106 ERR_CHANOPRIVSNEEDED: '482',
107 RPL_STARTTLS: '670'
108};
109
110
111
112this.parseIRCMessage = function (websocket, ircSocket, data) {
113 /*global ircSocketDataHandler */
2fc64ec2 114 var msg, regex, opts, options, opt, i, j, matches, nick, users, chan, channel, params, prefix, prefixes, nicklist, caps, rtn, obj, tmp, namespace;
337c9866
D
115 //regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?([a-z0-9]+)(?:(?: ([^:]+))?(?: :(.+))?)$/i;
116 //regex = /^(?::(\S+) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
117 regex = /^(?::(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
118
fd779420
D
119 msg = regex.exec(data);
120 if (msg) {
121 msg = {
122 prefix: msg[1],
123 nick: msg[2],
124 ident: msg[3],
125 hostname: msg[4] || '',
126 command: msg[5],
127 params: msg[6] || '',
128 trailing: (msg[7]) ? msg[7].trim() : ''
129 };
337c9866 130
fd779420
D
131 switch (msg.command.toUpperCase()) {
132 case 'PING':
133 websocket.sendServerLine('PONG ' + msg.trailing);
134 break;
135 case ircNumerics.RPL_WELCOME:
136 if (ircSocket.IRC.CAP.negotiating) {
137 ircSocket.IRC.CAP.negotiating = false;
138 ircSocket.IRC.CAP.enabled = [];
139 ircSocket.IRC.CAP.requested = [];
140 ircSocket.IRC.registered = true;
141 }
b73cccbd
D
142 //regex = /([a-z0-9\x5B-\x60\x7B-\x7D\.\-]+)!([a-z0-9~\.\-_|]+)@?([a-z0-9\.\-:\/]+)/i;
143 //matches = regex.exec(msg.trailing);
144 nick = msg.params.split(' ')[0];
145 websocket.sendClientEvent('connect', {connected: true, host: null, nick: nick});
fd779420
D
146 break;
147 case ircNumerics.RPL_ISUPPORT:
148 opts = msg.params.split(" ");
149 options = [];
150 for (i = 0; i < opts.length; i++) {
151 opt = opts[i].split("=", 2);
152 opt[0] = opt[0].toUpperCase();
337c9866 153 ircSocket.IRC.options[opt[0]] = (typeof opt[1] !== 'undefined') ? opt[1] : true;
fd779420
D
154 if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES'], opt[0])) {
155 if (opt[0] === 'PREFIX') {
156 regex = /\(([^)]*)\)(.*)/;
157 matches = regex.exec(opt[1]);
158 if ((matches) && (matches.length === 3)) {
159 ircSocket.IRC.options[opt[0]] = [];
160 for (j = 0; j < matches[2].length; j++) {
161 //ircSocket.IRC.options[opt[0]][matches[2].charAt(j)] = matches[1].charAt(j);
162 ircSocket.IRC.options[opt[0]].push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
fd779420 163 }
337c9866 164
fd779420
D
165 }
166 }
167 }
168 }
337c9866 169
fd779420
D
170 websocket.sendClientEvent('options', {server: '', "options": ircSocket.IRC.options});
171 break;
172 case ircNumerics.RPL_WHOISUSER:
173 case ircNumerics.RPL_WHOISSERVER:
174 case ircNumerics.RPL_WHOISOPERATOR:
175 case ircNumerics.RPL_ENDOFWHOIS:
176 case ircNumerics.RPL_WHOISCHANNELS:
177 case ircNumerics.RPL_WHOISMODES:
178 websocket.sendClientEvent('whois', {server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing});
179 break;
b10afa2d
D
180
181 case ircNumerics.RPL_LISTSTART:
182 (function () {
183 websocket.sendClientEvent('list_start', {server: ''});
5c7ac96f 184 websocket.kiwi.buffer.list = [];
b10afa2d
D
185 }());
186 break;
187 case ircNumerics.RPL_LISTEND:
188 (function () {
5c7ac96f
JA
189 if (websocket.kiwi.buffer.list.length > 0) {
190 websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) {
191 return channel.num_users;
192 });
193 websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list});
194 websocket.kiwi.buffer.list = [];
195 }
b10afa2d
D
196 websocket.sendClientEvent('list_end', {server: ''});
197 }());
198 break;
199
200 case ircNumerics.RPL_LIST:
201 (function () {
202 var parts, channel, num_users, modes, topic;
203
204 parts = msg.params.split(' ');
205 channel = parts[1];
206 num_users = parts[2];
207 modes = msg.trailing.split(' ', 1);
80c584e7 208 topic = msg.trailing.substring(msg.trailing.indexOf(' ') + 1);
b10afa2d 209
5c7ac96f
JA
210 //websocket.sendClientEvent('list_channel', {
211 websocket.kiwi.buffer.list.push({
b10afa2d
D
212 server: '',
213 channel: channel,
214 topic: topic,
215 modes: modes,
5c7ac96f 216 num_users: parseInt(num_users, 10)
b10afa2d 217 });
5c7ac96f
JA
218
219 if (websocket.kiwi.buffer.list.length > 200) {
220 websocket.kiwi.buffer.list = _.sortBy(websocket.kiwi.buffer.list, function (channel) {
221 return channel.num_users;
222 });
223 websocket.sendClientEvent('list_channel', {chans: websocket.kiwi.buffer.list});
224 websocket.kiwi.buffer.list = [];
225 }
226
b10afa2d
D
227 }());
228 break;
229
fd779420
D
230 case ircNumerics.RPL_WHOISIDLE:
231 params = msg.params.split(" ", 4);
232 rtn = {server: '', nick: params[1], idle: params[2]};
233 if (params[3]) {
234 rtn.logon = params[3];
235 }
236 websocket.sendClientEvent('whois', rtn);
237 break;
238 case ircNumerics.RPL_MOTD:
239 websocket.sendClientEvent('motd', {server: '', "msg": msg.trailing});
240 break;
241 case ircNumerics.RPL_NAMEREPLY:
242 params = msg.params.split(" ");
243 nick = params[0];
244 chan = params[2];
245 users = msg.trailing.split(" ");
246 prefixes = _.values(ircSocket.IRC.options.PREFIX);
247 nicklist = {};
248 i = 0;
249 _.each(users, function (user) {
250 if (_.include(prefix, user.charAt(0))) {
251 prefix = user.charAt(0);
252 user = user.substring(1);
253 nicklist[user] = prefix;
254 } else {
255 nicklist[user] = '';
256 }
257 if (i++ >= 50) {
258 websocket.sendClientEvent('userlist', {server: '', 'users': nicklist, channel: chan});
259 nicklist = {};
260 i = 0;
261 }
262 });
263 if (i > 0) {
264 websocket.sendClientEvent('userlist', {server: '', "users": nicklist, channel: chan});
265 } else {
266 console.log("oops");
267 }
268 break;
269 case ircNumerics.RPL_ENDOFNAMES:
270 websocket.sendClientEvent('userlist_end', {server: '', channel: msg.params.split(" ")[1]});
271 break;
272 case ircNumerics.ERR_LINKCHANNEL:
273 params = msg.params.split(" ");
274 websocket.sendClientEvent('channel_redirect', {from: params[1], to: params[2]});
275 break;
276 case ircNumerics.ERR_NOSUCHNICK:
277 websocket.sendClientEvent('irc_error', {error: 'no_such_nick', nick: msg.params.split(" ")[1], reason: msg.trailing});
278 break;
279 case 'JOIN':
280 // Some BNC's send malformed JOIN causing the channel to be as a
281 // parameter instead of trailing.
282 if (typeof msg.trailing === 'string' && msg.trailing !== '') {
283 channel = msg.trailing;
284 } else if (typeof msg.params === 'string' && msg.params !== '') {
285 channel = msg.params;
286 }
287
288 websocket.sendClientEvent('join', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: channel});
289 if (msg.nick === ircSocket.IRC.nick) {
290 websocket.sendServerLine('NAMES ' + msg.trailing);
291 }
292 break;
293 case 'PART':
294 websocket.sendClientEvent('part', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
295 break;
296 case 'KICK':
297 params = msg.params.split(" ");
298 websocket.sendClientEvent('kick', {kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
299 break;
300 case 'QUIT':
301 websocket.sendClientEvent('quit', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
302 break;
303 case 'NOTICE':
304 if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) {
305 // It's a CTCP response
306 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)});
307 } else {
a5ffcf00 308 websocket.sendClientEvent('notice', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, target: msg.params.trim(), msg: msg.trailing});
fd779420
D
309 }
310 break;
311 case 'NICK':
312 websocket.sendClientEvent('nick', {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
313 break;
314 case 'TOPIC':
315 obj = {nick: msg.nick, channel: msg.params, topic: msg.trailing};
316 websocket.sendClientEvent('topic', obj);
317 break;
318 case ircNumerics.RPL_TOPIC:
319 obj = {nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing};
320 websocket.sendClientEvent('topic', obj);
321 break;
322 case ircNumerics.RPL_NOTOPIC:
323 obj = {nick: '', channel: msg.params.split(" ")[1], topic: ''};
324 websocket.sendClientEvent('topic', obj);
325 break;
326 case 'MODE':
327 opts = msg.params.split(" ");
328 params = {nick: msg.nick};
329 switch (opts.length) {
330 case 1:
331 params.effected_nick = opts[0];
332 params.mode = msg.trailing;
333 break;
334 case 2:
335 params.channel = opts[0];
336 params.mode = opts[1];
337 break;
338 default:
339 params.channel = opts[0];
340 params.mode = opts[1];
341 params.effected_nick = opts[2];
342 break;
343 }
344 websocket.sendClientEvent('mode', params);
345 break;
346 case 'PRIVMSG':
347 if ((msg.trailing.charAt(0) === String.fromCharCode(1)) && (msg.trailing.charAt(msg.trailing.length - 1) === String.fromCharCode(1))) {
348 // It's a CTCP request
349 if (msg.trailing.substr(1, 6) === 'ACTION') {
350 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)});
abb46d2d 351 } else if (msg.trailing.substr(1, 4) === 'KIWI') {
2fc64ec2
JA
352 tmp = msg.trailing.substr(6, msg.trailing.length - 2);
353 namespace = tmp.split(' ', 1)[0];
354 websocket.sendClientEvent('kiwi', {namespace: namespace, data: tmp.substr(namespace.length + 1)});
abb46d2d 355
fd779420
D
356 } else if (msg.trailing.substr(1, 7) === 'VERSION') {
357 ircSocket.write('NOTICE ' + msg.nick + ' :' + String.fromCharCode(1) + 'VERSION KiwiIRC' + String.fromCharCode(1) + '\r\n');
358 } else {
359 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)});
360 }
361 } else {
362 obj = {nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing};
363 websocket.sendClientEvent('msg', obj);
364 }
365 break;
366 case 'CAP':
367 caps = kiwi.config.cap_options;
368 options = msg.trailing.split(" ");
369 switch (_.last(msg.params.split(" "))) {
370 case 'LS':
371 opts = '';
372 _.each(_.intersect(caps, options), function (cap) {
373 if (opts !== '') {
374 opts += " ";
375 }
376 opts += cap;
377 ircSocket.IRC.CAP.requested.push(cap);
378 });
379 if (opts.length > 0) {
380 websocket.sendServerLine('CAP REQ :' + opts);
381 } else {
382 websocket.sendServerLine('CAP END');
383 }
384 // TLS is special
385 /*if (_.include(options, 'tls')) {
386 websocket.sendServerLine('STARTTLS');
387 ircSocket.IRC.CAP.requested.push('tls');
388 }*/
389 break;
390 case 'ACK':
391 _.each(options, function (cap) {
392 ircSocket.IRC.CAP.enabled.push(cap);
393 });
394 if (_.last(msg.params.split(" ")) !== '*') {
395 ircSocket.IRC.CAP.requested = [];
396 ircSocket.IRC.CAP.negotiating = false;
397 websocket.sendServerLine('CAP END');
398 }
399 break;
400 case 'NAK':
401 ircSocket.IRC.CAP.requested = [];
402 ircSocket.IRC.CAP.negotiating = false;
403 websocket.sendServerLine('CAP END');
404 break;
405 }
406 break;
407 /*case ircNumerics.RPL_STARTTLS:
408 try {
409 IRC = ircSocket.IRC;
410 listeners = ircSocket.listeners('data');
411 ircSocket.removeAllListeners('data');
412 ssl_socket = starttls(ircSocket, {}, function () {
413 ssl_socket.on("data", function (data) {
414 ircSocketDataHandler(data, websocket, ssl_socket);
415 });
416 ircSocket = ssl_socket;
417 ircSocket.IRC = IRC;
418 _.each(listeners, function (listener) {
419 ircSocket.addListener('data', listener);
420 });
421 });
422 //console.log(ircSocket);
423 } catch (e) {
424 console.log(e);
425 }
426 break;*/
427 case ircNumerics.ERR_CANNOTSENDTOCHAN:
428 websocket.sendClientEvent('irc_error', {error: 'cannot_send_to_chan', channel: msg.params.split(" ")[1], reason: msg.trailing});
429 break;
430 case ircNumerics.ERR_TOOMANYCHANNELS:
431 websocket.sendClientEvent('irc_error', {error: 'too_many_channels', channel: msg.params.split(" ")[1], reason: msg.trailing});
432 break;
433 case ircNumerics.ERR_USERNOTINCHANNEL:
434 params = msg.params.split(" ");
435 websocket.sendClientEvent('irc_error', {error: 'user_not_in_channel', nick: params[0], channel: params[1], reason: msg.trainling});
436 break;
437 case ircNumerics.ERR_NOTONCHANNEL:
438 websocket.sendClientEvent('irc_error', {error: 'not_on_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
439 break;
440 case ircNumerics.ERR_CHANNELISFULL:
441 websocket.sendClientEvent('irc_error', {error: 'channel_is_full', channel: msg.params.split(" ")[1], reason: msg.trailing});
442 break;
443 case ircNumerics.ERR_INVITEONLYCHAN:
444 websocket.sendClientEvent('irc_error', {error: 'invite_only_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
445 break;
446 case ircNumerics.ERR_BANNEDFROMCHAN:
447 websocket.sendClientEvent('irc_error', {error: 'banned_from_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
448 break;
449 case ircNumerics.ERR_BADCHANNELKEY:
450 websocket.sendClientEvent('irc_error', {error: 'bad_channel_key', channel: msg.params.split(" ")[1], reason: msg.trailing});
451 break;
452 case ircNumerics.ERR_CHANOPRIVSNEEDED:
453 websocket.sendClientEvent('irc_error', {error: 'chanop_privs_needed', channel: msg.params.split(" ")[1], reason: msg.trailing});
454 break;
455 case ircNumerics.ERR_NICKNAMEINUSE:
456 websocket.sendClientEvent('irc_error', {error: 'nickname_in_use', nick: _.last(msg.params.split(" ")), reason: msg.trailing});
457 break;
458 case 'ERROR':
459 ircSocket.end();
460 websocket.sendClientEvent('irc_error', {error: 'error', reason: msg.trailing});
461 websocket.disconnect();
462 break;
463 case ircNumerics.ERR_NOTREGISTERED:
464 if (ircSocket.IRC.registered) {
465 console.log('Kiwi thinks user is registered, but the IRC server thinks differently');
466 }
467 break;
468 default:
469 console.log("Unknown command (" + String(msg.command).toUpperCase() + ")");
470 }
471 } else {
d6e9bff7 472 console.log("Malformed IRC line: " + data);
fd779420
D
473 }
474};
475
476
477
478
479
480
481/*
482 * NOTE: Some IRC servers or BNC's out there incorrectly use
483 * only \n as a line splitter.
484 */
485this.ircSocketDataHandler = function (data, websocket, ircSocket) {
486 var i;
487 if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
488 data = ircSocket.held + data;
489 ircSocket.holdLast = false;
490 ircSocket.held = '';
491 }
492 if (data.substr(-1) !== '\n') {
493 ircSocket.holdLast = true;
494 }
495 data = data.split("\n");
496 for (i = 0; i < data.length; i++) {
497 if (data[i]) {
498 if ((ircSocket.holdLast) && (i === data.length - 1)) {
499 ircSocket.held = data[i];
500 break;
501 }
502
503 // We have a complete line of data, parse it!
504 kiwi.parseIRCMessage(websocket, ircSocket, data[i].replace(/^\r+|\r+$/, ''));
505 }
506 }
507};
508
509
510
511
512
513this.httpHandler = function (request, response) {
8397d902 514 var uri, uri_parts, subs, useragent, agent, server_set, server, nick, debug, touchscreen, hash,
fd779420
D
515 min = {}, public_http_path;
516 if (kiwi.config.handle_http) {
517 uri = url.parse(request.url, true);
8397d902
D
518 uri_parts = uri.pathname.split('/');
519
fd779420
D
520 subs = uri.pathname.substr(0, 4);
521 if (uri.pathname === '/js/all.js') {
522 if (kiwi.cache.alljs === '') {
523 public_http_path = kiwi.kiwi_root + '/' + kiwi.config.public_http;
524
fde6ea95 525 min.underscore = fs.readFileSync(public_http_path + 'js/underscore.min.js');
fd779420
D
526 min.util = fs.readFileSync(public_http_path + 'js/util.js');
527 min.gateway = fs.readFileSync(public_http_path + 'js/gateway.js');
528 min.front = fs.readFileSync(public_http_path + 'js/front.js');
529 min.iscroll = fs.readFileSync(public_http_path + 'js/iscroll.js');
fde6ea95 530 min.ast = jsp.parse(min.underscore + min.util + min.gateway + min.front + min.iscroll);
fd779420
D
531 min.ast = pro.ast_mangle(min.ast);
532 min.ast = pro.ast_squeeze(min.ast);
533 min.final_code = pro.gen_code(min.ast);
534 kiwi.cache.alljs = min.final_code;
535 hash = crypto.createHash('md5').update(kiwi.cache.alljs);
536 kiwi.cache.alljs_hash = hash.digest('base64');
537 }
538 if (request.headers['if-none-match'] === kiwi.cache.alljs_hash) {
539 response.statusCode = 304;
540 } else {
34872bd6 541 response.setHeader('Content-type', 'application/javascript');
fd779420
D
542 response.setHeader('ETag', kiwi.cache.alljs_hash);
543 response.write(kiwi.cache.alljs);
544 }
545 response.end();
546 } else if ((subs === '/js/') || (subs === '/css') || (subs === '/img')) {
547 request.addListener('end', function () {
548 kiwi.fileServer.serve(request, response);
549 });
8397d902 550 } else if (uri.pathname === '/' || uri_parts[1] === 'client') {
a5ba2101 551 useragent = (typeof request.headers === 'string') ? request.headers['user-agent'] : '';
da41bd6b 552 if (useragent.match(/android/i) !== -1) {
fd779420
D
553 agent = 'android';
554 touchscreen = true;
da41bd6b 555 } else if (useragent.match(/iphone/) !== -1) {
fd779420
D
556 agent = 'iphone';
557 touchscreen = true;
da41bd6b 558 } else if (useragent.match(/ipad/) !== -1) {
fd779420
D
559 agent = 'ipad';
560 touchscreen = true;
da41bd6b 561 } else if (useragent.match(/ipod/) !== -1) {
fd779420
D
562 agent = 'ipod';
563 touchscreen = true;
564 } else {
565 agent = 'normal';
566 touchscreen = false;
567 }
da41bd6b
D
568 agent = 'normal';
569 touchscreen = false;
91726016
D
570
571 debug = (typeof uri.query.debug !== 'undefined');
fd779420 572
8397d902
D
573 if (uri_parts[1] !== 'client') {
574 if (uri.query) {
575 server_set = ((typeof uri.query.server !== 'undefined') && (uri.query.server !== ''));
576 server = uri.query.server || 'irc.anonnet.org';
577 nick = uri.query.nick || '';
8397d902
D
578 } else {
579 server_set = false;
580 server = 'irc.anonnet.org';
581 nick = '';
582 }
fd779420 583 } else {
8397d902
D
584 server_set = ((typeof uri_parts[2] !== 'undefined') && (uri_parts[2] !== ''));
585 server = server_set ? uri_parts[2] : 'irc.anonnet.org';
586 nick = uri.query.nick || '';
fd779420 587 }
8397d902 588
fd779420 589 response.setHeader('X-Generated-By', 'KiwiIRC');
34872bd6
D
590 hash = crypto.createHash('md5').update(touchscreen ? 't' : 'f')
591 .update(debug ? 't' : 'f')
592 .update(server_set ? 't' : 'f')
593 .update(server)
594 .update(nick)
595 .update(agent)
596 .update(JSON.stringify(kiwi.config))
597 .digest('base64');
fd779420
D
598 if (kiwi.cache.html[hash]) {
599 if (request.headers['if-none-match'] === kiwi.cache.html[hash].hash) {
600 response.statusCode = 304;
601 } else {
602 response.setHeader('Etag', kiwi.cache.html[hash].hash);
603 response.write(kiwi.cache.html[hash].html);
604 }
605 response.end();
606 } else {
b58015cb
JA
607 fs.readFile(__dirname + '/client/index.html.jade', 'utf8', function (err, str) {
608 var html, hash2;
fd779420 609 if (!err) {
b58015cb 610 html = kiwi.jade.compile(str)({ "touchscreen": touchscreen, "debug": debug, "server_set": server_set, "server": server, "nick": nick, "agent": agent, "config": kiwi.config });
b58015cb 611 hash2 = crypto.createHash('md5').update(html).digest('base64');
fd779420
D
612 kiwi.cache.html[hash] = {"html": html, "hash": hash2};
613 if (request.headers['if-none-match'] === hash2) {
614 response.statusCode = 304;
615 } else {
616 response.setHeader('Etag', hash2);
617 response.write(html);
618 }
619 } else {
620 response.statusCode = 500;
621 }
622 response.end();
623 });
624 }
625 } else if (uri.pathname.substr(0, 10) === '/socket.io') {
626 return;
627 } else {
628 response.statusCode = 404;
629 response.end();
630 }
631 }
632};
633
634
635
636
637this.websocketListen = function (port, host, handler, secure, key, cert) {
638 if (kiwi.httpServer) {
639 kiwi.httpServer.close();
640 }
641 if (secure) {
642 kiwi.httpServer = https.createServer({key: fs.readFileSync(__dirname + '/' + key), cert: fs.readFileSync(__dirname + '/' + cert)}, handler);
643 kiwi.io = ws.listen(kiwi.httpServer, {secure: true});
644 kiwi.httpServer.listen(port, host);
645 } else {
646 kiwi.httpServer = http.createServer(handler);
647 kiwi.io = ws.listen(kiwi.httpServer, {secure: false});
648 kiwi.httpServer.listen(port, host);
649 }
650
651 kiwi.io.set('log level', 1);
652 kiwi.io.enable('browser client minification');
653 kiwi.io.enable('browser client etag');
654 kiwi.io.set('transports', kiwi.config.transports);
655
656 kiwi.io.of('/kiwi').authorization(function (handshakeData, callback) {
657 var address = handshakeData.address.address;
658
659 if (typeof kiwi.connections[address] === 'undefined') {
660 kiwi.connections[address] = {count: 0, sockets: []};
661 }
662 callback(null, true);
663 }).on('connection', kiwi.websocketConnection);
664};
665
666
667
668
669
670
671this.websocketConnection = function (websocket) {
672 var con;
5c7ac96f 673 websocket.kiwi = {address: websocket.handshake.address.address, buffer: {list: []}};
fd779420
D
674 con = kiwi.connections[websocket.kiwi.address];
675
676 if (con.count >= kiwi.config.max_client_conns) {
677 websocket.emit('too_many_connections');
678 websocket.disconnect();
679 } else {
680 con.count += 1;
681 con.sockets.push(websocket);
682
683 websocket.sendClientEvent = function (event_name, data) {
4f06269b 684 var ev = kiwi.kiwi_mod.run(event_name, data, {websocket: this});
2fc64ec2
JA
685 if (ev === null) {
686 return;
687 }
4f06269b 688
fd779420
D
689 data.event = event_name;
690 websocket.emit('message', data);
691 };
692
693 websocket.sendServerLine = function (data, eol) {
694 eol = (typeof eol === 'undefined') ? '\r\n' : eol;
abb46d2d
D
695
696 try {
697 websocket.ircSocket.write(data + eol);
698 } catch (e) { }
fd779420
D
699 };
700
701 websocket.on('irc connect', kiwi.websocketIRCConnect);
702 websocket.on('message', kiwi.websocketMessage);
703 websocket.on('disconnect', kiwi.websocketDisconnect);
704 }
705};
706
707
708
709
710
711this.websocketIRCConnect = function (websocket, nick, host, port, ssl, callback) {
712 var ircSocket;
713 //setup IRC connection
714 if (!ssl) {
715 ircSocket = net.createConnection(port, host);
716 } else {
717 ircSocket = tls.connect(port, host);
718 }
719 ircSocket.setEncoding('ascii');
720 ircSocket.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}, registered: false};
721 ircSocket.on('error', function (e) {
722 if (ircSocket.IRC.registered) {
723 websocket.emit('disconnect');
724 } else {
725 websocket.emit('error', e.message);
726 }
727 });
728 websocket.ircSocket = ircSocket;
729 ircSocket.holdLast = false;
730 ircSocket.held = '';
731
732 ircSocket.on('data', function (data) {
733 kiwi.ircSocketDataHandler(data, websocket, ircSocket);
734 });
735
736 ircSocket.IRC.nick = nick;
737 // Send the login data
738 dns.reverse(websocket.kiwi.address, function (err, domains) {
739 //console.log(domains);
740 websocket.kiwi.hostname = (err) ? websocket.kiwi.address : _.first(domains);
741 if ((kiwi.config.webirc) && (kiwi.config.webirc_pass[host])) {
742 websocket.sendServerLine('WEBIRC ' + kiwi.config.webirc_pass[host] + ' KiwiIRC ' + websocket.kiwi.hostname + ' ' + websocket.kiwi.address);
743 }
744 websocket.sendServerLine('CAP LS');
745 websocket.sendServerLine('NICK ' + nick);
8397d902 746 websocket.sendServerLine('USER kiwi_' + nick.replace(/[^0-9a-zA-Z\-_.]/, '') + ' 0 0 :' + nick);
fd779420
D
747
748 if ((callback) && (typeof (callback) === 'function')) {
749 callback();
750 }
751 });
752};
753
754
755
756this.websocketMessage = function (websocket, msg, callback) {
757 var args, obj;
758 try {
759 msg.data = JSON.parse(msg.data);
760 args = msg.data.args;
761 switch (msg.data.method) {
762 case 'msg':
763 if ((args.target) && (args.msg)) {
764 obj = kiwi.kiwi_mod.run('msgsend', args, {websocket: websocket});
765 if (obj !== null) {
766 websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + args.msg);
767 }
768 }
769 break;
770 case 'action':
771 if ((args.target) && (args.msg)) {
2fc64ec2 772 websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + String.fromCharCode(1) + 'ACTION ' + args.msg + String.fromCharCode(1));
fd779420
D
773 }
774 break;
abb46d2d
D
775
776 case 'kiwi':
777 if ((args.target) && (args.data)) {
2fc64ec2 778 websocket.sendServerLine('PRIVMSG ' + args.target + ' :' + String.fromCharCode(1) + 'KIWI ' + args.data + String.fromCharCode(1));
abb46d2d
D
779 }
780 break;
781
fd779420
D
782 case 'raw':
783 websocket.sendServerLine(args.data);
784 break;
785 case 'join':
786 if (args.channel) {
787 _.each(args.channel.split(","), function (chan) {
788 websocket.sendServerLine('JOIN ' + chan);
789 });
790 }
791 break;
792 case 'topic':
793 if (args.channel) {
794 websocket.sendServerLine('TOPIC ' + args.channel + ' :' + args.topic);
795 }
796 break;
797 case 'quit':
798 websocket.ircSocket.end('QUIT :' + args.message + '\r\n');
799 websocket.sentQUIT = true;
800 websocket.ircSocket.destroySoon();
801 websocket.disconnect();
802 break;
803 case 'notice':
804 if ((args.target) && (args.msg)) {
805 websocket.sendServerLine('NOTICE ' + args.target + ' :' + args.msg);
806 }
807 break;
808 default:
809 }
810 if ((callback) && (typeof (callback) === 'function')) {
811 callback();
812 }
813 } catch (e) {
814 console.log("Caught error: " + e);
815 }
816};
817
818
819
820this.websocketDisconnect = function (websocket) {
821 var con;
822
823 if ((!websocket.sentQUIT) && (websocket.ircSocket)) {
824 try {
825 websocket.ircSocket.end('QUIT :' + kiwi.config.quit_message + '\r\n');
826 websocket.sentQUIT = true;
827 websocket.ircSocket.destroySoon();
828 } catch (e) {
829 }
830 }
831 con = kiwi.connections[websocket.kiwi.address];
832 con.count -= 1;
833 con.sockets = _.reject(con.sockets, function (sock) {
834 return sock === websocket;
835 });
836};
837
838
839
840
841
842
843this.rehash = function () {
844 var changes, i,
845 reload_config = kiwi.loadConfig();
846
847 // If loading the new config errored out, dont attempt any changes
848 if (reload_config === false) {
849 return false;
850 }
851
852 // We just want the settings that have been changed
853 changes = reload_config[1];
854
855 if (Object.keys(changes).length !== 0) {
856 console.log('%s config changes: \n', Object.keys(changes).length, changes);
857 for (i in changes) {
858 switch (i) {
859 case 'port':
860 case 'bind_address':
861 case 'listen_ssl':
862 case 'ssl_key':
863 case 'ssl_cert':
864 kiwi.websocketListen(kiwi.config.port, kiwi.config.bind_address, kiwi.httpHandler, kiwi.config.listen_ssl, kiwi.config.ssl_key, kiwi.config.ssl_cert);
865 delete changes.port;
866 delete changes.bind_address;
867 delete changes.listen_ssl;
868 delete changes.ssl_key;
869 delete changes.ssl_cert;
870 break;
871 case 'user':
872 case 'group':
873 kiwi.changeUser();
874 delete changes.user;
875 delete changes.group;
876 break;
877 case 'module_dir':
878 case 'modules':
879 kiwi.kiwi_mod.loadModules(kiwi_root, kiwi.config);
880 kiwi.kiwi_mod.printMods();
881 delete changes.module_dir;
882 delete changes.modules;
883 break;
884 }
885 }
886 }
887
888 // Also clear the kiwi.cached javascript and HTML
889 if (kiwi.config.handle_http) {
890 kiwi.cache = {alljs: '', html: []};
891 }
892
893 return true;
894};
895
896
897
898
899
900/*
901 * KiwiIRC controlling via STDIN
902 */
87a6abbe 903this.manageControll = function (data) {
2fc64ec2
JA
904 var parts = data.toString().trim().split(' '),
905 connections_cnt = 0,
906 i;
87a6abbe
D
907 switch (parts[0]) {
908 case 'rehash':
909 console.log('Rehashing...');
910 console.log(kiwi.rehash() ? 'Rehash complete' : 'Rehash failed');
911 break;
912
913 case 'recode':
914 console.log('Recoding...');
915 console.log(kiwi.recode() ? 'Recode complete' : 'Recode failed');
916 break;
917
918 case 'mod':
919 if (parts[1] === 'reload') {
920 console.log('Reloading module (' + parts[2] + ')..');
921 kiwi.kiwi_mod.reloadModule(parts[2]);
922 }
923 break;
4f06269b 924
87a6abbe
D
925 case 'cache':
926 if (parts[1] === 'clear') {
927 kiwi.cache.html = {};
604c5174 928 kiwi.cache.alljs = '';
87a6abbe
D
929 console.log('HTML cache cleared');
930 }
931 break;
932
933 case 'status':
2fc64ec2 934 for (i in kiwi.connections) {
87a6abbe
D
935 connections_cnt = connections_cnt + parseInt(kiwi.connections[i].count, 10);
936 }
937 console.log(connections_cnt.toString() + ' connected clients');
938 break;
c89b9fdf 939
87a6abbe
D
940 default:
941 console.log('Unknown command \'' + parts[0] + '\'');
942 }
2fc64ec2 943};