socket-function 1.1.28 → 1.1.30
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/SocketFunction.d.ts +0 -1
- package/SocketFunction.ts +0 -2
- package/index.d.ts +23 -7
- package/package.json +1 -1
- package/src/CallFactory.d.ts +2 -0
- package/src/CallFactory.ts +36 -29
- package/src/batching.ts +2 -1
- package/src/nodeCache.d.ts +0 -5
- package/src/nodeCache.ts +0 -14
- package/src/protocolNegotiation.d.ts +15 -0
- package/src/protocolNegotiation.ts +108 -0
- package/src/webSocketServer.ts +21 -0
- package/src/websocketFactory.d.ts +2 -1
- package/src/websocketFactory.ts +56 -52
package/SocketFunction.d.ts
CHANGED
package/SocketFunction.ts
CHANGED
|
@@ -61,8 +61,6 @@ export class SocketFunction {
|
|
|
61
61
|
|
|
62
62
|
public static HTTP_COMPRESS = false;
|
|
63
63
|
|
|
64
|
-
public static LEGACY_INITIALIZE = false;
|
|
65
|
-
|
|
66
64
|
// If you have HTTP resources that require cookies you might to set `SocketFunction.COEP = "require-corp"`
|
|
67
65
|
// - Cross-origin-resource-policy.
|
|
68
66
|
public static COEP = "credentialless";
|
package/index.d.ts
CHANGED
|
@@ -31,7 +31,6 @@ declare module "socket-function/SocketFunction" {
|
|
|
31
31
|
static HTTP_ETAG_CACHE: boolean;
|
|
32
32
|
static silent: boolean;
|
|
33
33
|
static HTTP_COMPRESS: boolean;
|
|
34
|
-
static LEGACY_INITIALIZE: boolean;
|
|
35
34
|
static COEP: string;
|
|
36
35
|
static COOP: string;
|
|
37
36
|
static TOTAL_CALLS: number;
|
|
@@ -472,6 +471,7 @@ declare module "socket-function/src/CallFactory" {
|
|
|
472
471
|
closedForever?: boolean;
|
|
473
472
|
isConnected?: boolean;
|
|
474
473
|
receivedInitializeState?: InitializeState;
|
|
474
|
+
protocolNegotiated?: boolean;
|
|
475
475
|
performCall(call: CallType): Promise<unknown>;
|
|
476
476
|
onNextDisconnect(callback: () => void): void;
|
|
477
477
|
disconnect(): void;
|
|
@@ -482,6 +482,7 @@ declare module "socket-function/src/CallFactory" {
|
|
|
482
482
|
export interface SenderInterface {
|
|
483
483
|
nodeId?: string;
|
|
484
484
|
_socket?: tls.TLSSocket;
|
|
485
|
+
protocol?: string;
|
|
485
486
|
send(data: string | Buffer): void;
|
|
486
487
|
close(): void;
|
|
487
488
|
addEventListener(event: "open", listener: () => void): void;
|
|
@@ -1079,11 +1080,6 @@ declare module "socket-function/src/nodeCache" {
|
|
|
1079
1080
|
export declare function getNodeIdDomain(nodeId: string): string;
|
|
1080
1081
|
export declare function getNodeIdDomainMaybeUndefined(nodeId: string): string | undefined;
|
|
1081
1082
|
export declare function registerNodeClient(callFactory: CallFactory): void;
|
|
1082
|
-
export declare function changeNodeId(config: {
|
|
1083
|
-
originalNodeId: string;
|
|
1084
|
-
newNodeId: string;
|
|
1085
|
-
callFactory: CallFactory;
|
|
1086
|
-
}): boolean;
|
|
1087
1083
|
export declare function getCreateCallFactory(nodeId: string): MaybePromise<CallFactory>;
|
|
1088
1084
|
export declare function getCallFactory(nodeId: string): MaybePromise<CallFactory | undefined>;
|
|
1089
1085
|
export declare function debugGetAllCallFactories(): CallFactory[];
|
|
@@ -1283,6 +1279,25 @@ declare module "socket-function/src/promiseRace" {
|
|
|
1283
1279
|
|
|
1284
1280
|
}
|
|
1285
1281
|
|
|
1282
|
+
declare module "socket-function/src/protocolNegotiation" {
|
|
1283
|
+
export type ConnectionFlags = {
|
|
1284
|
+
clientLZ4: boolean;
|
|
1285
|
+
serverLZ4: boolean;
|
|
1286
|
+
};
|
|
1287
|
+
export type DecodedProtocol = {
|
|
1288
|
+
target: string;
|
|
1289
|
+
flags: ConnectionFlags;
|
|
1290
|
+
};
|
|
1291
|
+
export declare function decodeProtocol(hex: string): DecodedProtocol | undefined;
|
|
1292
|
+
export declare function proposeProtocols(target: string | undefined, clientCapabilities: {
|
|
1293
|
+
lz4: boolean;
|
|
1294
|
+
}): string[];
|
|
1295
|
+
export declare function chooseProtocol(proposed: string[], serverNodeId: string, serverCapabilities: {
|
|
1296
|
+
lz4: boolean;
|
|
1297
|
+
}): string | undefined;
|
|
1298
|
+
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1286
1301
|
declare module "socket-function/src/runPromise" {
|
|
1287
1302
|
export declare const runAsync: typeof runPromise;
|
|
1288
1303
|
export declare function runPromise(command: string, config?: {
|
|
@@ -1419,10 +1434,11 @@ declare module "socket-function/src/websocketFactory" {
|
|
|
1419
1434
|
import { SenderInterface } from "socket-function/src/CallFactory";
|
|
1420
1435
|
import type * as ws from "ws";
|
|
1421
1436
|
export declare function getTLSSocket(webSocket: ws.WebSocket): tls.TLSSocket;
|
|
1437
|
+
export type WebsocketFactory = (nodeId: string, proposedProtocols?: string[]) => SenderInterface;
|
|
1422
1438
|
/** NOTE: We create a factory, which embeds the key/cert information. Otherwise retries might use
|
|
1423
1439
|
* a different key/cert context.
|
|
1424
1440
|
*/
|
|
1425
|
-
export declare function createWebsocketFactory():
|
|
1441
|
+
export declare function createWebsocketFactory(): WebsocketFactory;
|
|
1426
1442
|
|
|
1427
1443
|
}
|
|
1428
1444
|
|
package/package.json
CHANGED
package/src/CallFactory.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface CallFactory {
|
|
|
11
11
|
closedForever?: boolean;
|
|
12
12
|
isConnected?: boolean;
|
|
13
13
|
receivedInitializeState?: InitializeState;
|
|
14
|
+
protocolNegotiated?: boolean;
|
|
14
15
|
performCall(call: CallType): Promise<unknown>;
|
|
15
16
|
onNextDisconnect(callback: () => void): void;
|
|
16
17
|
disconnect(): void;
|
|
@@ -21,6 +22,7 @@ export interface CallFactory {
|
|
|
21
22
|
export interface SenderInterface {
|
|
22
23
|
nodeId?: string;
|
|
23
24
|
_socket?: tls.TLSSocket;
|
|
25
|
+
protocol?: string;
|
|
24
26
|
send(data: string | Buffer): void;
|
|
25
27
|
close(): void;
|
|
26
28
|
addEventListener(event: "open", listener: () => void): void;
|
package/src/CallFactory.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { convertErrorStackToError, formatNumberSuffixed, isBufferType, isNode, l
|
|
|
5
5
|
import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
|
|
6
6
|
import { SocketFunction } from "../SocketFunction";
|
|
7
7
|
import * as tls from "tls";
|
|
8
|
-
import {
|
|
8
|
+
import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
|
|
9
9
|
import debugbreak from "debugbreak";
|
|
10
10
|
import { lazy } from "./caching";
|
|
11
11
|
import { blue, green, red, yellow } from "./formatting/logColors";
|
|
@@ -18,7 +18,8 @@ import { setFlag } from "../require/compileFlags";
|
|
|
18
18
|
import { measureFnc, measureWrap, registerMeasureInfo } from "./profiling/measure";
|
|
19
19
|
import { MaybePromise } from "./types";
|
|
20
20
|
import { Zip } from "./Zip";
|
|
21
|
-
import {
|
|
21
|
+
import { decodeProtocol, proposeProtocols } from "./protocolNegotiation";
|
|
22
|
+
setImmediate(() => import("./lz4/LZ4"));
|
|
22
23
|
|
|
23
24
|
setFlag(require, "pako", "allowclient", true);
|
|
24
25
|
|
|
@@ -49,6 +50,9 @@ export interface CallFactory {
|
|
|
49
50
|
closedForever?: boolean;
|
|
50
51
|
isConnected?: boolean;
|
|
51
52
|
receivedInitializeState?: InitializeState;
|
|
53
|
+
// True if the connection was established with a negotiated
|
|
54
|
+
// Sec-WebSocket-Protocol (so the legacy initialize packet should be skipped).
|
|
55
|
+
protocolNegotiated?: boolean;
|
|
52
56
|
// NOTE: May or may not have reconnection or retry logic inside of performCall.
|
|
53
57
|
// Trigger performLocalCall on the other side of the connection
|
|
54
58
|
performCall(call: CallType): Promise<unknown>;
|
|
@@ -62,6 +66,9 @@ export interface SenderInterface {
|
|
|
62
66
|
nodeId?: string;
|
|
63
67
|
// Only set AFTER "open" (if set at all, as in the browser we don't have access to the socket).
|
|
64
68
|
_socket?: tls.TLSSocket;
|
|
69
|
+
// The chosen Sec-WebSocket-Protocol value (set after "open" by both Node `ws`
|
|
70
|
+
// and the browser WebSocket). Empty string means none was negotiated.
|
|
71
|
+
protocol?: string;
|
|
65
72
|
|
|
66
73
|
send(data: string | Buffer): void;
|
|
67
74
|
close(): void;
|
|
@@ -368,6 +375,23 @@ export async function createCallFactory(
|
|
|
368
375
|
} else {
|
|
369
376
|
onClose(new Error(`Websocket received in closed state`).stack!);
|
|
370
377
|
}
|
|
378
|
+
|
|
379
|
+
if (callFactory.lastClosed && callFactory.isConnected) {
|
|
380
|
+
console.log(`Successfully reconnected to ${nodeId}, which has been closed for ${formatTime(Date.now() - callFactory.lastClosed)}`, { nodeId });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (callFactory.isConnected && newWebSocket.protocol) {
|
|
384
|
+
let decoded = decodeProtocol(newWebSocket.protocol);
|
|
385
|
+
if (decoded) {
|
|
386
|
+
callFactory.receivedInitializeState = {
|
|
387
|
+
supportsLZ4: decoded.flags.serverLZ4,
|
|
388
|
+
};
|
|
389
|
+
callFactory.protocolNegotiated = true;
|
|
390
|
+
if (SocketFunction.logMessages) {
|
|
391
|
+
console.log(green(`Negotiated protocol with ${niceConnectionName}: target=${decoded.target}, clientLZ4=${decoded.flags.clientLZ4}, serverLZ4=${decoded.flags.serverLZ4}`));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
const BASE_LENGTH_OFFSET = 324_432_461_592_612;
|
|
@@ -458,12 +482,12 @@ export async function createCallFactory(
|
|
|
458
482
|
}
|
|
459
483
|
lastConnectionAttempt = Date.now();
|
|
460
484
|
|
|
461
|
-
// Try alternates, and if any work, use them
|
|
485
|
+
// Try alternates, and if any work, use them.
|
|
462
486
|
try {
|
|
463
487
|
let alternates = await SocketFunction.GET_ALTERNATE_NODE_IDS(nodeId);
|
|
464
488
|
if (alternates) {
|
|
465
489
|
for (let alternateNodeId of alternates) {
|
|
466
|
-
let newWebSocket = createWebsocket(alternateNodeId);
|
|
490
|
+
let newWebSocket = createWebsocket(alternateNodeId, proposeProtocols(isNode() ? nodeId : undefined, { lz4: true }));
|
|
467
491
|
await initializeWebsocket(newWebSocket, true);
|
|
468
492
|
|
|
469
493
|
if (callFactory.isConnected) {
|
|
@@ -475,7 +499,7 @@ export async function createCallFactory(
|
|
|
475
499
|
console.error("Error getting alternate node IDs", e);
|
|
476
500
|
}
|
|
477
501
|
|
|
478
|
-
let newWebSocket = createWebsocket(nodeId);
|
|
502
|
+
let newWebSocket = createWebsocket(nodeId, proposeProtocols(isNode() ? nodeId : undefined, { lz4: true }));
|
|
479
503
|
await initializeWebsocket(newWebSocket);
|
|
480
504
|
|
|
481
505
|
return newWebSocket;
|
|
@@ -556,11 +580,10 @@ export async function createCallFactory(
|
|
|
556
580
|
};
|
|
557
581
|
|
|
558
582
|
if (call.isReturn) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
583
|
+
// Tolerate the legacy initialize packet from old peers — silently
|
|
584
|
+
// ignore so it doesn't get logged as an unknown call. Our actual
|
|
585
|
+
// initialize state comes from Sec-WebSocket-Protocol negotiation.
|
|
586
|
+
if (call.seqNum === INITIALIZE_STATE_SEQ_NUM) {
|
|
564
587
|
return;
|
|
565
588
|
}
|
|
566
589
|
let callbackObj = pendingCalls.get(call.seqNum);
|
|
@@ -770,25 +793,6 @@ export async function createCallFactory(
|
|
|
770
793
|
}
|
|
771
794
|
|
|
772
795
|
|
|
773
|
-
if (!SocketFunction.LEGACY_INITIALIZE) {
|
|
774
|
-
let initState: InitializeState = {
|
|
775
|
-
supportsLZ4: true,
|
|
776
|
-
};
|
|
777
|
-
let initReturn: InternalReturnType = {
|
|
778
|
-
isReturn: true,
|
|
779
|
-
result: initState,
|
|
780
|
-
seqNum: INITIALIZE_STATE_SEQ_NUM,
|
|
781
|
-
};
|
|
782
|
-
if (SocketFunction.logMessages) {
|
|
783
|
-
console.log(`Sending initialize state to ${nodeId}`);
|
|
784
|
-
}
|
|
785
|
-
let data = await SocketFunction.WIRE_SERIALIZER.serialize(initReturn);
|
|
786
|
-
await send(data);
|
|
787
|
-
if (SocketFunction.logMessages) {
|
|
788
|
-
console.log(`Sent initialize state to ${nodeId}`);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
796
|
return callFactory;
|
|
793
797
|
}
|
|
794
798
|
|
|
@@ -850,6 +854,7 @@ const decompressObj = measureWrap(async function wireCallDecompress(obj: Buffer,
|
|
|
850
854
|
});
|
|
851
855
|
|
|
852
856
|
const compressObjLZ4 = measureWrap(async function wireCallCompressLZ4(obj: unknown, stats: CompressionStats): Promise<Buffer[]> {
|
|
857
|
+
const { LZ4 } = await import("./lz4/LZ4");
|
|
853
858
|
let headerParts: number[];
|
|
854
859
|
let dataBuffers: Buffer[];
|
|
855
860
|
|
|
@@ -952,6 +957,8 @@ const compressObjLZ4 = measureWrap(async function wireCallCompressLZ4(obj: unkno
|
|
|
952
957
|
});
|
|
953
958
|
|
|
954
959
|
const decompressObjLZ4 = measureWrap(async function wireCallDecompressLZ4(obj: Buffer[], stats: CompressionStats): Promise<unknown> {
|
|
960
|
+
|
|
961
|
+
const { LZ4 } = await import("./lz4/LZ4");
|
|
955
962
|
stats.compressedSize += obj.reduce((sum, buf) => sum + buf.length, 0);
|
|
956
963
|
|
|
957
964
|
let decompressed: Buffer[] = [];
|
package/src/batching.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatTime } from "./formatting/format";
|
|
2
|
+
import { red } from "./formatting/logColors";
|
|
2
3
|
import { PromiseObj, isNode, timeoutToError } from "./misc";
|
|
3
4
|
import { measureWrap } from "./profiling/measure";
|
|
4
5
|
import { AnyFunction, Args, MaybePromise } from "./types";
|
|
@@ -395,7 +396,7 @@ export async function safeLoop<T, R>(config: {
|
|
|
395
396
|
while (!done) {
|
|
396
397
|
await delay(5000);
|
|
397
398
|
if (done) break;
|
|
398
|
-
let message =
|
|
399
|
+
let message = `${red("SLOW LOOP")} | ${index} / ${data.length} | ${formatTime(Date.now() - startTime)} | ${label}`;
|
|
399
400
|
let timeOnCurrent = Date.now() - indexStartTime;
|
|
400
401
|
if (timeOnCurrent > 5000) {
|
|
401
402
|
message += `| SLOW INDEX (${index}) ${formatTime(timeOnCurrent)}+`;
|
package/src/nodeCache.d.ts
CHANGED
|
@@ -18,11 +18,6 @@ export declare function getNodeIdLocation(nodeId: string): {
|
|
|
18
18
|
export declare function getNodeIdDomain(nodeId: string): string;
|
|
19
19
|
export declare function getNodeIdDomainMaybeUndefined(nodeId: string): string | undefined;
|
|
20
20
|
export declare function registerNodeClient(callFactory: CallFactory): void;
|
|
21
|
-
export declare function changeNodeId(config: {
|
|
22
|
-
originalNodeId: string;
|
|
23
|
-
newNodeId: string;
|
|
24
|
-
callFactory: CallFactory;
|
|
25
|
-
}): boolean;
|
|
26
21
|
export declare function getCreateCallFactory(nodeId: string): MaybePromise<CallFactory>;
|
|
27
22
|
export declare function getCallFactory(nodeId: string): MaybePromise<CallFactory | undefined>;
|
|
28
23
|
export declare function debugGetAllCallFactories(): CallFactory[];
|
package/src/nodeCache.ts
CHANGED
|
@@ -77,20 +77,6 @@ export function registerNodeClient(callFactory: CallFactory) {
|
|
|
77
77
|
startCleanupLoop();
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export function changeNodeId(config: {
|
|
81
|
-
originalNodeId: string;
|
|
82
|
-
newNodeId: string;
|
|
83
|
-
callFactory: CallFactory;
|
|
84
|
-
}) {
|
|
85
|
-
if (nodeCache.has(config.newNodeId)) {
|
|
86
|
-
console.warn(`Received connection we already have. Likely we and them tried to connect at the same time. Other nodeId: ${config.newNodeId}`);
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
nodeCache.delete(config.originalNodeId);
|
|
90
|
-
nodeCache.set(config.newNodeId, config.callFactory);
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
80
|
export function getCreateCallFactory(nodeId: string): MaybePromise<CallFactory> {
|
|
95
81
|
let callFactory = nodeCache.get(nodeId);
|
|
96
82
|
if (callFactory === undefined) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ConnectionFlags = {
|
|
2
|
+
clientLZ4: boolean;
|
|
3
|
+
serverLZ4: boolean;
|
|
4
|
+
};
|
|
5
|
+
export type DecodedProtocol = {
|
|
6
|
+
target: string;
|
|
7
|
+
flags: ConnectionFlags;
|
|
8
|
+
};
|
|
9
|
+
export declare function decodeProtocol(hex: string): DecodedProtocol | undefined;
|
|
10
|
+
export declare function proposeProtocols(target: string | undefined, clientCapabilities: {
|
|
11
|
+
lz4: boolean;
|
|
12
|
+
}): string[];
|
|
13
|
+
export declare function chooseProtocol(proposed: string[], serverNodeId: string, serverCapabilities: {
|
|
14
|
+
lz4: boolean;
|
|
15
|
+
}): string | undefined;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Negotiates connection-level flags via Sec-WebSocket-Protocol on the WebSocket
|
|
2
|
+
// upgrade handshake. The client proposes every flag combination it accepts (hex-
|
|
3
|
+
// encoded so the value is a valid HTTP token). The server picks the first value
|
|
4
|
+
// whose target nodeId matches its own, then returns it verbatim. If none match,
|
|
5
|
+
// the server returns no protocol and the handshake fails — which is exactly the
|
|
6
|
+
// rejection semantics we want (indistinguishable from "node not reachable").
|
|
7
|
+
|
|
8
|
+
const PROTOCOL_VERSION = "v1";
|
|
9
|
+
|
|
10
|
+
export type ConnectionFlags = {
|
|
11
|
+
// Client supports receiving LZ4-compressed frames
|
|
12
|
+
clientLZ4: boolean;
|
|
13
|
+
// Server supports receiving LZ4-compressed frames (i.e. server can accept LZ4)
|
|
14
|
+
serverLZ4: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DecodedProtocol = {
|
|
18
|
+
target: string;
|
|
19
|
+
flags: ConnectionFlags;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function hexEncode(s: string): string {
|
|
23
|
+
return Buffer.from(s, "utf8").toString("hex");
|
|
24
|
+
}
|
|
25
|
+
function hexDecode(s: string): string {
|
|
26
|
+
return Buffer.from(s, "hex").toString("utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function encodeFlagBit(b: boolean): string { return b ? "1" : "0"; }
|
|
30
|
+
|
|
31
|
+
// An empty-string target encodes "match any server nodeId" — used for
|
|
32
|
+
// browser clients which don't know our internal nodeId because they're
|
|
33
|
+
// connecting through a Let's Encrypt cert on a public domain.
|
|
34
|
+
const WILDCARD_TARGET = "";
|
|
35
|
+
|
|
36
|
+
function encodeOne(target: string, flags: ConnectionFlags): string {
|
|
37
|
+
let plain = `${PROTOCOL_VERSION}|${target}|clz4=${encodeFlagBit(flags.clientLZ4)}|slz4=${encodeFlagBit(flags.serverLZ4)}`;
|
|
38
|
+
return hexEncode(plain);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function decodeProtocol(hex: string): DecodedProtocol | undefined {
|
|
42
|
+
if (!/^[0-9a-fA-F]+$/.test(hex)) return undefined;
|
|
43
|
+
let plain: string;
|
|
44
|
+
try {
|
|
45
|
+
plain = hexDecode(hex);
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
let parts = plain.split("|");
|
|
50
|
+
if (parts.length < 2) return undefined;
|
|
51
|
+
if (parts[0] !== PROTOCOL_VERSION) return undefined;
|
|
52
|
+
let target = parts[1];
|
|
53
|
+
let flags: ConnectionFlags = { clientLZ4: false, serverLZ4: false };
|
|
54
|
+
for (let i = 2; i < parts.length; i++) {
|
|
55
|
+
let [k, v] = parts[i].split("=");
|
|
56
|
+
if (k === "clz4") flags.clientLZ4 = v === "1";
|
|
57
|
+
else if (k === "slz4") flags.serverLZ4 = v === "1";
|
|
58
|
+
}
|
|
59
|
+
return { target, flags };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build the list of subprotocol values the client wants to propose. We enumerate
|
|
63
|
+
// every flag combination the client accepts — the server will pick whichever
|
|
64
|
+
// one it can serve (matching its own flag support). Caller is responsible for
|
|
65
|
+
// not proposing flags it can't handle.
|
|
66
|
+
//
|
|
67
|
+
// `target` is the nodeId the client wants to reach. Pass undefined for browser
|
|
68
|
+
// clients (or any context where the client doesn't know the server's nodeId,
|
|
69
|
+
// because it's reaching the server through a public DNS name + Let's Encrypt
|
|
70
|
+
// cert). The server then accepts the connection regardless of its identity,
|
|
71
|
+
// while still negotiating flags.
|
|
72
|
+
export function proposeProtocols(target: string | undefined, clientCapabilities: { lz4: boolean }): string[] {
|
|
73
|
+
let out: string[] = [];
|
|
74
|
+
let clientLZ4Options = clientCapabilities.lz4 ? [true, false] : [false];
|
|
75
|
+
let serverLZ4Options = [true, false];
|
|
76
|
+
let encodedTarget = target ?? WILDCARD_TARGET;
|
|
77
|
+
for (let clientLZ4 of clientLZ4Options) {
|
|
78
|
+
for (let serverLZ4 of serverLZ4Options) {
|
|
79
|
+
out.push(encodeOne(encodedTarget, { clientLZ4, serverLZ4 }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Server-side: given the proposed (hex-encoded) subprotocol values from the
|
|
86
|
+
// client, the server's own nodeId, and the server's capabilities — pick the
|
|
87
|
+
// first value matching this server with a flag combo we can support. Returns
|
|
88
|
+
// the chosen hex string verbatim (so the server echoes it back), or undefined
|
|
89
|
+
// to signal no match → reject the handshake.
|
|
90
|
+
//
|
|
91
|
+
// A proposal with target === WILDCARD_TARGET ("") matches any server (used
|
|
92
|
+
// by browsers that don't know our internal nodeId).
|
|
93
|
+
export function chooseProtocol(
|
|
94
|
+
proposed: string[],
|
|
95
|
+
serverNodeId: string,
|
|
96
|
+
serverCapabilities: { lz4: boolean }
|
|
97
|
+
): string | undefined {
|
|
98
|
+
for (let hex of proposed) {
|
|
99
|
+
let decoded = decodeProtocol(hex);
|
|
100
|
+
if (!decoded) continue;
|
|
101
|
+
if (decoded.target !== WILDCARD_TARGET && decoded.target !== serverNodeId) continue;
|
|
102
|
+
// Server capability check: if the proposal asks the server to receive
|
|
103
|
+
// LZ4 (slz4=1) but the server doesn't support it, skip.
|
|
104
|
+
if (decoded.flags.serverLZ4 && !serverCapabilities.lz4) continue;
|
|
105
|
+
return hex;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
package/src/webSocketServer.ts
CHANGED
|
@@ -3,6 +3,7 @@ import http from "http";
|
|
|
3
3
|
import net from "net";
|
|
4
4
|
import tls from "tls";
|
|
5
5
|
import { getNodeIdsFromRequest, httpCallHandler } from "./callHTTPHandler";
|
|
6
|
+
import { chooseProtocol, decodeProtocol } from "./protocolNegotiation";
|
|
6
7
|
import { SocketFunction } from "../SocketFunction";
|
|
7
8
|
import { getTrustedCertificates, watchTrustedCertificates } from "./certStore";
|
|
8
9
|
import { createCallFactory } from "./CallFactory";
|
|
@@ -63,6 +64,26 @@ export async function startSocketServer(
|
|
|
63
64
|
|
|
64
65
|
const webSocketServer = new ws.Server({
|
|
65
66
|
noServer: true,
|
|
67
|
+
// Negotiate connection-level flags via Sec-WebSocket-Protocol. The
|
|
68
|
+
// client proposes hex-encoded values that include the target nodeId;
|
|
69
|
+
// we accept only those whose target matches OUR identity
|
|
70
|
+
// (SocketFunction.mountedNodeId — not the address the client used to
|
|
71
|
+
// reach us). If none match we return false, which rejects the
|
|
72
|
+
// handshake — exactly the semantics we want (indistinguishable from
|
|
73
|
+
// "node not reachable"). If the client sent no Sec-WebSocket-Protocol
|
|
74
|
+
// at all, this callback isn't invoked and the handshake proceeds as
|
|
75
|
+
// a legacy client.
|
|
76
|
+
handleProtocols: (protocols, request) => {
|
|
77
|
+
const ourNodeId = SocketFunction.mountedNodeId;
|
|
78
|
+
const proposed = Array.from(protocols);
|
|
79
|
+
const chosen = chooseProtocol(proposed, ourNodeId, { lz4: true });
|
|
80
|
+
if (!chosen) {
|
|
81
|
+
const proposedDecoded = proposed.map(p => decodeProtocol(p) ?? `<undecodable: ${p}>`);
|
|
82
|
+
console.log(`Rejecting handshake on ${ourNodeId}: none of the ${proposed.length} proposed protocols target us`, { ourNodeId, proposedDecoded });
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return chosen;
|
|
86
|
+
},
|
|
66
87
|
});
|
|
67
88
|
|
|
68
89
|
async function setupHTTPSServer(watchOptions: Watchable<https.ServerOptions>) {
|
|
@@ -3,7 +3,8 @@ import tls from "tls";
|
|
|
3
3
|
import { SenderInterface } from "./CallFactory";
|
|
4
4
|
import type * as ws from "ws";
|
|
5
5
|
export declare function getTLSSocket(webSocket: ws.WebSocket): tls.TLSSocket;
|
|
6
|
+
export type WebsocketFactory = (nodeId: string, proposedProtocols?: string[]) => SenderInterface;
|
|
6
7
|
/** NOTE: We create a factory, which embeds the key/cert information. Otherwise retries might use
|
|
7
8
|
* a different key/cert context.
|
|
8
9
|
*/
|
|
9
|
-
export declare function createWebsocketFactory():
|
|
10
|
+
export declare function createWebsocketFactory(): WebsocketFactory;
|
package/src/websocketFactory.ts
CHANGED
|
@@ -1,52 +1,56 @@
|
|
|
1
|
-
import tls from "tls";
|
|
2
|
-
import { isNode } from "./misc";
|
|
3
|
-
import { SenderInterface } from "./CallFactory";
|
|
4
|
-
import { getTrustedCertificates } from "./certStore";
|
|
5
|
-
import { getNodeIdLocation } from "./nodeCache";
|
|
6
|
-
import debugbreak from "debugbreak";
|
|
7
|
-
import { SocketFunction } from "../SocketFunction";
|
|
8
|
-
import type * as ws from "ws";
|
|
9
|
-
|
|
10
|
-
export function getTLSSocket(webSocket: ws.WebSocket) {
|
|
11
|
-
return (webSocket as any)._socket as tls.TLSSocket;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
import tls from "tls";
|
|
2
|
+
import { isNode } from "./misc";
|
|
3
|
+
import { SenderInterface } from "./CallFactory";
|
|
4
|
+
import { getTrustedCertificates } from "./certStore";
|
|
5
|
+
import { getNodeIdLocation } from "./nodeCache";
|
|
6
|
+
import debugbreak from "debugbreak";
|
|
7
|
+
import { SocketFunction } from "../SocketFunction";
|
|
8
|
+
import type * as ws from "ws";
|
|
9
|
+
|
|
10
|
+
export function getTLSSocket(webSocket: ws.WebSocket) {
|
|
11
|
+
return (webSocket as any)._socket as tls.TLSSocket;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type WebsocketFactory = (nodeId: string, proposedProtocols?: string[]) => SenderInterface;
|
|
15
|
+
|
|
16
|
+
/** NOTE: We create a factory, which embeds the key/cert information. Otherwise retries might use
|
|
17
|
+
* a different key/cert context.
|
|
18
|
+
*/
|
|
19
|
+
export function createWebsocketFactory(): WebsocketFactory {
|
|
20
|
+
|
|
21
|
+
if (!isNode()) {
|
|
22
|
+
return (nodeId: string, proposedProtocols?: string[]) => {
|
|
23
|
+
let location = getNodeIdLocation(nodeId);
|
|
24
|
+
if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
|
|
25
|
+
let { address, port } = location;
|
|
26
|
+
|
|
27
|
+
if (!SocketFunction.silent) {
|
|
28
|
+
console.log(`Connecting to ${address}:${port}`);
|
|
29
|
+
}
|
|
30
|
+
if (proposedProtocols && proposedProtocols.length > 0) {
|
|
31
|
+
return new WebSocket(`wss://${address}:${port}`, proposedProtocols);
|
|
32
|
+
}
|
|
33
|
+
return new WebSocket(`wss://${address}:${port}`);
|
|
34
|
+
};
|
|
35
|
+
} else {
|
|
36
|
+
return (nodeId: string, proposedProtocols?: string[]) => {
|
|
37
|
+
let location = getNodeIdLocation(nodeId);
|
|
38
|
+
if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
|
|
39
|
+
let { address, port } = location;
|
|
40
|
+
|
|
41
|
+
if (!SocketFunction.silent) {
|
|
42
|
+
console.log(`Connecting to ${address}:${port}`);
|
|
43
|
+
}
|
|
44
|
+
const ws = require("ws") as typeof import("ws");
|
|
45
|
+
let webSocket = new ws.WebSocket(`wss://${address}:${port}`, proposedProtocols, {
|
|
46
|
+
ca: getTrustedCertificates(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// NOTE: Little setup is done here, because Sometimes websockets are created here,
|
|
50
|
+
// and sometimes via incoming connections, We should do most setup in
|
|
51
|
+
// CallFactory.ts:initializeWebsocket
|
|
52
|
+
|
|
53
|
+
return webSocket;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|