Merge branch 'development' into server_plugins
[KiwiIRC.git] / server / client.js
1 var util = require('util'),
2 events = require('events'),
3 crypto = require('crypto'),
4 _ = require('lodash'),
5 IrcConnection = require('./irc/connection.js').IrcConnection,
6 IrcCommands = require('./irc/commands.js'),
7 ClientCommands = require('./clientcommands.js');
8
9
10 var Client = function (websocket) {
11 var that = this;
12
13 events.EventEmitter.call(this);
14 this.websocket = websocket;
15
16 // Clients address
17 this.real_address = this.websocket.handshake.real_address;
18
19 // A hash to identify this client instance
20 this.hash = crypto.createHash('sha256')
21 .update(this.real_address)
22 .update('' + Date.now())
23 .update(Math.floor(Math.random() * 100000).toString())
24 .digest('hex');
25
26 this.irc_connections = [];
27 this.next_connection = 0;
28
29 this.buffer = {
30 list: [],
31 motd: ''
32 };
33
34 // Handler for any commands sent from the client
35 this.client_commands = new ClientCommands(this);
36
37 websocket.on('irc', function () {
38 handleClientMessage.apply(that, arguments);
39 });
40 websocket.on('kiwi', function () {
41 kiwiCommand.apply(that, arguments);
42 });
43 websocket.on('disconnect', function () {
44 websocketDisconnect.apply(that, arguments);
45 });
46 websocket.on('error', function () {
47 websocketError.apply(that, arguments);
48 });
49
50 global.modules.emit('client:connected', {client:this});
51 };
52 util.inherits(Client, events.EventEmitter);
53
54 module.exports.Client = Client;
55
56 // Callback API:
57 // Callbacks SHALL accept 2 arguments, error and response, in that order.
58 // error MUST be null where the command is successul.
59 // error MUST otherwise be a truthy value and SHOULD be a string where the cause of the error is known.
60 // response MAY be given even if error is truthy
61
62 Client.prototype.sendIrcCommand = function (command, data, callback) {
63 var c = {command: command, data: data};
64 this.websocket.emit('irc', c, callback);
65 };
66
67 Client.prototype.sendKiwiCommand = function (command, data, callback) {
68 var c = {command: command, data: data};
69 this.websocket.emit('kiwi', c, callback);
70 };
71
72 Client.prototype.dispose = function () {
73 this.emit('destroy');
74 this.removeAllListeners();
75 };
76
77 function handleClientMessage(msg, callback) {
78 var server, args, obj, channels, keys;
79
80 // Make sure we have a server number specified
81 if ((msg.server === null) || (typeof msg.server !== 'number')) {
82 return (typeof callback === 'function') ? callback('server not specified') : undefined;
83 } else if (!this.irc_connections[msg.server]) {
84 return (typeof callback === 'function') ? callback('not connected to server') : undefined;
85 }
86
87 // The server this command is directed to
88 server = this.irc_connections[msg.server];
89
90 if (typeof callback !== 'function') {
91 callback = null;
92 }
93
94 try {
95 msg.data = JSON.parse(msg.data);
96 } catch (e) {
97 kiwi.log('[handleClientMessage] JSON parsing error ' + msg.data);
98 return;
99 }
100
101 // Run the client command
102 this.client_commands.run(msg.data.method, msg.data.args, server, callback);
103 }
104
105
106
107
108 function kiwiCommand(command, callback) {
109 var that = this;
110
111 if (typeof callback !== 'function') {
112 callback = function () {};
113 }
114 switch (command.command) {
115 case 'connect':
116 if (command.hostname && command.port && command.nick) {
117 var con;
118
119 if (global.config.restrict_server) {
120 con = new IrcConnection(
121 global.config.restrict_server,
122 global.config.restrict_server_port,
123 global.config.restrict_server_ssl,
124 command.nick,
125 {hostname: this.websocket.handshake.revdns, address: this.websocket.handshake.real_address},
126 global.config.restrict_server_password);
127
128 } else {
129 con = new IrcConnection(
130 command.hostname,
131 command.port,
132 command.ssl,
133 command.nick,
134 {hostname: this.websocket.handshake.revdns, address: this.websocket.handshake.real_address},
135 command.password);
136 }
137
138 var con_num = this.next_connection++;
139 this.irc_connections[con_num] = con;
140
141 var irc_commands = new IrcCommands(con, con_num, this);
142 irc_commands.bindEvents();
143
144 con.on('connected', function () {
145 return callback(null, con_num);
146 });
147
148 con.on('error', function (err) {
149 console.log('irc_connection error (' + command.hostname + '):', err);
150 // TODO: Once multiple servers implemented, specify which server failed
151 //that.sendKiwiCommand('error', {server: con_num, error: err});
152 return callback(err.code, null);
153 });
154
155 con.on('close', function () {
156 that.irc_connections[con_num] = null;
157 });
158 } else {
159 return callback('Hostname, port and nickname must be specified');
160 }
161 break;
162 default:
163 callback();
164 }
165 }
166
167
168 // Websocket has disconnected, so quit all the IRC connections
169 function websocketDisconnect() {
170 _.each(this.irc_connections, function (irc_connection, i, cons) {
171 if (irc_connection) {
172 irc_connection.end('QUIT :' + (global.config.quit_message || ''));
173 irc_connection.dispose();
174 cons[i] = null;
175 }
176 });
177
178 this.dispose();
179 }
180
181
182 // TODO: Should this close all the websocket connections too?
183 function websocketError() {
184 this.dispose();
185 }