diff --git a/.eslintrc.json b/.eslintrc.json index becdf45..a3fa8c9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,7 @@ { "env": { - "es6": true + "es6": true, + "node": true }, "parserOptions": { "ecmaVersion": 8 diff --git a/bin/genieacs-cwmp b/bin/genieacs-cwmp index 01c6ec1..51e4cdf 100755 --- a/bin/genieacs-cwmp +++ b/bin/genieacs-cwmp @@ -1,9 +1,115 @@ #!/usr/bin/env node -var path = require('path'); -var fs = require('fs'); +"use strict"; +const path = require("path"); +const fs = require("fs"); -var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..'); -process.chdir(dir); +process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); -var cluster = require(path.resolve(dir, 'lib/cluster')); -cluster.start('cwmp'); +const config = require("../lib/config"); +const logger = require("../lib/logger"); +const cluster = require("../lib/cluster"); +const server = require("../lib/server"); +const cwmp = require("../lib/cwmp"); +const db = require("../lib/db"); +const extensions = require("../lib/extensions"); +const cache = require("../lib/cache"); + +const VERSION = require("../package.json").version; + +logger.init("cwmp", VERSION); + +const SERVICE_ADDRESS = config.get("CWMP_INTERFACE"); +const SERVICE_PORT = config.get("CWMP_PORT"); + +function exitWorkerGracefully() { + db.disconnect(); + cache.disconnect(); + extensions.killAll(); + cluster.worker.disconnect(); +} + +function exitWorkerUngracefully() { + extensions.killAll(() => { + process.exit(1); + }); +} + +if (!cluster.worker) { + const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES"); + + logger.info({ + message: `genieacs-cwmp starting`, + pid: process.pid, + version: VERSION + }); + + cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); + + process.on("SIGINT", () => { + logger.info({ + message: "Received signal SIGINT, exiting", + pid: process.pid + }); + + cluster.stop(); + }); + + process.on("SIGTERM", () => { + logger.info({ + message: "Received signal SIGTERM, exiting", + pid: process.pid + }); + + cluster.stop(); + }); +} else { + let ssl; + if (config.get("CWMP_SSL")) { + ssl = { + key: path.resolve(config.get("CONFIG_DIR"), "cwmp.key"), + cert: path.resolve(config.get("CONFIG_DIR"), "cwmp.crt"), + ca: path.resolve(config.get("CONFIG_DIR"), "cwmp.ca-bundle") + }; + } + + process.on("uncaughtException", err => { + if (err.code === "ERR_IPC_DISCONNECTED") return; + logger.error({ + message: "Uncaught exception", + exception: err, + pid: process.pid + }); + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGINT", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGTERM", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + db.connect(err => { + if (err) throw err; + cache.connect(err => { + if (err) throw err; + server.start( + SERVICE_PORT, + SERVICE_ADDRESS, + ssl, + cwmp.listener, + cwmp.onConnection + ); + }); + }); +} diff --git a/bin/genieacs-fs b/bin/genieacs-fs index c9c7592..03a33d6 100755 --- a/bin/genieacs-fs +++ b/bin/genieacs-fs @@ -1,9 +1,111 @@ #!/usr/bin/env node -var path = require('path'); -var fs = require('fs'); +"use strict"; +const path = require("path"); +const fs = require("fs"); -var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..'); -process.chdir(dir); +process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); -var cluster = require(path.resolve(dir, 'lib/cluster')); -cluster.start('fs'); +const config = require("../lib/config"); +const logger = require("../lib/logger"); +const cluster = require("../lib/cluster"); +const server = require("../lib/server"); +const _fs = require("../lib/fs"); +const db = require("../lib/db"); +const cache = require("../lib/cache"); + +const VERSION = require("../package.json").version; + +logger.init("fs", VERSION); + +const SERVICE_ADDRESS = config.get("FS_INTERFACE"); +const SERVICE_PORT = config.get("FS_PORT"); + +function exitWorkerGracefully() { + db.disconnect(); + cache.disconnect(); + cluster.worker.disconnect(); +} + +function exitWorkerUngracefully() { + process.exit(1); +} + +if (!cluster.worker) { + const WORKER_COUNT = config.get("FS_WORKER_PROCESSES"); + + logger.info({ + message: `genieacs-fs starting`, + pid: process.pid, + version: VERSION + }); + + cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); + + process.on("SIGINT", () => { + logger.info({ + message: "Received signal SIGINT, exiting", + pid: process.pid + }); + + cluster.stop(); + }); + + process.on("SIGTERM", () => { + logger.info({ + message: "Received signal SIGTERM, exiting", + pid: process.pid + }); + + cluster.stop(); + }); +} else { + let ssl; + if (config.get("FS_SSL")) { + ssl = { + key: path.resolve(config.get("CONFIG_DIR"), "fs.key"), + cert: path.resolve(config.get("CONFIG_DIR"), "fs.crt"), + ca: path.resolve(config.get("CONFIG_DIR"), "fs.ca-bundle") + }; + } + + process.on("uncaughtException", err => { + if (err.code === "ERR_IPC_DISCONNECTED") return; + logger.error({ + message: "Uncaught exception", + exception: err, + pid: process.pid + }); + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGINT", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGTERM", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + db.connect(err => { + if (err) throw err; + cache.connect(err => { + if (err) throw err; + server.start( + SERVICE_PORT, + SERVICE_ADDRESS, + ssl, + _fs.listener, + _fs.onConnection + ); + }); + }); +} diff --git a/bin/genieacs-nbi b/bin/genieacs-nbi index 98ab7cf..04e3a06 100755 --- a/bin/genieacs-nbi +++ b/bin/genieacs-nbi @@ -1,9 +1,115 @@ #!/usr/bin/env node -var path = require('path'); -var fs = require('fs'); +"use strict"; +const path = require("path"); +const fs = require("fs"); -var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..'); -process.chdir(dir); +process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); -var cluster = require(path.resolve(dir, 'lib/cluster')); -cluster.start('nbi'); +const config = require("../lib/config"); +const logger = require("../lib/logger"); +const cluster = require("../lib/cluster"); +const server = require("../lib/server"); +const nbi = require("../lib/nbi"); +const db = require("../lib/db"); +const extensions = require("../lib/extensions"); +const cache = require("../lib/cache"); + +const VERSION = require("../package.json").version; + +logger.init("nbi", VERSION); + +const SERVICE_ADDRESS = config.get("NBI_INTERFACE"); +const SERVICE_PORT = config.get("NBI_PORT"); + +function exitWorkerGracefully() { + db.disconnect(); + cache.disconnect(); + extensions.killAll(); + cluster.worker.disconnect(); +} + +function exitWorkerUngracefully() { + extensions.killAll(() => { + process.exit(1); + }); +} + +if (!cluster.worker) { + const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES"); + + logger.info({ + message: `genieacs-nbi starting`, + pid: process.pid, + version: VERSION + }); + + cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); + + process.on("SIGINT", () => { + logger.info({ + message: "Received signal SIGINT, exiting", + pid: process.pid + }); + + cluster.stop(); + }); + + process.on("SIGTERM", () => { + logger.info({ + message: "Received signal SIGTERM, exiting", + pid: process.pid + }); + + cluster.stop(); + }); +} else { + let ssl; + if (config.get("NBI_SSL")) { + ssl = { + key: path.resolve(config.get("CONFIG_DIR"), "nbi.key"), + cert: path.resolve(config.get("CONFIG_DIR"), "nbi.crt"), + ca: path.resolve(config.get("CONFIG_DIR"), "nbi.ca-bundle") + }; + } + + process.on("uncaughtException", err => { + if (err.code === "ERR_IPC_DISCONNECTED") return; + logger.error({ + message: "Uncaught exception", + exception: err, + pid: process.pid + }); + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGINT", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGTERM", () => { + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + db.connect(err => { + if (err) throw err; + cache.connect(err => { + if (err) throw err; + server.start( + SERVICE_PORT, + SERVICE_ADDRESS, + ssl, + nbi.listener, + nbi.onConnection + ); + }); + }); +} diff --git a/bin/genieacs-ui b/bin/genieacs-ui new file mode 100755 index 0000000..de9f54c --- /dev/null +++ b/bin/genieacs-ui @@ -0,0 +1,112 @@ +#!/usr/bin/env node +"use strict"; +const path = require("path"); +const fs = require("fs"); + +process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); + +const config = require("../lib/config"); +const logger = require("../lib/logger"); +const cluster = require("../lib/cluster"); +const server = require("../lib/server"); +const ui = require("../lib/ui"); +const db = require("../lib/ui/db"); +const cache = require("../lib/cache"); + +const VERSION = require("../package.json").version; + +logger.init("ui", VERSION); + +const SERVICE_ADDRESS = config.get("UI_INTERFACE"); +const SERVICE_PORT = config.get("UI_PORT"); + +function exitWorkerGracefully() { + db.disconnect(); + cache.disconnect(); + cluster.worker.disconnect(); +} + +function exitWorkerUngracefully() { + process.exit(1); +} + +if (!cluster.worker) { + const WORKER_COUNT = config.get("UI_WORKER_PROCESSES"); + + logger.info({ + message: `genieacs-ui starting`, + pid: process.pid, + version: VERSION + }); + + cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); + + process.on("SIGINT", () => { + logger.info({ + message: "Received signal SIGINT, exiting", + pid: process.pid + }); + + cluster.stop(); + }); + + process.on("SIGTERM", () => { + logger.info({ + message: "Received signal SIGTERM, exiting", + pid: process.pid + }); + + cluster.stop(); + }); +} else { + let ssl; + if (config.get("UI_SSL")) { + ssl = { + key: path.resolve(config.get("CONFIG_DIR"), "ui.key"), + cert: path.resolve(config.get("CONFIG_DIR"), "ui.crt"), + ca: path.resolve(config.get("CONFIG_DIR"), "ui.ca-bundle") + }; + } + + let stopping = false; + + process.on("uncaughtException", err => { + if (err.code === "ERR_IPC_DISCONNECTED") return; + logger.error({ + message: "Uncaught exception", + exception: err, + pid: process.pid + }); + stopping = true; + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGINT", () => { + stopping = true; + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + process.on("SIGTERM", () => { + stopping = true; + server + .stop() + .then(exitWorkerGracefully) + .catch(exitWorkerUngracefully); + }); + + const listener = (req, res) => { + if (stopping) res.setHeader("Connection", "close"); + ui.listener(req, res); + }; + + cache.connect(err => { + if (err) throw err; + server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener, ui.onConnection); + }); +} diff --git a/build/build.js b/build/build.js new file mode 100644 index 0000000..c30f364 --- /dev/null +++ b/build/build.js @@ -0,0 +1,196 @@ +const path = require("path"); +const fs = require("fs"); +const util = require("util"); +const webpack = require("webpack"); +const nodeExternals = require("webpack-node-externals"); +const postcss = require("postcss"); +const postcssImport = require("postcss-import"); +const postcssCssNext = require("postcss-cssnext"); +const cssnano = require("cssnano"); + +const MODE = "production"; + +const BUILD_METADATA = new Date() + .toISOString() + .split(".")[0] + .replace(/[^0-9]/g, ""); + +const INPUT_DIR = path.resolve(__dirname, ".."); +const OUTPUT_DIR = path.resolve(__dirname, "../dist"); + +function rmDirSync(dirPath) { + if (!fs.existsSync(dirPath)) return; + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const filePath = `${dirPath}/${file}`; + if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath); + else rmDirSync(filePath); + } + fs.rmdirSync(dirPath); +} + +function cpDirSync(src, dst) { + fs.mkdirSync(dst); + const files = fs.readdirSync(src); + for (const file of files) { + const current = fs.lstatSync(path.join(src, file)); + if (current.isDirectory()) { + cpDirSync(path.join(src, file), path.join(dst, file)); + } else if (current.isSymbolicLink()) { + const symlink = fs.readlinkSync(path.join(src, file)); + fs.symlinkSync(symlink, path.join(dst, file)); + } else { + fs.copyFileSync(path.join(src, file), path.join(dst, file)); + } + } +} + +async function init() { + // Delete any old output directory + rmDirSync(OUTPUT_DIR); + + // Create output directory layout + fs.mkdirSync(OUTPUT_DIR); + fs.mkdirSync(OUTPUT_DIR + "/bin"); + fs.mkdirSync(OUTPUT_DIR + "/config"); + fs.mkdirSync(OUTPUT_DIR + "/debug"); + fs.mkdirSync(OUTPUT_DIR + "/public"); + + // Create package.json + const packageJson = require("../package.json"); + delete packageJson["devDependencies"]; + packageJson["scripts"] = { + install: packageJson["scripts"].install, + configure: packageJson["scripts"].configure + }; + packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`; + fs.writeFileSync( + path.resolve(OUTPUT_DIR, "package.json"), + JSON.stringify(packageJson, null, 4) + ); +} + +async function copyStatic() { + const files = [ + "LICENSE", + "README.md", + "CHANGELOG.md", + "npm-shrinkwrap.json", + "config/config-sample.json", + "config/config-ui-sample.json", + "config/auth-sample.js", + "config/ext-sample.js", + "public/logo.svg", + "public/favicon.png" + ]; + + for (const file of files) { + fs.copyFileSync( + path.resolve(INPUT_DIR, file), + path.resolve(OUTPUT_DIR, file) + ); + } + + cpDirSync( + path.resolve(INPUT_DIR, "tools"), + path.resolve(OUTPUT_DIR, "tools") + ); +} + +async function generateCss() { + const cssInPath = path.resolve(INPUT_DIR, "ui/css/app.css"); + const cssOutPath = path.resolve(OUTPUT_DIR, "public/app.css"); + const cssIn = fs.readFileSync(cssInPath); + const cssOut = await postcss([ + postcssImport, + postcssCssNext({ warnForDuplicates: false }), + cssnano + ]).process(cssIn, { from: cssInPath, to: cssOutPath }); + fs.writeFileSync(cssOutPath, cssOut.css); +} + +async function generateJs() { + const backendConf = { + mode: MODE, + target: "node", + node: false, + entry: { + "genieacs-cwmp": path.resolve(INPUT_DIR, "bin/genieacs-cwmp"), + "genieacs-nbi": path.resolve(INPUT_DIR, "bin/genieacs-nbi"), + "genieacs-fs": path.resolve(INPUT_DIR, "bin/genieacs-fs"), + "genieacs-ui": path.resolve(INPUT_DIR, "bin/genieacs-ui") + }, + resolve: { + alias: { + [path.resolve(INPUT_DIR, "package.json")]: path.resolve( + OUTPUT_DIR, + "package.json" + ) + } + }, + externals: [nodeExternals()], + output: { + path: path.resolve(OUTPUT_DIR, "bin") + }, + module: { + rules: [ + { + include: path.resolve(INPUT_DIR, "bin"), + use: path.join(__dirname, "./shebang-loader") + } + ] + }, + plugins: [ + new webpack.BannerPlugin({ banner: "#!/usr/bin/env node", raw: true }) + ] + }; + + const frontendConf = { + mode: MODE, + entry: path.resolve(INPUT_DIR, "ui/app.js"), + output: { + path: path.resolve(OUTPUT_DIR, "public"), + filename: "app.js" + }, + resolve: { + alias: { + [path.resolve(INPUT_DIR, "package.json")]: path.resolve( + OUTPUT_DIR, + "package.json" + ) + } + } + }; + + const stats = await util.promisify(webpack)([backendConf, frontendConf]); + + // Remove js ext and mark as executable + for (const file of [ + "genieacs-cwmp", + "genieacs-nbi", + "genieacs-fs", + "genieacs-ui" + ]) { + fs.renameSync( + path.resolve(OUTPUT_DIR, `bin/${file}.js`), + path.resolve(OUTPUT_DIR, `bin/${file}`) + ); + const mode = fs.statSync(path.resolve(OUTPUT_DIR, `bin/${file}`)).mode; + fs.chmodSync(path.resolve(OUTPUT_DIR, `bin/${file}`), mode | 73); + } + + process.stdout.write(stats.toString({ colors: true }) + "\n"); +} + +init() + .then(() => { + Promise.all([copyStatic(), generateCss(), generateJs()]) + .then(() => {}) + .catch(err => { + process.stderr.write(err.stack + "\n"); + }); + }) + .catch(err => { + process.stderr.write(err.stack + "\n"); + }); diff --git a/build/shebang-loader.js b/build/shebang-loader.js new file mode 100644 index 0000000..2d4b80d --- /dev/null +++ b/build/shebang-loader.js @@ -0,0 +1,3 @@ +module.exports = function(source) { + return source.replace(/^#![^\n\r]*[\r\n]/, ""); +}; diff --git a/config/config-sample.json b/config/config-sample.json index 9925c9f..28f8c03 100644 --- a/config/config-sample.json +++ b/config/config-sample.json @@ -8,5 +8,7 @@ "FS_INTERFACE" : "0.0.0.0", "FS_PORT" : 7567, "FS_HOSTNAME" : "acs.example.com", + "UI_INTERFACE" : "0.0.0.0", + "UI_PORT" : 3000, "DEBUG" : false } diff --git a/config/config-ui-sample.json b/config/config-ui-sample.json index 4a7bc42..e2950b1 100644 --- a/config/config-ui-sample.json +++ b/config/config-ui-sample.json @@ -1,8 +1,5 @@ { "server": { - "workers": 4, - "interface": "0.0.0.0", - "port": 3000, "nbi": "http://127.0.0.1:7557/", "mongodbConnectionUrl": "mongodb://127.0.0.1:27017", "jwtSecret": "jwtSecret" @@ -312,11 +309,5 @@ "type": "device-actions" } ] - }, - "logger": { - "logFile": "", - "accessLogFile": "", - "logFormat": "simple", - "accessLogFormat": "simple" } } diff --git a/lib/.eslintrc.json b/lib/.eslintrc.json deleted file mode 100644 index 10d2238..0000000 --- a/lib/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "env": { - "node": true - } -} diff --git a/lib/cluster.js b/lib/cluster.js index e1a7a15..21d8325 100644 --- a/lib/cluster.js +++ b/lib/cluster.js @@ -20,44 +20,10 @@ const cluster = require("cluster"); const logger = require("./logger"); -const config = require("./config"); - -const VERSION = require("../package.json").version; let respawnTimestamp = 0; let tooManyCrashesTimestamp = 0; -function getDependencyVersions() { - const dependencies = [`node@${process.versions.node}`]; - try { - const npmls = JSON.parse( - require("child_process").execSync("npm ls --prod --json --depth 0") - ); - - for (const k in npmls.dependencies) - dependencies.push(`${k}@${npmls.dependencies[k].version}`); - } catch (err) { - // Continue regardless of error - } - - return dependencies.join(","); -} - -function getConfig() { - const conf = []; - const keys = Object.keys(process.env); - for (const k of keys) { - if (k.startsWith("GENIEACS_")) { - const name = k.slice(9); - const value = process.env[k]; - const def = config.getDefault(name); - if (def == null || value !== `${def}`) conf.push(`${name}=${value}`); - } - } - - return conf.join(","); -} - function restartWorker(worker, code, signal) { const msg = { message: "Worker died", @@ -96,24 +62,12 @@ function restartWorker(worker, code, signal) { }, respawnTimestamp - now); } -function start(service) { - logger.init(service, VERSION); - const SERVICE_ADDRESS = config.get(`${service.toUpperCase()}_INTERFACE`); - const SERVICE_PORT = config.get(`${service.toUpperCase()}_PORT`); - - logger.info({ - message: `GenieACS (genieacs-${service}) starting`, - pid: process.pid, - version: VERSION, - dependencies: getDependencyVersions(), - config: getConfig() - }); - +function start(workerCount, servicePort, serviceAddress) { cluster.on("listening", (worker, address) => { if ( (address.addressType === 4 || address.addressType === 6) && - address.address === SERVICE_ADDRESS && - address.port === SERVICE_PORT + address.address === serviceAddress && + address.port === servicePort ) { logger.info({ message: "Worker listening", @@ -126,46 +80,16 @@ function start(service) { cluster.on("exit", restartWorker); - process.on("SIGINT", () => { - logger.info({ - message: "Received signal SIGINT, exiting", - pid: process.pid - }); - - cluster.removeListener("exit", restartWorker); - }); - - process.on("SIGTERM", () => { - logger.info({ - message: "Received signal SIGTERM, exiting", - pid: process.pid - }); - - cluster.removeListener("exit", restartWorker); - for (const pid in cluster.workers) cluster.workers[pid].kill(); - }); - - cluster.setupMaster({ - exec: "lib/server", - args: [service] - }); - - let workerCount = config.get(`${service.toUpperCase()}_WORKER_PROCESSES`); - if (!workerCount) workerCount = Math.max(2, require("os").cpus().length); for (let i = 0; i < workerCount; ++i) cluster.fork(); } -process.on("uncaughtException", err => { - logger.error({ - message: "Uncaught exception in master process, exiting", - exception: err, - pid: process.pid - }); - process.exitCode = 1; +function stop() { cluster.removeListener("exit", restartWorker); for (const pid in cluster.workers) cluster.workers[pid].kill(); -}); +} exports.start = start; +exports.stop = stop; +exports.worker = cluster.worker; diff --git a/lib/config.js b/lib/config.js index dcc47eb..cf9fe8c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -19,6 +19,7 @@ "use strict"; const path = require("path"); +const fs = require("fs"); const options = { CONFIG_DIR: { type: "path", default: "config" }, @@ -52,6 +53,13 @@ const options = { FS_LOG_FILE: { type: "path", default: "" }, FS_ACCESS_LOG_FILE: { type: "path", default: "" }, + UI_WORKER_PROCESSES: { type: "int", default: 0 }, + UI_PORT: { type: "int", default: 3000 }, + UI_INTERFACE: { type: "string", default: "0.0.0.0" }, + UI_SSL: { type: "bool", default: false }, + UI_LOG_FILE: { type: "path", default: "" }, + UI_ACCESS_LOG_FILE: { type: "path", default: "" }, + UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 }, DOWNLOAD_TIMEOUT: { type: "int", default: 3600 }, @@ -175,7 +183,9 @@ for (const [k, v] of Object.entries(process.env)) setConfig("CONFIG_DIR", options["CONFIG_DIR"]["default"]); // Configuration file -const configFile = require(path.resolve(allConfig.CONFIG_DIR, "config")); +const configFile = JSON.parse( + fs.readFileSync(path.resolve(allConfig.CONFIG_DIR, "config.json")) +); for (const [k, v] of Object.entries(configFile)) { if (!setConfig(k, v)) // Pass as environment variable to be accessable by extensions @@ -226,7 +236,13 @@ function getDefault(optionName) { // Load authentication scripts try { - exports.auth = require(path.resolve(allConfig.CONFIG_DIR, "auth")); + const authScript = fs.readFileSync( + path.resolve(allConfig.CONFIG_DIR, "auth.js") + ); + exports.auth = (function(exports) { + eval(authScript.toString()); + return exports; + })({}); } catch (error) { // No auth.js exists } diff --git a/lib/logger.js b/lib/logger.js index a0eaf91..dd81c4a 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -184,6 +184,17 @@ function flatten(details) { delete details.fault; } + // For genieacs-ui + if (details.context) { + details.remoteAddress = details.context.request.ip; + if (details.context.state.user) + details.user = details.context.state.user.username; + delete details.context; + } + + for (const [k, v] of Object.entries(details)) + if (v == null) delete details[k]; + return details; } @@ -207,12 +218,15 @@ function formatSimple(details, systemd) { if (details.remoteAddress) { if (details.deviceId) remote = `${details.remoteAddress} ${details.deviceId}: `; + else if (details.user) + remote = `${details.user}@${details.remoteAddress}: `; else remote = `${details.remoteAddress}: `; } const keys = Object.keys(details); const skip = { deviceId: 1, + user: 1, remoteAddress: 1, severity: 1, timestamp: 1, diff --git a/lib/server.js b/lib/server.js index 64be8d8..07cb6cc 100644 --- a/lib/server.js +++ b/lib/server.js @@ -18,29 +18,11 @@ */ "use strict"; -const service = process.argv[2]; - -if (service == null) throw new Error("Missing argument cwmp, fs, or nbi"); - -const cluster = require("cluster"); -const path = require("path"); const fs = require("fs"); const http = require("http"); const https = require("https"); -const logger = require("./logger"); -logger.init(service, require("../package.json").version); -const config = require("./config"); -const db = require("./db"); -const cache = require("./cache"); -const extensions = require("./extensions"); -const networkInterface = config.get(`${service.toUpperCase()}_INTERFACE`); -const port = config.get(`${service.toUpperCase()}_PORT`); -const useHttps = config.get(`${service.toUpperCase()}_SSL`); - -const { listener, onConnection } = require(`./${service}`); - -let server; +let server, listener; function closeServer(timeout, callback) { if (!server) return void callback(); @@ -66,67 +48,42 @@ function closeServer(timeout, callback) { }); } -function exit() { - setTimeout(() => { - extensions.killAll(() => { - process.exit(1); - }); - }, 30000).unref(); +function start(port, networkInterface, ssl, _listener, onConnection) { + listener = _listener; - closeServer(20000, () => { - db.disconnect(); - cache.disconnect(); - extensions.killAll(); - if (cluster.worker) cluster.worker.disconnect(); - }); -} + if (ssl) { + const options = { + key: fs.readFileSync(ssl.key), + cert: fs.readFileSync(ssl.cert) + }; -process.on("uncaughtException", err => { - logger.error({ - message: "Uncaught exception", - exception: err, - pid: process.pid - }); - exit(); -}); - -if (useHttps) { - const httpsKey = path.resolve(config.get("CONFIG_DIR"), `${service}.key`); - const httpsCert = path.resolve(config.get("CONFIG_DIR"), `${service}.crt`); - const httpsCa = path.resolve( - config.get("CONFIG_DIR"), - `${service}.ca-bundle` - ); - const options = { - key: fs.readFileSync(httpsKey), - cert: fs.readFileSync(httpsCert) - }; - - try { - // Use intermediate certificates if available - options.ca = fs - .readFileSync(httpsCa) - .toString() - .match(/-+BEGIN CERTIFICATE-+[0-9a-zA-Z+\-/=\s]+?-+END CERTIFICATE-+/g); - } catch (error) { - // No intermediate certificate + try { + // Use intermediate certificates if available + options.ca = fs + .readFileSync(ssl.ca) + .toString() + .match(/-+BEGIN CERTIFICATE-+[0-9a-zA-Z+\-/=\s]+?-+END CERTIFICATE-+/g); + } catch (error) { + // No intermediate certificate + } + server = https.createServer(options, listener); + if (onConnection != null) server.on("secureConnection", onConnection); + } else { + server = http.createServer(listener); + if (onConnection != null) server.on("connection", onConnection); } - server = https.createServer(options, listener); - if (onConnection != null) server.on("secureConnection", onConnection); -} else { - server = http.createServer(listener); - if (onConnection != null) server.on("connection", onConnection); + + server.listen(port, networkInterface); } -db.connect(err => { - if (err) throw err; - - cache.connect(err => { - if (err) throw err; - - server.listen(port, networkInterface); +function stop() { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error("Could not close server in a timely manner")); + }, 30000).unref(); + closeServer(20000, resolve); }); -}); +} -process.on("SIGINT", exit); -process.on("SIGTERM", exit); +exports.start = start; +exports.stop = stop; diff --git a/lib/ui/server.js b/lib/ui.js similarity index 74% rename from lib/ui/server.js rename to lib/ui.js index 753e01d..8a7dd83 100644 --- a/lib/ui/server.js +++ b/lib/ui.js @@ -1,7 +1,5 @@ "use strict"; -const http = require("http"); -const cluster = require("cluster"); const Koa = require("koa"); const Router = require("koa-router"); const koaStatic = require("koa-static"); @@ -10,15 +8,11 @@ const koaBodyParser = require("koa-bodyparser"); const koaJwt = require("koa-jwt"); const jwt = require("jsonwebtoken"); -const config = require("./config"); -const api = require("./api"); -const Authorizer = require("../common/authorizer"); -const expression = require("../common/expression"); +const config = require("./ui/config"); +const api = require("./ui/api"); +const Authorizer = require("./common/authorizer"); +const expression = require("./common/expression"); const logger = require("./logger"); -const db = require("./db"); - -const VERSION = require("../../package.json")["version"]; -logger.init("server", VERSION); const koa = new Koa(); const router = new Router(); @@ -170,51 +164,7 @@ koa.use( }) ); -const server = http - .createServer(koa.callback()) - .listen(config.server.port, config.server.interface); - -koa.use(async (ctx, next) => { - await next(); - if (!server.listening) ctx.set("Connection", "close"); -}); - koa.use(router.routes()); koa.use(koaStatic("./public")); -function closeServer(timeout) { - return new Promise(resolve => { - // prevent new sockets to connect and close server eventually. - server.close(resolve); - setTimeout(resolve, timeout).unref(); - }); -} - -function exit() { - setTimeout(() => { - process.exit(1); - }, 30000).unref(); - - closeServer(20000).then(() => { - db.disconnect(); - if (cluster.worker) cluster.worker.disconnect(); - logger.close(); - }); -} - -process.on("unhandledRejection", err => { - throw err; -}); - -process.on("uncaughtException", err => { - logger.error({ - message: "Uncaught exception", - exception: err, - pid: process.pid - }); - process.exitCode = 1; - exit(); -}); - -process.on("SIGINT", exit); -process.on("SIGTERM", exit); +exports.listener = koa.callback(); diff --git a/lib/ui/api.js b/lib/ui/api.js index ead1dc9..01cd8c7 100644 --- a/lib/ui/api.js +++ b/lib/ui/api.js @@ -5,7 +5,7 @@ const Router = require("koa-router"); const db = require("./db"); const apiFunctions = require("./api-functions"); const expression = require("../common/expression"); -const logger = require("./logger"); +const logger = require("../logger"); const router = new Router(); diff --git a/lib/ui/cluster.js b/lib/ui/cluster.js deleted file mode 100644 index 5c96b51..0000000 --- a/lib/ui/cluster.js +++ /dev/null @@ -1,110 +0,0 @@ -"use strict"; - -const cluster = require("cluster"); -const config = require("./config"); -const logger = require("./logger"); - -const VERSION = require("../../package.json")["version"]; - -let respawnTimestamp = 0; -let tooManyCrashesTimestamp = 0; - -function restartWorker(worker, code, signal) { - const msg = { - message: "Worker died", - pid: worker.process.pid - }; - - if (code != null) msg.exitCode = code; - - if (signal != null) msg.signal = signal; - - logger.error(msg); - - const now = Date.now(); - respawnTimestamp = Math.max(now, respawnTimestamp + 2000); - if (respawnTimestamp === now) { - tooManyCrashesTimestamp = now; - cluster.fork(); - return; - } - - if (now - tooManyCrashesTimestamp > 60000) { - process.exitCode = 1; - cluster.removeListener("exit", restartWorker); - for (const pid in cluster.workers) cluster.workers[pid].kill(); - - logger.error({ - message: "Too many crashes, exiting", - pid: process.pid - }); - return; - } - - setTimeout(() => { - if (process.exitCode) return; - cluster.fork(); - }, respawnTimestamp - now); -} - -function start() { - logger.init(VERSION); - - logger.info({ - message: "GenieACS UI starting", - pid: process.pid, - version: VERSION - }); - - cluster.on("listening", (worker, address) => { - logger.info({ - message: "Worker listening", - pid: worker.process.pid, - address: address.address, - port: address.port - }); - }); - - cluster.on("exit", restartWorker); - - process.on("SIGINT", () => { - logger.info({ - message: "Received signal SIGINT, exiting", - pid: process.pid - }); - - cluster.removeListener("exit", restartWorker); - }); - - process.on("SIGTERM", () => { - logger.info({ - message: "Received signal SIGTERM, exiting", - pid: process.pid - }); - - cluster.removeListener("exit", restartWorker); - for (const pid in cluster.workers) cluster.workers[pid].kill(); - }); - - cluster.setupMaster({ exec: "./server" }); - - let workerCount = config.server.workers; - - if (!workerCount) workerCount = Math.max(2, require("os").cpus().length); - - for (let i = 0; i < workerCount; ++i) cluster.fork(); -} - -process.on("uncaughtException", err => { - logger.error({ - message: "Uncaught exception in master process, exiting", - exception: err, - pid: process.pid - }); - process.exitCode = 1; - cluster.removeListener("exit", restartWorker); - for (const pid in cluster.workers) cluster.workers[pid].kill(); -}); - -// Starting GenieACS UI -start(); diff --git a/lib/ui/config.js b/lib/ui/config.js index 1f9ceb4..867363f 100644 --- a/lib/ui/config.js +++ b/lib/ui/config.js @@ -1,7 +1,7 @@ "use strict"; const fs = require("fs"); -const configFile = JSON.parse(fs.readFileSync("./config-ui.json")); +const configFile = JSON.parse(fs.readFileSync("./config/config-ui.json")); const config = loadConfig(configFile); diff --git a/lib/ui/logger.js b/lib/ui/logger.js deleted file mode 100644 index 648f9fd..0000000 --- a/lib/ui/logger.js +++ /dev/null @@ -1,249 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const os = require("os"); - -const config = require("./config"); - -const REOPEN_EVERY = 60000; - -const LOG_FORMAT = config.logger.logFormat; -const ACCESS_LOG_FORMAT = config.logger.accessLogFormat || LOG_FORMAT; - -const defaultMeta = {}; - -let LOG_SYSTEMD = false; -let ACCESS_LOG_SYSTEMD = false; - -let LOG_FILE, ACCESS_LOG_FILE; - -let logStream = fs.createWriteStream(null, { fd: process.stderr.fd }); -let logStat = fs.fstatSync(logStream.fd); -let accessLogStream = fs.createWriteStream(null, { fd: process.stdout.fd }); -let accessLogStat = fs.fstatSync(accessLogStream.fd); - -// Reopen if original files have been moved (e.g. logrotate) -function reopen() { - let counter = 1; - - if (LOG_FILE) { - ++counter; - fs.stat(LOG_FILE, (err, stat) => { - if (err && !err.message.startsWith("ENOENT:")) throw err; - - if (!(stat && stat.dev === logStat.dev && stat.ino === logStat.ino)) { - logStream.end(); - logStream = fs.createWriteStream(null, { - fd: fs.openSync(LOG_FILE, "a") - }); - logStat = fs.fstatSync(logStream.fd); - } - - if (--counter === 0) - setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); - }); - } - - if (ACCESS_LOG_FILE) { - ++counter; - fs.stat(ACCESS_LOG_FILE, (err, stat) => { - if (err && !err.message.startsWith("ENOENT:")) throw err; - - if ( - !( - stat && - stat.dev === accessLogStat.dev && - stat.ino === accessLogStat.ino - ) - ) { - accessLogStream.end(); - accessLogStream = fs.createWriteStream(null, { - fd: fs.openSync(ACCESS_LOG_FILE, "a") - }); - accessLogStat = fs.fstatSync(accessLogStream.fd); - } - - if (--counter === 0) - setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); - }); - } - - if (--counter === 0) - setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); -} - -function init(version) { - defaultMeta.hostname = os.hostname(); - defaultMeta.pid = process.pid; - defaultMeta.name = "genieacs-ui"; - defaultMeta.version = version; - - LOG_FILE = config.logger.logFile; - ACCESS_LOG_FILE = config.logger.accessLogFile; - - if (LOG_FILE) { - logStream = fs.createWriteStream(null, { fd: fs.openSync(LOG_FILE, "a") }); - logStat = fs.fstatSync(logStream.fd); - } - - if (ACCESS_LOG_FILE) { - accessLogStream = fs.createWriteStream(null, { - fd: fs.openSync(ACCESS_LOG_FILE, "a") - }); - accessLogStat = fs.fstatSync(accessLogStream.fd); - } - - // Determine if logs are going to journald - const JOURNAL_STREAM = process.env["JOURNAL_STREAM"]; - - if (JOURNAL_STREAM) { - let [dev, inode] = JOURNAL_STREAM.split(":"); - dev = parseInt(dev); - inode = parseInt(inode); - - LOG_SYSTEMD = logStat.dev === dev && logStat.ino === inode; - ACCESS_LOG_SYSTEMD = - accessLogStat.dev === dev && accessLogStat.ino === inode; - } - - if (LOG_FILE || ACCESS_LOG_FILE) - setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); -} - -function close() { - accessLogStream.end(); - logStream.end(); -} - -function flatten(details) { - if (details.exception) { - details.exceptionName = details.exception.name; - details.exceptionMessage = details.exception.message; - details.exceptionStack = details.exception.stack; - delete details.exception; - } - - if (details.context) { - details.remoteAddress = details.context.request.ip; - if (details.context.state.user) - details.user = details.context.state.user.username; - delete details.context; - } - - for (const [k, v] of Object.entries(details)) - if (v == null) delete details[k]; - - return details; -} - -function formatJson(details, systemd) { - if (systemd) { - let severity = ""; - if (details.severity === "info") severity = "<6>"; - else if (details.severity === "warn") severity = "<4>"; - else if (details.severity === "error") severity = "<3>"; - - return `${severity}${JSON.stringify(flatten(details))}${os.EOL}`; - } - - return `${JSON.stringify(flatten(details))}${os.EOL}`; -} - -function formatSimple(details, systemd) { - flatten(details); - - let remote = ""; - if (details.remoteAddress) { - if (details.user) remote = `${details.user}@${details.remoteAddress}: `; - else remote = `${details.remoteAddress}: `; - } - - const keys = Object.keys(details); - const skip = { - user: 1, - remoteAddress: 1, - severity: 1, - timestamp: 1, - message: 1 - }; - - let meta = ""; - - const kv = []; - for (const k of keys) - if (!skip[k]) kv.push(`${k}=${JSON.stringify(details[k])}`); - - if (kv.length) meta = `; ${kv.join(" ")}`; - - if (systemd) { - let severity = ""; - if (details.severity === "info") severity = "<6>"; - else if (details.severity === "warn") severity = "<4>"; - else if (details.severity === "error") severity = "<3>"; - - return `${severity}${remote}${details.message}${meta}${os.EOL}`; - } - - return `${details.timestamp} [${details.severity.toUpperCase()}] ${remote}${ - details.message - }${meta}${os.EOL}`; -} - -function log(details) { - details.timestamp = new Date().toISOString(); - if (LOG_FORMAT === "json") { - details = Object.assign({}, defaultMeta, details); - logStream.write(formatJson(details, LOG_SYSTEMD)); - } else { - logStream.write(formatSimple(details, LOG_SYSTEMD)); - } -} - -function info(details) { - details.severity = "info"; - log(details); -} - -function warn(details) { - details.severity = "warn"; - log(details); -} - -function error(details) { - details.severity = "error"; - log(details); -} - -function accessLog(details) { - details.timestamp = new Date().toISOString(); - if (ACCESS_LOG_FORMAT === "json") { - Object.assign(details, defaultMeta); - accessLogStream.write(formatJson(details, ACCESS_LOG_SYSTEMD)); - } else { - accessLogStream.write(formatSimple(details, ACCESS_LOG_SYSTEMD)); - } -} - -function accessInfo(details) { - details.severity = "info"; - accessLog(details); -} - -function accessWarn(details) { - details.severity = "warn"; - accessLog(details); -} - -function accessError(details) { - details.severity = "error"; - accessLog(details); -} - -exports.init = init; -exports.close = close; -exports.info = info; -exports.warn = warn; -exports.error = error; -exports.accessInfo = accessInfo; -exports.accessWarn = accessWarn; -exports.accessError = accessError; diff --git a/package.json b/package.json index 9ce0912..1e43728 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "bin": "bin" }, "dependencies": { - "codemirror": "^5.40.0", "jsonwebtoken": "^8.3.0", "koa-bodyparser": "^4.2.1", "koa-compress": "^3.0.0", @@ -31,26 +30,23 @@ "libxmljs": "^0.19.5", "mongodb": "^3.1.10", "parsimmon": "^1.12.0", - "seedrandom": "^2.4.4", - "yaml": "^1.0.0" + "seedrandom": "^2.4.4" }, "optionalDependencies": { "redis": "^2.6.5" }, "devDependencies": { "babel-eslint": "^8.2.6", - "clean-webpack-plugin": "^0.1.19", - "copy-webpack-plugin": "^4.5.2", + "codemirror": "^5.40.0", "cssnano": "^4.0.5", "eslint": "^5.10.0", "mithril": "^1.1.6", - "postcss-cli": "^6.0.0", "postcss-cssnext": "^3.1.0", "postcss-import": "^11.1.0", "prettier": "^1.15.3", "webpack": "^4.16.3", - "webpack-cli": "^3.1.0", - "webpack-node-externals": "^1.7.2" + "webpack-node-externals": "^1.7.2", + "yaml": "^1.0.0" }, "engines": { "node": ">=8.14.0" @@ -60,8 +56,7 @@ "configure": "if [ ! -f ./config/config.json ]; then cp ./config/config-sample.json ./config/config.json; cp ./config/config-ui-sample.json ./config/config-ui.json; fi", "install": "npm run configure", "test": "node test", - "lint": "eslint --fix lib ui; prettier --write \"lib/**/*.js\" \"ui/**/*.js\"", - "build-ui": "npm run lint;webpack;postcss -c postcss.config.js ui/css/app.css -o dist/public/app.css", - "start-ui": "cd dist && node cluster" + "lint": "eslint --fix lib ui build bin/*; prettier --write \"lib/**/*.js\" \"ui/**/*.js\" \"build/**/*.js\" bin/*", + "build": "node build/build" } } diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 6183fc4..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: [ - require("postcss-import"), - require("postcss-cssnext")({ warnForDuplicates: false }), - require("cssnano") - ] -}; diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json index 5438791..9513ebf 100644 --- a/ui/.eslintrc.json +++ b/ui/.eslintrc.json @@ -1,6 +1,7 @@ { "env": { - "browser": true + "browser": true, + "node": false }, "parser": "babel-eslint", "parserOptions": { diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index a6ac7bf..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,59 +0,0 @@ -const nodeExternals = require("webpack-node-externals"); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const CleanWebpackPlugin = require("clean-webpack-plugin"); -const path = require("path"); - -const mode = "production"; - -function transformPackageJson(content) { - const pkg = JSON.parse(content); - delete pkg["devDependencies"]; - pkg["scripts"] = { - "start-ui": "node cluster" - }; - return JSON.stringify(pkg, null, 2); -} - -module.exports = [ - { - mode: mode, - entry: "./lib/ui/server.js", - target: "node", - externals: [nodeExternals()], - output: { - filename: "server.js" - }, - plugins: [ - new CleanWebpackPlugin(["dist"]), - new CopyWebpackPlugin( - [ - { from: "public/logo.svg", to: "public/logo.svg" }, - { from: "public/favicon.png", to: "public/favicon.png" }, - { from: "config/config-ui.json", to: "config-ui.json" }, - { - from: "package.json", - to: "package.json", - transform: transformPackageJson - } - ], - {} - ) - ] - }, - { - mode: mode, - entry: "./lib/ui/cluster.js", - target: "node", - output: { - filename: "cluster.js" - } - }, - { - mode: mode, - entry: "./ui/app.js", - output: { - path: path.resolve(__dirname, "dist/public"), - filename: "app.js" - } - } -];