cloud-red
Version:
Harnessing Serverless for your cloud integration needs
434 lines (402 loc) • 11.9 kB
JavaScript
const http = require('http');
const https = require('https');
const util = require('util');
const express = require('express');
const bcrypt = require('bcryptjs');
const path = require('path');
const fs = require('fs-extra');
const { error } = require('./util/error');
const minimist = require('minimist');
const RED = require('../../core/lib/red.js');
const { createFile, genFlowJson } = require('./init');
const { Select } = require('enquirer');
let app = express();
/**
*
* @param {minimist.ParsedArgs} args
* @returns {string}
*/
function selectUserDir(args) {
let userDir = null;
let paramUserDir = args._[1];
if (paramUserDir) {
userDir = path.resolve(paramUserDir);
} else {
console.log(
'Warning! userDir has been set to CLOUD_RED_HOME, *not* to your current directory.'
);
console.log(
'Run: "cloud-red open ." If you want to open current directory. Type: "cloud-red open ."'
);
userDir = path.join(
process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH,
'.cloud-red'
);
}
return userDir;
}
async function selectFlowFile(userDir) {
// check whether userDir exist or not
if (!fs.existsSync(userDir)) {
throw new Error(`${userDir} directory not found`);
}
const potentialFlowFiles = fs.readdirSync(userDir).filter(file => {
return (
path.parse(file).ext === '.json' &&
file !== 'package.json' &&
file !== 'package-lock.json' &&
file !== 'tsconfig.json' &&
file !== 'cloud-red-config.json' &&
!file.startsWith('.')
);
});
let foundFlowFilename = null;
if (potentialFlowFiles.length === 0) {
const DEFAULT_FLOW_FILENAME = 'flow.json';
createFile(userDir, DEFAULT_FLOW_FILENAME, genFlowJson());
foundFlowFilename = DEFAULT_FLOW_FILENAME;
} else if (potentialFlowFiles.length === 1) {
foundFlowFilename = potentialFlowFiles[0];
} else {
const prompt = new Select({
name: 'fileSelected',
message: 'Pick up the flowFile to open:',
choices: potentialFlowFiles
});
foundFlowFilename = prompt
.run()
.then(answer => answer)
.catch(err => {
error(`FlowFile is required to open the editor`, true);
});
}
return foundFlowFilename;
}
/**
*
* @param {string} userDir - user project directory
* @param {minimist.ParsedArgs} args - arguments passed to `open` command
* @returns {string} settings file
*/
function selectSettingsFile(userDir, args) {
// Resolving the settings
let settingsFile = null;
// (a) case of user defined by flag
if (args.settings) {
if (fs.existsSync(path.join(userDir, args.settings))) {
settingsFile = path.join(userDir, args.settings);
} else {
error(`Settings file "${args.settings}" not found`, true);
}
}
// (b) settings not provided looking at the chain in this order:
// 1) $CLOUD_RED_HOME/settings.js
// 2) currentDir/settings.js,
// 3) $HOME/.cloud-red/settings.js
process.env.CLOUD_RED_HOME = process.env.CLOUD_RED_HOME || userDir;
if (fs.existsSync(path.join(process.env.CLOUD_RED_HOME, 'settings.js'))) {
settingsFile = path.join(process.env.CLOUD_RED_HOME, 'settings.js');
} else {
// last resort looking in the homedir
// if its there ... safe the location
if (
fs.existsSync(
path.join(
process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH,
'.cloud-red',
'settings.js'
)
)
) {
settingsFile = path.join(
process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH,
'.cloud-red',
'settings.js'
);
// otherwise write a default settings file
} else {
const targetSettingsFile = path.join(
process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH,
'.cloud-red',
'settings.js'
);
let defaultSettings = path.join(
__dirname,
'templates',
'default-settings.js'
);
fs.copySync(defaultSettings, targetSettingsFile);
settingsFile = targetSettingsFile;
console.log(`Default settings created in ${targetSettingsFile}`);
}
}
return settingsFile;
}
module.exports = async args => {
// predefine the current directory as the userDir
let userDir = selectUserDir(args);
// Loading the settings file
let settingsFile = selectSettingsFile(userDir, args);
try {
var settings = require(settingsFile);
settings.settingsFile = settingsFile;
} catch (err) {
error(
`Loading settings file: ${settingsFile} due to ${err.toString()}`,
true
);
}
// Set the flowFile
try {
settings.flowFile = await selectFlowFile(userDir);
} catch (err) {
error(`Directory: "${userDir}" not found `, true);
}
// Set the UI port
settings.uiPort = settings.uiPort || 1880;
// Set the UI Host
settings.uiHost = settings.uiHost || '0.0.0.0';
// Setting the userDir
settings.userDir = userDir;
// Setting the nodesDir
settings.nodesDir =
settings.nodesDir ||
path.join(
process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH,
'.cloud-red',
'./nodes'
);
// Setting the httpRoot, httpAdminRoot and httpNodeRoot
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;
}
// Setting up the express app
let server;
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);
// Initializing RED ...
try {
RED.init(server, settings);
} catch (err) {
if (err.code == 'unsupported_version') {
console.log('Unsupported version of node.js:', process.version);
console.log('Cloud-RED requires node.js v4 or later');
} else if (err.code == 'not_built') {
console.log('Cloud-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 (
bcrypt
.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;
}
// Starting RED ...
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 = 'Cloud-red';
RED.log.info(
RED.log._('server.now-running', { listenpath: getListenPath() })
);
});
} else {
RED.log.info(RED.log._('server.headless-mode'));
}
})
.catch(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.toString());
}
process.exit(1);
});
process.on('SIGINT', function() {
RED.stop().then(function() {
process.exit();
});
});
};