signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
272 lines (271 loc) • 11.6 kB
JavaScript
"use strict";
/*
* 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.
*/
const { createDebug } = require('../debug');
const debug = createDebug('signalk-server:interfaces:rest');
const express = require('express');
const { getMetadata } = require('@signalk/path-metadata');
const ports = require('../ports');
const { resolveDisplayUnits, getDefaultCategory } = require('../unitpreferences');
function enhanceMetadataResponse(metadata, signalkPath, username) {
if (!metadata)
return metadata;
let storedDisplayUnits = metadata.displayUnits;
if (!storedDisplayUnits?.category && signalkPath) {
const defaultCategory = getDefaultCategory(signalkPath, metadata.units, username);
if (defaultCategory) {
storedDisplayUnits = { category: defaultCategory };
}
}
if (storedDisplayUnits?.category) {
try {
const enhanced = resolveDisplayUnits(storedDisplayUnits, metadata.units, username);
if (enhanced) {
metadata.displayUnits = enhanced;
}
}
catch (err) {
debug('Error enhancing metadata with displayUnits:', err);
}
}
return metadata;
}
const SKIP_KEYS = new Set([
'value',
'meta',
'$source',
'timestamp',
'pgn',
'sentence',
'values'
]);
function enhanceTreeMetadata(node, pathParts, username) {
if (node === null || typeof node !== 'object')
return;
if (node.meta) {
const signalkPath = pathParts.join('.');
const metaCopy = structuredClone(node.meta);
enhanceMetadataResponse(metaCopy, signalkPath, username);
node.meta = metaCopy;
}
for (const key in node) {
if (!SKIP_KEYS.has(key)) {
enhanceTreeMetadata(node[key], pathParts.concat(key), username);
}
}
}
function collectMeta(node, pathParts, result) {
if (node === null || typeof node !== 'object')
return;
if (node.meta) {
result[pathParts.join('.')] = structuredClone(node.meta);
}
for (const key in node) {
if (!SKIP_KEYS.has(key)) {
collectMeta(node[key], pathParts.concat(key), result);
}
}
}
const iso8601rexexp = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?Z$/;
module.exports = function (app) {
'use strict';
const pathPrefix = '/signalk';
const versionPrefix = '/v1';
const apiPathPrefix = pathPrefix + versionPrefix + '/api/';
const snapshotPathPrefix = pathPrefix + versionPrefix + '/snapshot/';
const streamPath = pathPrefix + versionPrefix + '/stream';
const KNOWN_OTHER_PATH_PREFIXES = ['resources'];
return {
start: function () {
app.use('/', express.static(__dirname + '/../../public'));
function snapshotHandler(snapshotPath, req, res, next) {
if (!req.query.time) {
res.status(400).send('Snapshot api requires time query parameter');
}
else if (!iso8601rexexp.test(req.query.time)) {
res
.status(400)
.send('Time query parameter must be a valid ISO 8601 UTC time value like 2018-12-11T18:40:03.246');
}
else if (!app.historyProvider) {
res.status(501).send('No history provider');
}
else {
app.historyProvider.getHistory(new Date(req.query.time), snapshotPath, (deltas) => {
if (deltas.length === 0) {
res.status(404).send('No data found for the given time');
return;
}
const full = app.deltaCache.buildFullFromDeltas(req.skPrincipal, deltas);
if (!full) {
next();
return;
}
let result = full;
for (const p of snapshotPath) {
if (typeof result[p] !== 'undefined') {
result = result[p];
}
else {
next();
return;
}
}
res.json(result);
});
}
}
// Spec-compliant snapshot path: /signalk/v1/snapshot/...
app.get(snapshotPathPrefix + '*', function (req, res, next) {
const pathStr = req.params[0] || '';
const snapshotPath = pathStr.length > 0
? pathStr
.replace(/\/$/, '')
.split('/')
.map((p) => (p === 'self' ? app.selfId : p))
: [];
snapshotHandler(snapshotPath, req, res, next);
});
app.get(apiPathPrefix + '*', function (req, res, next) {
let path = req.path.replace(apiPathPrefix, '');
if (path === 'self') {
return res.json(`vessels.${app.selfId}`);
}
path = path.length > 0 ? path.replace(/\/$/, '').split('/') : [];
if (KNOWN_OTHER_PATH_PREFIXES.indexOf(path[0]) >= 0) {
next();
return;
}
// GET /vessels/<id>/meta: return all metadata for a vessel
if (path.length === 3 && path[0] === 'vessels' && path[2] === 'meta') {
const vesselId = path[1] === 'self' ? app.selfId : path[1];
const full = app.securityStrategy.anyACLs()
? app.deltaCache.buildFull(req.skPrincipal, ['vessels', vesselId])
: app.signalk.retrieve();
const vessel = full?.vessels?.[vesselId];
if (vessel) {
const result = {};
const username = req.skPrincipal?.identifier;
collectMeta(vessel, [], result);
for (const skPath in result) {
enhanceMetadataResponse(result[skPath], skPath, username);
}
res.json(result);
}
else {
next();
}
return;
}
if (path.length > 4 && path[path.length - 1] === 'meta') {
const metaPath = path.slice(0, path.length - 1).join('.');
const meta = getMetadata(metaPath);
if (meta) {
const metaCopy = structuredClone(meta);
const signalkPath = metaPath.replace(/^vessels\.[^.]+\./, '');
const username = req.skPrincipal?.identifier;
res.json(enhanceMetadataResponse(metaCopy, signalkPath, username));
return;
}
}
if (path.length > 5 && path[path.length - 2] === 'meta') {
const meta = getMetadata(path.slice(0, path.length - 2).join('.'));
const value = meta && meta[path[path.length - 1]];
if (value) {
res.json(value);
return;
}
}
path = path.map((p) => (p === 'self' ? app.selfId : p));
function sendResult(last, aPath) {
if (!last) {
next();
return;
}
for (const p of aPath) {
if (typeof last[p] !== 'undefined') {
last = last[p];
}
else {
next();
return;
}
}
if (aPath[0] === 'vessels' || aPath.length === 0) {
const username = req.skPrincipal?.identifier;
// Strip vessels.<id> prefix so paths are signalk-relative for displayUnits lookup
const baseParts = aPath[0] === 'vessels' && aPath.length >= 2
? aPath.slice(2)
: aPath.slice();
enhanceTreeMetadata(last, baseParts, username);
}
return res.json(last);
}
if (path[0] === 'snapshot') {
return snapshotHandler(path.slice(1), req, res, next);
}
const last = app.securityStrategy.anyACLs()
? app.deltaCache.buildFull(req.skPrincipal, path)
: app.signalk.retrieve();
sendResult(last, path);
});
app.get(pathPrefix, function (req, res) {
const host = req.headers.host;
const splitHost = host.split(':');
let httpProtocol = 'http://';
let wsProtocol = 'ws://';
if (app.config.settings.ssl ||
req.headers['x-forwarded-proto'] === 'https') {
httpProtocol = 'https://';
wsProtocol = 'wss://';
}
const services = {
version: getVersion(),
'signalk-http': httpProtocol + host + apiPathPrefix,
'signalk-ws': wsProtocol + host + streamPath
};
if (app.interfaces.tcp?.data) {
services['signalk-tcp'] =
`tcp://${splitHost[0]}:${app.interfaces.tcp.data.port}`;
}
res.json({
endpoints: {
v1: services
},
server: {
id: 'signalk-server-node',
version: app.config.version
}
});
});
if (app.historyProvider?.registerHistoryApiRoute) {
debug('Adding history api route');
const historyApiRouter = express.Router();
app.historyProvider.registerHistoryApiRoute(historyApiRouter);
app.use(pathPrefix + versionPrefix + '/history', historyApiRouter);
}
},
mdns: {
name: app.config.settings.ssl || app.config.isExternalSsl()
? '_signalk-https'
: '_signalk-http',
type: 'tcp',
port: ports.getExternalPort(app)
}
};
};
// @ts-ignore
const getVersion = () => require('../../package.json').version;
//# sourceMappingURL=rest.js.map