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