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