artdaq_node_server  v1_00_08
 All Classes Namespaces Files Variables Pages
serverbase.js
1 #!node
2 // serverbase.js : v0.5 : Node HTTPS Server
3 // Author: Eric Flumerfelt, FNAL RSI
4 // Last Modified: June 3, 2015
5 // Modified By: Eric Flumerfelt
6 //
7 // serverbase sets up a basic HTTPS server and directs requests
8 // to one of its submodules.
9 //
10 // Implementation Notes: modules should assign their emitter to the module_holder[<modulename>] object
11 // modules will emit 'data' and 'end' signals and implement the function MasterInitFunction()
12 
13 var cluster = require('cluster');
14 var numCPUs = require("os").cpus().length;
15 var fs = require('fs');
16 var path_module = require('path');
17 var module_holder = {};
18 var workerData = {
19  "serverbase": {}
20 };
21 var child_process = require('child_process');
22 
23 var util = require('util');
24 var log_file = fs.createWriteStream('/tmp/server.' + process.env["USER"] + '.log', { flags: 'a' });
25 var log_stdout = process.stdout;
26 
27 var getversion = function () {
28  console.log("Getting Server Version");
29  if (fs.existsSync("./version.txt")) {
30  console.log("Reading Server Version from File");
31  return "" + fs.readFileSync("./version.txt");
32  }
33  else {
34  child_process.exec("git describe --tags", function (error, stdout, stderr) {
35  version = stdout.trim() + "-Git";
36  child_process.exec("git status --porcelain", function (error, stdout) {
37  if (stdout.length > 0) {
38  version += "*";
39  }
40  });
41  });
42  }
43 }
44 var version = getversion();
45 
46 var config = {
47  ignored_modules: [],
48  baseport: 8080,
49  portOffset: 80,
50  hostname: "localhost",
51  module_config: []
52 };
53 function loadConfig() {
54  if (fs.existsSync("config.json")) {
55  config = JSON.parse(fs.readFileSync("config.json"));
56  } else {
57  fs.writeFileSync("config.json", JSON.stringify(config));
58  }
59 
60  if (process.env.ARTDAQDEMO_BASE_PORT) {
61  config.baseport = parseInt(process.env["ARTDAQDEMO_BASE_PORT"]) + config.portOffset;
62  }
63 
64  if (config.hostname === "localhost" && cluster.isMaster) {
65  console.log("Listening only on localhost. To listen on a different address, set \"hostname\" in config.json.\nUse \"0.0.0.0\" to listen on all interfaces.");
66  }
67 }
68 
69 loadConfig();
70 
71 console.log = function (d) { //
72  log_file.write(util.format(d) + '\n');
73  log_stdout.write(util.format(d) + '\n');
74 };
75 
76 function LoadCerts(path) {
77  var output = [];
78  var files = fs.readdirSync(path);
79  for (var i = 0; i < files.length; i++) {
80  if (files[i].search(".pem") > 0 || files[i].search(".crt") > 0) {
81  output.push(fs.readFileSync(path + "/" + files[i]));
82  }
83  }
84  return output;
85 }
86 
87 function GetCILogonCRL(path) {
88  // Always fetch the latest CRL lists from CILogon:
89  var file = fs.createWriteStream(path_module.join(path, "cilogon-basic.r0"));
90  http.get("http://crl-cilogon.ncsa-security.net/cilogon-basic.r0", function (res) { res.pipe(file); });
91  var file2 = fs.createWriteStream(path_module.join(path, "cilogon-basic.crl"));
92  http.get("http://crl-cilogon.ncsa-security.net/cilogon-basic.crl", function (res) { res.pipe(file2); });
93 }
94 
95 function LoadCRLs(path) {
96  GetCILogonCRL(path);
97  var output = [];
98  var files = fs.readdirSync(path);
99  for (var i = 0; i < files.length; i++) {
100  if (files[i].search(".r0") > 0 || files[i].search(".crl") > 0) {
101  output.push(fs.readFileSync(path + "/" + files[i]));
102  }
103  }
104  return output;
105 }
106 
107 // Sub-Module files
108 // From: http://stackoverflow.com/questions/10914751/loading-node-js-modules-dynamically-based-on-route
109 function LoadModules(path) {
110  var stat = fs.lstatSync(path);
111  if (stat.isDirectory()) {
112  // we have a directory: do a tree walk
113  var files = fs.readdirSync(path);
114  var f, l = files.length;
115  for (var i = 0; i < l; i++) {
116  f = path_module.join(path, files[i]);
117  LoadModules(f);
118  }
119  } else if (path.search("_module.js") > 0 && path.search("js~") < 0) {
120  for (var im in config.ignored_modules) {
121  if (config.ignored_modules.hasOwnProperty(im) && path.search(config.ignored_modules[im]) >= 0) {
122  return;
123  }
124  }
125  console.log("Loading Submodule " + path);
126  // we have a file: load it
127  // ReSharper disable once UseOfImplicitGlobalInFunctionScope
128  require(path)(module_holder);
129  console.log("Initialized Submodule " + path);
130  }
131 }
132 var DIR = path_module.join(__dirname, "modules");
133 LoadModules(DIR);
134 
135 // Node.js by default is single-threaded. Start multiple servers sharing
136 // the same port so that an error doesn't bring the whole system down
137 if (cluster.isMaster) {
138 
139  function messageHandler(msg) {
140  //console.log("Received message from worker");
141  if (!msg["name"]) {
142  //workerData = msg;
143  console.log("Depreciated message recieved!");
144  }
145  if (msg["name"]) {
146  if (msg["name"] === "request") {
147  console.log("Request for Worker Data received");
148  Object.keys(cluster.workers).forEach(function (id) {
149  cluster.workers[id].send(workerData);
150  });
151  }
152  else if (!msg["target"]) {
153  console.log("Depreciated message recieved!");
154  //console.log("Recieved message from worker: Setting workerData[" + msg.name + "].");
155  //workerData[msg.name] = msg.data;
156  //Object.keys( cluster.workers ).forEach( function ( id ) {
157  // cluster.workers[id].send( {name:msg.name, data:workerData[msg.name]} );
158  //} );
159  } else {
160  if (!msg["method"]) {
161  //console.log("Recieved message from worker: Setting workerData[" + msg.name + "]["+msg.target+"].");
162  workerData[msg.name][msg.target] = msg.data;
163  }
164  else if (msg["method"] === "push") {
165  //console.log("Recieved message from worker: Adding to workerData[" + msg.name + "]["+msg.target+"].");
166  workerData[msg.name][msg.target].push(msg.data);
167  }
168  Object.keys(cluster.workers).forEach(function (id) {
169  cluster.workers[id].send({ name: msg.name, target: msg.target, data: workerData[msg.name][msg.target] });
170  });
171  }
172  }
173  }
174 
175  // Call Master Init functions
176  for (var name in module_holder) {
177  if (module_holder.hasOwnProperty(name)) {
178  try {
179  module_holder[name].MasterInitFunction(workerData,config.module_config[name]);
180  } catch (err) {
181  ;
182  }
183  module_holder[name].on("message", messageHandler);
184  }
185  }
186  //fs.createWriteStream(__dirname + '/server.log', { flags : 'w' });
187 
188  cluster.on('online', function (worker) {
189  worker.send(workerData);
190  });
191 
192  // Start workers for each CPU on the host
193  for (var i = 0; i < numCPUs; i++) {
194  //for (var i = 0; i < 1; i++) {
195  var worker = cluster.fork();
196  worker.on('message', messageHandler);
197  }
198 
199  // If one dies, start a new one!
200  cluster.on("exit", function () {
201  var newWorker = cluster.fork();
202  newWorker.on('message', messageHandler);
203  });
204 } else {
205  // Node.js framework "includes"
206  var https = require('https');
207  var http = require('http');
208  var url = require('url');
209  var qs = require('querystring');
210 
211  function workerMessageHandler(msg) {
212  if (!msg["name"]) {
213  //console.log("Received Data Dump from Master!");
214  //console.log(JSON.stringify(msg));
215  workerData = msg;
216  for (var name in module_holder) {
217  if (module_holder.hasOwnProperty(name)) {
218  try {
219  module_holder[name].Update(workerData[name]);
220  } catch (err) {
221  ;
222  }
223  }
224  }
225  } else {
226  if (!msg["target"]) {
227  //console.log("Received message from master: Setting workerData[" + msg.name + "].");
228  workerData[msg.name] = msg.data;
229  } else {
230  //console.log("Received message from master: Setting workerData[" + msg.name + "][" + msg.target + "].");
231  workerData[msg.name][msg.target] = msg.data;
232  }
233  try {
234  module_holder[msg.name].Update(workerData[msg.name]);
235  } catch (err) { ; }
236  }
237  }
238 
239  process.send({ name: 'request' });
240  process.on('message', workerMessageHandler);
241 
242  for (var name in module_holder) {
243  if (module_holder.hasOwnProperty(name)) {
244  module_holder[name].on("message", function (data) {
245  //console.log("Received message from module " + data.name);
246  // ReSharper disable once UseOfImplicitGlobalInFunctionScope
247  process.send(data);
248  });
249  try {
250  module_holder[name].WorkerInitFunction(workerData);
251  } catch (err) {
252  ;
253  }
254  }
255  }
256 
257  function serve(req, res, readOnly, username) {
258  // req is the HTTP request, res is the response the server will send
259  // pathname is the URL after the http://host:port/ clause
260  var pathname = url.parse(req.url, true).pathname;
261  if (pathname[0] === '/') {
262  pathname = pathname.substr(1);
263  }
264 
265  var moduleName = pathname.substr(0, pathname.indexOf('/'));
266  var functionName = pathname.substr(pathname.indexOf('/') + 1);
267  if (workerData["serverbase"][req.connection.remoteAddress]) {
268  var clientInfo = workerData["serverbase"][req.connection.remoteAddress];
269  if (clientInfo.lastModuleName === moduleName && clientInfo.lastFunctionName === functionName) {
270  if (clientInfo.lastReqTime >= Date.now() - 500) {
271  console.log("Flood control active, denying request!");
272  res.writeHeader(200, { 'Content-Type': 'text/html' });
273  res.end("{\"Success\": true}");
274  return;
275  }
276  }
277  }
278  process.send({name: "serverbase", target:req.connection.remoteAddress, data: {
279  lastModuleName: moduleName,
280  lastFunctionName: functionName,
281  lastReqTime: Date.now()
282  }});
283 
284  var dnsDone = false;
285  // ReSharper disable once UseOfImplicitGlobalInFunctionScope
286  require('dns').reverse(req.connection.remoteAddress, function (err, domains) {
287  dnsDone = true;
288  if (!err) {
289  if (functionName.search(".min.map") < 0) {
290  // ReSharper disable UseOfImplicitGlobalInFunctionScope
291  console.log("Received " + req.method + ", Client: " + domains[0] + " [" + req.connection.remoteAddress + "], PID: " + process.pid + " Module: " + moduleName + ", function: " + functionName);
292  // ReSharper restore UseOfImplicitGlobalInFunctionScope
293  }
294  return domains[0];
295  } else {
296  if (functionName.search(".min.map") < 0) {
297  // ReSharper disable UseOfImplicitGlobalInFunctionScope
298  console.log("Received " + req.method + ", Client: " + req.connection.remoteAddress + ", PID: " + process.pid + " Module: " + moduleName + ", function: " + functionName);
299  // ReSharper restore UseOfImplicitGlobalInFunctionScope
300  }
301  return "";
302  }
303  });
304  if (functionName.search("GET_ServerVersion") >= 0) {
305  res.setHeader("Content-Type", "text/plain");
306  res.statusCode = 200;
307  res.end(version);
308  return;
309  }
310  if (moduleName === ".." || functionName.search("\\.\\.") >= 0) {
311  console.log("Possible break-in attempt!: " + pathname);
312  res.writeHeader(404, { 'Content-Type': 'text/html' });
313  res.end("Error");
314  return;
315  }
316  res.setHeader("Content-Type", "application/json");
317  res.statusCode = 200;
318  // Log to console...
319  //console.log("Received " + req.method + " for " + pathname);
320  //console.log("Proceeding...");
321 
322  // If we're recieving a POST to /runcommand (As defined in the module),
323  // handle that here
324  if (req.method === "POST") {
325  var body = "";
326 
327  // Callback for request data (may come in async)
328  req.on('data', function (data) {
329  body += data;
330  });
331 
332  req.on('end', function () {
333  // Get the content of the POST request
334  var post;
335  try {
336  post = JSON.parse(body);
337  } catch (e) {
338  post = qs.parse(body);
339  }
340  post.who = username;
341 
342  if (module_holder[moduleName] != null) {
343  console.log("Module " + moduleName + ", function " + functionName + " accessType " + (readOnly ? "RO" : "RW"));
344  var dataTemp = "";
345  module_holder[moduleName].removeAllListeners('data').on('data', function (data) {
346  dataTemp += data;
347  });
348  module_holder[moduleName].removeAllListeners('end').on('end', function (data) {
349  //console.log("POST Operation Complete, sending data to client: " + JSON.stringify(dataTemp + data));
350  res.end(JSON.stringify(dataTemp + data));
351  });
352  module_holder[moduleName].removeAllListeners('stream').on('stream', function (str, hdrs, code) {
353  console.log("Stream message received: " + hdrs + " CODE: " + code);
354  res.writeHead(code, hdrs);
355  str.pipe(res);
356  });
357  var data;
358  if (readOnly) {
359  try {
360  data = module_holder[moduleName]["RO_" + functionName](post, workerData[moduleName]);
361  if (data != null) {
362  //console.log("RO POST Returning: " + JSON.stringify(data));
363  res.end(JSON.stringify(data));
364  }
365  } catch (err) {
366  if (err instanceof TypeError) {
367  //console.log( "Unauthorized access attempt: " + username + ": " + moduleName + "/" + functionName );
368  res.end(JSON.stringify(null));
369  }
370  }
371  } else {
372  try {
373  data = module_holder[moduleName]["RW_" + functionName](post, workerData[moduleName]);
374  if (data != null) {
375  //console.log("RW POST returned data: " + JSON.stringify(data));
376  res.end(JSON.stringify(data));
377  }
378  } catch (err2) {
379  console.log("Error caught; text: " + JSON.stringify(err2));
380  if (err2 instanceof TypeError) {
381  //RW_ version not available, try read-only version:
382  data = module_holder[moduleName]["RO_" + functionName](post, workerData[moduleName]);
383  if (data != null) {
384  //console.log("RO Fallback POST returned data: " + JSON.stringify(data));
385  res.end(JSON.stringify(data));
386  }
387  }
388  }
389  }
390  } else {
391  console.log("Unknown POST URL: " + pathname);
392  res.writeHeader(404, { 'Content-Type': 'text/html' });
393  res.end("Error");
394  }
395  });
396  }
397  //We got a GET request!
398  if (req.method === "GET" || req.method === "HEAD") {
399  //console.log(req.headers);
400  if (functionName.indexOf(".") > 0) {
401  //console.log("Client File Access Requested");
402  var ext = functionName.substr(functionName.lastIndexOf(".") + 1);
403  res.setHeader("Content-Type", "text/plain");
404  //console.log("Extension: " + ext);
405  switch (ext) {
406  case "css":
407  res.setHeader("Content-Type", "text/css");
408  break;
409  case "js":
410  res.setHeader("Content-Type", "text/javascript");
411  break;
412  case "html":
413  res.setHeader("Content-Type", "text/html");
414  break;
415  case "htm":
416  res.setHeader("Content-Type", "text/html");
417  break;
418  case "root":
419  res.setHeader("Content-Type", "application/root+root.exe");
420  break;
421  case "gif":
422  res.setHeader("Content-Type", "image/gif");
423  break;
424  }
425 
426  var filename = "./modules/" + moduleName + "/client/" + functionName;
427  if (functionName.search("favicon.ico") >= 0) {
428  filename = "./modules/base/client/images/favicon.ico";
429  }
430  if (fs.existsSync(filename)) {
431  res.setHeader("Content-Length", fs.statSync(filename)["size"]);
432  if (req.headers.range != null) {
433  var range = req.headers.range;
434  var offset = parseInt(range.substr(range.indexOf('=') + 1, range.indexOf('-') - (range.indexOf('=') + 1)));
435  var endOffset = parseInt(range.substr(range.indexOf('-') + 1));
436  console.log("Reading (" + offset + ", " + endOffset + ")");
437 
438  res.setHeader("Content-Length", (endOffset - offset + 1).toString());
439  var readStream = fs.createReadStream(filename, { start: parseInt(offset), end: parseInt(endOffset) });
440  readStream.pipe(res);
441  } else {
442  res.end(fs.readFileSync(filename));
443  }
444  //console.log("Done sending file");
445  } else {
446  console.log("File not found: " + filename);
447  res.setHeader("Content-Type", "text/plain");
448  res.end("File Not Found.");
449  }
450  } else if (module_holder[moduleName] != null) {
451  //console.log("Module " + moduleName + ", function GET_" + functionName);
452 
453  var dataTemp = "";
454  module_holder[moduleName].removeAllListeners('data').on('data', function (data) {
455  //res.write(JSON.stringify(data));
456  dataTemp += data;
457  });
458  module_holder[moduleName].removeAllListeners('end').on('end', function (data) {
459  //console.log("GET Operation complete, sending response to client: " + JSON.stringify(dataTemp + data));
460  res.end(JSON.stringify(dataTemp + data));
461  });
462  module_holder[moduleName].removeAllListeners('stream').on('stream', function (str, hdrs, code) {
463  res.writeHead(code, hdrs);
464  str.pipe(res);
465  });
466  var data = module_holder[moduleName]["GET_" + functionName](workerData[moduleName]);
467  if (data != null) {
468  //console.log("GET Returned a value, sending response to client: " + JSON.stringify(data));
469  res.end(JSON.stringify(data));
470  }
471  } else {
472  console.log("Sending client.html");
473  // Write out the frame code
474  res.setHeader("Content-Type", "text/html");
475  res.end(fs.readFileSync("./client.html"), 'utf-8');
476  console.log("Done sending client.html");
477  }
478  }
479  };
480 
481  console.log("Setting up options");
482  var options = {
483  key: fs.readFileSync('./certs/server.key'),
484  cert: fs.readFileSync('./certs/server.crt'),
485  ca: LoadCerts("./certs/certificates"),
486  crl: LoadCRLs("./certs/certificates"),
487  requestCert: true,
488  rejectUnauthorized: false
489  };
490  var authlist = " " + fs.readFileSync("./certs/authorized_users");
491  console.log("Done setting up options");
492 
493  // Make an http server
494  var server = https.createServer(options, function (req, res) {
495  var readOnly = true;
496  var clientCertificate = req.connection.getPeerCertificate();
497  var username = "HTTPS User";
498  if (req.client.authorized) {
499  //console.log(JSON.stringify(clientCertificate));
500  //var org = clientCertificate.subject.O[0];
501  //if (org !== "Fermi National Accelerator Laboratory") {
502  // readOnly = true;
503  //}
504  username = clientCertificate.subject.CN[0];
505  var useremail = clientCertificate.subjectaltname.substr(6);
506  if (authlist.search(username) > 0 || authlist.search(useremail) > 0) {
507  readOnly = false;
508  }
509  }
510 
511  console.log("User: " + username + ", readOnly: " + readOnly);
512 
513  try {
514  serve(req, res, readOnly, username);
515  } catch (e) {
516  console.trace("Unhandled error in serve: " + JSON.stringify(e));
517  }
518  });
519  var insecureServer = http.createServer(function (req, res) {
520  // serve( req,res,true,"HTTP User" );
521  try {
522  serve(req, res, false, "HTTP User");
523  } catch (e) {
524  console.trace("Unhandled error in serve: " + JSON.stringify(e));
525  }
526  });
527 
528  console.log("Listening on " + config.hostname + " ports " + config.baseport + " and " + (config.baseport + 1));
529  server.listen(config.baseport + 1, config.hostname);
530  insecureServer.listen(config.baseport, config.hostname);
531 }