signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
585 lines (584 loc) • 24.3 kB
JavaScript
;
/* 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