UNPKG

signalk-server

Version:

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

868 lines (867 loc) 37.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /* * 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 cookie_1 = __importDefault(require("cookie")); const primus_1 = __importDefault(require("primus")); const ws_1 = __importDefault(require("ws")); const path_metadata_1 = require("@signalk/path-metadata"); const security_1 = require("../security"); const login_rate_limiter_1 = require("../login-rate-limiter"); const server_api_1 = require("@signalk/server-api"); const requestResponse_1 = require("../requestResponse"); const put_1 = require("../put"); const debug_1 = require("../debug"); const jsonwebtoken_1 = require("jsonwebtoken"); const events_1 = require("../events"); const BackpressureManager_1 = require("../BackpressureManager"); const ports_1 = require("../ports"); const unitpreferences_1 = require("../unitpreferences"); const server_api_2 = require("@signalk/server-api"); const debug = (0, debug_1.createDebug)('signalk-server:interfaces:ws'); const debugConnection = (0, debug_1.createDebug)('signalk-server:interfaces:ws:connections'); function incrementIpCount(ipConnectionCounts, ip) { ipConnectionCounts.set(ip, (ipConnectionCounts.get(ip) ?? 0) + 1); } function decrementIpCount(ipConnectionCounts, ip) { const count = ipConnectionCounts.get(ip); if (count !== undefined && count > 1) { ipConnectionCounts.set(ip, count - 1); } else { ipConnectionCounts.delete(ip); } } function isWsRequestReply(msg) { if (!msg || typeof msg !== 'object') { return false; } const candidate = msg; const state = candidate.state; return (typeof candidate.requestId === 'string' && (state === 'PENDING' || state === 'COMPLETED' || state === null)); } function normalizeStatusCode(statusCode) { return typeof statusCode === 'number' && Number.isFinite(statusCode) ? statusCode : null; } function normalizeMessage(message) { return typeof message === 'string' ? message : null; } function normalizePercentComplete(percentComplete) { return typeof percentComplete === 'number' && Number.isFinite(percentComplete) && percentComplete >= 0 && percentComplete <= 100 ? percentComplete : null; } const DEFAULT_MAX_WS_CONNECTIONS_PER_IP = 30; function wsInterface(app) { const maxWsConnectionsPerIp = parseInt(process.env.MAX_WS_CONNECTIONS_PER_IP ?? String(DEFAULT_MAX_WS_CONNECTIONS_PER_IP), 10); const backpressureThresholds = (0, BackpressureManager_1.parseBackpressureThresholds)({ maxSendBufferSize: app.config.maxSendBufferSize, maxSendBufferCheckTime: app.config.maxSendBufferCheckTime }); debug('Backpressure thresholds: enter=%d, exit=%d, max=%d, maxTime=%d', backpressureThresholds.enterThreshold, backpressureThresholds.exitThreshold, backpressureThresholds.maxBufferSize, backpressureThresholds.maxBufferCheckTime); let primuses = []; const pathSources = {}; const ipConnectionCounts = new Map(); const api = { mdns: { name: app.config.settings.ssl || app.config.isExternalSsl() ? '_signalk-wss' : '_signalk-ws', type: 'tcp', port: (0, ports_1.getExternalPort)(app) }, numClients: function () { let count = 0; primuses.forEach((primus) => primus.forEach(() => { count++; return true; })); return count; }, canHandlePut: function (path, source) { const sources = pathSources[path]; return sources !== undefined && (!source || sources[source] !== undefined); }, handlePut: function (requestId, context, path, source, value) { return new Promise((resolve, reject) => { const sources = pathSources[path]; if (sources) { let spark; if (source) { spark = sources[source]; } else if (Object.keys(sources).length === 1) { spark = Object.values(sources)[0]; } else { (0, requestResponse_1.updateRequest)(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 (!spark) { reject(new Error('no spark found')); return; } const listener = (msg) => { let parsedMsg = msg; if (typeof parsedMsg === 'string' || Buffer.isBuffer(parsedMsg)) { try { parsedMsg = JSON.parse(String(parsedMsg)); } catch (_err) { return; } } if (isWsRequestReply(parsedMsg) && parsedMsg.requestId === requestId) { const updateOptions = { statusCode: normalizeStatusCode(parsedMsg.statusCode), data: parsedMsg.data ?? null, message: normalizeMessage(parsedMsg.message), percentComplete: normalizePercentComplete(parsedMsg.percentComplete) }; (0, requestResponse_1.updateRequest)(requestId, parsedMsg.state, updateOptions) .then((reply) => { if (reply.state !== 'PENDING') { spark.removeListener('data', listener); } }) .catch(() => { console.error(`could not update requestId ${requestId}`); }); } }; spark.on('data', listener); setTimeout(() => { const request = (0, requestResponse_1.findRequest)((r) => r.requestId === requestId); if (request && request.state === 'PENDING') { spark.removeListener('data', listener); (0, requestResponse_1.updateRequest)(requestId, 'COMPLETED', { statusCode: 504 }); } }, 60 * 1000); spark.write({ requestId: requestId, context: context, put: [{ path: path, value: value }] }); (0, requestResponse_1.updateRequest)(requestId, 'PENDING', { statusCode: 202 }) .then(resolve) .catch(reject); } else { reject(new Error('no source found')); } }); }, start: function () { debug('Starting Primus/WS interface'); const wsPingInterval = app.config.settings.wsPingInterval ?? 30000; let baseOptions = { transformer: 'websockets', pingInterval: false }; if (app.config.settings.wsCompression) { baseOptions = { ...baseOptions, compression: true, transport: { perMessageDeflate: { threshold: 0 } } }; } const allWsOptions = [ { ...baseOptions, pathname: '/signalk/v1/stream', isPlayback: false }, { ...baseOptions, pathname: '/signalk/v1/playback', isPlayback: true } ]; primuses = allWsOptions.map((primusOptions) => { const primus = new primus_1.default(app.server, primusOptions); if (wsPingInterval) { const interval = setInterval(() => { primus.forEach((primusSpark) => { const spark = primusSpark; if (spark.wsAlive === false) { debug('heartbeat timeout for spark %s, closing', spark.id); return spark.end(undefined, { reconnect: true }); } spark.wsAlive = false; if (spark.socket && spark.socket.readyState === ws_1.default.OPEN) { spark.socket.ping(); } }); }, wsPingInterval); primus.once('close', () => clearInterval(interval)); } primus.authorize(createPrimusAuthorize(app.securityStrategy.canAuthorizeWS() ? app.securityStrategy.authorizeWS : undefined, ipConnectionCounts, maxWsConnectionsPerIp, app.config.settings.trustProxy)); primus.on('connection', function (primusSpark) { const spark = primusSpark; const sparkIp = spark.request._resolvedIp; incrementIpCount(ipConnectionCounts, sparkIp); let principalId; if (spark.request.skPrincipal) { principalId = spark.request.skPrincipal.identifier; } debugConnection(`${spark.id} connected ${JSON.stringify(spark.query)} ${spark.request._resolvedIp}:${principalId} (ip connections: ${ipConnectionCounts.get(spark.request._resolvedIp) ?? '?'})`); spark.sendMetaDeltas = spark.query.sendMeta === 'all'; spark.sourcePolicy = spark.query.sourcePolicy || 'preferred'; spark.sentMetaData = {}; spark.backpressureManager = new BackpressureManager_1.BackpressureManager({ get id() { return spark.id; }, getBufferLength() { return spark.request.socket.writableLength; }, write(delta) { spark.write(delta); }, destroy() { spark.end({ errorMessage: 'Server outgoing buffer overflow, terminating connection' }); } }, { ...backpressureThresholds, beforeWrite: (delta) => sendMetaData(app, spark, delta) }); spark.request.socket.on('drain', () => { spark.backpressureManager.onDrain(); }); if (wsPingInterval) { spark.wsAlive = true; spark.socket.on('pong', () => { spark.wsAlive = true; }); } const selfOnly = isSelfSubscription(spark.query); const subscribeNone = spark.query.subscribe === 'none'; const canVerify = app.securityStrategy.canAuthorizeWS(); const onChange = (delta) => { if (canVerify) { try { app.securityStrategy.verifyWS(spark.request); } catch (error) { handleVerifyWSError(spark, error); return; } } if (subscribeNone) return; if (selfOnly && delta.context && delta.context !== app.selfContext) { return; } const filtered = app.securityStrategy.filterReadDelta(spark.request.skPrincipal, delta); if (filtered === null) return; spark.backpressureManager.send(filtered); }; const unsubscribes = []; if (primusOptions.isPlayback) { spark.on('data', () => { console.error('Playback does not support ws upstream messages'); spark.end('Playback does not support ws upstream messages'); }); } else { spark.on('data', function (msg) { let parsedMsg; try { parsedMsg = JSON.parse(String(msg)); } catch (e) { debug('Failed to parse message: ' + e.message); return; } // Guard to avoid JSON.stringify on every inbound message // when the debug namespace is disabled — the argument to // debug() would otherwise be eagerly evaluated for each // message. Pattern mirrors tcp.ts:169/181. debug.enabled && debug('<' + JSON.stringify(parsedMsg)); try { if (parsedMsg.updates) { processUpdates(app, pathSources, spark, parsedMsg); } if (parsedMsg.subscribe) { processSubscribe(app, unsubscribes, spark, parsedMsg); } if (parsedMsg.unsubscribe) { processUnsubscribe(app, unsubscribes, parsedMsg, onChange, spark); } if (parsedMsg.accessRequest) { processAccessRequest(app, spark, parsedMsg); } if (parsedMsg.login && app.securityStrategy.supportsLogin()) { processLoginRequest(app, spark, parsedMsg); } if (parsedMsg.put) { processPutRequest(app, spark, parsedMsg); } if (parsedMsg.delete) { processDeleteRequest(app, spark, parsedMsg); } if (parsedMsg.requestId && parsedMsg.query) { processRequestQuery(spark, parsedMsg); } } catch (e) { console.error(e); } }); } spark.on('end', function () { debugConnection(`${spark.id} end ${JSON.stringify(spark.query)} ${spark.request._resolvedIp}:${principalId}`); unsubscribes.forEach((unsubscribe) => unsubscribe()); Object.keys(pathSources).forEach((path) => { Object.keys(pathSources[path]).forEach((source) => { if (pathSources[path][source] === spark) { debug('removing source for %s', path); delete pathSources[path][source]; } }); }); }); spark.onDisconnects = [ () => spark.backpressureManager?.clear(), () => decrementIpCount(ipConnectionCounts, sparkIp) ]; if (primusOptions.isPlayback) { if (!spark.query.startTime) { spark.end('startTime is a required query parameter for playback connections'); } else { handlePlaybackConnection(app, spark, onChange); } } else { handleRealtimeConnection(app, spark, onChange); } }); primus.on('disconnection', function (primusSpark) { const spark = primusSpark; spark.onDisconnects.forEach((f) => f()); debug(spark.id + ' disconnected'); }); return primus; }); }, stop: function () { debug('Destroying primuses...'); primuses.forEach((primus) => primus.destroy({ close: false, timeout: 500, reconnect: false }, () => { /* noop */ })); } }; function processRequestQuery(spark, msg) { (0, requestResponse_1.queryRequest)(msg.requestId) .then((reply) => { spark.write(reply); }) .catch(() => { spark.write({ requestId: msg.requestId, statusCode: 404 }); }); } function processPutRequest(app, spark, msg) { (0, put_1.putPath)(app, msg.context || null, msg.put.path, msg.put, spark.request, msg.requestId || null, (reply) => { debug('sending put update %j', reply); spark.write(reply); }).catch((err) => { console.error(err); spark.write({ requestId: msg.requestId, state: 'COMPLETED', statusCode: 502, message: err.message }); }); } function processDeleteRequest(app, spark, msg) { (0, put_1.deletePath)(app, msg.context || null, msg.delete.path, spark.request, msg.requestId || null, (reply) => { debug('sending delete update %j', reply); spark.write(reply); }).catch((err) => { console.error(err); spark.write({ requestId: msg.requestId, state: 'COMPLETED', statusCode: 502, message: err.message }); }); } function processAccessRequest(app, spark, msg) { if (spark.skPendingAccessRequest) { spark.write({ requestId: msg.requestId, state: 'COMPLETED', statusCode: 400, message: 'A request has already been submitted' }); } else { const clientIp = spark.request._resolvedIp; (0, security_1.requestAccess)(app, msg, clientIp, (res) => { if (res.state === 'COMPLETED') { spark.skPendingAccessRequest = false; if (res.accessRequest && res.accessRequest.token) { spark.request.token = res.accessRequest.token; app.securityStrategy.authorizeWS(spark.request); spark.request.source = 'ws.' + spark.request.skPrincipal.identifier.replace(/\./g, '_'); } } spark.write(res); }) .then((res) => { if (res.state === 'PENDING') { spark.skPendingAccessRequest = true; } }) .catch((err) => { console.log(err.stack); spark.write({ requestId: msg.requestId, state: 'COMPLETED', statusCode: 502, message: err.message }); }); } } function processLoginRequest(app, spark, msg) { const rateLimiter = app.securityStrategy.loginRateLimiter; if (rateLimiter) { const { allowed } = rateLimiter.check(spark.request._resolvedIp); if (!allowed) { spark.write({ requestId: msg.requestId, state: 'COMPLETED', statusCode: 429, message: login_rate_limiter_1.LOGIN_RATE_LIMIT_MESSAGE }); return; } } app.securityStrategy .login(msg.login.username, msg.login.password) .then((reply) => { if (reply.token) { spark.request.token = reply.token; app.securityStrategy.authorizeWS(spark.request); } spark.write({ requestId: msg.requestId, state: 'COMPLETED', statusCode: reply.statusCode, login: { timeToLive: reply.timeToLive, token: reply.token } }); }) .catch((err) => { console.error(err); spark.write({ requestId: msg.requestId, state: 'COMPLETED', statusCode: 502, message: err.message }); }); } return api; } function createPrimusAuthorize(authorizeWS, ipConnectionCounts, maxConnectionsPerIp, trustProxy) { return function (primusReq, authorized) { const req = primusReq; const forwardedFor = req.headers['x-forwarded-for']; const firstForwardedIp = trustProxy && trustProxy !== 'false' && typeof forwardedFor === 'string' ? forwardedFor.split(',')[0].trim() : undefined; req._resolvedIp = firstForwardedIp || req.connection.remoteAddress; const ip = req._resolvedIp; const isWebSocketUpgrade = String(req.headers.upgrade || '').toLowerCase() === 'websocket'; if (isWebSocketUpgrade && (ipConnectionCounts.get(ip) ?? 0) >= maxConnectionsPerIp) { debug('IP %s exceeded max connections (%d)', ip, maxConnectionsPerIp); const err = Object.assign(new Error(JSON.stringify({ error: 'Too many concurrent websocket connections from same IP address' })), { statusCode: 429 }); authorized(err); return; } if (!authorizeWS) { authorized(); return; } try { const cookieHeader = req.headers.cookie; if (typeof cookieHeader === 'string') { req.cookies = cookie_1.default.parse(cookieHeader); } authorizeWS(req); authorized(); const identifier = req.skPrincipal?.identifier; if (identifier) { debug(`authorized username: ${identifier}`); req.source = 'ws.' + identifier.replace(/\./g, '_'); } } catch (error) { if (error instanceof security_1.InvalidTokenError || error instanceof jsonwebtoken_1.JsonWebTokenError || error instanceof jsonwebtoken_1.TokenExpiredError) { authorized(error); } else { authorized(); } } }; } function processUpdates(app, pathSources, spark, msg) { if (!app.securityStrategy.shouldAllowWrite(spark.request, msg)) { debug('security disallowed update'); app.setProviderError('ws', spark.request.connection.remoteAddress + ' needs authentication'); return; } app.handleMessage(spark.request.source || 'ws', msg); msg.updates?.forEach((update) => { if ((0, server_api_2.hasValues)(update)) { let source = update.$source; if (!source && update.source) { source = (0, server_api_1.getSourceId)(update.source); } if (source) { update.values.forEach((valuePath) => { if (!pathSources[valuePath.path]) { pathSources[valuePath.path] = {}; } if (!pathSources[valuePath.path][source] || pathSources[valuePath.path][source] !== spark) { if (pathSources[valuePath.path][source]) { console.log(`WARNING: got a new ws client for path ${valuePath.path} source ${source}`); } debug('registered spark for source %s path %s = %s', source, valuePath.path, spark.id); pathSources[valuePath.path][source] = spark; } }); } } }); } let canonical_meta_contextpath_values = {}; const getContextPathMetaKey = (context, path) => { const contextPaths = canonical_meta_contextpath_values[context] || (canonical_meta_contextpath_values[context] = {}); const result = contextPaths[path] || (contextPaths[path] = `${context}.${path}`); return result; }; setInterval(() => { canonical_meta_contextpath_values = {}; }, 30 * 60 * 1000); function handleValuesMeta(kp) { const fullContextPathKey = getContextPathMetaKey(this.context, kp.path); if (kp.path && !this.spark.sentMetaData[fullContextPathKey]) { const split = kp.path.split('.'); for (let i = split.length; i > 1; i--) { const path = split.slice(0, i).join('.'); const partialContextPathKey = getContextPathMetaKey(this.context, path); if (this.spark.sentMetaData[partialContextPathKey]) { break; } else { this.spark.sentMetaData[partialContextPathKey] = true; const meta = (0, path_metadata_1.getMetadata)(partialContextPathKey); if (meta) { // Clone and enhance metadata with displayUnits formulas const metaClone = structuredClone(meta); let storedDisplayUnits = metaClone.displayUnits; const username = this.spark.request.skPrincipal?.identifier; if (!storedDisplayUnits?.category && path) { const defaultCategory = (0, unitpreferences_1.getDefaultCategory)(path, metaClone.units, username); if (defaultCategory) { storedDisplayUnits = { category: defaultCategory }; } } if (storedDisplayUnits?.category) { const enhanced = (0, unitpreferences_1.resolveDisplayUnits)(storedDisplayUnits, metaClone.units, username); if (enhanced) { metaClone.displayUnits = enhanced; } } this.spark.write({ context: this.context, updates: [ { timestamp: this.timestamp, meta: [ { path: path, value: metaClone } ] } ] }); } } } } } function handleUpdatesMeta(update) { if ((0, server_api_2.hasValues)(update)) { this.timestamp = update.timestamp; update.values.forEach(handleValuesMeta, this); } } function sendMetaData(app, spark, delta) { if (spark.sendMetaDeltas && delta.updates) { const thisContext = { context: delta.context || '', spark }; delta.updates.forEach(handleUpdatesMeta, thisContext); } } function processSubscribe(app, unsubscribes, spark, msg) { if (Array.isArray(msg.subscribe) && msg.subscribe.length > 0 && msg.subscribe[0].path === 'log') { if (!spark.logUnsubscribe) { spark.logUnsubscribe = startServerLog(app, spark); } } else { // Per-message sourcePolicy is honoured: subscriptionmanager.subscribe // attaches its own listener on the chosen bus (filtered or unfiltered) // for each subscribe call, independent of the connection-wide delta // listener attached in handleRealtimeConnection. Falling back to the // spark-level policy keeps the URL-query default working for clients // that never send a per-message override. // // excludeSelf is only meaningful for plugin subscriptions (where // the server can resolve it to the plugin's id); WebSocket clients // have no identity to resolve against, so it's logged and ignored. // Explicit excludeSources is forwarded as-is. if (msg.excludeSelf) { debug('WebSocket subscribe ignores excludeSelf: there is no plugin identity to resolve; use excludeSources instead'); } app.subscriptionmanager.subscribe(msg, unsubscribes, spark.write.bind(spark), (message) => { const filtered = app.securityStrategy.filterReadDelta(spark.request.skPrincipal, message); if (!filtered) return; spark.backpressureManager.send(filtered); }, spark.request.skPrincipal, msg.sourcePolicy ?? spark.sourcePolicy, Array.isArray(msg.excludeSources) && msg.excludeSources.length > 0 ? msg.excludeSources : undefined); } } function processUnsubscribe(app, unsubscribes, msg, onChange, spark) { try { if (Array.isArray(msg.unsubscribe) && msg.unsubscribe.length > 0 && msg.unsubscribe[0].path === 'log') { if (spark.logUnsubscribe) { spark.logUnsubscribe(); spark.logUnsubscribe = undefined; } } else { app.subscriptionmanager.unsubscribe(msg, unsubscribes); const deltaEvent = spark.sourcePolicy === 'all' ? 'unfilteredDelta' : 'delta'; app.signalk.removeListener(deltaEvent, onChange); spark.sentMetaData = {}; } } catch (e) { console.log(e); spark.write(e.message); spark.end(); } } const isSelfSubscription = (query) => !query.subscribe || query.subscribe === 'self'; function handleVerifyWSError(spark, error) { if (!spark.skPendingAccessRequest) { spark.end('{message: "Connection disconnected by security constraint"}', { reconnect: true }); } const identifier = spark.request.skPrincipal?.identifier || spark.request.connection.remoteAddress || 'unknown'; console.error(`WebSocket security error for ${identifier}: ${error.message}`); } function wrapWithVerifyWS(securityStrategy, spark, theFunction) { if (!securityStrategy.canAuthorizeWS()) { return theFunction; } return (msg) => { try { securityStrategy.verifyWS(spark.request); theFunction(msg); } catch (error) { handleVerifyWSError(spark, error); return; } }; } function sendHello(app, helloProps, spark) { spark.write({ ...app.getHello(), ...helloProps }); } function handlePlaybackConnection(app, spark, onChange) { if (app.historyProvider === undefined) { spark.end('No history provider'); return; } const startTimeStr = spark.query.startTime; if (!startTimeStr) { spark.end('startTime is required'); return; } const options = { startTime: new Date(startTimeStr), playbackRate: spark.query.playbackRate || 1 }; sendHello(app, options, spark); options.subscribe = spark.query.subscribe; app.historyProvider.hasAnyData(options, (hasResults) => { if (hasResults) { spark.onDisconnects.push(app.historyProvider.streamHistory(spark, options, onChange)); spark.isHistory = true; } else { spark.end('No data found'); } }); } function handleRealtimeConnection(app, spark, onChange) { sendHello(app, {}, spark); const deltaEvent = spark.sourcePolicy === 'all' ? 'unfilteredDelta' : 'delta'; app.signalk.on(deltaEvent, onChange); spark.onDisconnects.push(() => { app.signalk.removeListener(deltaEvent, onChange); }); if (spark.sendMetaDeltas) { const onUnitPrefsChanged = (event) => { const username = spark.request.skPrincipal?.identifier; const ev = event; if (ev?.username && ev.username !== username) return; const allPaths = app.streambundle.getAvailablePaths(); const meta = allPaths.reduce((acc, path) => { const fullPath = 'vessels.self.' + path; const pathMeta = (0, path_metadata_1.getMetadata)(fullPath) || {}; const storedDU = pathMeta.displayUnits; const category = storedDU?.category || (0, unitpreferences_1.getDefaultCategory)(path, pathMeta.units, username); if (category) { const displayUnits = (0, unitpreferences_1.resolveDisplayUnits)({ ...storedDU, category }, pathMeta.units, username); if (displayUnits) { acc.push({ path, value: { ...pathMeta, displayUnits } }); } } return acc; }, []); if (meta.length > 0) { const timestamp = new Date().toISOString(); spark.write({ context: 'vessels.' + app.selfId, updates: [{ timestamp, meta }] }); } }; app.on('unitpreferencesChanged', onUnitPrefsChanged); spark.onDisconnects.push(() => { app.removeListener('unitpreferencesChanged', onUnitPrefsChanged); }); } if (spark.request.query?.sendCachedValues !== 'false') { sendLatestDeltas(app, app.deltaCache, app.selfContext, spark); } if (spark.query.serverevents === 'all') { spark.hasServerEvents = true; (0, events_1.startServerEvents)(app, spark, wrapWithVerifyWS(app.securityStrategy, spark, spark.write.bind(spark))); } if (spark.query.events) { (0, events_1.startEvents)(app, spark, wrapWithVerifyWS(app.securityStrategy, spark, spark.write.bind(spark)), spark.query.events); } } function sendLatestDeltas(app, deltaCache, selfContext, spark) { let deltaFilter = () => false; if (!spark.query.subscribe || spark.query.subscribe === 'self') { deltaFilter = (delta) => delta.context === selfContext; } else if (spark.query.subscribe === 'all') { deltaFilter = () => true; } deltaCache .getCachedDeltas(deltaFilter, spark.request.skPrincipal, undefined, spark.sourcePolicy) .forEach((delta) => { sendMetaData(app, spark, delta); spark.write(delta); }); } function startServerLog(app, spark) { const onServerLogEvent = wrapWithVerifyWS(app.securityStrategy, spark, spark.write.bind(spark)); app.on('serverlog', onServerLogEvent); spark.onDisconnects.push(() => { app.removeListener('serverlog', onServerLogEvent); }); app.logging.getLog().forEach((log) => { spark.write({ type: 'LOG', data: log }); }); return () => { app.removeListener('serverlog', onServerLogEvent); }; } module.exports = wsInterface; //# sourceMappingURL=ws.js.map