1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | (function () {
|
8 | 'use strict';
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | |
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | var cluster, collect_garbage, configure, corser, http, is_Function,
|
34 | katamari, spawn_workers, warn;
|
35 |
|
36 |
|
37 |
|
38 | cluster = require('cluster');
|
39 |
|
40 | collect_garbage = function (f, options) {
|
41 |
|
42 | if (cluster.isWorker) {
|
43 | return;
|
44 | }
|
45 | if (options.hasOwnProperty('gc_interval') === false) {
|
46 | throw new Error('WTF');
|
47 | }
|
48 | setInterval(f, options.gc_interval * 1000);
|
49 | return;
|
50 | };
|
51 |
|
52 | configure = require('./configure');
|
53 |
|
54 | corser = require('corser');
|
55 |
|
56 | http = require('http');
|
57 |
|
58 | is_Function = function (f) {
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | return ((typeof f === 'function') && (f instanceof Function));
|
65 | };
|
66 |
|
67 | katamari = require('./katamari');
|
68 |
|
69 | spawn_workers = function (n) {
|
70 |
|
71 | var spawn_worker;
|
72 | spawn_worker = function () {
|
73 |
|
74 | var worker = cluster.fork();
|
75 | worker.on('error', function (err) {
|
76 |
|
77 | console.error(err);
|
78 | return;
|
79 | });
|
80 | worker.on('message', function (message) {
|
81 |
|
82 | console.log(worker.pid + ':', message.cmd);
|
83 | return;
|
84 | });
|
85 | return worker;
|
86 | };
|
87 | if ((cluster.isMaster) && (n > 0)) {
|
88 | cluster.on('exit', function (prev_worker) {
|
89 |
|
90 | var next_worker = spawn_worker();
|
91 | console.log(prev_worker.pid + ':', 'RIP', next_worker.pid);
|
92 | return;
|
93 | });
|
94 | while (n > 0) {
|
95 | spawn_worker();
|
96 | n -= 1;
|
97 | }
|
98 | }
|
99 | return;
|
100 | };
|
101 |
|
102 | warn = function (lines) {
|
103 |
|
104 | var text;
|
105 | text = lines.join(' ').replace(/([\w\-\:\,\.\s]{65,79})\s/g, '$1\n');
|
106 | console.warn('\n%s\n', text);
|
107 | return;
|
108 | };
|
109 |
|
110 |
|
111 |
|
112 | exports.launch = function (obj) {
|
113 |
|
114 |
|
115 | var config, defs, enable_cors, hang_up, log, rules, save, server,
|
116 | static_content;
|
117 | config = configure(obj, {
|
118 | enable_api_server: false,
|
119 | enable_CORS: false,
|
120 | enable_web_server: false,
|
121 | hostname: '0.0.0.0',
|
122 | log: function (request) {
|
123 |
|
124 | return {
|
125 | host: request.headers.host,
|
126 | method: request.method,
|
127 | timestamp: new Date(),
|
128 | url: request.url
|
129 | };
|
130 | },
|
131 | match_hostname: false,
|
132 | max_http_sockets: 500,
|
133 | max_upload_size: 1048576,
|
134 | persistent_storage: {
|
135 | avar_ttl: 86400,
|
136 | gc_interval: 60
|
137 | },
|
138 | port: 8177,
|
139 | static_content: 'katamari.json',
|
140 | trafficlog_storage: {},
|
141 | worker_procs: 0
|
142 | });
|
143 | if ((config.enable_api_server === false) &&
|
144 | (config.enable_web_server === false)) {
|
145 |
|
146 | warn(['No servers specified.']);
|
147 | return;
|
148 | }
|
149 | hang_up = function (response) {
|
150 |
|
151 | response.writeHead(444);
|
152 | response.end();
|
153 | return;
|
154 | };
|
155 | log = function (request) {
|
156 |
|
157 | save(config.log(request));
|
158 | return;
|
159 | };
|
160 | rules = [];
|
161 | if (config.trafficlog_storage.hasOwnProperty('couch')) {
|
162 | save = require('./defs-couch').log(config.trafficlog_storage);
|
163 | } else if (config.trafficlog_storage.hasOwnProperty('mongo')) {
|
164 | save = require('./defs-mongo').log(config.trafficlog_storage);
|
165 | } else if (config.trafficlog_storage.hasOwnProperty('postgres')) {
|
166 | save = require('./defs-postgres').log(config.trafficlog_storage);
|
167 | } else {
|
168 | save = function (obj) {
|
169 |
|
170 |
|
171 | var i, keys, n, temp;
|
172 | keys = Object.keys(obj).sort();
|
173 | n = keys.length;
|
174 | temp = {};
|
175 | for (i = 0; i < n; i += 1) {
|
176 | temp[keys[i]] = obj[keys[i]];
|
177 | }
|
178 | console.log(JSON.stringify(temp, undefined, 4));
|
179 | return;
|
180 | };
|
181 | }
|
182 | if (config.enable_CORS === true) {
|
183 | enable_cors = corser.create({});
|
184 | server = http.createServer(function (request, response) {
|
185 |
|
186 | if ((config.match_hostname === true) &&
|
187 | (request.headers.host !== config.hostname)) {
|
188 | hang_up(response);
|
189 | return;
|
190 | }
|
191 | enable_cors(request, response, function () {
|
192 |
|
193 | var flag, i, n, params, rule, url;
|
194 | flag = false;
|
195 | n = rules.length;
|
196 | url = request.url;
|
197 | for (i = 0; (flag === false) && (i < n); i += 1) {
|
198 | rule = rules[i];
|
199 | if ((request.method === rule.method) &&
|
200 | (rule.pattern.test(url))) {
|
201 | flag = true;
|
202 | params = url.match(rule.pattern).slice(1);
|
203 | rule.handler(request, response, params);
|
204 | }
|
205 | }
|
206 | if (flag === true) {
|
207 | log(request);
|
208 | } else {
|
209 | hang_up(response);
|
210 | }
|
211 | return;
|
212 | });
|
213 | return;
|
214 | });
|
215 | rules.push({
|
216 | method: 'OPTIONS',
|
217 | pattern: /^\//,
|
218 | handler: function (request, response) {
|
219 |
|
220 | response.writeHead(204);
|
221 | response.end();
|
222 | return;
|
223 | }
|
224 | });
|
225 | } else {
|
226 | server = http.createServer(function (request, response) {
|
227 |
|
228 | if ((config.match_hostname === true) &&
|
229 | (request.headers.host !== config.hostname)) {
|
230 | hang_up(response);
|
231 | return;
|
232 | }
|
233 | var flag, i, n, params, rule, url;
|
234 | flag = false;
|
235 | n = rules.length;
|
236 | url = request.url;
|
237 | for (i = 0; (flag === false) && (i < n); i += 1) {
|
238 | rule = rules[i];
|
239 | if ((request.method === rule.method) &&
|
240 | (rule.pattern.test(url))) {
|
241 | flag = true;
|
242 | params = url.match(rule.pattern).slice(1);
|
243 | rule.handler(request, response, params);
|
244 | }
|
245 | }
|
246 | if (flag === true) {
|
247 | log(request);
|
248 | } else {
|
249 | hang_up(response);
|
250 | }
|
251 | return;
|
252 | });
|
253 | }
|
254 | if (config.enable_api_server === true) {
|
255 |
|
256 | if (config.persistent_storage.hasOwnProperty('couch')) {
|
257 | defs = require('./defs-couch').api(config.persistent_storage);
|
258 | } else if (config.persistent_storage.hasOwnProperty('mongo')) {
|
259 | if ((config.max_upload_size > 4194304) && (cluster.isMaster)) {
|
260 | warn([
|
261 | 'WARNING: Older versions of MongoDB cannot save',
|
262 | 'documents greater than 4MB (when converted to BSON).',
|
263 | 'Consider setting a smaller "max_upload_size".'
|
264 | ]);
|
265 | }
|
266 | defs = require('./defs-mongo').api(config.persistent_storage);
|
267 | } else if (config.persistent_storage.hasOwnProperty('postgres')) {
|
268 | defs = require('./defs-postgres')
|
269 | .api(config.persistent_storage);
|
270 | } else if (config.persistent_storage.hasOwnProperty('redis')) {
|
271 | defs = require('./defs-redis')(config.persistent_storage);
|
272 | } else if (config.persistent_storage.hasOwnProperty('sqlite')) {
|
273 | if ((config.persistent_storage.sqlite === ':memory:') &&
|
274 | (cluster.isMaster) && (config.worker_procs > 0)) {
|
275 | warn([
|
276 | 'WARNING: In-memory SQLite databases do not provide',
|
277 | 'shared persistent storage because each worker will',
|
278 | 'create and use its own individual database. Thus,',
|
279 | 'you should expect your API server to behave',
|
280 | 'erratically at best.'
|
281 | ]);
|
282 | }
|
283 | defs = require('./defs-sqlite')(config.persistent_storage);
|
284 | } else {
|
285 | throw new Error('No persistent storage was specified.');
|
286 | }
|
287 |
|
288 | if (is_Function(defs.collect_garbage) === false) {
|
289 | throw new TypeError('No "collect_garbage" method is defined.');
|
290 | }
|
291 | if (is_Function(defs.get_avar) === false) {
|
292 | throw new TypeError('No "get_avar" method is defined.');
|
293 | }
|
294 | if (is_Function(defs.get_list) === false) {
|
295 | throw new TypeError('No "get_list" method is defined.');
|
296 | }
|
297 | if (is_Function(defs.set_avar) === false) {
|
298 | throw new TypeError('No "set_avar" method is defined.');
|
299 | }
|
300 | rules.push({
|
301 | method: 'GET',
|
302 | pattern: /^\/(?:box|v1)\/([\w\-]+)\?key=([A-z0-9]+)$/,
|
303 | handler: function (request, response, params) {
|
304 |
|
305 | var callback;
|
306 | callback = function (err, results) {
|
307 |
|
308 | if (err !== null) {
|
309 | console.error(err);
|
310 | }
|
311 | if ((results === null) || (results === undefined)) {
|
312 | results = '{}';
|
313 | }
|
314 | response.writeHead(200, {
|
315 | 'Content-Type': 'application/json'
|
316 | });
|
317 | response.end(results);
|
318 | return;
|
319 | };
|
320 | return defs.get_avar(params, callback);
|
321 | }
|
322 | });
|
323 | rules.push({
|
324 | method: 'GET',
|
325 | pattern: /^\/(?:box|v1)\/([\w\-]+)\?status=([A-z0-9]+)$/,
|
326 | handler: function (request, response, params) {
|
327 |
|
328 | var callback;
|
329 | callback = function (err, results) {
|
330 |
|
331 | if (err !== null) {
|
332 | console.error(err);
|
333 | }
|
334 | if ((results instanceof Array) === false) {
|
335 | results = [];
|
336 | }
|
337 | response.writeHead(200, {
|
338 | 'Content-Type': 'application/json'
|
339 | });
|
340 | response.end(JSON.stringify(results));
|
341 | return;
|
342 | };
|
343 | return defs.get_list(params, callback);
|
344 | }
|
345 | });
|
346 | rules.push({
|
347 | method: 'POST',
|
348 | pattern: /^\/(?:box|v1)\/([\w\-]+)\?key=([A-z0-9]+)$/,
|
349 | handler: function (request, response, params) {
|
350 |
|
351 | var callback, headers, temp;
|
352 | callback = function (err) {
|
353 |
|
354 | if (err !== null) {
|
355 | console.error(err);
|
356 | return hang_up(response);
|
357 | }
|
358 | response.writeHead(201, {
|
359 | 'Content-Type': 'text/plain'
|
360 | });
|
361 | response.end();
|
362 | return;
|
363 | };
|
364 | headers = request.headers;
|
365 | if (headers.hasOwnProperty('content-length') === false) {
|
366 | return callback('Missing "content-length" header');
|
367 | }
|
368 | if (headers['content-length'] > config.max_fu_size) {
|
369 | return callback('Maximum file upload size exceeded');
|
370 | }
|
371 | temp = [];
|
372 | request.on('data', function (chunk) {
|
373 |
|
374 | temp.push(chunk.toString());
|
375 | return;
|
376 | });
|
377 | request.on('end', function () {
|
378 |
|
379 | var body, box, key, obj2;
|
380 | body = temp.join('');
|
381 | box = params[0];
|
382 | key = params[1];
|
383 | try {
|
384 | obj2 = JSON.parse(body);
|
385 | if ((obj2.box !== box) || (obj2.key !== key)) {
|
386 | throw new Error('Mismatched JSON properties');
|
387 | }
|
388 | if ((typeof obj2.status === 'string') &&
|
389 | (/^[A-z0-9]+$/).test(obj2.status)) {
|
390 | params.push(obj2.status);
|
391 | }
|
392 | } catch (err) {
|
393 | return callback(err);
|
394 | }
|
395 | params.push(body);
|
396 | return defs.set_avar(params, callback);
|
397 | });
|
398 | return;
|
399 | }
|
400 | });
|
401 | if (config.enable_web_server === false) {
|
402 |
|
403 |
|
404 | rules.push({
|
405 | method: 'GET',
|
406 | pattern: /^\/robots\.txt$/,
|
407 | handler: function (request, response) {
|
408 |
|
409 | response.writeHead(200, {
|
410 | 'Content-Type': 'text/plain'
|
411 | });
|
412 | response.end('User-agent: *\nDisallow: /\n');
|
413 | return;
|
414 | }
|
415 | });
|
416 | }
|
417 | collect_garbage(defs.collect_garbage, config.persistent_storage);
|
418 | }
|
419 | if (config.enable_web_server === true) {
|
420 | static_content = katamari.unroll(config.static_content);
|
421 | rules.push({
|
422 | method: 'GET',
|
423 | pattern: /^(\/[\w\-\.]*)/,
|
424 | handler: function (request, response, params) {
|
425 |
|
426 | var headers, name, resource, temp;
|
427 | headers = request.headers;
|
428 | name = (params[0] === '/') ? '/index.html' : params[0];
|
429 | if (static_content.hasOwnProperty(name) === false) {
|
430 | return hang_up(response);
|
431 | }
|
432 | resource = static_content[name];
|
433 | if (headers.hasOwnProperty('if-modified-since')) {
|
434 | try {
|
435 | temp = new Date(headers['if-modified-since']);
|
436 | } catch (err) {
|
437 | return hang_up(response);
|
438 | }
|
439 | if (resource.last_mod_date <= temp) {
|
440 | response.writeHead(304, {
|
441 | 'Date': (new Date()).toGMTString()
|
442 | });
|
443 | response.end();
|
444 | return;
|
445 | }
|
446 | }
|
447 | response.writeHead(200, {
|
448 | 'Content-Type': resource.mime_type,
|
449 | 'Date': (new Date()).toGMTString(),
|
450 | 'Last-Modified': resource.last_modified
|
451 | });
|
452 | response.end(resource.buffer);
|
453 | return;
|
454 | }
|
455 | });
|
456 | }
|
457 | if ((cluster.isMaster) && (config.worker_procs > 0)) {
|
458 | spawn_workers(config.worker_procs);
|
459 | server = null;
|
460 | return;
|
461 | }
|
462 | http.globalAgent.maxSockets = config.max_http_sockets;
|
463 | server.on('error', function (message) {
|
464 |
|
465 | console.error('Server error:', message);
|
466 | return;
|
467 | });
|
468 | server.listen(config.port, config.hostname, function () {
|
469 |
|
470 | console.log('QM up -> http://%s:%d ...',
|
471 | config.hostname, config.port);
|
472 | return;
|
473 | });
|
474 | return;
|
475 | };
|
476 |
|
477 |
|
478 |
|
479 | return;
|
480 |
|
481 | }());
|
482 |
|
483 |
|