socket-function 0.8.34 → 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 +63 -30
- package/SocketFunctionTypes.ts +6 -24
- package/hot/HotReloadController.ts +2 -2
- package/package.json +6 -7
- package/require/CSSShim.ts +1 -0
- package/require/RequireController.ts +4 -3
- package/require/compileFlags.ts +1 -0
- package/src/CallFactory.ts +64 -142
- package/src/caching.ts +4 -0
- package/src/callHTTPHandler.ts +3 -3
- package/src/callManager.ts +17 -5
- package/src/certStore.ts +7 -33
- package/src/misc.ts +3 -0
- package/src/nodeCache.ts +12 -2
- package/src/webSocketServer.ts +72 -34
- package/src/websocketFactory.ts +46 -0
- package/test/client.ts +2 -2
- package/test/shared.ts +3 -9
- package/src/nodeAuthentication.ts +0 -123
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
|
|
|
@@ -28,8 +32,12 @@ export class SocketFunction {
|
|
|
28
32
|
type: "gzip";
|
|
29
33
|
};
|
|
30
34
|
public static httpETagCache = false;
|
|
31
|
-
public static rejectUnauthorized = true;
|
|
32
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).
|
|
33
41
|
public static register<
|
|
34
42
|
ClassInstance extends object,
|
|
35
43
|
Shape extends SocketExposedShape<SocketExposedInterface, CallContext>,
|
|
@@ -37,20 +45,28 @@ export class SocketFunction {
|
|
|
37
45
|
>(
|
|
38
46
|
classGuid: string,
|
|
39
47
|
instance: ClassInstance,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
(
|
|
44
|
-
SocketRegistered<ExtractShape<ClassInstance, Shape>, CallContext>
|
|
45
|
-
) {
|
|
46
|
-
|
|
47
|
-
for (let value of Object.values(shape)) {
|
|
48
|
-
if (!value) continue;
|
|
49
|
-
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
50
|
-
value.hooks = [...(defaultHooks?.hooks || []), ...(value.hooks || [])];
|
|
51
|
-
value.dataImmutable = defaultHooks?.dataImmutable ?? value.dataImmutable;
|
|
48
|
+
shapeFnc: () => Shape,
|
|
49
|
+
defaultHooksFnc?: () => SocketExposedShape[""] & {
|
|
50
|
+
onMount?: () => MaybePromise<void>;
|
|
52
51
|
}
|
|
53
|
-
|
|
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
|
+
});
|
|
54
70
|
|
|
55
71
|
let nodeProxy = getCallProxy(classGuid, async (call) => {
|
|
56
72
|
let nodeId = call.nodeId;
|
|
@@ -62,7 +78,7 @@ export class SocketFunction {
|
|
|
62
78
|
try {
|
|
63
79
|
let callFactory = await getCreateCallFactoryLocation(nodeId, SocketFunction.mountedNodeId);
|
|
64
80
|
|
|
65
|
-
let shapeObj =
|
|
81
|
+
let shapeObj = getShape()[functionName];
|
|
66
82
|
if (!shapeObj) {
|
|
67
83
|
throw new Error(`Function ${functionName} is not in shape`);
|
|
68
84
|
}
|
|
@@ -73,20 +89,6 @@ export class SocketFunction {
|
|
|
73
89
|
return hookResult.overrideResult;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
if (hookResult.callTimeout !== undefined) {
|
|
77
|
-
let timeout = hookResult.callTimeout;
|
|
78
|
-
let time = Date.now();
|
|
79
|
-
let timeoutPromise = new Promise((resolve, reject) => {
|
|
80
|
-
setTimeout(() => {
|
|
81
|
-
reject(new Error(`Call timed out after ${Date.now() - time}ms`));
|
|
82
|
-
}, timeout);
|
|
83
|
-
});
|
|
84
|
-
return await Promise.race([
|
|
85
|
-
callFactory.performCall(call),
|
|
86
|
-
timeoutPromise,
|
|
87
|
-
]);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
92
|
return await callFactory.performCall(call);
|
|
91
93
|
} finally {
|
|
92
94
|
time = Date.now() - time;
|
|
@@ -102,6 +104,18 @@ export class SocketFunction {
|
|
|
102
104
|
_classGuid: classGuid,
|
|
103
105
|
};
|
|
104
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
|
+
|
|
105
119
|
return output as any;
|
|
106
120
|
}
|
|
107
121
|
|
|
@@ -126,14 +140,33 @@ export class SocketFunction {
|
|
|
126
140
|
*/
|
|
127
141
|
public static expose(socketRegistered: SocketRegistered) {
|
|
128
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
|
+
}
|
|
129
153
|
}
|
|
130
154
|
|
|
131
155
|
public static mountedNodeId: string = "NOTMOUNTED";
|
|
156
|
+
private static hasMounted = false;
|
|
132
157
|
public static async mount(config: SocketServerConfig) {
|
|
133
158
|
if (this.mountedNodeId !== "NOTMOUNTED") {
|
|
134
159
|
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
135
160
|
}
|
|
136
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
|
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
+
/// <reference path="./require/RequireController.ts" />
|
|
2
|
+
|
|
1
3
|
module.allowclient = true;
|
|
2
4
|
|
|
3
|
-
import debugbreak from "debugbreak";
|
|
4
|
-
import * as tls from "tls";
|
|
5
|
-
import { SenderInterface } from "./src/CallFactory";
|
|
6
|
-
import { isNode } from "./src/misc";
|
|
7
|
-
import { CertInfo, getNodeIdFromCert } from "./src/nodeAuthentication";
|
|
8
|
-
import { getClientNodeId } from "./src/nodeCache";
|
|
9
5
|
import { getCallObj } from "./src/nodeProxy";
|
|
10
6
|
import { Args, MaybePromise } from "./src/types";
|
|
11
7
|
|
|
@@ -40,9 +36,6 @@ export interface CallType {
|
|
|
40
36
|
classGuid: string;
|
|
41
37
|
functionName: string;
|
|
42
38
|
args: unknown[];
|
|
43
|
-
// NOTE: When making calls this needs to be set in the client hook.
|
|
44
|
-
// To set a timeout on returns, you can set it in the server hook.
|
|
45
|
-
reconnectTimeout?: number;
|
|
46
39
|
}
|
|
47
40
|
export interface FullCallType extends CallType {
|
|
48
41
|
nodeId: string;
|
|
@@ -50,20 +43,18 @@ export interface FullCallType extends CallType {
|
|
|
50
43
|
|
|
51
44
|
export interface SocketFunctionHook<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> {
|
|
52
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>;
|
|
53
48
|
}
|
|
54
49
|
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> = {
|
|
55
|
-
call:
|
|
50
|
+
call: FullCallType;
|
|
56
51
|
context: SocketRegistered<ExposedType, CallContext>["context"];
|
|
57
52
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
58
53
|
overrideResult?: unknown;
|
|
59
54
|
};
|
|
60
55
|
|
|
61
56
|
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> = {
|
|
62
|
-
call:
|
|
63
|
-
/** If the calls takes longer than this (for ANY reason), we return with an error.
|
|
64
|
-
* - Different from reconnectTimeout, which only errors if we lose the connection.
|
|
65
|
-
*/
|
|
66
|
-
callTimeout?: number;
|
|
57
|
+
call: FullCallType;
|
|
67
58
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
68
59
|
overrideResult?: unknown;
|
|
69
60
|
};
|
|
@@ -101,15 +92,6 @@ export type CallerContextBase = {
|
|
|
101
92
|
// a more permanent identity, you must derive it from certInfo yourself.
|
|
102
93
|
nodeId: string;
|
|
103
94
|
|
|
104
|
-
/** Gives further info on the node. When we set this, we always make sure it has a verified
|
|
105
|
-
* issuer. It may be set by app code, which should make sure the issuer is verified (not
|
|
106
|
-
* necessarily by the machine, but just in some sense, 'verified', to secure the common name
|
|
107
|
-
* of the cert and prevent anyone from using the same common name as someone else).
|
|
108
|
-
* IF set, is directly used to derive nodeId (by nodeAuthentication.ts)
|
|
109
|
-
*/
|
|
110
|
-
certInfo: CertInfo | undefined;
|
|
111
|
-
updateCertInfo?: (certInfo: CertInfo, callbackPort: number | undefined) => void;
|
|
112
|
-
|
|
113
95
|
// The nodeId they contacted. This is useful to determine their intention (otherwise
|
|
114
96
|
// requests can be redirected to us and would accept them, even though they are being
|
|
115
97
|
// blatantly MITMed).
|
|
@@ -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,16 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.8.
|
|
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
|
-
"debugbreak": "^0.6.5",
|
|
14
9
|
"mobx": "^6.6.2",
|
|
15
10
|
"node-forge": "https://github.com/sliftist/forge#name",
|
|
16
11
|
"preact": "^10.10.6",
|
|
@@ -22,6 +17,10 @@
|
|
|
22
17
|
"type": "yarn tsc --noEmit"
|
|
23
18
|
},
|
|
24
19
|
"devDependencies": {
|
|
25
|
-
"
|
|
20
|
+
"@types/cookie": "^0.5.1",
|
|
21
|
+
"@types/node-forge": "^1.3.1",
|
|
22
|
+
"@types/ws": "^8.5.3",
|
|
23
|
+
"debugbreak": "^0.6.5",
|
|
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
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { CallerContext, CallerContextBase, CallType } from "../SocketFunctionTypes";
|
|
1
|
+
import { CallerContext, CallerContextBase, CallType, FullCallType } from "../SocketFunctionTypes";
|
|
2
2
|
import * as ws from "ws";
|
|
3
3
|
import { performLocalCall } from "./callManager";
|
|
4
4
|
import { convertErrorStackToError, formatNumberSuffixed, isNode } from "./misc";
|
|
5
|
-
import { createWebsocketFactory,
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
type InternalCallType = CallType & {
|
|
13
|
+
type InternalCallType = FullCallType & {
|
|
14
14
|
seqNum: number;
|
|
15
15
|
isReturn: false;
|
|
16
16
|
compress: boolean;
|
|
@@ -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
|
|
@@ -77,16 +73,7 @@ export async function createCallFactory(
|
|
|
77
73
|
|
|
78
74
|
let callerContext: CallerContextBase = {
|
|
79
75
|
nodeId,
|
|
80
|
-
localNodeId
|
|
81
|
-
certInfo: webSocketBase?.socket?.getPeerCertificate(true),
|
|
82
|
-
updateCertInfo: (certRaw, port) => {
|
|
83
|
-
let nodeId = getNodeIdFromCert(certRaw, port);
|
|
84
|
-
if (!nodeId) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
callerContext.nodeId = nodeId;
|
|
88
|
-
callerContext.certInfo = certRaw;
|
|
89
|
-
}
|
|
76
|
+
localNodeId
|
|
90
77
|
};
|
|
91
78
|
|
|
92
79
|
let callFactory: CallFactory = {
|
|
@@ -99,6 +86,7 @@ export async function createCallFactory(
|
|
|
99
86
|
|
|
100
87
|
let seqNum = nextSeqNum++;
|
|
101
88
|
let fullCall: InternalCallType = {
|
|
89
|
+
nodeId,
|
|
102
90
|
isReturn: false,
|
|
103
91
|
args: call.args,
|
|
104
92
|
classGuid: call.classGuid,
|
|
@@ -119,144 +107,78 @@ export async function createCallFactory(
|
|
|
119
107
|
resolve(result.result);
|
|
120
108
|
}
|
|
121
109
|
};
|
|
122
|
-
pendingCalls.set(seqNum, { callback, data, call: fullCall
|
|
110
|
+
pendingCalls.set(seqNum, { callback, data, call: fullCall });
|
|
123
111
|
});
|
|
124
112
|
|
|
125
|
-
await
|
|
113
|
+
await send(data);
|
|
126
114
|
|
|
127
115
|
return await resultPromise;
|
|
128
116
|
}
|
|
129
117
|
};
|
|
130
118
|
|
|
131
|
-
let
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
webSocket = webSocketBase;
|
|
136
|
-
setupWebsocket(webSocketBase);
|
|
119
|
+
let webSocketPromise: Promise<SenderInterface> | undefined;
|
|
120
|
+
if (webSocketBase) {
|
|
121
|
+
webSocketPromise = Promise.resolve(webSocketBase);
|
|
122
|
+
await initializeWebsocket(webSocketBase);
|
|
137
123
|
}
|
|
138
124
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
retriesEnabled = false;
|
|
155
|
-
resolve(webSocket);
|
|
156
|
-
}, reconnectTimeout)
|
|
157
|
-
)
|
|
158
|
-
]);
|
|
159
|
-
} else {
|
|
160
|
-
await reconnectingPromise;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!retriesEnabled) {
|
|
165
|
-
webSocket.send(data);
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
webSocket.send(data);
|
|
171
|
-
break;
|
|
172
|
-
} catch (e) {
|
|
173
|
-
// Ignore errors, as we will catch them synchronously in the next loop.
|
|
174
|
-
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
|
+
});
|
|
175
140
|
}
|
|
176
141
|
}
|
|
177
|
-
}
|
|
178
|
-
function tryToReconnect(): Promise<void> {
|
|
179
|
-
if (reconnectingPromise) return reconnectingPromise;
|
|
180
|
-
return reconnectingPromise = (async () => {
|
|
181
|
-
while (true) {
|
|
182
|
-
if (!retriesEnabled) {
|
|
183
|
-
callFactory.closedForever = true;
|
|
184
|
-
console.log(`Cannot reconnect to ${niceConnectionName}, aborting pendingCalls: ${pendingCalls.size}`);
|
|
185
|
-
for (let call of pendingCalls.values()) {
|
|
186
|
-
call.callback({
|
|
187
|
-
isReturn: true,
|
|
188
|
-
result: undefined,
|
|
189
|
-
error: `Connection lost to ${niceConnectionName}`,
|
|
190
|
-
seqNum: call.call.seqNum,
|
|
191
|
-
resultSize: 0,
|
|
192
|
-
compressed: false,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
let newWebSocket = createWebsocket(nodeId);
|
|
199
|
-
|
|
200
|
-
let connectError = await new Promise<string | undefined>(resolve => {
|
|
201
|
-
newWebSocket.addEventListener("open", () => {
|
|
202
|
-
resolve(undefined);
|
|
203
|
-
});
|
|
204
|
-
newWebSocket.addEventListener("close", () => {
|
|
205
|
-
resolve("Connection closed for non-error reason?");
|
|
206
|
-
});
|
|
207
|
-
newWebSocket.addEventListener("error", e => {
|
|
208
|
-
resolve(String(e.message));
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
142
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
});
|
|
216
149
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
150
|
+
newWebSocket.addEventListener("close", async () => {
|
|
151
|
+
//console.log(`Websocket closed ${niceConnectionName}`);
|
|
152
|
+
onClose(`Connection closed to ${niceConnectionName}`);
|
|
153
|
+
});
|
|
220
154
|
|
|
221
|
-
|
|
155
|
+
newWebSocket.addEventListener("message", onMessage);
|
|
222
156
|
|
|
223
|
-
for (let call of pendingCalls.values()) {
|
|
224
|
-
sendWithRetry(call.reconnectTimeout, call.data).catch(e => {
|
|
225
|
-
call.callback({
|
|
226
|
-
isReturn: true,
|
|
227
|
-
result: undefined,
|
|
228
|
-
error: String(e),
|
|
229
|
-
seqNum: call.call.seqNum,
|
|
230
|
-
resultSize: 0,
|
|
231
|
-
compressed: false,
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
157
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
+
}
|
|
243
170
|
}
|
|
244
171
|
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
webSocket.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.log(`Websocket closed ${niceConnectionName}`);
|
|
254
|
-
if (retriesEnabled) {
|
|
255
|
-
await tryToReconnect();
|
|
256
|
-
}
|
|
257
|
-
});
|
|
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);
|
|
258
180
|
|
|
259
|
-
|
|
181
|
+
return newWebSocket;
|
|
260
182
|
}
|
|
261
183
|
|
|
262
184
|
|
|
@@ -336,7 +258,7 @@ export async function createCallFactory(
|
|
|
336
258
|
} else {
|
|
337
259
|
result = Buffer.from(JSON.stringify(response));
|
|
338
260
|
}
|
|
339
|
-
await
|
|
261
|
+
await send(result);
|
|
340
262
|
}
|
|
341
263
|
return;
|
|
342
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
|
|
package/src/callHTTPHandler.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "http";
|
|
2
2
|
import tls from "tls";
|
|
3
|
-
import { CallerContext, CallType } from "../SocketFunctionTypes";
|
|
3
|
+
import { CallerContext, CallType, FullCallType } from "../SocketFunctionTypes";
|
|
4
4
|
import { isDataImmutable, performLocalCall } from "./callManager";
|
|
5
5
|
import { SocketFunction } from "../SocketFunction";
|
|
6
6
|
import { gzip } from "zlib";
|
|
@@ -79,7 +79,6 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
79
79
|
|
|
80
80
|
let caller: CallerContext = {
|
|
81
81
|
nodeId,
|
|
82
|
-
certInfo: undefined,
|
|
83
82
|
localNodeId,
|
|
84
83
|
};
|
|
85
84
|
|
|
@@ -112,7 +111,8 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
112
111
|
args = JSON.parse(payload.toString())["args"] as unknown[];
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
let call:
|
|
114
|
+
let call: FullCallType = {
|
|
115
|
+
nodeId,
|
|
116
116
|
classGuid,
|
|
117
117
|
functionName,
|
|
118
118
|
args,
|
package/src/callManager.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { CallContextType, CallerContext, CallType, ClientHookContext, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
|
|
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]: {
|
|
@@ -14,7 +16,7 @@ let globalClientHooks: SocketFunctionClientHook[] = [];
|
|
|
14
16
|
|
|
15
17
|
export async function performLocalCall(
|
|
16
18
|
config: {
|
|
17
|
-
call:
|
|
19
|
+
call: FullCallType;
|
|
18
20
|
caller: CallerContext;
|
|
19
21
|
}
|
|
20
22
|
): Promise<unknown> {
|
|
@@ -92,11 +94,21 @@ export function unregisterGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export async function runClientHooks(
|
|
95
|
-
callType:
|
|
97
|
+
callType: FullCallType,
|
|
96
98
|
hooks: SocketExposedShape[""],
|
|
97
99
|
): Promise<ClientHookContext> {
|
|
98
100
|
let context: ClientHookContext = { call: callType };
|
|
99
|
-
|
|
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;
|
|
@@ -106,7 +118,7 @@ export async function runClientHooks(
|
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
async function runServerHooks(
|
|
109
|
-
callType:
|
|
121
|
+
callType: FullCallType,
|
|
110
122
|
context: SocketRegistered["context"],
|
|
111
123
|
hooks: SocketExposedShape[""],
|
|
112
124
|
): Promise<HookContext> {
|
package/src/certStore.ts
CHANGED
|
@@ -1,50 +1,24 @@
|
|
|
1
|
-
import * as os from "os";
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import * as fsSync from "fs";
|
|
4
|
-
import * as child_process from "child_process";
|
|
5
1
|
import * as tls from "tls";
|
|
6
|
-
import { SocketFunction } from "../SocketFunction";
|
|
7
|
-
import { isNode, isNodeTrue, sha256Hash } from "./misc";
|
|
8
|
-
import { lazy } from "./caching";
|
|
9
2
|
|
|
10
3
|
let trustedCerts = new Set<string>();
|
|
11
|
-
let loadedTrustedCerts = false;
|
|
12
4
|
let watchCallbacks = new Set<(certs: string[]) => void>();
|
|
13
5
|
|
|
14
|
-
let storePath = isNodeTrue() && process.argv[1].replaceAll("\\", "/").split("/").slice(0, -1).join("/") + "/certstore/";
|
|
15
|
-
if (isNode()) {
|
|
16
|
-
if (!fsSync.existsSync(storePath)) {
|
|
17
|
-
fsSync.mkdirSync(storePath);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
6
|
/** Must be populated before the server starts */
|
|
22
|
-
export
|
|
7
|
+
export function trustCertificate(cert: string | Buffer) {
|
|
8
|
+
cert = cert.toString();
|
|
23
9
|
if (trustedCerts.has(cert)) return;
|
|
24
10
|
trustedCerts.add(cert);
|
|
25
|
-
|
|
26
|
-
let certs = getTrustedUserCertificates();
|
|
11
|
+
let certs = getTrustedCertificates();
|
|
27
12
|
for (let callback of watchCallbacks) {
|
|
28
13
|
callback(certs);
|
|
29
14
|
}
|
|
30
15
|
}
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
for (let file of files) {
|
|
34
|
-
let cert = await fs.readFile(storePath + file, "utf8");
|
|
35
|
-
trustedCerts.add(cert);
|
|
36
|
-
}
|
|
37
|
-
loadedTrustedCerts = true;
|
|
38
|
-
});
|
|
39
|
-
export function getTrustedUserCertificates(): string[] {
|
|
40
|
-
if (!loadedTrustedCerts) {
|
|
41
|
-
throw new Error("Must call loadTrustedUserCertificates (and await it) before calling getTrustedUserCertificates");
|
|
42
|
-
}
|
|
43
|
-
return Array.from(trustedCerts);
|
|
16
|
+
export function getTrustedCertificates(): string[] {
|
|
17
|
+
return tls.rootCertificates.concat(Array.from(trustedCerts));
|
|
44
18
|
}
|
|
45
19
|
|
|
46
|
-
export function
|
|
20
|
+
export function watchTrustedCertificates(callback: (certs: string[]) => void) {
|
|
47
21
|
watchCallbacks.add(callback);
|
|
48
|
-
callback(
|
|
22
|
+
callback(getTrustedCertificates());
|
|
49
23
|
return () => watchCallbacks.delete(callback);
|
|
50
24
|
}
|
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(":");
|
package/src/webSocketServer.ts
CHANGED
|
@@ -3,28 +3,33 @@ import http from "http";
|
|
|
3
3
|
import net from "net";
|
|
4
4
|
import tls from "tls";
|
|
5
5
|
import * as ws from "ws";
|
|
6
|
-
import { getCertKeyPair, getNodeIdFromCert } from "./nodeAuthentication";
|
|
7
6
|
import { getNodeIdsFromRequest, httpCallHandler } from "./callHTTPHandler";
|
|
8
7
|
import { SocketFunction } from "../SocketFunction";
|
|
9
|
-
import {
|
|
8
|
+
import { getTrustedCertificates, watchTrustedCertificates } from "./certStore";
|
|
10
9
|
import { createCallFactory } from "./CallFactory";
|
|
11
10
|
import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
|
|
12
11
|
import debugbreak from "debugbreak";
|
|
12
|
+
import { getNodeId } from "./nodeCache";
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
import { Watchable } from "./misc";
|
|
13
15
|
|
|
14
16
|
export type SocketServerConfig = (
|
|
15
17
|
https.ServerOptions & {
|
|
18
|
+
key: string | Buffer;
|
|
19
|
+
cert: string | Buffer;
|
|
20
|
+
|
|
16
21
|
port: number;
|
|
22
|
+
/** You can also set `port: 0` if you don't care what port you want at all. */
|
|
23
|
+
useAvailablePortIfPortInUse?: boolean;
|
|
17
24
|
|
|
18
25
|
// public sets ip to "0.0.0.0", otherwise it defaults to "127.0.0.1", which
|
|
19
26
|
// causes the server to only accept local connections.
|
|
20
27
|
public?: boolean;
|
|
21
28
|
ip?: string;
|
|
22
29
|
|
|
23
|
-
/** If the SNI matches this domain, we use a different key/cert.
|
|
24
|
-
* - Also requestCert may be specified (otherwise it defaults to true)
|
|
25
|
-
*/
|
|
30
|
+
/** If the SNI matches this domain, we use a different key/cert. */
|
|
26
31
|
SNICerts?: {
|
|
27
|
-
[domain: string]: https.ServerOptions
|
|
32
|
+
[domain: string]: Watchable<https.ServerOptions>;
|
|
28
33
|
};
|
|
29
34
|
}
|
|
30
35
|
);
|
|
@@ -32,25 +37,30 @@ export type SocketServerConfig = (
|
|
|
32
37
|
export async function startSocketServer(
|
|
33
38
|
config: SocketServerConfig
|
|
34
39
|
): Promise<string> {
|
|
35
|
-
let isSecure = "cert" in config || "key" in config || "pfx" in config;
|
|
36
|
-
if (!isSecure) {
|
|
37
|
-
let { key, cert } = getCertKeyPair();
|
|
38
|
-
config.key = key;
|
|
39
|
-
config.cert = cert;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
40
|
|
|
43
41
|
const webSocketServer = new ws.Server({
|
|
44
42
|
noServer: true,
|
|
45
43
|
});
|
|
46
44
|
|
|
47
|
-
|
|
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;
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
options.ca = tls.rootCertificates.concat(getTrustedUserCertificates());
|
|
53
|
-
httpsServer.setSecureContext(options);
|
|
61
|
+
watchTrustedCertificates(() => {
|
|
62
|
+
lastOptions.ca = getTrustedCertificates();
|
|
63
|
+
httpsServer.setSecureContext(lastOptions);
|
|
54
64
|
});
|
|
55
65
|
|
|
56
66
|
httpsServer.on("connection", socket => {
|
|
@@ -102,15 +112,19 @@ export async function startSocketServer(
|
|
|
102
112
|
// TODO: Only allow unauthorized for ip certificates, and then for domains use the domain as the nodeId,
|
|
103
113
|
// so it is easy to read, and consistent.
|
|
104
114
|
let options: https.ServerOptions = {
|
|
105
|
-
rejectUnauthorized: SocketFunction.rejectUnauthorized,
|
|
106
|
-
requestCert: true,
|
|
107
115
|
...config,
|
|
108
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
|
+
}
|
|
109
123
|
|
|
110
|
-
const mainHTTPSServer = setupHTTPSServer(options);
|
|
124
|
+
const mainHTTPSServer = await setupHTTPSServer(callback => callback(options));
|
|
111
125
|
let sniServers = new Map<string, https.Server>();
|
|
112
126
|
for (let [domain, obj] of Object.entries(config.SNICerts || {})) {
|
|
113
|
-
sniServers.set(domain, setupHTTPSServer(obj));
|
|
127
|
+
sniServers.set(domain, await setupHTTPSServer(obj));
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
let httpServer = http.createServer({}, async function (req, res) {
|
|
@@ -137,10 +151,9 @@ export async function startSocketServer(
|
|
|
137
151
|
if (buffer[0] !== 22) {
|
|
138
152
|
server = httpServer;
|
|
139
153
|
} else {
|
|
140
|
-
debugbreak(1);
|
|
141
|
-
debugger;
|
|
142
154
|
let data = parseTLSHello(buffer);
|
|
143
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)}`);
|
|
144
157
|
server = sniServers.get(sni) || mainHTTPSServer;
|
|
145
158
|
}
|
|
146
159
|
|
|
@@ -169,18 +182,43 @@ export async function startSocketServer(
|
|
|
169
182
|
host = "0.0.0.0";
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
|
|
173
|
-
|
|
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);
|
|
174
208
|
|
|
175
209
|
await listenPromise;
|
|
176
210
|
|
|
177
|
-
|
|
211
|
+
port = (realServer.address() as net.AddressInfo).port;
|
|
212
|
+
let nodeId = getNodeId(getCommonName(config.cert), port);
|
|
213
|
+
console.log(`Started Listening on ${nodeId}`);
|
|
178
214
|
|
|
179
|
-
|
|
215
|
+
return nodeId;
|
|
216
|
+
}
|
|
180
217
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
186
224
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import ws from "ws";
|
|
2
|
+
import tls from "tls";
|
|
3
|
+
import { isNode } from "./misc";
|
|
4
|
+
import { SenderInterface } from "./CallFactory";
|
|
5
|
+
import { getTrustedCertificates } from "./certStore";
|
|
6
|
+
import { getNodeIdLocation } from "./nodeCache";
|
|
7
|
+
import debugbreak from "debugbreak";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function getTLSSocket(webSocket: ws.WebSocket) {
|
|
11
|
+
return (webSocket as any)._socket as tls.TLSSocket;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** NOTE: We create a factory, which embeds the key/cert information. Otherwise retries might use
|
|
15
|
+
* a different key/cert context.
|
|
16
|
+
*/
|
|
17
|
+
export function createWebsocketFactory(): (nodeId: string) => SenderInterface {
|
|
18
|
+
|
|
19
|
+
if (!isNode()) {
|
|
20
|
+
return (nodeId: string) => {
|
|
21
|
+
let location = getNodeIdLocation(nodeId);
|
|
22
|
+
if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
|
|
23
|
+
let { address, port } = location;
|
|
24
|
+
|
|
25
|
+
console.log(`Connecting to ${address}:${port}`);
|
|
26
|
+
return new WebSocket(`wss://${address}:${port}`);
|
|
27
|
+
};
|
|
28
|
+
} else {
|
|
29
|
+
return (nodeId: string) => {
|
|
30
|
+
let location = getNodeIdLocation(nodeId);
|
|
31
|
+
if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
|
|
32
|
+
let { address, port } = location;
|
|
33
|
+
|
|
34
|
+
console.log(`Connecting to ${address}:${port}`);
|
|
35
|
+
let webSocket = new ws.WebSocket(`wss://${address}:${port}`, {
|
|
36
|
+
ca: tls.rootCertificates.concat(getTrustedCertificates()),
|
|
37
|
+
});
|
|
38
|
+
let result = Object.assign(webSocket, { socket: undefined as tls.TLSSocket | undefined });
|
|
39
|
+
webSocket.once("upgrade", e => {
|
|
40
|
+
result.socket = e.socket as tls.TLSSocket;
|
|
41
|
+
});
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
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
|
);
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import ws from "ws";
|
|
2
|
-
import tls from "tls";
|
|
3
|
-
import net from "net";
|
|
4
|
-
import { getAppFolder } from "./storagePath";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import child_process from "child_process";
|
|
7
|
-
import { cacheWeak, lazy } from "./caching";
|
|
8
|
-
import https from "https";
|
|
9
|
-
import debugbreak from "debugbreak";
|
|
10
|
-
import crypto from "crypto";
|
|
11
|
-
import { isNode, sha256Hash } from "./misc";
|
|
12
|
-
import { getArgs } from "./args";
|
|
13
|
-
import { SenderInterface } from "./CallFactory";
|
|
14
|
-
import { SocketFunction } from "../SocketFunction";
|
|
15
|
-
import { getTrustedUserCertificates } from "./certStore";
|
|
16
|
-
import { getClientNodeId, getNodeId, getNodeIdLocation } from "./nodeCache";
|
|
17
|
-
|
|
18
|
-
export type CertInfo = { raw: Buffer | string; issuerCertificate: { raw: Buffer | string } };
|
|
19
|
-
|
|
20
|
-
let certKeyPairOverride: { key: Buffer; cert: Buffer } | undefined;
|
|
21
|
-
export function getCertKeyPair(): { key: Buffer; cert: Buffer } {
|
|
22
|
-
if (certKeyPairOverride) return certKeyPairOverride;
|
|
23
|
-
return getCertKeyPairBase();
|
|
24
|
-
}
|
|
25
|
-
const getCertKeyPairBase = lazy((): { key: Buffer; cert: Buffer } => {
|
|
26
|
-
// https://nodejs.org/en/knowledge/HTTP/servers/how-to-create-a-HTTPS-server/
|
|
27
|
-
|
|
28
|
-
let folder = getAppFolder();
|
|
29
|
-
let identityPrefix = getArgs().identity || "";
|
|
30
|
-
let keyPath = folder + identityPrefix + "key.pem";
|
|
31
|
-
let certPath = folder + identityPrefix + "cert.pem";
|
|
32
|
-
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
|
|
33
|
-
child_process.execSync(`openssl genrsa -out "${keyPath}"`);
|
|
34
|
-
child_process.execSync(`openssl req -new -key "${keyPath}" -out csr.pem -subj "/CN=notused"`);
|
|
35
|
-
child_process.execSync(`openssl x509 -req -days 9999 -in csr.pem -signkey "${keyPath}" -out "${certPath}"`);
|
|
36
|
-
fs.rmSync("csr.pem");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let key = fs.readFileSync(keyPath);
|
|
40
|
-
let cert = fs.readFileSync(certPath);
|
|
41
|
-
return { key, cert };
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
export function overrideCertKeyPair<T>(certKey: { key: Buffer; cert: Buffer; }, code: () => T): T {
|
|
45
|
-
if (!isNode()) {
|
|
46
|
-
throw new Error(`Cannot override cert/key pair in browser`);
|
|
47
|
-
}
|
|
48
|
-
let prevOverride = certKeyPairOverride;
|
|
49
|
-
certKeyPairOverride = certKey;
|
|
50
|
-
try {
|
|
51
|
-
return code();
|
|
52
|
-
} finally {
|
|
53
|
-
certKeyPairOverride = prevOverride;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function getTLSSocket(webSocket: ws.WebSocket) {
|
|
58
|
-
return (webSocket as any)._socket as tls.TLSSocket;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function getOwnNodeId() {
|
|
62
|
-
if (!isNode()) {
|
|
63
|
-
throw new Error(`Clientside nodeIds are not exposed to the client`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// This is BASICALLY just sha256Hash(getCertKeyPari().cert), however... I'm not 100% the format
|
|
67
|
-
// is the same, we would have to verify it. It isn't that important, other nodes know our nodeId,
|
|
68
|
-
// and clients don't really have a reason to use this anyway (they can't verify it, they can only
|
|
69
|
-
// really verify with a location).
|
|
70
|
-
throw new Error(`TODO: Implement getOwnNodeId`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function getNodeIdFromCert(certRaw: { raw: Buffer | string } | undefined, callbackPort: number | undefined) {
|
|
74
|
-
if (!certRaw?.raw) return undefined;
|
|
75
|
-
let cert = new crypto.X509Certificate(certRaw.raw);
|
|
76
|
-
if (!callbackPort) {
|
|
77
|
-
return getClientNodeId(cert.subject);
|
|
78
|
-
}
|
|
79
|
-
let subject = cert.subject;
|
|
80
|
-
if (subject.startsWith("CN=")) {
|
|
81
|
-
subject = subject.slice("CN=".length);
|
|
82
|
-
}
|
|
83
|
-
return getNodeId(subject, callbackPort);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** NOTE: We create a factory, which embeds the key/cert information. Otherwise retries might use
|
|
87
|
-
* a different key/cert context.
|
|
88
|
-
*/
|
|
89
|
-
export function createWebsocketFactory(): (nodeId: string) => SenderInterface {
|
|
90
|
-
|
|
91
|
-
if (!isNode()) {
|
|
92
|
-
return (nodeId: string) => {
|
|
93
|
-
let location = getNodeIdLocation(nodeId);
|
|
94
|
-
if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
|
|
95
|
-
let { address, port } = location;
|
|
96
|
-
|
|
97
|
-
console.log(`Connecting to ${address}:${port}`);
|
|
98
|
-
return new WebSocket(`wss://${address}:${port}`);
|
|
99
|
-
};
|
|
100
|
-
} else {
|
|
101
|
-
let { key, cert } = getCertKeyPair();
|
|
102
|
-
let rejectUnauthorized = SocketFunction.rejectUnauthorized;
|
|
103
|
-
return (nodeId: string) => {
|
|
104
|
-
let location = getNodeIdLocation(nodeId);
|
|
105
|
-
if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
|
|
106
|
-
let { address, port } = location;
|
|
107
|
-
|
|
108
|
-
console.log(`Connecting to ${address}:${port}`);
|
|
109
|
-
let webSocket = new ws.WebSocket(`wss://${address}:${port}`, {
|
|
110
|
-
cert,
|
|
111
|
-
key,
|
|
112
|
-
rejectUnauthorized,
|
|
113
|
-
ca: tls.rootCertificates.concat(getTrustedUserCertificates()),
|
|
114
|
-
});
|
|
115
|
-
let result = Object.assign(webSocket, { socket: undefined as tls.TLSSocket | undefined });
|
|
116
|
-
webSocket.once("upgrade", e => {
|
|
117
|
-
result.socket = e.socket as tls.TLSSocket;
|
|
118
|
-
});
|
|
119
|
-
return result;
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|