@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.
- package/LICENSE +21 -0
- package/README.md +1208 -0
- package/dist/Logging/ILogger.d.ts +7 -0
- package/dist/Logging/ILogger.d.ts.map +1 -0
- package/dist/Logging/ILogger.js +2 -0
- package/dist/Logging/ILogger.js.map +1 -0
- package/dist/NodeRpcServer.d.ts +55 -0
- package/dist/NodeRpcServer.d.ts.map +1 -0
- package/dist/NodeRpcServer.js +46 -0
- package/dist/NodeRpcServer.js.map +1 -0
- package/dist/RPC/Auth.d.ts +98 -0
- package/dist/RPC/Auth.d.ts.map +1 -0
- package/dist/RPC/Auth.js +14 -0
- package/dist/RPC/Auth.js.map +1 -0
- package/dist/RPC/Codec.d.ts +24 -0
- package/dist/RPC/Codec.d.ts.map +1 -0
- package/dist/RPC/Codec.js +21 -0
- package/dist/RPC/Codec.js.map +1 -0
- package/dist/RPC/Compatibility.d.ts +39 -0
- package/dist/RPC/Compatibility.d.ts.map +1 -0
- package/dist/RPC/Compatibility.js +195 -0
- package/dist/RPC/Compatibility.js.map +1 -0
- package/dist/RPC/Core.d.ts +188 -0
- package/dist/RPC/Core.d.ts.map +1 -0
- package/dist/RPC/Core.js +354 -0
- package/dist/RPC/Core.js.map +1 -0
- package/dist/RPC/Expose.d.ts +90 -0
- package/dist/RPC/Expose.d.ts.map +1 -0
- package/dist/RPC/Expose.js +110 -0
- package/dist/RPC/Expose.js.map +1 -0
- package/dist/RPC/Idempotency.d.ts +90 -0
- package/dist/RPC/Idempotency.d.ts.map +1 -0
- package/dist/RPC/Idempotency.js +46 -0
- package/dist/RPC/Idempotency.js.map +1 -0
- package/dist/RPC/Introspection.d.ts +92 -0
- package/dist/RPC/Introspection.d.ts.map +1 -0
- package/dist/RPC/Introspection.js +188 -0
- package/dist/RPC/Introspection.js.map +1 -0
- package/dist/RPC/Introspection.types.json +1093 -0
- package/dist/RPC/Messages.d.ts +108 -0
- package/dist/RPC/Messages.d.ts.map +1 -0
- package/dist/RPC/Messages.js +30 -0
- package/dist/RPC/Messages.js.map +1 -0
- package/dist/RPC/Rpc.d.ts +51 -0
- package/dist/RPC/Rpc.d.ts.map +1 -0
- package/dist/RPC/Rpc.js +54 -0
- package/dist/RPC/Rpc.js.map +1 -0
- package/dist/RPC/RpcClientHandler.d.ts +129 -0
- package/dist/RPC/RpcClientHandler.d.ts.map +1 -0
- package/dist/RPC/RpcClientHandler.js +267 -0
- package/dist/RPC/RpcClientHandler.js.map +1 -0
- package/dist/RPC/RpcServerHandler.d.ts +226 -0
- package/dist/RPC/RpcServerHandler.d.ts.map +1 -0
- package/dist/RPC/RpcServerHandler.js +676 -0
- package/dist/RPC/RpcServerHandler.js.map +1 -0
- package/dist/RPC/Schema.d.ts +150 -0
- package/dist/RPC/Schema.d.ts.map +1 -0
- package/dist/RPC/Schema.js +198 -0
- package/dist/RPC/Schema.js.map +1 -0
- package/dist/RPC/Signing.d.ts +118 -0
- package/dist/RPC/Signing.d.ts.map +1 -0
- package/dist/RPC/Signing.js +171 -0
- package/dist/RPC/Signing.js.map +1 -0
- package/dist/RPC/Tokens.d.ts +49 -0
- package/dist/RPC/Tokens.d.ts.map +1 -0
- package/dist/RPC/Tokens.js +45 -0
- package/dist/RPC/Tokens.js.map +1 -0
- package/dist/RpcClient.d.ts +91 -0
- package/dist/RpcClient.d.ts.map +1 -0
- package/dist/RpcClient.js +145 -0
- package/dist/RpcClient.js.map +1 -0
- package/dist/RpcServer.d.ts +191 -0
- package/dist/RpcServer.d.ts.map +1 -0
- package/dist/RpcServer.js +293 -0
- package/dist/RpcServer.js.map +1 -0
- package/dist/Transports/Mqtt5Frame.d.ts +107 -0
- package/dist/Transports/Mqtt5Frame.d.ts.map +1 -0
- package/dist/Transports/Mqtt5Frame.js +162 -0
- package/dist/Transports/Mqtt5Frame.js.map +1 -0
- package/dist/Transports/MqttTransport.d.ts +279 -0
- package/dist/Transports/MqttTransport.d.ts.map +1 -0
- package/dist/Transports/MqttTransport.js +843 -0
- package/dist/Transports/MqttTransport.js.map +1 -0
- package/dist/Transports/Presence.d.ts +68 -0
- package/dist/Transports/Presence.d.ts.map +1 -0
- package/dist/Transports/Presence.js +21 -0
- package/dist/Transports/Presence.js.map +1 -0
- package/dist/Transports/SocketIoClientTransport.d.ts +89 -0
- package/dist/Transports/SocketIoClientTransport.d.ts.map +1 -0
- package/dist/Transports/SocketIoClientTransport.js +280 -0
- package/dist/Transports/SocketIoClientTransport.js.map +1 -0
- package/dist/Transports/SocketIoServerTransport.d.ts +135 -0
- package/dist/Transports/SocketIoServerTransport.d.ts.map +1 -0
- package/dist/Transports/SocketIoServerTransport.js +505 -0
- package/dist/Transports/SocketIoServerTransport.js.map +1 -0
- package/dist/Utilities/Converters.d.ts +22 -0
- package/dist/Utilities/Converters.d.ts.map +1 -0
- package/dist/Utilities/Converters.js +58 -0
- package/dist/Utilities/Converters.js.map +1 -0
- package/dist/Utilities/Filter.d.ts +7 -0
- package/dist/Utilities/Filter.d.ts.map +1 -0
- package/dist/Utilities/Filter.js +14 -0
- package/dist/Utilities/Filter.js.map +1 -0
- package/dist/Utilities/ReadableName.d.ts +38 -0
- package/dist/Utilities/ReadableName.d.ts.map +1 -0
- package/dist/Utilities/ReadableName.js +58 -0
- package/dist/Utilities/ReadableName.js.map +1 -0
- package/dist/Utilities/Switch.d.ts +21 -0
- package/dist/Utilities/Switch.d.ts.map +1 -0
- package/dist/Utilities/Switch.js +70 -0
- package/dist/Utilities/Switch.js.map +1 -0
- package/dist/Utilities/TryCatch.d.ts +11 -0
- package/dist/Utilities/TryCatch.d.ts.map +1 -0
- package/dist/Utilities/TryCatch.js +13 -0
- package/dist/Utilities/TryCatch.js.map +1 -0
- package/dist/index-web.d.ts +25 -0
- package/dist/index-web.d.ts.map +1 -0
- package/dist/index-web.js +28 -0
- package/dist/index-web.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- 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
|