UNPKG

cloud-red

Version:

Serverless Node-RED for your cloud integration needs

414 lines (383 loc) 11.2 kB
#!/usr/bin/env node /** * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ var http = require('http'); var https = require('https'); var util = require('util'); var express = require('express'); var crypto = require('crypto'); var bcrypt = require('bcryptjs'); var nopt = require('nopt'); var path = require('path'); var fs = require('fs-extra'); var RED = require('../core/lib/red'); var server; var app = express(); var settingsFile; var flowFile; var knownOpts = { help: Boolean, port: Number, settings: [path], title: String, userDir: [path], verbose: Boolean, safe: Boolean }; var shortHands = { '?': ['--help'], p: ['--port'], s: ['--settings'], // As we want to reserve -t for now, adding a shorthand to help so it // doesn't get treated as --title t: ['--help'], u: ['--userDir'], v: ['--verbose'] }; nopt.invalidHandler = function(k, v, t) { // TODO: console.log(k,v,t); }; var parsedArgs = nopt(knownOpts, shortHands, process.argv, 2); if (parsedArgs.help) { console.log('Cloud-RED v' + RED.version()); console.log( 'Usage: cloud-red [-v] [-?] [--settings settings.js] [--userDir DIR]' ); console.log( ' [--port PORT] [--title TITLE] [--safe] [flows.json]' ); console.log(''); console.log('Options:'); console.log(' -p, --port PORT port to listen on'); console.log(' -s, --settings FILE use specified settings file'); console.log(' --title TITLE process window title'); console.log(' -u, --userDir DIR use specified user directory'); console.log(' -v, --verbose enable verbose output'); console.log(' --safe enable safe mode'); console.log(' -?, --help show this help'); console.log(''); console.log('Documentation can be found at http://nodered.org'); process.exit(); } if (parsedArgs.argv.remain.length > 0) { flowFile = parsedArgs.argv.remain[0]; } process.env.NODE_RED_HOME = process.env.NODE_RED_HOME || __dirname; if (parsedArgs.settings) { // User-specified settings file settingsFile = parsedArgs.settings; } else if ( parsedArgs.userDir && fs.existsSync(path.join(parsedArgs.userDir, 'settings.js')) ) { // User-specified userDir that contains a settings.js settingsFile = path.join(parsedArgs.userDir, 'settings.js'); } else { if (fs.existsSync(path.join(process.env.NODE_RED_HOME, '.config.json'))) { // NODE_RED_HOME contains user data - use its settings.js settingsFile = path.join(process.env.NODE_RED_HOME, 'settings.js'); } else if ( process.env.HOMEPATH && fs.existsSync(path.join(process.env.HOMEPATH, '.node-red', '.config.json')) ) { // Consider compatibility for older versions settingsFile = path.join(process.env.HOMEPATH, '.node-red', 'settings.js'); } else { var userDir = parsedArgs.userDir || path.join( process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH, '.node-red' ); var userSettingsFile = path.join(userDir, 'settings.js'); if (fs.existsSync(userSettingsFile)) { // $HOME/.node-red/settings.js exists settingsFile = userSettingsFile; } else { var defaultSettings = path.join(__dirname, 'settings.js'); var settingsStat = fs.statSync(defaultSettings); if (settingsStat.mtime.getTime() <= settingsStat.ctime.getTime()) { // Default settings file has not been modified - safe to copy fs.copySync(defaultSettings, userSettingsFile); settingsFile = userSettingsFile; } else { // Use default settings.js as it has been modified settingsFile = defaultSettings; } } } } try { var settings = require(settingsFile); settings.settingsFile = settingsFile; } catch (err) { console.log('Error loading settings file: ' + settingsFile); if (err.code == 'MODULE_NOT_FOUND') { if (err.toString().indexOf(settingsFile) === -1) { console.log(err.toString()); } } else { console.log(err); } process.exit(); } if (parsedArgs.verbose) { settings.verbose = true; } if (parsedArgs.safe) { settings.safeMode = true; } if (settings.https) { server = https.createServer(settings.https, function(req, res) { app(req, res); }); } 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 v4 or later'); } else if (err.code == 'not_built') { console.log('Node-RED has not been built. See README.md for details'); } 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 || 'Cloud-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(); }); });