UNPKG

20.1 kBJavaScriptView Raw
1//- JavaScript source code
2
3//- service.js ~~
4// ~~ (c) SRW, 24 Nov 2012
5// ~~ last updated 10 Aug 2014
6
7(function () {
8 'use strict';
9
10 // Pragmas
11
12 /*jshint maxparams: 3, quotmark: single, strict: true */
13
14 /*jslint indent: 4, maxlen: 80, node: true */
15
16 /*properties
17 api, apply, avar_ttl, box, buffer, cmd, collect_garbage, connection,
18 'Content-Type', 'content-length', create, createServer, Date,
19 enable_api_server, enable_CORS, enable_web_server, end, error, fork,
20 gc_interval, get_avar, get_list, globalAgent, handler, hasOwnProperty,
21 headers, host, hostname, 'if-modified-since', isMaster, isWorker, join,
22 key, keys, last_mod_date, 'Last-Modified', last_modified, launch,
23 length, listen, log, match, match_hostname, max_fu_size,
24 max_http_sockets, maxSockets, max_upload_size, method, mime_type, on,
25 parse, pattern, persistent_storage, pid, port, push, replace, set_avar,
26 slice, sort, split, sqlite, static_content, status, stringify, test,
27 timestamp, toGMTString, toString, trafficlog_storage, unroll, url,
28 warn, worker_procs, writeHead, 'x-forwarded-for'
29 */
30
31 // Declarations
32
33 var cluster, collect_garbage, configure, corser, http, is_Function,
34 katamari, spawn_workers, warn;
35
36 // Definitions
37
38 cluster = require('cluster');
39
40 collect_garbage = function (f, options) {
41 // This function needs documentation.
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 // This function returns `true` if and only if input argument `f` is a
60 // function. The second condition is necessary to avoid a false positive
61 // in a pre-ES5 environment when `f` is a regular expression. Since we
62 // know that Node.js supports ES5, it probably isn't necessary, but it's
63 // not causing a bottleneck or anything ...
64 return ((typeof f === 'function') && (f instanceof Function));
65 };
66
67 katamari = require('./katamari');
68
69 spawn_workers = function (n) {
70 // This function needs documentation.
71 var spawn_worker;
72 spawn_worker = function () {
73 // This function needs documentation.
74 var worker = cluster.fork();
75 worker.on('error', function (err) {
76 // This function needs documentation.
77 console.error(err);
78 return;
79 });
80 worker.on('message', function (message) {
81 // This function needs documentation.
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 // This function needs documentation.
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 // This function needs documentation.
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 // Out-of-scope definitions
111
112 exports.launch = function (obj) {
113 // This function needs documentation.
114 /*jslint unparam: true */
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', //- aka INADDR_ANY
122 log: function (request) {
123 // This function is the default logging function.
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, //- 1024 * 1024 = 1 Megabyte
134 persistent_storage: {
135 avar_ttl: 86400, //- expire avars after 24 hours
136 gc_interval: 60 //- collect garbage every _ seconds
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 // Exit early if the configuration is underspecified.
146 warn(['No servers specified.']);
147 return;
148 }
149 hang_up = function (response) {
150 // This function needs documentation.
151 response.writeHead(444);
152 response.end();
153 return;
154 };
155 log = function (request) {
156 // This function delegates to the user-specified `config.log` :-)
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 // This function prints traffic data to stdout. It alphabetizes
170 // the keys to make debugging easier.
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 // This function needs documentation.
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 // This function needs documentation.
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 // This function supports CORS preflight for all routes.
220 response.writeHead(204);
221 response.end();
222 return;
223 }
224 });
225 } else {
226 server = http.createServer(function (request, response) {
227 // This function needs documentation.
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 // This part makes my eyes bleed, but it works really well.
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 // These are mainly here for debugging at the moment ...
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 // This function needs documentation.
305 var callback;
306 callback = function (err, results) {
307 // This function needs documentation.
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 // This function needs documentation.
328 var callback;
329 callback = function (err, results) {
330 // This function needs documentation.
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 // This function needs documentation.
351 var callback, headers, temp;
352 callback = function (err) {
353 // This function needs documentation.
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 // This function needs documentation.
374 temp.push(chunk.toString());
375 return;
376 });
377 request.on('end', function () {
378 // This function needs documentation.
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 // If this is a standalone API server, then the "robots.txt" file
403 // won't be present.
404 rules.push({
405 method: 'GET',
406 pattern: /^\/robots\.txt$/,
407 handler: function (request, response) {
408 // This function returns a bare-minimum "robots.txt" file.
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 // This function needs documentation.
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 // This function needs documentation.
465 console.error('Server error:', message);
466 return;
467 });
468 server.listen(config.port, config.hostname, function () {
469 // This function needs documentation.
470 console.log('QM up -> http://%s:%d ...',
471 config.hostname, config.port);
472 return;
473 });
474 return;
475 };
476
477 // That's all, folks!
478
479 return;
480
481}());
482
483//- vim:set syntax=javascript: