querysub 0.662.0 → 0.663.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.
|
|
3
|
+
"version": "0.663.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",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
80
80
|
"pako": "^2.1.0",
|
|
81
81
|
"peggy": "^5.0.6",
|
|
82
|
-
"sliftutils": "^1.7.
|
|
82
|
+
"sliftutils": "^1.7.133",
|
|
83
83
|
"socket-function": "^1.2.35",
|
|
84
84
|
"terser": "^5.31.0",
|
|
85
85
|
"typenode": "^6.6.1",
|
|
@@ -140,6 +140,12 @@ export interface ChangeIdentityPayload {
|
|
|
140
140
|
debugEntryPoint: string | undefined;
|
|
141
141
|
callerIsNode: boolean;
|
|
142
142
|
}
|
|
143
|
+
export interface ChangeIdentityResponse {
|
|
144
|
+
payload: ChangeIdentityPayload;
|
|
145
|
+
signature: string;
|
|
146
|
+
cert: string;
|
|
147
|
+
certIssuer: string;
|
|
148
|
+
}
|
|
143
149
|
class IdentityControllerBase {
|
|
144
150
|
// IMPORTANT! We HAVE to call changeIdentity NOT JUST because we can't use peer certificates in the browser, BUT, also
|
|
145
151
|
// because this removes the need to load peer certificates into the trust store. This greatly simplifies
|
|
@@ -147,7 +153,7 @@ class IdentityControllerBase {
|
|
|
147
153
|
// if we want to trust a node (instead of having to trust a node before it can connect to us, because
|
|
148
154
|
// NodeJS will throw/ignore the peer cert if it isn't trusted).
|
|
149
155
|
@measureFnc
|
|
150
|
-
public async changeIdentity(signature: string, payload: ChangeIdentityPayload) {
|
|
156
|
+
public async changeIdentity(signature: string, payload: ChangeIdentityPayload): Promise<ChangeIdentityResponse> {
|
|
151
157
|
let time = Date.now();
|
|
152
158
|
const caller = SocketFunction.getCaller();
|
|
153
159
|
// Verify it wasn't signed too long ago (to make signature stealing WAY more difficult).
|
|
@@ -236,6 +242,14 @@ class IdentityControllerBase {
|
|
|
236
242
|
clientId: caller.nodeId,
|
|
237
243
|
});
|
|
238
244
|
});
|
|
245
|
+
|
|
246
|
+
let ourThreadKeyCert = getThreadKeyCert(getDomain());
|
|
247
|
+
return {
|
|
248
|
+
payload,
|
|
249
|
+
signature: sign(ourThreadKeyCert, payload),
|
|
250
|
+
cert: ourThreadKeyCert.cert.toString(),
|
|
251
|
+
certIssuer: getIdentityCA(getDomain()).cert.toString(),
|
|
252
|
+
};
|
|
239
253
|
}
|
|
240
254
|
|
|
241
255
|
@measureFnc
|
|
@@ -285,11 +299,28 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
|
|
|
285
299
|
callerIsNode: isServer() && !isIpDomain(nodeId),
|
|
286
300
|
};
|
|
287
301
|
let signature = sign(threadKeyCert, payload);
|
|
288
|
-
await timeoutToError(
|
|
302
|
+
let response = await timeoutToError(
|
|
289
303
|
MAX_CHANGE_IDENTITY_TIMEOUT,
|
|
290
304
|
IdentityController.nodes[nodeId].changeIdentity(signature, payload),
|
|
291
305
|
() => new Error(`Timeout calling changeIdentity for ${nodeId}`)
|
|
292
306
|
);
|
|
307
|
+
if (!response) {
|
|
308
|
+
throw new Error(`No identity response from ${nodeId}, so we can't know who we are talking to.`);
|
|
309
|
+
}
|
|
310
|
+
if (JSON.stringify(response.payload) !== JSON.stringify(payload)) {
|
|
311
|
+
throw new Error(`Identity response from ${nodeId} signed a different payload than we sent. Sent ${JSON.stringify(payload)}, they signed ${JSON.stringify(response.payload)}`);
|
|
312
|
+
}
|
|
313
|
+
validateCertificate(getDomain(), response.cert, response.certIssuer);
|
|
314
|
+
verify(response.cert, response.signature, response.payload);
|
|
315
|
+
let nodeObj = decodeNodeId(nodeId, getDomain());
|
|
316
|
+
// If we're not talking to a specific thread, then it likely means there's no machine ID either, ex, it's 127-0-0-1.querysubtest.com. And we chose to talk to this host. The node ID comes from us, So we're not really worried about attackers using this to trick us into thinking there's somebody else.
|
|
317
|
+
if (nodeObj?.threadId) {
|
|
318
|
+
let signerMachineId = getMachineId(getCommonName(response.certIssuer), getDomain());
|
|
319
|
+
let expectedMachineId = nodeObj.machineId;
|
|
320
|
+
if (signerMachineId !== expectedMachineId) {
|
|
321
|
+
throw new Error(`Identity response is from another machine. We connected to ${nodeId} (machine ${expectedMachineId}), but the response was signed by machine ${signerMachineId}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
293
324
|
if (isServer() && !payload.mountedPort) {
|
|
294
325
|
logErrors((async () => {
|
|
295
326
|
await SocketFunction.mountPromise;
|
|
@@ -14,6 +14,8 @@ import { SERVICE_NAME, SERVICE_UNIT_NAME } from "./machineDaemonShared";
|
|
|
14
14
|
Querysub;
|
|
15
15
|
|
|
16
16
|
const pinnedNodeVersion = 22;
|
|
17
|
+
// The Node we target: covers lz4's WASM reference types (externref, default-on from Node 18) with headroom, without going all the way to pinnedNodeVersion on machines that already work. Existing machines below this are upgraded to exactly this.
|
|
18
|
+
const minimumNodeMajor = 20;
|
|
17
19
|
|
|
18
20
|
const DAEMON_SCRIPT_NAME = "machine-daemon.sh";
|
|
19
21
|
// How long after the process dies before it is started again. Short, because until it is back nothing on the machine is being deployed or repaired.
|
|
@@ -120,6 +122,52 @@ async function sshValue(sshRemote: string, command: string, config?: { nothrow?:
|
|
|
120
122
|
return (output.trim().split("\n").at(-1) || "").trim();
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
const UNOFFICIAL_BUILDS_URL = "https://unofficial-builds.nodejs.org/download/release";
|
|
126
|
+
// The nodejs project's builds for systems too old for the official binaries (which need glibc 2.28+)
|
|
127
|
+
const UNOFFICIAL_BUILD_FLAVOR = "linux-x64-glibc-217";
|
|
128
|
+
async function installUnofficialNode(sshRemote: string, major: number): Promise<void> {
|
|
129
|
+
let response = await fetch(`${UNOFFICIAL_BUILDS_URL}/index.json`);
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`Expected the unofficial builds index at ${UNOFFICIAL_BUILDS_URL}/index.json to be readable, was ${response.status}`);
|
|
132
|
+
}
|
|
133
|
+
let releases = await response.json() as { version: string; files: string[] }[];
|
|
134
|
+
// Newest first, so find gives the latest release of the requested major
|
|
135
|
+
let release = releases.find(release => release.version.startsWith(`v${major}.`) && release.files.includes(UNOFFICIAL_BUILD_FLAVOR));
|
|
136
|
+
if (!release) {
|
|
137
|
+
throw new Error(`Expected a ${UNOFFICIAL_BUILD_FLAVOR} build of Node ${major} at ${UNOFFICIAL_BUILDS_URL}, there is none`);
|
|
138
|
+
}
|
|
139
|
+
let name = `node-${release.version}-${UNOFFICIAL_BUILD_FLAVOR}`;
|
|
140
|
+
console.log(`Installing ${name} to /usr/local/bin (which shadows the distro's node)...`);
|
|
141
|
+
await runPromise(`ssh ${sshRemote} "curl -fsSL ${UNOFFICIAL_BUILDS_URL}/${release.version}/${name}.tar.xz -o /tmp/${name}.tar.xz"`);
|
|
142
|
+
await runPromise(`ssh ${sshRemote} "sudo mkdir -p /usr/local/lib/nodejs && sudo tar -xJf /tmp/${name}.tar.xz -C /usr/local/lib/nodejs && rm /tmp/${name}.tar.xz"`);
|
|
143
|
+
for (let bin of ["node", "npm", "npx"]) {
|
|
144
|
+
await runPromise(`ssh ${sshRemote} "sudo ln -sf /usr/local/lib/nodejs/${name}/bin/${bin} /usr/local/bin/${bin}"`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// How often a pending question is re-printed, as other logging keeps running while we wait and buries it
|
|
149
|
+
const QUESTION_REMIND_INTERVAL = 15 * 1000;
|
|
150
|
+
async function askQuestion(prompt: string): Promise<string> {
|
|
151
|
+
const rl = readline.createInterface({
|
|
152
|
+
// Cast, as different @types/node versions disagree on the stream types
|
|
153
|
+
input: process.stdin as unknown as NodeJS.ReadableStream,
|
|
154
|
+
output: process.stdout,
|
|
155
|
+
});
|
|
156
|
+
let remind = setInterval(() => {
|
|
157
|
+
console.log(`\n(still waiting for an answer)\n${prompt}`);
|
|
158
|
+
}, QUESTION_REMIND_INTERVAL);
|
|
159
|
+
try {
|
|
160
|
+
return await new Promise<string>(resolve => {
|
|
161
|
+
rl.question(prompt, answer => {
|
|
162
|
+
resolve(answer.trim());
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
} finally {
|
|
166
|
+
clearInterval(remind);
|
|
167
|
+
rl.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
123
171
|
function getGitHubKeyCachePath(repoUrl: string): string {
|
|
124
172
|
let repoOwner = "";
|
|
125
173
|
let repoName = "";
|
|
@@ -199,19 +247,7 @@ async function getGitHubApiKey(repoUrl: string, sshRemote: string, forceRefresh
|
|
|
199
247
|
|
|
200
248
|
await open(tokenUrl);
|
|
201
249
|
|
|
202
|
-
const
|
|
203
|
-
// Cast, as different @types/node versions disagree on the stream types
|
|
204
|
-
input: process.stdin as unknown as NodeJS.ReadableStream,
|
|
205
|
-
output: process.stdout
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const apiKey = await new Promise<string>((resolve) => {
|
|
209
|
-
const repoInfo = repoOwner && repoName ? ` for repository ${repoOwner}/${repoName}` : "";
|
|
210
|
-
rl.question(`Please paste your GitHub API token (${repoInfo}): `, (answer) => {
|
|
211
|
-
rl.close();
|
|
212
|
-
resolve(answer.trim());
|
|
213
|
-
});
|
|
214
|
-
});
|
|
250
|
+
const apiKey = await askQuestion(`Please paste your GitHub API token (${repoOwner && repoName && `for repository ${repoOwner}/${repoName}` || ""}): `);
|
|
215
251
|
|
|
216
252
|
// Cache the key
|
|
217
253
|
fs.writeFileSync(cacheFile, JSON.stringify({ apiKey }));
|
|
@@ -453,8 +489,32 @@ async function main() {
|
|
|
453
489
|
// 3. Ensure nodejs is installed on remote server
|
|
454
490
|
console.log("Ensuring Node.js is installed...");
|
|
455
491
|
try {
|
|
456
|
-
let nodeVersion = await
|
|
457
|
-
console.log("✅ Node.js already installed:", nodeVersion
|
|
492
|
+
let nodeVersion = await sshValue(sshRemote, "node --version");
|
|
493
|
+
console.log("✅ Node.js already installed:", nodeVersion);
|
|
494
|
+
let nodeMajor = parseInt(nodeVersion.replace(/^v/, "").split(".")[0]);
|
|
495
|
+
if (Number.isFinite(nodeMajor) && nodeMajor < minimumNodeMajor) {
|
|
496
|
+
let answer = await askQuestion(`Node ${nodeVersion} on ${sshRemote} is too old (our code targets Node ${minimumNodeMajor}, and needs at least 18 for WASM reference types). Upgrade it to Node ${minimumNodeMajor} - and no further, so nothing else on the machine breaks? [y/N] `);
|
|
497
|
+
if (answer.trim().toLowerCase().startsWith("y")) {
|
|
498
|
+
console.log(`Upgrading Node.js to v${minimumNodeMajor}...`);
|
|
499
|
+
await runPromise(`ssh ${sshRemote} "curl -fsSL https://deb.nodesource.com/setup_${minimumNodeMajor}.x | sudo -E bash -"`);
|
|
500
|
+
await runPromise(`ssh ${sshRemote} "sudo apt-get install -y nodejs"`);
|
|
501
|
+
let upgradedVersion = await sshValue(sshRemote, "node --version");
|
|
502
|
+
let upgradedMajor = parseInt(upgradedVersion.replace(/^v/, "").split(".")[0]);
|
|
503
|
+
if (!Number.isFinite(upgradedMajor) || upgradedMajor < minimumNodeMajor) {
|
|
504
|
+
// nodesource silently keeps the old repo when it has no build for this distro (its Node 18+ debs need glibc 2.28+), so old machines get the unofficial glibc-2.17 build instead
|
|
505
|
+
console.warn(`⚠️ Node is still ${upgradedVersion} after the nodesource upgrade, so this distro is too old for nodesource. Installing the unofficial glibc-2.17 build of Node ${minimumNodeMajor} into /usr/local/bin instead...`);
|
|
506
|
+
await installUnofficialNode(sshRemote, minimumNodeMajor);
|
|
507
|
+
upgradedVersion = await sshValue(sshRemote, "node --version");
|
|
508
|
+
upgradedMajor = parseInt(upgradedVersion.replace(/^v/, "").split(".")[0]);
|
|
509
|
+
if (!Number.isFinite(upgradedMajor) || upgradedMajor < minimumNodeMajor) {
|
|
510
|
+
throw new Error(`Node is still ${upgradedVersion} after installing the unofficial build. Check what "which -a node" resolves to on ${sshRemote} - something earlier on the PATH is shadowing /usr/local/bin.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
console.log("✅ Node.js upgraded:", upgradedVersion);
|
|
514
|
+
} else {
|
|
515
|
+
console.warn(`⚠️ Keeping Node ${nodeVersion}. The machine process will likely crash with "invalid value type 'externref'" until Node is upgraded.`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
458
518
|
} catch {
|
|
459
519
|
console.log(`Installing Node.js (v${pinnedNodeVersion})...`);
|
|
460
520
|
await runPromise(`ssh ${sshRemote} "curl -fsSL https://deb.nodesource.com/setup_${pinnedNodeVersion}.x | sudo -E bash -"`);
|