BSD and expact license modified
[KiwiIRC.git] / server / client.js
1 var util = require('util'),
2 events = require('events'),
3 crypto = require('crypto'),
4 _ = require('lodash'),
5 State = require('./irc/state.js'),
6 IrcConnection = require('./irc/connection.js').IrcConnection,
7 ClientCommands = require('./clientcommands.js'),
8 WebsocketRpc = require('./websocketrpc.js'),
9 Stats = require('./stats.js');
10
11
12 var Client = function (websocket, opts) {
13 var that = this;
14
15 Stats.incr('client.created');
16
17 events.EventEmitter.call(this);
18 this.websocket = websocket;
19
20 // Keep a record of how this client connected
21 this.server_config = opts.server_config;
22
23 this.rpc = new WebsocketRpc(this.websocket);
24 this.rpc.on('all', function(func_name, return_fn) {
25 if (typeof func_name === 'string' && typeof return_fn === 'function') {
26 Stats.incr('client.command');
27 Stats.incr('client.command.' + func_name);
28 }
29 });
30
31 // Clients address
32 this.real_address = this.websocket.meta.real_address;
33
34 // A hash to identify this client instance
35 this.hash = crypto.createHash('sha256')
36 .update(this.real_address)
37 .update('' + Date.now())
38 .update(Math.floor(Math.random() * 100000).toString())
39 .digest('hex');
40
41 this.state = new State(this);
42
43 this.buffer = {
44 list: [],
45 motd: ''
46 };
47
48 // Handler for any commands sent from the client
49 this.client_commands = new ClientCommands(this);
50 this.client_commands.addRpcEvents(this, this.rpc);
51
52 // Handles the kiwi.* RPC functions
53 this.attachKiwiCommands();
54
55 websocket.on('message', function() {
56 // A message from the client is a sure sign the client is still alive, so consider it a heartbeat
57 that.heartbeat();
58 });
59
60 websocket.on('close', function () {
61 websocketDisconnect.apply(that, arguments);
62 });
63 websocket.on('error', function () {
64 websocketError.apply(that, arguments);
65 });
66
67 this.disposed = false;
68
69 // Let the client know it's finished connecting
70 this.sendKiwiCommand('connected');
71 };
72 util.inherits(Client, events.EventEmitter);
73
74 module.exports.Client = Client;
75
76 // Callback API:
77 // Callbacks SHALL accept 2 arguments, error and response, in that order.
78 // error MUST be null where the command is successul.
79 // error MUST otherwise be a truthy value and SHOULD be a string where the cause of the error is known.
80 // response MAY be given even if error is truthy
81
82 Client.prototype.sendIrcCommand = function (command, data, callback) {
83 var c = {command: command, data: data};
84 this.rpc('irc', c, callback);
85 };
86
87 Client.prototype.sendKiwiCommand = function (command, data, callback) {
88 var c = {command: command, data: data};
89 this.rpc('kiwi', c, callback);
90 };
91
92 Client.prototype.dispose = function () {
93 Stats.incr('client.disposed');
94
95 if (this._heartbeat_tmr) {
96 clearTimeout(this._heartbeat_tmr);
97 }
98
99 this.rpc.dispose();
100 this.websocket.removeAllListeners();
101
102 this.disposed = true;
103 this.emit('dispose');
104
105 this.removeAllListeners();
106 };
107
108
109
110 Client.prototype.heartbeat = function() {
111 if (this._heartbeat_tmr) {
112 clearTimeout(this._heartbeat_tmr);
113 }
114
115 // After 2 minutes of this heartbeat not being called again, assume the client has disconnected
116 this._heartbeat_tmr = setTimeout(_.bind(this._heartbeat_timeout, this), 120000);
117 };
118
119
120 Client.prototype._heartbeat_timeout = function() {
121 Stats.incr('client.timeout');
122 this.dispose();
123 };
124
125
126
127 Client.prototype.attachKiwiCommands = function() {
128 var that = this;
129
130 this.rpc.on('kiwi.connect_irc', function(callback, command) {
131 if (command.hostname && command.port && command.nick) {
132 var options = {};
133
134 // Get any optional parameters that may have been passed
135 if (command.encoding)
136 options.encoding = command.encoding;
137
138 options.password = global.config.restrict_server_password || command.password;
139
140 that.state.connect(
141 (global.config.restrict_server || command.hostname),
142 (global.config.restrict_server_port || command.port),
143 (typeof global.config.restrict_server_ssl !== 'undefined' ?
144 global.config.restrict_server_ssl :
145 command.ssl),
146 command.nick,
147 {hostname: that.websocket.meta.revdns, address: that.websocket.meta.real_address},
148 options,
149 callback);
150 } else {
151 return callback('Hostname, port and nickname must be specified');
152 }
153 });
154
155
156 this.rpc.on('kiwi.client_info', function(callback, args) {
157 // keep hold of selected parts of the client_info
158 that.client_info = {
159 build_version: args.build_version.toString() || undefined
160 };
161 });
162
163
164 // Just to let us know the client is still there
165 this.rpc.on('kiwi.heartbeat', function(callback, args) {
166 that.heartbeat();
167 });
168 };
169
170
171
172 // Websocket has disconnected, so quit all the IRC connections
173 function websocketDisconnect() {
174 this.emit('disconnect');
175
176 this.dispose();
177 }
178
179
180 // TODO: Should this close all the websocket connections too?
181 function websocketError() {
182 this.dispose();
183 }