querysub 0.436.0 → 0.438.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.eslintrc.js +50 -50
  2. package/bin/deploy.js +0 -0
  3. package/bin/function.js +0 -0
  4. package/bin/server.js +0 -0
  5. package/costsBenefits.txt +115 -115
  6. package/deploy.ts +2 -2
  7. package/package.json +1 -1
  8. package/spec.txt +1192 -1192
  9. package/src/-a-archives/archives.ts +202 -202
  10. package/src/-a-archives/archivesBackBlaze.ts +0 -1
  11. package/src/-a-archives/archivesDisk.ts +454 -454
  12. package/src/-a-auth/certs.ts +540 -540
  13. package/src/-a-auth/node-forge-ed25519.d.ts +16 -16
  14. package/src/-b-authorities/dnsAuthority.ts +138 -138
  15. package/src/-c-identity/IdentityController.ts +258 -258
  16. package/src/-d-trust/NetworkTrust2.ts +180 -180
  17. package/src/-e-certs/EdgeCertController.ts +252 -252
  18. package/src/-e-certs/certAuthority.ts +201 -201
  19. package/src/-f-node-discovery/NodeDiscovery.ts +640 -640
  20. package/src/-g-core-values/NodeCapabilities.ts +200 -200
  21. package/src/-h-path-value-serialize/stringSerializer.ts +175 -175
  22. package/src/0-path-value-core/PathValueCommitter.ts +468 -468
  23. package/src/0-path-value-core/PathValueController.ts +0 -2
  24. package/src/2-proxy/PathValueProxyWatcher.ts +2542 -2542
  25. package/src/2-proxy/TransactionDelayer.ts +94 -94
  26. package/src/2-proxy/pathDatabaseProxyBase.ts +36 -36
  27. package/src/2-proxy/pathValueProxy.ts +159 -159
  28. package/src/3-path-functions/PathFunctionRunnerMain.ts +87 -87
  29. package/src/3-path-functions/pathFunctionLoader.ts +516 -516
  30. package/src/3-path-functions/tests/rejectTest.ts +76 -76
  31. package/src/4-deploy/deployCheck.ts +6 -6
  32. package/src/4-dom/css.tsx +29 -29
  33. package/src/4-dom/cssTypes.d.ts +211 -211
  34. package/src/4-dom/qreact.tsx +2799 -2799
  35. package/src/4-dom/qreactTest.tsx +410 -410
  36. package/src/4-querysub/permissions.ts +335 -335
  37. package/src/4-querysub/querysubPrediction.ts +483 -483
  38. package/src/5-diagnostics/qreactDebug.tsx +377 -346
  39. package/src/TestController.ts +34 -34
  40. package/src/bits.ts +104 -104
  41. package/src/buffers.ts +69 -69
  42. package/src/diagnostics/ActionsHistory.ts +57 -57
  43. package/src/diagnostics/listenOnDebugger.ts +71 -71
  44. package/src/diagnostics/periodic.ts +111 -111
  45. package/src/diagnostics/trackResources.ts +91 -91
  46. package/src/diagnostics/watchdog.ts +120 -120
  47. package/src/errors.ts +133 -133
  48. package/src/forceProduction.ts +2 -2
  49. package/src/fs.ts +80 -80
  50. package/src/functional/diff.ts +857 -857
  51. package/src/functional/promiseCache.ts +78 -78
  52. package/src/functional/random.ts +8 -8
  53. package/src/functional/stats.ts +60 -60
  54. package/src/heapDumps.ts +665 -665
  55. package/src/https.ts +1 -1
  56. package/src/library-components/AspectSizedComponent.tsx +87 -87
  57. package/src/library-components/ButtonSelector.tsx +64 -64
  58. package/src/library-components/DropdownCustom.tsx +150 -150
  59. package/src/library-components/DropdownSelector.tsx +31 -31
  60. package/src/library-components/InlinePopup.tsx +66 -66
  61. package/src/misc/color.ts +29 -29
  62. package/src/misc/hash.ts +83 -83
  63. package/src/misc/ipPong.js +13 -13
  64. package/src/misc/networking.ts +1 -1
  65. package/src/misc/random.ts +44 -44
  66. package/src/misc.ts +196 -196
  67. package/src/path.ts +255 -255
  68. package/src/persistentLocalStore.ts +41 -41
  69. package/src/promise.ts +14 -14
  70. package/src/storage/fileSystemPointer.ts +71 -71
  71. package/src/test/heapProcess.ts +35 -35
  72. package/src/zip.ts +15 -15
  73. package/tsconfig.json +26 -26
  74. package/yarnSpec.txt +56 -56
@@ -1,259 +1,259 @@
1
- /**
2
- * Allows clients to set their certificate, as long as it meets our strict schema requirements.
3
- * - Our requirements ensure that even though the certificates are self signed, they can't
4
- * be impersonated by other users (because their common name contains their public key).
5
- */
6
-
7
- import debugbreak from "debugbreak";
8
- import { SocketFunction } from "socket-function/SocketFunction";
9
- import { CallerContext } from "socket-function/SocketFunctionTypes";
10
- import { cache, cacheWeak, lazy } from "socket-function/src/caching";
11
- import { getClientNodeId, getNodeId, getNodeIdDomain, getNodeIdIP, getNodeIdLocation, isClientNodeId } from "socket-function/src/nodeCache";
12
- import { decodeNodeId, getCommonName, getIdentityCA, getMachineId, getOwnMachineId, getPublicIdentifier, getThreadKeyCert, parseCert, sign, validateCertificate, verify } from "../-a-auth/certs";
13
- import { getShortNumber } from "../bits";
14
- import { measureBlock, measureFnc, measureWrap } from "socket-function/src/profiling/measure";
15
- import { timeoutToError } from "../errors";
16
- import { delay } from "socket-function/src/batching";
17
- import { formatTime } from "socket-function/src/formatting/format";
18
- import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
19
- import { red } from "socket-function/src/formatting/logColors";
20
- import { isNode } from "typesafecss";
21
- import { areNodeIdsEqual, getOwnNodeId, getOwnThreadId } from "../-f-node-discovery/NodeDiscovery";
22
- import { timeInMinute } from "socket-function/src/misc";
23
-
24
- // NOTE: This used to be small, but we cache this, so it would mean a node on startup would time out, and then we would refuse to talk to it ever again. So... this can't be small
25
- const MAX_CHANGE_IDENTITY_TIMEOUT = timeInMinute * 5;
26
-
27
- let callerInfo = new Map<CallerContext, {
28
- reconnectNodeId: string | undefined;
29
- machineId: string;
30
- cert: CertInfo;
31
- pubKey: Buffer;
32
- pubKeyShort: number;
33
- }>();
34
- const callerInfoErrorString = `Internal error, caller did not updated their identity. Is this an HTTPS call (instead of a websocket call)? The caller should import the server controller, which imports this function, which will add a client hook to always update the identity`;
35
-
36
- /** Gets the nodeId of the caller suitable for reconnecting (to the same process).
37
- * - Also useful for logs, to identify a node on the network.
38
- * - Is global, so can be passed between nodes (although, the chance of the other process
39
- * staying alive for a long period of time is low).
40
- */
41
- export function IdentityController_getReconnectNodeId(callerContext: CallerContext, allowUndefined?: "allowUndefined"): string | undefined {
42
- if (!isClientNodeId(callerContext.nodeId)) {
43
- return callerContext.nodeId;
44
- }
45
- let info = callerInfo.get(callerContext);
46
- if (!info) {
47
- if (allowUndefined) return undefined;
48
- throw new Error(callerInfoErrorString);
49
- }
50
- return info.reconnectNodeId;
51
- }
52
- export function IdentityController_getReconnectNodeIdAssert(callerContext: CallerContext): string {
53
- if (!isClientNodeId(callerContext.nodeId)) {
54
- return callerContext.nodeId;
55
- }
56
- let reconnectId = IdentityController_getReconnectNodeId(callerContext);
57
- if (!reconnectId) throw new Error(`Caller did not mount before connecting. This call requires the caller to be listening.`);
58
- return reconnectId;
59
- }
60
- export function IdentityController_getSecureIP(callerContext: CallerContext): string {
61
- return getNodeIdIP(callerContext.nodeId);
62
- }
63
- export function IdentityController_getCurrentReconnectNodeIdAssert(): string {
64
- return IdentityController_getReconnectNodeIdAssert(SocketFunction.getCaller());
65
- }
66
- export function IdentityController_getCurrentReconnectNodeId(): string | undefined {
67
- return IdentityController_getReconnectNodeId(SocketFunction.getCaller());
68
- }
69
-
70
- export function debugNodeId(nodeId: string) {
71
- let info = Array.from(callerInfo.entries()).find(x => x[0].nodeId === nodeId);
72
- return info?.[1].reconnectNodeId || nodeId;
73
- };
74
- (globalThis as any).debugNodeId = debugNodeId;
75
-
76
- export function debugNodeThread(nodeId: string) {
77
- nodeId = debugNodeId(nodeId);
78
- return decodeNodeId(nodeId)?.threadId || nodeId;
79
- }
80
-
81
- /** Gets the nodeId of the machine, which should be consistent. When deciding to trust a node
82
- * this should be used instead of the reconnectId (otherwise you will have to trust
83
- * every single process individually, which will take forever!)
84
- */
85
- export function IdentityController_getMachineId(callerContext: CallerContext, allowEmpty?: "allowEmpty"): string {
86
- let location = getNodeIdLocation(callerContext.nodeId);
87
- if (location) {
88
- return getMachineId(location.address);
89
- }
90
- let info = callerInfo.get(callerContext);
91
- if (!info) {
92
- if (allowEmpty) return "";
93
- throw new Error(callerInfoErrorString);
94
- }
95
- return info.machineId;
96
- }
97
-
98
- export type CertInfo = { certPEM: Buffer | string; issuerPEM: Buffer | string; };
99
- export function IdentityController_getCertInfo(callerContext: CallerContext): CertInfo {
100
- let info = callerInfo.get(callerContext);
101
- if (!info) throw new Error(callerInfoErrorString);
102
- return info.cert;
103
- }
104
-
105
- export function IdentityController_getPubKeyShort(callerContext: CallerContext): number {
106
- let info = callerInfo.get(callerContext);
107
- if (!info) throw new Error(callerInfoErrorString);
108
- return info.pubKeyShort;
109
- }
110
- export const IdentityController_getOwnPubKeyShort = lazy((): number => {
111
- let cert = getThreadKeyCert();
112
- let pubKey = getPublicIdentifier(cert.cert);
113
- return getShortNumber(pubKey);
114
- });
115
-
116
-
117
- export interface ChangeIdentityPayload {
118
- time: number;
119
- cert: string;
120
- certIssuer: string;
121
- serverId: string;
122
- mountedPort: number | undefined;
123
- debugEntryPoint: string | undefined;
124
- clientIsNode: boolean;
125
- }
126
- class IdentityControllerBase {
127
- // IMPORTANT! We HAVE to call changeIdentity NOT JUST because we can't use peer certificates in the browser, BUT, also
128
- // because this removes the need to load peer certificates into the trust store. This greatly simplifies
129
- // a lot of the trust system and the trust call order, allowing us to use connections to determine
130
- // if we want to trust a node (instead of having to trust a node before it can connect to us, because
131
- // NodeJS will throw/ignore the peer cert if it isn't trusted).
132
- @measureFnc
133
- public async changeIdentity(signature: string, payload: ChangeIdentityPayload) {
134
- let time = Date.now();
135
- const caller = SocketFunction.getCaller();
136
- // Verify it wasn't signed too long ago (to make signature stealing WAY more difficult).
137
- const signedThreshold = Date.now() - MAX_CHANGE_IDENTITY_TIMEOUT;
138
- if (payload.time < signedThreshold) {
139
- throw new Error(`Signed payload too old, ${payload.time} < ${signedThreshold} from ${caller.localNodeId} (${caller.nodeId})`);
140
- }
141
-
142
- if (payload.clientIsNode && payload.serverId !== getOwnNodeId()) {
143
- // This is extremely common when we reuse ports, which we do frequently for the edge nodes.
144
- throw new Error(`You tried to contact another server. We are ${getOwnNodeId()}, you tried to contact ${payload.serverId}.`);
145
- }
146
-
147
- // Verify the signature is meant for us, otherwise any other site can hijack the login!
148
- // (We don't have to worry about other servers on the same domain, as all servers
149
- // on the same domain should be the same!)
150
- let localNodeId = caller.localNodeId;
151
- if (!areNodeIdsEqual(payload.serverId, localNodeId)) {
152
- throw new Error(`Identity is for another server! The connection is calling us ${localNodeId}, but signature is for ${payload.serverId}`);
153
- }
154
- // If they're calling from the browser, then they're not going to be able to use our machine ID, etc. However, they should be calling an actual https node, so it should still be secure for them.
155
- if (payload.clientIsNode) {
156
- let calledMachineId = getMachineId(payload.serverId);
157
- if (calledMachineId !== "127-0-0-1" && calledMachineId !== getOwnMachineId()) {
158
- throw new Error(`Tried to call a different machine. We are ${getOwnMachineId()}, they called ${calledMachineId}`);
159
- }
160
- let calledThreadId = decodeNodeId(payload.serverId)?.threadId;
161
- if (calledThreadId && calledThreadId !== "127-0-0-1" && calledThreadId !== getOwnThreadId()) {
162
- throw new Error(`Tried to call a different thread. We are ${getOwnThreadId()}, they called ${calledThreadId}`);
163
- }
164
- }
165
-
166
-
167
- // Verify the caller can sign as the cert
168
- verify(payload.cert, signature, payload);
169
-
170
- // Verify we even trust the issuer/cert pair
171
- validateCertificate(payload.cert, payload.certIssuer);
172
-
173
- let reconnectNodeId: string | undefined;
174
- if (payload.mountedPort) {
175
- // NOTE: We use the common name, and not getNodeIdIP... because anything connecting will need
176
- // to use HTTPS, so connecting over the IP won't work! (unless they turn off certificate validation,
177
- // which we shouldn't do...)
178
- reconnectNodeId = getNodeId(getCommonName(payload.cert), payload.mountedPort);
179
- }
180
-
181
- let pubKey = getPublicIdentifier(payload.cert);
182
- let machineId = getMachineId(getCommonName(payload.certIssuer));
183
-
184
- callerInfo.set(caller, {
185
- cert: { certPEM: payload.cert, issuerPEM: payload.certIssuer },
186
- machineId,
187
- reconnectNodeId,
188
- pubKey,
189
- pubKeyShort: getShortNumber(pubKey),
190
- });
191
-
192
- let duration = Date.now() - time;
193
- console.log(`Authenticated identity for ${caller.nodeId} in ${formatTime(duration)}, at ${Date.now()}`, {
194
- clientId: caller.nodeId,
195
- reconnectNodeId,
196
- duration,
197
- mountedPort: payload.mountedPort,
198
- debugEntryPoint: payload.debugEntryPoint,
199
- });
200
-
201
- SocketFunction.onNextDisconnect(caller.nodeId, () => {
202
- // NOTE: I don't really see any purpose of deleting from caller info. I don't think we're going to run out of memory because of too many callers authenticating.
203
- // However, logging here is useful as it allows us to complete the life cycle so we know how long a client was connected for.
204
- console.log(`Disconnected client`, {
205
- clientId: caller.nodeId,
206
- });
207
- });
208
- }
209
- }
210
-
211
- const IdentityController = SocketFunction.register(
212
- "IdentityController-4a1b77d7-2825-433e-a27c-e2b8a2e74457",
213
- new IdentityControllerBase(),
214
- () => ({
215
- changeIdentity: {
216
- // If we use hooks here, it can cause an infinite loop (which SocketFunction just turns into
217
- // a deadlock). So... we just don't use any hooks for this call.
218
- // Actually.. I think we were seeing a different issue. And client hooks are required,
219
- // so we can trust the remote cert correctly.
220
- // noClientHooks: true,
221
- // noDefaultHooks: true,
222
- },
223
- })
224
- );
225
-
226
- // IMPORTANT! We need to cache per connection, not per nodeId, so caching based on connectionId is required!
227
- const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectionId: { nodeId: string }) {
228
- let nodeId = connectionId.nodeId;
229
- let threadKeyCert = getThreadKeyCert();
230
- let issuer = getIdentityCA();
231
- let payload: ChangeIdentityPayload = {
232
- time: Date.now(),
233
- serverId: nodeId,
234
- cert: threadKeyCert.cert.toString(),
235
- certIssuer: issuer.cert.toString(),
236
- mountedPort: getNodeIdLocation(SocketFunction.mountedNodeId)?.port,
237
- debugEntryPoint: isNode() ? process.argv[1] : "browser",
238
- clientIsNode: isNode(),
239
- };
240
- let signature = sign(threadKeyCert, payload);
241
- await timeoutToError(
242
- MAX_CHANGE_IDENTITY_TIMEOUT,
243
- IdentityController.nodes[nodeId].changeIdentity(signature, payload),
244
- () => new Error(`Timeout calling changeIdentity for ${nodeId}`)
245
- );
246
- });
247
- SocketFunction.addGlobalClientHook(async function identityHook(context) {
248
- if (context.call.classGuid === IdentityController._classGuid) return;
249
- // This is for US to tell them our identity. And if they established the connection the identity will come from their original connection url (that they used to connect to us), and they validated it either being a real certificate, or they added the cert from the trusted backblaze bucket. If it just from a real certificate it means we identified them, but they might not have network trust. But that's fine, as IdentityController is JUST for identification, and if it's a real certificate we know who they are! (Which doesn't mean we trust them).
250
- if (isClientNodeId(context.call.nodeId)) {
251
- return;
252
- }
253
- let time = Date.now();
254
- await changeIdentityOnce(context.connectionId);
255
- let duration = Date.now() - time;
256
- if (duration > 200) {
257
- console.log(red(`IdentityHook took ${formatTime(duration)} for ${context.connectionId.nodeId} ${context.call.classGuid}.${context.call.functionName}`));
258
- }
1
+ /**
2
+ * Allows clients to set their certificate, as long as it meets our strict schema requirements.
3
+ * - Our requirements ensure that even though the certificates are self signed, they can't
4
+ * be impersonated by other users (because their common name contains their public key).
5
+ */
6
+
7
+ import debugbreak from "debugbreak";
8
+ import { SocketFunction } from "socket-function/SocketFunction";
9
+ import { CallerContext } from "socket-function/SocketFunctionTypes";
10
+ import { cache, cacheWeak, lazy } from "socket-function/src/caching";
11
+ import { getClientNodeId, getNodeId, getNodeIdDomain, getNodeIdIP, getNodeIdLocation, isClientNodeId } from "socket-function/src/nodeCache";
12
+ import { decodeNodeId, getCommonName, getIdentityCA, getMachineId, getOwnMachineId, getPublicIdentifier, getThreadKeyCert, parseCert, sign, validateCertificate, verify } from "../-a-auth/certs";
13
+ import { getShortNumber } from "../bits";
14
+ import { measureBlock, measureFnc, measureWrap } from "socket-function/src/profiling/measure";
15
+ import { timeoutToError } from "../errors";
16
+ import { delay } from "socket-function/src/batching";
17
+ import { formatTime } from "socket-function/src/formatting/format";
18
+ import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
19
+ import { red } from "socket-function/src/formatting/logColors";
20
+ import { isNode } from "typesafecss";
21
+ import { areNodeIdsEqual, getOwnNodeId, getOwnThreadId } from "../-f-node-discovery/NodeDiscovery";
22
+ import { timeInMinute } from "socket-function/src/misc";
23
+
24
+ // NOTE: This used to be small, but we cache this, so it would mean a node on startup would time out, and then we would refuse to talk to it ever again. So... this can't be small
25
+ const MAX_CHANGE_IDENTITY_TIMEOUT = timeInMinute * 5;
26
+
27
+ let callerInfo = new Map<CallerContext, {
28
+ reconnectNodeId: string | undefined;
29
+ machineId: string;
30
+ cert: CertInfo;
31
+ pubKey: Buffer;
32
+ pubKeyShort: number;
33
+ }>();
34
+ const callerInfoErrorString = `Internal error, caller did not updated their identity. Is this an HTTPS call (instead of a websocket call)? The caller should import the server controller, which imports this function, which will add a client hook to always update the identity`;
35
+
36
+ /** Gets the nodeId of the caller suitable for reconnecting (to the same process).
37
+ * - Also useful for logs, to identify a node on the network.
38
+ * - Is global, so can be passed between nodes (although, the chance of the other process
39
+ * staying alive for a long period of time is low).
40
+ */
41
+ export function IdentityController_getReconnectNodeId(callerContext: CallerContext, allowUndefined?: "allowUndefined"): string | undefined {
42
+ if (!isClientNodeId(callerContext.nodeId)) {
43
+ return callerContext.nodeId;
44
+ }
45
+ let info = callerInfo.get(callerContext);
46
+ if (!info) {
47
+ if (allowUndefined) return undefined;
48
+ throw new Error(callerInfoErrorString);
49
+ }
50
+ return info.reconnectNodeId;
51
+ }
52
+ export function IdentityController_getReconnectNodeIdAssert(callerContext: CallerContext): string {
53
+ if (!isClientNodeId(callerContext.nodeId)) {
54
+ return callerContext.nodeId;
55
+ }
56
+ let reconnectId = IdentityController_getReconnectNodeId(callerContext);
57
+ if (!reconnectId) throw new Error(`Caller did not mount before connecting. This call requires the caller to be listening.`);
58
+ return reconnectId;
59
+ }
60
+ export function IdentityController_getSecureIP(callerContext: CallerContext): string {
61
+ return getNodeIdIP(callerContext.nodeId);
62
+ }
63
+ export function IdentityController_getCurrentReconnectNodeIdAssert(): string {
64
+ return IdentityController_getReconnectNodeIdAssert(SocketFunction.getCaller());
65
+ }
66
+ export function IdentityController_getCurrentReconnectNodeId(): string | undefined {
67
+ return IdentityController_getReconnectNodeId(SocketFunction.getCaller());
68
+ }
69
+
70
+ export function debugNodeId(nodeId: string) {
71
+ let info = Array.from(callerInfo.entries()).find(x => x[0].nodeId === nodeId);
72
+ return info?.[1].reconnectNodeId || nodeId;
73
+ };
74
+ (globalThis as any).debugNodeId = debugNodeId;
75
+
76
+ export function debugNodeThread(nodeId: string) {
77
+ nodeId = debugNodeId(nodeId);
78
+ return decodeNodeId(nodeId)?.threadId || nodeId;
79
+ }
80
+
81
+ /** Gets the nodeId of the machine, which should be consistent. When deciding to trust a node
82
+ * this should be used instead of the reconnectId (otherwise you will have to trust
83
+ * every single process individually, which will take forever!)
84
+ */
85
+ export function IdentityController_getMachineId(callerContext: CallerContext, allowEmpty?: "allowEmpty"): string {
86
+ let location = getNodeIdLocation(callerContext.nodeId);
87
+ if (location) {
88
+ return getMachineId(location.address);
89
+ }
90
+ let info = callerInfo.get(callerContext);
91
+ if (!info) {
92
+ if (allowEmpty) return "";
93
+ throw new Error(callerInfoErrorString);
94
+ }
95
+ return info.machineId;
96
+ }
97
+
98
+ export type CertInfo = { certPEM: Buffer | string; issuerPEM: Buffer | string; };
99
+ export function IdentityController_getCertInfo(callerContext: CallerContext): CertInfo {
100
+ let info = callerInfo.get(callerContext);
101
+ if (!info) throw new Error(callerInfoErrorString);
102
+ return info.cert;
103
+ }
104
+
105
+ export function IdentityController_getPubKeyShort(callerContext: CallerContext): number {
106
+ let info = callerInfo.get(callerContext);
107
+ if (!info) throw new Error(callerInfoErrorString);
108
+ return info.pubKeyShort;
109
+ }
110
+ export const IdentityController_getOwnPubKeyShort = lazy((): number => {
111
+ let cert = getThreadKeyCert();
112
+ let pubKey = getPublicIdentifier(cert.cert);
113
+ return getShortNumber(pubKey);
114
+ });
115
+
116
+
117
+ export interface ChangeIdentityPayload {
118
+ time: number;
119
+ cert: string;
120
+ certIssuer: string;
121
+ serverId: string;
122
+ mountedPort: number | undefined;
123
+ debugEntryPoint: string | undefined;
124
+ clientIsNode: boolean;
125
+ }
126
+ class IdentityControllerBase {
127
+ // IMPORTANT! We HAVE to call changeIdentity NOT JUST because we can't use peer certificates in the browser, BUT, also
128
+ // because this removes the need to load peer certificates into the trust store. This greatly simplifies
129
+ // a lot of the trust system and the trust call order, allowing us to use connections to determine
130
+ // if we want to trust a node (instead of having to trust a node before it can connect to us, because
131
+ // NodeJS will throw/ignore the peer cert if it isn't trusted).
132
+ @measureFnc
133
+ public async changeIdentity(signature: string, payload: ChangeIdentityPayload) {
134
+ let time = Date.now();
135
+ const caller = SocketFunction.getCaller();
136
+ // Verify it wasn't signed too long ago (to make signature stealing WAY more difficult).
137
+ const signedThreshold = Date.now() - MAX_CHANGE_IDENTITY_TIMEOUT;
138
+ if (payload.time < signedThreshold) {
139
+ throw new Error(`Signed payload too old, ${payload.time} < ${signedThreshold} from ${caller.localNodeId} (${caller.nodeId})`);
140
+ }
141
+
142
+ if (payload.clientIsNode && payload.serverId !== getOwnNodeId()) {
143
+ // This is extremely common when we reuse ports, which we do frequently for the edge nodes.
144
+ throw new Error(`You tried to contact another server. We are ${getOwnNodeId()}, you tried to contact ${payload.serverId}.`);
145
+ }
146
+
147
+ // Verify the signature is meant for us, otherwise any other site can hijack the login!
148
+ // (We don't have to worry about other servers on the same domain, as all servers
149
+ // on the same domain should be the same!)
150
+ let localNodeId = caller.localNodeId;
151
+ if (!areNodeIdsEqual(payload.serverId, localNodeId)) {
152
+ throw new Error(`Identity is for another server! The connection is calling us ${localNodeId}, but signature is for ${payload.serverId}`);
153
+ }
154
+ // If they're calling from the browser, then they're not going to be able to use our machine ID, etc. However, they should be calling an actual https node, so it should still be secure for them.
155
+ if (payload.clientIsNode) {
156
+ let calledMachineId = getMachineId(payload.serverId);
157
+ if (calledMachineId !== "127-0-0-1" && calledMachineId !== getOwnMachineId()) {
158
+ throw new Error(`Tried to call a different machine. We are ${getOwnMachineId()}, they called ${calledMachineId}`);
159
+ }
160
+ let calledThreadId = decodeNodeId(payload.serverId)?.threadId;
161
+ if (calledThreadId && calledThreadId !== "127-0-0-1" && calledThreadId !== getOwnThreadId()) {
162
+ throw new Error(`Tried to call a different thread. We are ${getOwnThreadId()}, they called ${calledThreadId}`);
163
+ }
164
+ }
165
+
166
+
167
+ // Verify the caller can sign as the cert
168
+ verify(payload.cert, signature, payload);
169
+
170
+ // Verify we even trust the issuer/cert pair
171
+ validateCertificate(payload.cert, payload.certIssuer);
172
+
173
+ let reconnectNodeId: string | undefined;
174
+ if (payload.mountedPort) {
175
+ // NOTE: We use the common name, and not getNodeIdIP... because anything connecting will need
176
+ // to use HTTPS, so connecting over the IP won't work! (unless they turn off certificate validation,
177
+ // which we shouldn't do...)
178
+ reconnectNodeId = getNodeId(getCommonName(payload.cert), payload.mountedPort);
179
+ }
180
+
181
+ let pubKey = getPublicIdentifier(payload.cert);
182
+ let machineId = getMachineId(getCommonName(payload.certIssuer));
183
+
184
+ callerInfo.set(caller, {
185
+ cert: { certPEM: payload.cert, issuerPEM: payload.certIssuer },
186
+ machineId,
187
+ reconnectNodeId,
188
+ pubKey,
189
+ pubKeyShort: getShortNumber(pubKey),
190
+ });
191
+
192
+ let duration = Date.now() - time;
193
+ console.log(`Authenticated identity for ${caller.nodeId} in ${formatTime(duration)}, at ${Date.now()}`, {
194
+ clientId: caller.nodeId,
195
+ reconnectNodeId,
196
+ duration,
197
+ mountedPort: payload.mountedPort,
198
+ debugEntryPoint: payload.debugEntryPoint,
199
+ });
200
+
201
+ SocketFunction.onNextDisconnect(caller.nodeId, () => {
202
+ // NOTE: I don't really see any purpose of deleting from caller info. I don't think we're going to run out of memory because of too many callers authenticating.
203
+ // However, logging here is useful as it allows us to complete the life cycle so we know how long a client was connected for.
204
+ console.log(`Disconnected client`, {
205
+ clientId: caller.nodeId,
206
+ });
207
+ });
208
+ }
209
+ }
210
+
211
+ const IdentityController = SocketFunction.register(
212
+ "IdentityController-4a1b77d7-2825-433e-a27c-e2b8a2e74457",
213
+ new IdentityControllerBase(),
214
+ () => ({
215
+ changeIdentity: {
216
+ // If we use hooks here, it can cause an infinite loop (which SocketFunction just turns into
217
+ // a deadlock). So... we just don't use any hooks for this call.
218
+ // Actually.. I think we were seeing a different issue. And client hooks are required,
219
+ // so we can trust the remote cert correctly.
220
+ // noClientHooks: true,
221
+ // noDefaultHooks: true,
222
+ },
223
+ })
224
+ );
225
+
226
+ // IMPORTANT! We need to cache per connection, not per nodeId, so caching based on connectionId is required!
227
+ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectionId: { nodeId: string }) {
228
+ let nodeId = connectionId.nodeId;
229
+ let threadKeyCert = getThreadKeyCert();
230
+ let issuer = getIdentityCA();
231
+ let payload: ChangeIdentityPayload = {
232
+ time: Date.now(),
233
+ serverId: nodeId,
234
+ cert: threadKeyCert.cert.toString(),
235
+ certIssuer: issuer.cert.toString(),
236
+ mountedPort: getNodeIdLocation(SocketFunction.mountedNodeId)?.port,
237
+ debugEntryPoint: isNode() ? process.argv[1] : "browser",
238
+ clientIsNode: isNode(),
239
+ };
240
+ let signature = sign(threadKeyCert, payload);
241
+ await timeoutToError(
242
+ MAX_CHANGE_IDENTITY_TIMEOUT,
243
+ IdentityController.nodes[nodeId].changeIdentity(signature, payload),
244
+ () => new Error(`Timeout calling changeIdentity for ${nodeId}`)
245
+ );
246
+ });
247
+ SocketFunction.addGlobalClientHook(async function identityHook(context) {
248
+ if (context.call.classGuid === IdentityController._classGuid) return;
249
+ // This is for US to tell them our identity. And if they established the connection the identity will come from their original connection url (that they used to connect to us), and they validated it either being a real certificate, or they added the cert from the trusted backblaze bucket. If it just from a real certificate it means we identified them, but they might not have network trust. But that's fine, as IdentityController is JUST for identification, and if it's a real certificate we know who they are! (Which doesn't mean we trust them).
250
+ if (isClientNodeId(context.call.nodeId)) {
251
+ return;
252
+ }
253
+ let time = Date.now();
254
+ await changeIdentityOnce(context.connectionId);
255
+ let duration = Date.now() - time;
256
+ if (duration > 200) {
257
+ console.log(red(`IdentityHook took ${formatTime(duration)} for ${context.connectionId.nodeId} ${context.call.classGuid}.${context.call.functionName}`));
258
+ }
259
259
  });