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