UNPKG

cloudcms-server

Version:
796 lines (639 loc) 23.2 kB
var path = require('path'); var fs = require('fs'); var temp = require('temp'); var url = require('url'); var async = require("async"); var util = require("./util/util"); var http = require('http'); var https = require('https'); var cluster = require("cluster"); var logFactory = require("./util/logger"); // system logger var systemLogger = logFactory("system", { wid: true }); if (typeof(process.env.CLOUDCMS_SYSTEM_LOGGER_LEVEL) !== "undefined") { systemLogger.setLevel(("" + process.env.CLOUDCMS_SYSTEM_LOGGER_LEVEL).toLowerCase(), true); } else { systemLogger.setLevel("info"); } process.logInfo = process.log = function(text, level) { systemLogger.log(text, level); }; // var debugLog = process.debugLog = function(req, message) // { // var text = "[" + req.id + "] URL: " + req.url; // // if (req.headers) // // { // // text += ", HEADERS: " + JSON.stringify(req.headers); // // } // if (req.query) // { // text += ", QUERY: " + JSON.stringify(req.query); // } // text += ", MESSAGE: " + message; // // console.log(text); // }; // // var debugMiddleware = process.debugMiddleware = function(message) // { // return function(req, res, next) // { // debugLog(req, message); // // next(); // } // }; // by default, set up Gitana driver so that it limits to five concurrent HTTP requests back to Cloud CMS API at at time var Gitana = require("gitana"); // default keep alive (3 minutes) process.defaultKeepAliveMs = (3 * 60 * 1000); // default http timeout (2 minutes) process.defaultHttpTimeoutMs = 2 * 60 * 1000; // default exclusive lock timeout (2 minutes) process.defaultExclusiveLockTimeoutMs = 2 * 60 * 1000; if (process.env.DEFAULT_HTTP_TIMEOUT_MS) { try { process.defaultHttpTimeoutMs = parseInt(process.env.DEFAULT_HTTP_TIMEOUT_MS); } catch (e) { } } // dns fix for Node 17 + // see: https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder var dns = require("dns"); if (typeof(dns.setDefaultResultOrder) !== "undefined") { dns.setDefaultResultOrder("ipv4first"); } // default agents var HttpKeepAliveAgent = require('agentkeepalive'); var HttpsKeepAliveAgent = require('agentkeepalive').HttpsAgent; http.globalAgent = new HttpKeepAliveAgent({ keepAlive: true, keepAliveMsecs: process.defaultKeepAliveMs, maxSockets: 1024, maxFreeSockets: 256, timeout: process.defaultHttpTimeoutMs, freeSocketTimeout: 5000 }); https.globalAgent = new HttpsKeepAliveAgent({ keepAlive: true, keepAliveMsecs: process.defaultKeepAliveMs, maxSockets: 1024, maxFreeSockets: 256, timeout: process.defaultHttpTimeoutMs, freeSocketTimeout: 5000 }); // install dns cache const CacheableLookup = require("cacheable-lookup"); const cacheable = new CacheableLookup({ // Set any custom options here }); cacheable.install(http.globalAgent); cacheable.install(https.globalAgent); // disable for now /* // report http/https socket state every minute var socketReportFn = function() { setTimeout(function() { var http = require("http"); var https = require("https"); process.log("--- START SOCKET REPORT ---"); process.log("[http]: " + JSON.stringify(http.globalAgent.getCurrentStatus(), null, " ")); process.log("[https]:" + JSON.stringify(https.globalAgent.getCurrentStatus(), null, " ")); process.log("--- END SOCKET REPORT ---"); socketReportFn(); }, 60 * 1000); }; socketReportFn(); */ // root ssl ca's require("ssl-root-cas").inject(); /** * Supports the following directory structure: * * * /hosts * * /<host> * * /public index.html * * /content * /local * /en_us * image.jpg * * /cloudcms * /<branchId> * /en_us * image.jpg * * @type {exports} */ exports = module.exports = function() { // track temporary files temp.track(); // TODO: this is to disable really annoying Express 3.0 deprecated's for multipart() which should hopefully // TODO: be resolved soon console.warn = function() {}; process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; // assume app-server base path if none provided if (!process.env.CLOUDCMS_APPSERVER_BASE_PATH) { process.env.CLOUDCMS_APPSERVER_BASE_PATH = process.cwd(); } // if there is a "gitana.json" file in the app server base path, we load proxy settings from there if they're // not already specified var defaultGitanaProxyScheme = "https"; var defaultGitanaProxyHost = "api.cloudcms.com"; var defaultGitanaProxyPath = ""; var defaultGitanaProxyPort = 443; var gitanaJsonPath = path.join(process.env.CLOUDCMS_APPSERVER_BASE_PATH, "gitana.json"); if (fs.existsSync(gitanaJsonPath)) { var gitanaJson = util.jsonParse("" + fs.readFileSync(gitanaJsonPath)); if (gitanaJson && gitanaJson.baseURL) { var parsedUrl = url.parse(gitanaJson.baseURL); defaultGitanaProxyHost = parsedUrl.hostname; defaultGitanaProxyPath = parsedUrl.path; defaultGitanaProxyScheme = parsedUrl.protocol.substring(0, parsedUrl.protocol.length - 1); // remove the : if (parsedUrl.port) { defaultGitanaProxyPort = parsedUrl.port; } else { defaultGitanaProxyPort = 80; if (defaultGitanaProxyScheme === "https") { defaultGitanaProxyPort = 443; } } } } // init if (!process.env.GITANA_PROXY_SCHEME) { process.env.GITANA_PROXY_SCHEME = defaultGitanaProxyScheme; } if (!process.env.GITANA_PROXY_HOST) { process.env.GITANA_PROXY_HOST = defaultGitanaProxyHost; } if (!process.env.GITANA_PROXY_PATH) { process.env.GITANA_PROXY_PATH = defaultGitanaProxyPath; } if (!process.env.GITANA_PROXY_PORT) { process.env.GITANA_PROXY_PORT = defaultGitanaProxyPort; } if (cluster.isMaster) { process.log("Gitana Proxy pointed to: " + util.asURL(process.env.GITANA_PROXY_SCHEME, process.env.GITANA_PROXY_HOST, process.env.GITANA_PROXY_PORT, process.env.GITANA_PROXY_PATH)); } // all web modules are included by default if (!process.includeWebModule) { process.includeWebModule = function(host, moduleId) { return true; }; } // middleware var admin = require("./middleware/admin/admin"); var authentication = require("./middleware/authentication/authentication"); var authorization = require("./middleware/authorization/authorization"); var cache = require("./middleware/cache/cache"); var cloudcms = require("./middleware/cloudcms/cloudcms"); var config = require("./middleware/config/config"); var debug = require("./middleware/debug/debug"); var deployment = require("./middleware/deployment/deployment"); var driver = require("./middleware/driver/driver"); var driverConfig = require("./middleware/driver-config/driver-config"); var final = require("./middleware/final/final"); var flow = require("./middleware/flow/flow"); var form = require("./middleware/form/form"); var healthcheck = require("./middleware/healthcheck/healthcheck"); var host = require("./middleware/host/host"); var graphql = require("./middleware/graphql/graphql"); var libraries = require("./middleware/libraries/libraries"); var local = require("./middleware/local/local"); var locale = require("./middleware/locale/locale"); var modules = require("./middleware/modules/modules"); var perf = require("./middleware/perf/perf"); var proxy = require("./middleware/proxy/proxy"); var registration = require("./middleware/registration/registration"); var resources = require("./middleware/resources/resources"); var runtime = require("./middleware/runtime/runtime"); var storeService = require("./middleware/stores/stores"); var templates = require("./middleware/templates/templates"); var virtualConfig = require("./middleware/virtual-config/virtual-config"); var virtualFiles = require("./middleware/virtual-files/virtual-files"); var welcome = require("./middleware/welcome/welcome"); var awareness = require("./middleware/awareness/awareness"); var userAgent = require('express-useragent'); // services var notifications = require("./notifications/notifications"); var broadcast = require("./broadcast/broadcast"); var locks = require("./locks/locks"); // cache //process.cache = cache; // read the package.json file and determine the build timestamp var packageJsonPath = path.resolve(__dirname, "package.json"); if (fs.existsSync(packageJsonPath)) { var packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()); process.env.CLOUDCMS_APPSERVER_PACKAGE_NAME = packageJson.name; process.env.CLOUDCMS_APPSERVER_PACKAGE_VERSION = packageJson.version; } // object that we hand back var r = {}; r.init = function(app, callback) { if (process.configuration && process.configuration.gitana) { if (typeof(process.configuration.gitana.httpWorkQueueSize) !== "undefined") { if (process.configuration.gitana.httpWorkQueueSize > -1) { Gitana.HTTP_WORK_QUEUE_SIZE = process.configuration.gitana.httpWorkQueueSize; process.log("Limiting Gitana Driver HTTP work queue to size: " + Gitana.HTTP_WORK_QUEUE_SIZE); } } } process.cache = app.cache = cache; process.broadcast = app.broadcast = broadcast; process.locks = app.locks = locks; process.authentication = app.authentication = authentication; // app specific app.filter = process.authentication.filter; var fns = [ locks.init, broadcast.start, storeService.init, notifications.start, cache.init, awareness.init ]; async.series(fns, function(err) { callback(err); }); }; r.common1 = function(app) { // app config interceptor applyApplicationConfiguration(app); // sets locale onto the request app.use(locale.localeInterceptor()); // sets host onto the request app.use(host.hostInterceptor()); }; r.common2 = function(app) { // bind stores into the request app.use(storeService.storesInterceptor()); // puts req.descriptor into the request and req.virtualFiles = true app.use(virtualFiles.interceptor()); // puts req.runtime into the request app.use(runtime.interceptor()); // if virtual hosting is enabled, loads "gitana.json" from cloud cms and places it into rootStore // for convenience, also populates req.gitanaConfig app.use(virtualConfig.interceptor()); // general method for finding "gitana.json" in root store and populating req.gitanaConfig app.use(driverConfig.interceptor()); }; r.common3 = function(app) { // binds "req.gitana" into the request for the loaded "req.gitanaConfig" app.use(driver.driverInterceptor()); }; r.common4 = function(app, includeCloudCMS) { var configuration = app.configuration; if (includeCloudCMS) { // bind a cache helper app.use(cache.cacheInterceptor()); // auto-select the application app.use(cloudcms.applicationInterceptor()); // auto-select the application settings app.use(cloudcms.applicationSettingsInterceptor()); // auto-select which gitana repository to use app.use(cloudcms.repositoryInterceptor()); // auto-select which gitana branch to use // allows for branch specification via request parameter app.use(cloudcms.branchInterceptor()); // auto-select which gitana domain to use app.use(cloudcms.domainInterceptor()); // enables ICE menu // app.use(cloudcms.iceInterceptor()); // enables cms logging app.use(cloudcms.cmsLogInterceptor()); } // graphql app.use(graphql.interceptor()); }; r.perf1 = function(app) { app.use(perf.pathPerformanceInterceptor()); }; r.proxy = function(app) { app.use(proxy.proxy()); }; r.perf2 = function(app) { app.use(perf.mimeTypePerformanceInterceptor()); }; r.perf3 = function(app) { app.use(perf.developmentPerformanceInterceptor()); }; var applyApplicationConfiguration = r.applyApplicationConfiguration = function(app) { // binds req.config describing the proper app config to use for the request's current application app.use(function(req, res, next) { var finish = function(configuration) { req.configuration = function(name, callback) { if (typeof(name) === "function") { return callback(null, configuration); } if (!name) { return callback(); } callback(null, configuration[name]); }; req.isEnabled = function(name) { return (configuration && configuration[name] && configuration[name].enabled); }; next(); }; var configuration = util.clone(process.configuration); if (req.application) { req.application(function(err, application) { if (application) { var applicationConfiguration = application.runtime; if (applicationConfiguration) { // merge configs util.merge(applicationConfiguration, configuration); } finish(configuration); } else { finish(configuration); } }); } else { finish(configuration); } }); }; r.welcome = function(app) { // support for "welcome" files (i.e. index.html) app.use(welcome.welcomeInterceptor()); }; r.healthcheck = function(app) { // support for healthcheck urls app.use(healthcheck.handler()); }; r.interceptors = function(app, includeCloudCMS) { var configuration = app.configuration; // authentication interceptor (binds helper methods) app.use(authentication.interceptor()); // authorization interceptor app.use(authorization.authorizationInterceptor()); // supports user-configured dynamic configuration app.use(config.remoteConfigInterceptor()); }; r.handlers = function(app, includeCloudCMS) { if (includeCloudCMS) { // handles /login and /logout for cloudcms principals app.use(cloudcms.authenticationHandler(app)); } // handles awareness commands app.use(awareness.handler()); // handles admin commands app.use(admin.handler()); // handles debug commands app.use(debug.handler()); // handles deploy/undeploy commands app.use(deployment.handler()); // serve back static configuration app.use(config.staticConfigHandler()); // serve back dynamic configuration app.use(config.remoteConfigHandler()); // handles calls to the templates service app.use(templates.handler()); // handles calls to the modules service app.use(modules.handler()); // handles calls to resources app.use(resources.handler()); // handles thirdparty browser libraries that are included with cloudcms-server app.use(libraries.handler()); // authentication app.use(authentication.handler(app)); if (includeCloudCMS) { // handles virtualized content retrieval from cloud cms app.use(cloudcms.virtualNodeHandler()); // handles virtualized principal retrieval from cloud cms app.use(cloudcms.virtualPrincipalHandler()); } // registration app.use(registration.handler()); // handles calls to web flow controllers app.use(flow.handlers()); // handles calls to form controllers app.use(form.formHandler()); // handles runtime status calls app.use(runtime.handler()); // handles virtualized local content retrieval from disk //app.use(local.webStoreHandler()); // handles default content retrieval from disk (this includes virtualized) app.use(local.defaultHandler()); // add User-Agent device info to req app.use(userAgent.express()); // handles 404 app.use(final.finalHandler()); }; r.bodyParser = function() { return function(req, res, next) { if (req._body) { return next(); } var contentType = req.get("Content-Type"); //if (contentType == "application/json" && req.method.toLowerCase() == "post") { if (req.method.toLowerCase() == "post") { req._body = true; var responseString = ""; req.on('data', function(data) { responseString += data; }); req.on('end', function() { if (responseString.length > 0) { try { var b = JSON.parse(responseString); if (b) { req.body = b; } } catch (e) { } } next(); }); } else { next(); } }; }; /** * Ensures that headers are set to enable CORS cross-domain functionality. * * @returns {Function} */ r.ensureCORS = function() { return util.createInterceptor("cors", function(req, res, next, stores, cache, configuration) { var origin = configuration.origin; if (!origin) { origin = req.headers["origin"]; } if (!origin) { origin = req.headers["x-cloudcms-origin"]; } if (!origin) { origin = "*"; } var methods = configuration.methods; var headers = configuration.headers; var credentials = configuration.credentials; util.setHeaderOnce(res, "Access-Control-Allow-Origin", origin); if (methods) { util.setHeaderOnce(res, "Access-Control-Allow-Methods", methods); } if (headers) { util.setHeaderOnce(res, "Access-Control-Allow-Headers", headers); } if (credentials) { util.setHeaderOnce(res, "Access-Control-Allow-Credentials", "" + credentials); } // res.set('Access-Control-Allow-Max-Age', 3600); if ('OPTIONS' === req.method) { return res.sendStatus(200); } next(); }); }; r.ensureHeaders = function() { return function(req, res, next) { // defaults var xFrameOptions = "SAMEORIGIN"; var xXssProtection = "1; mode=block"; // TODO: allow overrides here? if (xFrameOptions) { util.setHeaderOnce(res, "X-Frame-Options", xFrameOptions); } if (xXssProtection) { util.setHeaderOnce(res, "X-XSS-Protection", xXssProtection) } util.setHeaderOnce(res, "X-Powered-By", "Cloud CMS"); next(); }; }; var stringifyError = function(err) { var stack = err.stack; if (stack) { return String(stack) } return JSON.stringify(err, null, " "); }; r.consoleErrorLogger = function(app, callback) { // generic logger to console app.use(function(err, req, res, next) { console.error(stringifyError(err)); next(err); }); callback(); }; var errorHandler = require("errorhandler"); r.refreshTokenErrorHandler = function(app, callback) { app.use(function(err, req, res, next) { if (err) { if (req.method.toLowerCase() === "get") { if (err.status === 401) { if (err.message) { if (err.message.toLowerCase().indexOf("expired") > -1) { if (err.message.toLowerCase().indexOf("refresh") > -1) { var url = req.path; process.log("Refresh Token Expired, re-requesting resource: " + url); var html = ""; html += "<html>"; html += "<head>"; html += "<meta http-equiv='refresh' content='1;URL=" + url + "'>"; html += "</head>"; html += "<body>"; html += "</body>"; html += "</html>"; return res.status(200).type("text/html").send(html); } } } } } } next(err); }); callback(); }; r.defaultErrorHandler = function(app, callback) { app.use(function(err, req, res, next) { // use the stock error handler errorHandler()(err, req, res, next); }); callback(); }; return r; }();