UNPKG

signalk-server

Version:

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

433 lines (432 loc) 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const _ = require('lodash'); const debug_1 = require("./debug"); const debug = (0, debug_1.createDebug)('signalk-server:put'); const { createRequest, updateRequest } = require('./requestResponse'); const skConfig = require('./config/config'); const { getMetadata } = require('@signalk/signalk-schema'); const pathPrefix = '/signalk'; const versionPrefix = '/v1'; const apiPathPrefix = pathPrefix + versionPrefix + '/api/'; const actionHandlers = {}; let putMetaHandler, deleteMetaHandler, putNotificationHandler; module.exports = { start: function (app) { app.registerActionHandler = registerActionHandler; app.deRegisterActionHandler = deRegisterActionHandler; app.delete(apiPathPrefix + '*', function (req, res) { let path = String(req.path).replace(apiPathPrefix, ''); path = path.replace(/\/$/, '').replace(/\//g, '.'); const parts = path.length > 0 ? path.split('.') : []; if (parts.length < 3) { res.status(400).send('invalid path'); return; } const context = `${parts[0]}.${parts[1]}`; const skpath = parts.slice(2).join('.'); deletePath(app, context, skpath, req) .then((reply) => { res.status(reply.statusCode); res.json(reply); }) .catch((err) => { console.error(err); res.status(500).send(err.message); }); }); app.put(apiPathPrefix + '*', function (req, res) { let path = String(req.path).replace(apiPathPrefix, ''); const value = req.body; if (_.isUndefined(value.value)) { res.status(400).send('input is missing a value'); return; } path = path.replace(/\/$/, '').replace(/\//g, '.'); const parts = path.length > 0 ? path.split('.') : []; if (parts.length < 3) { res.status(400).send('invalid path'); return; } const context = `${parts[0]}.${parts[1]}`; const skpath = parts.slice(2).join('.'); putPath(app, context, skpath, value, req) .then((reply) => { res.status(reply.statusCode); res.json(reply); }) .catch((err) => { console.error(err); res.status(500).send(err.message); }); }); putMetaHandler = (context, path, value, cb) => { let parts = path.split('.'); let metaPath = path; let metaValue = value; if (parts[parts.length - 1] !== 'meta') { let name = parts[parts.length - 1]; metaPath = parts.slice(0, parts.length - 2).join('.'); metaValue = { ...app.config.baseDeltaEditor.getMeta(context, metaPath), [name]: value }; } else { metaPath = parts.slice(0, parts.length - 1).join('.'); } // set empty zones array explicitly to null for (const prop in metaValue) { if (Array.isArray(metaValue[prop]) && metaValue[prop].length === 0) { metaValue[prop] = null; } } app.config.baseDeltaEditor.setMeta(context, metaPath, metaValue); let full_meta = getMetadata('vessels.self.' + metaPath); app.handleMessage('defaults', { context: 'vessels.self', updates: [ { meta: [ { path: metaPath, value: { ...full_meta, ...metaValue } } ] } ] }); if (app.config.hasOldDefaults) { let data; try { data = skConfig.readDefaultsFile(app); } catch (e) { if (e.code && e.code === 'ENOENT') { data = {}; } else { console.error(e); cb({ state: 'FAILURE', message: 'Unable to read defaults file' }); return; } } const pathWithContext = context + '.' + path; _.set(data, pathWithContext, value); skConfig.writeDefaultsFile(app, data, (err) => { if (err) { cb({ state: 'FAILURE', message: 'Unable to save to defaults file' }); } else { cb({ state: 'SUCCESS' }); } }); } else { skConfig .writeBaseDeltasFile(app, app.config.baseDeltas) .then(() => { cb({ state: 'SUCCESS' }); }) .catch(() => { cb({ state: 'FAILURE', message: 'Unable to save to defaults file' }); }); } return { state: 'PENDING' }; }; deleteMetaHandler = (context, path, cb) => { let parts = path.split('.'); let metaPath = path; let full_meta; //fixme, make sure meta path exists... if (parts[parts.length - 1] !== 'meta') { let name = parts[parts.length - 1]; metaPath = parts.slice(0, parts.length - 2).join('.'); let metaValue = { ...app.config.baseDeltaEditor.getMeta(context, metaPath) }; if (typeof metaValue[name] === 'undefined') { return { state: 'COMPLETED', statusCode: 404 }; } delete metaValue[name]; full_meta = getMetadata('vessels.self.' + metaPath); delete full_meta[name]; app.config.baseDeltaEditor.setMeta(context, metaPath, metaValue); if (Object.keys(metaValue).length == 0) { app.config.baseDeltaEditor.removeMeta(context, metaPath); } } else { metaPath = parts.slice(0, parts.length - 1).join('.'); full_meta = getMetadata('vessels.self.' + metaPath); let metaValue = app.config.baseDeltaEditor.getMeta(context, metaPath); if (!metaValue) { return { state: 'COMPLETED', statusCode: 404 }; } Object.keys(metaValue).forEach((key) => { delete full_meta[key]; }); app.config.baseDeltaEditor.removeMeta(context, metaPath); } app.handleMessage('defaults', { context: 'vessels.self', updates: [ { meta: [ { path: metaPath, value: full_meta } ] } ] }); skConfig .writeBaseDeltasFile(app, app.config.baseDeltas) .then(() => { cb({ state: 'COMPLETED', statusCode: 200 }); }) .catch(() => { cb({ state: 'COMPLETED', statusCode: 502, message: 'Unable to save to defaults file' }); }); return { state: 'PENDING' }; }; putNotificationHandler = (context, path, value) => { return putNotification(app, context, path, value); }; }, registerActionHandler: registerActionHandler, putPath: putPath, deletePath }; function deletePath(app, contextParam, path, req, requestId, updateCb) { const context = contextParam || 'vessels.self'; debug('received delete %s %s', context, path); return new Promise((resolve, reject) => { createRequest(app, 'delete', { context: context, requestId: requestId, delete: { path: path } }, req && req.skPrincipal ? req.skPrincipal.identifier : undefined, null, updateCb) .then((request) => { if (req && app.securityStrategy.shouldAllowPut(req, context, null, path) === false) { updateRequest(request.requestId, 'COMPLETED', { statusCode: 403 }) .then(resolve) .catch(reject); return; } const parts = path.split('.'); let handler; if ((parts.length > 1 && parts[parts.length - 1] === 'meta') || (parts.length > 1 && parts[parts.length - 2] === 'meta')) { handler = deleteMetaHandler; } if (handler) { const actionResult = handler(context, path, (reply) => { debug('got result: %j', reply); updateRequest(request.requestId, reply.state, reply) .then(() => undefined) .catch((err) => { console.error(err); }); }); Promise.resolve(actionResult) .then((result) => { debug('got result: %j', result); updateRequest(request.requestId, result.state, result) .then((reply) => { if (reply.state === 'PENDING') { // backwards compatibility reply.action = { href: reply.href }; } resolve(reply); }) .catch(reject); }) .catch((err) => { updateRequest(request.requestId, 'COMPLETED', { statusCode: 500, message: err.message }) .then(resolve) .catch(reject); }); } else { updateRequest(request.requestId, 'COMPLETED', { statusCode: 405, message: `DELTETE not supported for ${path}` }) .then(resolve) .catch(reject); } }) .catch(reject); }); } function putPath(app, contextParam, path, body, req, requestId, updateCb) { const context = contextParam || 'vessels.self'; debug('received put %s %s %j', context, path, body); return new Promise((resolve, reject) => { createRequest(app, 'put', { context: context, requestId: requestId, put: { path: path, value: body.value } }, req && req.skPrincipal ? req.skPrincipal.identifier : undefined, null, updateCb) .then((request) => { if (req && app.securityStrategy.shouldAllowPut(req, context, null, path) === false) { updateRequest(request.requestId, 'COMPLETED', { statusCode: 403 }) .then(resolve) .catch(reject); return; } let handler; const parts = path.split('.'); if ((parts.length > 1 && parts[parts.length - 1] === 'meta') || (parts.length > 1 && parts[parts.length - 2] === 'meta')) { handler = putMetaHandler; } else { const handlers = actionHandlers[context] ? actionHandlers[context][path] : null; if (_.keys(handlers).length > 0) { if (body.source) { handler = handlers[body.source]; } else if (_.keys(handlers).length === 1) { handler = _.values(handlers)[0]; } else { updateRequest(request.requestId, 'COMPLETED', { statusCode: 400, message: 'there are multiple sources for the given path, but no source was specified in the request' }) .then(resolve) .catch(reject); return; } } if (!handler && parts[0] === 'notifications') { handler = putNotificationHandler; } } if (handler) { function fixReply(reply) { if (reply.state === 'FAILURE') { reply.state = 'COMPLETED'; reply.statusCode = 502; } else if (reply.state === 'SUCCESS') { reply.state = 'COMPLETED'; reply.statusCode = 200; } } const actionResult = handler(context, path, body.value, (reply) => { debug('got result: %j', reply); fixReply(reply); updateRequest(request.requestId, reply.state, reply) .then(() => undefined) .catch((err) => { console.error(err); }); }); Promise.resolve(actionResult) .then((result) => { debug('got result: %j', result); fixReply(result); updateRequest(request.requestId, result.state, result) .then((reply) => { if (reply.state === 'PENDING') { // backwards compatibility reply.action = { href: reply.href }; } resolve(reply); }) .catch(reject); }) .catch((err) => { updateRequest(request.requestId, 'COMPLETED', { statusCode: 500, message: err.message }) .then(resolve) .catch(reject); }); } else if (app.interfaces.ws && app.interfaces.ws.canHandlePut(path, body.source)) { app.interfaces.ws .handlePut(request.requestId, context, path, body.source, body.value) .then(resolve) .catch(reject); } else { updateRequest(request.requestId, 'COMPLETED', { statusCode: 405, message: `PUT not supported for ${path}` }) .then(resolve) .catch(reject); } }) .catch(reject); }); } function registerActionHandler(context, path, source, callback) { debug(`registered action handler for ${context} ${path} ${source}`); if (_.isUndefined(actionHandlers[context])) { actionHandlers[context] = {}; } if (_.isUndefined(actionHandlers[context][path])) { actionHandlers[context][path] = {}; } actionHandlers[context][path][source] = callback; return () => { deRegisterActionHandler(context, path, source, callback); }; } function deRegisterActionHandler(context, path, source, callback) { if (actionHandlers[context] && actionHandlers[context][path][source] === callback) { delete actionHandlers[context][path][source]; debug(`de-registered action handler for ${context} ${path} ${source}`); } } function putNotification(app, context, path, value) { const parts = path.split('.'); const notifPath = parts.slice(0, parts.length - 1).join('.'); const key = parts[parts.length - 1]; const existing = _.get(app.signalk.self, notifPath); if (_.isUndefined(existing) || !existing.value) { return { state: 'COMPLETED', statusCode: 404 }; } if (key !== 'method' && key !== 'state') { return { state: 'COMPLETED', statusCode: 405 }; } existing.value[key] = value; existing.timestamp = new Date().toISOString(); const delta = { updates: [ { $source: existing.$source, values: [ { path: notifPath, value: existing.value } ] } ] }; app.handleMessage('server', delta); return { state: 'COMPLETED', statusCode: 200 }; }