From 00d41c6de2f14aaafe03aadf1e456c6ba5078364 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Sat, 2 May 2020 07:52:20 +0200 Subject: [PATCH 01/17] Refresh https settings --- packages/node_modules/node-red/red.js | 41 ++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 26b6d1fc8..92b5b5a36 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -143,7 +143,46 @@ if (process.env.NODE_RED_ENABLE_PROJECTS) { } if (settings.https) { - server = https.createServer(settings.https,function(req,res) {app(req,res);}); + var startupHttps = settings.https; + + if (typeof startupHttps === "function") { + // Get the result of the function, because createServer doesn't accept functions as input + startupHttps = startupHttps(); + } + + server = https.createServer(startupHttps,function(req,res) {app(req,res);}); + + // Refresh https settings at intervals for NodeJs version 11 and above + if (settings.httpsRefreshInterval) { + if (typeof startupHttps === "function") { + if (server.setSecureContext) { + console.log("Refreshing https settings every " + parseInt(settings.credentialRenewalTime) + " seconds."); + setInterval(function () { + try { + // Get the result of the function, because createServer doesn't accept functions as input + var refreshedHttps = settings.https(); + + if (!refreshedHttps.key || !refreshedHttps.cert) { + console.log("Cannot refresh the https settings when the https property function doesn't return a 'key' and 'cert'."); + return; + } + + // Only update the credentials in the server when key or cert has changed + if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { + server.setSecureContext(refreshedHttps); + console.log("The https settings have been refreshed."); + } + } catch(err) { + console.log("Failed to refresh the https settings: " + err); + } + }, parseInt(settings.credentialRenewalTime) * 1000); + } else { + console.log("Cannot refresh the https settings automatically, because NodeJs version 11 or above is required."); + } + } else { + console.log("Cannot refresh the https settings automatically (at httpsRefreshInterval), because the https property needs to be a function."); + } + } } else { server = http.createServer(function(req,res) {app(req,res);}); } From 9a19477796f363168c16fd72cc62a71edc480f20 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Sat, 2 May 2020 07:54:58 +0200 Subject: [PATCH 02/17] Refresh https settings --- packages/node_modules/node-red/settings.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index c5e4355e1..8be7bf9bd 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -139,13 +139,24 @@ module.exports = { // The following property can be used to enable HTTPS // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener // for details on its contents. - // See the comment at the top of this file on how to load the `fs` module used by - // this setting. - // + // See the comment at the top of this file on how to load the `fs` module used by this setting. + // This property can be an object, containing both a (private) key and a (public) certificate: //https: { - // key: fs.readFileSync('privatekey.pem'), - // cert: fs.readFileSync('certificate.pem') + // key: fs.readFileSync('privkey.pem'), + // cert: fs.readFileSync('cert.pem') //}, + // This property can also be a function (e.g. to automatic refresh the https settings): + //https: function() { + // return { + // key: fs.readFileSync('privkey.pem'), + // cert: fs.readFileSync('cert.pem') + // } + //}, + + // The following property can be used to refresh the https settings at regular time intervals (seconds). + // Prerequisite: the 'https' property should be enabled (based on a function)! + // Caution: NodeJs version 11 or above is required to use this option! + //httpsRefreshInterval : 3600, // The following property can be used to cause insecure HTTP connections to // be redirected to HTTPS. From f468d6e947dcb39fd17aa5d87759a1bcd5c3bbc4 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Sat, 2 May 2020 14:40:01 +0200 Subject: [PATCH 03/17] Rename fix --- packages/node_modules/node-red/red.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 92b5b5a36..79b3d8bb2 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -156,7 +156,7 @@ if (settings.https) { if (settings.httpsRefreshInterval) { if (typeof startupHttps === "function") { if (server.setSecureContext) { - console.log("Refreshing https settings every " + parseInt(settings.credentialRenewalTime) + " seconds."); + console.log("Refreshing https settings every " + parseInt(settings.httpsRefreshInterval) + " mseconds."); setInterval(function () { try { // Get the result of the function, because createServer doesn't accept functions as input @@ -175,7 +175,7 @@ if (settings.https) { } catch(err) { console.log("Failed to refresh the https settings: " + err); } - }, parseInt(settings.credentialRenewalTime) * 1000); + }, parseInt(settings.httpsRefreshInterval)); } else { console.log("Cannot refresh the https settings automatically, because NodeJs version 11 or above is required."); } From 4694644043e3c7adf8b89f1bb4fea9babe55139b Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Sat, 2 May 2020 14:41:46 +0200 Subject: [PATCH 04/17] Refresh interval in milliseconds --- packages/node_modules/node-red/settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 8be7bf9bd..64cafb116 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -153,10 +153,10 @@ module.exports = { // } //}, - // The following property can be used to refresh the https settings at regular time intervals (seconds). + // The following property can be used to refresh the https settings at regular time intervals (milliseconds). // Prerequisite: the 'https' property should be enabled (based on a function)! // Caution: NodeJs version 11 or above is required to use this option! - //httpsRefreshInterval : 3600, + //httpsRefreshInterval : 3600000, // The following property can be used to cause insecure HTTP connections to // be redirected to HTTPS. From e16f48c9fdbdbbf9b54831ab03494c9075f17628 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Sat, 2 May 2020 22:22:36 +0200 Subject: [PATCH 05/17] httpsRefreshInterval in seconds --- packages/node_modules/node-red/settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 64cafb116..8be7bf9bd 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -153,10 +153,10 @@ module.exports = { // } //}, - // The following property can be used to refresh the https settings at regular time intervals (milliseconds). + // The following property can be used to refresh the https settings at regular time intervals (seconds). // Prerequisite: the 'https' property should be enabled (based on a function)! // Caution: NodeJs version 11 or above is required to use this option! - //httpsRefreshInterval : 3600000, + //httpsRefreshInterval : 3600, // The following property can be used to cause insecure HTTP connections to // be redirected to HTTPS. From f7e0f55c1332331b394ad9df4df034ef1ec85e36 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Sat, 2 May 2020 22:24:04 +0200 Subject: [PATCH 06/17] httpsRefreshInterval in seconds --- packages/node_modules/node-red/red.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 79b3d8bb2..9543176d5 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -156,7 +156,7 @@ if (settings.https) { if (settings.httpsRefreshInterval) { if (typeof startupHttps === "function") { if (server.setSecureContext) { - console.log("Refreshing https settings every " + parseInt(settings.httpsRefreshInterval) + " mseconds."); + console.log("Refreshing https settings every " + parseInt(settings.httpsRefreshInterval) + " seconds."); setInterval(function () { try { // Get the result of the function, because createServer doesn't accept functions as input @@ -175,7 +175,7 @@ if (settings.https) { } catch(err) { console.log("Failed to refresh the https settings: " + err); } - }, parseInt(settings.httpsRefreshInterval)); + }, parseInt(settings.httpsRefreshInterval)*1000); } else { console.log("Cannot refresh the https settings automatically, because NodeJs version 11 or above is required."); } From 15f97bbf26a90ae1c691f902d2c4467f5bbe6b09 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Mon, 11 May 2020 23:29:38 +0200 Subject: [PATCH 07/17] Asynchronous https support --- packages/node_modules/node-red/settings.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 8be7bf9bd..3195f70ef 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -145,13 +145,20 @@ module.exports = { // key: fs.readFileSync('privkey.pem'), // cert: fs.readFileSync('cert.pem') //}, - // This property can also be a function (e.g. to automatic refresh the https settings): + // This property can also be a function (e.g. to automatic refresh the https settings synchronously): //https: function() { // return { // key: fs.readFileSync('privkey.pem'), // cert: fs.readFileSync('cert.pem') // } //}, + // This property can also be a promise (e.g. to automatic refresh the https settings asynchronously): + //https: function() { + // return Promise.resolve({ + // key: fs.readFileSync('privkey.pem'), + // cert: fs.readFileSync('cert.pem') + // }); + //}, // The following property can be used to refresh the https settings at regular time intervals (seconds). // Prerequisite: the 'https' property should be enabled (based on a function)! From bfa5f39b6d7e9bdab837b81fac67de6780512b64 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Mon, 11 May 2020 23:33:54 +0200 Subject: [PATCH 08/17] Asynchronous https support --- packages/node_modules/node-red/red.js | 481 ++++++++++++++------------ 1 file changed, 256 insertions(+), 225 deletions(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 9543176d5..ccbd6dc38 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -142,247 +142,278 @@ if (process.env.NODE_RED_ENABLE_PROJECTS) { settings.editorTheme.projects.enabled = !/^false$/i.test(process.env.NODE_RED_ENABLE_PROJECTS); } +var httpsPromise; if (settings.https) { var startupHttps = settings.https; - + if (typeof startupHttps === "function") { // Get the result of the function, because createServer doesn't accept functions as input startupHttps = startupHttps(); } - server = https.createServer(startupHttps,function(req,res) {app(req,res);}); + if (startupHttps.hasOwnProperty('then') && typeof startupHttps.then === 'function') { + // A promise was returned + httpsPromise = startupHttps; + } else { + // An object was returned - wrap in a promise + httpsPromise = Promise.resolve(startupHttps); + } +} +else { + // No https is enable - wrap null + httpsPromise = Promise.resolve(null); +} - // Refresh https settings at intervals for NodeJs version 11 and above - if (settings.httpsRefreshInterval) { - if (typeof startupHttps === "function") { - if (server.setSecureContext) { - console.log("Refreshing https settings every " + parseInt(settings.httpsRefreshInterval) + " seconds."); - setInterval(function () { - try { - // Get the result of the function, because createServer doesn't accept functions as input - var refreshedHttps = settings.https(); - - if (!refreshedHttps.key || !refreshedHttps.cert) { - console.log("Cannot refresh the https settings when the https property function doesn't return a 'key' and 'cert'."); - return; - } +httpsPromise.then(function(startupHttps) { + if (startupHttps) { + server = https.createServer(startupHttps,function(req,res) {app(req,res);}); + + // Refresh https settings at intervals for NodeJs version 11 and above + if (settings.httpsRefreshInterval) { + if (typeof settings.https === "function") { + if (server.setSecureContext) { + console.log("Refreshing https settings every " + parseInt(settings.httpsRefreshInterval) + " seconds."); + setInterval(function () { + try { + // Get the result of the function, because createServer doesn't accept functions as input + var refreshedHttps = settings.https(); - // Only update the credentials in the server when key or cert has changed - if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { - server.setSecureContext(refreshedHttps); - console.log("The https settings have been refreshed."); + var httpsPromise; + if (refreshedHttps.hasOwnProperty('then') && typeof refreshedHttps.then === 'function') { + // A promise was returned + httpsPromise = refreshedHttps; + } else { + // An object was returned - wrap in a promise + httpsPromise = Promise.resolve(refreshedHttps); + } + + httpsPromise.then(function(refreshedHttps) { + // Use the refreshed https settings + if (!refreshedHttps.key || !refreshedHttps.cert) { + console.log("Cannot refresh the https settings when the https property function doesn't return a 'key' and 'cert'."); + return; + } + + // Only update the credentials in the server when key or cert has changed + if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { + server.setSecureContext(refreshedHttps); + console.log("The https settings have been refreshed."); + } + }).catch(function(err) { + console.log("Failed to apply the refreshed https settings: " + err); + }); + } catch(err) { + console.log("Failed to get the refreshed https settings: " + err); } - } catch(err) { - console.log("Failed to refresh the https settings: " + err); - } - }, parseInt(settings.httpsRefreshInterval)*1000); - } else { - console.log("Cannot refresh the https settings automatically, because NodeJs version 11 or above is required."); - } - } else { - console.log("Cannot refresh the https settings automatically (at httpsRefreshInterval), because the https property needs to be a function."); - } - } -} else { - server = http.createServer(function(req,res) {app(req,res);}); -} -server.setMaxListeners(0); - -function formatRoot(root) { - if (root[0] != "/") { - root = "/" + root; - } - if (root.slice(-1) != "/") { - root = root + "/"; - } - return root; -} - -if (settings.httpRoot === false) { - settings.httpAdminRoot = false; - settings.httpNodeRoot = false; -} else { - settings.httpRoot = settings.httpRoot||"/"; - settings.disableEditor = settings.disableEditor||false; -} - -if (settings.httpAdminRoot !== false) { - settings.httpAdminRoot = formatRoot(settings.httpAdminRoot || settings.httpRoot || "/"); - settings.httpAdminAuth = settings.httpAdminAuth || settings.httpAuth; -} else { - settings.disableEditor = true; -} - -if (settings.httpNodeRoot !== false) { - settings.httpNodeRoot = formatRoot(settings.httpNodeRoot || settings.httpRoot || "/"); - settings.httpNodeAuth = settings.httpNodeAuth || settings.httpAuth; -} - -// if we got a port from command line, use it (even if 0) -// replicate (settings.uiPort = parsedArgs.port||settings.uiPort||1880;) but allow zero -if (parsedArgs.port !== undefined){ - settings.uiPort = parsedArgs.port; -} else { - if (settings.uiPort === undefined){ - settings.uiPort = 1880; - } -} - -settings.uiHost = settings.uiHost||"0.0.0.0"; - -if (flowFile) { - settings.flowFile = flowFile; -} -if (parsedArgs.userDir) { - settings.userDir = parsedArgs.userDir; -} - -try { - RED.init(server,settings); -} catch(err) { - if (err.code == "unsupported_version") { - console.log("Unsupported version of Node.js:",process.version); - console.log("Node-RED requires Node.js v8.9.0 or later"); - } else { - console.log("Failed to start server:"); - if (err.stack) { - console.log(err.stack); - } else { - console.log(err); - } - } - process.exit(1); -} - -function basicAuthMiddleware(user,pass) { - var basicAuth = require('basic-auth'); - var checkPassword; - var localCachedPassword; - if (pass.length == "32") { - // Assume its a legacy md5 password - checkPassword = function(p) { - return crypto.createHash('md5').update(p,'utf8').digest('hex') === pass; - } - } else { - checkPassword = function(p) { - return bcrypt.compareSync(p,pass); - } - } - - var checkPasswordAndCache = function(p) { - // For BasicAuth routes we know the password cannot change without - // a restart of Node-RED. This means we can cache the provided crypted - // version to save recalculating each time. - if (localCachedPassword === p) { - return true; - } - var result = checkPassword(p); - if (result) { - localCachedPassword = p; - } - return result; - } - - return function(req,res,next) { - if (req.method === 'OPTIONS') { - return next(); - } - var requestUser = basicAuth(req); - if (!requestUser || requestUser.name !== user || !checkPasswordAndCache(requestUser.pass)) { - res.set('WWW-Authenticate', 'Basic realm="Authorization Required"'); - return res.sendStatus(401); - } - next(); - } -} - -if (settings.httpAdminRoot !== false && settings.httpAdminAuth) { - RED.log.warn(RED.log._("server.httpadminauth-deprecated")); - app.use(settings.httpAdminRoot, basicAuthMiddleware(settings.httpAdminAuth.user,settings.httpAdminAuth.pass)); -} - -if (settings.httpAdminRoot !== false) { - app.use(settings.httpAdminRoot,RED.httpAdmin); -} -if (settings.httpNodeRoot !== false && settings.httpNodeAuth) { - app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass)); -} -if (settings.httpNodeRoot !== false) { - app.use(settings.httpNodeRoot,RED.httpNode); -} -if (settings.httpStatic) { - settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth; - if (settings.httpStaticAuth) { - app.use("/",basicAuthMiddleware(settings.httpStaticAuth.user,settings.httpStaticAuth.pass)); - } - app.use("/",express.static(settings.httpStatic)); -} - -function getListenPath() { - var port = settings.serverPort; - if (port === undefined){ - port = settings.uiPort; - } - - var listenPath = 'http'+(settings.https?'s':'')+'://'+ - (settings.uiHost == '::'?'localhost':(settings.uiHost == '0.0.0.0'?'127.0.0.1':settings.uiHost))+ - ':'+port; - if (settings.httpAdminRoot !== false) { - listenPath += settings.httpAdminRoot; - } else if (settings.httpStatic) { - listenPath += "/"; - } - return listenPath; -} - -RED.start().then(function() { - if (settings.httpAdminRoot !== false || settings.httpNodeRoot !== false || settings.httpStatic) { - server.on('error', function(err) { - if (err.errno === "EADDRINUSE") { - RED.log.error(RED.log._("server.unable-to-listen", {listenpath:getListenPath()})); - RED.log.error(RED.log._("server.port-in-use")); - } else { - RED.log.error(RED.log._("server.uncaught-exception")); - if (err.stack) { - RED.log.error(err.stack); + }, parseInt(settings.httpsRefreshInterval)*1000); } else { - RED.log.error(err); + console.log("Cannot refresh the https settings automatically, because NodeJs version 11 or above is required."); } - } - process.exit(1); - }); - server.listen(settings.uiPort,settings.uiHost,function() { - if (settings.httpAdminRoot === false) { - RED.log.info(RED.log._("server.admin-ui-disabled")); - } - settings.serverPort = server.address().port; - process.title = parsedArgs.title || 'node-red'; - RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()})); - }); + } else { + console.log("Cannot refresh the https settings automatically (at httpsRefreshInterval), because the https property needs to be a function."); + } + } } else { - RED.log.info(RED.log._("server.headless-mode")); + server = http.createServer(function(req,res) {app(req,res);}); } -}).otherwise(function(err) { - RED.log.error(RED.log._("server.failed-to-start")); - if (err.stack) { - RED.log.error(err.stack); - } else { - RED.log.error(err); - } -}); + server.setMaxListeners(0); -process.on('uncaughtException',function(err) { - util.log('[red] Uncaught Exception:'); - if (err.stack) { - util.log(err.stack); - } else { - util.log(err); + function formatRoot(root) { + if (root[0] != "/") { + root = "/" + root; + } + if (root.slice(-1) != "/") { + root = root + "/"; + } + return root; } - process.exit(1); -}); -process.on('SIGINT', function () { - RED.stop().then(function() { - process.exit(); + if (settings.httpRoot === false) { + settings.httpAdminRoot = false; + settings.httpNodeRoot = false; + } else { + settings.httpRoot = settings.httpRoot||"/"; + settings.disableEditor = settings.disableEditor||false; + } + + if (settings.httpAdminRoot !== false) { + settings.httpAdminRoot = formatRoot(settings.httpAdminRoot || settings.httpRoot || "/"); + settings.httpAdminAuth = settings.httpAdminAuth || settings.httpAuth; + } else { + settings.disableEditor = true; + } + + if (settings.httpNodeRoot !== false) { + settings.httpNodeRoot = formatRoot(settings.httpNodeRoot || settings.httpRoot || "/"); + settings.httpNodeAuth = settings.httpNodeAuth || settings.httpAuth; + } + + // if we got a port from command line, use it (even if 0) + // replicate (settings.uiPort = parsedArgs.port||settings.uiPort||1880;) but allow zero + if (parsedArgs.port !== undefined){ + settings.uiPort = parsedArgs.port; + } else { + if (settings.uiPort === undefined){ + settings.uiPort = 1880; + } + } + + settings.uiHost = settings.uiHost||"0.0.0.0"; + + if (flowFile) { + settings.flowFile = flowFile; + } + if (parsedArgs.userDir) { + settings.userDir = parsedArgs.userDir; + } + + try { + RED.init(server,settings); + } catch(err) { + if (err.code == "unsupported_version") { + console.log("Unsupported version of Node.js:",process.version); + console.log("Node-RED requires Node.js v8.9.0 or later"); + } else { + console.log("Failed to start server:"); + if (err.stack) { + console.log(err.stack); + } else { + console.log(err); + } + } + process.exit(1); + } + + function basicAuthMiddleware(user,pass) { + var basicAuth = require('basic-auth'); + var checkPassword; + var localCachedPassword; + if (pass.length == "32") { + // Assume its a legacy md5 password + checkPassword = function(p) { + return crypto.createHash('md5').update(p,'utf8').digest('hex') === pass; + } + } else { + checkPassword = function(p) { + return bcrypt.compareSync(p,pass); + } + } + + var checkPasswordAndCache = function(p) { + // For BasicAuth routes we know the password cannot change without + // a restart of Node-RED. This means we can cache the provided crypted + // version to save recalculating each time. + if (localCachedPassword === p) { + return true; + } + var result = checkPassword(p); + if (result) { + localCachedPassword = p; + } + return result; + } + + return function(req,res,next) { + if (req.method === 'OPTIONS') { + return next(); + } + var requestUser = basicAuth(req); + if (!requestUser || requestUser.name !== user || !checkPasswordAndCache(requestUser.pass)) { + res.set('WWW-Authenticate', 'Basic realm="Authorization Required"'); + return res.sendStatus(401); + } + next(); + } + } + + if (settings.httpAdminRoot !== false && settings.httpAdminAuth) { + RED.log.warn(RED.log._("server.httpadminauth-deprecated")); + app.use(settings.httpAdminRoot, basicAuthMiddleware(settings.httpAdminAuth.user,settings.httpAdminAuth.pass)); + } + + if (settings.httpAdminRoot !== false) { + app.use(settings.httpAdminRoot,RED.httpAdmin); + } + if (settings.httpNodeRoot !== false && settings.httpNodeAuth) { + app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass)); + } + if (settings.httpNodeRoot !== false) { + app.use(settings.httpNodeRoot,RED.httpNode); + } + if (settings.httpStatic) { + settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth; + if (settings.httpStaticAuth) { + app.use("/",basicAuthMiddleware(settings.httpStaticAuth.user,settings.httpStaticAuth.pass)); + } + app.use("/",express.static(settings.httpStatic)); + } + + function getListenPath() { + var port = settings.serverPort; + if (port === undefined){ + port = settings.uiPort; + } + + var listenPath = 'http'+(settings.https?'s':'')+'://'+ + (settings.uiHost == '::'?'localhost':(settings.uiHost == '0.0.0.0'?'127.0.0.1':settings.uiHost))+ + ':'+port; + if (settings.httpAdminRoot !== false) { + listenPath += settings.httpAdminRoot; + } else if (settings.httpStatic) { + listenPath += "/"; + } + return listenPath; + } + + RED.start().then(function() { + if (settings.httpAdminRoot !== false || settings.httpNodeRoot !== false || settings.httpStatic) { + server.on('error', function(err) { + if (err.errno === "EADDRINUSE") { + RED.log.error(RED.log._("server.unable-to-listen", {listenpath:getListenPath()})); + RED.log.error(RED.log._("server.port-in-use")); + } else { + RED.log.error(RED.log._("server.uncaught-exception")); + if (err.stack) { + RED.log.error(err.stack); + } else { + RED.log.error(err); + } + } + process.exit(1); + }); + server.listen(settings.uiPort,settings.uiHost,function() { + if (settings.httpAdminRoot === false) { + RED.log.info(RED.log._("server.admin-ui-disabled")); + } + settings.serverPort = server.address().port; + process.title = parsedArgs.title || 'node-red'; + RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()})); + }); + } else { + RED.log.info(RED.log._("server.headless-mode")); + } + }).otherwise(function(err) { + RED.log.error(RED.log._("server.failed-to-start")); + if (err.stack) { + RED.log.error(err.stack); + } else { + RED.log.error(err); + } + }); + + process.on('uncaughtException',function(err) { + util.log('[red] Uncaught Exception:'); + if (err.stack) { + util.log(err.stack); + } else { + util.log(err); + } + process.exit(1); + }); + + process.on('SIGINT', function () { + RED.stop().then(function() { + process.exit(); + }); }); }); From 90f62e5e4afe43d3a90aa98bf5e2203956793fec Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Wed, 13 May 2020 23:23:29 +0200 Subject: [PATCH 09/17] Update packages/node_modules/node-red/red.js Co-authored-by: Nick O'Leary --- packages/node_modules/node-red/red.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index ccbd6dc38..1b7e55dbb 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -150,14 +150,7 @@ if (settings.https) { // Get the result of the function, because createServer doesn't accept functions as input startupHttps = startupHttps(); } - - if (startupHttps.hasOwnProperty('then') && typeof startupHttps.then === 'function') { - // A promise was returned - httpsPromise = startupHttps; - } else { - // An object was returned - wrap in a promise - httpsPromise = Promise.resolve(startupHttps); - } + httpsPromise = Promise.resolve(startupHttps); } else { // No https is enable - wrap null From f4d4bf8779cffefdb6415ce6d8b5f2015b621cee Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Wed, 13 May 2020 23:24:40 +0200 Subject: [PATCH 10/17] Update packages/node_modules/node-red/red.js Co-authored-by: Nick O'Leary --- packages/node_modules/node-red/red.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 1b7e55dbb..fca16e48c 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -169,16 +169,7 @@ httpsPromise.then(function(startupHttps) { setInterval(function () { try { // Get the result of the function, because createServer doesn't accept functions as input - var refreshedHttps = settings.https(); - - var httpsPromise; - if (refreshedHttps.hasOwnProperty('then') && typeof refreshedHttps.then === 'function') { - // A promise was returned - httpsPromise = refreshedHttps; - } else { - // An object was returned - wrap in a promise - httpsPromise = Promise.resolve(refreshedHttps); - } + var httpsPromise = Promise.resolve(settings.https()); httpsPromise.then(function(refreshedHttps) { // Use the refreshed https settings From cc760acb626c618f1fcd7187abf6e3fc372c3c33 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Wed, 13 May 2020 23:24:57 +0200 Subject: [PATCH 11/17] Update packages/node_modules/node-red/red.js Co-authored-by: Nick O'Leary --- packages/node_modules/node-red/red.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index fca16e48c..112d82021 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -400,4 +400,6 @@ httpsPromise.then(function(startupHttps) { process.exit(); }); }); -}); +}).catch(function(err) { + console.log("Failed to get https settings: " + err); +});; From 6c766eba8601e847762f84ba7c7c9b23ace0113b Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Wed, 13 May 2020 23:46:33 +0200 Subject: [PATCH 12/17] Logs internationalisation --- packages/node_modules/node-red/red.js | 44 ++++++++++++++------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 112d82021..171f4b886 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -141,21 +141,17 @@ if (process.env.NODE_RED_ENABLE_PROJECTS) { settings.editorTheme.projects = settings.editorTheme.projects || {}; settings.editorTheme.projects.enabled = !/^false$/i.test(process.env.NODE_RED_ENABLE_PROJECTS); } +debugger; -var httpsPromise; -if (settings.https) { - var startupHttps = settings.https; +// Delay logging of (translated) messages until the RED object has been initialized +var delayedLogItems = []; - if (typeof startupHttps === "function") { - // Get the result of the function, because createServer doesn't accept functions as input - startupHttps = startupHttps(); - } - httpsPromise = Promise.resolve(startupHttps); -} -else { - // No https is enable - wrap null - httpsPromise = Promise.resolve(null); -} +var startupHttps = settings.https; +if (typeof startupHttps === "function") { + // Get the result of the function, because createServer doesn't accept functions as input + startupHttps = startupHttps(); +} +var httpsPromise = Promise.resolve(startupHttps); httpsPromise.then(function(startupHttps) { if (startupHttps) { @@ -165,7 +161,7 @@ httpsPromise.then(function(startupHttps) { if (settings.httpsRefreshInterval) { if (typeof settings.https === "function") { if (server.setSecureContext) { - console.log("Refreshing https settings every " + parseInt(settings.httpsRefreshInterval) + " seconds."); + delayedLogItems.push({type:"info", id:"server.https.refresh-interval", params:{interval:parseInt(settings.httpsRefreshInterval)}}); setInterval(function () { try { // Get the result of the function, because createServer doesn't accept functions as input @@ -174,27 +170,27 @@ httpsPromise.then(function(startupHttps) { httpsPromise.then(function(refreshedHttps) { // Use the refreshed https settings if (!refreshedHttps.key || !refreshedHttps.cert) { - console.log("Cannot refresh the https settings when the https property function doesn't return a 'key' and 'cert'."); + RED.log.warn(RED.log._("server.https.missing-fields")); return; } // Only update the credentials in the server when key or cert has changed if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { server.setSecureContext(refreshedHttps); - console.log("The https settings have been refreshed."); + RED.log.info(RED.log._("server.https.settings-refreshed")); } }).catch(function(err) { - console.log("Failed to apply the refreshed https settings: " + err); + RED.log.error(RED.log._("server.https.apply-failed",{message:err})); }); } catch(err) { - console.log("Failed to get the refreshed https settings: " + err); + RED.log.error(RED.log._("server.https.get-failed",{message:err})); } }, parseInt(settings.httpsRefreshInterval)*1000); } else { - console.log("Cannot refresh the https settings automatically, because NodeJs version 11 or above is required."); + delayedLogItems.push({type:"warn", id:"server.https.nodejs-version", params:{}}); } } else { - console.log("Cannot refresh the https settings automatically (at httpsRefreshInterval), because the https property needs to be a function."); + delayedLogItems.push({type:"warn", id:"server.https.function-required", params:{}}); } } } else { @@ -365,6 +361,12 @@ httpsPromise.then(function(startupHttps) { } process.exit(1); }); + + // Log all the delayed messages, since they can be translated at this point + delayedLogItems.forEach(function (delayedLogItem, index) { + RED.log[delayedLogItem.type](RED.log._(delayedLogItem.id, delayedLogItem.params)); + }); + server.listen(settings.uiPort,settings.uiHost,function() { if (settings.httpAdminRoot === false) { RED.log.info(RED.log._("server.admin-ui-disabled")); @@ -402,4 +404,4 @@ httpsPromise.then(function(startupHttps) { }); }).catch(function(err) { console.log("Failed to get https settings: " + err); -});; +}); From 0d3bf0cd0038bfde807f701de36d30c97fd0f1f1 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Wed, 13 May 2020 23:49:30 +0200 Subject: [PATCH 13/17] Https refresh settings --- .../@node-red/runtime/locales/en-US/runtime.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index d56031d2f..140593451 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -47,7 +47,16 @@ "now-running": "Server now running at __listenpath__", "failed-to-start": "Failed to start server:", "headless-mode": "Running in headless mode", - "httpadminauth-deprecated": "use of httpAdminAuth is deprecated. Use adminAuth instead" + "httpadminauth-deprecated": "use of httpAdminAuth is deprecated. Use adminAuth instead", + "https": { + "missing-fields": "Cannot refresh the https settings when the https property function doesn't return a 'key' and 'cert'", + "settings-refreshed": "The https settings have been refreshed", + "apply-failed": "Failed to apply the refreshed https settings: __message__", + "get-failed": "Failed to get the refreshed https settings: __message__", + "refresh-interval": "Refreshing https settings every __interval__ seconds", + "nodejs-version": "Cannot refresh the https settings automatically, because NodeJs version 11 or above is required", + "function-required": "Cannot refresh the https settings automatically (at httpsRefreshInterval), because the https property needs to be a function" + } }, "api": { From dec3762b7ac5495a1e7b25d56154547f7d4a2908 Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Thu, 14 May 2020 22:43:50 +0200 Subject: [PATCH 14/17] Remove debugger statement --- packages/node_modules/node-red/red.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 171f4b886..bfea79d75 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -141,7 +141,6 @@ if (process.env.NODE_RED_ENABLE_PROJECTS) { settings.editorTheme.projects = settings.editorTheme.projects || {}; settings.editorTheme.projects.enabled = !/^false$/i.test(process.env.NODE_RED_ENABLE_PROJECTS); } -debugger; // Delay logging of (translated) messages until the RED object has been initialized var delayedLogItems = []; From 4adcb9c439a50fdacb693ef74cc4edb7a790514d Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Fri, 29 May 2020 00:08:07 +0200 Subject: [PATCH 15/17] Refresh interval in hours --- packages/node_modules/node-red/red.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index bfea79d75..ed370214f 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -184,7 +184,7 @@ httpsPromise.then(function(startupHttps) { } catch(err) { RED.log.error(RED.log._("server.https.get-failed",{message:err})); } - }, parseInt(settings.httpsRefreshInterval)*1000); + }, parseInt(settings.httpsRefreshInterval)*60*60*1000); } else { delayedLogItems.push({type:"warn", id:"server.https.nodejs-version", params:{}}); } From 40101df6ec30a10c08281670ec4c8e80c373193e Mon Sep 17 00:00:00 2001 From: bartbutenaers Date: Fri, 29 May 2020 00:11:14 +0200 Subject: [PATCH 16/17] Refresh interval in hours --- packages/node_modules/node-red/settings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 3195f70ef..e3d9f5e19 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -160,10 +160,10 @@ module.exports = { // }); //}, - // The following property can be used to refresh the https settings at regular time intervals (seconds). + // The following property can be used to refresh the https settings at regular time intervals (hours). // Prerequisite: the 'https' property should be enabled (based on a function)! // Caution: NodeJs version 11 or above is required to use this option! - //httpsRefreshInterval : 3600, + //httpsRefreshInterval : 12, // The following property can be used to cause insecure HTTP connections to // be redirected to HTTPS. From bb41ab482c7abdff838a397e926d5fe2e5b00e3c Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 29 May 2020 16:50:53 +0100 Subject: [PATCH 17/17] Rework the https refresh logic - puts the node version check first - validates the refresh interval and keeps it in valid range - simplifies the error messages - uses parseFloat not parseInt so we can use fractions of hour --- .../runtime/locales/en-US/runtime.json | 14 ++--- packages/node_modules/node-red/red.js | 58 +++++++++---------- packages/node_modules/node-red/settings.js | 17 ++++-- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json index 140593451..fea4d5591 100644 --- a/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json +++ b/packages/node_modules/@node-red/runtime/locales/en-US/runtime.json @@ -49,14 +49,12 @@ "headless-mode": "Running in headless mode", "httpadminauth-deprecated": "use of httpAdminAuth is deprecated. Use adminAuth instead", "https": { - "missing-fields": "Cannot refresh the https settings when the https property function doesn't return a 'key' and 'cert'", - "settings-refreshed": "The https settings have been refreshed", - "apply-failed": "Failed to apply the refreshed https settings: __message__", - "get-failed": "Failed to get the refreshed https settings: __message__", - "refresh-interval": "Refreshing https settings every __interval__ seconds", - "nodejs-version": "Cannot refresh the https settings automatically, because NodeJs version 11 or above is required", - "function-required": "Cannot refresh the https settings automatically (at httpsRefreshInterval), because the https property needs to be a function" - } + "refresh-interval": "Refreshing https settings every __interval__ hours", + "settings-refreshed": "Server https settings have been refreshed", + "refresh-failed": "Failed to refresh https settings: __message__", + "nodejs-version": "httpsRefreshInterval requires Node.js 11 or later", + "function-required": "httpsRefreshInterval requires https property to be a function" + } }, "api": { diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index ed370214f..6dfef03ff 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -148,49 +148,49 @@ var delayedLogItems = []; var startupHttps = settings.https; if (typeof startupHttps === "function") { // Get the result of the function, because createServer doesn't accept functions as input - startupHttps = startupHttps(); -} + startupHttps = startupHttps(); +} var httpsPromise = Promise.resolve(startupHttps); - + httpsPromise.then(function(startupHttps) { if (startupHttps) { server = https.createServer(startupHttps,function(req,res) {app(req,res);}); - - // Refresh https settings at intervals for NodeJs version 11 and above + if (settings.httpsRefreshInterval) { - if (typeof settings.https === "function") { - if (server.setSecureContext) { - delayedLogItems.push({type:"info", id:"server.https.refresh-interval", params:{interval:parseInt(settings.httpsRefreshInterval)}}); + var httpsRefreshInterval = parseFloat(settings.httpsRefreshInterval)||12; + if (httpsRefreshInterval > 596) { + // Max value based on (2^31-1)ms - the max that setInterval can accept + httpsRefreshInterval = 596; + } + // Check whether setSecureContext is available (Node.js 11+) + if (server.setSecureContext) { + // Check whether `http` is a callable function + if (typeof settings.https === "function") { + delayedLogItems.push({type:"info", id:"server.https.refresh-interval", params:{interval:httpsRefreshInterval}}); setInterval(function () { try { // Get the result of the function, because createServer doesn't accept functions as input - var httpsPromise = Promise.resolve(settings.https()); - - httpsPromise.then(function(refreshedHttps) { - // Use the refreshed https settings - if (!refreshedHttps.key || !refreshedHttps.cert) { - RED.log.warn(RED.log._("server.https.missing-fields")); - return; - } - - // Only update the credentials in the server when key or cert has changed - if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { - server.setSecureContext(refreshedHttps); - RED.log.info(RED.log._("server.https.settings-refreshed")); + Promise.resolve(settings.https()).then(function(refreshedHttps) { + if (refreshedHttps) { + // Only update the credentials in the server when key or cert has changed + if(!server.key || !server.cert || !server.key.equals(refreshedHttps.key) || !server.cert.equals(refreshedHttps.cert)) { + server.setSecureContext(refreshedHttps); + RED.log.info(RED.log._("server.https.settings-refreshed")); + } } }).catch(function(err) { - RED.log.error(RED.log._("server.https.apply-failed",{message:err})); + RED.log.error(RED.log._("server.https.refresh-failed",{message:err})); }); } catch(err) { - RED.log.error(RED.log._("server.https.get-failed",{message:err})); + RED.log.error(RED.log._("server.https.refresh-failed",{message:err})); } - }, parseInt(settings.httpsRefreshInterval)*60*60*1000); + }, httpsRefreshInterval*60*60*1000); } else { - delayedLogItems.push({type:"warn", id:"server.https.nodejs-version", params:{}}); + delayedLogItems.push({type:"warn", id:"server.https.function-required"}); } } else { - delayedLogItems.push({type:"warn", id:"server.https.function-required", params:{}}); - } + delayedLogItems.push({type:"warn", id:"server.https.nodejs-version"}); + } } } else { server = http.createServer(function(req,res) {app(req,res);}); @@ -360,10 +360,10 @@ httpsPromise.then(function(startupHttps) { } process.exit(1); }); - + // Log all the delayed messages, since they can be translated at this point delayedLogItems.forEach(function (delayedLogItem, index) { - RED.log[delayedLogItem.type](RED.log._(delayedLogItem.id, delayedLogItem.params)); + RED.log[delayedLogItem.type](RED.log._(delayedLogItem.id, delayedLogItem.params||{})); }); server.listen(settings.uiPort,settings.uiHost,function() { diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index e3d9f5e19..718792579 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -140,19 +140,21 @@ module.exports = { // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener // for details on its contents. // See the comment at the top of this file on how to load the `fs` module used by this setting. - // This property can be an object, containing both a (private) key and a (public) certificate: + // This property can be either an object, containing both a (private) key and a (public) certificate, + // or a function that returns such an object: + //// https object: //https: { // key: fs.readFileSync('privkey.pem'), // cert: fs.readFileSync('cert.pem') //}, - // This property can also be a function (e.g. to automatic refresh the https settings synchronously): + ////https synchronous function: //https: function() { // return { // key: fs.readFileSync('privkey.pem'), // cert: fs.readFileSync('cert.pem') // } //}, - // This property can also be a promise (e.g. to automatic refresh the https settings asynchronously): + //// https asynchronous function: //https: function() { // return Promise.resolve({ // key: fs.readFileSync('privkey.pem'), @@ -160,9 +162,12 @@ module.exports = { // }); //}, - // The following property can be used to refresh the https settings at regular time intervals (hours). - // Prerequisite: the 'https' property should be enabled (based on a function)! - // Caution: NodeJs version 11 or above is required to use this option! + // The following property can be used to refresh the https settings at a + // regular time interval in hours. + // This requires: + // - the `https` setting to be a function that can be called to get + // the refreshed settings. + // - Node.js 11 or later. //httpsRefreshInterval : 12, // The following property can be used to cause insecure HTTP connections to