Cache clearing, default channel bug fix, embedded image clicking bug fix
[KiwiIRC.git] / node / kiwi.js
index 05a0e0b3e4c04b9d9f86dbd2827fc2c1f94fb92f..189301f414747387b51b654a378e4a9f2e1a9d40 100644 (file)
-/*jslint regexp: true, confusion: true, undef: false, node: true, sloppy: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
-
+/*jslint continue: true, forin: true, regexp: true, undef: false, node: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */
+"use strict";
 var tls = require('tls'),
     net = require('net'),
     http = require('http'),
+    https = require('https'),
     fs = require('fs'),
+    url = require('url'),
+    dns = require('dns'),
+    crypto = require('crypto'),
     ws = require('socket.io'),
+    jsp = require("uglify-js").parser,
+    pro = require("uglify-js").uglify,
     _ = require('./lib/underscore.min.js'),
-    starttls = require('./lib/starttls.js');
-
-var config = JSON.parse(fs.readFileSync(__dirname + '/config.json', 'ascii'));
-
-var ircNumerics = {
-    RPL_WELCOME:        '001',
-    RPL_ISUPPORT:       '005',
-    RPL_WHOISUSER:      '311',
-    RPL_WHOISSERVER:    '312',
-    RPL_WHOISOPERATOR:  '313',
-    RPL_WHOISIDLE:      '317',
-    RPL_ENDOFWHOIS:     '318',
-    RPL_WHOISCHANNELS:  '319',
-    RPL_TOPIC:          '332',
-    RPL_NAMEREPLY:      '353',
-    RPL_ENDOFNAMES:     '366',
-    RPL_MOTD:           '372',
-    RPL_WHOISMODES:     '379',
-    ERR_NOSUCHNICK:     '401',
-    ERR_LINKCHANNEL:    '470',
-    RPL_STARTTLS:       '670'
-};
+    starttls = require('./lib/starttls.js'),
+    app = require(__dirname + '/app.js');
 
 
-var parseIRCMessage = function (websocket, ircSocket, data) {
-    /*global ircSocketDataHandler */
-    var msg, regex, opts, options, opt, i, j, matches, nick, users, chan, params, prefix, prefixes, nicklist, caps;
-    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;
-    msg = regex.exec(data);
-    if (msg) {
-        msg = {
-            prefix:     msg[1],
-            nick:       msg[2],
-            ident:      msg[3],
-            hostname:   msg[4],
-            command:    msg[5],
-            params:     msg[6] || '',
-            trailing:   (msg[7]) ? msg[7].trim() : ''
-        };
-        switch (msg.command.toUpperCase()) {
-        case 'PING':
-            ircSocket.write('PONG ' + msg.trailing + '\r\n');
-            break;
-        case ircNumerics.RPL_WELCOME:
-            if (ircSocket.IRC.CAP.negotiating) {
-                ircSocket.IRC.CAP.negotiating = false;
-                ircSocket.IRC.CAP.enabled = [];
-                ircSocket.IRC.CAP.requested = [];
-            }
-            websocket.emit('message', {event: 'connect', connected: true, host: null});
-            break;
-        case ircNumerics.RPL_ISUPPORT:
-            opts = msg.params.split(" ");
-            options = [];
-            for (i = 0; i < opts.length; i++) {
-                opt = opts[i].split("=", 2);
-                opt[0] = opt[0].toUpperCase();
-                ircSocket.IRC.options[opt[0]] = opt[1] || true;
-                if (_.include(['NETWORK', 'PREFIX', 'CHANTYPES'], opt[0])) {
-                    if (opt[0] === 'PREFIX') {
-                        regex = /\(([^)]*)\)(.*)/;
-                        matches = regex.exec(opt[1]);
-                        if ((matches) && (matches.length === 3)) {
-                            ircSocket.IRC.options[opt[0]] = {};
-                            for (j = 0; j < matches[2].length; j++) {
-                                ircSocket.IRC.options[opt[0]][matches[2].charAt(j)] = matches[1].charAt(j);
-                            }
-                        }
+
+
+
+// Libraries may need to know kiwi.js path as __dirname
+// only gives that librarys path. Set it here for usage later.
+this.kiwi_root = __dirname;
+
+
+
+/*
+ * Configuration and rehashing routines
+ */
+var config_filename = 'config.json',
+    config_dirs = ['/etc/kiwiirc/', this.kiwi_root + '/'];
+
+this.config = {};
+this.loadConfig = function () {
+    var i, j,
+        nconf = {},
+        cconf = {},
+        found_config = false;
+    
+    for (i in config_dirs) {
+        try {
+            if (fs.lstatSync(config_dirs[i] + config_filename).isDirectory() === false) {
+                found_config = true;
+                nconf = JSON.parse(fs.readFileSync(config_dirs[i] + config_filename, 'ascii'));
+                for (j in nconf) {
+                    // If this has changed from the previous config, mark it as changed
+                    if (!_.isEqual(this.config[j], nconf[j])) {
+                        cconf[j] = nconf[j];
                     }
+
+                    this.config[j] = nconf[j];
                 }
-            }
-            websocket.emit('message', {event: 'options', server: '', "options": ircSocket.IRC.options});
-            break;
-        case ircNumerics.RPL_WHOISUSER:
-        case ircNumerics.RPL_WHOISSERVER:
-        case ircNumerics.RPL_WHOISOPERATOR:
-        case ircNumerics.RPL_WHOISIDLE:
-        case ircNumerics.RPL_ENDOFWHOIS:
-        case ircNumerics.RPL_WHOISCHANNELS:
-        case ircNumerics.RPL_WHOISMODES:
-            websocket.emit('message', {event: 'whois', server: '', nick: msg.params.split(" ", 3)[1], "msg": msg.trailing});
-            break;
-        case ircNumerics.RPL_MOTD:
-            websocket.emit('message', {event: 'motd', server: '', "msg": msg.trailing});
-            break;
-        case ircNumerics.RPL_NAMEREPLY:
-            params = msg.params.split(" ");
-            nick = params[0];
-            chan = params[2];
-            users = msg.trailing.split(" ");
-            prefixes = _.values(ircSocket.IRC.options.PREFIX);
-            nicklist = {};
-            i = 0;
-            _.each(users, function (user) {
-                if (_.include(prefix, user.charAt(0))) {
-                    prefix = user.charAt(0);
-                    user = user.substring(1);
-                    nicklist[user] = prefix;
-                } else {
-                    nicklist[user] = '';
-                }
-                if (i++ >= 50) {
-                    websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
-                    nicklist = {};
-                    i = 0;
-                }
-            });
-            if (i > 0) {
-                websocket.emit('message', {event: 'userlist', server: '', "users": nicklist, channel: chan});
-            } else {
-                console.log("oops");
-            }
-            break;
-        case ircNumerics.RPL_ENDOFNAMES:
-            websocket.emit('message', {event: 'userlist_end', server: '', channel: msg.params.split(" ")[1]});
-            break;
-        case ircNumerics.ERR_LINKCHANNEL:
-            params = msg.params.split(" "); 
-            websocket.emit('message', {event: 'channel_redirect', from: params[1], to: params[2]});
-            break;
-        case ircNumerics.ERR_NOSUCHNICK:
-                       //TODO: shit
-                       break;
-        case 'JOIN':
-            websocket.emit('message', {event: 'join', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.trailing});
-            if (msg.nick === ircSocket.IRC.nick) {
-                ircSocket.write('NAMES ' + msg.trailing + '\r\n');
-            }
-            break;
-        case 'PART':
-            websocket.emit('message', {event: 'part', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), message: msg.trailing});
-            break;
-        case 'KICK':
-            params = msg.params.split(" ");
-            websocket.emit('message', {event: 'kick', kicked: params[1], nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: params[0].trim(), message: msg.trailing});
-            break;
-        case 'QUIT':
-            websocket.emit('message', {event: 'quit', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, message: msg.trailing});
-            break;
-        case 'NOTICE':
-            websocket.emit('message', {event: 'notice', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
-            break;
-        case 'NICK':
-            websocket.emit('message', {event: 'nick', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, newnick: msg.trailing});
-            break;
-        case 'TOPIC':
-            websocket.emit('message', {event: 'topic', nick: msg.nick, channel: msg.params, topic: msg.trailing});
-            break;
-        case ircNumerics.RPL_TOPIC:
-            websocket.emit('message', {event: 'topic', nick: '', channel: msg.params.split(" ")[1], topic: msg.trailing});
-            break;
-        case 'MODE':
-            opts = msg.params.split(" ");
-            params = {event: 'mode', nick: msg.nick};
-            switch (opts.length) {
-            case 1:
-                params.effected_nick = opts[0];
-                params.mode = msg.trailing;
-                break;
-            case 2:
-                params.channel = opts[0];
-                params.mode = opts[1];
-                break;
-            default:
-                params.channel = opts[0];
-                params.mode = opts[1];
-                params.effected_nick = opts[2];
+
+                console.log('Loaded config file ' + config_dirs[i] + config_filename);
                 break;
             }
-            websocket.emit('message', params);
-            break;
-        case 'PRIVMSG':
-            websocket.emit('message', {event: 'msg', nick: msg.nick, ident: msg.ident, hostname: msg.hostname, channel: msg.params.trim(), msg: msg.trailing});
-            break;
-        case 'CAP':
-            caps = config.cap_options;
-            options = msg.trailing.split(" ");
-            switch (_.first(msg.params.split(" "))) {
-            case 'LS':
-                opts = '';
-                _.each(_.intersect(caps, options), function (cap) {
-                    if (opts !== '') {
-                        opts += " ";
-                    }
-                    opts += cap;
-                    ircSocket.IRC.CAP.requested.push(cap);
-                });
-                if (opts.length > 0) {
-                    ircSocket.write('CAP REQ :' + opts + '\r\n');
-                } else {
-                    ircSocket.write('CAP END\r\n');
-                }
-                // TLS is special
-                /*if (_.include(options, 'tls')) {
-                    ircSocket.write('STARTTLS\r\n');
-                    ircSocket.IRC.CAP.requested.push('tls');
-                }*/
-                break;
-            case 'ACK':
-                _.each(options, function (cap) {
-                    ircSocket.IRC.CAP.enabled.push(cap);
-                });
-                if (_.last(msg.params.split(" ")) !== '*') {
-                    ircSocket.IRC.CAP.requested = [];
-                    ircSocket.IRC.CAP.negotiating = false;
-                    ircSocket.write('CAP END\r\n');
-                }
-                break;
-            case 'NAK':
-                ircSocket.IRC.CAP.requested = [];
-                ircSocket.IRC.CAP.negotiating = false;
-                ircSocket.write('CAP END\r\n');
+        } catch (e) {
+            switch (e.code) {
+            case 'ENOENT':      // No file/dir
                 break;
+            default:
+                console.log('An error occured parsing the config file ' + config_dirs[i] + config_filename + ': ' + e.message);
+                return false;
             }
-            break;
-        /*case ircNumerics.RPL_STARTTLS:
-            try {
-                IRC = ircSocket.IRC;
-                listeners = ircSocket.listeners('data');
-                ircSocket.removeAllListeners('data');
-                ssl_socket = starttls(ircSocket, {}, function () {
-                    ssl_socket.on("data", function (data) {
-                        ircSocketDataHandler(data, websocket, ssl_socket);
-                    });
-                    ircSocket = ssl_socket;
-                    ircSocket.IRC = IRC;
-                    _.each(listeners, function (listener) {
-                        ircSocket.addListener('data', listener);
-                    });
-                });
-                //console.log(ircSocket);
-            } catch (e) {
-                console.log(e);
-            }
-            break;*/
+            continue;
         }
-    } else {
-        console.log("Unknown command.\r\n");
     }
-};
-
-var ircSocketDataHandler = function (data, websocket, ircSocket) {
-    var i;
-    if ((ircSocket.holdLast) && (ircSocket.held !== '')) {
-        data = ircSocket.held + data;
-        ircSocket.holdLast = false;
-        ircSocket.held = '';
-    }
-    if (data.substr(-2) === '\r\n') {
-        ircSocket.holdLast = true;
-    }
-    data = data.split("\r\n");         
-    for (i = 0; i < data.length; i++) {
-        if (data[i]) {
-            if ((ircSocket.holdLast) && (i === data.length - 1)) {
-                ircSocket.held = data[i];
-                break;
-            }
-            console.log("->" + data[i]);
-            parseIRCMessage(websocket, ircSocket, data[i]);
+    if (Object.keys(this.config).length === 0) {
+        if (!found_config) {
+            console.log('Couldn\'t find a config file!');
         }
+        return false;
     }
+    return [nconf, cconf];
 };
 
-//setup websocket listener
-if (config.listen_ssl) {
-    var io = ws.listen('127.0.0.1:'+config.port.toString(), {secure: true, key: fs.readFileSync(__dirname + '/' + config.ssl_key), cert: fs.readFileSync(__dirname + '/' + config.ssl_cert)});
-} else {
-    var io = ws.listen('127.0.0.1:'+config.port.toString(), {secure: false});
+
+// Reloads the config during runtime
+this.rehash = function () {
+    return app.rehash();
 }
-io.sockets.on('connection', function (websocket) {
-    websocket.on('irc connect', function (nick, host, port, ssl, callback) {
-        var ircSocket;
-        //setup IRC connection
-        if (!ssl) {
-            ircSocket = net.createConnection(port, host);
-        } else {
-            ircSocket = tls.connect(port, host);
-        }
-        ircSocket.setEncoding('ascii');
-        ircSocket.IRC = {options: {}, CAP: {negotiating: true, requested: [], enabled: []}};
-        websocket.ircSocket = ircSocket;
-        ircSocket.holdLast = false;
-        ircSocket.held = '';
-        
-        ircSocket.on('data', function (data) {
-            ircSocketDataHandler(data, websocket, ircSocket);
-        });
-        
-        ircSocket.IRC.nick = nick;
-        // Send the login data
-        ircSocket.write('CAP LS\r\n');
-        ircSocket.write('NICK ' + nick + '\r\n');
-        ircSocket.write('USER ' + nick + '_kiwi 0 0 :' + nick + '\r\n');
-        
-        if ((callback) && (typeof (callback) === 'function')) {
-            callback();
-        }
-    });
-    websocket.on('message', function (msg, callback) {
-        var args;
-        try {
-            msg.data = JSON.parse(msg.data);
-            args = msg.data.args;
-            switch (msg.data.method) {
-            case 'msg':
-                if ((args.target) && (args.msg)) {
-                    websocket.ircSocket.write('PRIVMSG ' + args.target + ' :' + args.msg + '\r\n');
-                }
-                break;
-               case 'action':
-                if ((args.target) && (args.msg)) {
-                    websocket.ircSocket.write('PRIVMSG ' + args.target + ' :\ 1ACTION ' + args.msg + '\ 1\r\n');
-                }
-                break;
-               case 'raw':
-                websocket.ircSocket.write(args.data + '\r\n');
-                break;
-               case 'join':
-                if (args.channel) {
-                    _.each(args.channel.split(","), function (chan) {
-                        websocket.ircSocket.write('JOIN ' + chan + '\r\n');
-                    });
-                }
-                break;
-               case 'quit':
-                websocket.ircSocket.end('QUIT :' + args.message + '\r\n');
-                websocket.sentQUIT = true;
-                websocket.ircSocket.destroySoon();
-                websocket.disconnect();
-                break;
-            case 'notice':
-                if ((args.target) && (args.msg)) {
-                    websocket.ircSocket.write('NOTICE ' + args.target + ' :' + args.msg + '\r\n');
-                }
-                break;
-            default:
-            }
-            if ((callback) && (typeof (callback) === 'function')) {
-                callback();
-            }
-        } catch (e) {
-            console.log("Caught error: " + e);
-        }
-    });
-    websocket.on('disconnect', function () {
-        if ((!websocket.sentQUIT) && (websocket.ircSocket)) {
-            websocket.ircSocket.end('QUIT :' + config.quit_message + '\r\n');
-            websocket.sentQUIT = true;
-            websocket.ircSocket.destroySoon();
-        }
-    });
-});
+
+// Reloads app.js during runtime for any recoding
+this.recode = function () {
+    if (typeof require.cache[this.kiwi_root + '/app.js'] !== 'undefined'){
+        delete require.cache[this.kiwi_root + '/app.js'];
+    }
+
+    app = null;
+    app = require(__dirname + '/app.js');
+
+    var objs = {tls:tls, net:net, http:http, https:https, fs:fs, url:url, dns:dns, crypto:crypto, ws:ws, jsp:jsp, pro:pro, _:_, starttls:starttls};
+    app.init(objs);
+
+    return true;
+}
+
+
+
+
+
+
+/*
+ * Before we continue we need the config loaded
+ */
+if (!this.loadConfig()) {
+    process.exit(0);
+}
+
+
+
+
+
+
+
+/*
+ * HTTP file serving
+ */
+if (this.config.handle_http) {
+    this.fileServer = new (require('node-static').Server)(__dirname + this.config.public_http);
+    this.jade = require('jade');
+    this.cache = {alljs: '', html: []};
+}
+this.httpServer = null;
+this.httpHandler = function (request, response) {
+    return app.httpHandler(request, response);
+}
+
+
+
+
+
+
+/*
+ * Websocket handling
+ */
+this.connections = {};
+this.io = null;
+this.websocketListen = function (port, host, handler, secure, key, cert) {
+    return app.websocketListen(port, host, handler, secure, key, cert);
+}
+this.websocketConnection = function (websocket) {
+    return app.websocketConnection(websocket);
+}
+this.websocketDisconnect = function () {
+    return app.websocketDisconnect(this);
+}
+this.websocketMessage = function (msg, callback) {
+    return app.websocketMessage(this, msg, callback);
+}
+this.websocketIRCConnect = function (nick, host, port, ssl, callback) {
+    return app.websocketIRCConnect(this, nick, host, port, ssl, callback);
+}
+
+
+
+
+/*
+ * IRC handling
+ */
+this.parseIRCMessage = function (websocket, ircSocket, data) {
+    return app.parseIRCMessage(websocket, ircSocket, data);
+}
+this.ircSocketDataHandler = function (data, websocket, ircSocket) {
+    return app.ircSocketDataHandler(data, websocket, ircSocket);
+}
+
+
+
+
+
+
+
+
+
+/*
+ * Load up main application source
+ */
+if (!this.recode()) {
+    process.exit(0);
+}
+
+
+
+// Set the process title
+app.setTitle();
+
+
+
+/*
+ * Load the modules as set in the config and print them out
+ */
+this.kiwi_mod = require('./lib/kiwi_mod.js');
+this.kiwi_mod.loadModules(this.kiwi_root, this.config);
+this.kiwi_mod.printMods();
+
+
+
+
+// Start the server up
+this.websocketListen(this.config.port, this.config.bind_address, this.httpHandler, this.config.listen_ssl, this.config.ssl_key, this.config.ssl_cert);
+
+// Now we're listening on the network, set our UID/GIDs if required
+app.changeUser();
+
+// Listen for controll messages
+app.startControll();
+
+