UNPKG

js-cgi

Version:

Web Process Manager for Node.js.

412 lines (379 loc) 13.2 kB
/******************************************* * js-cgi.js * Copyright (c) 2015, Darrel Kathan * Licensed under the MIT license. * * A current version and some documentation is available at * https://github.com/kathan/js-cgi * * @summary Javascript CGI process manager * @description js-cgi is a javascript CGI process manager, similar to php-fpm, for executing node.js compatible scripts behind NGINX or Apache. * @file js-cgi.js * @version 1.1.1 * @author Darrel Kathan * @license MIT * 2/16/16 - Added cluster node cache * 7/6/16 - Added script timeout override * 2/16/17 - Added middleware option * 4/13/17 - Major timeout bug fix * 11/3/17 - Added cgi function * 11/29/17 - Added path_info *******************************************/ "use strict"; var cluster = require("cluster"), _ = require("lodash"), url = require("url"), fs = require("fs"), vm = require("vm"), os = require("os"), util = require("util"), path = require("path"), express = require("express"), app = express(), config_name = "js-cgi.config", use_name = "use.js", config = {}; /****************************************** * Capture console.log and console.error * input to log file. *******************************************/ var console_log = console.log, console_error = console.error; console.log = function() { var d = new Date(), log_msg = d.toString() + " - (" + process.pid + "): "; if (config.output_log) { fs.appendFile( config.output_log, log_msg + util.format.apply(this, arguments) + "\n", function() { return; } ); } return console_log.apply(this, arguments); }; console.error = function(err) { var d = new Date(), log_msg = d.toString() + " - (" + process.pid + "): "; if (config.error_log) { fs.appendFile( config.error_log, log_msg + util.format.apply(this, arguments) + "\n", function() { return; } ); if (err && err.stack) { fs.appendFile( config.error_log, err.stack + util.format.apply(this, arguments) + "\n", function() { return; } ); } } return console_error.apply(this, arguments); }; /******************************************* * Prepare web server to handle cookies and * format JSON. *******************************************/ app.set("json spaces", 2); //console.log(config.output_log); if (fs.existsSync(path.join(__dirname, config_name))) { //Load the congfig file console.log("Loading " + config_name + "..."); config = require("./" + config_name); } if (fs.existsSync(path.join(__dirname, use_name))) { //Load the congfig file console.log("Loading " + use_name + "..."); var mid_ware = require("./" + use_name); if (typeof mid_ware === "function") { mid_ware(app); } else { console.error("Use file is malformed."); } } //Set default config items !config.output_log ? (config.output_log = path.dirname(process.argv[1]) + "/js-cgi.log") : ""; !config.error_log ? (config.error_log = path.dirname(process.argv[1]) + "/err.log") : ""; !config.port ? (config.port = 3000) : ""; !config.localhostOnly ? (config.localhostOnly = true) : ""; !config.timeout ? (config.timeout = 30000) : ""; !config.workers ? (config.workers = os.cpus().length - 1) : ""; //Subtract one for the master !config.watch_required ? (config.watch_required = false) : ""; /******************************************* * Override the "require" function to watch * if required files have change and expire * them from cache when they do. *******************************************/ var assert = require("assert").ok, Module = require("module"); if (typeof Module._watching !== "object") { Module._watching = {}; } function watchRequired(fn) { /*var fs = require('fs') console = console;*/ //console.log(fn); if (Module._watching && !Module._watching.hasOwnProperty(fn)) { if (fs.existsSync(fn) && !Module._watching.hasOwnProperty(fn)) { console.log("Watching " + fn); Module._watching[fn] = fs.watch(fn, (event, f) => { //Module._watching[fn] = fs.watchFile(fn, function(curr, prev){ console.log(fn, event); if (Module._cache.hasOwnProperty(fn)) { console.log("Expiring " + fn); //fs.unwatch(fn, function(){return;}); return delete Module._cache[fn]; } return; }); } } } Module.prototype.require = function(path) { assert(path, "missing path"); assert(typeof path === "string", "path must be a string"); if (config.watch_required) { watchRequired(Module._resolveFilename(path, this)); } return Module._load(path, this); }; /******************************************* * Start up the server node. *******************************************/ if (cluster.isMaster) { cluster.globals = {}; cluster.fork().on("error", console.error); //At least one worker is required. console.log(process.versions); for (var w = 2; w <= config.workers; w++) { cluster.fork().on("error", console.error); } cluster.on("disconnect", worker => { cluster.fork().on("error", console.error); }); cluster.on("listening", (worker, address) => { console.log("A worker is now connected to " + JSON.stringify(address)); }); } else { // the worker var domain = require("domain"), server, d = domain.create(); d.on("error", err => { console.error("Domain Error:", err, err.stack); //console.trace(); try { // make sure we close down if it times out //var killtimer = setTimeout(() => { //var err = new Error('Killing process.'); //console.error('Domain Error:', err); //console.trace(); console.log("Killing process."); process.exit(1); //}, config.timeout); // But don't keep the process open just for that! //killtimer.unref(); // stop taking new requests. server.close(); // Let the master know we're dead. This will trigger a // 'disconnect' in the cluster master, and then it will fork // a new worker. cluster.worker.disconnect(); } catch (er2) { // oh well, not much we can do at this point. console.error(er2); } }); /******************************************* * Now run the handler function in the domain. *******************************************/ d.run(() => { console.log("Listening on " + config.port); server = app.listen(config.port); app.all("*", (req, res) => { //console.log('req'); return handleRequest(req, res); }); }); } /******************************************* * This is where all of the web requests are * handled. *******************************************/ function handleRequest(req, res) { //console.log('request:', req); var url_obj = url.parse(req.url, true), file_path; console.log(req.headers); /*var cache = [], j = JSON.stringify(req, function(key, value) { if (typeof value === 'object' && value !== null) { var idx = _.findIndex(cache, value); if (idx > -1) {return '[Circular '+idx+']';} cache.push(value); } return value; }); res.send(j);*/ //console.log('req.connection.remoteFamily:', '"'+req.connection.remoteFamily+'"'); //console.log('req.connection.remoteAddress:', '"'+req.connection.remoteAddress+'"'); if (config.localhostOnly) { if ( (req.connection.remoteFamily === "IPv4" && req.connection.remoteAddress !== "127.0.0.1") || (req.connection.remoteFamily === "IPv6" && (!_.endsWith(req.connection.remoteAddress, "::1") && !_.endsWith(req.connection.remoteAddress, ":127.0.0.1"))) ) { return res.sendStatus(401); } } //Set to the directory path if (!req.headers.path_translated) { req.headers.path_translated = __dirname + "/www"; //console.log(req.headers.path_translated); if (!fs.existsSync(req.headers.path_translated)) { fs.mkdirSync(req.headers.path_translated); } } // if(req.headers.path_translated && req.headers.script_name) file_path = req.headers.path_translated + req.headers.script_name; else if (req.headers.path_translated && url_obj.pathname) { file_path = req.headers.path_translated + url_obj.pathname; } else { file_path = url_obj.pathname; } console.log(req.method + ": " + file_path); function resolveModule(module) { if (module.charAt(0) !== ".") { return module; } return path.resolve(path.dirname(file_path), module); } //If the requested file exists... fs.exists(file_path, exists => { if (exists) { fs.readFile(file_path, (err, source) => { if (err) { //Error reading file console.error(err); return res .status(500) .send({ error: err.toString(), stack: err.stack }); } try { //console.log(require.cache); //var timer = 'not set'; req.timeout = config.timeout; var timer, sandbox = { console: console, setImmediate: setImmediate, setInterval: setInterval, setTimeout: setTimeout, clearTimeout: clearTimeout, Buffer: Buffer, JSON: JSON, require: function(name) { var mod_path = resolveModule(name); //watchRequired(mod_path); return require(mod_path); }, req: req, res: res, process: process, __dirname: path.dirname(file_path), __filename: path.basename(file_path) }, setKilltimer = function() { //!req.timeout ? (req.timeout = config.timeout) : ""; var to_start = Date.now(); var timeout = req.timeout; if (timeout > 0) { return setTimeout(() => { var to_end = Date.now(), msg = "Timeout expired (" + (to_end - to_start) + " ms) for " + file_path, err; //if(!res.headerSent){ res.status(408).send(msg); //} //err = new Error(msg); console.error(msg); return; }, req.timeout); } }; /**************************** * Capture "res.send" and * "res.sendFile" calls to * be able to unset timeout * when the script ends. */ var send = res.send; res.send = function(){ if(timer){ console.log('timeout cleared'); clearTimeout(timer); } return send.apply(this, arguments); }; var sendFile = res.sendFile; res.sendFile = function(){ if(timer){ console.log('timeout cleared'); clearTimeout(timer); } return sendFile.apply(this, arguments); } /***************************/ /***************** * Start VM Code * *****************/ var c = vm.createContext(sandbox), code = `function runInContext() {try{${source} //0 if(typeof cgi === 'function'){ //1 cgi(req, res); //2 } //3 }catch(e){ //4 console.error(e); //5 res.status(500).send({error: e.toString(), stack: e.stack});//6 } //7 } //8 //9 runInContext();`; /*************** * End VM Code * ***************/ /*************************************************** * Set the kill timer after executing the script so * the script can set the timeout duration. ***************************************************/ var result = vm.runInContext(code, c, file_path); timer = setKilltimer(); return result; } catch (err) { console.error(err); return res .status(500) .send({ error: err.toString(), stack: err.stack }); } }); } else { //File does not exist return res.status(404).send("File not found. " + file_path); } }); }