Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
/*
* Copyright (C) 2021 Sienci Labs Inc.
*
* This file is part of gSender.
*
* gSender is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, under version 3 of the License.
*
* gSender is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with gSender. If not, see <https://www.gnu.org/licenses/>.
*
* Contact for information regarding this program and its license
* can be sent through gSender@sienci.com or mailed to the main office
* of Sienci Labs Inc. in Waterloo, Ontario, Canada.
*
*/
import bcrypt from "bcrypt-nodejs";
import chalk from "chalk";
import dns from "dns";
import ensureArray from "ensure-array";
import expandTilde from "expand-tilde";
import express from "express";
import fs from "fs";
import httpProxy from "http-proxy";
import escapeRegExp from "lodash/escapeRegExp";
import get from "lodash/get";
import isEqual from "lodash/isEqual";
import set from "lodash/set";
import size from "lodash/size";
import trimEnd from "lodash/trimEnd";
import uniqWith from "lodash/uniqWith";
import os from "os";
import path from "path";
import url from "url";
import webappengine from "webappengine";
import app from "./app";
import settings from "./config/settings";
import { ensureString } from "./lib/ensure-type";
import logger, { setLevel } from "./lib/logger";
import urljoin from "./lib/urljoin";
import cncengine from "./services/cncengine";
import config, { validateAndRepairConfigFile } from "./services/configstore";
import errorConfig from "./services/configstore/alarmStore";
import jobConfig from "./services/configstore/jobStore";
import monitor from "./services/monitor";
import pluginRegistry from "./services/pluginregistry";
const log = logger("init");
const createServer = (options, callback) => {
options = { ...options };
{
// verbosity
const verbosity = options.verbosity;
// https://github.com/winstonjs/winston#logging-levels
if (verbosity === 1) {
set(settings, "verbosity", verbosity);
setLevel("verbose");
}
if (verbosity === 2) {
set(settings, "verbosity", verbosity);
setLevel("debug");
}
if (verbosity === 3) {
set(settings, "verbosity", verbosity);
setLevel("silly");
}
}
const rcfile = path.resolve(options.configFile || settings.rcfile);
const errorFile = path.resolve(options.errorFile || settings.errorFile);
const jobFile = path.resolve(options.jobFile || settings.jobFile);
// Safeguard: if the config file is corrupted (unparseable), back it up and
// reset it to defaults before loading so startup can't crash on bad JSON.
const configRecovery = validateAndRepairConfigFile(rcfile);
// configstore service
log.info(
`Loading configuration from ${chalk.yellow(JSON.stringify(rcfile))}`,
);
errorConfig.load(rcfile, errorFile);
jobConfig.load(rcfile, jobFile);
config.load(rcfile);
// rcfile
settings.rcfile = rcfile;
// secret
if (!config.get("secret")) {
// generate a secret key
const secret = bcrypt.genSaltSync(); // TODO: use a strong secret
config.set("secret", secret);
}
settings.secret = config.get("secret", settings.secret);
{
// watchDirectory
const watchDirectory =
options.watchDirectory || config.get("watchDirectory");
if (watchDirectory) {
if (fs.existsSync(watchDirectory)) {
log.info(
`Watching ${chalk.yellow(JSON.stringify(watchDirectory))} for file changes`,
);
// monitor service
monitor.start({ watchDirectory: watchDirectory });
} else {
log.error(
`The directory ${chalk.yellow(JSON.stringify(watchDirectory))} does not exist.`,
);
}
}
}
{
// accessTokenLifetime
const accessTokenLifetime =
options.accessTokenLifetime || config.get("accessTokenLifetime");
if (accessTokenLifetime) {
set(settings, "accessTokenLifetime", accessTokenLifetime);
}
}
{
// allowRemoteAccess
const allowRemoteAccess =
options.allowRemoteAccess || config.get("allowRemoteAccess", false);
if (allowRemoteAccess) {
if (size(config.get("users")) === 0) {
log.warn(
"You've enabled remote access to the server. It's recommended to create an user account to protect against malicious attacks.",
);
}
set(settings, "allowRemoteAccess", allowRemoteAccess);
}
}
{
// kiosk mode
const kiosk = options.kiosk || config.get("kiosk", false);
if (kiosk) {
set(settings, "kiosk", kiosk);
}
}
let { backlog, port = 0, host } = options;
const isInDevelopmentMode = process.env.NODE_ENV === "development";
// If headless setting is ON, change to correct port and IP
const remoteSettings = config.get("remoteSettings", {});
// Don't do this if: disabled, default IP, dev mode
const setPort = get(remoteSettings, "port", null);
const setIP = get(remoteSettings, "ip", null);
if (
remoteSettings.headlessStatus &&
!isInDevelopmentMode &&
setIP &&
setPort &&
setIP !== "0.0.0.0" &&
remoteSettings.ip.length > 0
) {
port = setPort;
host = setIP;
}
// Serve the plugin SDK's built runtime so the import map in a plugin's
// index.html can resolve the bare @sienci/gsender-plugin-sdk specifiers
const pluginSdkMountPoints = [];
if (settings.pluginSdkDir && fs.existsSync(settings.pluginSdkDir)) {
pluginSdkMountPoints.push({
route: settings.pluginSdkRoute,
target: settings.pluginSdkDir,
});
} else {
log.warn(
`Plugin SDK runtime not found at ${chalk.yellow(JSON.stringify(settings.pluginSdkDir))}; plugins importing the SDK will fail to load. Build packages/plugin-sdk first.`,
);
}
const mountPoints = uniqWith(
[
...ensureArray(options.mountPoints),
...ensureArray(config.get("mountPoints")),
...pluginRegistry.getMountPointsFromPlugins(),
...pluginSdkMountPoints,
],
isEqual,
).filter((mount) => {
if (!mount?.route || mount.route === "/") {
log.error(
`Must specify a valid route path ${JSON.stringify(mount.route)}.`,
);
return false;
}
return true;
});
const routes = [];
mountPoints.forEach((mount) => {
if (ensureString(mount.target).match(/^(http|https):\/\//i)) {
log.info(
`Starting a proxy server to proxy all requests starting with ${chalk.yellow(mount.route)} to ${chalk.yellow(mount.target)}`,
);
routes.push({
type: "server",
route: mount.route,
server: (options) => {
const { route = "/" } = { ...options };
const routeWithoutTrailingSlash = trimEnd(route, "/");
const target = mount.target;
const targetPathname = url.parse(target).pathname;
const proxyPathPattern = new RegExp(
"^" +
escapeRegExp(urljoin(targetPathname, routeWithoutTrailingSlash)),
"i",
);
log.debug(`> route=${chalk.yellow(route)}`);
log.debug(
`> routeWithoutTrailingSlash=${chalk.yellow(routeWithoutTrailingSlash)}`,
);
log.debug(`> target=${chalk.yellow(target)}`);
log.debug(`> targetPathname=${chalk.yellow(targetPathname)}`);
log.debug(
`> proxyPathPattern=RegExp(${chalk.yellow(proxyPathPattern)})`,
);
const proxy = httpProxy.createProxyServer({
// Change the origin of the host header to the target URL
changeOrigin: true,
// Do not verify the SSL certificate for self-signed certs
//secure: false,
target: target,
});
proxy.on("proxyReq", (proxyReq, req, res, options) => {
const originalPath = proxyReq.path || "";
proxyReq.path = originalPath
.replace(proxyPathPattern, targetPathname)
.replace("//", "/");
log.debug(
`proxy.on('proxyReq'): modifiedPath=${chalk.yellow(proxyReq.path)}, originalPath=${chalk.yellow(originalPath)}`,
);
});
proxy.on("proxyRes", (proxyRes, req, res) => {
log.debug(
`proxy.on('proxyRes'): headers=${JSON.stringify(proxyRes.headers, true, 2)}`,
);
});
const app = express();
// Matched routes:
// /widget/
// /widget/v1/
app.all(urljoin(routeWithoutTrailingSlash, "*"), (req, res) => {
const url = req.url;
log.debug(`proxy.web(): url=${chalk.yellow(url)}`);
proxy.web(req, res);
});
// Matched routes:
// /widget
app.all(routeWithoutTrailingSlash, (req, res, next) => {
const url = req.url;
// Redirect URL with a trailing slash
if (
url.indexOf(routeWithoutTrailingSlash) === 0 &&
url.indexOf(routeWithoutTrailingSlash + "/") < 0
) {
const redirectUrl =
routeWithoutTrailingSlash +
"/" +
url.slice(routeWithoutTrailingSlash.length);
log.debug(
`redirect: url=${chalk.yellow(url)}, redirectUrl=${chalk.yellow(redirectUrl)}`,
);
res.redirect(301, redirectUrl);
return;
}
next();
});
return app;
},
});
} else {
// expandTilde('~') => '/Users/<userhome>'
const directory = expandTilde(ensureString(mount.target)).trim();
log.info(
`Mounting a directory ${chalk.yellow(JSON.stringify(directory))} to serve requests starting with ${chalk.yellow(mount.route)}`,
);
if (!directory) {
log.error(
`The directory path ${chalk.yellow(JSON.stringify(directory))} must not be empty.`,
);
return;
}
if (!path.isAbsolute(directory)) {
log.error(
`The directory path ${chalk.yellow(JSON.stringify(directory))} must be absolute.`,
);
return;
}
if (!fs.existsSync(directory)) {
log.error(
`The directory path ${chalk.yellow(JSON.stringify(directory))} does not exist.`,
);
return;
}
routes.push({
type: "static",
route: mount.route,
directory: directory,
});
}
});
routes.push({
type: "server",
route: "/",
server: () => app(),
});
webappengine({ port, host, backlog, routes })
.on("ready", (server) => {
// cncengine service
cncengine.start(
server,
options.controller || config.get("controller", ""),
);
// Dev-only: live-reload plugin iframes when their files change.
if (process.env.NODE_ENV === "development") {
pluginRegistry.watchPlugins(({ dir, filename }) => {
log.info(
`Plugin change detected in ${dir}${filename ? ` (${filename})` : ""}; notifying clients`,
);
// Dev only, and this fires on every file save — so any open
// plugin parser block is flushed with reason "reload" quite
// often here. That is expected in dev; it is logged at info so
// it isn't mistaken for a parser bug.
cncengine.reloadPluginParsers();
cncengine.emit("plugins:changed", { dir, filename });
});
}
const address = server.address().address;
const port = server.address().port;
callback &&
callback(null, {
address,
port,
mountPoints,
configRestored: configRecovery.restored,
configBackupPath: configRecovery.backupPath,
});
if (address !== "0.0.0.0") {
log.info(
"Starting the server at " + chalk.yellow(`http://${address}:${port}`),
);
return;
}
dns.lookup(os.hostname(), { family: 4, all: true }, (err, addresses) => {
if (err) {
log.error("Can't resolve host name:", err);
return;
}
addresses.forEach(({ address, family }) => {
log.info(
"Starting the server at " +
chalk.yellow(`http://${address}:${port}`),
);
});
});
})
.on("error", (err) => {
log.error(err);
log.error(err.name);
const errData = {};
// Handle invalid IP by disabling remote mode until enabled again and signaling error
if (
err.message.includes("address not available") ||
err.message.includes("address already in use")
) {
config.set("remoteSettings.headlessStatus", false);
config.set("remoteSettings.error", true);
errData.bindingErr = true;
}
callback && callback(err, errData);
});
};
export { createServer };
b6d6e761eThis file is pinned to the selected release. Download the original to open it in your design tool.