UNPKG

@signalk/streams

Version:

Utilities for handling streams of Signal K data

263 lines (262 loc) 10.6 kB
"use strict"; /* * Copyright 2016 Teppo Kurki <teppo.kurki@iki.fi> * * 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 }); const stream_1 = require("stream"); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); const client_1 = require("@signalk/client"); const signalk_schema_1 = require("@signalk/signalk-schema"); class MdnsWs extends stream_1.Transform { options; selfHost; selfPort; remoteServers = {}; debug; dataDebug; subscriptions = []; handleContext; signalkClient; isDestroying = false; fetchedMetaPaths = new Set(); constructor(options) { super({ objectMode: true }); this.options = options; this.selfHost = options.app.config.getExternalHostname() + '.'; this.selfPort = options.app.config.getExternalPort(); this.remoteServers[this.selfHost + ':' + this.selfPort] = {}; const deltaStreamBehaviour = options.subscription ? 'none' : 'all'; const createDebug = options.createDebug ?? require('debug'); this.debug = createDebug('signalk:streams:mdns-ws'); this.dataDebug = createDebug('signalk:streams:mdns-ws-data'); this.debug(`deltaStreamBehaviour:${deltaStreamBehaviour}`); this.handleContext = () => { }; if (options.selfHandling === 'manualSelf') { if (options.remoteSelf) { this.debug(`Using manual remote self ${options.remoteSelf}`); this.handleContext = (delta) => { if (delta.context === options.remoteSelf) { delete delta.context; } }; } else { console.error('Manual self handling speficied but no remoteSelf configured'); } } if (options.ignoreServers) { options.ignoreServers.forEach((s) => { this.remoteServers[s] = {}; }); } if (options.subscription) { try { const parsed = JSON.parse(options.subscription); this.subscriptions = Array.isArray(parsed) ? parsed : [parsed]; } catch (ex) { const error = ex; options.app.setProviderError(options.providerId, `unable to parse subscription json: ${options.subscription}: ${error.message}`); console.error(`unable to parse subscription json: ${options.subscription}: ${error.message}`); return; } } if (options.host && options.port) { this.signalkClient = new client_1.Client({ hostname: options.host, port: options.port, useTLS: options.type === 'wss', reconnect: true, notifications: false, autoConnect: false, deltaStreamBehaviour, rejectUnauthorized: !(options.selfsignedcert === true), wsKeepaliveInterval: 10 }); this.connectClient(this.signalkClient); } else { this.options.app.setProviderError(this.options.providerId, 'This connection is deprecated and must be deleted'); } } verifyRemoteToken() { const protocol = this.options.type === 'wss' ? https_1.default : http_1.default; return new Promise((resolve, reject) => { const reqOptions = { hostname: this.options.host, port: this.options.port, path: '/signalk/v1/api/self', method: 'GET', headers: { Authorization: `JWT ${this.options.token}` }, rejectUnauthorized: !(this.options.selfsignedcert === true) }; const req = protocol.request(reqOptions, (response) => { response.resume(); resolve(response.statusCode === 200); }); req.on('error', (err) => reject(err)); req.setTimeout(10000, () => { req.destroy(new Error('Token verification timed out')); }); req.end(); }); } setProviderStatus(message, isError) { if (!isError) { this.options.app.setProviderStatus(this.options.providerId, message); } else { this.options.app.setProviderError(this.options.providerId, message); } } connectClient(client) { client.on('connect', () => { this.fetchedMetaPaths.clear(); this.options.app.setProviderStatus(this.options.providerId, `ws connection connected to ${client.options.hostname}:${client.options.port}`); if (this.options.token) { const conn = client.connection; if (conn) { conn.send(JSON.stringify({ token: this.options.token })).catch(() => { // ignore send errors; error event handler will report }); conn.setAuthenticated(this.options.token, 'JWT'); this.debug('Sent authentication token to remote server'); this.verifyRemoteToken() .then((isValid) => { if (!isValid) { this.setProviderStatus(`Authentication failed for ${client.options.hostname}:${client.options.port} — token may be invalid or revoked`, true); conn.disconnect(); } }) .catch((err) => { if (this.debug.enabled) { this.debug('Token verification error: ' + err.message); } }); } } if (this.options.selfHandling !== 'manualSelf' && this.options.selfHandling !== 'noSelf') { client .API() .then((api) => api.get('/self')) .then((selfFromServer) => { this.debug(`Mapping context ${selfFromServer} to self (empty context)`); this.handleContext = (delta) => { if (delta.context === selfFromServer) { delete delta.context; } }; }) .catch((err) => { console.error('Error retrieving self from remote server'); console.error(err); }); } this.remoteServers[client.options.hostname + ':' + client.options.port] = client; this.subscriptions.forEach((sub, idx) => { if (this.debug.enabled) { this.debug('sending subscription %j', sub); } client.subscribe(sub, String(idx)); }); }); client.on('disconnect', () => { if (this.isDestroying) { return; } this.setProviderStatus(`Disconnected from ${client.options.hostname}:${client.options.port}`, true); }); client.on('error', (err) => { if (this.isDestroying) { return; } this.setProviderStatus(`Connection error: ${err.message}`, true); }); client.on('delta', (data) => { if (data && data.updates) { this.handleContext(data); if (this.dataDebug.enabled) { this.dataDebug(JSON.stringify(data)); } data.updates.forEach((update) => { update['$source'] = `${this.options.providerId}.${client.options.hostname}:${client.options.port}.${update['$source'] ?? '-'}`; }); } this.push(data); if (data?.updates) { for (const update of data.updates) { const values = update.values; if (values) { for (const pv of values) { if (!this.fetchedMetaPaths.has(pv.path)) { this.fetchedMetaPaths.add(pv.path); this.fetchMetaIfNeeded(client, data.context, pv.path); } } } } } }); client.connect().catch((err) => { if (this.debug.enabled) { this.debug('connect() promise rejected: %s', err?.message ?? err); } }); } fetchMetaIfNeeded(client, context, path) { if ((0, signalk_schema_1.getMetadata)('vessels.self.' + path)) { return; } client .API() .then((api) => api.getMeta(`/vessels/self/${path.replace(/\./g, '/')}`)) .then((meta) => { if (meta) { this.debug(`fetched meta for ${path} from remote`); this.push({ context, updates: [{ meta: [{ path, value: meta }] }] }); } }) .catch((err) => { this.debug(`failed to fetch meta for ${path}: ${err.message}`); }); } _transform(chunk, encoding, done) { done(); } _destroy(error, callback) { this.isDestroying = true; try { this.signalkClient?.disconnect(); } catch (err) { this.debug('error disconnecting client: %s', err.message); } this.signalkClient = undefined; callback(error); } } exports.default = MdnsWs; //# sourceMappingURL=mdns-ws.js.map