Support for some error messages (cannot join channel etc). Issue #7
[KiwiIRC.git] / node / kiwi.js
1 /*jslint regexp: true, confusion: true, undef: false, node: true, sloppy: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
2
3 var tls = require('tls'),
4 net = require('net'),
5 http = require('http'),
6 https = require('https'),
7 fs = require('fs'),
8 url = require('url'),
9 ws = require('socket.io'),
10 _ = require('./lib/underscore.min.js'),
11 starttls = require('./lib/starttls.js');
12
13
14 /*
15 * Find a config file in the following order:
16 * - /etc/kiwi/config.json
17 * - ./config.json
18 */
19 var config = null, config_filename = 'config.json';
20 var config_dirs = ['/etc/kiwiirc/', __dirname + '/'];
21 for(var i in config_dirs){
22 try {
23 if(fs.lstatSync(config_dirs[i] + config_filename).isDirectory() === false){
24 config = JSON.parse(fs.readFileSync(config_dirs[i] + config_filename, 'ascii'));
25 console.log('Using config file ' + config_dirs[i] + config_filename);
26 break;
27 }
28 } catch(e){
29 continue;
30 }
31 }
32
33 if(config === null){
34 console.log('Couldn\'t find a config file!');
35 process.exit(0);
36 }
37
38
39
40
41 /*
42 * Some process changes
43 */
44 process.title = 'kiwiirc';
45 function changeUser(){
46 if(typeof config.group !== 'undefined' && config.group !== ''){
47 try {
48 process.setgid(config.group);
49 }
50 catch (err) {
51 console.log('Failed to set gid: ' + err);
52 process.exit();
53 }
54 }
55
56 if(typeof config.user !== 'undefined' && config.user !== ''){
57 try {
58 process.setuid(config.user);
59 }
60 catch (err) {
61 console.log('Failed to set uid: ' + err);
62 process.exit();
63 }
64 }
65 }
66
67
68 /*
69 * And now KiwiIRC, the server :)
70 */
71
72 var ircNumerics = {
73 RPL_WELCOME: '001',
74 RPL_ISUPPORT: '005',
75 RPL_WHOISUSER: '311',
76 RPL_WHOISSERVER: '312',
77 RPL_WHOISOPERATOR: '313',
78 RPL_WHOISIDLE: '317',
79 RPL_ENDOFWHOIS: '318',
80 RPL_WHOISCHANNELS: '319',
81 RPL_TOPIC: '332',
82 RPL_NAMEREPLY: '353',
83 RPL_ENDOFNAMES: '366',
84 RPL_MOTD: '372',
85 RPL_WHOISMODES: '379',
86 ERR_NOSUCHNICK: '401',
87 ERR_CANNOTSENDTOCHAN: '404',
88 ERR_TOOMANYCHANNELS: '405',
89 ERR_USERNOTINCHANNEL: '441',
90 ERR_NOTONCHANNEL: '442',
91 ERR_LINKCHANNEL: '470',
92 ERR_CHANNELISFULL: '471',
93 ERR_INVITEONLYCHAN: '473',
94 ERR_BANNEDFROMCHAN: '474',
95 ERR_BADCHANNELKEY: '475',
96 RPL_STARTTLS: '670'
97 };
98
99
100 var parseIRCMessage = function (websocket, ircSocket, data) {
101 /*global ircSocketDataHandler */
102 var msg, regex, opts, options, opt, i, j, matches, nick, users, chan, params, prefix, prefixes, nicklist, caps, rtn;
103 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;
104 msg = regex.exec(data);
105 if (msg) {
106 msg = {
107 prefix: msg[1],
108 nick: msg[2],
109 ident: msg[3],
110 hostname: msg[4],
111 command: msg[5],
112 params: msg[6] || '',
113 trailing: (msg[7]) ? msg[7].trim() : ''
114 };
115 switch (msg.command.toUpperCase()) {
116 case 'PING':
117 ircSocket.write('PONG ' + msg.trailing + '\r\n');
118 break;
119 case ircNumerics.RPL_WELCOME:
120 if (ircSocket.IRC.CAP.negotiating) {
121 ircSocket.IRC.CAP.negotiating = false;
122 ircSocket.IRC.CAP.enabled = [];
123 ircSocket.IRC.CAP.requested = [];
124 }
125 websocket.emit('message', {event: 'connect', connected: true, host: null});
126 break;
127 case ircNumerics.RPL_ISUPPORT:
128 opts = msg.params.split(" ");
129 options = [];
130 for (i = 0; i < opts.length; i++) {
131 opt = opts[i].split("=", 2);
132 opt[0] = opt[0].toUpperCase();
133 ircSocket.IRC.options[opt[0]] = opt[1] || true;
134 if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES'], opt[0])) {
135 if (opt[0] === 'PREFIX') {
136 regex = /\(([^)]*)\)(.*)/;
137 matches = regex.exec(opt[1]);
138 if ((matches) && (matches.length === 3)) {
139 ircSocket.IRC.options[opt[0]] = [];
140 for (j = 0; j < matches[2].length; j++) {
141 //ircSocket.IRC.options[opt[0]][matches[2].charAt(j)] = matches[1].charAt(j);
142 ircSocket.IRC.options[opt[0]].push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
143 //console.log({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
144 }
145 console.log(ircSocket.IRC.options);
146 }
147 }
148 }
149 }
150 websocket.emit('message', {event: 'options', server: '', "options": ircSocket.IRC.options});
151 break;
152 case ircNumerics.RPL_WHOISUSER:
153 case ircNumerics.RPL_WHOISSERVER:
154 case ircNumerics.RPL_WHOISOPERATOR:
155 case ircNumerics.RPL_ENDOFWHOIS:
156 case ircNumerics.RPL_WHOISCHANNELS:
157 case ircNumerics.RPL_WHOISMODES:
158 websocket.emit('message', {event: 'whois', server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing});
159 break;
160 case ircNumerics.RPL_WHOISIDLE:
161 params = msg.params.split(" ", 4);
162 rtn = {event: 'whois', server: '', nick: params[1], idle: params[2]};
163 if (params[3]) {
164 rtn.logon = params[3];
165 }
166 websocket.emit('message', rtn);
167 break;
168 case ircNumerics.RPL_MOTD:
169 websocket.emit('message', {event: 'motd', server: '', "msg": msg.trailing});
170 break;
171 case ircNumerics.RPL_NAMEREPLY:
172 params = msg.params.split(" ");
173 nick = params[0];
174 chan = params[2];
175 users = msg.trailing.split(" ");
176 prefixes = _.values(ircSocket.IRC.options.PREFIX);
177 nicklist = {};
178 i = 0;
179 _.each(users, function (user) {
180 if (_.include(prefix, user.charAt(0))) {
181 prefix = user.charAt(0);
182 user = user.substring(1);
183 nicklist[user] = prefix;
184 } else {
185 nicklist[user] = '';
186 }
187 if (i++ >= 50) {
188 websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
189 nicklist = {};
190 i = 0;
191 }
192 });
193 if (i > 0) {
194 websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
195 } else {
196 console.log("oops");
197 }
198 break;
199 case ircNumerics.RPL_ENDOFNAMES:
200 websocket.emit('message', {event: 'userlist_end', server: '', channel: msg.params.split(" ")[1]});
201 break;
202 case ircNumerics.ERR_LINKCHANNEL:
203 params = msg.params.split(" ");
204 websocket.emit('message', {event: 'channel_redirect', from: params[1], to: params[2]});
205 break;
206 case ircNumerics.ERR_NOSUCHNICK:
207 //TODO: shit
208 break;
209 case 'JOIN':
210 websocket.emit('message', {event: 'join', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.trailing});
211 if (msg.nick === ircSocket.IRC.nick) {
212 ircSocket.write('NAMES ' + msg.trailing + '\r\n');
213 }
214 break;
215 case 'PART':
216 websocket.emit('message', {event: 'part', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
217 break;
218 case 'KICK':
219 params = msg.params.split(" ");
220 websocket.emit('message', {event: 'kick', kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
221 break;
222 case 'QUIT':
223 websocket.emit('message', {event: 'quit', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
224 break;
225 case 'NOTICE':
226 if ((msg.trailing.charAt(0) === '\001') && (msg.trailing.charAt(msg.trailing.length - 1) === '\001')) {
227 // It's a CTCP response
228 websocket.emit('message', {event: 'ctcp_response', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)});
229 } else {
230 websocket.emit('message', {event: 'notice', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
231 }
232 break;
233 case 'NICK':
234 websocket.emit('message', {event: 'nick', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
235 break;
236 case 'TOPIC':
237 websocket.emit('message', {event: 'topic', nick: msg.nick, channel: msg.params, topic: msg.trailing});
238 break;
239 case ircNumerics.RPL_TOPIC:
240 websocket.emit('message', {event: 'topic', nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing});
241 break;
242 case 'MODE':
243 opts = msg.params.split(" ");
244 params = {event: 'mode', nick: msg.nick};
245 switch (opts.length) {
246 case 1:
247 params.effected_nick = opts[0];
248 params.mode = msg.trailing;
249 break;
250 case 2:
251 params.channel = opts[0];
252 params.mode = opts[1];
253 break;
254 default:
255 params.channel = opts[0];
256 params.mode = opts[1];
257 params.effected_nick = opts[2];
258 break;
259 }
260 websocket.emit('message', params);
261 break;
262 case 'PRIVMSG':
263 if ((msg.trailing.charAt(0) === '\001') && (msg.trailing.charAt(msg.trailing.length - 1) === '\001')) {
264 // It's a CTCP request
265 if (msg.trailing.substr(1, 6) === 'ACTION') {
266 websocket.emit('message', {event: 'action', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(7, msg.trailing.length - 2)});
267 } else if (msg.trailing.substr(1, 7) === 'VERSION') {
268 ircSocket.write('NOTICE ' + msg.nick + ' :\001VERSION KiwiIRC\001\r\n');
269 } else {
270 websocket.emit('message', {event: 'ctcp_request', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing.substr(1, msg.trailing.length - 2)});
271 }
272 } else {
273 websocket.emit('message', {event: 'msg', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
274 }
275 break;
276 case 'CAP':
277 caps = config.cap_options;
278 options = msg.trailing.split(" ");
279 switch (_.first(msg.params.split(" "))) {
280 case 'LS':
281 opts = '';
282 _.each(_.intersect(caps, options), function (cap) {
283 if (opts !== '') {
284 opts += " ";
285 }
286 opts += cap;
287 ircSocket.IRC.CAP.requested.push(cap);
288 });
289 if (opts.length > 0) {
290 ircSocket.write('CAP REQ :' + opts + '\r\n');
291 } else {
292 ircSocket.write('CAP END\r\n');
293 }
294 // TLS is special
295 /*if (_.include(options, 'tls')) {
296 ircSocket.write('STARTTLS\r\n');
297 ircSocket.IRC.CAP.requested.push('tls');
298 }*/
299 break;
300 case 'ACK':
301 _.each(options, function (cap) {
302 ircSocket.IRC.CAP.enabled.push(cap);
303 });
304 if (_.last(msg.params.split(" ")) !== '*') {
305 ircSocket.IRC.CAP.requested = [];
306 ircSocket.IRC.CAP.negotiating = false;
307 ircSocket.write('CAP END\r\n');
308 }
309 break;
310 case 'NAK':
311 ircSocket.IRC.CAP.requested = [];
312 ircSocket.IRC.CAP.negotiating = false;
313 ircSocket.write('CAP END\r\n');
314 break;
315 }
316 break;
317 /*case ircNumerics.RPL_STARTTLS:
318 try {
319 IRC = ircSocket.IRC;
320 listeners = ircSocket.listeners('data');
321 ircSocket.removeAllListeners('data');
322 ssl_socket = starttls(ircSocket, {}, function () {
323 ssl_socket.on("data", function (data) {
324 ircSocketDataHandler(data, websocket, ssl_socket);
325 });
326 ircSocket = ssl_socket;
327 ircSocket.IRC = IRC;
328 _.each(listeners, function (listener) {
329 ircSocket.addListener('data', listener);
330 });
331 });
332 //console.log(ircSocket);
333 } catch (e) {
334 console.log(e);
335 }
336 break;*/
337 case ircNumerics.ERR_CANNOTSENDTOCHAN:
338 websocket.emit('message', {event: 'irc_error', error: 'cannot_send_to_chan', channel: msg.params.split(" ")[1], reason: msg.trailing});
339 break;
340 case ircNumerics.ERR_TOOMANYCHANNELS:
341 websocket.emit('message', {event: 'irc_error', error: 'too_many_channels', channel: msg.params.split(" ")[1], reason: msg.trailing});
342 break;
343 case ircNumerics.ERR_USERNOTINCHANNEL:
344 params = msg.params.split(" ");
345 websocket.emit('message', {event: 'irc_error', error: 'user_not_in_channel', nick: params[0], channel: params[1], reason: msg.trainling});
346 break;
347 case ircNumerics.ERR_NOTONCHANNEL:
348 websocket.emit('message', {event: 'irc_error', error: 'not_on_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
349 break;
350 case ircNumerics.ERR_CHANNELISFULL:
351 websocket.emit('message', {event: 'irc_error', error: 'channel_is_full', channel: msg.params.split(" ")[1], reason: msg.trailing});
352 break;
353 case ircNumerics.ERR_INVITEONLYCHAN:
354 websocket.emit('message', {event: 'irc_error', error: 'invite_only_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
355 break;
356 case ircNumerics.ERR_BANNEDFROMCHAN:
357 websocket.emit('message', {event: 'irc_error', error: 'banned_from_channel', channel: msg.params.split(" ")[1], reason: msg.trailing});
358 break;
359 case ircNumerics.ERR_BADCHANNELKEY:
360 websocket.emit('message', {event: 'irc_error', error: 'bad_channel_key', channel: msg.params.split(" ")[1], reason: msg.trailing});
361 break;
362 }
363 } else {
364 console.log("Unknown command.\r\n");
365 }
366 };
367
368 var ircSocketDataHandler = function (data, websocket, ircSocket) {
369 var i;
370 if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
371 data = ircSocket.held + data;
372 ircSocket.holdLast = false;
373 ircSocket.held = '';
374 }
375 if (data.substr(-2) === '\r\n') {
376 ircSocket.holdLast = true;
377 }
378 data = data.split("\r\n");
379 for (i = 0; i < data.length; i++) {
380 if (data[i]) {
381 if ((ircSocket.holdLast) && (i === data.length - 1)) {
382 ircSocket.held = data[i];
383 break;
384 }
385 console.log("->" + data[i]);
386 parseIRCMessage(websocket, ircSocket, data[i]);
387 }
388 }
389 };
390
391 if (config.handle_http) {
392 var fileServer = new (require('node-static').Server)(__dirname + config.public_http);
393 var jade = require('jade');
394 }
395
396 var httpHandler = function (request, response) {
397 var uri, subs, useragent, agent, server_set, server, nick, debug, touchscreen;
398 if (config.handle_http) {
399 uri = url.parse(request.url);
400 subs = uri.pathname.substr(0, 4);
401 if ((subs === '/js/') || (subs === '/css') || (subs === '/img')) {
402 request.addListener('end', function () {
403 fileServer.serve(request, response);
404 });
405 } else if (uri.pathname === '/') {
406 useragent = (response.headers) ? response.headers['user-agent']: '';
407 if (useragent.indexOf('android') !== -1) {
408 agent = 'android';
409 touchscreen = true;
410 } else if (useragent.indexOf('iphone') !== -1) {
411 agent = 'iphone';
412 touchscreen = true;
413 } else if (useragent.indexOf('ipad') !== -1) {
414 agent = 'ipad';
415 touchscreen = true;
416 } else if (useragent.indexOf('ipod') !== -1) {
417 agent = 'ipod';
418 touchscreen = true;
419 } else {
420 agent = 'normal';
421 touchscreen = false;
422 }
423 if (uri.query) {
424 server_set = (uri.query.server !== '');
425 server = uri.query.server || 'irc.anonnet.org';
426 nick = uri.query.nick || '';
427 debug = (uri.query.debug !== '');
428 } else {
429 server = 'irc.anonnet.org';
430 nick = '';
431 }
432 response.setHeader('Connection', 'close');
433 response.setHeader('X-Generated-By', 'KiwiIRC');
434 jade.renderFile(__dirname + '/client/index.html.jade', { locals: { "touchscreen": touchscreen, "debug": debug, "server_set": server_set, "server": server, "nick": nick, "agent": agent, "config": config }}, function (err, html) {
435 if (!err) {
436 response.write(html);
437 } else {
438 response.statusCode = 500;
439 }
440 response.end();
441 });
442 } else if (uri.pathname.substr(0, 10) === '/socket.io') {
443 // Do nothing!
444 } else {
445 response.statusCode = 404;
446 response.end();
447 }
448 }
449 };
450
451 //setup websocket listener
452 if (config.listen_ssl) {
453 var httpServer = https.createServer({key: fs.readFileSync(__dirname + '/' + config.ssl_key), cert: fs.readFileSync(__dirname + '/' + config.ssl_cert)}, httpHandler);
454 var io = ws.listen(httpServer, {secure: true});
455 httpServer.listen(config.port, config.bind_address);
456 } else {
457 var httpServer = http.createServer(httpHandler);
458 var io = ws.listen(httpServer, {secure: false});
459 httpServer.listen(config.port, config.bind_address);
460 }
461
462 // Now we're listening on the network, set our UID/GIDs if required
463 changeUser();
464
465 io.of('/kiwi').on('connection', function (websocket) {
466 websocket.on('irc connect', function (nick, host, port, ssl, callback) {
467 var ircSocket;
468 //setup IRC connection
469 if (!ssl) {
470 ircSocket = net.createConnection(port, host);
471 } else {
472 ircSocket = tls.connect(port, host);
473 }
474 ircSocket.setEncoding('ascii');
475 ircSocket.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}};
476 websocket.ircSocket = ircSocket;
477 ircSocket.holdLast = false;
478 ircSocket.held = '';
479
480 ircSocket.on('data', function (data) {
481 ircSocketDataHandler(data, websocket, ircSocket);
482 });
483
484 ircSocket.IRC.nick = nick;
485 // Send the login data
486 ircSocket.write('CAP LS\r\n');
487 ircSocket.write('NICK ' + nick + '\r\n');
488 ircSocket.write('USER ' + nick + '_kiwi 0 0 :' + nick + '\r\n');
489
490 if ((callback) && (typeof (callback) === 'function')) {
491 callback();
492 }
493 });
494 websocket.on('message', function (msg, callback) {
495 var args;
496 try {
497 msg.data = JSON.parse(msg.data);
498 args = msg.data.args;
499 switch (msg.data.method) {
500 case 'msg':
501 if ((args.target) && (args.msg)) {
502 websocket.ircSocket.write('PRIVMSG ' + args.target + ' :' + args.msg + '\r\n');
503 }
504 break;
505 case 'action':
506 if ((args.target) && (args.msg)) {
507 websocket.ircSocket.write('PRIVMSG ' + args.target + ' :\ 1ACTION ' + args.msg + '\ 1\r\n');
508 }
509 break;
510 case 'raw':
511 websocket.ircSocket.write(args.data + '\r\n');
512 break;
513 case 'join':
514 if (args.channel) {
515 _.each(args.channel.split(","), function (chan) {
516 websocket.ircSocket.write('JOIN ' + chan + '\r\n');
517 });
518 }
519 break;
520 case 'quit':
521 websocket.ircSocket.end('QUIT :' + args.message + '\r\n');
522 websocket.sentQUIT = true;
523 websocket.ircSocket.destroySoon();
524 websocket.disconnect();
525 break;
526 case 'notice':
527 if ((args.target) && (args.msg)) {
528 websocket.ircSocket.write('NOTICE ' + args.target + ' :' + args.msg + '\r\n');
529 }
530 break;
531 default:
532 }
533 if ((callback) && (typeof (callback) === 'function')) {
534 callback();
535 }
536 } catch (e) {
537 console.log("Caught error: " + e);
538 }
539 });
540 websocket.on('disconnect', function () {
541 if ((!websocket.sentQUIT) && (websocket.ircSocket)) {
542 websocket.ircSocket.end('QUIT :' + config.quit_message + '\r\n');
543 websocket.sentQUIT = true;
544 websocket.ircSocket.destroySoon();
545 }
546 });
547 });
548