-/*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'),
- node_static = require('node-static'),
- fs = require('fs'),
- url = require('url'),
- dns = require('dns'),
- crypto = require('crypto'),
- events = require("events"),
- util = require('util'),
- ws = require('socket.io'),
- jsp = require("uglify-js").parser,
- pro = require("uglify-js").uglify,
- _ = require('./lib/underscore.min.js'),
- starttls = require('./lib/starttls.js'),
- app = require(__dirname + '/app.js');
-
-
-// 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;
-
-
-
-// How to handle log output
-this.log = function(str, level) {
- level = level || 0;
- console.log(str);
-}
+var fs = require('fs'),
+ _ = 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');
+
+
+
+process.chdir(__dirname + '/../');
+
+// Get our own version from package.json
+global.build_version = require('../package.json').version;
+
+// 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]);
+ }
+ }
+ config.loadConfig();
-/*
- * 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];
- }
+})(process.argv);
- this.log('Loaded config file ' + config_dirs[i] + config_filename);
- break;
- }
- } catch (e) {
- switch (e.code) {
- case 'ENOENT': // No file/dir
- break;
- default:
- this.log('An error occured parsing the config file ' + config_dirs[i] + config_filename + ': ' + e.message);
- return false;
- }
- continue;
- }
- }
- if (Object.keys(this.config).length === 0) {
- if (!found_config) {
- this.log('Couldn\'t find a config file!');
+
+// 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 false;
- }
- return [nconf, cconf];
-};
+ 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;
+ }
-// Reloads the config during runtime
-this.rehash = function () {
- return app.rehash();
+ // 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);
+ })();
}
-// 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, node_static:node_static, fs:fs, url:url, dns:dns, crypto:crypto, events:events, util:util, ws:ws, jsp:jsp, pro:pro, _:_, starttls:starttls};
- app.init(objs);
- app.rebindIRCCommands();
+// Make sure we have a valid config file and at least 1 server
+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);
+}
- return true;
+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);
-/*
- * Before we continue we need the config loaded
- */
-if (!this.loadConfig()) {
- process.exit(0);
+// 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];
+ }
+ }
+ },
-/*
- * HTTP file serving
- */
-if (this.config.handle_http) {
- this.fileServer = new (require('node-static').Server)(__dirname + this.config.public_http);
- this.cache = {alljs: '', html: []};
-}
-this.httpServers = [];
-this.httpHandler = function (request, response) {
- return app.httpHandler(request, response);
-}
+ 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);
-/*
- * Websocket handling
- */
-this.connections = {};
-this.io = [];
-this.websocketListen = function (servers, handler) {
- return app.websocketListen(servers, handler);
-}
-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.websocketKiwiMessage = function (msg, callback) {
- return app.websocketKiwiMessage(this, msg, callback);
-}
-this.websocketIRCConnect = function (nick, host, port, ssl, callback) {
- return app.websocketIRCConnect(this, nick, host, port, ssl, callback);
-}
+ 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');
+});
/*
- * IRC handling
+ * Identd server
*/
-this.parseIRCMessage = function (websocket, ircSocket, data) {
- return app.parseIRCMessage(websocket, ircSocket, data);
-}
-this.ircSocketDataHandler = function (data, websocket, ircSocket) {
- return app.ircSocketDataHandler(data, websocket, ircSocket);
-}
-this.IRCConnection = function (websocket, nick, host, port, ssl, password, callback) {
- return app.IRCConnection.call(this, websocket, nick, host, port, ssl, password, callback);
-}
-util.inherits(this.IRCConnection, events.EventEmitter);
+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();
-this.bindIRCCommands = function (irc_connection, websocket) {
- return app.bindIRCCommands.call(this, irc_connection, websocket);
-}
-this.rebindIRCCommands = function () {
- return app.rebindIRCCommands.call(this);
-}
+ 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();
+}
/*
- * Load up main application source
+ * Web listeners
*/
-if (!this.recode()) {
- process.exit(0);
-}
+// Start up a weblistener for each found in the config
+_.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();
+ }
+}
-// Set the process title
-app.setTitle();
/*
- * Load the modules as set in the config and print them out
+ * Process settings
*/
-this.kiwi_mod = require('./lib/kiwi_mod.js');
-this.kiwi_mod.loadModules(this.kiwi_root, this.config);
-this.kiwi_mod.printMods();
+// Set process title
+process.title = 'kiwiirc';
-// Make sure Kiwi doesn't simply quit on an exception
-//process.on('uncaughtException', function (e) {
-// console.log('[Uncaught exception] ' + e);
-//});
+// Change UID/GID
+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);
+ }
+}
-// Start the server up
-this.websocketListen(this.config.servers, this.httpHandler);
-// Now we're listening on the network, set our UID/GIDs if required
-app.changeUser();
+// Make sure Kiwi doesn't simply quit on an exception
+process.on('uncaughtException', function (e) {
+ winston.error('[Uncaught exception] %s', e, {stack: e.stack});
+});
-// Listen for controll messages
-process.stdin.resume();
-process.stdin.on('data', function (data) { app.manageControll(data); });
+
+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();
+new ControlInterface(process.stdin, process.stdout, {prompt: ''});