socket-function 0.8.35 → 0.8.37
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.ts +114 -54
- package/SocketFunctionTypes.ts +15 -28
- package/hot/HotReloadController.ts +2 -2
- package/package.json +5 -6
- package/require/CSSShim.ts +1 -0
- package/require/RequireController.ts +4 -3
- package/require/compileFlags.ts +1 -0
- package/src/CallFactory.ts +96 -128
- package/src/caching.ts +4 -0
- package/src/callManager.ts +19 -8
- package/src/misc.ts +3 -0
- package/src/nodeCache.ts +16 -3
- package/src/webSocketServer.ts +69 -14
- package/src/{nodeAuthentication.ts → websocketFactory.ts} +1 -0
- package/test/client.ts +2 -2
- package/test/shared.ts +3 -9
package/SocketFunction.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference path="./require/RequireController.ts" />
|
|
2
|
+
|
|
3
|
+
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType } from "./SocketFunctionTypes";
|
|
2
4
|
import { exposeClass, registerClass, registerGlobalClientHook, registerGlobalHook, runClientHooks } from "./src/callManager";
|
|
3
5
|
import { SocketServerConfig, startSocketServer } from "./src/webSocketServer";
|
|
4
|
-
import {
|
|
6
|
+
import { getCallFactory, getCreateCallFactory, getNodeId, getNodeIdLocation } from "./src/nodeCache";
|
|
5
7
|
import { getCallProxy } from "./src/nodeProxy";
|
|
6
|
-
import { Args } from "./src/types";
|
|
8
|
+
import { Args, MaybePromise } from "./src/types";
|
|
7
9
|
import { setDefaultHTTPCall } from "./src/callHTTPHandler";
|
|
10
|
+
import debugbreak from "debugbreak";
|
|
11
|
+
import { lazy } from "./src/caching";
|
|
8
12
|
|
|
9
13
|
module.allowclient = true;
|
|
10
14
|
|
|
@@ -29,27 +33,46 @@ export class SocketFunction {
|
|
|
29
33
|
};
|
|
30
34
|
public static httpETagCache = false;
|
|
31
35
|
|
|
36
|
+
private static onMountCallbacks = new Map<string, (() => MaybePromise<void>)[]>();
|
|
37
|
+
public static exposedClasses = new Set<string>();
|
|
38
|
+
|
|
39
|
+
public static callerContext: CallerContext | undefined;
|
|
40
|
+
public static getCaller(): CallerContext {
|
|
41
|
+
const caller = SocketFunction.callerContext;
|
|
42
|
+
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
43
|
+
return caller;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
47
|
+
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
32
48
|
public static register<
|
|
33
49
|
ClassInstance extends object,
|
|
34
|
-
Shape extends SocketExposedShape<SocketExposedInterface
|
|
35
|
-
CallContext extends CallContextType
|
|
50
|
+
Shape extends SocketExposedShape<SocketExposedInterface>,
|
|
36
51
|
>(
|
|
37
52
|
classGuid: string,
|
|
38
53
|
instance: ClassInstance,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
(
|
|
43
|
-
SocketRegistered<ExtractShape<ClassInstance, Shape>, CallContext>
|
|
44
|
-
) {
|
|
45
|
-
|
|
46
|
-
for (let value of Object.values(shape)) {
|
|
47
|
-
if (!value) continue;
|
|
48
|
-
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
49
|
-
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
50
|
-
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
54
|
+
shapeFnc: () => Shape,
|
|
55
|
+
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
56
|
+
onMount?: () => MaybePromise<void>;
|
|
51
57
|
}
|
|
52
|
-
|
|
58
|
+
): SocketRegistered<ExtractShape<ClassInstance, Shape>> {
|
|
59
|
+
let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
|
|
60
|
+
const getShape = lazy(() => {
|
|
61
|
+
let shape = shapeFnc();
|
|
62
|
+
let defaultHooks = getDefaultHooks?.();
|
|
63
|
+
|
|
64
|
+
for (let value of Object.values(shape)) {
|
|
65
|
+
if (!value) continue;
|
|
66
|
+
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
67
|
+
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
68
|
+
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
69
|
+
}
|
|
70
|
+
return shape as any as SocketExposedShape;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
setImmediate(() => {
|
|
74
|
+
registerClass(classGuid, instance as SocketExposedInterface, getShape());
|
|
75
|
+
});
|
|
53
76
|
|
|
54
77
|
let nodeProxy = getCallProxy(classGuid, async (call) => {
|
|
55
78
|
let nodeId = call.nodeId;
|
|
@@ -59,9 +82,9 @@ export class SocketFunction {
|
|
|
59
82
|
console.log(`START\t\t\t${classGuid}.${functionName}`);
|
|
60
83
|
}
|
|
61
84
|
try {
|
|
62
|
-
let callFactory = await
|
|
85
|
+
let callFactory = await getCreateCallFactory(nodeId, SocketFunction.mountedNodeId);
|
|
63
86
|
|
|
64
|
-
let shapeObj =
|
|
87
|
+
let shapeObj = getShape()[functionName];
|
|
65
88
|
if (!shapeObj) {
|
|
66
89
|
throw new Error(`Function ${functionName} is not in shape`);
|
|
67
90
|
}
|
|
@@ -72,20 +95,6 @@ export class SocketFunction {
|
|
|
72
95
|
return hookResult.overrideResult;
|
|
73
96
|
}
|
|
74
97
|
|
|
75
|
-
if (hookResult.callTimeout !== undefined) {
|
|
76
|
-
let timeout = hookResult.callTimeout;
|
|
77
|
-
let time = Date.now();
|
|
78
|
-
let timeoutPromise = new Promise((resolve, reject) => {
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
reject(new Error(`Call timed out after ${Date.now() - time}ms`));
|
|
81
|
-
}, timeout);
|
|
82
|
-
});
|
|
83
|
-
return await Promise.race([
|
|
84
|
-
callFactory.performCall(call),
|
|
85
|
-
timeoutPromise,
|
|
86
|
-
]);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
98
|
return await callFactory.performCall(call);
|
|
90
99
|
} finally {
|
|
91
100
|
time = Date.now() - time;
|
|
@@ -96,14 +105,59 @@ export class SocketFunction {
|
|
|
96
105
|
});
|
|
97
106
|
|
|
98
107
|
let output: SocketRegistered = {
|
|
99
|
-
context: curSocketContext,
|
|
100
108
|
nodes: nodeProxy,
|
|
101
109
|
_classGuid: classGuid,
|
|
102
110
|
};
|
|
103
111
|
|
|
112
|
+
setImmediate(() => {
|
|
113
|
+
let onMount = getDefaultHooks?.().onMount;
|
|
114
|
+
if (onMount) {
|
|
115
|
+
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
116
|
+
if (!callbacks) {
|
|
117
|
+
callbacks = [];
|
|
118
|
+
SocketFunction.onMountCallbacks.set(classGuid, callbacks);
|
|
119
|
+
}
|
|
120
|
+
callbacks.push(onMount);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
104
124
|
return output as any;
|
|
105
125
|
}
|
|
106
126
|
|
|
127
|
+
public static onNextDisconnect(nodeId: string, callback: () => void) {
|
|
128
|
+
(async () => {
|
|
129
|
+
let factory = await getCallFactory(nodeId);
|
|
130
|
+
if (!factory) {
|
|
131
|
+
callback();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
factory.onNextDisconnect(callback);
|
|
136
|
+
})().catch(() => {
|
|
137
|
+
callback();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
public static getLastDisconnectTime(nodeId: string): number | undefined {
|
|
141
|
+
let factory = getCallFactory(nodeId);
|
|
142
|
+
if (!factory) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
if (factory instanceof Promise) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return factory.lastClosed;
|
|
149
|
+
}
|
|
150
|
+
public static isNodeConnected(nodeId: string): boolean {
|
|
151
|
+
let factory = getCallFactory(nodeId);
|
|
152
|
+
if (!factory) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (factory instanceof Promise) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return !!factory.isConnected;
|
|
159
|
+
}
|
|
160
|
+
|
|
107
161
|
/** NOTE: Only works if the nodeIs used is from SocketFunction.connect (we can't convert arbitrary nodeIds into urls,
|
|
108
162
|
* as we have no way of knowing how to contain a nodeId).
|
|
109
163
|
* */
|
|
@@ -125,15 +179,33 @@ export class SocketFunction {
|
|
|
125
179
|
*/
|
|
126
180
|
public static expose(socketRegistered: SocketRegistered) {
|
|
127
181
|
exposeClass(socketRegistered);
|
|
182
|
+
SocketFunction.exposedClasses.add(socketRegistered._classGuid);
|
|
183
|
+
|
|
184
|
+
if (this.hasMounted) {
|
|
185
|
+
let mountCallbacks = SocketFunction.onMountCallbacks.get(socketRegistered._classGuid);
|
|
186
|
+
for (let onMount of mountCallbacks || []) {
|
|
187
|
+
Promise.resolve(onMount()).catch(e => {
|
|
188
|
+
console.error("Error in onMount callback exposed after mount", e);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
128
192
|
}
|
|
129
193
|
|
|
130
194
|
public static mountedNodeId: string = "NOTMOUNTED";
|
|
131
|
-
|
|
195
|
+
private static hasMounted = false;
|
|
196
|
+
public static async mount(config: SocketServerConfig) {
|
|
132
197
|
if (this.mountedNodeId !== "NOTMOUNTED") {
|
|
133
198
|
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
134
199
|
}
|
|
135
|
-
this.mountedNodeId = config
|
|
136
|
-
|
|
200
|
+
this.mountedNodeId = await startSocketServer(config);
|
|
201
|
+
this.hasMounted = true;
|
|
202
|
+
for (let classGuid of SocketFunction.exposedClasses) {
|
|
203
|
+
let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
|
|
204
|
+
if (!callbacks) continue;
|
|
205
|
+
for (let callback of callbacks) {
|
|
206
|
+
await callback();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
137
209
|
return this.mountedNodeId;
|
|
138
210
|
}
|
|
139
211
|
|
|
@@ -157,41 +229,29 @@ export class SocketFunction {
|
|
|
157
229
|
return getNodeId(location.address, location.port);
|
|
158
230
|
}
|
|
159
231
|
|
|
160
|
-
public static addGlobalHook
|
|
232
|
+
public static addGlobalHook(hook: SocketFunctionHook<SocketExposedInterface>) {
|
|
161
233
|
registerGlobalHook(hook as SocketFunctionHook);
|
|
162
234
|
}
|
|
163
|
-
public static addGlobalClientHook
|
|
235
|
+
public static addGlobalClientHook(hook: SocketFunctionClientHook<SocketExposedInterface>) {
|
|
164
236
|
registerGlobalClientHook(hook as SocketFunctionClientHook);
|
|
165
237
|
}
|
|
166
238
|
}
|
|
167
239
|
|
|
168
240
|
|
|
169
|
-
const curSocketContext: SocketRegistered["context"] = {
|
|
170
|
-
curContext: undefined,
|
|
171
|
-
caller: undefined,
|
|
172
|
-
getCaller() {
|
|
173
|
-
const caller = curSocketContext.caller;
|
|
174
|
-
if (!caller) throw new Error(`Tried to access caller when not in the synchronous phase of a function call`);
|
|
175
|
-
return caller;
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
241
|
let socketContextSeqNum = 1;
|
|
179
242
|
|
|
180
243
|
export function _setSocketContext<T>(
|
|
181
|
-
callContext: CallContextType,
|
|
182
244
|
caller: CallerContext,
|
|
183
245
|
code: () => T,
|
|
184
246
|
) {
|
|
185
247
|
socketContextSeqNum++;
|
|
186
248
|
let seqNum = socketContextSeqNum;
|
|
187
|
-
|
|
188
|
-
curSocketContext.caller = caller;
|
|
249
|
+
SocketFunction.callerContext = caller;
|
|
189
250
|
try {
|
|
190
251
|
return code();
|
|
191
252
|
} finally {
|
|
192
253
|
if (seqNum === socketContextSeqNum) {
|
|
193
|
-
|
|
194
|
-
curSocketContext.caller = undefined;
|
|
254
|
+
SocketFunction.callerContext = undefined;
|
|
195
255
|
}
|
|
196
256
|
}
|
|
197
257
|
}
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
/// <reference path="./require/RequireController.ts" />
|
|
2
|
+
|
|
1
3
|
module.allowclient = true;
|
|
2
4
|
|
|
5
|
+
import { SocketFunction } from "./SocketFunction";
|
|
3
6
|
import { getCallObj } from "./src/nodeProxy";
|
|
4
7
|
import { Args, MaybePromise } from "./src/types";
|
|
5
8
|
|
|
@@ -19,14 +22,14 @@ export type SocketExposedInterfaceClass = {
|
|
|
19
22
|
new(): unknown;
|
|
20
23
|
prototype: unknown;
|
|
21
24
|
};
|
|
22
|
-
export interface SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
25
|
+
export interface SocketExposedShape<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
23
26
|
[functionName: string]: {
|
|
24
27
|
/** Indicates with the same input, we give the same output, forever,
|
|
25
28
|
* independent of code changes. This only works for data storage.
|
|
26
29
|
*/
|
|
27
30
|
dataImmutable?: boolean;
|
|
28
|
-
hooks?: SocketFunctionHook<ExposedType
|
|
29
|
-
clientHooks?: SocketFunctionClientHook<ExposedType
|
|
31
|
+
hooks?: SocketFunctionHook<ExposedType>[];
|
|
32
|
+
clientHooks?: SocketFunctionClientHook<ExposedType>[];
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -34,42 +37,32 @@ export interface CallType {
|
|
|
34
37
|
classGuid: string;
|
|
35
38
|
functionName: string;
|
|
36
39
|
args: unknown[];
|
|
37
|
-
// NOTE: When making calls this needs to be set in the client hook.
|
|
38
|
-
// To set a timeout on returns, you can set it in the server hook.
|
|
39
|
-
reconnectTimeout?: number;
|
|
40
40
|
}
|
|
41
41
|
export interface FullCallType extends CallType {
|
|
42
42
|
nodeId: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
46
|
-
(config: HookContext<ExposedType
|
|
45
|
+
export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
46
|
+
(config: HookContext<ExposedType>): MaybePromise<void>;
|
|
47
|
+
/** NOTE: This is useful when we need a clientside hook to set up state specifically for our serverside hook. */
|
|
48
|
+
clientHook?: SocketFunctionClientHook<ExposedType>;
|
|
47
49
|
}
|
|
48
|
-
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
50
|
+
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
49
51
|
call: FullCallType;
|
|
50
|
-
context: SocketRegistered<ExposedType, CallContext>["context"];
|
|
51
52
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
52
53
|
overrideResult?: unknown;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
56
|
+
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface> = {
|
|
56
57
|
call: FullCallType;
|
|
57
|
-
/** If the calls takes longer than this (for ANY reason), we return with an error.
|
|
58
|
-
* - Different from reconnectTimeout, which only errors if we lose the connection.
|
|
59
|
-
*/
|
|
60
|
-
callTimeout?: number;
|
|
61
58
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
62
59
|
overrideResult?: unknown;
|
|
63
60
|
};
|
|
64
|
-
export interface SocketFunctionClientHook<ExposedType extends SocketExposedInterface = SocketExposedInterface
|
|
65
|
-
(config: ClientHookContext<ExposedType
|
|
61
|
+
export interface SocketFunctionClientHook<ExposedType extends SocketExposedInterface = SocketExposedInterface> {
|
|
62
|
+
(config: ClientHookContext<ExposedType>): MaybePromise<void>;
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
export
|
|
69
|
-
[key: string]: unknown;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export interface SocketRegistered<ExposedType = any, DynamicCallContext extends CallContextType = CallContextType> {
|
|
65
|
+
export interface SocketRegistered<ExposedType = any> {
|
|
73
66
|
nodes: {
|
|
74
67
|
// NOTE: Don't pass around nodeId to other nodes, instead pass around NetworkLocation (which they
|
|
75
68
|
// then turn into a nodeId, which they can then check permissions on themself).
|
|
@@ -79,12 +72,6 @@ export interface SocketRegistered<ExposedType = any, DynamicCallContext extends
|
|
|
79
72
|
}
|
|
80
73
|
};
|
|
81
74
|
};
|
|
82
|
-
context: {
|
|
83
|
-
// If undefined we are not synchronously in a call
|
|
84
|
-
curContext: DynamicCallContext | undefined;
|
|
85
|
-
caller: CallerContext | undefined;
|
|
86
|
-
getCaller(): CallerContext;
|
|
87
|
-
};
|
|
88
75
|
_classGuid: string;
|
|
89
76
|
}
|
|
90
77
|
export type CallerContext = Readonly<CallerContextBase>;
|
|
@@ -64,8 +64,8 @@ class HotReloadControllerBase {
|
|
|
64
64
|
export const HotReloadController = SocketFunction.register(
|
|
65
65
|
"HotReloadController-032b2250-3aac-4187-8c95-75412742b8f5",
|
|
66
66
|
new HotReloadControllerBase(),
|
|
67
|
-
{
|
|
67
|
+
() => ({
|
|
68
68
|
watchFiles: {},
|
|
69
69
|
fileUpdated: {}
|
|
70
|
-
}
|
|
70
|
+
})
|
|
71
71
|
);
|
package/package.json
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.37",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@types/cookie": "^0.5.1",
|
|
9
|
-
"@types/node": "^18.0.0",
|
|
10
|
-
"@types/node-forge": "^1.3.1",
|
|
11
|
-
"@types/ws": "^8.5.3",
|
|
12
8
|
"cookie": "^0.5.0",
|
|
13
9
|
"mobx": "^6.6.2",
|
|
14
10
|
"node-forge": "https://github.com/sliftist/forge#name",
|
|
@@ -21,7 +17,10 @@
|
|
|
21
17
|
"type": "yarn tsc --noEmit"
|
|
22
18
|
},
|
|
23
19
|
"devDependencies": {
|
|
20
|
+
"@types/cookie": "^0.5.1",
|
|
21
|
+
"@types/node-forge": "^1.3.1",
|
|
22
|
+
"@types/ws": "^8.5.3",
|
|
24
23
|
"debugbreak": "^0.6.5",
|
|
25
|
-
"typedev": "^0.1.
|
|
24
|
+
"typedev": "^0.1.1"
|
|
26
25
|
}
|
|
27
26
|
}
|
package/require/CSSShim.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference path="../../typenode/index.d.ts" />
|
|
1
2
|
import debugbreak from "debugbreak";
|
|
2
3
|
import fs from "fs";
|
|
3
4
|
import { SocketFunction } from "../SocketFunction";
|
|
@@ -9,7 +10,7 @@ module.allowclient = true;
|
|
|
9
10
|
declare global {
|
|
10
11
|
namespace NodeJS {
|
|
11
12
|
interface Module {
|
|
12
|
-
/**
|
|
13
|
+
/** Indicates the module is allowed clientside. */
|
|
13
14
|
allowclient?: boolean;
|
|
14
15
|
|
|
15
16
|
/** Indicates the module is definitely not allowed clientside */
|
|
@@ -237,10 +238,10 @@ export function setRequireBootRequire(path: string) {
|
|
|
237
238
|
export const RequireController = SocketFunction.register(
|
|
238
239
|
"RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d",
|
|
239
240
|
baseController,
|
|
240
|
-
{
|
|
241
|
+
() => ({
|
|
241
242
|
getModules: {},
|
|
242
243
|
requireHTML: {},
|
|
243
244
|
bufferJS: {},
|
|
244
245
|
requireJS: {},
|
|
245
|
-
}
|
|
246
|
+
})
|
|
246
247
|
);
|
package/require/compileFlags.ts
CHANGED
package/src/CallFactory.ts
CHANGED
|
@@ -2,13 +2,15 @@ import { CallerContext, CallerContextBase, CallType, FullCallType } from "../Soc
|
|
|
2
2
|
import * as ws from "ws";
|
|
3
3
|
import { performLocalCall } from "./callManager";
|
|
4
4
|
import { convertErrorStackToError, formatNumberSuffixed, isNode } from "./misc";
|
|
5
|
-
import { createWebsocketFactory, getTLSSocket } from "./
|
|
5
|
+
import { createWebsocketFactory, getTLSSocket } from "./websocketFactory";
|
|
6
6
|
import { SocketFunction } from "../SocketFunction";
|
|
7
7
|
import { gzip } from "zlib";
|
|
8
8
|
import * as tls from "tls";
|
|
9
9
|
import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCache";
|
|
10
|
+
import debugbreak from "debugbreak";
|
|
11
|
+
import { lazy } from "./caching";
|
|
10
12
|
|
|
11
|
-
const
|
|
13
|
+
const MIN_RETRY_DELAY = 1000;
|
|
12
14
|
|
|
13
15
|
type InternalCallType = FullCallType & {
|
|
14
16
|
seqNum: number;
|
|
@@ -28,10 +30,13 @@ type InternalReturnType = {
|
|
|
28
30
|
|
|
29
31
|
export interface CallFactory {
|
|
30
32
|
nodeId: string;
|
|
33
|
+
lastClosed: number;
|
|
34
|
+
closedForever?: boolean;
|
|
35
|
+
isConnected?: boolean;
|
|
31
36
|
// NOTE: May or may not have reconnection or retry logic inside of performCall.
|
|
32
37
|
// Trigger performLocalCall on the other side of the connection
|
|
33
38
|
performCall(call: CallType): Promise<unknown>;
|
|
34
|
-
|
|
39
|
+
onNextDisconnect(callback: () => void): void;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
export interface SenderInterface {
|
|
@@ -45,6 +50,8 @@ export interface SenderInterface {
|
|
|
45
50
|
addEventListener(event: "close", listener: () => void): void;
|
|
46
51
|
addEventListener(event: "error", listener: (err: { message: string }) => void): void;
|
|
47
52
|
addEventListener(event: "message", listener: (data: ws.RawData | ws.MessageEvent | string) => void): void;
|
|
53
|
+
|
|
54
|
+
readyState: number;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
export async function createCallFactory(
|
|
@@ -55,19 +62,15 @@ export async function createCallFactory(
|
|
|
55
62
|
let niceConnectionName = nodeId;
|
|
56
63
|
|
|
57
64
|
const createWebsocket = createWebsocketFactory();
|
|
65
|
+
const registerOnce = lazy(() => registerNodeClient(callFactory));
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
const canReconnect = !!getNodeIdLocation(nodeId);
|
|
60
68
|
|
|
61
69
|
let lastReceivedSeqNum = 0;
|
|
62
70
|
|
|
63
|
-
let reconnectingPromise: Promise<void> | undefined;
|
|
64
|
-
let reconnectAttempts = 0;
|
|
65
|
-
|
|
66
|
-
|
|
67
71
|
let pendingCalls: Map<number, {
|
|
68
72
|
data: Buffer;
|
|
69
73
|
call: InternalCallType;
|
|
70
|
-
reconnectTimeout: number | undefined;
|
|
71
74
|
callback: (resultJSON: InternalReturnType) => void;
|
|
72
75
|
}> = new Map();
|
|
73
76
|
// NOTE: It is important to make this as random as possible, to prevent
|
|
@@ -75,19 +78,23 @@ export async function createCallFactory(
|
|
|
75
78
|
// in return calls.
|
|
76
79
|
let nextSeqNum = Math.random();
|
|
77
80
|
|
|
81
|
+
let lastConnectionAttempt = 0;
|
|
82
|
+
|
|
78
83
|
let callerContext: CallerContextBase = {
|
|
79
84
|
nodeId,
|
|
80
85
|
localNodeId
|
|
81
86
|
};
|
|
82
87
|
|
|
88
|
+
let disconnectCallbacks: (() => void)[] = [];
|
|
89
|
+
function onNextDisconnect(callback: () => void): void {
|
|
90
|
+
disconnectCallbacks.push(callback);
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
let callFactory: CallFactory = {
|
|
84
94
|
nodeId,
|
|
85
|
-
|
|
95
|
+
lastClosed: 0,
|
|
96
|
+
onNextDisconnect,
|
|
86
97
|
async performCall(call: CallType) {
|
|
87
|
-
if (callFactory.closedForever) {
|
|
88
|
-
throw new Error(`Connection lost to ${niceConnectionName}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
98
|
let seqNum = nextSeqNum++;
|
|
92
99
|
let fullCall: InternalCallType = {
|
|
93
100
|
nodeId,
|
|
@@ -111,144 +118,105 @@ export async function createCallFactory(
|
|
|
111
118
|
resolve(result.result);
|
|
112
119
|
}
|
|
113
120
|
};
|
|
114
|
-
pendingCalls.set(seqNum, { callback, data, call: fullCall
|
|
121
|
+
pendingCalls.set(seqNum, { callback, data, call: fullCall });
|
|
115
122
|
});
|
|
116
123
|
|
|
117
|
-
await
|
|
124
|
+
await send(data);
|
|
118
125
|
|
|
119
126
|
return await resultPromise;
|
|
120
127
|
}
|
|
121
128
|
};
|
|
122
129
|
|
|
123
|
-
let
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
webSocket = webSocketBase;
|
|
128
|
-
setupWebsocket(webSocketBase);
|
|
130
|
+
let webSocketPromise: Promise<SenderInterface> | undefined;
|
|
131
|
+
if (webSocketBase) {
|
|
132
|
+
webSocketPromise = Promise.resolve(webSocketBase);
|
|
133
|
+
await initializeWebsocket(webSocketBase);
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
async function sendWithRetry(reconnectTimeout: number | undefined, data: Buffer) {
|
|
134
|
-
if (!retriesEnabled) {
|
|
135
|
-
webSocket.send(data);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
136
|
+
async function initializeWebsocket(newWebSocket: SenderInterface) {
|
|
137
|
+
registerOnce();
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
new Promise<SenderInterface>(resolve =>
|
|
145
|
-
setTimeout(() => {
|
|
146
|
-
retriesEnabled = false;
|
|
147
|
-
resolve(webSocket);
|
|
148
|
-
}, reconnectTimeout)
|
|
149
|
-
)
|
|
150
|
-
]);
|
|
151
|
-
} else {
|
|
152
|
-
await reconnectingPromise;
|
|
153
|
-
}
|
|
139
|
+
function onClose(error: string) {
|
|
140
|
+
callFactory.lastClosed = Date.now();
|
|
141
|
+
webSocketPromise = undefined;
|
|
142
|
+
if (!canReconnect) {
|
|
143
|
+
callFactory.closedForever = true;
|
|
154
144
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
145
|
+
for (let [key, call] of pendingCalls) {
|
|
146
|
+
pendingCalls.delete(key);
|
|
147
|
+
call.callback({
|
|
148
|
+
isReturn: true,
|
|
149
|
+
result: undefined,
|
|
150
|
+
error: error,
|
|
151
|
+
seqNum: call.call.seqNum,
|
|
152
|
+
resultSize: 0,
|
|
153
|
+
compressed: false,
|
|
154
|
+
});
|
|
159
155
|
}
|
|
160
156
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
let callbacks = disconnectCallbacks;
|
|
158
|
+
disconnectCallbacks = [];
|
|
159
|
+
for (let callback of callbacks) {
|
|
160
|
+
try {
|
|
161
|
+
callback();
|
|
162
|
+
} catch { }
|
|
167
163
|
}
|
|
168
164
|
}
|
|
169
|
-
}
|
|
170
|
-
function tryToReconnect(): Promise<void> {
|
|
171
|
-
if (reconnectingPromise) return reconnectingPromise;
|
|
172
|
-
return reconnectingPromise = (async () => {
|
|
173
|
-
while (true) {
|
|
174
|
-
if (!retriesEnabled) {
|
|
175
|
-
callFactory.closedForever = true;
|
|
176
|
-
console.log(`Cannot reconnect to ${niceConnectionName}, aborting pendingCalls: ${pendingCalls.size}`);
|
|
177
|
-
for (let call of pendingCalls.values()) {
|
|
178
|
-
call.callback({
|
|
179
|
-
isReturn: true,
|
|
180
|
-
result: undefined,
|
|
181
|
-
error: `Connection lost to ${niceConnectionName}`,
|
|
182
|
-
seqNum: call.call.seqNum,
|
|
183
|
-
resultSize: 0,
|
|
184
|
-
compressed: false,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
let newWebSocket = createWebsocket(nodeId);
|
|
191
|
-
|
|
192
|
-
let connectError = await new Promise<string | undefined>(resolve => {
|
|
193
|
-
newWebSocket.addEventListener("open", () => {
|
|
194
|
-
resolve(undefined);
|
|
195
|
-
});
|
|
196
|
-
newWebSocket.addEventListener("close", () => {
|
|
197
|
-
resolve("Connection closed for non-error reason?");
|
|
198
|
-
});
|
|
199
|
-
newWebSocket.addEventListener("error", e => {
|
|
200
|
-
resolve(String(e.message));
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
165
|
|
|
204
|
-
|
|
166
|
+
newWebSocket.addEventListener("error", e => {
|
|
167
|
+
// NOTE: No more logging, as we throw, so the caller should be logging the
|
|
168
|
+
// error (or swallowing it, if that is what it wants to do).
|
|
169
|
+
//console.log(`Websocket error for ${niceConnectionName}`, e.message);
|
|
170
|
+
onClose(`Connection error for ${niceConnectionName}: ${e.message}`);
|
|
171
|
+
});
|
|
205
172
|
|
|
206
|
-
|
|
207
|
-
|
|
173
|
+
newWebSocket.addEventListener("close", async () => {
|
|
174
|
+
//console.log(`Websocket closed ${niceConnectionName}`);
|
|
175
|
+
onClose(`Connection closed to ${niceConnectionName}`);
|
|
176
|
+
});
|
|
208
177
|
|
|
209
|
-
|
|
210
|
-
//reconnectAttempts = 0;
|
|
211
|
-
reconnectingPromise = undefined;
|
|
178
|
+
newWebSocket.addEventListener("message", onMessage);
|
|
212
179
|
|
|
213
|
-
webSocket = newWebSocket;
|
|
214
180
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
181
|
+
if (newWebSocket.readyState === 0 /* CONNECTING */) {
|
|
182
|
+
await new Promise<void>(resolve => {
|
|
183
|
+
newWebSocket.addEventListener("open", () => {
|
|
184
|
+
console.log(`Connection established to ${niceConnectionName}`);
|
|
185
|
+
callFactory.isConnected = true;
|
|
186
|
+
resolve();
|
|
187
|
+
});
|
|
188
|
+
newWebSocket.addEventListener("close", () => resolve());
|
|
189
|
+
newWebSocket.addEventListener("error", () => resolve());
|
|
190
|
+
});
|
|
191
|
+
} else if (newWebSocket.readyState !== 1 /* OPEN */) {
|
|
192
|
+
onClose(`Websocket received in closed state`);
|
|
193
|
+
callFactory.isConnected = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
229
196
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
197
|
+
async function send(data: Buffer) {
|
|
198
|
+
if (!webSocketPromise) {
|
|
199
|
+
if (canReconnect) {
|
|
200
|
+
webSocketPromise = tryToReconnect();
|
|
201
|
+
} else {
|
|
202
|
+
throw new Error(`Cannot send data to ${niceConnectionName} as the connection has closed`);
|
|
233
203
|
}
|
|
234
|
-
}
|
|
204
|
+
}
|
|
205
|
+
let webSocket = await webSocketPromise;
|
|
206
|
+
webSocket.send(data);
|
|
235
207
|
}
|
|
208
|
+
async function tryToReconnect(): Promise<SenderInterface> {
|
|
209
|
+
// Don't try to reconnect too often!
|
|
210
|
+
let timeSinceLastAttempt = Date.now() - lastConnectionAttempt;
|
|
211
|
+
if (timeSinceLastAttempt < MIN_RETRY_DELAY) {
|
|
212
|
+
await new Promise(r => setTimeout(r, MIN_RETRY_DELAY - timeSinceLastAttempt));
|
|
213
|
+
}
|
|
214
|
+
lastConnectionAttempt = Date.now();
|
|
236
215
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
webSocket.addEventListener("error", e => {
|
|
241
|
-
console.log(`Websocket error for ${niceConnectionName}`, e);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
webSocket.addEventListener("close", async () => {
|
|
245
|
-
console.log(`Websocket closed ${niceConnectionName}`);
|
|
246
|
-
if (retriesEnabled) {
|
|
247
|
-
await tryToReconnect();
|
|
248
|
-
}
|
|
249
|
-
});
|
|
216
|
+
let newWebSocket = createWebsocket(nodeId);
|
|
217
|
+
await initializeWebsocket(newWebSocket);
|
|
250
218
|
|
|
251
|
-
|
|
219
|
+
return newWebSocket;
|
|
252
220
|
}
|
|
253
221
|
|
|
254
222
|
|
|
@@ -328,7 +296,7 @@ export async function createCallFactory(
|
|
|
328
296
|
} else {
|
|
329
297
|
result = Buffer.from(JSON.stringify(response));
|
|
330
298
|
}
|
|
331
|
-
await
|
|
299
|
+
await send(result);
|
|
332
300
|
}
|
|
333
301
|
return;
|
|
334
302
|
}
|
package/src/caching.ts
CHANGED
|
@@ -33,6 +33,7 @@ export function cacheEmptyArray<T>(array: T[]): T[] {
|
|
|
33
33
|
|
|
34
34
|
export function cache<Output, Key>(getValue: (key: Key) => Output): {
|
|
35
35
|
(key: Key): Output;
|
|
36
|
+
clear(key: Key): void;
|
|
36
37
|
} {
|
|
37
38
|
let startingCalculating = new Set<Key>();
|
|
38
39
|
let values = new Map<Key, Output>();
|
|
@@ -51,6 +52,9 @@ export function cache<Output, Key>(getValue: (key: Key) => Output): {
|
|
|
51
52
|
values.set(key, value);
|
|
52
53
|
return value;
|
|
53
54
|
}
|
|
55
|
+
cache.clear = (key: Key) => {
|
|
56
|
+
values.delete(key);
|
|
57
|
+
};
|
|
54
58
|
return cache;
|
|
55
59
|
}
|
|
56
60
|
|
package/src/callManager.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CallerContext, CallType, ClientHookContext, FullCallType, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
|
|
2
2
|
import { _setSocketContext } from "../SocketFunction";
|
|
3
|
+
import { isNode } from "./misc";
|
|
4
|
+
import debugbreak from "debugbreak";
|
|
3
5
|
|
|
4
6
|
let classes: {
|
|
5
7
|
[classGuid: string]: {
|
|
@@ -39,14 +41,13 @@ export async function performLocalCall(
|
|
|
39
41
|
throw new Error(`Function ${call.functionName} does not exist`);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
let
|
|
43
|
-
let serverContext = await runServerHooks(call, { caller, curContext, getCaller: () => caller }, functionShape);
|
|
44
|
+
let serverContext = await runServerHooks(call, caller, functionShape);
|
|
44
45
|
if ("overrideResult" in serverContext) {
|
|
45
46
|
return serverContext.overrideResult;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// NOTE: We purposely don't await inside _setSocketContext, so the context is reset synchronously
|
|
49
|
-
let result = _setSocketContext(
|
|
50
|
+
let result = _setSocketContext(caller, () => {
|
|
50
51
|
return controller[call.functionName](...call.args);
|
|
51
52
|
});
|
|
52
53
|
|
|
@@ -96,7 +97,17 @@ export async function runClientHooks(
|
|
|
96
97
|
hooks: SocketExposedShape[""],
|
|
97
98
|
): Promise<ClientHookContext> {
|
|
98
99
|
let context: ClientHookContext = { call: callType };
|
|
99
|
-
|
|
100
|
+
|
|
101
|
+
let clientHooks = (
|
|
102
|
+
globalClientHooks
|
|
103
|
+
.concat(hooks.clientHooks || [])
|
|
104
|
+
);
|
|
105
|
+
for (let otherClientHook of globalHooks.concat(hooks.hooks || []).map(x => x.clientHook)) {
|
|
106
|
+
if (otherClientHook) {
|
|
107
|
+
clientHooks.push(otherClientHook);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (let hook of clientHooks) {
|
|
100
111
|
await hook(context);
|
|
101
112
|
if ("overrideResult" in context) {
|
|
102
113
|
break;
|
|
@@ -107,12 +118,12 @@ export async function runClientHooks(
|
|
|
107
118
|
|
|
108
119
|
async function runServerHooks(
|
|
109
120
|
callType: FullCallType,
|
|
110
|
-
|
|
121
|
+
caller: CallerContext,
|
|
111
122
|
hooks: SocketExposedShape[""],
|
|
112
123
|
): Promise<HookContext> {
|
|
113
|
-
let hookContext: HookContext = { call: callType
|
|
124
|
+
let hookContext: HookContext = { call: callType };
|
|
114
125
|
for (let hook of globalHooks.concat(hooks.hooks || [])) {
|
|
115
|
-
await hook(hookContext);
|
|
126
|
+
await _setSocketContext(caller, () => hook(hookContext));
|
|
116
127
|
if ("overrideResult" in hookContext) {
|
|
117
128
|
break;
|
|
118
129
|
}
|
package/src/misc.ts
CHANGED
package/src/nodeCache.ts
CHANGED
|
@@ -19,11 +19,21 @@ export function getNodeId(domain: string, port: number): string {
|
|
|
19
19
|
|
|
20
20
|
/** A nodeId not available for reconnecting. */
|
|
21
21
|
export function getClientNodeId(address: string): string {
|
|
22
|
-
return `
|
|
22
|
+
return `client:${address}:${Date.now()}:${Math.random()}`;
|
|
23
|
+
}
|
|
24
|
+
/** Will always be available, even if getNodeIdLocation is not (as we don't always have the port,
|
|
25
|
+
* but we should always have an address).
|
|
26
|
+
* - Rarely used, as for logging you can just log the nodeId. ALSO, it isn't sufficient to reconnect, as the port is also needed!
|
|
27
|
+
* */
|
|
28
|
+
export function getNodeIdIP(nodeId: string): string {
|
|
29
|
+
if (nodeId.startsWith("client:")) {
|
|
30
|
+
return nodeId.split(":")[1];
|
|
31
|
+
}
|
|
32
|
+
return getNodeIdLocation(nodeId)!.address;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
export function getNodeIdLocation(nodeId: string): { address: string, port: number; } | undefined {
|
|
26
|
-
if (nodeId.startsWith("
|
|
36
|
+
if (nodeId.startsWith("client:")) {
|
|
27
37
|
return undefined;
|
|
28
38
|
}
|
|
29
39
|
let [address, port] = nodeId.split(":");
|
|
@@ -49,7 +59,7 @@ export function registerNodeClient(callFactory: CallFactory) {
|
|
|
49
59
|
startCleanupLoop();
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
export function
|
|
62
|
+
export function getCreateCallFactory(nodeId: string, mountedNodeId: string): MaybePromise<CallFactory> {
|
|
53
63
|
let callFactory = nodeCache.get(nodeId);
|
|
54
64
|
if (callFactory === undefined) {
|
|
55
65
|
callFactory = createCallFactory(undefined, nodeId, mountedNodeId);
|
|
@@ -57,6 +67,9 @@ export function getCreateCallFactoryLocation(nodeId: string, mountedNodeId: stri
|
|
|
57
67
|
}
|
|
58
68
|
return callFactory;
|
|
59
69
|
}
|
|
70
|
+
export function getCallFactory(nodeId: string): MaybePromise<CallFactory | undefined> {
|
|
71
|
+
return nodeCache.get(nodeId);
|
|
72
|
+
}
|
|
60
73
|
|
|
61
74
|
const startCleanupLoop = lazy(() => {
|
|
62
75
|
(async () => {
|
package/src/webSocketServer.ts
CHANGED
|
@@ -9,15 +9,18 @@ import { getTrustedCertificates, watchTrustedCertificates } from "./certStore";
|
|
|
9
9
|
import { createCallFactory } from "./CallFactory";
|
|
10
10
|
import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
|
|
11
11
|
import debugbreak from "debugbreak";
|
|
12
|
+
import { getNodeId } from "./nodeCache";
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
import { Watchable } from "./misc";
|
|
12
15
|
|
|
13
16
|
export type SocketServerConfig = (
|
|
14
17
|
https.ServerOptions & {
|
|
15
|
-
nodeId?: string;
|
|
16
|
-
|
|
17
18
|
key: string | Buffer;
|
|
18
19
|
cert: string | Buffer;
|
|
19
20
|
|
|
20
21
|
port: number;
|
|
22
|
+
/** You can also set `port: 0` if you don't care what port you want at all. */
|
|
23
|
+
useAvailablePortIfPortInUse?: boolean;
|
|
21
24
|
|
|
22
25
|
// public sets ip to "0.0.0.0", otherwise it defaults to "127.0.0.1", which
|
|
23
26
|
// causes the server to only accept local connections.
|
|
@@ -26,24 +29,38 @@ export type SocketServerConfig = (
|
|
|
26
29
|
|
|
27
30
|
/** If the SNI matches this domain, we use a different key/cert. */
|
|
28
31
|
SNICerts?: {
|
|
29
|
-
[domain: string]: https.ServerOptions
|
|
32
|
+
[domain: string]: Watchable<https.ServerOptions>;
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
35
|
);
|
|
33
36
|
|
|
34
37
|
export async function startSocketServer(
|
|
35
38
|
config: SocketServerConfig
|
|
36
|
-
): Promise<
|
|
39
|
+
): Promise<string> {
|
|
37
40
|
|
|
38
41
|
const webSocketServer = new ws.Server({
|
|
39
42
|
noServer: true,
|
|
40
43
|
});
|
|
41
44
|
|
|
42
|
-
function setupHTTPSServer(
|
|
43
|
-
let
|
|
45
|
+
async function setupHTTPSServer(watchOptions: Watchable<https.ServerOptions>) {
|
|
46
|
+
let httpsServerLast: https.Server | undefined;
|
|
47
|
+
let onHttpServer: (server: https.Server) => void;
|
|
48
|
+
let httpServerPromise = new Promise<https.Server>(r => onHttpServer = r);
|
|
49
|
+
let lastOptions!: https.ServerOptions;
|
|
50
|
+
await watchOptions(value => {
|
|
51
|
+
lastOptions = { ...value, ca: getTrustedCertificates() };
|
|
52
|
+
if (!httpsServerLast) {
|
|
53
|
+
httpsServerLast = https.createServer(lastOptions);
|
|
54
|
+
} else {
|
|
55
|
+
httpsServerLast.setSecureContext(lastOptions);
|
|
56
|
+
}
|
|
57
|
+
onHttpServer(httpsServerLast);
|
|
58
|
+
});
|
|
59
|
+
let httpsServer = await httpServerPromise;
|
|
60
|
+
|
|
44
61
|
watchTrustedCertificates(() => {
|
|
45
|
-
|
|
46
|
-
httpsServer.setSecureContext(
|
|
62
|
+
lastOptions.ca = getTrustedCertificates();
|
|
63
|
+
httpsServer.setSecureContext(lastOptions);
|
|
47
64
|
});
|
|
48
65
|
|
|
49
66
|
httpsServer.on("connection", socket => {
|
|
@@ -97,11 +114,17 @@ export async function startSocketServer(
|
|
|
97
114
|
let options: https.ServerOptions = {
|
|
98
115
|
...config,
|
|
99
116
|
};
|
|
117
|
+
if (!config.cert) {
|
|
118
|
+
throw new Error("No cert specified");
|
|
119
|
+
}
|
|
120
|
+
if (!config.key) {
|
|
121
|
+
throw new Error("No key specified");
|
|
122
|
+
}
|
|
100
123
|
|
|
101
|
-
const mainHTTPSServer = setupHTTPSServer(options);
|
|
124
|
+
const mainHTTPSServer = await setupHTTPSServer(callback => callback(options));
|
|
102
125
|
let sniServers = new Map<string, https.Server>();
|
|
103
126
|
for (let [domain, obj] of Object.entries(config.SNICerts || {})) {
|
|
104
|
-
sniServers.set(domain, setupHTTPSServer(obj));
|
|
127
|
+
sniServers.set(domain, await setupHTTPSServer(obj));
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
let httpServer = http.createServer({}, async function (req, res) {
|
|
@@ -130,6 +153,7 @@ export async function startSocketServer(
|
|
|
130
153
|
} else {
|
|
131
154
|
let data = parseTLSHello(buffer);
|
|
132
155
|
let sni = data.extensions.filter(x => x.type === SNIType).flatMap(x => parseSNIExtension(x.data))[0];
|
|
156
|
+
console.log(`Received TCP connection with SNI ${JSON.stringify(sni)}`);
|
|
133
157
|
server = sniServers.get(sni) || mainHTTPSServer;
|
|
134
158
|
}
|
|
135
159
|
|
|
@@ -158,12 +182,43 @@ export async function startSocketServer(
|
|
|
158
182
|
host = "0.0.0.0";
|
|
159
183
|
}
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
let port = config.port;
|
|
186
|
+
if (config.useAvailablePortIfPortInUse && port) {
|
|
187
|
+
async function isPortInUse(port: number): Promise<boolean> {
|
|
188
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
189
|
+
let server = net.createServer();
|
|
190
|
+
server.listen(port, host)
|
|
191
|
+
.on("listening", function () {
|
|
192
|
+
server.close();
|
|
193
|
+
resolve(false);
|
|
194
|
+
}).on("close", function () {
|
|
195
|
+
resolve(true);
|
|
196
|
+
}).on("error", function (e) {
|
|
197
|
+
resolve(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (await isPortInUse(port)) {
|
|
202
|
+
port = 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`Trying to listening on ${host}:${port}`);
|
|
207
|
+
realServer.listen(port, host);
|
|
163
208
|
|
|
164
209
|
await listenPromise;
|
|
165
210
|
|
|
166
|
-
|
|
211
|
+
port = (realServer.address() as net.AddressInfo).port;
|
|
212
|
+
let nodeId = getNodeId(getCommonName(config.cert), port);
|
|
213
|
+
console.log(`Started Listening on ${nodeId}`);
|
|
214
|
+
|
|
215
|
+
return nodeId;
|
|
216
|
+
}
|
|
167
217
|
|
|
168
|
-
|
|
218
|
+
function getCommonName(cert: Buffer | string) {
|
|
219
|
+
let subject = new crypto.X509Certificate(cert).subject;
|
|
220
|
+
let subjectKVPs = new Map(subject.split(",").map(x => x.trim().split("=")).map(x => [x[0], x.slice(1).join("=")]));
|
|
221
|
+
let commonName = subjectKVPs.get("CN");
|
|
222
|
+
if (!commonName) throw new Error(`No common name in subject: ${subject}`);
|
|
223
|
+
return commonName;
|
|
169
224
|
}
|
|
@@ -4,6 +4,7 @@ import { isNode } from "./misc";
|
|
|
4
4
|
import { SenderInterface } from "./CallFactory";
|
|
5
5
|
import { getTrustedCertificates } from "./certStore";
|
|
6
6
|
import { getNodeIdLocation } from "./nodeCache";
|
|
7
|
+
import debugbreak from "debugbreak";
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
export function getTLSSocket(webSocket: ws.WebSocket) {
|
package/test/client.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference path="../require/RequireController.ts" />
|
|
2
|
+
|
|
1
3
|
// https://letx.ca:2542/?classGuid=RequireController-e2f811f3-14b8-4759-b0d6-73f14516cf1d&functionName=requireHTML&args=[%22./test/test%22]
|
|
2
4
|
|
|
3
5
|
//import typescript from "typescript";
|
|
@@ -24,8 +26,6 @@ void main();
|
|
|
24
26
|
async function main() {
|
|
25
27
|
if (isNode()) return;
|
|
26
28
|
|
|
27
|
-
SocketFunction.rejectUnauthorized = false;
|
|
28
|
-
|
|
29
29
|
SocketFunction.expose(Test);
|
|
30
30
|
|
|
31
31
|
console.log("cool");
|
package/test/shared.ts
CHANGED
|
@@ -41,7 +41,7 @@ class TestBase {
|
|
|
41
41
|
export const Test = SocketFunction.register(
|
|
42
42
|
"80d9f328-72df-4baa-8be8-019c1003d4a2",
|
|
43
43
|
new TestBase(),
|
|
44
|
-
{
|
|
44
|
+
() => ({
|
|
45
45
|
add: {
|
|
46
46
|
// hooks: [
|
|
47
47
|
// async (config) => {
|
|
@@ -49,17 +49,11 @@ export const Test = SocketFunction.register(
|
|
|
49
49
|
// }
|
|
50
50
|
// ]
|
|
51
51
|
},
|
|
52
|
-
callMe: {
|
|
53
|
-
clientHooks: [
|
|
54
|
-
async (config) => {
|
|
55
|
-
config.call.reconnectTimeout = 2000;
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
},
|
|
52
|
+
callMe: {},
|
|
59
53
|
callBack: {
|
|
60
54
|
|
|
61
55
|
},
|
|
62
56
|
//fncNotAsync: {},
|
|
63
57
|
//notAFnc: {},
|
|
64
|
-
}
|
|
58
|
+
})
|
|
65
59
|
);
|