UNPKG

signalk-server

Version:

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

585 lines (584 loc) 24.3 kB
"use strict"; /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-this-alias */ /* * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl> * * 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 }); if (typeof [].includes !== 'function') { console.log('Minimum required Node.js version is 6, please update.'); process.exit(-1); } const server_api_1 = require("@signalk/server-api"); const signalk_schema_1 = require("@signalk/signalk-schema"); const express_1 = __importDefault(require("express")); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); const lodash_1 = __importDefault(require("lodash")); const path_1 = __importDefault(require("path")); const api_1 = require("./api"); const config_1 = require("./config/config"); const debug_1 = require("./debug"); const deltacache_1 = __importDefault(require("./deltacache")); const deltachain_1 = __importDefault(require("./deltachain")); const deltaPriority_1 = require("./deltaPriority"); const deltastats_1 = require("./deltastats"); const modules_1 = require("./modules"); const ports_1 = require("./ports"); const security_js_1 = require("./security.js"); const cors_1 = require("./cors"); const subscriptionmanager_1 = __importDefault(require("./subscriptionmanager")); const pipedproviders_1 = require("./pipedproviders"); const events_1 = require("./events"); const zones_1 = require("./zones"); const version_1 = __importDefault(require("./version")); const debug = (0, debug_1.createDebug)('signalk-server'); const streambundle_1 = require("./streambundle"); class Server { app; constructor(opts) { (0, version_1.default)(); const FILEUPLOADSIZELIMIT = process.env.FILEUPLOADSIZELIMIT || '10mb'; const bodyParser = require('body-parser'); const app = (0, express_1.default)(); app.use(require('compression')()); app.use(bodyParser.json({ limit: FILEUPLOADSIZELIMIT })); this.app = app; app.started = false; lodash_1.default.merge(app, opts); (0, config_1.load)(app); app.logging = require('./logging')(app); app.version = '0.0.1'; (0, cors_1.setupCors)(app, (0, security_js_1.getSecurityConfig)(app)); (0, security_js_1.startSecurity)(app, opts ? opts.securityConfig : null); require('./serverroutes')(app, security_js_1.saveSecurityConfig, security_js_1.getSecurityConfig); require('./put').start(app); app.signalk = new signalk_schema_1.FullSignalK(app.selfId, app.selfType); app.propertyValues = new server_api_1.PropertyValues(); const deltachainV1 = new deltachain_1.default(app.signalk.addDelta.bind(app.signalk)); const deltachainV2 = new deltachain_1.default((delta) => app.signalk.emit('delta', delta)); app.registerDeltaInputHandler = (handler) => { const unRegisterHandlers = [ deltachainV1.register(handler), deltachainV2.register(handler) ]; return () => unRegisterHandlers.forEach((f) => f()); }; app.providerStatus = {}; // feature detection app.getFeatures = async (enabled) => { return { apis: enabled === false ? [] : app.apis, plugins: await app.getPluginsList(enabled) }; }; // create first temporary pluginManager to get typechecks, as // app is any and not typechecked // TODO separate app.plugins and app.pluginsMap from app const pluginManager = { setPluginOpenApi: (pluginId, openApi) => { app.pluginsMap[pluginId].openApi = openApi; }, getPluginOpenApi: (pluginId) => ({ name: `plugins/${pluginId}`, path: `/plugins/${pluginId}`, apiDoc: app.pluginsMap[pluginId].openApi }), getPluginOpenApiRecords: () => Object.keys(app.pluginsMap).reduce((acc, pluginId) => { if (app.pluginsMap[pluginId].openApi) { acc.push({ name: `plugins/${pluginId}`, path: `/plugins/${pluginId}`, apiDoc: app.pluginsMap[pluginId].openApi }); } return acc; }, []) }; Object.assign(app, pluginManager); app.setPluginStatus = (providerId, statusMessage) => { doSetProviderStatus(providerId, statusMessage, 'status', 'plugin'); }; app.setPluginError = (providerId, errorMessage) => { doSetProviderStatus(providerId, errorMessage, 'error', 'plugin'); }; app.setProviderStatus = (providerId, statusMessage) => { doSetProviderStatus(providerId, statusMessage, 'status'); }; app.setProviderError = (providerId, errorMessage) => { doSetProviderStatus(providerId, errorMessage, 'error'); }; function doSetProviderStatus(providerId, statusMessage, type, statusType = 'provider') { if (!statusMessage) { delete app.providerStatus[providerId]; return; } if (lodash_1.default.isUndefined(app.providerStatus[providerId])) { app.providerStatus[providerId] = {}; } const status = app.providerStatus[providerId]; if (status.type === 'error' && status.message !== statusMessage) { status.lastError = status.message; status.lastErrorTimeStamp = status.timeStamp; } status.type = type; status.id = providerId; status.statusType = statusType; status.timeStamp = new Date().toLocaleString(); status.message = statusMessage; app.emit('serverevent', { type: 'PROVIDERSTATUS', from: 'signalk-server', data: app.getProviderStatus() }); } app.getProviderStatus = () => { const providerStatus = lodash_1.default.values(app.providerStatus); if (app.plugins) { app.plugins.forEach((plugin) => { try { if (typeof plugin.statusMessage === 'function' && lodash_1.default.isUndefined(app.providerStatus[plugin.id])) { let message = plugin.statusMessage(); if (message) { message = message.trim(); if (message.length > 0) { providerStatus.push({ message, type: 'status', id: plugin.id, statusType: 'plugin' }); } } } } catch (e) { console.error(e); providerStatus.push({ message: 'Error fetching provider status, see server log for details', type: 'status', id: plugin.id }); } }); } return providerStatus; }; app.registerHistoryProvider = (provider) => { app.historyProvider = provider; }; app.unregisterHistoryProvider = () => { delete app.historyProvider; }; let toPreferredDelta = () => undefined; app.activateSourcePriorities = () => { try { toPreferredDelta = (0, deltaPriority_1.getToPreferredDelta)(app.config.settings.sourcePriorities); } catch (e) { console.error(`getToPreferredDelta failed: ${e.message}`); } }; app.activateSourcePriorities(); app.handleMessage = (providerId, data, skVersion = server_api_1.SKVersion.v1) => { if (data && Array.isArray(data.updates)) { (0, deltastats_1.incDeltaStatistics)(app, providerId); if (typeof data.context === 'undefined' || data.context === 'vessels.self') { data.context = ('vessels.' + app.selfId); } const now = new Date(); data.updates = data.updates .map((update) => { if (typeof update.source !== 'undefined') { update.source.label = providerId; if (!update.$source) { update.$source = (0, signalk_schema_1.getSourceId)(update.source); } } else { if (typeof update.$source === 'undefined') { update.$source = providerId; } } if (!update.timestamp || app.config.overrideTimestampWithNow) { update.timestamp = now.toISOString(); } if ('values' in update && !Array.isArray(update.values)) { debug(`handleMessage: ignoring invalid values`, update.values); delete update.values; } if ('meta' in update && !Array.isArray(update.meta)) { debug(`handleMessage: ignoring invalid meta`, update.meta); delete update.meta; } if ('values' in update || 'meta' in update) { return update; } }) .filter((update) => update !== undefined); // No valid updates, discarding if (data.updates.length < 1) return; try { let delta = filterStaticSelfData(data, app.selfContext); delta = toPreferredDelta(delta, now, app.selfContext); if (skVersion == server_api_1.SKVersion.v1) { deltachainV1.process(delta); } else { deltachainV2.process(delta); } } catch (err) { console.error(err); } } }; app.streambundle = new streambundle_1.StreamBundle(app.selfId); new zones_1.Zones(app.streambundle, (delta) => process.nextTick(() => app.handleMessage('self.notificationhandler', delta))); app.signalk.on('delta', app.streambundle.pushDelta.bind(app.streambundle)); app.subscriptionmanager = new subscriptionmanager_1.default(app); app.deltaCache = new deltacache_1.default(app, app.streambundle); app.getHello = () => ({ name: app.config.name, version: app.config.version, self: `vessels.${app.selfId}`, roles: ['master', 'main'], timestamp: new Date() }); app.isNmea2000OutAvailable = false; app.on('nmea2000OutAvailable', () => { app.isNmea2000OutAvailable = true; }); } start() { const self = this; const app = this.app; app.wrappedEmitter = (0, events_1.wrapEmitter)(app); app.emit = app.wrappedEmitter.emit; app.on = app.wrappedEmitter.addListener; app.addListener = app.wrappedEmitter.addListener; this.app.intervals = []; this.app.intervals.push(setInterval(app.signalk.pruneContexts.bind(app.signalk, (app.config.settings.pruneContextsMinutes || 60) * 60), 60 * 1000)); this.app.intervals.push(setInterval(app.deltaCache.pruneContexts.bind(app.deltaCache, (app.config.settings.pruneContextsMinutes || 60) * 60), 60 * 1000)); app.intervals.push(setInterval(() => { app.emit('serverevent', { type: 'PROVIDERSTATUS', from: 'signalk-server', data: app.getProviderStatus() }); }, 5 * 1000)); function serverUpgradeIsAvailable(err, newVersion) { if (err) { console.error(err); return; } const msg = `A new version (${newVersion}) of the server is available`; console.log(msg); app.handleMessage(app.config.name, { updates: [ { values: [ { path: 'notifications.server.newVersion', value: { state: 'normal', method: [], message: msg } } ] } ] }); } if (!process.env.SIGNALK_DISABLE_SERVER_UPDATES) { (0, modules_1.checkForNewServerVersion)(app.config.version, serverUpgradeIsAvailable); app.intervals.push(setInterval(() => (0, modules_1.checkForNewServerVersion)(app.config.version, serverUpgradeIsAvailable), 60 * 1000 * 60 * 24)); } this.app.providers = []; app.lastServerEvents = {}; app.on('serverevent', (event) => { if (event.type) { app.lastServerEvents[event.type] = event; } }); app.intervals.push((0, deltastats_1.startDeltaStatistics)(app)); return new Promise(async (resolve, reject) => { createServer(app, async (err, server) => { if (err) { reject(err); return; } app.server = server; app.interfaces = {}; app.clients = 0; debug('ID type: ' + app.selfType); debug('ID: ' + app.selfId); (0, config_1.sendBaseDeltas)(app); app.apis = await (0, api_1.startApis)(app); await startInterfaces(app); startMdns(app); app.providers = (0, pipedproviders_1.pipedProviders)(app).start(); const primaryPort = (0, ports_1.getPrimaryPort)(app); debug(`primary port:${primaryPort}`); server.listen(primaryPort, () => { console.log('signalk-server running at 0.0.0.0:' + primaryPort.toString() + '\n'); app.started = true; resolve(self); }); const secondaryPort = (0, ports_1.getSecondaryPort)(app); debug(`secondary port:${primaryPort}`); if (app.config.settings.ssl && secondaryPort) { startRedirectToSsl(secondaryPort, (0, ports_1.getExternalPort)(app), (anErr, aServer) => { if (!anErr) { app.redirectServer = aServer; } }); } }); }); } reload(mixed) { let settings; const self = this; if (typeof mixed === 'string') { try { settings = require(path_1.default.join(process.cwd(), mixed)); } catch (_e) { debug(`Settings file '${settings}' does not exist`); } } if (mixed !== null && typeof mixed === 'object') { settings = mixed; } if (settings) { this.app.config.settings = settings; } this.stop().catch((e) => console.error(e)); setTimeout(() => { self.start().catch((e) => console.error(e)); }, 1000); return this; } stop(cb) { return new Promise((resolve, reject) => { if (!this.app.started) { resolve(this); } else { try { lodash_1.default.each(this.app.interfaces, (intf) => { if (intf !== null && typeof intf === 'object' && typeof intf.stop === 'function') { intf.stop(); } }); this.app.intervals.forEach((interval) => { clearInterval(interval); }); this.app.providers.forEach((providerHolder) => { providerHolder.pipeElements[0].end(); }); debug('Closing server...'); const that = this; this.app.server.close(() => { debug('Server closed'); if (that.app.redirectServer) { try { that.app.redirectServer.close(() => { debug('Redirect server closed'); delete that.app.redirectServer; that.app.started = false; cb && cb(); resolve(that); }); } catch (err) { reject(err); } } else { that.app.started = false; cb && cb(); resolve(that); } }); } catch (err) { reject(err); } } }); } } module.exports = Server; function createServer(app, cb) { if (app.config.settings.ssl) { (0, security_js_1.getCertificateOptions)(app, (err, options) => { if (err) { cb(err); } else { debug('Starting server to serve both http and https'); cb(null, https_1.default.createServer(options, app)); } }); return; } let server; try { debug('Starting server to serve only http'); server = http_1.default.createServer(app); } catch (e) { cb(e); return; } cb(null, server); } function startRedirectToSsl(port, redirectPort, cb) { const redirectApp = (0, express_1.default)(); redirectApp.use((req, res) => { const host = req.headers.host?.split(':')[0]; res.redirect(`https://${host}:${redirectPort}${req.path}`); }); const server = http_1.default.createServer(redirectApp); server.listen(port, () => { console.log(`Redirect server running on port ${port.toString()}`); cb(null, server); }); } function startMdns(app) { if (lodash_1.default.isUndefined(app.config.settings.mdns) || app.config.settings.mdns) { debug(`Starting interface 'mDNS'`); try { app.interfaces.mdns = require('./mdns')(app); } catch (ex) { console.error('Could not start mDNS:' + ex); } } else { debug(`Interface 'mDNS' was disabled in configuration`); } } async function startInterfaces(app) { debug('Interfaces config:' + JSON.stringify(app.config.settings.interfaces)); const availableInterfaces = require('./interfaces'); return await Promise.all(Object.keys(availableInterfaces).map(async (name) => { const theInterface = availableInterfaces[name]; if (lodash_1.default.isUndefined(app.config.settings.interfaces) || lodash_1.default.isUndefined((app.config.settings.interfaces || {})[name]) || (app.config.settings.interfaces || {})[name]) { debug(`Loading interface '${name}'`); const boundEventMethods = app.wrappedEmitter.bindMethodsById(`interface:${name}`); const appCopy = { ...app, ...boundEventMethods }; const handler = { set(obj, prop, value) { ; app[prop] = value; return true; }, get(target, prop, _receiver) { return app[prop]; } }; const _interface = (appCopy.interfaces[name] = theInterface(new Proxy(appCopy, handler))); if (_interface && lodash_1.default.isFunction(_interface.start)) { if (lodash_1.default.isUndefined(_interface.forceInactive) || !_interface.forceInactive) { debug(`Starting interface '${name}'`); _interface.data = _interface.start(); } else { debug(`Not starting interface '${name}' by forceInactive`); } } } else { debug(`Not loading interface '${name}' because of configuration`); } })); } function filterStaticSelfData(delta, selfContext) { if (delta.context === selfContext) { delta.updates && delta.updates.forEach((update) => { if ('values' in update && update['$source'] !== 'defaults') { update.values = update.values.reduce((acc, pathValue) => { const nvp = filterSelfDataKP(pathValue); if (nvp) { acc.push(nvp); } return acc; }, []); if (update.values.length == 0) { delete update.values; } } }); } return delta; } function filterSelfDataKP(pathValue) { const deepKeys = { '': ['name', 'mmsi'] }; const filteredPaths = [ 'design.aisShipType', 'design.beam', 'design.length', 'design.draft', 'sensors.gps.fromBow', 'sensors.gps.fromCenter' ]; const deep = deepKeys[pathValue.path]; const filterValues = (obj, items) => { const res = {}; Object.keys(obj).forEach((k) => { if (!items.includes(k)) { res[k] = obj[k]; } }); return res; }; if (deep !== undefined) { if (Object.keys(pathValue.value).some((k) => deep.includes(k))) { pathValue.value = filterValues(pathValue.value, deep); } if (pathValue.path === '' && pathValue.value.communication !== undefined) { pathValue.value.communication = filterValues(pathValue.value.communication, ['callsignVhf']); } if (Object.keys(pathValue.value).length == 0) { return null; } } else if (filteredPaths.includes(pathValue.path)) { return null; } return pathValue; } //# sourceMappingURL=index.js.map