querysub 0.646.0 → 0.648.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.646.0",
3
+ "version": "0.648.0",
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",
@@ -12,28 +12,39 @@ import { getClientNodeId, getNodeId, getNodeIdDomain, getNodeIdIP, getNodeIdLoca
12
12
  import { decodeNodeId, getCommonName, getIdentityCA, getMachineId, getOwnMachineId, getPublicIdentifier, getThreadKeyCert, parseCert, sign, validateCertificate, verify } from "sliftutils/misc/https/certs";
13
13
  import { getShortNumber } from "socket-function/src/bits";
14
14
  import { measureBlock, measureFnc, measureWrap } from "socket-function/src/profiling/measure";
15
- import { timeoutToError } from "../errors";
15
+ import { logErrors, timeoutToError } from "../errors";
16
16
  import { delay } from "socket-function/src/batching";
17
17
  import { formatTime } from "socket-function/src/formatting/format";
18
18
  import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
19
19
  import { red } from "socket-function/src/formatting/logColors";
20
20
  import { isNode } from "typesafecss";
21
21
  import { areNodeIdsEqual, getOwnNodeId, getOwnThreadId } from "../-f-node-discovery/NodeDiscovery";
22
- import { timeInMinute, isIpDomain } from "socket-function/src/misc";
22
+ import { timeInMinute, isIpDomain, PromiseObj } from "socket-function/src/misc";
23
23
  import { isClient, isServer } from "../config2";
24
24
  import { getDomain } from "../config";
25
25
 
26
26
  // 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
27
27
  const MAX_CHANGE_IDENTITY_TIMEOUT = timeInMinute * 5;
28
+ // Such a large timeout because sometimes startup can be really slow, such as if our storage system has to fall back (as the storage system does retries, etc).
29
+ const MOUNT_WAIT_TIMEOUT = timeInMinute * 3;
28
30
 
29
31
  let callerInfo = new Map<CallerContext, {
30
- reconnectNodeId: string | undefined;
32
+ reconnectNodeId: string | Promise<string>;
33
+ reconnectNodeIdPromiseObj: PromiseObj<string> | undefined;
34
+ certCommonName: string;
31
35
  machineId: string;
32
36
  cert: CertInfo;
33
37
  pubKey: Buffer;
34
38
  pubKeyShort: number;
35
39
  }>();
36
- 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`;
40
+
41
+ function getCallerInfoAssert(callerContext: CallerContext) {
42
+ let info = callerInfo.get(callerContext);
43
+ if (!info) {
44
+ throw new Error(`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`);
45
+ }
46
+ return info;
47
+ }
37
48
 
38
49
  /** Gets the nodeId of the caller suitable for reconnecting (to the same process).
39
50
  * - Also useful for logs, to identify a node on the network.
@@ -47,27 +58,24 @@ export function IdentityController_getReconnectNodeId(callerContext: CallerConte
47
58
  let info = callerInfo.get(callerContext);
48
59
  if (!info) {
49
60
  if (allowUndefined) return undefined;
50
- throw new Error(callerInfoErrorString);
61
+ info = getCallerInfoAssert(callerContext);
62
+ }
63
+ if (typeof info.reconnectNodeId === "string") {
64
+ return info.reconnectNodeId;
51
65
  }
52
- return info.reconnectNodeId;
66
+ return undefined;
53
67
  }
54
- export function IdentityController_getReconnectNodeIdAssert(callerContext: CallerContext): string {
68
+ export async function IdentityController_getReconnectNodeIdAssert(callerContext: CallerContext): Promise<string> {
55
69
  if (!isClientNodeId(callerContext.nodeId)) {
56
70
  return callerContext.nodeId;
57
71
  }
58
- let reconnectId = IdentityController_getReconnectNodeId(callerContext);
59
- if (!reconnectId) {
60
- console.error(`Caller did not mount before connecting. This call requires the caller to be listening.`, {
61
- callerNodeId: callerContext.nodeId,
62
- });
63
- throw new Error(`Caller did not mount before connecting. This call requires the caller to be listening.`);
64
- }
65
- return reconnectId;
72
+ let info = getCallerInfoAssert(callerContext);
73
+ return await info.reconnectNodeId;
66
74
  }
67
75
  export function IdentityController_getSecureIP(callerContext: CallerContext): string {
68
76
  return getNodeIdIP(callerContext.nodeId);
69
77
  }
70
- export function IdentityController_getCurrentReconnectNodeIdAssert(): string {
78
+ export function IdentityController_getCurrentReconnectNodeIdAssert(): Promise<string> {
71
79
  return IdentityController_getReconnectNodeIdAssert(SocketFunction.getCaller());
72
80
  }
73
81
  export function IdentityController_getCurrentReconnectNodeId(): string | undefined {
@@ -76,7 +84,11 @@ export function IdentityController_getCurrentReconnectNodeId(): string | undefin
76
84
 
77
85
  export function debugNodeId(nodeId: string) {
78
86
  let info = Array.from(callerInfo.entries()).find(x => x[0].nodeId === nodeId);
79
- return info?.[1].reconnectNodeId || nodeId;
87
+ let reconnectNodeId = info?.[1].reconnectNodeId;
88
+ if (typeof reconnectNodeId !== "string") {
89
+ return nodeId;
90
+ }
91
+ return reconnectNodeId;
80
92
  };
81
93
  (globalThis as any).debugNodeId = debugNodeId;
82
94
 
@@ -97,21 +109,19 @@ export function IdentityController_getMachineId(callerContext: CallerContext, al
97
109
  let info = callerInfo.get(callerContext);
98
110
  if (!info) {
99
111
  if (allowEmpty) return "";
100
- throw new Error(callerInfoErrorString);
112
+ info = getCallerInfoAssert(callerContext);
101
113
  }
102
114
  return info.machineId;
103
115
  }
104
116
 
105
117
  export type CertInfo = { certPEM: Buffer | string; issuerPEM: Buffer | string; };
106
118
  export function IdentityController_getCertInfo(callerContext: CallerContext): CertInfo {
107
- let info = callerInfo.get(callerContext);
108
- if (!info) throw new Error(callerInfoErrorString);
119
+ let info = getCallerInfoAssert(callerContext);
109
120
  return info.cert;
110
121
  }
111
122
 
112
123
  export function IdentityController_getPubKeyShort(callerContext: CallerContext): number {
113
- let info = callerInfo.get(callerContext);
114
- if (!info) throw new Error(callerInfoErrorString);
124
+ let info = getCallerInfoAssert(callerContext);
115
125
  return info.pubKeyShort;
116
126
  }
117
127
  export const IdentityController_getOwnPubKeyShort = lazy((): number => {
@@ -128,7 +138,7 @@ export interface ChangeIdentityPayload {
128
138
  serverId: string;
129
139
  mountedPort: number | undefined;
130
140
  debugEntryPoint: string | undefined;
131
- clientIsNode: boolean;
141
+ callerIsNode: boolean;
132
142
  }
133
143
  class IdentityControllerBase {
134
144
  // IMPORTANT! We HAVE to call changeIdentity NOT JUST because we can't use peer certificates in the browser, BUT, also
@@ -146,7 +156,7 @@ class IdentityControllerBase {
146
156
  throw new Error(`Signed payload too old, ${payload.time} < ${signedThreshold} from ${caller.localNodeId} (${caller.nodeId})`);
147
157
  }
148
158
 
149
- if (payload.clientIsNode && payload.serverId !== getOwnNodeId()) {
159
+ if (payload.callerIsNode && payload.serverId !== getOwnNodeId()) {
150
160
  // This is extremely common when we reuse ports, which we do frequently for the edge nodes.
151
161
  throw new Error(`You tried to contact another server. We are ${getOwnNodeId()}, you tried to contact ${payload.serverId}.`);
152
162
  }
@@ -159,7 +169,7 @@ class IdentityControllerBase {
159
169
  throw new Error(`Identity is for another server! The connection is calling us ${localNodeId}, but signature is for ${payload.serverId}`);
160
170
  }
161
171
  // 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.
162
- if (payload.clientIsNode) {
172
+ if (payload.callerIsNode) {
163
173
  let calledMachineId = getMachineId(payload.serverId, getDomain());
164
174
  if (calledMachineId !== "127-0-0-1" && calledMachineId !== getOwnMachineId(getDomain())) {
165
175
  throw new Error(`Tried to call a different machine. We are ${getOwnMachineId(getDomain())}, they called ${calledMachineId}`);
@@ -177,12 +187,24 @@ class IdentityControllerBase {
177
187
  // Verify we even trust the issuer/cert pair
178
188
  validateCertificate(getDomain(), payload.cert, payload.certIssuer);
179
189
 
180
- let reconnectNodeId: string | undefined;
190
+ // NOTE: We use the common name, and not getNodeIdIP... because anything connecting will need
191
+ // to use HTTPS, so connecting over the IP won't work! (unless they turn off certificate validation,
192
+ // which we shouldn't do...)
193
+ let certCommonName = getCommonName(payload.cert);
194
+ let reconnectNodeId: string | Promise<string>;
195
+ let reconnectNodeIdPromiseObj: PromiseObj<string> | undefined;
181
196
  if (payload.mountedPort) {
182
- // NOTE: We use the common name, and not getNodeIdIP... because anything connecting will need
183
- // to use HTTPS, so connecting over the IP won't work! (unless they turn off certificate validation,
184
- // which we shouldn't do...)
185
- reconnectNodeId = getNodeId(getCommonName(payload.cert), payload.mountedPort);
197
+ reconnectNodeId = getNodeId(certCommonName, payload.mountedPort);
198
+ } else {
199
+ reconnectNodeIdPromiseObj = new PromiseObj<string>();
200
+ reconnectNodeId = reconnectNodeIdPromiseObj.promise;
201
+ setTimeout(() => {
202
+ reconnectNodeIdPromiseObj?.reject(new Error(`Caller did not call setMountedPort within ${formatTime(MOUNT_WAIT_TIMEOUT)} of connecting. This call requires the caller to be listening.`));
203
+ }, MOUNT_WAIT_TIMEOUT);
204
+ }
205
+
206
+ if (!payload.callerIsNode) {
207
+ reconnectNodeIdPromiseObj?.reject(new Error(`The caller says they are a client, so they are never going to have a reconnect node id`));
186
208
  }
187
209
 
188
210
  let pubKey = getPublicIdentifier(payload.cert);
@@ -191,7 +213,9 @@ class IdentityControllerBase {
191
213
  callerInfo.set(caller, {
192
214
  cert: { certPEM: payload.cert, issuerPEM: payload.certIssuer },
193
215
  machineId,
216
+ certCommonName,
194
217
  reconnectNodeId,
218
+ reconnectNodeIdPromiseObj,
195
219
  pubKey,
196
220
  pubKeyShort: getShortNumber(pubKey),
197
221
  });
@@ -206,13 +230,28 @@ class IdentityControllerBase {
206
230
  });
207
231
 
208
232
  SocketFunction.onNextDisconnect(caller.nodeId, () => {
209
- // 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.
233
+ // 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.
210
234
  // However, logging here is useful as it allows us to complete the life cycle so we know how long a client was connected for.
211
235
  console.info(`Disconnected client`, {
212
236
  clientId: caller.nodeId,
213
237
  });
214
238
  });
215
239
  }
240
+
241
+ @measureFnc
242
+ public async setMountedPort(mountedPort: number) {
243
+ const caller = SocketFunction.getCaller();
244
+ let info = getCallerInfoAssert(caller);
245
+ if (typeof info.reconnectNodeId === "string") throw new Error(`Already have reconnectNodeId, not sure why we are being given another, Have ${info.reconnectNodeId}, given port: ${mountedPort}`);
246
+ let reconnectNodeId = getNodeId(info.certCommonName, mountedPort);
247
+ info.reconnectNodeId = reconnectNodeId;
248
+ info.reconnectNodeIdPromiseObj?.resolve(reconnectNodeId);
249
+ console.info(`Caller mounted after connecting, set reconnect node id`, {
250
+ clientId: caller.nodeId,
251
+ reconnectNodeId,
252
+ mountedPort,
253
+ });
254
+ }
216
255
  }
217
256
 
218
257
  const IdentityController = SocketFunction.register(
@@ -227,6 +266,7 @@ const IdentityController = SocketFunction.register(
227
266
  // noClientHooks: true,
228
267
  // noDefaultHooks: true,
229
268
  },
269
+ setMountedPort: {},
230
270
  })
231
271
  );
232
272
 
@@ -242,7 +282,7 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
242
282
  certIssuer: issuer.cert.toString(),
243
283
  mountedPort: getNodeIdLocation(SocketFunction.mountedNodeId)?.port,
244
284
  debugEntryPoint: isServer() ? process.argv[1] : "browser",
245
- clientIsNode: isServer() && !isIpDomain(nodeId),
285
+ callerIsNode: isServer() && !isIpDomain(nodeId),
246
286
  };
247
287
  let signature = sign(threadKeyCert, payload);
248
288
  await timeoutToError(
@@ -250,6 +290,15 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
250
290
  IdentityController.nodes[nodeId].changeIdentity(signature, payload),
251
291
  () => new Error(`Timeout calling changeIdentity for ${nodeId}`)
252
292
  );
293
+ if (isServer() && !payload.mountedPort) {
294
+ logErrors((async () => {
295
+ await SocketFunction.mountPromise;
296
+ let mountedPort = getNodeIdLocation(SocketFunction.mountedNodeId)?.port;
297
+ if (!mountedPort) throw new Error(`Mounted after connecting to ${nodeId}, but no mounted port found? mounted node id: ${SocketFunction.mountedNodeId}`);
298
+ console.info(`Mounted after connecting to ${nodeId}, sending mounted port ${mountedPort}`);
299
+ await IdentityController.nodes[nodeId].setMountedPort(mountedPort);
300
+ })());
301
+ }
253
302
  });
254
303
  let startupTime = Date.now();
255
304
  async function identityHook(context: ClientHookContext) {
@@ -180,7 +180,7 @@ class AuthorityLookup {
180
180
  isReady: boolean;
181
181
  scheduledShutdownTime?: number;
182
182
  }> {
183
- let nodeId = IdentityController_getCurrentReconnectNodeIdAssert();
183
+ let nodeId = await IdentityController_getCurrentReconnectNodeIdAssert();
184
184
  this.updatePaths(nodeId, config.spec, config.isReady, config.scheduledShutdownTime);
185
185
  console.info(`Received network sync, returning our data`, {
186
186
  nodeId,
@@ -211,7 +211,7 @@ class AuthorityLookup {
211
211
  }, stopObj);
212
212
 
213
213
  const onNodeRemoved = () => {
214
- this.topology.nodes.delete(nodeId);
214
+ this.removeNode(nodeId, "node removed from node discovery");
215
215
  stopObj.stop = true;
216
216
  this.connectTo.clear(nodeId);
217
217
  this.disconnectWatch.clear(nodeId);
@@ -230,15 +230,20 @@ class AuthorityLookup {
230
230
  let onDisconnect = () => {
231
231
  SocketFunction.onNextDisconnect(nodeId, onDisconnect, "iKnowThatServerNodeIdsMayReconnect_andIHandleReconnections");
232
232
  // NOTE: Will be re-added once we receive an updatePaths call (or the node will be removed by NodeDiscovery, in which case we will be entirely cleaned up).
233
- this.topology.nodes.delete(nodeId);
233
+ this.removeNode(nodeId, "socket disconnected");
234
234
  };
235
235
  SocketFunction.onNextDisconnect(nodeId, onDisconnect, "iKnowThatServerNodeIdsMayReconnect_andIHandleReconnections");
236
236
  });
237
+ private removeNode(nodeId: string, reason: string) {
238
+ if (!this.topology.nodes.has(nodeId)) return;
239
+ this.topology.nodes.delete(nodeId);
240
+ console.log(`Removed authority topology entry for ${nodeId} (${this.topology.nodes.size} entries remaining). Reason: ${reason}`);
241
+ }
237
242
  private async syncNodeNow(nodeId: string) {
238
243
  try {
239
244
  await this.syncNodeNowBase(nodeId);
240
- } catch {
241
- this.topology.nodes.delete(nodeId);
245
+ } catch (e: any) {
246
+ this.removeNode(nodeId, `sync failed: ${String(e?.stack || e)}`);
242
247
  }
243
248
  }
244
249
 
@@ -210,6 +210,7 @@ class PathValueCommitter {
210
210
  timeId: pathValue.time.time,
211
211
  source: pathValue.source,
212
212
  otherAuthorities,
213
+ rawTopology: authorityLookup.getTopologySync(),
213
214
  });
214
215
  continue;
215
216
  }
@@ -66,7 +66,8 @@ class StorageLogsControllerBase {
66
66
  let stores: ServerPathValue["stores"] = [];
67
67
  try {
68
68
  let routing = await controller.get2({ account, bucketName, path: ROUTING_FILE, sourceConfig: fabricatedSource, internal: true });
69
- let sources = routing && parseRoutingData(routing.data).sources.map(normalizeSource) || [];
69
+ let routingParsed = routing && parseRoutingData(routing.data);
70
+ let sources = routingParsed && routingParsed.sources.map(normalizeSource) || [];
70
71
  let seenNames = new Set<string>();
71
72
  for (let source of sources) {
72
73
  if (source.type !== "remote") continue;