Jack Allnutt Update engine.io(-client) from 1.3.1 to 1.4.0
[KiwiIRC.git] / server / kiwi.js
index ad620e844ea96d42466b68a71e68f6079f315822..58df8106a84b64f3c9f39b172137cdd33d372dff 100755 (executable)
 var fs          = require('fs'),
-    _           = require('underscore'),
-    WebListener = require('./weblistener.js');
+    _           = require('lodash'),
+    util        = require('util'),
+    winston     = require('winston'),
+    WebListener = require('./weblistener.js'),
+    config      = require('./configuration.js'),
+    modules     = require('./modules.js'),
+    Identd      = require('./identd.js'),
+    Proxy       = require('./proxy.js'),
+    ControlInterface = require('./controlinterface.js');
 
 
 
-/*
- * Config loading
- */
+process.chdir(__dirname + '/../');
 
-var config_filename = 'config.js',
-    config_dirs = ['/etc/kiwiirc/', __dirname + '/'];
+// Get our own version from package.json
+global.build_version = require('../package.json').version;
 
-var config;
+// Load the config, using -c argument if available
+(function (argv) {
+    var conf_switch = argv.indexOf('-c');
+    if (conf_switch !== -1) {
+        if (argv[conf_switch + 1]) {
+            return config.loadConfig(argv[conf_switch + 1]);
+        }
+    }
 
-function loadConfig() {
-    var new_config,
-        conf_filepath;
+    config.loadConfig();
 
-    // Loop through the possible config paths and find a usable one
-    for (var i in config_dirs) {
-        conf_filepath = config_dirs[i] + config_filename;
+})(process.argv);
 
-        try {
-            if (fs.lstatSync(conf_filepath).isFile() === true) {
-                // Clear the loaded config cache
-                delete require.cache[require.resolve(conf_filepath)];
 
-                // Try load the new config file
-                new_config = require(conf_filepath).production;
-                console.log('Loaded config file ' + config_dirs[i] + config_filename);
-                break;
-            }
-        } 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;
-            }
-            continue;
+// If we're not running in the forground and we have a log file.. switch
+// console.log to output to a file
+if (process.argv.indexOf('-f') === -1 && global.config && global.config.log) {
+    (function () {
+        var log_file_name = global.config.log;
+
+        if (log_file_name[0] !== '/') {
+            log_file_name = __dirname + '/../' + log_file_name;
         }
-    }
 
-    return new_config;
+        winston.add(winston.transports.File, {
+            filename: log_file_name,
+            json: false,
+            timestamp: function() {
+                var year, month, day, time_string,
+                    d = new Date();
+
+                year = String(d.getFullYear());
+                month = String(d.getMonth() + 1);
+                if (month.length === 1) {
+                    month = "0" + month;
+                }
+
+                day = String(d.getDate());
+                if (day.length === 1) {
+                    day = "0" + day;
+                }
+
+                // Take the time from the existing toTimeString() format
+                time_string = (new Date()).toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1");
+
+                return year + "-" + month + "-" + day + ' ' + time_string;
+            }
+        });
+
+        winston.remove(winston.transports.Console);
+    })();
 }
 
 
-config = loadConfig() || Object.create(null);
 
 // Make sure we have a valid config file and at least 1 server
-if (Object.keys(config).length === 0) {
-    console.log('Couldn\'t find a valid config file!');
+if (!global.config || Object.keys(global.config).length === 0) {
+    winston.error('Couldn\'t find a valid config.js file (Did you copy the config.example.js file yet?)');
     process.exit(1);
 }
 
-if ((!config.servers) || (config.servers.length < 1)) {
-    console.log('No servers defined in config file');
+if ((!global.config.servers) || (global.config.servers.length < 1)) {
+    winston.error('No servers defined in config file');
     process.exit(2);
 }
 
 
 
 
+// Create a plugin interface
+global.modules = new modules.Publisher();
+
+// Register as the active interface
+modules.registerPublisher(global.modules);
+
+// Load any modules in the config
+if (global.config.module_dir) {
+    (global.config.modules || []).forEach(function (module_name) {
+        if (modules.load(module_name)) {
+            winston.info('Module %s loaded successfully', module_name);
+        } else {
+            winston.warn('Module %s failed to load', module_name);
+        }
+    });
+}
+
+
+
+
+// Holder for all the connected clients
+global.clients = {
+    clients: Object.create(null),
+    addresses: Object.create(null),
+
+    // Local and foriegn port pairs for identd lookups
+    // {'65483_6667': client_obj, '54356_6697': client_obj}
+    port_pairs: {},
+
+    add: function (client) {
+        this.clients[client.hash] = client;
+        if (typeof this.addresses[client.real_address] === 'undefined') {
+            this.addresses[client.real_address] = Object.create(null);
+        }
+        this.addresses[client.real_address][client.hash] = client;
+    },
+
+    remove: function (client) {
+        if (typeof this.clients[client.hash] !== 'undefined') {
+            delete this.clients[client.hash];
+            delete this.addresses[client.real_address][client.hash];
+            if (Object.keys(this.addresses[client.real_address]).length < 1) {
+                delete this.addresses[client.real_address];
+            }
+        }
+    },
+
+    numOnAddress: function (addr) {
+        if (typeof this.addresses[addr] !== 'undefined') {
+            return Object.keys(this.addresses[addr]).length;
+        } else {
+            return 0;
+        }
+    },
+
+    broadcastKiwiCommand: function (command, data, callback) {
+        var clients = [];
+
+        // Get an array of clients for us to work with
+        for (var client in global.clients.clients) {
+            clients.push(global.clients.clients[client]);
+        }
+
+
+        // Sending of the command in batches
+        var sendCommandBatch = function (list) {
+            var batch_size = 100,
+                cutoff;
+
+            if (list.length >= batch_size) {
+                // If we have more clients than our batch size, call ourself with the next batch
+                setTimeout(function () {
+                    sendCommandBatch(list.slice(batch_size));
+                }, 200);
+
+                cutoff = batch_size;
+
+            } else {
+                cutoff = list.length;
+            }
+
+            list.slice(0, cutoff).forEach(function (client) {
+                if (!client.disposed) {
+                    client.sendKiwiCommand(command, data);
+                }
+            });
+
+            if (cutoff === list.length && typeof callback === 'function') {
+                callback();
+            }
+        };
+
+        sendCommandBatch(clients);
+    }
+};
+
+global.servers = {
+    servers: Object.create(null),
+
+    addConnection: function (connection) {
+        var host = connection.irc_host.hostname;
+        if (!this.servers[host]) {
+            this.servers[host] = [];
+        }
+        this.servers[host].push(connection);
+    },
+
+    removeConnection: function (connection) {
+        var host = connection.irc_host.hostname
+        if (this.servers[host]) {
+            this.servers[host] = _.without(this.servers[host], connection);
+            if (this.servers[host].length === 0) {
+                delete this.servers[host];
+            }
+        }
+    },
+
+    numOnHost: function (host) {
+        if (this.servers[host]) {
+            return this.servers[host].length;
+        } else {
+            return 0;
+        }
+    }
+};
+
+
+
+/**
+ * When a new config is loaded, send out an alert to the clients so
+ * so they can reload it
+ */
+config.on('loaded', function () {
+    global.clients.broadcastKiwiCommand('reconfig');
+});
+
+
+
+/*
+ * Identd server
+ */
+if (global.config.identd && global.config.identd.enabled) {
+    var identd_resolve_user = function(port_here, port_there) {
+        var key = port_here.toString() + '_' + port_there.toString();
+
+        if (typeof global.clients.port_pairs[key] == 'undefined') {
+            return;
+        }
+
+        return global.clients.port_pairs[key].username;
+    };
+
+    var identd_server = new Identd({
+        bind_addr: global.config.identd.address,
+        bind_port: global.config.identd.port,
+        user_id: identd_resolve_user
+    });
+
+    identd_server.start();
+}
+
+
+
 
 /*
  * Web listeners
  */
 
-// Holder for all the connected clients
-// TODO: Change from an array to an object. Generate sha1 hash within the client
-// and use that as the key. (Much less work involved in removing a client)
-var clients = [];
 
 // Start up a weblistener for each found in the config
-_.each(config.servers, function (server) {
-    var wl = new WebListener(server, config.transports);
-    wl.on('connection', function (client) {
-        clients.push(client);
-    });
-    wl.on('destroy', function (client) {
-        clients = _.reject(clients, function (c) {
-            return client === c;
+_.each(global.config.servers, function (server) {
+    if (server.type == 'proxy') {
+        // Start up a kiwi proxy server
+        var serv = new Proxy.ProxyServer();
+        serv.listen(server.port, server.address, server);
+
+        serv.on('listening', function() {
+            winston.info('Kiwi proxy listening on %s:%s %s SSL', server.address, server.port, (server.ssl ? 'with' : 'without'));
         });
-    });
+
+        serv.on('socket_connected', function(pipe) {
+            // SSL connections have the raw socket as a property
+            var socket = pipe.irc_socket.socket ?
+                    pipe.irc_socket.socket :
+                    pipe.irc_socket;
+
+            pipe.identd_pair = socket.localPort.toString() + '_' + socket.remotePort.toString();
+            global.clients.port_pairs[pipe.identd_pair] = pipe.meta;
+        });
+
+        serv.on('connection_close', function(pipe) {
+            delete global.clients.port_pairs[pipe.identd_pair];
+        });
+
+    } else {
+        // Start up a kiwi web server
+        var wl = new WebListener(server, global.config.transports);
+
+        wl.on('connection', function (client) {
+            clients.add(client);
+        });
+
+        wl.on('client_dispose', function (client) {
+            clients.remove(client);
+        });
+
+        wl.on('listening', function () {
+            winston.info('Listening on %s:%s %s SSL', server.address, server.port, (server.ssl ? 'with' : 'without'));
+            webListenerRunning();
+        });
+
+        wl.on('error', function (err) {
+            winston.info('Error listening on %s:%s: %s', server.address, server.port, err.code);
+            // TODO: This should probably be refactored. ^JA
+            webListenerRunning();
+        });
+    }
 });
 
+// Once all the listeners are listening, set the processes UID/GID
+var num_listening = 0;
+function webListenerRunning() {
+    num_listening++;
+    if (num_listening === global.config.servers.length) {
+        setProcessUid();
+    }
+}
 
 
 
@@ -98,42 +324,39 @@ _.each(config.servers, function (server) {
 process.title = 'kiwiirc';
 
 // Change UID/GID
-if ((config.user) && (config.user !== '')) {
-    process.setuid(config.user);
-}
-if ((config.group) && (config.group !== '')) {
-    process.setgid(config.group);
+function setProcessUid() {
+    if ((global.config.group) && (global.config.group !== '')) {
+        process.setgid(global.config.group);
+    }
+    if ((global.config.user) && (global.config.user !== '')) {
+        process.setuid(global.config.user);
+    }
 }
 
 
+// Make sure Kiwi doesn't simply quit on an exception
+process.on('uncaughtException', function (e) {
+    winston.error('[Uncaught exception] %s', e, {stack: e.stack});
+});
+
+
+process.on('SIGUSR1', function() {
+    if (config.loadConfig()) {
+        winston.info('New config file loaded');
+    } else {
+        winston.info('No new config file was loaded');
+    }
+});
+
+
+process.on('SIGUSR2', function() {
+    winston.info('Connected clients: %s', _.size(global.clients.clients));
+    winston.info('Num. remote hosts: %s', _.size(global.clients.addresses));
+});
+
 
 /*
  * Listen for runtime commands
  */
-
 process.stdin.resume();
-process.stdin.on('data', function (buffered) {
-    var data = buffered.toString().trim();
-
-    switch (data) {
-        case 'stats':
-            console.log('Connected clients: ' + _.size(clients).toString());
-            break;
-
-        case 'reconfig':
-            (function () {
-                var new_conf = loadConfig();
-                if (new_conf) {
-                    config = new_conf;
-                    console.log('New config file loaded');
-                } else {
-                    console.log("No new config file was loaded");
-                }
-            })();
-
-            break;
-
-        default:
-            console.log('Unrecognised command: ' + data);
-    }
-});
+new ControlInterface(process.stdin, process.stdout, {prompt: ''});