clavarium 0.1.0-next.7 → 0.1.0-next.8
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/README.md +6 -2
- package/deploy/online.mjs +46 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -216,8 +216,12 @@ The host needs `tmux` and `bindfs` (the read-only views).
|
|
|
216
216
|
# the stable version from npm, in local mode, asking nothing (Node.js pinned and
|
|
217
217
|
# checked first); it prints the next command and the public keys (text and QR)
|
|
218
218
|
curl -fsSL https://clavarium.org/install.sh | sudo bash
|
|
219
|
-
#
|
|
220
|
-
#
|
|
219
|
+
# online in the same line: it installs, shows the setup key (text and QR), waits for
|
|
220
|
+
# the relay, sets it up over SSH, tests from the internet, shows the password once
|
|
221
|
+
curl -fsSL https://clavarium.org/install.sh | sudo bash -s -- --relay relay.example.net \
|
|
222
|
+
--relay-user ubuntu --domain clav.example.com --admin you@example.com
|
|
223
|
+
curl -fsSL https://clavarium.org/install.sh | sudo bash -s -- --direct --domain clav.example.com
|
|
224
|
+
# or later, from local mode
|
|
221
225
|
sudo clavarium online --relay relay.example.net --domain clav.example.com
|
|
222
226
|
sudo clavarium online --port --domain clav.example.com
|
|
223
227
|
sudo clavarium key # the public keys again
|
package/deploy/online.mjs
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* `sys`, so the tests can stand in for SSH, npm and DNS.
|
|
25
25
|
*/
|
|
26
26
|
import { spawn } from 'node:child_process';
|
|
27
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, mkdtempSync, renameSync, openSync, readSync, writeSync, closeSync } from 'node:fs';
|
|
27
|
+
import { createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, mkdtempSync, renameSync, openSync, readSync, writeSync, closeSync } from 'node:fs';
|
|
28
28
|
import { promises as dns } from 'node:dns';
|
|
29
|
+
import { connect } from 'node:net';
|
|
29
30
|
import { createInterface } from 'node:readline';
|
|
30
31
|
import { tmpdir } from 'node:os';
|
|
31
32
|
import { join } from 'node:path';
|
|
@@ -144,7 +145,7 @@ export async function showKeys(sys, P = PATHS, { which = 'all' } = {}) {
|
|
|
144
145
|
|
|
145
146
|
/* ---------------------------------- SSH ---------------------------------- */
|
|
146
147
|
const sshOptions = (key, knownHosts, port) => [
|
|
147
|
-
'-o', 'BatchMode=yes', '-o', 'IdentitiesOnly=yes', '-o', `IdentityFile=${key}`,
|
|
148
|
+
'-o', 'BatchMode=yes', '-o', 'IdentitiesOnly=yes', '-o', `IdentityFile=${key}`, '-o', 'PreferredAuthentications=publickey', '-o', 'NumberOfPasswordPrompts=0',
|
|
148
149
|
'-o', `UserKnownHostsFile=${knownHosts}`, '-o', 'StrictHostKeyChecking=yes', '-o', 'ConnectTimeout=10', '-p', String(port),
|
|
149
150
|
];
|
|
150
151
|
const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
@@ -304,12 +305,34 @@ async function fingerprintOf(sys, P, line) {
|
|
|
304
305
|
return { file, print };
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
|
|
308
|
+
/* NOT BANNED WHILE WAITING. A relay may run fail2ban (by default: banned after
|
|
309
|
+
5 failed logins in 10 minutes). So: while its SSH port does not even answer,
|
|
310
|
+
only that port is looked at (no login, nothing counted); once it answers,
|
|
311
|
+
one key, one try, then 5 minutes, a second, 5 minutes, a third (the
|
|
312
|
+
operator's rule): after three failures it waits for Enter on the keyboard
|
|
313
|
+
(with --yes and no keyboard: 5 minutes more each time). */
|
|
314
|
+
export const KNOCK_MS = 5 * 60_000;
|
|
315
|
+
export const KNOCKS = 3;
|
|
316
|
+
const portAnswers = (host, port, ms = 5000) => new Promise((resolve) => {
|
|
317
|
+
const s = connect({ host, port: Number(port) });
|
|
318
|
+
const done = (ok) => { s.destroy(); resolve(ok); };
|
|
319
|
+
s.setTimeout(ms, () => done(false)); s.once('connect', () => done(true)); s.once('error', () => done(false));
|
|
320
|
+
});
|
|
321
|
+
export async function waitForSetupKey(sys, o, at, deadline) {
|
|
308
322
|
const check = o.user === 'root' ? 'true' : 'sudo -n true';
|
|
309
323
|
let said = '';
|
|
310
324
|
const say = (text) => { if (text !== said) { said = text; sys.print(text); } };
|
|
325
|
+
let tries = 0;
|
|
326
|
+
const knock = sys.portAnswers ?? portAnswers;
|
|
311
327
|
for (;;) {
|
|
328
|
+
if (!(await knock(o.relay, o.relayPort ?? 22))) {
|
|
329
|
+
say(`${o.relay} does not answer on its SSH port yet: waiting, without trying to log in...`);
|
|
330
|
+
if (sys.now() > deadline) throw new Failed(`${o.relay} did not answer on its SSH port within ${WAIT_MS / 60000} minutes`);
|
|
331
|
+
await sys.sleep(15_000);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
312
334
|
const r = await sys.run(['ssh', ...at, `${o.user}@${o.relay}`, check]);
|
|
335
|
+
tries += 1;
|
|
313
336
|
if (r.code === 0) { sys.print(`The setup key opens ${o.user}@${o.relay}${o.user === 'root' ? '' : ', with sudo'}.`); return; }
|
|
314
337
|
const err = r.err || '';
|
|
315
338
|
if (/password is required|not in the sudoers|may not run sudo/i.test(err)) say(`${o.user} on ${o.relay} needs sudo without a password: add "${o.user} ALL=(ALL) NOPASSWD: ALL" in /etc/sudoers.d there. Waiting...`);
|
|
@@ -317,7 +340,14 @@ async function waitForSetupKey(sys, o, at, deadline) {
|
|
|
317
340
|
else if (/Host key verification failed|REMOTE HOST IDENTIFICATION/i.test(err)) throw new Refused(`${o.relay} answered with another key than a moment ago: stopped`);
|
|
318
341
|
else say(`${o.relay} does not answer yet (${tail(r)}). Waiting...`);
|
|
319
342
|
if (sys.now() > deadline) throw new Failed(`the setup key was not accepted on ${o.user}@${o.relay} within ${WAIT_MS / 60000} minutes`);
|
|
320
|
-
|
|
343
|
+
if (tries >= KNOCKS && !o.yes && sys.waitEnter) {
|
|
344
|
+
sys.print(` ${KNOCKS} tries refused: put the setup key on the relay, then press Enter to try again (Ctrl-C to stop).`);
|
|
345
|
+
if (!(await sys.waitEnter())) throw new Failed('no keyboard to wait on: run sudo clavarium online again once the key is on the relay');
|
|
346
|
+
tries = 0;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
sys.print(` next try in ${KNOCK_MS / 60_000} min (${tries} of ${KNOCKS})${sys.sleepOrEnter && !o.yes ? ' -- or press Enter to try now' : ''}`);
|
|
350
|
+
await (sys.sleepOrEnter && !o.yes ? sys.sleepOrEnter(KNOCK_MS) : sys.sleep(KNOCK_MS));
|
|
321
351
|
}
|
|
322
352
|
}
|
|
323
353
|
|
|
@@ -478,6 +508,18 @@ export function realSys() {
|
|
|
478
508
|
const exited = new Promise((resolve) => { child.on('error', () => { ended = true; resolve({ code: 127 }); }); child.on('close', (code) => { ended = true; resolve({ code: code ?? 1 }); }); });
|
|
479
509
|
return { exited, ended: () => ended, stop: () => { if (!ended) child.kill('SIGTERM'); } };
|
|
480
510
|
},
|
|
511
|
+
/* A wait that Enter on the keyboard cuts short (no keyboard: the whole wait). */
|
|
512
|
+
sleepOrEnter: (ms) => new Promise((resolve) => {
|
|
513
|
+
let tty; let timer;
|
|
514
|
+
const done = () => { clearTimeout(timer); try { tty?.destroy(); } catch { /* gone */ } resolve(); };
|
|
515
|
+
timer = setTimeout(done, ms);
|
|
516
|
+
try { tty = createReadStream('/dev/tty'); tty.on('data', (d) => { if (d.includes(10) || d.includes(13)) done(); }); tty.on('error', () => {}); } catch { /* no keyboard: the timer alone */ }
|
|
517
|
+
}),
|
|
518
|
+
waitEnter: async () => {
|
|
519
|
+
let fd;
|
|
520
|
+
try { fd = openSync('/dev/tty', 'r'); } catch { return false; }
|
|
521
|
+
try { const buf = Buffer.alloc(256); let s = ''; while (!s.includes('\n')) { const n = readSync(fd, buf, 0, buf.length, null); if (!n) break; s += buf.toString('utf8', 0, n); } return true; } finally { closeSync(fd); }
|
|
522
|
+
},
|
|
481
523
|
ask: async (question) => {
|
|
482
524
|
let fd;
|
|
483
525
|
try { fd = openSync('/dev/tty', 'r+'); } catch { return false; }
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.8",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clavarium",
|
|
9
|
-
"version": "0.1.0-next.
|
|
9
|
+
"version": "0.1.0-next.8",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|