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 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
- # then online: HTTPS, an administrator, a password shown once -- after a real
220
- # test from the internet (Let's Encrypt staging); the relay is set up over SSH
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
- async function waitForSetupKey(sys, o, at, deadline) {
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
- await sys.sleep(5000);
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; }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "clavarium",
3
- "version": "0.1.0-next.7",
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.7",
9
+ "version": "0.1.0-next.8",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clavarium",
3
- "version": "0.1.0-next.7",
3
+ "version": "0.1.0-next.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A conversation is a VM with a terminal in it. Everything else is a folder.",