UNPKG

19.2 kBJavaScriptView Raw
1//- JavaScript source code
2
3//- service.js ~~
4// ~~ (c) SRW, 24 Nov 2012
5// ~~ last updated 21 Mar 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_www_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, last_mod_date, 'Last-Modified', last_modified, launch, length,
23 listen, log, match, match_hostname, max_fu_size, max_http_sockets,
24 maxSockets, max_upload_size, method, mime_type, on, parse, pattern,
25 persistent_storage, pid, port, push, replace, set_avar, slice, split,
26 sqlite, static_content, status, stringify, test, timestamp,
27 toGMTString, toString, trafficlog_storage, unroll, url, warn,
28 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 needs documentation.
60 return ((typeof f === 'function') && (f instanceof Function));
61 };
62
63 katamari = require('./katamari');
64
65 spawn_workers = function (n) {
66 // This function needs documentation.
67 var spawn_worker;
68 spawn_worker = function () {
69 // This function needs documentation.
70 var worker = cluster.fork();
71 worker.on('error', function (err) {
72 // This function needs documentation.
73 console.error(err);
74 return;
75 });
76 worker.on('message', function (message) {
77 // This function needs documentation.
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 // This function needs documentation.
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 // This function needs documentation.
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 // Out-of-scope definitions
107
108 exports.launch = function (obj) {
109 // This function needs documentation.
110 /*jslint unparam: true */
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 // This function is the default logging function.
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, //- 1024 * 1024 = 1 Megabyte
130 persistent_storage: {
131 avar_ttl: 86400, //- expire avars after 24 hours
132 gc_interval: 10 //- collect garbage every _ seconds
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 // Exit early if the configuration is underspecified.
142 warn(['No servers specified.']);
143 return;
144 }
145 hang_up = function (response) {
146 // This function needs documentation.
147 response.writeHead(444);
148 response.end();
149 return;
150 };
151 log = function (request) {
152 // This function delegates to the user-specified `config.log` :-)
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 // This function needs documentation.
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 // This function needs documentation.
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 // This function needs documentation.
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 // This function needs documentation.
208 response.writeHead(204);
209 response.end();
210 return;
211 }
212 });
213 } else {
214 server = http.createServer(function (request, response) {
215 // This function needs documentation.
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 // This part makes my eyes bleed, but it works really well.
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 // These are mainly here for debugging at the moment ...
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 // This function needs documentation.
293 var callback;
294 callback = function (err, results) {
295 // This function needs documentation.
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 // This function needs documentation.
316 var callback;
317 callback = function (err, results) {
318 // This function needs documentation.
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 // This function needs documentation.
339 var callback, headers, temp;
340 callback = function (err) {
341 // This function needs documentation.
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 // This function needs documentation.
362 temp.push(chunk.toString());
363 return;
364 });
365 request.on('end', function () {
366 // This function needs documentation.
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 // If this is a standalone API server, then the "robots.txt" file
391 // won't be present.
392 rules.push({
393 method: 'GET',
394 pattern: /^\/robots\.txt$/,
395 handler: function (request, response) {
396 // This function returns a bare-minimum "robots.txt" file.
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 // This function needs documentation.
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 // This function needs documentation.
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 // That's all, folks!
462
463 return;
464
465}());
466
467//- vim:set syntax=javascript: