socket-function 0.8.35 → 0.8.36

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 CHANGED
@@ -1,10 +1,14 @@
1
+ /// <reference path="./require/RequireController.ts" />
2
+
1
3
  import { SocketExposedInterface, CallContextType, 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
6
  import { getCreateCallFactoryLocation, 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,6 +33,11 @@ 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
+ // NOTE: We use callbacks we don't run into issues with cyclic dependencies
40
+ // (ex, using a hook in a controller where the hook also calls the controller).
32
41
  public static register<
33
42
  ClassInstance extends object,
34
43
  Shape extends SocketExposedShape<SocketExposedInterface, CallContext>,
@@ -36,20 +45,28 @@ export class SocketFunction {
36
45
  >(
37
46
  classGuid: string,
38
47
  instance: ClassInstance,
39
- shape: Shape,
40
- defaultHooks?: SocketExposedShape[""]
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;
48
+ shapeFnc: () => Shape,
49
+ defaultHooksFnc?: () => SocketExposedShape[""] & {
50
+ onMount?: () => MaybePromise<void>;
51
51
  }
52
- registerClass(classGuid, instance as SocketExposedInterface, shape as any as SocketExposedShape);
52
+ ): SocketRegistered<ExtractShape<ClassInstance, Shape>, CallContext> {
53
+ let getDefaultHooks = defaultHooksFnc && lazy(defaultHooksFnc);
54
+ const getShape = lazy(() => {
55
+ let shape = shapeFnc();
56
+ let defaultHooks = getDefaultHooks?.();
57
+
58
+ for (let value of Object.values(shape)) {
59
+ if (!value) continue;
60
+ value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
61
+ value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
62
+ value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
63
+ }
64
+ return shape as any as SocketExposedShape;
65
+ });
66
+
67
+ setImmediate(() => {
68
+ registerClass(classGuid, instance as SocketExposedInterface, getShape());
69
+ });
53
70
 
54
71
  let nodeProxy = getCallProxy(classGuid, async (call) => {
55
72
  let nodeId = call.nodeId;
@@ -61,7 +78,7 @@ export class SocketFunction {
61
78
  try {
62
79
  let callFactory = await getCreateCallFactoryLocation(nodeId, SocketFunction.mountedNodeId);
63
80
 
64
- let shapeObj = shape[functionName];
81
+ let shapeObj = getShape()[functionName];
65
82
  if (!shapeObj) {
66
83
  throw new Error(`Function ${functionName} is not in shape`);
67
84
  }
@@ -72,20 +89,6 @@ export class SocketFunction {
72
89
  return hookResult.overrideResult;
73
90
  }
74
91
 
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
92
  return await callFactory.performCall(call);
90
93
  } finally {
91
94
  time = Date.now() - time;
@@ -101,6 +104,18 @@ export class SocketFunction {
101
104
  _classGuid: classGuid,
102
105
  };
103
106
 
107
+ setImmediate(() => {
108
+ let onMount = getDefaultHooks?.().onMount;
109
+ if (onMount) {
110
+ let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
111
+ if (!callbacks) {
112
+ callbacks = [];
113
+ SocketFunction.onMountCallbacks.set(classGuid, callbacks);
114
+ }
115
+ callbacks.push(onMount);
116
+ }
117
+ });
118
+
104
119
  return output as any;
105
120
  }
106
121
 
@@ -125,15 +140,33 @@ export class SocketFunction {
125
140
  */
126
141
  public static expose(socketRegistered: SocketRegistered) {
127
142
  exposeClass(socketRegistered);
143
+ SocketFunction.exposedClasses.add(socketRegistered._classGuid);
144
+
145
+ if (this.hasMounted) {
146
+ let mountCallbacks = SocketFunction.onMountCallbacks.get(socketRegistered._classGuid);
147
+ for (let onMount of mountCallbacks || []) {
148
+ Promise.resolve(onMount()).catch(e => {
149
+ console.error("Error in onMount callback exposed after mount", e);
150
+ });
151
+ }
152
+ }
128
153
  }
129
154
 
130
155
  public static mountedNodeId: string = "NOTMOUNTED";
131
- public static async mount(config: SocketServerConfig & { nodeId: string }) {
156
+ private static hasMounted = false;
157
+ public static async mount(config: SocketServerConfig) {
132
158
  if (this.mountedNodeId !== "NOTMOUNTED") {
133
159
  throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
134
160
  }
135
- this.mountedNodeId = config.nodeId;
136
- await startSocketServer(config);
161
+ this.mountedNodeId = await startSocketServer(config);
162
+ this.hasMounted = true;
163
+ for (let classGuid of SocketFunction.exposedClasses) {
164
+ let callbacks = SocketFunction.onMountCallbacks.get(classGuid);
165
+ if (!callbacks) continue;
166
+ for (let callback of callbacks) {
167
+ await callback();
168
+ }
169
+ }
137
170
  return this.mountedNodeId;
138
171
  }
139
172
 
@@ -1,3 +1,5 @@
1
+ /// <reference path="./require/RequireController.ts" />
2
+
1
3
  module.allowclient = true;
2
4
 
3
5
  import { getCallObj } from "./src/nodeProxy";
@@ -34,9 +36,6 @@ export interface CallType {
34
36
  classGuid: string;
35
37
  functionName: string;
36
38
  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
39
  }
41
40
  export interface FullCallType extends CallType {
42
41
  nodeId: string;
@@ -44,6 +43,8 @@ export interface FullCallType extends CallType {
44
43
 
45
44
  export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> {
46
45
  (config: HookContext<ExposedType, CallContext>): MaybePromise<void>;
46
+ /** NOTE: This is useful when we need a clientside hook to set up state specifically for our serverside hook. */
47
+ clientHook?: SocketFunctionClientHook<ExposedType, CallContext>;
47
48
  }
48
49
  export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> = {
49
50
  call: FullCallType;
@@ -54,10 +55,6 @@ export type HookContext<ExposedType extends SocketExposedInterface = SocketExpos
54
55
 
55
56
  export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> = {
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,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.35",
3
+ "version": "0.8.36",
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.0"
24
+ "typedev": "^0.1.1"
26
25
  }
27
26
  }
@@ -1,3 +1,4 @@
1
+ /// <reference path=".//RequireController.ts" />
1
2
  import debugbreak from "debugbreak";
2
3
  import { compileTransformBefore } from "typenode";
3
4
 
@@ -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
- /** Indiciates the module is allowed clientside. */
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
  );
@@ -1,3 +1,4 @@
1
+ /// <reference path="./RequireController.ts" />
1
2
  module.allowclient = true;
2
3
 
3
4
  /**
@@ -2,13 +2,13 @@ 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 "./nodeAuthentication";
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
-
11
- const retryInterval = 2000;
10
+ import debugbreak from "debugbreak";
11
+ import { lazy } from "./caching";
12
12
 
13
13
  type InternalCallType = FullCallType & {
14
14
  seqNum: number;
@@ -45,6 +45,8 @@ export interface SenderInterface {
45
45
  addEventListener(event: "close", listener: () => void): void;
46
46
  addEventListener(event: "error", listener: (err: { message: string }) => void): void;
47
47
  addEventListener(event: "message", listener: (data: ws.RawData | ws.MessageEvent | string) => void): void;
48
+
49
+ readyState: number;
48
50
  }
49
51
 
50
52
  export async function createCallFactory(
@@ -55,19 +57,13 @@ export async function createCallFactory(
55
57
  let niceConnectionName = nodeId;
56
58
 
57
59
  const createWebsocket = createWebsocketFactory();
58
-
59
- let retriesEnabled = !!getNodeIdLocation(nodeId);
60
+ const registerOnce = lazy(() => registerNodeClient(callFactory));
60
61
 
61
62
  let lastReceivedSeqNum = 0;
62
63
 
63
- let reconnectingPromise: Promise<void> | undefined;
64
- let reconnectAttempts = 0;
65
-
66
-
67
64
  let pendingCalls: Map<number, {
68
65
  data: Buffer;
69
66
  call: InternalCallType;
70
- reconnectTimeout: number | undefined;
71
67
  callback: (resultJSON: InternalReturnType) => void;
72
68
  }> = new Map();
73
69
  // NOTE: It is important to make this as random as possible, to prevent
@@ -111,144 +107,78 @@ export async function createCallFactory(
111
107
  resolve(result.result);
112
108
  }
113
109
  };
114
- pendingCalls.set(seqNum, { callback, data, call: fullCall, reconnectTimeout: call.reconnectTimeout });
110
+ pendingCalls.set(seqNum, { callback, data, call: fullCall });
115
111
  });
116
112
 
117
- await sendWithRetry(call.reconnectTimeout, data);
113
+ await send(data);
118
114
 
119
115
  return await resultPromise;
120
116
  }
121
117
  };
122
118
 
123
- let webSocket!: SenderInterface;
124
- if (!webSocketBase) {
125
- await tryToReconnect();
126
- } else {
127
- webSocket = webSocketBase;
128
- setupWebsocket(webSocketBase);
119
+ let webSocketPromise: Promise<SenderInterface> | undefined;
120
+ if (webSocketBase) {
121
+ webSocketPromise = Promise.resolve(webSocketBase);
122
+ await initializeWebsocket(webSocketBase);
129
123
  }
130
124
 
131
- niceConnectionName = `${niceConnectionName} (${callerContext.nodeId})`;
132
-
133
- async function sendWithRetry(reconnectTimeout: number | undefined, data: Buffer) {
134
- if (!retriesEnabled) {
135
- webSocket.send(data);
136
- return;
137
- }
138
-
139
- while (true) {
140
- if (reconnectingPromise) {
141
- if (reconnectTimeout) {
142
- await Promise.race([
143
- reconnectingPromise,
144
- new Promise<SenderInterface>(resolve =>
145
- setTimeout(() => {
146
- retriesEnabled = false;
147
- resolve(webSocket);
148
- }, reconnectTimeout)
149
- )
150
- ]);
151
- } else {
152
- await reconnectingPromise;
153
- }
154
- }
155
-
156
- if (!retriesEnabled) {
157
- webSocket.send(data);
158
- break;
159
- }
160
-
161
- try {
162
- webSocket.send(data);
163
- break;
164
- } catch (e) {
165
- // Ignore errors, as we will catch them synchronously in the next loop.
166
- void (tryToReconnect());
125
+ async function initializeWebsocket(newWebSocket: SenderInterface) {
126
+ registerOnce();
127
+
128
+ function onClose(error: string) {
129
+ webSocketPromise = undefined;
130
+ for (let [key, call] of pendingCalls) {
131
+ pendingCalls.delete(key);
132
+ call.callback({
133
+ isReturn: true,
134
+ result: undefined,
135
+ error: error,
136
+ seqNum: call.call.seqNum,
137
+ resultSize: 0,
138
+ compressed: false,
139
+ });
167
140
  }
168
141
  }
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
-
204
- setupWebsocket(newWebSocket);
205
142
 
206
- if (!connectError) {
207
- console.log(`Reconnected to ${niceConnectionName}`);
143
+ newWebSocket.addEventListener("error", e => {
144
+ // NOTE: No more logging, as we throw, so the caller should be logging the
145
+ // error (or swallowing it, if that is what it wants to do).
146
+ //console.log(`Websocket error for ${niceConnectionName}`, e.message);
147
+ onClose(`Connection error for ${niceConnectionName}: ${e.message}`);
148
+ });
208
149
 
209
- // I'm not sure if we should clear reconnectAttempts? Maybe if we eventually have a max reconnectAttempts?
210
- //reconnectAttempts = 0;
211
- reconnectingPromise = undefined;
150
+ newWebSocket.addEventListener("close", async () => {
151
+ //console.log(`Websocket closed ${niceConnectionName}`);
152
+ onClose(`Connection closed to ${niceConnectionName}`);
153
+ });
212
154
 
213
- webSocket = newWebSocket;
155
+ newWebSocket.addEventListener("message", onMessage);
214
156
 
215
- for (let call of pendingCalls.values()) {
216
- sendWithRetry(call.reconnectTimeout, call.data).catch(e => {
217
- call.callback({
218
- isReturn: true,
219
- result: undefined,
220
- error: String(e),
221
- seqNum: call.call.seqNum,
222
- resultSize: 0,
223
- compressed: false,
224
- });
225
- });
226
- }
227
- return;
228
- }
229
157
 
230
- reconnectAttempts++;
231
- console.error(`Connection retry to ${niceConnectionName} failed (attempt ${reconnectAttempts}), retrying in ${retryInterval}ms, error: ${JSON.stringify(connectError)}`);
232
- await new Promise(resolve => setTimeout(resolve, retryInterval));
233
- }
234
- })();
158
+ if (newWebSocket.readyState === 0 /* CONNECTING */) {
159
+ await new Promise<void>(resolve => {
160
+ newWebSocket.addEventListener("open", () => {
161
+ console.log(`Connection established to ${niceConnectionName}`);
162
+ resolve();
163
+ });
164
+ newWebSocket.addEventListener("close", () => resolve());
165
+ newWebSocket.addEventListener("error", () => resolve());
166
+ });
167
+ } else if (newWebSocket.readyState !== 1 /* OPEN */) {
168
+ onClose(`Websocket received in closed state`);
169
+ }
235
170
  }
236
171
 
237
- function setupWebsocket(webSocket: SenderInterface) {
238
- registerNodeClient(callFactory);
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
- });
172
+ async function send(data: Buffer) {
173
+ webSocketPromise = webSocketPromise || tryToReconnect();
174
+ let webSocket = await webSocketPromise;
175
+ webSocket.send(data);
176
+ }
177
+ async function tryToReconnect(): Promise<SenderInterface> {
178
+ let newWebSocket = createWebsocket(nodeId);
179
+ await initializeWebsocket(newWebSocket);
250
180
 
251
- webSocket.addEventListener("message", onMessage);
181
+ return newWebSocket;
252
182
  }
253
183
 
254
184
 
@@ -328,7 +258,7 @@ export async function createCallFactory(
328
258
  } else {
329
259
  result = Buffer.from(JSON.stringify(response));
330
260
  }
331
- await sendWithRetry(call.reconnectTimeout, result);
261
+ await send(result);
332
262
  }
333
263
  return;
334
264
  }
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
 
@@ -1,5 +1,7 @@
1
1
  import { CallContextType, 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]: {
@@ -96,7 +98,17 @@ export async function runClientHooks(
96
98
  hooks: SocketExposedShape[""],
97
99
  ): Promise<ClientHookContext> {
98
100
  let context: ClientHookContext = { call: callType };
99
- for (let hook of globalClientHooks.concat(hooks.clientHooks || [])) {
101
+
102
+ let clientHooks = (
103
+ globalClientHooks
104
+ .concat(hooks.clientHooks || [])
105
+ );
106
+ for (let otherClientHook of globalHooks.concat(hooks.hooks || []).map(x => x.clientHook)) {
107
+ if (otherClientHook) {
108
+ clientHooks.push(otherClientHook);
109
+ }
110
+ }
111
+ for (let hook of clientHooks) {
100
112
  await hook(context);
101
113
  if ("overrideResult" in context) {
102
114
  break;
package/src/misc.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import * as crypto from "crypto";
2
+ import { MaybePromise } from "./types";
3
+
4
+ export type Watchable<T> = (callback: (value: T) => void) => MaybePromise<void>;
2
5
 
3
6
  export function convertErrorStackToError(error: string): Error {
4
7
  let errorObj = new Error();
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 `client_${address}:${Date.now()}:${Math.random()}`;
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("client_")) {
36
+ if (nodeId.startsWith("client:")) {
27
37
  return undefined;
28
38
  }
29
39
  let [address, port] = nodeId.split(":");
@@ -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<void> {
39
+ ): Promise<string> {
37
40
 
38
41
  const webSocketServer = new ws.Server({
39
42
  noServer: true,
40
43
  });
41
44
 
42
- function setupHTTPSServer(options: https.ServerOptions) {
43
- let httpsServer = https.createServer(options);
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
- options.ca = getTrustedCertificates();
46
- httpsServer.setSecureContext(options);
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
- console.log(`Trying to listening on ${host}:${config.port}`);
162
- realServer.listen(config.port, host);
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
- let port = (realServer.address() as net.AddressInfo).port;
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
- console.log(`Started Listening on ${config.nodeId || host}:${port}`);
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
  );