Added setuid+setgid and alternative config file locations
[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_LINKCHANNEL: '470',
88 RPL_STARTTLS: '670'
89 };
90
91
92 var parseIRCMessage = function (websocket, ircSocket, data) {
93 /*global ircSocketDataHandler */
94 var msg, regex, opts, options, opt, i, j, matches, nick, users, chan, params, prefix, prefixes, nicklist, caps, rtn;
95 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;
96 msg = regex.exec(data);
97 if (msg) {
98 msg = {
99 prefix: msg[1],
100 nick: msg[2],
101 ident: msg[3],
102 hostname: msg[4],
103 command: msg[5],
104 params: msg[6] || '',
105 trailing: (msg[7]) ? msg[7].trim() : ''
106 };
107 switch (msg.command.toUpperCase()) {
108 case 'PING':
109 ircSocket.write('PONG ' + msg.trailing + '\r\n');
110 break;
111 case ircNumerics.RPL_WELCOME:
112 if (ircSocket.IRC.CAP.negotiating) {
113 ircSocket.IRC.CAP.negotiating = false;
114 ircSocket.IRC.CAP.enabled = [];
115 ircSocket.IRC.CAP.requested = [];
116 }
117 websocket.emit('message', {event: 'connect', connected: true, host: null});
118 break;
119 case ircNumerics.RPL_ISUPPORT:
120 opts = msg.params.split(" ");
121 options = [];
122 for (i = 0; i < opts.length; i++) {
123 opt = opts[i].split("=", 2);
124 opt[0] = opt[0].toUpperCase();
125 ircSocket.IRC.options[opt[0]] = opt[1] || true;
126 if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES'], opt[0])) {
127 if (opt[0] === 'PREFIX') {
128 regex = /\(([^)]*)\)(.*)/;
129 matches = regex.exec(opt[1]);
130 if ((matches) && (matches.length === 3)) {
131 ircSocket.IRC.options[opt[0]] = [];
132 for (j = 0; j < matches[2].length; j++) {
133 //ircSocket.IRC.options[opt[0]][matches[2].charAt(j)] = matches[1].charAt(j);
134 ircSocket.IRC.options[opt[0]].push({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
135 //console.log({symbol: matches[2].charAt(j), mode: matches[1].charAt(j)});
136 }
137 console.log(ircSocket.IRC.options);
138 }
139 }
140 }
141 }
142 websocket.emit('message', {event: 'options', server: '', "options": ircSocket.IRC.options});
143 break;
144 case ircNumerics.RPL_WHOISUSER:
145 case ircNumerics.RPL_WHOISSERVER:
146 case ircNumerics.RPL_WHOISOPERATOR:
147 case ircNumerics.RPL_ENDOFWHOIS:
148 case ircNumerics.RPL_WHOISCHANNELS:
149 case ircNumerics.RPL_WHOISMODES:
150 websocket.emit('message', {event: 'whois', server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing});
151 break;
152 case ircNumerics.RPL_WHOISIDLE:
153 params = msg.params.split(" ", 4);
154 rtn = {event: 'whois', server: '', nick: params[1], idle: params[2]};
155 if (params[3]) {
156 rtn.logon = params[3];
157 }
158 websocket.emit('message', rtn);
159 break;
160 case ircNumerics.RPL_MOTD:
161 websocket.emit('message', {event: 'motd', server: '', "msg": msg.trailing});
162 break;
163 case ircNumerics.RPL_NAMEREPLY:
164 params = msg.params.split(" ");
165 nick = params[0];
166 chan = params[2];
167 users = msg.trailing.split(" ");
168 prefixes = _.values(ircSocket.IRC.options.PREFIX);
169 nicklist = {};
170 i = 0;
171 _.each(users, function (user) {
172 if (_.include(prefix, user.charAt(0))) {
173 prefix = user.charAt(0);
174 user = user.substring(1);
175 nicklist[user] = prefix;
176 } else {
177 nicklist[user] = '';
178 }
179 if (i++ >= 50) {
180 websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
181 nicklist = {};
182 i = 0;
183 }
184 });
185 if (i > 0) {
186 websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
187 } else {
188 console.log("oops");
189 }
190 break;
191 case ircNumerics.RPL_ENDOFNAMES:
192 websocket.emit('message', {event: 'userlist_end', server: '', channel: msg.params.split(" ")[1]});
193 break;
194 case ircNumerics.ERR_LINKCHANNEL:
195 params = msg.params.split(" ");
196 websocket.emit('message', {event: 'channel_redirect', from: params[1], to: params[2]});
197 break;
198 case ircNumerics.ERR_NOSUCHNICK:
199 //TODO: shit
200 break;
201 case 'JOIN':
202 websocket.emit('message', {event: 'join', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.trailing});
203 if (msg.nick === ircSocket.IRC.nick) {
204 ircSocket.write('NAMES ' + msg.trailing + '\r\n');
205 }
206 break;
207 case 'PART':
208 websocket.emit('message', {event: 'part', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
209 break;
210 case 'KICK':
211 params = msg.params.split(" ");
212 websocket.emit('message', {event: 'kick', kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
213 break;
214 case 'QUIT':
215 websocket.emit('message', {event: 'quit', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
216 break;
217 case 'NOTICE':
218 if ((msg.trailing.charAt(0) === '\001') && (msg.trailing.charAt(msg.trailing.length - 1) === '\001')) {
219 // It's a CTCP response
220 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)});
221 } else {
222 websocket.emit('message', {event: 'notice', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
223 }
224 break;
225 case 'NICK':
226 websocket.emit('message', {event: 'nick', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
227 break;
228 case 'TOPIC':
229 websocket.emit('message', {event: 'topic', nick: msg.nick, channel: msg.params, topic: msg.trailing});
230 break;
231 case ircNumerics.RPL_TOPIC:
232 websocket.emit('message', {event: 'topic', nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing});
233 break;
234 case 'MODE':
235 opts = msg.params.split(" ");
236 params = {event: 'mode', nick: msg.nick};
237 switch (opts.length) {
238 case 1:
239 params.effected_nick = opts[0];
240 params.mode = msg.trailing;
241 break;
242 case 2:
243 params.channel = opts[0];
244 params.mode = opts[1];
245 break;
246 default:
247 params.channel = opts[0];
248 params.mode = opts[1];
249 params.effected_nick = opts[2];
250 break;
251 }
252 websocket.emit('message', params);
253 break;
254 case 'PRIVMSG':
255 if ((msg.trailing.charAt(0) === '\001') && (msg.trailing.charAt(msg.trailing.length - 1) === '\001')) {
256 // It's a CTCP request
257 if (msg.trailing.substr(1, 6) === 'ACTION') {
258 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)});
259 } else if (msg.trailing.substr(1, 7) === 'VERSION') {
260 ircSocket.write('NOTICE ' + msg.nick + ' :\001VERSION KiwiIRC\001\r\n');
261 } else {
262 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)});
263 }
264 } else {
265 websocket.emit('message', {event: 'msg', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
266 }
267 break;
268 case 'CAP':
269 caps = config.cap_options;
270 options = msg.trailing.split(" ");
271 switch (_.first(msg.params.split(" "))) {
272 case 'LS':
273 opts = '';
274 _.each(_.intersect(caps, options), function (cap) {
275 if (opts !== '') {
276 opts += " ";
277 }
278 opts += cap;
279 ircSocket.IRC.CAP.requested.push(cap);
280 });
281 if (opts.length > 0) {
282 ircSocket.write('CAP REQ :' + opts + '\r\n');
283 } else {
284 ircSocket.write('CAP END\r\n');
285 }
286 // TLS is special
287 /*if (_.include(options, 'tls')) {
288 ircSocket.write('STARTTLS\r\n');
289 ircSocket.IRC.CAP.requested.push('tls');
290 }*/
291 break;
292 case 'ACK':
293 _.each(options, function (cap) {
294 ircSocket.IRC.CAP.enabled.push(cap);
295 });
296 if (_.last(msg.params.split(" ")) !== '*') {
297 ircSocket.IRC.CAP.requested = [];
298 ircSocket.IRC.CAP.negotiating = false;
299 ircSocket.write('CAP END\r\n');
300 }
301 break;
302 case 'NAK':
303 ircSocket.IRC.CAP.requested = [];
304 ircSocket.IRC.CAP.negotiating = false;
305 ircSocket.write('CAP END\r\n');
306 break;
307 }
308 break;
309 /*case ircNumerics.RPL_STARTTLS:
310 try {
311 IRC = ircSocket.IRC;
312 listeners = ircSocket.listeners('data');
313 ircSocket.removeAllListeners('data');
314 ssl_socket = starttls(ircSocket, {}, function () {
315 ssl_socket.on("data", function (data) {
316 ircSocketDataHandler(data, websocket, ssl_socket);
317 });
318 ircSocket = ssl_socket;
319 ircSocket.IRC = IRC;
320 _.each(listeners, function (listener) {
321 ircSocket.addListener('data', listener);
322 });
323 });
324 //console.log(ircSocket);
325 } catch (e) {
326 console.log(e);
327 }
328 break;*/
329 }
330 } else {
331 console.log("Unknown command.\r\n");
332 }
333 };
334
335 var ircSocketDataHandler = function (data, websocket, ircSocket) {
336 var i;
337 if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
338 data = ircSocket.held + data;
339 ircSocket.holdLast = false;
340 ircSocket.held = '';
341 }
342 if (data.substr(-2) === '\r\n') {
343 ircSocket.holdLast = true;
344 }
345 data = data.split("\r\n");
346 for (i = 0; i < data.length; i++) {
347 if (data[i]) {
348 if ((ircSocket.holdLast) && (i === data.length - 1)) {
349 ircSocket.held = data[i];
350 break;
351 }
352 console.log("->" + data[i]);
353 parseIRCMessage(websocket, ircSocket, data[i]);
354 }
355 }
356 };
357
358 if (config.handle_http) {
359 var fileServer = new (require('node-static').Server)(__dirname + config.public_http);
360 var jade = require('jade');
361 }
362
363 var httpHandler = function (request, response) {
364 var uri, subs, useragent, agent, server_set, server, nick, debug, touchscreen;
365 if (config.handle_http) {
366 uri = url.parse(request.url);
367 subs = uri.pathname.substr(0, 4);
368 if ((subs === '/js/') || (subs === '/css') || (subs === '/img')) {
369 request.addListener('end', function () {
370 fileServer.serve(request, response);
371 });
372 } else if (uri.pathname === '/') {
373 useragent = (response.headers) ? response.headers['user-agent']: '';
374 if (useragent.indexOf('android') !== -1) {
375 agent = 'android';
376 touchscreen = true;
377 } else if (useragent.indexOf('iphone') !== -1) {
378 agent = 'iphone';
379 touchscreen = true;
380 } else if (useragent.indexOf('ipad') !== -1) {
381 agent = 'ipad';
382 touchscreen = true;
383 } else if (useragent.indexOf('ipod') !== -1) {
384 agent = 'ipod';
385 touchscreen = true;
386 } else {
387 agent = 'normal';
388 touchscreen = false;
389 }
390 if (uri.query) {
391 server_set = (uri.query.server !== '');
392 server = uri.query.server || 'irc.anonnet.org';
393 nick = uri.query.nick || '';
394 debug = (uri.query.debug !== '');
395 } else {
396 server = 'irc.anonnet.org';
397 nick = '';
398 }
399 response.setHeader('Connection', 'close');
400 response.setHeader('X-Generated-By', 'KiwiIRC');
401 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) {
402 if (!err) {
403 response.write(html);
404 } else {
405 response.statusCode = 500;
406 }
407 response.end();
408 });
409 } else if (uri.pathname.substr(0, 10) === '/socket.io') {
410 // Do nothing!
411 } else {
412 response.statusCode = 404;
413 response.end();
414 }
415 }
416 };
417
418 //setup websocket listener
419 if (config.listen_ssl) {
420 var httpServer = https.createServer({key: fs.readFileSync(__dirname + '/' + config.ssl_key), cert: fs.readFileSync(__dirname + '/' + config.ssl_cert)}, httpHandler);
421 var io = ws.listen(httpServer, {secure: true});
422 httpServer.listen(config.port, config.bind_address);
423 } else {
424 var httpServer = http.createServer(httpHandler);
425 var io = ws.listen(httpServer, {secure: false});
426 httpServer.listen(config.port, config.bind_address);
427 }
428
429 // Now we're listening on the network, set our UID/GIDs if required
430 changeUser();
431
432 io.of('/kiwi').on('connection', function (websocket) {
433 websocket.on('irc connect', function (nick, host, port, ssl, callback) {
434 var ircSocket;
435 //setup IRC connection
436 if (!ssl) {
437 ircSocket = net.createConnection(port, host);
438 } else {
439 ircSocket = tls.connect(port, host);
440 }
441 ircSocket.setEncoding('ascii');
442 ircSocket.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}};
443 websocket.ircSocket = ircSocket;
444 ircSocket.holdLast = false;
445 ircSocket.held = '';
446
447 ircSocket.on('data', function (data) {
448 ircSocketDataHandler(data, websocket, ircSocket);
449 });
450
451 ircSocket.IRC.nick = nick;
452 // Send the login data
453 ircSocket.write('CAP LS\r\n');
454 ircSocket.write('NICK ' + nick + '\r\n');
455 ircSocket.write('USER ' + nick + '_kiwi 0 0 :' + nick + '\r\n');
456
457 if ((callback) && (typeof (callback) === 'function')) {
458 callback();
459 }
460 });
461 websocket.on('message', function (msg, callback) {
462 var args;
463 try {
464 msg.data = JSON.parse(msg.data);
465 args = msg.data.args;
466 switch (msg.data.method) {
467 case 'msg':
468 if ((args.target) && (args.msg)) {
469 websocket.ircSocket.write('PRIVMSG ' + args.target + ' :' + args.msg + '\r\n');
470 }
471 break;
472 case 'action':
473 if ((args.target) && (args.msg)) {
474 websocket.ircSocket.write('PRIVMSG ' + args.target + ' :\ 1ACTION ' + args.msg + '\ 1\r\n');
475 }
476 break;
477 case 'raw':
478 websocket.ircSocket.write(args.data + '\r\n');
479 break;
480 case 'join':
481 if (args.channel) {
482 _.each(args.channel.split(","), function (chan) {
483 websocket.ircSocket.write('JOIN ' + chan + '\r\n');
484 });
485 }
486 break;
487 case 'quit':
488 websocket.ircSocket.end('QUIT :' + args.message + '\r\n');
489 websocket.sentQUIT = true;
490 websocket.ircSocket.destroySoon();
491 websocket.disconnect();
492 break;
493 case 'notice':
494 if ((args.target) && (args.msg)) {
495 websocket.ircSocket.write('NOTICE ' + args.target + ' :' + args.msg + '\r\n');
496 }
497 break;
498 default:
499 }
500 if ((callback) && (typeof (callback) === 'function')) {
501 callback();
502 }
503 } catch (e) {
504 console.log("Caught error: " + e);
505 }
506 });
507 websocket.on('disconnect', function () {
508 if ((!websocket.sentQUIT) && (websocket.ircSocket)) {
509 websocket.ircSocket.end('QUIT :' + config.quit_message + '\r\n');
510 websocket.sentQUIT = true;
511 websocket.ircSocket.destroySoon();
512 }
513 });
514 });
515