signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
854 lines (852 loc) • 37.8 kB
JavaScript
;
/*
* Copyright 2017 Teppo Kurki <teppo.kurki@iki.fi>
*
* 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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const busboy_1 = __importDefault(require("busboy"));
const command_exists_1 = __importDefault(require("command-exists"));
const express_1 = __importDefault(require("express"));
const express_easy_zip_1 = __importDefault(require("express-easy-zip"));
const fs_1 = __importDefault(require("fs"));
const lodash_1 = require("lodash");
const moment_1 = __importDefault(require("moment"));
const ncp_1 = __importDefault(require("ncp"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const unzipper_1 = __importDefault(require("unzipper"));
const util_1 = __importDefault(require("util"));
const swagger_1 = require("./api/swagger");
const config_1 = require("./config/config");
const constants_1 = require("./constants");
const cors_1 = require("./cors");
const debug_1 = require("./debug");
const modules_1 = require("./modules");
const ports_1 = require("./ports");
const requestResponse_1 = require("./requestResponse");
const serialports_1 = require("./serialports");
const signalk_schema_1 = require("@signalk/signalk-schema");
const interfaces_1 = __importDefault(require("./interfaces"));
const redirects_json_1 = __importDefault(require("./redirects.json"));
const readdir = util_1.default.promisify(fs_1.default.readdir);
const debug = (0, debug_1.createDebug)('signalk-server:serverroutes');
const ncp = ncp_1.default.ncp;
const defaultSecurityStrategy = './tokensecurity';
const skPrefix = '/signalk/v1';
module.exports = function (app, saveSecurityConfig, getSecurityConfig) {
let securityWasEnabled = false;
let restoreFilePath;
const logopath = path_1.default.resolve(app.config.configPath, 'logo.svg');
if (fs_1.default.existsSync(logopath)) {
debug(`Found custom logo at ${logopath}, adding route for it`);
app.use('/admin/fonts/signal-k-logo-image-text.*', (req, res) => res.sendFile(logopath));
// Check for custom logo for minimized sidebar, otherwise use the existing logo.
const minimizedLogoPath = path_1.default.resolve(app.config.configPath, 'logo-minimized.svg');
const minimizedLogo = fs_1.default.existsSync(minimizedLogoPath)
? minimizedLogoPath
: logopath;
app.use('/admin/fonts/signal-k-logo-image.*', (req, res) => res.sendFile(minimizedLogo));
}
// mount before the main /admin
(0, swagger_1.mountSwaggerUi)(app, '/doc/openapi');
// mount server-guide
app.use('/documentation', express_1.default.static(__dirname + '/../docs/dist'));
// Redirect old documentation URLs to new ones
let oldpath;
for (oldpath in redirects_json_1.default) {
const from = `/documentation/${oldpath}`;
const to = `/documentation/${redirects_json_1.default[oldpath]}`;
app.get(from, (_, res) => {
res.redirect(301, to);
});
}
app.get('/admin/', (req, res) => {
fs_1.default.readFile(path_1.default.join(__dirname, '/../node_modules/@signalk/server-admin-ui/public/index.html'), (err, indexContent) => {
if (err) {
console.error(err);
res.status(500);
res.type('text/plain');
res.send('Could not handle admin ui root request');
}
res.type('html');
const addonScripts = (0, lodash_1.uniq)([]
.concat(app.addons)
.concat(app.pluginconfigurators)
.concat(app.embeddablewebapps));
setNoCache(res);
res.send(indexContent.toString().replace(/%ADDONSCRIPTS%/g, addonScripts
.map((moduleInfo) => `<script src="/${moduleInfo.name}/remoteEntry.js"></script>`)
.join('\n')
.toString()));
});
});
app.use('/admin', express_1.default.static(__dirname + '/../node_modules/@signalk/server-admin-ui/public'));
app.get('/', (req, res) => {
let landingPage = '/admin/';
// if accessed with hostname that starts with a webapp's displayName redirect there
//strip possible port number
const firstHostName = (req.headers?.host || '')
.split(':')[0]
.split('.')[0]
.toLowerCase();
const targetWebapp = app.webapps.find((webapp) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
webapp.signalk?.displayName.toLowerCase() === firstHostName);
if (targetWebapp) {
landingPage = `/${targetWebapp.name}/`;
}
res.redirect(app.config.settings.landingPage || landingPage);
});
app.get('/@signalk/server-admin-ui', (req, res) => {
res.redirect('/admin/');
});
app.put(`${constants_1.SERVERROUTESPREFIX}/restart`, (req, res) => {
if (app.securityStrategy.allowRestart(req)) {
res.json('Restarting...');
setTimeout(function () {
process.exit(0);
}, 2000);
}
else {
res.status(401).json('Restart not allowed');
}
});
const getLoginStatus = (req, res) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = app.securityStrategy.getLoginStatus(req);
result.securityWasEnabled = securityWasEnabled;
setNoCache(res);
res.json(result);
};
app.get(`${constants_1.SERVERROUTESPREFIX}/loginStatus`, getLoginStatus);
//TODO remove after a grace period
app.get(`/loginStatus`, (req, res) => {
console.log(`/loginStatus is deprecated, try updating webapps to the latest version`);
getLoginStatus(req, res);
});
app.get(`${constants_1.SERVERROUTESPREFIX}/security/config`, (req, res) => {
if (app.securityStrategy.allowConfigure(req)) {
const config = getSecurityConfig(app);
res.json(app.securityStrategy.getConfig(config));
}
else {
res.status(401).json('Security config not allowed');
}
});
app.put(`${constants_1.SERVERROUTESPREFIX}/security/config`, (req, res) => {
if (app.securityStrategy.allowConfigure(req)) {
try {
app.securityStrategy.validateConfiguration(req.body);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (err) {
res.status(400).send(err.message);
return;
}
let config = getSecurityConfig(app);
const configToSave = (0, cors_1.handleAdminUICORSOrigin)(req.body);
config = app.securityStrategy.setConfig(config, configToSave);
saveSecurityConfig(app, config, (err) => {
if (err) {
console.log(err);
res.status(500);
res.json('Unable to save configuration change');
return;
}
res.json('security config saved');
});
}
else {
res.status(401).send('Security config not allowed');
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getConfigSavingCallback(success, failure, res) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (err, config) => {
if (err) {
console.log(err);
res.status(500).type('text/plain').send(failure);
}
else if (config) {
saveSecurityConfig(app, config, (theError) => {
if (theError) {
console.log(theError);
res.status(500).send('Unable to save configuration change');
return;
}
res.type('text/plain').send(success);
});
}
else {
res.type('text/plain').send(success);
}
};
}
function checkAllowConfigure(req, res) {
if (app.securityStrategy.allowConfigure(req)) {
return true;
}
else {
res.status(401).json('Security config not allowed');
return false;
}
}
app.get(`${constants_1.SERVERROUTESPREFIX}/security/devices`, (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
res.json(app.securityStrategy.getDevices(config));
}
});
app.put(`${constants_1.SERVERROUTESPREFIX}/security/devices/:uuid`, (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
app.securityStrategy.updateDevice(config, req.params.uuid, req.body, getConfigSavingCallback('Device updated', 'Unable to update device', res));
}
});
app.delete(`${constants_1.SERVERROUTESPREFIX}/security/devices/:uuid`, (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
app.securityStrategy.deleteDevice(config, req.params.uuid, getConfigSavingCallback('Device deleted', 'Unable to delete device', res));
}
});
app.get(`${constants_1.SERVERROUTESPREFIX}/security/users`, (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
res.json(app.securityStrategy.getUsers(config));
}
});
app.put(`${constants_1.SERVERROUTESPREFIX}/security/users/:id`, (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
app.securityStrategy.updateUser(config, req.params.id, req.body, getConfigSavingCallback('User updated', 'Unable to add user', res));
}
});
app.post(`${constants_1.SERVERROUTESPREFIX}/security/users/:id`, (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
const user = req.body;
user.userId = req.params.id;
app.securityStrategy.addUser(config, user, getConfigSavingCallback('User added', 'Unable to add user', res));
}
});
app.put(`${constants_1.SERVERROUTESPREFIX}/security/user/:username/password`, (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
app.securityStrategy.setPassword(config, req.params.username, req.body, getConfigSavingCallback('Password changed', 'Unable to change password', res));
}
});
app.delete(`${constants_1.SERVERROUTESPREFIX}/security/users/:username`, (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
app.securityStrategy.deleteUser(config, req.params.username, getConfigSavingCallback('User deleted', 'Unable to delete user', res));
}
});
app.put([
`${constants_1.SERVERROUTESPREFIX}/security/access/requests/:identifier/:status`,
'/security/access/requests/:identifier/:status' // for backwards compatibly with existing clients
], (req, res) => {
if (checkAllowConfigure(req, res)) {
const config = getSecurityConfig(app);
app.securityStrategy.setAccessRequestStatus(config, req.params.identifier, req.params.status, req.body, getConfigSavingCallback('Request updated', 'Unable update request', res));
}
});
app.get(`${constants_1.SERVERROUTESPREFIX}/security/access/requests`, (req, res) => {
if (checkAllowConfigure(req, res)) {
res.json(app.securityStrategy.getAccessRequestsResponse());
}
});
app.post(`${skPrefix}/access/requests`, (req, res) => {
const config = getSecurityConfig(app);
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (!app.securityStrategy.requestAccess) {
res.status(404).json({
message: 'Access requests not available. Server security may not be enabled.'
});
return;
}
app.securityStrategy
.requestAccess(config, { accessRequest: req.body }, ip)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.then((reply) => {
res.status(reply.state === 'PENDING' ? 202 : reply.statusCode);
res.json(reply);
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch((err) => {
console.log(err.stack);
res.status(500).send(err.message);
});
});
app.get(`${skPrefix}/requests/:id`, (req, res) => {
(0, requestResponse_1.queryRequest)(req.params.id)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.then((reply) => {
res.json(reply);
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch((err) => {
console.log(err);
res.status(500);
res.type('text/plain').send(`Unable to check request: ${err.message}`);
});
});
app.get(`${constants_1.SERVERROUTESPREFIX}/settings`, (req, res) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const settings = {
interfaces: {},
options: {
mdns: app.config.settings.mdns || false,
wsCompression: app.config.settings.wsCompression || false,
accessLogging: (0, lodash_1.isUndefined)(app.config.settings.accessLogging) ||
app.config.settings.accessLogging,
enablePluginLogging: (0, lodash_1.isUndefined)(app.config.settings.enablePluginLogging) ||
app.config.settings.enablePluginLogging
},
loggingDirectory: app.config.settings.loggingDirectory,
pruneContextsMinutes: app.config.settings.pruneContextsMinutes || 60,
keepMostRecentLogsOnly: (0, lodash_1.isUndefined)(app.config.settings.keepMostRecentLogsOnly) ||
app.config.settings.keepMostRecentLogsOnly,
logCountToKeep: app.config.settings.logCountToKeep || 24,
runFromSystemd: process.env.RUN_FROM_SYSTEMD === 'true',
courseApi: {
apiOnly: app.config.settings.courseApi?.apiOnly || false
}
};
if (!settings.runFromSystemd) {
settings.sslport = (0, ports_1.getSslPort)(app);
settings.port = (0, ports_1.getHttpPort)(app);
settings.options.ssl = app.config.settings.ssl || false;
}
(0, lodash_1.forIn)(interfaces_1.default, function (_interface, name) {
settings.interfaces[name] =
(0, lodash_1.isUndefined)(app.config.settings.interfaces) ||
(0, lodash_1.isUndefined)(app.config.settings.interfaces[name]) ||
app.config.settings.interfaces[name];
});
res.json(settings);
});
if (app.securityStrategy.getUsers(getSecurityConfig(app)).length === 0) {
app.post(`${constants_1.SERVERROUTESPREFIX}/enableSecurity`, (req, res) => {
if (app.securityStrategy.isDummy()) {
app.config.settings.security = { strategy: defaultSecurityStrategy };
const adminUser = req.body;
if (!adminUser.userId ||
adminUser.userId.length === 0 ||
!adminUser.password ||
adminUser.password.length === 0) {
res.status(400).send('userId or password missing or too short');
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(0, config_1.writeSettingsFile)(app, app.config.settings, (err) => {
if (err) {
console.log(err);
res.status(500).send('Unable to save to settings file');
}
else {
const config = {};
// eslint-disable-next-line @typescript-eslint/no-require-imports
const securityStrategy = require(defaultSecurityStrategy)(app, config, saveSecurityConfig);
addUser(req, res, securityStrategy, config);
}
});
}
else {
addUser(req, res, app.securityStrategy);
}
securityWasEnabled = true;
function addUser(request, response, securityStrategy,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config) {
if (!config) {
config = app.securityStrategy.getConfiguration();
}
securityStrategy.addUser(config, request.body, (err, theConfig) => {
if (err) {
console.log(err);
response.status(500);
response.send('Unable to add user');
}
else {
saveSecurityConfig(app, theConfig, (theError) => {
if (theError) {
console.log(theError);
response.status(500);
response.send('Unable to save security configuration change');
}
response.send('Security enabled');
});
}
});
}
});
}
app.put(`${constants_1.SERVERROUTESPREFIX}/settings`, (req, res) => {
const settings = req.body;
(0, lodash_1.forIn)(settings.interfaces, (enabled, name) => {
const interfaces = app.config.settings.interfaces || (app.config.settings.interfaces = {});
interfaces[name] = enabled;
});
if (!(0, lodash_1.isUndefined)(settings.options.mdns)) {
app.config.settings.mdns = settings.options.mdns;
}
if (!(0, lodash_1.isUndefined)(settings.options.ssl)) {
app.config.settings.ssl = settings.options.ssl;
}
if (!(0, lodash_1.isUndefined)(settings.options.wsCompression)) {
app.config.settings.wsCompression = settings.options.wsCompression;
}
if (!(0, lodash_1.isUndefined)(settings.options.accessLogging)) {
app.config.settings.accessLogging = settings.options.accessLogging;
}
if (!(0, lodash_1.isUndefined)(settings.options.enablePluginLogging)) {
app.config.settings.enablePluginLogging =
settings.options.enablePluginLogging;
}
if (!(0, lodash_1.isUndefined)(settings.port)) {
app.config.settings.port = Number(settings.port);
}
if (!(0, lodash_1.isUndefined)(settings.sslport)) {
app.config.settings.sslport = Number(settings.sslport);
}
if (!(0, lodash_1.isUndefined)(settings.loggingDirectory)) {
app.config.settings.loggingDirectory = settings.loggingDirectory;
}
if (!(0, lodash_1.isUndefined)(settings.pruneContextsMinutes)) {
app.config.settings.pruneContextsMinutes = Number(settings.pruneContextsMinutes);
}
if (!(0, lodash_1.isUndefined)(settings.keepMostRecentLogsOnly)) {
app.config.settings.keepMostRecentLogsOnly =
settings.keepMostRecentLogsOnly;
}
if (!(0, lodash_1.isUndefined)(settings.logCountToKeep)) {
app.config.settings.logCountToKeep = Number(settings.logCountToKeep);
}
(0, lodash_1.forIn)(settings.courseApi, (enabled, name) => {
const courseApi = app.config.settings.courseApi || (app.config.settings.courseApi = {});
courseApi[name] = enabled;
});
(0, config_1.writeSettingsFile)(app, app.config.settings, (err) => {
if (err) {
res.status(500).send('Unable to save to settings file');
}
else {
res.type('text/plain').send('Settings changed');
}
});
});
app.get(`${constants_1.SERVERROUTESPREFIX}/vessel`, (req, res) => {
const de = app.config.baseDeltaEditor;
const communication = de.getSelfValue('communication');
const draft = de.getSelfValue('design.draft');
const length = de.getSelfValue('design.length');
const type = de.getSelfValue('design.aisShipType');
const json = {
name: app.config.vesselName,
mmsi: app.config.vesselMMSI,
uuid: app.config.vesselUUID,
draft: draft && draft.maximum,
length: length && length.overall,
beam: de.getSelfValue('design.beam'),
height: de.getSelfValue('design.airHeight'),
gpsFromBow: de.getSelfValue('sensors.gps.fromBow'),
gpsFromCenter: de.getSelfValue('sensors.gps.fromCenter'),
aisShipType: type && type.id,
callsignVhf: communication && communication.callsignVhf
};
res.json(json);
});
function writeOldDefaults(req, res) {
let self;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let data;
try {
data = (0, config_1.readDefaultsFile)(app);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (e) {
if (e.code && e.code === 'ENOENT') {
data = {};
}
else {
console.error(e);
res.status(500).send('Unable to read defaults file');
}
}
self = (0, lodash_1.get)(data, 'vessels.self');
if ((0, lodash_1.isUndefined)(self)) {
self = (0, lodash_1.set)(data, 'vessels.self', {});
}
const newVessel = req.body;
function setString(skPath, value) {
(0, lodash_1.set)(data.vessels.self, skPath, value && value.length > 0 ? value : undefined);
}
function setNumber(skPath, rmPath, value) {
if ((0, lodash_1.isNumber)(value) || (value && value.length > 0)) {
(0, lodash_1.set)(data.vessels.self, skPath, Number(value));
}
else {
(0, lodash_1.unset)(data.vessels.self, rmPath);
}
}
setString('name', newVessel.name);
setString('mmsi', newVessel.mmsi);
if (newVessel.uuid && !self.mmsi) {
setString('uuid', newVessel.uuid);
}
else {
delete self.uuid;
}
setNumber('design.draft.value.maximum', 'design.draft', newVessel.draft);
setNumber('design.length.value.overall', 'design.length', newVessel.length);
setNumber('design.beam.value', 'design.beam', newVessel.beam);
setNumber('design.airHeight.value', 'design.airHeight', newVessel.height);
setNumber('sensors.gps.fromBow.value', 'sensors.gps.fromBow', newVessel.gpsFromBow);
setNumber('sensors.gps.fromCenter.value', 'sensors.gps.fromCenter', newVessel.gpsFromCenter);
if (newVessel.aisShipType) {
(0, lodash_1.set)(data.vessels.self, 'design.aisShipType.value', {
name: (0, signalk_schema_1.getAISShipTypeName)(newVessel.aisShipType),
id: Number(newVessel.aisShipType)
});
}
else {
delete self.design.aisShipType;
}
if (newVessel.callsignVhf) {
setString('communication.callsignVhf', newVessel.callsignVhf);
}
else {
delete self.communication;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(0, config_1.writeDefaultsFile)(app, data, (err) => {
if (err) {
res.status(500).send('Unable to save to defaults file');
}
else {
res.type('text/plain').send('Vessel changed');
}
});
}
app.put(`${constants_1.SERVERROUTESPREFIX}/vessel`, (req, res) => {
const de = app.config.baseDeltaEditor;
const vessel = req.body;
de.setSelfValue('name', vessel.name);
app.config.vesselName = vessel.name;
de.setSelfValue('mmsi', vessel.mmsi);
app.config.vesselMMSI = vessel.mmsi;
if (vessel.uuid && !vessel.mmsi) {
de.setSelfValue('uuid', vessel.uuid);
app.config.vesselUUID = vessel.uuid;
}
else {
de.removeSelfValue('uuid');
delete app.config.vesselUUID;
}
function makeNumber(num) {
return !(0, lodash_1.isUndefined)(num) && ((0, lodash_1.isNumber)(num) || num.length)
? Number(num)
: undefined;
}
de.setSelfValue('design.draft', !(0, lodash_1.isUndefined)(vessel.draft) ? { maximum: Number(vessel.draft) } : undefined);
de.setSelfValue('design.length', !(0, lodash_1.isUndefined)(vessel.length)
? { overall: Number(vessel.length) }
: undefined);
de.setSelfValue('design.beam', makeNumber(vessel.beam));
de.setSelfValue('design.airHeight', makeNumber(vessel.height));
de.setSelfValue('sensors.gps.fromBow', makeNumber(vessel.gpsFromBow));
de.setSelfValue('sensors.gps.fromCenter', makeNumber(vessel.gpsFromCenter));
de.setSelfValue('design.aisShipType', !(0, lodash_1.isUndefined)(vessel.aisShipType)
? {
name: (0, signalk_schema_1.getAISShipTypeName)(vessel.aisShipType),
id: Number(vessel.aisShipType)
}
: undefined);
de.setSelfValue('communication', !(0, lodash_1.isUndefined)(vessel.callsignVhf) && vessel.callsignVhf.length
? { callsignVhf: vessel.callsignVhf }
: undefined);
app.emit('serverevent', {
type: 'VESSEL_INFO',
data: {
name: app.config.vesselName,
mmsi: app.config.vesselMMSI,
uuid: app.config.vesselUUID
}
});
(0, config_1.sendBaseDeltas)(app);
if (app.config.hasOldDefaults) {
writeOldDefaults(req, res);
}
else {
(0, config_1.writeBaseDeltasFile)(app)
.then(() => {
res.type('text/plain').send('Vessel changed');
})
.catch(() => {
res.status(500).send('Unable to save to defaults file');
});
}
});
app.get(`${constants_1.SERVERROUTESPREFIX}/availablePaths`, (req, res) => {
res.json(app.streambundle.getAvailablePaths());
});
app.securityStrategy.addAdminMiddleware(`${constants_1.SERVERROUTESPREFIX}/eventsRoutingData`);
app.get(`${constants_1.SERVERROUTESPREFIX}/eventsRoutingData`, (req, res) => {
res.json(app.wrappedEmitter.getEventRoutingData());
});
app.get(`${constants_1.SERVERROUTESPREFIX}/serialports`, (req, res, next) => {
(0, serialports_1.listAllSerialPorts)()
.then((ports) => res.json(ports))
.catch(next);
});
app.get(`${constants_1.SERVERROUTESPREFIX}/hasAnalyzer`, (req, res) => {
(0, command_exists_1.default)('analyzer')
.then(() => res.json(true))
.catch(() => res.json(false));
});
app.get(`${constants_1.SERVERROUTESPREFIX}/sourcePriorities`, (req, res) => {
res.json(app.config.settings.sourcePriorities || {});
});
app.put(`${constants_1.SERVERROUTESPREFIX}/sourcePriorities`, (req, res) => {
app.config.settings.sourcePriorities = req.body;
app.activateSourcePriorities();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(0, config_1.writeSettingsFile)(app, app.config.settings, (err) => {
if (err) {
res
.status(500)
.send('Unable to save to sourcePrefences in settings file');
}
else {
res.json({ result: 'ok' });
}
});
});
app.post(`${constants_1.SERVERROUTESPREFIX}/debug`, (req, res) => {
if (!app.logging.enableDebug(req.body.value)) {
res.status(400).send('invalid debug value');
}
else {
res.status(200).send();
}
});
app.get(`${constants_1.SERVERROUTESPREFIX}/debugKeys`, (req, res) => {
res.json((0, debug_1.listKnownDebugs)());
});
app.post(`${constants_1.SERVERROUTESPREFIX}/rememberDebug`, (req, res) => {
app.logging.rememberDebug(req.body.value);
res.status(200).send();
});
app.get(`${skPrefix}/apps/list`, (req, res) => {
res.json(app.webapps.map((webapp) => {
return {
name: webapp.name,
version: webapp.version,
description: webapp.description,
location: `/${webapp.name}`,
license: webapp.license,
author: (0, modules_1.getAuthor)(webapp)
};
}));
});
const safeFiles = [
'settings.json',
'defaults.json',
'security.json',
'package.json',
'baseDeltas.json'
];
function listSafeRestoreFiles(restorePath) {
return new Promise((resolve, reject) => {
readdir(restorePath)
.catch(reject)
.then((filenames) => {
const goodFiles = filenames?.filter((name) => safeFiles.indexOf(name) !== -1) || [];
filenames?.forEach((name) => {
try {
const stats = fs_1.default.lstatSync(path_1.default.join(restorePath, name));
if (stats.isDirectory()) {
goodFiles.push(name + '/');
}
resolve(goodFiles);
}
catch (err) {
reject(err);
}
});
});
});
}
function sendRestoreStatus(state, message, percentComplete) {
const status = {
state,
message,
percentComplete: percentComplete ? percentComplete * 100 : '-'
};
app.emit('serverevent', {
type: 'RESTORESTATUS',
from: 'signalk-server',
data: status
});
}
app.post(`${constants_1.SERVERROUTESPREFIX}/restore`, (req, res) => {
if (!restoreFilePath) {
res.status(400).send('not exting restore file');
}
else if (!fs_1.default.existsSync(restoreFilePath)) {
res.status(400).send('restore file does not exist');
}
else {
res.status(202).send();
}
listSafeRestoreFiles(restoreFilePath)
.then((files) => {
const wanted = files.filter((name) => {
return req.body[name];
});
let hasError = false;
for (let i = 0; i < wanted.length; i++) {
const name = wanted[i];
sendRestoreStatus('Copying Files', `Copying ${name}`, i / wanted.length);
ncp(path_1.default.join(restoreFilePath, name), path_1.default.join(app.config.configPath, name), { stopOnErr: true },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(err) => {
if (err) {
sendRestoreStatus('error', err.message, null);
hasError = true;
}
});
}
if (!hasError) {
sendRestoreStatus('Installing Plugins', '', 1);
(0, modules_1.restoreModules)(app.config, (output) => {
sendRestoreStatus('Installing Plugins', `${output}`, 1);
console.log(`stdout: ${output}`);
}, (output) => {
//sendRestoreStatus('Error', `${output}`, 1)
console.error(`stderr: ${output}`);
}, () => {
sendRestoreStatus('Complete', 'Please restart', 1);
});
}
})
.catch((err) => {
console.error(err);
sendRestoreStatus('error', err.message, null);
});
});
app.post(`${constants_1.SERVERROUTESPREFIX}/validateBackup`, (req, res) => {
const bb = (0, busboy_1.default)({ headers: req.headers });
bb.on('file', (fieldname,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
file, { filename }) => {
try {
if (!filename.endsWith('.backup')) {
res
.status(400)
.send('the backup file does not have the .backup extension');
return;
}
if (!filename.startsWith('signalk-')) {
res
.status(400)
.send('the backup file does not start with signalk-');
return;
}
const tmpDir = os_1.default.tmpdir();
restoreFilePath = fs_1.default.mkdtempSync(`${tmpDir}${path_1.default.sep}`);
const zipFileDir = fs_1.default.mkdtempSync(`${tmpDir}${path_1.default.sep}`);
const zipFile = path_1.default.join(zipFileDir, 'backup.zip');
const unzipStream = unzipper_1.default.Extract({ path: restoreFilePath });
file
.pipe(fs_1.default.createWriteStream(zipFile))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.on('error', (err) => {
console.error(err);
res.status(500).send(err.message);
})
.on('close', () => {
const zipStream = fs_1.default.createReadStream(zipFile);
zipStream
.pipe(unzipStream)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.on('error', (err) => {
console.error(err);
res.status(500).send(err.message);
})
.on('close', () => {
fs_1.default.unlinkSync(zipFile);
listSafeRestoreFiles(restoreFilePath)
.then((files) => {
res.type('text/plain').send(files);
})
.catch((err) => {
console.error(err);
res.status(500).send(err.message);
});
});
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (err) {
console.log(err);
res.status(500).send(err.message);
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
bb.on('error', (err) => {
console.log(err);
res.status(500).send(err.message);
});
bb.on('finish', function () {
console.log('finish');
});
req.pipe(bb);
});
app.use((0, express_easy_zip_1.default)());
app.get(`${constants_1.SERVERROUTESPREFIX}/backup`, (req, res) => {
readdir(app.config.configPath).then((filenames) => {
const files = filenames
.filter((file) => {
return ((file !== 'node_modules' ||
(file === 'node_modules' &&
req.query.includePlugins === 'true')) &&
!file.endsWith('.log') &&
file !== 'signalk-server' &&
file !== '.npmrc');
})
.map((name) => {
const filename = path_1.default.join(app.config.configPath, name);
return {
path: filename,
name
};
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyRes = res;
anyRes.zip({
files,
filename: `signalk-${(0, moment_1.default)().format('MMM-DD-YYYY-HHTmm')}.backup`
});
});
});
};
const setNoCache = (res) => {
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
res.header('Pragma', 'no-cache');
res.header('Expires', '0');
};