@source-repo/rpc 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1208 -0
  3. package/dist/Logging/ILogger.d.ts +7 -0
  4. package/dist/Logging/ILogger.d.ts.map +1 -0
  5. package/dist/Logging/ILogger.js +2 -0
  6. package/dist/Logging/ILogger.js.map +1 -0
  7. package/dist/NodeRpcServer.d.ts +55 -0
  8. package/dist/NodeRpcServer.d.ts.map +1 -0
  9. package/dist/NodeRpcServer.js +46 -0
  10. package/dist/NodeRpcServer.js.map +1 -0
  11. package/dist/RPC/Auth.d.ts +98 -0
  12. package/dist/RPC/Auth.d.ts.map +1 -0
  13. package/dist/RPC/Auth.js +14 -0
  14. package/dist/RPC/Auth.js.map +1 -0
  15. package/dist/RPC/Codec.d.ts +24 -0
  16. package/dist/RPC/Codec.d.ts.map +1 -0
  17. package/dist/RPC/Codec.js +21 -0
  18. package/dist/RPC/Codec.js.map +1 -0
  19. package/dist/RPC/Compatibility.d.ts +39 -0
  20. package/dist/RPC/Compatibility.d.ts.map +1 -0
  21. package/dist/RPC/Compatibility.js +195 -0
  22. package/dist/RPC/Compatibility.js.map +1 -0
  23. package/dist/RPC/Core.d.ts +188 -0
  24. package/dist/RPC/Core.d.ts.map +1 -0
  25. package/dist/RPC/Core.js +354 -0
  26. package/dist/RPC/Core.js.map +1 -0
  27. package/dist/RPC/Expose.d.ts +90 -0
  28. package/dist/RPC/Expose.d.ts.map +1 -0
  29. package/dist/RPC/Expose.js +110 -0
  30. package/dist/RPC/Expose.js.map +1 -0
  31. package/dist/RPC/Idempotency.d.ts +90 -0
  32. package/dist/RPC/Idempotency.d.ts.map +1 -0
  33. package/dist/RPC/Idempotency.js +46 -0
  34. package/dist/RPC/Idempotency.js.map +1 -0
  35. package/dist/RPC/Introspection.d.ts +92 -0
  36. package/dist/RPC/Introspection.d.ts.map +1 -0
  37. package/dist/RPC/Introspection.js +188 -0
  38. package/dist/RPC/Introspection.js.map +1 -0
  39. package/dist/RPC/Introspection.types.json +1093 -0
  40. package/dist/RPC/Messages.d.ts +108 -0
  41. package/dist/RPC/Messages.d.ts.map +1 -0
  42. package/dist/RPC/Messages.js +30 -0
  43. package/dist/RPC/Messages.js.map +1 -0
  44. package/dist/RPC/Rpc.d.ts +51 -0
  45. package/dist/RPC/Rpc.d.ts.map +1 -0
  46. package/dist/RPC/Rpc.js +54 -0
  47. package/dist/RPC/Rpc.js.map +1 -0
  48. package/dist/RPC/RpcClientHandler.d.ts +129 -0
  49. package/dist/RPC/RpcClientHandler.d.ts.map +1 -0
  50. package/dist/RPC/RpcClientHandler.js +267 -0
  51. package/dist/RPC/RpcClientHandler.js.map +1 -0
  52. package/dist/RPC/RpcServerHandler.d.ts +226 -0
  53. package/dist/RPC/RpcServerHandler.d.ts.map +1 -0
  54. package/dist/RPC/RpcServerHandler.js +676 -0
  55. package/dist/RPC/RpcServerHandler.js.map +1 -0
  56. package/dist/RPC/Schema.d.ts +150 -0
  57. package/dist/RPC/Schema.d.ts.map +1 -0
  58. package/dist/RPC/Schema.js +198 -0
  59. package/dist/RPC/Schema.js.map +1 -0
  60. package/dist/RPC/Signing.d.ts +118 -0
  61. package/dist/RPC/Signing.d.ts.map +1 -0
  62. package/dist/RPC/Signing.js +171 -0
  63. package/dist/RPC/Signing.js.map +1 -0
  64. package/dist/RPC/Tokens.d.ts +49 -0
  65. package/dist/RPC/Tokens.d.ts.map +1 -0
  66. package/dist/RPC/Tokens.js +45 -0
  67. package/dist/RPC/Tokens.js.map +1 -0
  68. package/dist/RpcClient.d.ts +91 -0
  69. package/dist/RpcClient.d.ts.map +1 -0
  70. package/dist/RpcClient.js +145 -0
  71. package/dist/RpcClient.js.map +1 -0
  72. package/dist/RpcServer.d.ts +191 -0
  73. package/dist/RpcServer.d.ts.map +1 -0
  74. package/dist/RpcServer.js +293 -0
  75. package/dist/RpcServer.js.map +1 -0
  76. package/dist/Transports/Mqtt5Frame.d.ts +107 -0
  77. package/dist/Transports/Mqtt5Frame.d.ts.map +1 -0
  78. package/dist/Transports/Mqtt5Frame.js +162 -0
  79. package/dist/Transports/Mqtt5Frame.js.map +1 -0
  80. package/dist/Transports/MqttTransport.d.ts +279 -0
  81. package/dist/Transports/MqttTransport.d.ts.map +1 -0
  82. package/dist/Transports/MqttTransport.js +843 -0
  83. package/dist/Transports/MqttTransport.js.map +1 -0
  84. package/dist/Transports/Presence.d.ts +68 -0
  85. package/dist/Transports/Presence.d.ts.map +1 -0
  86. package/dist/Transports/Presence.js +21 -0
  87. package/dist/Transports/Presence.js.map +1 -0
  88. package/dist/Transports/SocketIoClientTransport.d.ts +89 -0
  89. package/dist/Transports/SocketIoClientTransport.d.ts.map +1 -0
  90. package/dist/Transports/SocketIoClientTransport.js +280 -0
  91. package/dist/Transports/SocketIoClientTransport.js.map +1 -0
  92. package/dist/Transports/SocketIoServerTransport.d.ts +135 -0
  93. package/dist/Transports/SocketIoServerTransport.d.ts.map +1 -0
  94. package/dist/Transports/SocketIoServerTransport.js +505 -0
  95. package/dist/Transports/SocketIoServerTransport.js.map +1 -0
  96. package/dist/Utilities/Converters.d.ts +22 -0
  97. package/dist/Utilities/Converters.d.ts.map +1 -0
  98. package/dist/Utilities/Converters.js +58 -0
  99. package/dist/Utilities/Converters.js.map +1 -0
  100. package/dist/Utilities/Filter.d.ts +7 -0
  101. package/dist/Utilities/Filter.d.ts.map +1 -0
  102. package/dist/Utilities/Filter.js +14 -0
  103. package/dist/Utilities/Filter.js.map +1 -0
  104. package/dist/Utilities/ReadableName.d.ts +38 -0
  105. package/dist/Utilities/ReadableName.d.ts.map +1 -0
  106. package/dist/Utilities/ReadableName.js +58 -0
  107. package/dist/Utilities/ReadableName.js.map +1 -0
  108. package/dist/Utilities/Switch.d.ts +21 -0
  109. package/dist/Utilities/Switch.d.ts.map +1 -0
  110. package/dist/Utilities/Switch.js +70 -0
  111. package/dist/Utilities/Switch.js.map +1 -0
  112. package/dist/Utilities/TryCatch.d.ts +11 -0
  113. package/dist/Utilities/TryCatch.d.ts.map +1 -0
  114. package/dist/Utilities/TryCatch.js +13 -0
  115. package/dist/Utilities/TryCatch.js.map +1 -0
  116. package/dist/index-web.d.ts +25 -0
  117. package/dist/index-web.d.ts.map +1 -0
  118. package/dist/index-web.js +28 -0
  119. package/dist/index-web.js.map +1 -0
  120. package/dist/index.d.ts +28 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +30 -0
  123. package/dist/index.js.map +1 -0
  124. package/package.json +77 -0
@@ -0,0 +1,135 @@
1
+ import * as SocketIo from 'socket.io';
2
+ import { Server as HttpServer } from 'http';
3
+ import { Server as HttpsServer, type ServerOptions as TlsServerOptions } from 'https';
4
+ import { GenericModule, IGenericModule, Message } from '../RPC/Core.js';
5
+ import { FrameCodec } from '../RPC/Codec.js';
6
+ import { RpcAuthenticator, RpcIdentity } from '../RPC/Auth.js';
7
+ import { RelayRule } from './Presence.js';
8
+ type Servers = HttpServer | HttpsServer | SocketIo.Server;
9
+ export declare class SocketIoServerTransport extends GenericModule<Message, unknown, Message, unknown> {
10
+ server?: Servers | undefined;
11
+ authenticate?: RpcAuthenticator | undefined;
12
+ closed: boolean;
13
+ /** Set when this transport can never come up - a port already in use, most often. */
14
+ startupError?: unknown;
15
+ /** Owned here rather than by a converter above, so the transport decides its own wire form. */
16
+ codec: FrameCodec;
17
+ io?: SocketIo.Server;
18
+ ourServer: boolean;
19
+ /**
20
+ * Peer name -> the socket it was last seen on, learned from the source field of inbound frames.
21
+ * Without it this transport can only broadcast, which puts every reply and every event on every
22
+ * connected socket. Peer names are expected to be unique; the most recent socket wins, so a
23
+ * reconnecting peer re-binds to its new socket on its first frame.
24
+ */
25
+ peerSockets: Map<string, SocketIo.Socket<SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, any>>;
26
+ /** Peer name -> the identity its connection authenticated as. Empty when no authenticator is set. */
27
+ peerIdentities: Map<string, RpcIdentity>;
28
+ /**
29
+ * Forward a frame addressed to another connected peer instead of handling it here. On by
30
+ * default: a peer that can only be reached by dialling out - a browser page hosting an
31
+ * RpcServer, most obviously - is reachable no other way. `false` keeps a server strictly
32
+ * point-to-point, and a predicate decides per connection, which is the useful case: an
33
+ * operator's page may deserve a route to a cell controller where a visitor's does not.
34
+ */
35
+ relay: RelayRule;
36
+ /**
37
+ * What each connection says it can reach, beyond itself. Kept per socket so a link going away
38
+ * takes exactly its own peers with it, and so a peer offered by two links can fall back to the
39
+ * other one instead of vanishing.
40
+ */
41
+ private readonly carriedBy;
42
+ constructor(name: string, server?: Servers | undefined, port?: number,
43
+ /**
44
+ * Certificate and key for a server opened here. Present means HTTPS.
45
+ *
46
+ * This used to be `https?: boolean`, and `true` called createHttpsServer() with nothing in
47
+ * it - a server that listens, completes no handshake, and refuses every client with an
48
+ * error about missing certificates. There is no useful HTTPS server without key material,
49
+ * so the material is what asks for one.
50
+ */
51
+ tls?: TlsServerOptions, sources?: IGenericModule[], socketIoOptions?: Partial<SocketIo.ServerOptions>, authenticate?: RpcAuthenticator | undefined);
52
+ private onSocketMessage;
53
+ /**
54
+ * Which module carries a peer, or undefined to handle the frame here. The registry is shared
55
+ * with this server's other transports, so a peer on the broker resolves to the MQTT transport
56
+ * and a socket.io peer can call it without either end knowing the other's transport.
57
+ */
58
+ private peerCarrying;
59
+ /** Asked only once a frame really is deliverable elsewhere, never about a peer that is absent. */
60
+ private mayRelay;
61
+ /**
62
+ * Routes a rule has already allowed, in both directions. Every call has a reply and most have
63
+ * events after it, so asking a rule about each frame separately would mean `source === 'hmi'`
64
+ * silently stranding the answer coming back - the reply's source is the far peer, not the hmi.
65
+ * A permitted call opens the pair, the way connection tracking does, until one of them leaves.
66
+ */
67
+ private readonly openRoutes;
68
+ private static pair;
69
+ private permitted;
70
+ private forgetRoutes;
71
+ /** Named once each, so a peer that reconnects in a loop does not bury the first report. */
72
+ private warnedAboutDisplacing;
73
+ private warnAboutDisplacement;
74
+ private warnedAboutRelay;
75
+ /**
76
+ * Said once, and only when this server actually forwards something. Without an authenticator
77
+ * the source of a frame is an unverified claim, so a relay passes on whatever it is told and
78
+ * the peer at the far end has no way to tell who really sent it. Warning at construction would
79
+ * fire for every ordinary server that never relays anything.
80
+ */
81
+ private warnAboutUnauthenticatedRelay;
82
+ /**
83
+ * Refuse to register a peer name this transport has not yet decided to trust.
84
+ *
85
+ * GenericModule registers a frame's source as it parses the header, which is right for MQTT -
86
+ * the broker is the authority there, and there is no connection anyone could have checked. Here
87
+ * there is one, and the header is a claim until it has been checked against it. Registering on
88
+ * parse let a peer put any name it liked into the registry this server shares with its other
89
+ * transports, just by sending one frame that was then rejected: enough to have the bus advertise
90
+ * a peer that does not exist, and to point lookups for a real peer's name at this transport,
91
+ * where nothing answers to it. The frame never went anywhere - peerSockets is what delivery uses
92
+ * and only learnPeer writes it - but the routing table had already been told.
93
+ *
94
+ * So registration happens where the trust decision is made, in learnPeer and forgetPeer, which
95
+ * call `super` to say they are past this point. Nothing changes for a transport with no
96
+ * authenticator: without one, a name was never evidence of anything to begin with.
97
+ */
98
+ setKnownSource(source: string): void;
99
+ /**
100
+ * Record a peer against the socket that reaches it and tell everyone else it is here.
101
+ *
102
+ * A peer announcing itself owns its name and takes the route over. One merely carried by a
103
+ * neighbour does not: the first link to offer it keeps it, and a second offering the same name
104
+ * is remembered only as a fallback. Letting carried announcements steal the route would make
105
+ * two neighbours advertising the same peer flip it back and forth, each flip re-announced
106
+ * onwards - chatter that never settles.
107
+ */
108
+ private learnPeer;
109
+ /** A peer is gone from here unless another link still offers it. */
110
+ private forgetPeer;
111
+ /**
112
+ * A peer saying who it is, which is the whole of discovery here. It answers with the peers
113
+ * already connected, standing in for the retained presence an MQTT subscriber is handed.
114
+ */
115
+ private onAnnouncement;
116
+ /**
117
+ * Everyone this server can put a frame in front of: its own connections, plus whatever its
118
+ * other transports have registered. On a server holding a socket.io listener and a broker
119
+ * connection, that is what lets a browser peer see and call a peer on the broker.
120
+ */
121
+ reachablePeers(): string[];
122
+ /** Tell the connected peers about one that arrived or left on a different transport. */
123
+ announcePeer(peer: string, state: 'online' | 'offline'): void;
124
+ /** Apply a link's latest claim about what lies behind it, adding and dropping as it changes. */
125
+ private updateCarried;
126
+ private broadcastPresence;
127
+ /** Send to a peer's socket, carrying a hop count the ordinary send path has no reason to know. */
128
+ forward(message: Message, source: string, target: string, hops: number): void;
129
+ receive(message: Message, source: string, target: string): Promise<void>;
130
+ close(): Promise<void>;
131
+ getIdentity(source: string): RpcIdentity | undefined;
132
+ isTransport(): boolean;
133
+ }
134
+ export {};
135
+ //# sourceMappingURL=SocketIoServerTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SocketIoServerTransport.d.ts","sourceRoot":"","sources":["../../src/Transports/SocketIoServerTransport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,505 @@
1
+ import * as SocketIo from 'socket.io';
2
+ import { createServer as createHttpServer } from 'http';
3
+ import { createServer as createHttpsServer } from 'https';
4
+ import { GenericModule, TransportEvent } from '../RPC/Core.js';
5
+ import { msgPackCodec } from '../RPC/Codec.js';
6
+ import { isUsablePeerName, MAX_CARRIED_PEERS, MAX_RELAY_HOPS, PRESENCE_EVENT } from './Presence.js';
7
+ export class SocketIoServerTransport extends GenericModule {
8
+ server;
9
+ authenticate;
10
+ closed = false;
11
+ /** Set when this transport can never come up - a port already in use, most often. */
12
+ startupError;
13
+ /** Owned here rather than by a converter above, so the transport decides its own wire form. */
14
+ codec = msgPackCodec;
15
+ io;
16
+ ourServer = false;
17
+ /**
18
+ * Peer name -> the socket it was last seen on, learned from the source field of inbound frames.
19
+ * Without it this transport can only broadcast, which puts every reply and every event on every
20
+ * connected socket. Peer names are expected to be unique; the most recent socket wins, so a
21
+ * reconnecting peer re-binds to its new socket on its first frame.
22
+ */
23
+ peerSockets = new Map();
24
+ /** Peer name -> the identity its connection authenticated as. Empty when no authenticator is set. */
25
+ peerIdentities = new Map();
26
+ /**
27
+ * Forward a frame addressed to another connected peer instead of handling it here. On by
28
+ * default: a peer that can only be reached by dialling out - a browser page hosting an
29
+ * RpcServer, most obviously - is reachable no other way. `false` keeps a server strictly
30
+ * point-to-point, and a predicate decides per connection, which is the useful case: an
31
+ * operator's page may deserve a route to a cell controller where a visitor's does not.
32
+ */
33
+ relay = true;
34
+ /**
35
+ * What each connection says it can reach, beyond itself. Kept per socket so a link going away
36
+ * takes exactly its own peers with it, and so a peer offered by two links can fall back to the
37
+ * other one instead of vanishing.
38
+ */
39
+ carriedBy = new Map();
40
+ constructor(name, server, port,
41
+ /**
42
+ * Certificate and key for a server opened here. Present means HTTPS.
43
+ *
44
+ * This used to be `https?: boolean`, and `true` called createHttpsServer() with nothing in
45
+ * it - a server that listens, completes no handshake, and refuses every client with an
46
+ * error about missing certificates. There is no useful HTTPS server without key material,
47
+ * so the material is what asks for one.
48
+ */
49
+ tls, sources, socketIoOptions = {}, authenticate) {
50
+ super(name, sources);
51
+ this.server = server;
52
+ this.authenticate = authenticate;
53
+ this.ourServer = server === undefined;
54
+ if (tls && !tls.cert && !tls.pfx && !tls.SNICallback)
55
+ throw new Error(`SocketIoServerTransport '${name}': tls needs a certificate - pass cert and key, or pfx, or an SNICallback that supplies them`);
56
+ if (!server)
57
+ this.server = tls ? createHttpsServer(tls) : createHttpServer();
58
+ if (this.server instanceof SocketIo.Server)
59
+ this.io = this.server;
60
+ else {
61
+ const configuredAllowRequest = socketIoOptions?.allowRequest;
62
+ this.io = new SocketIo.Server(this.server, {
63
+ cors: {
64
+ origin: '*',
65
+ methods: ['GET', 'POST'],
66
+ credentials: true
67
+ },
68
+ serveClient: false,
69
+ ...socketIoOptions,
70
+ /**
71
+ * Refused at the handshake, which is before engine.io builds a Socket for it.
72
+ *
73
+ * close() disconnects the sockets it can see and then waits for the server to shut
74
+ * down. A handshake that completes inside that window is created *after* the sweep
75
+ * has passed, so nothing ever disconnects it - and its ping timer keeps the process
76
+ * alive with no way to reach the peer it belongs to. Clients reconnect on their own,
77
+ * which is what drives connections into the window in the first place.
78
+ */
79
+ allowRequest: (request, callback) => {
80
+ if (this.closed)
81
+ return callback('server closing', false);
82
+ if (configuredAllowRequest)
83
+ return configuredAllowRequest(request, callback);
84
+ callback(null, true);
85
+ }
86
+ });
87
+ }
88
+ // Runs before 'connection', so an unauthenticated peer never reaches the RPC layer.
89
+ if (this.authenticate) {
90
+ this.io.use(async (socket, next) => {
91
+ try {
92
+ const identity = await this.authenticate(socket.handshake.auth, { address: socket.handshake.address });
93
+ if (!identity)
94
+ return next(new Error('unauthorized'));
95
+ socket.data.identity = identity;
96
+ next();
97
+ }
98
+ catch {
99
+ next(new Error('unauthorized'));
100
+ }
101
+ });
102
+ }
103
+ this.io.on('connection', (socket) => {
104
+ // A handshake already in flight when close() began still arrives here. allowRequest
105
+ // catches the ones that had not started; this catches the rest.
106
+ if (this.closed) {
107
+ socket.disconnect(true);
108
+ return;
109
+ }
110
+ this.emit('connection', socket);
111
+ socket.on(PRESENCE_EVENT, (announcement) => {
112
+ // Guarded because socket.io emits this synchronously from its parser: a listener
113
+ // that throws unwinds into the engine rather than into anything that can report it.
114
+ try {
115
+ this.onAnnouncement(socket, announcement);
116
+ }
117
+ catch (e) {
118
+ this.emit(TransportEvent.rejected, { source: 'unknown', reason: `bad presence announcement: ${String(e)}`, error: e });
119
+ }
120
+ });
121
+ // The whole body is guarded, not just the parse: this is an async listener, so anything
122
+ // that escapes it is an unhandled rejection, and Node's default is to end the process.
123
+ // One peer sending one bad frame must not take down a server answering everybody else.
124
+ socket.on('message', (messageArray) => void this.onSocketMessage(socket, messageArray).catch((e) => this.emit(TransportEvent.rejected, { source: 'unknown', reason: `failed to handle frame: ${String(e)}`, error: e })));
125
+ socket.on('disconnect', (reason, details) => {
126
+ this.carriedBy.delete(socket);
127
+ for (const peer of [...this.peerSockets.keys()]) {
128
+ if (this.peerSockets.get(peer) !== socket)
129
+ continue;
130
+ // forgetPeer keeps it if another link still carries it; otherwise this is what
131
+ // lets the RPC layer drop the event subscriptions held for a peer that is gone.
132
+ this.forgetPeer(peer, socket);
133
+ }
134
+ // Emitted rather than printed. These used to go to console.log, which put three
135
+ // lines of socket.io internals on the output of every disconnect and gave anything
136
+ // above this transport no way to see them at all.
137
+ this.emit(TransportEvent.disconnected, reason, details);
138
+ });
139
+ });
140
+ if (this.server && this.ourServer && !(this.server instanceof SocketIo.Server)) {
141
+ const listener = this.server;
142
+ // Without a handler Node throws on the unhandled 'error', so a port already in use took
143
+ // the process down with a stack trace instead of a diagnosis.
144
+ listener.on('error', (e) => {
145
+ // Recorded as well as emitted. A listener that cannot bind never becomes ready, and
146
+ // without this the only symptom was ready() timing out with nothing about the port.
147
+ this.startupError = e;
148
+ this.emit(TransportEvent.transportError, e);
149
+ });
150
+ // Ready means listening. It used to be set here regardless, so ready() resolved before
151
+ // the port was bound and a server could announce itself and then die of EADDRINUSE.
152
+ listener.listen(port, () => {
153
+ this.readyFlag = true;
154
+ console.log(`Socket.io server listening on port ${port}`);
155
+ });
156
+ }
157
+ else
158
+ this.readyFlag = true;
159
+ }
160
+ async onSocketMessage(socket, messageArray) {
161
+ const [header, payload, reason] = this.extractHeader(new Uint8Array(messageArray));
162
+ if (!header) {
163
+ // Reported rather than dropped in silence, which the sender only ever saw as a timeout.
164
+ this.emit(TransportEvent.rejected, { source: 'unknown', reason: reason ?? 'no msgrpc header' });
165
+ return;
166
+ }
167
+ const identity = socket.data.identity;
168
+ if (this.authenticate) {
169
+ // The source field is written by the sender. Pinning it to the identity this
170
+ // connection authenticated as is what stops one peer addressing messages as
171
+ // another and inheriting its rights.
172
+ if (!identity || header.source !== identity.name) {
173
+ this.emit(TransportEvent.rejected, { source: header.source, reason: 'source does not match authenticated identity' });
174
+ return;
175
+ }
176
+ this.peerIdentities.set(header.source, identity);
177
+ }
178
+ // Learned before the routing check, so a peer stays addressable even when a particular
179
+ // frame turns out to be undeliverable.
180
+ this.learnPeer(header.source, socket);
181
+ let message;
182
+ try {
183
+ message = this.codec.decode(payload);
184
+ }
185
+ catch (e) {
186
+ this.emit(TransportEvent.rejected, { source: header.source, reason: `undecodable frame: ${String(e)}` });
187
+ return;
188
+ }
189
+ // Whether this frame is for us at all. A server used to run whatever reached it, testing
190
+ // the target only for being a name it had heard of - so a call addressed to another peer
191
+ // was executed here, the addressee never saw it, and the caller was answered by the wrong
192
+ // peer.
193
+ const elsewhere = header.target !== this.name ? this.peerCarrying(header.target) : undefined;
194
+ if (elsewhere) {
195
+ if (!this.mayRelay(header.source, header.target, identity)) {
196
+ // Deliberately not falling through to local handling: running a call meant for a
197
+ // peer this caller is not allowed to reach would answer it with the wrong
198
+ // implementation and call that a success.
199
+ this.emit(TransportEvent.unroutable, { source: header.source, target: header.target, reason: 'relay refused' });
200
+ return;
201
+ }
202
+ // Announced here rather than in forward(), so that a frame crossing to another
203
+ // transport is reported too: this is the one point both relay paths pass through, and a
204
+ // tap that saw only same-transport traffic would quietly miss half a mixed network.
205
+ // Guarded because this runs per frame, and building the object for nobody is the cost.
206
+ if (this.listenerCount(TransportEvent.relayed))
207
+ this.emit(TransportEvent.relayed, { source: header.source, target: header.target, message });
208
+ // Forwarding within this transport is done here rather than through receive(), so the
209
+ // hop count survives it. A frame that has been round too many relays is dropped: tables
210
+ // settle after a link fails, but a frame circling in the meantime never stops on its own.
211
+ if (elsewhere === this) {
212
+ const hops = (header.hops ?? 0) + 1;
213
+ if (hops > MAX_RELAY_HOPS) {
214
+ this.emit(TransportEvent.unroutable, { source: header.source, target: header.target, reason: `over ${MAX_RELAY_HOPS} relays` });
215
+ return;
216
+ }
217
+ this.forward(message, header.source, header.target, hops);
218
+ return;
219
+ }
220
+ await elsewhere.receive(message, header.source, header.target);
221
+ return;
222
+ }
223
+ if (this.targetExists(header.target)) {
224
+ await this.send(message, header.source, header.target);
225
+ return;
226
+ }
227
+ // Neither this server nor anywhere it can forward to. Reported rather than dropped in
228
+ // silence, which the caller only ever saw as an unexplained timeout.
229
+ this.emit(TransportEvent.unroutable, { source: header.source, target: header.target });
230
+ }
231
+ /**
232
+ * Which module carries a peer, or undefined to handle the frame here. The registry is shared
233
+ * with this server's other transports, so a peer on the broker resolves to the MQTT transport
234
+ * and a socket.io peer can call it without either end knowing the other's transport.
235
+ */
236
+ peerCarrying(target) {
237
+ if (this.peerSockets.has(target))
238
+ return this;
239
+ // The registry is shared with this server's other transports, so a peer on the broker
240
+ // resolves to the MQTT transport and a socket.io peer can reach it without either end
241
+ // knowing which transport the other is on.
242
+ const known = this.peerRegistry.get(target);
243
+ return known && known !== this && known.isTransport() ? known : undefined;
244
+ }
245
+ /** Asked only once a frame really is deliverable elsewhere, never about a peer that is absent. */
246
+ mayRelay(source, target, identity) {
247
+ if (this.relay === false)
248
+ return false;
249
+ if (typeof this.relay === 'function' && !this.permitted(source, target, identity))
250
+ return false;
251
+ this.warnAboutUnauthenticatedRelay();
252
+ return true;
253
+ }
254
+ /**
255
+ * Routes a rule has already allowed, in both directions. Every call has a reply and most have
256
+ * events after it, so asking a rule about each frame separately would mean `source === 'hmi'`
257
+ * silently stranding the answer coming back - the reply's source is the far peer, not the hmi.
258
+ * A permitted call opens the pair, the way connection tracking does, until one of them leaves.
259
+ */
260
+ openRoutes = new Set();
261
+ static pair = (a, b) => `${a}\u0000${b}`;
262
+ permitted(source, target, identity) {
263
+ if (this.openRoutes.has(SocketIoServerTransport.pair(source, target)))
264
+ return true;
265
+ let allowed;
266
+ try {
267
+ allowed = this.relay({ source, target, identity });
268
+ }
269
+ catch {
270
+ // A rule that throws refuses, for the same reason an authorizer that throws denies.
271
+ allowed = false;
272
+ }
273
+ if (!allowed)
274
+ return false;
275
+ this.openRoutes.add(SocketIoServerTransport.pair(source, target));
276
+ this.openRoutes.add(SocketIoServerTransport.pair(target, source));
277
+ return true;
278
+ }
279
+ forgetRoutes(peer) {
280
+ const mark = `${peer}\u0000`;
281
+ for (const route of this.openRoutes)
282
+ if (route.startsWith(mark) || route.endsWith(`\u0000${peer}`))
283
+ this.openRoutes.delete(route);
284
+ }
285
+ /** Named once each, so a peer that reconnects in a loop does not bury the first report. */
286
+ warnedAboutDisplacing = new Set();
287
+ warnAboutDisplacement(name) {
288
+ this.emit(TransportEvent.peerDisplaced, name);
289
+ if (this.warnedAboutDisplacing.has(name))
290
+ return;
291
+ this.warnedAboutDisplacing.add(name);
292
+ console.warn(`source-rpc: '${name}' announced itself on '${this.name}' while another live connection already held that name. ` +
293
+ 'The newcomer takes the address, so if both are really running, replies will reach the wrong one. Give them distinct names.');
294
+ }
295
+ warnedAboutRelay = false;
296
+ /**
297
+ * Said once, and only when this server actually forwards something. Without an authenticator
298
+ * the source of a frame is an unverified claim, so a relay passes on whatever it is told and
299
+ * the peer at the far end has no way to tell who really sent it. Warning at construction would
300
+ * fire for every ordinary server that never relays anything.
301
+ */
302
+ warnAboutUnauthenticatedRelay() {
303
+ if (this.warnedAboutRelay || this.authenticate)
304
+ return;
305
+ this.warnedAboutRelay = true;
306
+ console.warn(`source-rpc: '${this.name}' is relaying frames between peers with no authenticate configured, so their source is an unverified claim. ` +
307
+ 'Set authenticate, or relay: false to forward nothing.');
308
+ }
309
+ /**
310
+ * Refuse to register a peer name this transport has not yet decided to trust.
311
+ *
312
+ * GenericModule registers a frame's source as it parses the header, which is right for MQTT -
313
+ * the broker is the authority there, and there is no connection anyone could have checked. Here
314
+ * there is one, and the header is a claim until it has been checked against it. Registering on
315
+ * parse let a peer put any name it liked into the registry this server shares with its other
316
+ * transports, just by sending one frame that was then rejected: enough to have the bus advertise
317
+ * a peer that does not exist, and to point lookups for a real peer's name at this transport,
318
+ * where nothing answers to it. The frame never went anywhere - peerSockets is what delivery uses
319
+ * and only learnPeer writes it - but the routing table had already been told.
320
+ *
321
+ * So registration happens where the trust decision is made, in learnPeer and forgetPeer, which
322
+ * call `super` to say they are past this point. Nothing changes for a transport with no
323
+ * authenticator: without one, a name was never evidence of anything to begin with.
324
+ */
325
+ setKnownSource(source) {
326
+ if (this.authenticate)
327
+ return;
328
+ super.setKnownSource(source);
329
+ }
330
+ /**
331
+ * Record a peer against the socket that reaches it and tell everyone else it is here.
332
+ *
333
+ * A peer announcing itself owns its name and takes the route over. One merely carried by a
334
+ * neighbour does not: the first link to offer it keeps it, and a second offering the same name
335
+ * is remembered only as a fallback. Letting carried announcements steal the route would make
336
+ * two neighbours advertising the same peer flip it back and forth, each flip re-announced
337
+ * onwards - chatter that never settles.
338
+ */
339
+ learnPeer(name, socket, carried = false) {
340
+ const known = this.peerSockets.get(name);
341
+ if (known === socket)
342
+ return;
343
+ if (known && carried)
344
+ return;
345
+ // A live connection already answers to this name, and the newcomer is about to take the
346
+ // address off it. The takeover is deliberate - a peer reconnecting after a blip announces
347
+ // itself while the server may still hold the dead socket, and refusing it would lock a peer
348
+ // out of its own name - but it must not be silent. Two peers genuinely sharing a name send
349
+ // each other's replies into the wrong socket, which reads as calls timing out for no reason.
350
+ if (known && known.connected)
351
+ this.warnAboutDisplacement(name);
352
+ this.peerSockets.set(name, socket);
353
+ // Registered as well as recorded: the shared registry is what the switch routes on, and
354
+ // what a server reads to work out which peers it can advertise onwards. `super`, because
355
+ // this is the point the name stops being a claim - see setKnownSource above.
356
+ super.setKnownSource(name);
357
+ this.emit(TransportEvent.peerOnline, name);
358
+ this.broadcastPresence({ peer: name, state: 'online' });
359
+ }
360
+ /** A peer is gone from here unless another link still offers it. */
361
+ forgetPeer(name, socket) {
362
+ if (this.peerSockets.get(name) !== socket)
363
+ return;
364
+ this.peerSockets.delete(name);
365
+ this.peerIdentities.delete(name);
366
+ for (const [other, carried] of this.carriedBy) {
367
+ if (other === socket || !carried.has(name))
368
+ continue;
369
+ // Still reachable the other way, so nothing above this needs to hear about it.
370
+ this.peerSockets.set(name, other);
371
+ super.setKnownSource(name);
372
+ return;
373
+ }
374
+ this.emit(TransportEvent.peerGone, name);
375
+ this.forgetRoutes(name);
376
+ this.broadcastPresence({ peer: name, state: 'offline' });
377
+ }
378
+ /**
379
+ * A peer saying who it is, which is the whole of discovery here. It answers with the peers
380
+ * already connected, standing in for the retained presence an MQTT subscriber is handed.
381
+ */
382
+ onAnnouncement(socket, announcement) {
383
+ const name = announcement?.name;
384
+ if (!isUsablePeerName(name)) {
385
+ this.emit(TransportEvent.rejected, { source: String(name), reason: 'unusable peer name in presence announcement' });
386
+ return;
387
+ }
388
+ const identity = socket.data.identity;
389
+ if (this.authenticate) {
390
+ // Same rule as a frame's source: a name is a claim until a connection vouches for it,
391
+ // and an unchecked one here would let a peer be listed and addressed as someone else.
392
+ if (!identity || name !== identity.name) {
393
+ this.emit(TransportEvent.rejected, { source: name, reason: 'announced name does not match authenticated identity' });
394
+ return;
395
+ }
396
+ this.peerIdentities.set(name, identity);
397
+ }
398
+ this.learnPeer(name, socket);
399
+ this.updateCarried(socket, name, announcement.carrying);
400
+ // This peer's own name goes first: a newcomer has to know what to call the thing it just
401
+ // connected to, and a client that routes on its registry - which RpcServer.proxy does -
402
+ // cannot address the hub at all until something puts it there.
403
+ //
404
+ // Split horizon applies to the rest. Handing a link back the peers it just told this one
405
+ // about makes it believe they are reachable the way it came, so it stops advertising them,
406
+ // and they disappear from everyone a hop further out.
407
+ socket.emit(PRESENCE_EVENT, {
408
+ peers: [this.name, ...this.reachablePeers().filter((peer) => peer !== name && this.peerSockets.get(peer) !== socket)]
409
+ });
410
+ }
411
+ /**
412
+ * Everyone this server can put a frame in front of: its own connections, plus whatever its
413
+ * other transports have registered. On a server holding a socket.io listener and a broker
414
+ * connection, that is what lets a browser peer see and call a peer on the broker.
415
+ */
416
+ reachablePeers() {
417
+ const names = new Set(this.peerSockets.keys());
418
+ for (const name of this.peerRegistry.names())
419
+ names.add(name);
420
+ names.delete(this.name);
421
+ return [...names];
422
+ }
423
+ /** Tell the connected peers about one that arrived or left on a different transport. */
424
+ announcePeer(peer, state) {
425
+ if (this.peerSockets.has(peer))
426
+ return;
427
+ this.broadcastPresence({ peer, state });
428
+ }
429
+ /** Apply a link's latest claim about what lies behind it, adding and dropping as it changes. */
430
+ updateCarried(socket, announcer, carrying) {
431
+ const claimed = new Set((Array.isArray(carrying) ? carrying : [])
432
+ .slice(0, MAX_CARRIED_PEERS)
433
+ .filter((peer) => isUsablePeerName(peer) && peer !== this.name && peer !== announcer));
434
+ const previous = this.carriedBy.get(socket) ?? new Set();
435
+ this.carriedBy.set(socket, claimed);
436
+ for (const peer of previous)
437
+ if (!claimed.has(peer))
438
+ this.forgetPeer(peer, socket);
439
+ for (const peer of claimed)
440
+ if (!previous.has(peer))
441
+ this.learnPeer(peer, socket, true);
442
+ }
443
+ broadcastPresence(update) {
444
+ // Everyone but the peer it is about; it already knows.
445
+ const subject = update.peer ? this.peerSockets.get(update.peer) : undefined;
446
+ for (const socket of new Set(this.peerSockets.values()))
447
+ if (socket !== subject)
448
+ socket.emit(PRESENCE_EVENT, update);
449
+ }
450
+ /** Send to a peer's socket, carrying a hop count the ordinary send path has no reason to know. */
451
+ forward(message, source, target, hops) {
452
+ const socket = this.peerSockets.get(target);
453
+ if (!socket) {
454
+ this.emit(TransportEvent.unroutable, { source, target });
455
+ return;
456
+ }
457
+ try {
458
+ socket.emit('message', this.frameMessage(this.buildHeader(source, target, { hops }), this.codec.encode(message)));
459
+ }
460
+ catch (e) {
461
+ // Relaying is done on someone else's behalf, so there is no caller here to reject.
462
+ // Reported instead, or an unframeable relay would be indistinguishable from a lost one.
463
+ this.emit(TransportEvent.unroutable, { source, target, reason: `cannot forward: ${String(e)}`, error: e });
464
+ }
465
+ }
466
+ async receive(message, source, target) {
467
+ const socket = target === undefined ? undefined : this.peerSockets.get(target);
468
+ if (!socket) {
469
+ // Deliberately no io.emit() fallback: broadcasting would put this peer's reply on
470
+ // every other client's socket. An unknown target means the peer never identified
471
+ // itself or has gone away, so the frame is dropped.
472
+ this.emit(TransportEvent.unroutable, { source, target });
473
+ return;
474
+ }
475
+ socket.emit('message', this.frameMessage(this.buildHeader(source, target), this.codec.encode(message)));
476
+ }
477
+ async close() {
478
+ if (this.closed) {
479
+ return;
480
+ }
481
+ this.closed = true;
482
+ this.peerSockets.clear();
483
+ this.peerIdentities.clear();
484
+ const io = this.io;
485
+ const server = this.server;
486
+ this.io = undefined;
487
+ this.server = undefined;
488
+ this.emit('close');
489
+ const ownHttpServer = server && this.ourServer && !(server instanceof SocketIo.Server) ? server : undefined;
490
+ io?.disconnectSockets(true);
491
+ // Keep-alive connections would otherwise hold the listener open long past close().
492
+ ownHttpServer?.closeAllConnections();
493
+ if (io)
494
+ await new Promise((resolve) => io.close(() => resolve()));
495
+ if (ownHttpServer?.listening)
496
+ await new Promise((resolve) => ownHttpServer.close(() => resolve()));
497
+ }
498
+ getIdentity(source) {
499
+ return this.peerIdentities.get(source);
500
+ }
501
+ isTransport() {
502
+ return true;
503
+ }
504
+ }
505
+ //# sourceMappingURL=SocketIoServerTransport.js.map