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