Removed hard-coded kiwi location
[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 static_server = require('node-static'),
10 ws = require('socket.io'),
11 jade = require('jade'),
12 _ = require('./lib/underscore.min.js'),
13 starttls = require('./lib/starttls.js');
14
15 var config = JSON.parse(fs.readFileSync(__dirname + '/config.json', 'ascii'));
16
17 var ircNumerics = {
18 RPL_WELCOME: '001',
19 RPL_ISUPPORT: '005',
20 RPL_WHOISUSER: '311',
21 RPL_WHOISSERVER: '312',
22 RPL_WHOISOPERATOR: '313',
23 RPL_WHOISIDLE: '317',
24 RPL_ENDOFWHOIS: '318',
25 RPL_WHOISCHANNELS: '319',
26 RPL_TOPIC: '332',
27 RPL_NAMEREPLY: '353',
28 RPL_ENDOFNAMES: '366',
29 RPL_MOTD: '372',
30 RPL_WHOISMODES: '379',
31 ERR_NOSUCHNICK: '401',
32 ERR_LINKCHANNEL: '470',
33 RPL_STARTTLS: '670'
34 };
35
36
37 var parseIRCMessage = function (websocket, ircSocket, data) {
38 /*global ircSocketDataHandler */
39 var msg, regex, opts, options, opt, i, j, matches, nick, users, chan, params, prefix, prefixes, nicklist, caps;
40 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;
41 msg = regex.exec(data);
42 if (msg) {
43 msg = {
44 prefix: msg[1],
45 nick: msg[2],
46 ident: msg[3],
47 hostname: msg[4],
48 command: msg[5],
49 params: msg[6] || '',
50 trailing: (msg[7]) ? msg[7].trim() : ''
51 };
52 switch (msg.command.toUpperCase()) {
53 case 'PING':
54 ircSocket.write('PONG ' + msg.trailing + '\r\n');
55 break;
56 case ircNumerics.RPL_WELCOME:
57 if (ircSocket.IRC.CAP.negotiating) {
58 ircSocket.IRC.CAP.negotiating = false;
59 ircSocket.IRC.CAP.enabled = [];
60 ircSocket.IRC.CAP.requested = [];
61 }
62 websocket.emit('message', {event: 'connect', connected: true, host: null});
63 break;
64 case ircNumerics.RPL_ISUPPORT:
65 opts = msg.params.split(" ");
66 options = [];
67 for (i = 0; i < opts.length; i++) {
68 opt = opts[i].split("=", 2);
69 opt[0] = opt[0].toUpperCase();
70 ircSocket.IRC.options[opt[0]] = opt[1] || true;
71 if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES'], opt[0])) {
72 if (opt[0] === 'PREFIX') {
73 regex = /\(([^)]*)\)(.*)/;
74 matches = regex.exec(opt[1]);
75 if ((matches) && (matches.length === 3)) {
76 ircSocket.IRC.options[opt[0]] = {};
77 for (j = 0; j < matches[2].length; j++) {
78 ircSocket.IRC.options[opt[0]][matches[2].charAt(j)] = matches[1].charAt(j);
79 }
80 }
81 }
82 }
83 }
84 websocket.emit('message', {event: 'options', server: '', "options": ircSocket.IRC.options});
85 break;
86 case ircNumerics.RPL_WHOISUSER:
87 case ircNumerics.RPL_WHOISSERVER:
88 case ircNumerics.RPL_WHOISOPERATOR:
89 case ircNumerics.RPL_ENDOFWHOIS:
90 case ircNumerics.RPL_WHOISCHANNELS:
91 case ircNumerics.RPL_WHOISMODES:
92 websocket.emit('message', {event: 'whois', server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing});
93 break;
94 case ircNumerics.RPL_WHOISIDLE:
95 params = msg.params.split(" ", 3);
96 websocket.emit('message', {event: 'whois', server: '', nick: params[1], "msg": params[2] + ' ' + msg.trailing});
97 case ircNumerics.RPL_MOTD:
98 websocket.emit('message', {event: 'motd', server: '', "msg": msg.trailing});
99 break;
100 case ircNumerics.RPL_NAMEREPLY:
101 params = msg.params.split(" ");
102 nick = params[0];
103 chan = params[2];
104 users = msg.trailing.split(" ");
105 prefixes = _.values(ircSocket.IRC.options.PREFIX);
106 nicklist = {};
107 i = 0;
108 _.each(users, function (user) {
109 if (_.include(prefix, user.charAt(0))) {
110 prefix = user.charAt(0);
111 user = user.substring(1);
112 nicklist[user] = prefix;
113 } else {
114 nicklist[user] = '';
115 }
116 if (i++ >= 50) {
117 websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
118 nicklist = {};
119 i = 0;
120 }
121 });
122 if (i > 0) {
123 websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
124 } else {
125 console.log("oops");
126 }
127 break;
128 case ircNumerics.RPL_ENDOFNAMES:
129 websocket.emit('message', {event: 'userlist_end', server: '', channel: msg.params.split(" ")[1]});
130 break;
131 case ircNumerics.ERR_LINKCHANNEL:
132 params = msg.params.split(" ");
133 websocket.emit('message', {event: 'channel_redirect', from: params[1], to: params[2]});
134 break;
135 case ircNumerics.ERR_NOSUCHNICK:
136 //TODO: shit
137 break;
138 case 'JOIN':
139 websocket.emit('message', {event: 'join', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.trailing});
140 if (msg.nick === ircSocket.IRC.nick) {
141 ircSocket.write('NAMES ' + msg.trailing + '\r\n');
142 }
143 break;
144 case 'PART':
145 websocket.emit('message', {event: 'part', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
146 break;
147 case 'KICK':
148 params = msg.params.split(" ");
149 websocket.emit('message', {event: 'kick', kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
150 break;
151 case 'QUIT':
152 websocket.emit('message', {event: 'quit', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
153 break;
154 case 'NOTICE':
155 websocket.emit('message', {event: 'notice', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
156 break;
157 case 'NICK':
158 websocket.emit('message', {event: 'nick', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
159 break;
160 case 'TOPIC':
161 websocket.emit('message', {event: 'topic', nick: msg.nick, channel: msg.params, topic: msg.trailing});
162 break;
163 case ircNumerics.RPL_TOPIC:
164 websocket.emit('message', {event: 'topic', nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing});
165 break;
166 case 'MODE':
167 opts = msg.params.split(" ");
168 params = {event: 'mode', nick: msg.nick};
169 switch (opts.length) {
170 case 1:
171 params.effected_nick = opts[0];
172 params.mode = msg.trailing;
173 break;
174 case 2:
175 params.channel = opts[0];
176 params.mode = opts[1];
177 break;
178 default:
179 params.channel = opts[0];
180 params.mode = opts[1];
181 params.effected_nick = opts[2];
182 break;
183 }
184 websocket.emit('message', params);
185 break;
186 case 'PRIVMSG':
187 websocket.emit('message', {event: 'msg', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
188 break;
189 case 'CAP':
190 caps = config.cap_options;
191 options = msg.trailing.split(" ");
192 switch (_.first(msg.params.split(" "))) {
193 case 'LS':
194 opts = '';
195 _.each(_.intersect(caps, options), function (cap) {
196 if (opts !== '') {
197 opts += " ";
198 }
199 opts += cap;
200 ircSocket.IRC.CAP.requested.push(cap);
201 });
202 if (opts.length > 0) {
203 ircSocket.write('CAP REQ :' + opts + '\r\n');
204 } else {
205 ircSocket.write('CAP END\r\n');
206 }
207 // TLS is special
208 /*if (_.include(options, 'tls')) {
209 ircSocket.write('STARTTLS\r\n');
210 ircSocket.IRC.CAP.requested.push('tls');
211 }*/
212 break;
213 case 'ACK':
214 _.each(options, function (cap) {
215 ircSocket.IRC.CAP.enabled.push(cap);
216 });
217 if (_.last(msg.params.split(" ")) !== '*') {
218 ircSocket.IRC.CAP.requested = [];
219 ircSocket.IRC.CAP.negotiating = false;
220 ircSocket.write('CAP END\r\n');
221 }
222 break;
223 case 'NAK':
224 ircSocket.IRC.CAP.requested = [];
225 ircSocket.IRC.CAP.negotiating = false;
226 ircSocket.write('CAP END\r\n');
227 break;
228 }
229 break;
230 /*case ircNumerics.RPL_STARTTLS:
231 try {
232 IRC = ircSocket.IRC;
233 listeners = ircSocket.listeners('data');
234 ircSocket.removeAllListeners('data');
235 ssl_socket = starttls(ircSocket, {}, function () {
236 ssl_socket.on("data", function (data) {
237 ircSocketDataHandler(data, websocket, ssl_socket);
238 });
239 ircSocket = ssl_socket;
240 ircSocket.IRC = IRC;
241 _.each(listeners, function (listener) {
242 ircSocket.addListener('data', listener);
243 });
244 });
245 //console.log(ircSocket);
246 } catch (e) {
247 console.log(e);
248 }
249 break;*/
250 }
251 } else {
252 console.log("Unknown command.\r\n");
253 }
254 };
255
256 var ircSocketDataHandler = function (data, websocket, ircSocket) {
257 var i;
258 if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
259 data = ircSocket.held + data;
260 ircSocket.holdLast = false;
261 ircSocket.held = '';
262 }
263 if (data.substr(-2) === '\r\n') {
264 ircSocket.holdLast = true;
265 }
266 data = data.split("\r\n");
267 for (i = 0; i < data.length; i++) {
268 if (data[i]) {
269 if ((ircSocket.holdLast) && (i === data.length - 1)) {
270 ircSocket.held = data[i];
271 break;
272 }
273 console.log("->" + data[i]);
274 parseIRCMessage(websocket, ircSocket, data[i]);
275 }
276 }
277 };
278
279 var fileServer = new (static_server.Server)(__dirname + '/client');
280
281 var httpHandler = function (request, response) {
282 var uri, subs, useragent, agent, server_set, server, nick, debug, touchscreen;
283 if (config.handle_http) {
284 uri = url.parse(request.url);
285 subs = uri.pathname.substr(0, 4);
286 if ((subs === '/js/') || (subs === '/css') || (subs === '/img')) {
287 request.addListener('end', function () {
288 fileServer.serve(request, response);
289 });
290 } else if (uri.pathname === '/') {
291 useragent = (response.headers) ? response.headers['user-agent']: '';
292 if (useragent.indexOf('android') !== -1) {
293 agent = 'android';
294 touchscreen = true;
295 } else if (useragent.indexOf('iphone') !== -1) {
296 agent = 'iphone';
297 touchscreen = true;
298 } else if (useragent.indexOf('ipad') !== -1) {
299 agent = 'ipad';
300 touchscreen = true;
301 } else if (useragent.indexOf('ipod') !== -1) {
302 agent = 'ipod';
303 touchscreen = true;
304 } else {
305 agent = 'normal';
306 touchscreen = false;
307 }
308 if (uri.query) {
309 server_set = (uri.query.server !== '');
310 server = uri.query.server || 'irc.anonnet.org';
311 nick = uri.query.nick || '';
312 debug = (uri.query.debug !== '');
313 } else {
314 server = 'irc.anonnet.org';
315 nick = '';
316 }
317 response.setHeader('Connection', 'close');
318 response.setHeader('X-Generated-By', 'KiwiIRC');
319 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) {
320 if (!err) {
321 response.write(html);
322 } else {
323 response.statusCode = 500;
324 }
325 response.end();
326 });
327 } else if (uri.pathname.substr(0, 10) === '/socket.io') {
328 // Do nothing!
329 } else {
330 response.statusCode = 404;
331 response.end();
332 }
333 }
334 };
335
336 //setup websocket listener
337 if (config.listen_ssl) {
338 var httpServer = https.createServer({key: fs.readFileSync(__dirname + '/' + config.ssl_key), cert: fs.readFileSync(__dirname + '/' + config.ssl_cert)}, httpHandler);
339 var io = ws.listen(httpServer, {secure: true});
340 httpServer.listen(config.port, config.bind_address);
341 } else {
342 var httpServer = http.createServer(httpHandler);
343 var io = ws.listen(httpServer, {secure: false});
344 httpServer.listen(config.port, config.bind_address);
345 }
346 io.of('/kiwi').on('connection', function (websocket) {
347 websocket.on('irc connect', function (nick, host, port, ssl, callback) {
348 var ircSocket;
349 //setup IRC connection
350 if (!ssl) {
351 ircSocket = net.createConnection(port, host);
352 } else {
353 ircSocket = tls.connect(port, host);
354 }
355 ircSocket.setEncoding('ascii');
356 ircSocket.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}};
357 websocket.ircSocket = ircSocket;
358 ircSocket.holdLast = false;
359 ircSocket.held = '';
360
361 ircSocket.on('data', function (data) {
362 ircSocketDataHandler(data, websocket, ircSocket);
363 });
364
365 ircSocket.IRC.nick = nick;
366 // Send the login data
367 ircSocket.write('CAP LS\r\n');
368 ircSocket.write('NICK ' + nick + '\r\n');
369 ircSocket.write('USER ' + nick + '_kiwi 0 0 :' + nick + '\r\n');
370
371 if ((callback) && (typeof (callback) === 'function')) {
372 callback();
373 }
374 });
375 websocket.on('message', function (msg, callback) {
376 var args;
377 try {
378 msg.data = JSON.parse(msg.data);
379 args = msg.data.args;
380 switch (msg.data.method) {
381 case 'msg':
382 if ((args.target) && (args.msg)) {
383 websocket.ircSocket.write('PRIVMSG ' + args.target + ' :' + args.msg + '\r\n');
384 }
385 break;
386 case 'action':
387 if ((args.target) && (args.msg)) {
388 websocket.ircSocket.write('PRIVMSG ' + args.target + ' :\ 1ACTION ' + args.msg + '\ 1\r\n');
389 }
390 break;
391 case 'raw':
392 websocket.ircSocket.write(args.data + '\r\n');
393 break;
394 case 'join':
395 if (args.channel) {
396 _.each(args.channel.split(","), function (chan) {
397 websocket.ircSocket.write('JOIN ' + chan + '\r\n');
398 });
399 }
400 break;
401 case 'quit':
402 websocket.ircSocket.end('QUIT :' + args.message + '\r\n');
403 websocket.sentQUIT = true;
404 websocket.ircSocket.destroySoon();
405 websocket.disconnect();
406 break;
407 case 'notice':
408 if ((args.target) && (args.msg)) {
409 websocket.ircSocket.write('NOTICE ' + args.target + ' :' + args.msg + '\r\n');
410 }
411 break;
412 default:
413 }
414 if ((callback) && (typeof (callback) === 'function')) {
415 callback();
416 }
417 } catch (e) {
418 console.log("Caught error: " + e);
419 }
420 });
421 websocket.on('disconnect', function () {
422 if ((!websocket.sentQUIT) && (websocket.ircSocket)) {
423 websocket.ircSocket.end('QUIT :' + config.quit_message + '\r\n');
424 websocket.sentQUIT = true;
425 websocket.ircSocket.destroySoon();
426 }
427 });
428 });
429