8e0f29d740efb08b7f6ff02c9cb9fc3d0b80b125
[KiwiIRC.git] / server / kiwi.js
1 var fs = require('fs'),
2 _ = require('lodash'),
3 util = require('util'),
4 winston = require('winston'),
5 WebListener = require('./weblistener.js'),
6 config = require('./configuration.js'),
7 modules = require('./modules.js'),
8 Identd = require('./identd.js'),
9 Proxy = require('./proxy.js'),
10 ControlInterface = require('./controlinterface.js');
11
12
13
14 process.chdir(__dirname + '/../');
15
16 (function (argv) {
17 var conf_switch = argv.indexOf('-c');
18 if (conf_switch !== -1) {
19 if (argv[conf_switch + 1]) {
20 return config.loadConfig(argv[conf_switch + 1]);
21 }
22 }
23
24 config.loadConfig();
25
26 })(process.argv);
27
28
29 // If we're not running in the forground and we have a log file.. switch
30 // console.log to output to a file
31 if (process.argv.indexOf('-f') === -1 && global.config && global.config.log) {
32 (function () {
33 var log_file_name = global.config.log;
34
35 if (log_file_name[0] !== '/') {
36 log_file_name = __dirname + '/../' + log_file_name;
37 }
38
39 winston.add(winston.transports.File, { filename: log_file_name, json: false});
40 winston.remove(winston.transports.Console);
41 })();
42 }
43
44
45
46 // Make sure we have a valid config file and at least 1 server
47 if (!global.config || Object.keys(global.config).length === 0) {
48 //console.log('Couldn\'t find a valid config.js file (Did you copy the config.example.js file yet?)');
49 winston.error('Couldn\'t find a valid config.js file (Did you copy the config.example.js file yet?)');
50 process.exit(1);
51 }
52
53 if ((!global.config.servers) || (global.config.servers.length < 1)) {
54 //console.log('No servers defined in config file');
55 winston.error('No servers defined in config file');
56 process.exit(2);
57 }
58
59
60
61
62 // Create a plugin interface
63 global.modules = new modules.Publisher();
64
65 // Register as the active interface
66 modules.registerPublisher(global.modules);
67
68 // Load any modules in the config
69 if (global.config.module_dir) {
70 (global.config.modules || []).forEach(function (module_name) {
71 if (modules.load(module_name)) {
72 //console.log('Module ' + module_name + ' loaded successfuly');
73 winston.info('Module %s loaded successfully', module_name);
74 } else {
75 //console.log('Module ' + module_name + ' failed to load');
76 winston.warn('Module %s failed to load', module_name);
77 }
78 });
79 }
80
81
82
83
84 // Holder for all the connected clients
85 global.clients = {
86 clients: Object.create(null),
87 addresses: Object.create(null),
88
89 // Local and foriegn port pairs for identd lookups
90 // {'65483_6667': client_obj, '54356_6697': client_obj}
91 port_pairs: {},
92
93 add: function (client) {
94 this.clients[client.hash] = client;
95 if (typeof this.addresses[client.real_address] === 'undefined') {
96 this.addresses[client.real_address] = Object.create(null);
97 }
98 this.addresses[client.real_address][client.hash] = client;
99 },
100
101 remove: function (client) {
102 if (typeof this.clients[client.hash] !== 'undefined') {
103 delete this.clients[client.hash];
104 delete this.addresses[client.real_address][client.hash];
105 if (Object.keys(this.addresses[client.real_address]).length < 1) {
106 delete this.addresses[client.real_address];
107 }
108 }
109 },
110
111 numOnAddress: function (addr) {
112 if (typeof this.addresses[addr] !== 'undefined') {
113 return Object.keys(this.addresses[addr]).length;
114 } else {
115 return 0;
116 }
117 },
118
119 broadcastKiwiCommand: function (command, data, callback) {
120 var clients = [];
121
122 // Get an array of clients for us to work with
123 for (var client in global.clients.clients) {
124 clients.push(global.clients.clients[client]);
125 }
126
127
128 // Sending of the command in batches
129 var sendCommandBatch = function (list) {
130 var batch_size = 100,
131 cutoff;
132
133 if (list.length >= batch_size) {
134 // If we have more clients than our batch size, call ourself with the next batch
135 setTimeout(function () {
136 sendCommandBatch(list.slice(batch_size));
137 }, 200);
138
139 cutoff = batch_size;
140
141 } else {
142 cutoff = list.length;
143 }
144
145 list.slice(0, cutoff).forEach(function (client) {
146 if (!client.disposed) {
147 client.sendKiwiCommand(command, data);
148 }
149 });
150
151 if (cutoff === list.length && typeof callback === 'function') {
152 callback();
153 }
154 };
155
156 sendCommandBatch(clients);
157 }
158 };
159
160 global.servers = {
161 servers: Object.create(null),
162
163 addConnection: function (connection) {
164 var host = connection.irc_host.hostname;
165 if (!this.servers[host]) {
166 this.servers[host] = [];
167 }
168 this.servers[host].push(connection);
169 },
170
171 removeConnection: function (connection) {
172 var host = connection.irc_host.hostname
173 if (this.servers[host]) {
174 this.servers[host] = _.without(this.servers[host], connection);
175 if (this.servers[host].length === 0) {
176 delete this.servers[host];
177 }
178 }
179 },
180
181 numOnHost: function (host) {
182 if (this.servers[host]) {
183 return this.servers[host].length;
184 } else {
185 return 0;
186 }
187 }
188 };
189
190
191
192 /**
193 * When a new config is loaded, send out an alert to the clients so
194 * so they can reload it
195 */
196 config.on('loaded', function () {
197 global.clients.broadcastKiwiCommand('reconfig');
198 });
199
200
201
202 /*
203 * Identd server
204 */
205 if (global.config.identd && global.config.identd.enabled) {
206 var identd_resolve_user = function(port_here, port_there) {
207 var key = port_here.toString() + '_' + port_there.toString();
208
209 if (typeof global.clients.port_pairs[key] == 'undefined') {
210 return;
211 }
212
213 return global.clients.port_pairs[key].username;
214 };
215
216 var identd_server = new Identd({
217 bind_addr: global.config.identd.address,
218 bind_port: global.config.identd.port,
219 user_id: identd_resolve_user
220 });
221
222 identd_server.start();
223 }
224
225
226
227
228 /*
229 * Web listeners
230 */
231
232
233 // Start up a weblistener for each found in the config
234 _.each(global.config.servers, function (server) {
235 if (server.type == 'proxy') {
236 // Start up a kiwi proxy server
237 var serv = new Proxy.ProxyServer();
238 serv.listen(server.port, server.address, server);
239
240 serv.on('listening', function() {
241 //console.log('Kiwi proxy listening on %s:%s %s SSL', server.address, server.port, (server.ssl ? 'with' : 'without'));
242 winston.info('Kiwi proxy listening on %s:%s %s SSL', server.address, server.port, (server.ssl ? 'with' : 'without'));
243 });
244
245 serv.on('socket_connected', function(pipe) {
246 // SSL connections have the raw socket as a property
247 var socket = pipe.irc_socket.socket ?
248 pipe.irc_socket.socket :
249 pipe.irc_socket;
250
251 pipe.identd_pair = socket.localPort.toString() + '_' + socket.remotePort.toString();
252 global.clients.port_pairs[pipe.identd_pair] = pipe.meta;
253 });
254
255 serv.on('connection_close', function(pipe) {
256 delete global.clients.port_pairs[pipe.identd_pair];
257 });
258
259 } else {
260 // Start up a kiwi web server
261 var wl = new WebListener(server, global.config.transports);
262
263 wl.on('connection', function (client) {
264 clients.add(client);
265 });
266
267 wl.on('client_dispose', function (client) {
268 clients.remove(client);
269 });
270
271 wl.on('listening', function () {
272 //console.log('Listening on %s:%s %s SSL', server.address, server.port, (server.ssl ? 'with' : 'without'));
273 winston.info('Listening on %s:%s %s SSL', server.address, server.port, (server.ssl ? 'with' : 'without'));
274 webListenerRunning();
275 });
276
277 wl.on('error', function (err) {
278 //console.log('Error listening on %s:%s: %s', server.address, server.port, err.code);
279 winston.info('Error listening on %s:%s: %s', server.address, server.port, err.code);
280 // TODO: This should probably be refactored. ^JA
281 webListenerRunning();
282 });
283 }
284 });
285
286 // Once all the listeners are listening, set the processes UID/GID
287 var num_listening = 0;
288 function webListenerRunning() {
289 num_listening++;
290 if (num_listening === global.config.servers.length) {
291 setProcessUid();
292 }
293 }
294
295
296
297
298 /*
299 * Process settings
300 */
301
302 // Set process title
303 process.title = 'kiwiirc';
304
305 // Change UID/GID
306 function setProcessUid() {
307 if ((global.config.group) && (global.config.group !== '')) {
308 process.setgid(global.config.group);
309 }
310 if ((global.config.user) && (global.config.user !== '')) {
311 process.setuid(global.config.user);
312 }
313 }
314
315
316 // Make sure Kiwi doesn't simply quit on an exception
317 process.on('uncaughtException', function (e) {
318 //console.log('[Uncaught exception] ' + e);
319 //console.log(e.stack);
320 winston.error('[Uncaught exception] %s', e, {stack: e.stack});
321 });
322
323
324 process.on('SIGUSR1', function() {
325 if (config.loadConfig()) {
326 //console.log('New config file loaded');
327 winston.info('New config file loaded');
328 } else {
329 //console.log("No new config file was loaded");
330 winston.info('No new config file was loaded');
331 }
332 });
333
334
335 process.on('SIGUSR2', function() {
336 //console.log('Connected clients: ' + _.size(global.clients.clients).toString());
337 //console.log('Num. remote hosts: ' + _.size(global.clients.addresses).toString());
338 winston.info('Connected clients: %s', _.size(global.clients.clients));
339 winston.info('Num. remote hosts: %s', _.size(global.clients.addresses));
340 });
341
342
343 /*
344 * Listen for runtime commands
345 */
346 process.stdin.resume();
347 new ControlInterface(process.stdin, process.stdout, {prompt: ''});