1 /*jslint regexp: true, confusion: true, undef: false, node: true, sloppy: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
3 var tls
= require('tls'),
5 http
= require('http'),
6 https
= require('https'),
9 ws
= require('socket.io'),
10 _
= require('./lib/underscore.min.js'),
11 starttls
= require('./lib/starttls.js');
15 * Find a config file in the following order:
16 * - /etc/kiwi/config.json
19 var config
= null, config_filename
= 'config.json';
20 var config_dirs
= ['/etc/kiwiirc/', __dirname
+ '/'];
21 for(var i
in config_dirs
){
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
);
34 console
.log('Couldn\'t find a config file!');
42 * Some process changes
44 process
.title
= 'kiwiirc';
45 function changeUser(){
46 if(typeof config
.group
!== 'undefined' && config
.group
!== ''){
48 process
.setgid(config
.group
);
51 console
.log('Failed to set gid: ' + err
);
56 if(typeof config
.user
!== 'undefined' && config
.user
!== ''){
58 process
.setuid(config
.user
);
61 console
.log('Failed to set uid: ' + err
);
69 * And now KiwiIRC, the server :)
76 RPL_WHOISSERVER
: '312',
77 RPL_WHOISOPERATOR
: '313',
79 RPL_ENDOFWHOIS
: '318',
80 RPL_WHOISCHANNELS
: '319',
83 RPL_ENDOFNAMES
: '366',
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',
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
);
112 params
: msg
[6] || '',
113 trailing
: (msg
[7]) ? msg
[7].trim() : ''
115 switch (msg
.command
.toUpperCase()) {
117 ircSocket
.write('PONG ' + msg
.trailing
+ '\r\n');
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
= [];
125 websocket
.emit('message', {event
: 'connect', connected
: true, host
: null});
127 case ircNumerics
.RPL_ISUPPORT
:
128 opts
= msg
.params
.split(" ");
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)});
145 console
.log(ircSocket
.IRC
.options
);
150 websocket
.emit('message', {event
: 'options', server
: '', "options": ircSocket
.IRC
.options
});
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
});
160 case ircNumerics
.RPL_WHOISIDLE
:
161 params
= msg
.params
.split(" ", 4);
162 rtn
= {event
: 'whois', server
: '', nick
: params
[1], idle
: params
[2]};
164 rtn
.logon
= params
[3];
166 websocket
.emit('message', rtn
);
168 case ircNumerics
.RPL_MOTD
:
169 websocket
.emit('message', {event
: 'motd', server
: '', "msg": msg
.trailing
});
171 case ircNumerics
.RPL_NAMEREPLY
:
172 params
= msg
.params
.split(" ");
175 users
= msg
.trailing
.split(" ");
176 prefixes
= _
.values(ircSocket
.IRC
.options
.PREFIX
);
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
;
188 websocket
.emit('message', {event
: 'userlist', server
: '', "users": nicklist
, channel
: chan
});
194 websocket
.emit('message', {event
: 'userlist', server
: '', "users": nicklist
, channel
: chan
});
199 case ircNumerics
.RPL_ENDOFNAMES
:
200 websocket
.emit('message', {event
: 'userlist_end', server
: '', channel
: msg
.params
.split(" ")[1]});
202 case ircNumerics
.ERR_LINKCHANNEL
:
203 params
= msg
.params
.split(" ");
204 websocket
.emit('message', {event
: 'channel_redirect', from: params
[1], to
: params
[2]});
206 case ircNumerics
.ERR_NOSUCHNICK
:
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');
216 websocket
.emit('message', {event
: 'part', nick
: msg
.nick
, ident
: msg
.ident
, hostname
: msg
.hostname
, channel
: msg
.params
.trim(), message
: msg
.trailing
});
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
});
223 websocket
.emit('message', {event
: 'quit', nick
: msg
.nick
, ident
: msg
.ident
, hostname
: msg
.hostname
, message
: msg
.trailing
});
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)});
230 websocket
.emit('message', {event
: 'notice', nick
: msg
.nick
, ident
: msg
.ident
, hostname
: msg
.hostname
, channel
: msg
.params
.trim(), msg
: msg
.trailing
});
234 websocket
.emit('message', {event
: 'nick', nick
: msg
.nick
, ident
: msg
.ident
, hostname
: msg
.hostname
, newnick
: msg
.trailing
});
237 websocket
.emit('message', {event
: 'topic', nick
: msg
.nick
, channel
: msg
.params
, topic
: msg
.trailing
});
239 case ircNumerics
.RPL_TOPIC
:
240 websocket
.emit('message', {event
: 'topic', nick
: '', channel
: msg
.params
.split(" ")[1], topic
: msg
.trailing
});
243 opts
= msg
.params
.split(" ");
244 params
= {event
: 'mode', nick
: msg
.nick
};
245 switch (opts
.length
) {
247 params
.effected_nick
= opts
[0];
248 params
.mode
= msg
.trailing
;
251 params
.channel
= opts
[0];
252 params
.mode
= opts
[1];
255 params
.channel
= opts
[0];
256 params
.mode
= opts
[1];
257 params
.effected_nick
= opts
[2];
260 websocket
.emit('message', params
);
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');
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)});
273 websocket
.emit('message', {event
: 'msg', nick
: msg
.nick
, ident
: msg
.ident
, hostname
: msg
.hostname
, channel
: msg
.params
.trim(), msg
: msg
.trailing
});
277 caps
= config
.cap_options
;
278 options
= msg
.trailing
.split(" ");
279 switch (_
.first(msg
.params
.split(" "))) {
282 _
.each(_
.intersect(caps
, options
), function (cap
) {
287 ircSocket
.IRC
.CAP
.requested
.push(cap
);
289 if (opts
.length
> 0) {
290 ircSocket
.write('CAP REQ :' + opts
+ '\r\n');
292 ircSocket
.write('CAP END\r\n');
295 /*if (_.include(options, 'tls')) {
296 ircSocket.write('STARTTLS\r\n');
297 ircSocket.IRC.CAP.requested.push('tls');
301 _
.each(options
, function (cap
) {
302 ircSocket
.IRC
.CAP
.enabled
.push(cap
);
304 if (_
.last(msg
.params
.split(" ")) !== '*') {
305 ircSocket
.IRC
.CAP
.requested
= [];
306 ircSocket
.IRC
.CAP
.negotiating
= false;
307 ircSocket
.write('CAP END\r\n');
311 ircSocket
.IRC
.CAP
.requested
= [];
312 ircSocket
.IRC
.CAP
.negotiating
= false;
313 ircSocket
.write('CAP END\r\n');
317 /*case ircNumerics.RPL_STARTTLS:
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);
326 ircSocket = ssl_socket;
328 _.each(listeners, function (listener) {
329 ircSocket.addListener('data', listener);
332 //console.log(ircSocket);
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
});
340 case ircNumerics
.ERR_TOOMANYCHANNELS
:
341 websocket
.emit('message', {event
: 'irc_error', error
: 'too_many_channels', channel
: msg
.params
.split(" ")[1], reason
: msg
.trailing
});
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
});
347 case ircNumerics
.ERR_NOTONCHANNEL
:
348 websocket
.emit('message', {event
: 'irc_error', error
: 'not_on_channel', channel
: msg
.params
.split(" ")[1], reason
: msg
.trailing
});
350 case ircNumerics
.ERR_CHANNELISFULL
:
351 websocket
.emit('message', {event
: 'irc_error', error
: 'channel_is_full', channel
: msg
.params
.split(" ")[1], reason
: msg
.trailing
});
353 case ircNumerics
.ERR_INVITEONLYCHAN
:
354 websocket
.emit('message', {event
: 'irc_error', error
: 'invite_only_channel', channel
: msg
.params
.split(" ")[1], reason
: msg
.trailing
});
356 case ircNumerics
.ERR_BANNEDFROMCHAN
:
357 websocket
.emit('message', {event
: 'irc_error', error
: 'banned_from_channel', channel
: msg
.params
.split(" ")[1], reason
: msg
.trailing
});
359 case ircNumerics
.ERR_BADCHANNELKEY
:
360 websocket
.emit('message', {event
: 'irc_error', error
: 'bad_channel_key', channel
: msg
.params
.split(" ")[1], reason
: msg
.trailing
});
364 console
.log("Unknown command.\r\n");
368 var ircSocketDataHandler = function (data
, websocket
, ircSocket
) {
370 if ((ircSocket
.holdLast
) && (ircSocket
.held
!== '')) {
371 data
= ircSocket
.held
+ data
;
372 ircSocket
.holdLast
= false;
375 if (data
.substr(-2) === '\r\n') {
376 ircSocket
.holdLast
= true;
378 data
= data
.split("\r\n");
379 for (i
= 0; i
< data
.length
; i
++) {
381 if ((ircSocket
.holdLast
) && (i
=== data
.length
- 1)) {
382 ircSocket
.held
= data
[i
];
385 console
.log("->" + data
[i
]);
386 parseIRCMessage(websocket
, ircSocket
, data
[i
]);
391 if (config
.handle_http
) {
392 var fileServer
= new (require('node-static').Server
)(__dirname
+ config
.public_http
);
393 var jade
= require('jade');
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
);
405 } else if (uri
.pathname
=== '/') {
406 useragent
= (response
.headers
) ? response
.headers
['user-agent']: '';
407 if (useragent
.indexOf('android') !== -1) {
410 } else if (useragent
.indexOf('iphone') !== -1) {
413 } else if (useragent
.indexOf('ipad') !== -1) {
416 } else if (useragent
.indexOf('ipod') !== -1) {
424 server_set
= (uri
.query
.server
!== '');
425 server
= uri
.query
.server
|| 'irc.anonnet.org';
426 nick
= uri
.query
.nick
|| '';
427 debug
= (uri
.query
.debug
!== '');
429 server
= 'irc.anonnet.org';
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
) {
436 response
.write(html
);
438 response
.statusCode
= 500;
442 } else if (uri
.pathname
.substr(0, 10) === '/socket.io') {
445 response
.statusCode
= 404;
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
);
457 var httpServer
= http
.createServer(httpHandler
);
458 var io
= ws
.listen(httpServer
, {secure
: false});
459 httpServer
.listen(config
.port
, config
.bind_address
);
462 // Now we're listening on the network, set our UID/GIDs if required
465 io
.of('/kiwi').on('connection', function (websocket
) {
466 websocket
.on('irc connect', function (nick
, host
, port
, ssl
, callback
) {
468 //setup IRC connection
470 ircSocket
= net
.createConnection(port
, host
);
472 ircSocket
= tls
.connect(port
, host
);
474 ircSocket
.setEncoding('ascii');
475 ircSocket
.IRC
= {options
: {}, CAP
: {negotiating
: true, requested
: [], enabled
: []}};
476 websocket
.ircSocket
= ircSocket
;
477 ircSocket
.holdLast
= false;
480 ircSocket
.on('data', function (data
) {
481 ircSocketDataHandler(data
, websocket
, ircSocket
);
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');
490 if ((callback
) && (typeof (callback
) === 'function')) {
494 websocket
.on('message', function (msg
, callback
) {
497 msg
.data
= JSON
.parse(msg
.data
);
498 args
= msg
.data
.args
;
499 switch (msg
.data
.method
) {
501 if ((args
.target
) && (args
.msg
)) {
502 websocket
.ircSocket
.write('PRIVMSG ' + args
.target
+ ' :' + args
.msg
+ '\r\n');
506 if ((args
.target
) && (args
.msg
)) {
507 websocket
.ircSocket
.write('PRIVMSG ' + args
.target
+ ' :\ 1ACTION ' + args
.msg
+ '\ 1\r\n');
511 websocket
.ircSocket
.write(args
.data
+ '\r\n');
515 _
.each(args
.channel
.split(","), function (chan
) {
516 websocket
.ircSocket
.write('JOIN ' + chan
+ '\r\n');
521 websocket
.ircSocket
.end('QUIT :' + args
.message
+ '\r\n');
522 websocket
.sentQUIT
= true;
523 websocket
.ircSocket
.destroySoon();
524 websocket
.disconnect();
527 if ((args
.target
) && (args
.msg
)) {
528 websocket
.ircSocket
.write('NOTICE ' + args
.target
+ ' :' + args
.msg
+ '\r\n');
533 if ((callback
) && (typeof (callback
) === 'function')) {
537 console
.log("Caught error: " + e
);
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();