UNPKG

signalk-server

Version:

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

272 lines (271 loc) 11.6 kB
"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