var util = require('util'),
events = require('events'),
- _ = require('underscore'),
- IRCConnection = require('./irc-connection.js').IRCConnection,
- IRCCommands = require('./irc-commands.js'),
- ClientCommandset = require('./client-commands.js').ClientCommandset;
-
-var Client = function (websocket) {
- var c = this;
-
+ crypto = require('crypto'),
+ _ = require('lodash'),
+ State = require('./irc/state.js'),
+ IrcConnection = require('./irc/connection.js').IrcConnection,
+ ClientCommands = require('./clientcommands.js'),
+ WebsocketRpc = require('./websocketrpc.js'),
+ Stats = require('./stats.js');
+
+
+var Client = function (websocket, opts) {
+ var that = this;
+
+ Stats.incr('client.created');
+
events.EventEmitter.call(this);
this.websocket = websocket;
-
- this.IRC_connections = [];
- this.next_connection = 0;
-
+
+ // Keep a record of how this client connected
+ this.server_config = opts.server_config;
+
+ this.rpc = new WebsocketRpc(this.websocket);
+ this.rpc.on('all', function(func_name, return_fn) {
+ if (typeof func_name === 'string' && typeof return_fn === 'function') {
+ Stats.incr('client.command');
+ Stats.incr('client.command.' + func_name);
+ }
+ });
+
+ // Clients address
+ this.real_address = this.websocket.meta.real_address;
+
+ // A hash to identify this client instance
+ this.hash = crypto.createHash('sha256')
+ .update(this.real_address)
+ .update('' + Date.now())
+ .update(Math.floor(Math.random() * 100000).toString())
+ .digest('hex');
+
+ this.state = new State(this);
+
this.buffer = {
list: [],
motd: ''
};
-
+
// Handler for any commands sent from the client
- this.client_commands = new ClientCommandset(this);
+ this.client_commands = new ClientCommands(this);
+ this.client_commands.addRpcEvents(this, this.rpc);
- websocket.on('irc', function () {
- handleClientMessage.apply(c, arguments);
- });
- websocket.on('kiwi', function () {
- kiwi_command.apply(c, arguments);
+ // Handles the kiwi.* RPC functions
+ this.attachKiwiCommands();
+
+ websocket.on('message', function() {
+ // A message from the client is a sure sign the client is still alive, so consider it a heartbeat
+ that.heartbeat();
});
- websocket.on('disconnect', function () {
- disconnect.apply(c, arguments);
+
+ websocket.on('close', function () {
+ websocketDisconnect.apply(that, arguments);
});
websocket.on('error', function () {
- error.apply(c, arguments);
+ websocketError.apply(that, arguments);
});
+
+ this.disposed = false;
+
+ // Let the client know it's finished connecting
+ this.sendKiwiCommand('connected');
};
util.inherits(Client, events.EventEmitter);
// error MUST otherwise be a truthy value and SHOULD be a string where the cause of the error is known.
// response MAY be given even if error is truthy
-Client.prototype.sendIRCCommand = function (command, data, callback) {
+Client.prototype.sendIrcCommand = function (command, data, callback) {
var c = {command: command, data: data};
- console.log('C<--', c);
- this.websocket.emit('irc', c, callback);
+ this.rpc('irc', c, callback);
};
-Client.prototype.sendKiwiCommand = function (command, callback) {
- this.websocket.emit('kiwi', command, callback);
+Client.prototype.sendKiwiCommand = function (command, data, callback) {
+ var c = {command: command, data: data};
+ this.rpc('kiwi', c, callback);
};
-var handleClientMessage = function (msg, callback) {
- var server, args, obj, channels, keys;
+Client.prototype.dispose = function () {
+ Stats.incr('client.disposed');
- // Make sure we have a server number specified
- if ((msg.server === null) || (typeof msg.server !== 'number')) {
- return callback('server not specified');
- } else if (!this.IRC_connections[msg.server]) {
- return callback('not connected to server');
+ if (this._heartbeat_tmr) {
+ clearTimeout(this._heartbeat_tmr);
}
- // The server this command is directed to
- server = this.IRC_connections[msg.server];
+ this.rpc.dispose();
+ this.websocket.removeAllListeners();
- if (typeof callback !== 'function') {
- callback = null;
- }
-
- try {
- msg.data = JSON.parse(msg.data);
- } catch (e) {
- kiwi.log('[handleClientMessage] JSON parsing error ' + msg.data);
- return;
- }
+ this.disposed = true;
+ this.emit('dispose');
- // Run the client command
- this.client_commands.run(msg.data.method, msg.data.args, server, callback);
+ this.removeAllListeners();
};
-
-var kiwi_command = function (command, callback) {
- var that = this;
- console.log(typeof callback);
- if (typeof callback !== 'function') {
- callback = function () {};
- }
- switch (command.command) {
- case 'connect':
- if ((command.hostname) && (command.port) && (command.nick)) {
- var con = new IRCConnection(command.hostname, command.port, command.ssl,
- command.nick, {hostname: this.websocket.handshake.revdns, address: this.websocket.handshake.address.address},
- command.password, null);
-
- var con_num = this.next_connection++;
- this.IRC_connections[con_num] = con;
-
- var binder = new IRCCommands.Binder(con, con_num, this);
- binder.bind_irc_commands();
-
- con.on('connected', function () {
- console.log("con.on('connected')");
- return callback(null, con_num);
- });
-
- con.on('error', function (err) {
- this.websocket.sendKiwiCommand('error', {server: con_num, error: err});
- });
-
- con.on('close', function () {
- that.IRC_connections[con_num] = null;
- });
- } else {
- return callback('Hostname, port and nickname must be specified');
- }
- break;
- default:
- callback();
+Client.prototype.heartbeat = function() {
+ if (this._heartbeat_tmr) {
+ clearTimeout(this._heartbeat_tmr);
}
+
+ // After 2 minutes of this heartbeat not being called again, assume the client has disconnected
+ console.log('resetting heartbeat');
+ this._heartbeat_tmr = setTimeout(_.bind(this._heartbeat_timeout, this), 120000);
};
-var extension_command = function (command, callback) {
- if (typeof callback === 'function') {
- callback('not implemented');
- }
+
+Client.prototype._heartbeat_timeout = function() {
+ console.log('heartbeat stopped');
+ Stats.incr('client.timeout');
+ this.dispose();
};
-var disconnect = function () {
- _.each(this.IRC_connections, function (irc_connection, i, cons) {
- if (irc_connection) {
- irc_connection.end('QUIT :Kiwi IRC');
- cons[i] = null;
+
+
+Client.prototype.attachKiwiCommands = function() {
+ var that = this;
+
+ this.rpc.on('kiwi.connect_irc', function(callback, command) {
+ if (command.hostname && command.port && command.nick) {
+ var options = {};
+
+ // Get any optional parameters that may have been passed
+ if (command.encoding)
+ options.encoding = command.encoding;
+
+ options.password = global.config.restrict_server_password || command.password;
+
+ that.state.connect(
+ (global.config.restrict_server || command.hostname),
+ (global.config.restrict_server_port || command.port),
+ (typeof global.config.restrict_server_ssl !== 'undefined' ?
+ global.config.restrict_server_ssl :
+ command.ssl),
+ command.nick,
+ {hostname: that.websocket.meta.revdns, address: that.websocket.meta.real_address},
+ options,
+ callback);
+ } else {
+ return callback('Hostname, port and nickname must be specified');
}
});
- this.emit('destroy');
-};
-var error = function () {
- this.emit('destroy');
+
+ this.rpc.on('kiwi.client_info', function(callback, args) {
+ // keep hold of selected parts of the client_info
+ that.client_info = {
+ build_version: args.build_version.toString() || undefined
+ };
+ });
+
+
+ // Just to let us know the client is still there
+ this.rpc.on('kiwi.heartbeat', function(callback, args) {
+ that.heartbeat();
+ });
};
+
+
+
+// Websocket has disconnected, so quit all the IRC connections
+function websocketDisconnect() {
+ this.emit('disconnect');
+
+ this.dispose();
+}
+
+
+// TODO: Should this close all the websocket connections too?
+function websocketError() {
+ this.dispose();
+}