querysub 0.433.0 → 0.437.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.
- package/.eslintrc.js +50 -50
- package/bin/deploy.js +0 -0
- package/bin/function.js +0 -0
- package/bin/server.js +0 -0
- package/costsBenefits.txt +115 -115
- package/deploy.ts +2 -2
- package/package.json +1 -1
- package/spec.txt +1192 -1192
- package/src/-a-archives/archives.ts +202 -202
- package/src/-a-archives/archivesDisk.ts +454 -454
- package/src/-a-auth/certs.ts +540 -540
- package/src/-a-auth/node-forge-ed25519.d.ts +16 -16
- package/src/-b-authorities/dnsAuthority.ts +138 -138
- package/src/-c-identity/IdentityController.ts +258 -258
- package/src/-d-trust/NetworkTrust2.ts +180 -180
- package/src/-e-certs/EdgeCertController.ts +252 -252
- package/src/-e-certs/certAuthority.ts +201 -201
- package/src/-f-node-discovery/NodeDiscovery.ts +640 -640
- package/src/-g-core-values/NodeCapabilities.ts +200 -200
- package/src/-h-path-value-serialize/stringSerializer.ts +175 -175
- package/src/0-path-value-core/PathValueCommitter.ts +468 -468
- package/src/0-path-value-core/pathValueCore.ts +2 -2
- package/src/2-proxy/PathValueProxyWatcher.ts +2542 -2542
- package/src/2-proxy/TransactionDelayer.ts +94 -94
- package/src/2-proxy/pathDatabaseProxyBase.ts +36 -36
- package/src/2-proxy/pathValueProxy.ts +159 -159
- package/src/3-path-functions/PathFunctionRunnerMain.ts +87 -87
- package/src/3-path-functions/pathFunctionLoader.ts +516 -516
- package/src/3-path-functions/tests/rejectTest.ts +76 -76
- package/src/4-deploy/deployCheck.ts +6 -6
- package/src/4-dom/css.tsx +29 -29
- package/src/4-dom/cssTypes.d.ts +211 -211
- package/src/4-dom/qreact.tsx +2799 -2799
- package/src/4-dom/qreactTest.tsx +410 -410
- package/src/4-querysub/permissions.ts +335 -335
- package/src/4-querysub/querysubPrediction.ts +483 -483
- package/src/5-diagnostics/qreactDebug.tsx +346 -346
- package/src/TestController.ts +34 -34
- package/src/bits.ts +104 -104
- package/src/buffers.ts +69 -69
- package/src/diagnostics/ActionsHistory.ts +57 -57
- package/src/diagnostics/listenOnDebugger.ts +71 -71
- package/src/diagnostics/periodic.ts +111 -111
- package/src/diagnostics/trackResources.ts +91 -91
- package/src/diagnostics/watchdog.ts +120 -120
- package/src/errors.ts +133 -133
- package/src/forceProduction.ts +2 -2
- package/src/fs.ts +80 -80
- package/src/functional/diff.ts +857 -857
- package/src/functional/promiseCache.ts +78 -78
- package/src/functional/random.ts +8 -8
- package/src/functional/stats.ts +60 -60
- package/src/heapDumps.ts +665 -665
- package/src/https.ts +1 -1
- package/src/library-components/AspectSizedComponent.tsx +87 -87
- package/src/library-components/ButtonSelector.tsx +64 -64
- package/src/library-components/DropdownCustom.tsx +150 -150
- package/src/library-components/DropdownSelector.tsx +31 -31
- package/src/library-components/InlinePopup.tsx +66 -66
- package/src/misc/color.ts +29 -29
- package/src/misc/hash.ts +83 -83
- package/src/misc/ipPong.js +13 -13
- package/src/misc/networking.ts +1 -1
- package/src/misc/random.ts +44 -44
- package/src/misc.ts +196 -196
- package/src/path.ts +255 -255
- package/src/persistentLocalStore.ts +41 -41
- package/src/promise.ts +14 -14
- package/src/storage/fileSystemPointer.ts +71 -71
- package/src/test/heapProcess.ts +35 -35
- package/src/zip.ts +15 -15
- package/tsconfig.json +26 -26
- 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
|
});
|