UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

854 lines (852 loc) 37.8 kB
"use strict"; /* * 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'); };