clavarium 0.1.0-next.6 → 0.1.0-next.7
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 +10 -0
- package/deploy/clavarium.mjs +31 -3
- package/deploy/install.sh +54 -18
- package/deploy/online.mjs +492 -0
- package/deploy/relay.sh +70 -82
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,6 +213,16 @@ The host needs `tmux` and `bindfs` (the read-only views).
|
|
|
213
213
|
# to try it on a machine of your own, with no domain: this machine only, no sign-in
|
|
214
214
|
sudo deploy/install.sh --local
|
|
215
215
|
|
|
216
|
+
# the stable version from npm, in local mode, asking nothing (Node.js pinned and
|
|
217
|
+
# checked first); it prints the next command and the public keys (text and QR)
|
|
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
|
|
221
|
+
sudo clavarium online --relay relay.example.net --domain clav.example.com
|
|
222
|
+
sudo clavarium online --port --domain clav.example.com
|
|
223
|
+
sudo clavarium key # the public keys again
|
|
224
|
+
sudo clavarium relay status # status | repair | update-os | update-relay | forget-setup-key
|
|
225
|
+
|
|
216
226
|
# from npm, an exact version, checked (integrity, provenance, signatures);
|
|
217
227
|
# it then updates itself (Settings, Updates; --updates auto|asap|ask|off,
|
|
218
228
|
# --update-window 02:00-05:00, --channel latest|next) and keeps the last
|
package/deploy/clavarium.mjs
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* sudo npx clavarium@X.Y.Z install --local
|
|
6
6
|
* npx clavarium --version
|
|
7
7
|
*
|
|
8
|
+
* Once installed, the same program is the command `clavarium` (install.sh
|
|
9
|
+
* puts it in /usr/local/bin), with what is done from the keyboard:
|
|
10
|
+
*
|
|
11
|
+
* sudo clavarium online --relay HOST --domain NAME [...] see deploy/online.mjs
|
|
12
|
+
* sudo clavarium online --port --domain NAME [...]
|
|
13
|
+
* sudo clavarium key the public keys, text and QR
|
|
14
|
+
* sudo clavarium relay status | repair | update-os | update-relay | forget-setup-key
|
|
15
|
+
*
|
|
8
16
|
* `install` runs deploy/install.sh of this package, which installs THIS
|
|
9
17
|
* version from npm (checked) into /opt/clavarium/releases/X.Y.Z; every other
|
|
10
18
|
* parameter is the installer's (install.sh --help-like usage on a bad one).
|
|
@@ -17,11 +25,31 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
25
|
const version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
18
26
|
const [verb, ...rest] = process.argv.slice(2);
|
|
19
27
|
if (verb === '--version' || verb === '-v') { console.log(version); process.exit(0); }
|
|
28
|
+
if (['online', 'key', 'relay'].includes(verb)) {
|
|
29
|
+
const o = await import('./online.mjs');
|
|
30
|
+
if (process.getuid?.() !== 0) { console.error(`clavarium ${verb}: run it as root: sudo clavarium ${verb} ...`); process.exit(78); }
|
|
31
|
+
const sys = o.realSys();
|
|
32
|
+
try {
|
|
33
|
+
if (verb === 'online') await o.online(o.parseOnline(rest), sys);
|
|
34
|
+
else if (verb === 'key') {
|
|
35
|
+
if (rest.length > 1 || (rest[0] && !['setup', 'tunnel'].includes(rest[0]))) throw new o.Bad('usage: sudo clavarium key [setup|tunnel]');
|
|
36
|
+
await o.showKeys(sys, o.PATHS, { which: rest[0] ?? 'all' });
|
|
37
|
+
} else await o.relayCommand(rest, sys);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof o.Bad) { console.error(`clavarium ${verb}: ${error.message}`); if (verb === 'online') console.error(o.USAGE); process.exit(64); }
|
|
41
|
+
console.error(`clavarium ${verb}: ${error instanceof o.Refused ? 'REFUSED: ' : ''}${error.message}`);
|
|
42
|
+
process.exit(error instanceof o.Refused ? 78 : 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
20
45
|
if (verb !== 'install') {
|
|
21
46
|
console.error(`Clavarium ${version}: each AI agent in its own machine, with only what you attach.\n\n`
|
|
22
|
-
+
|
|
23
|
-
+
|
|
24
|
-
+ '
|
|
47
|
+
+ ' curl -fsSL https://clavarium.org/install.sh | sudo bash install, on Ubuntu 24.04 or 26.04 (local mode)\n'
|
|
48
|
+
+ ' sudo clavarium online --relay HOST --domain NAME then online, through a relay of yours\n'
|
|
49
|
+
+ ' sudo clavarium online --port --domain NAME or with ports 80 and 443 open\n'
|
|
50
|
+
+ ' sudo clavarium key the public keys again (text and QR)\n'
|
|
51
|
+
+ ' sudo clavarium relay status|repair|update-os|update-relay|forget-setup-key\n\n'
|
|
52
|
+
+ 'https://clavarium.org');
|
|
25
53
|
process.exit(verb ? 64 : 0);
|
|
26
54
|
}
|
|
27
55
|
const script = fileURLToPath(new URL('./install.sh', import.meta.url));
|
package/deploy/install.sh
CHANGED
|
@@ -33,8 +33,17 @@
|
|
|
33
33
|
# Settings and waits; off. Changed later in Settings.
|
|
34
34
|
# --update-window HH:MM-HH:MM when "auto" may update (default 02:00-05:00)
|
|
35
35
|
# --channel C latest (default) or next (versions with a "-": tests)
|
|
36
|
+
# --keep-release keep the release that runs (change the settings only:
|
|
37
|
+
# what `clavarium online` does to go from local mode to
|
|
38
|
+
# server mode)
|
|
39
|
+
# --relay-installed the relay was set up already (by `clavarium online`):
|
|
40
|
+
# do not print the command to run on it
|
|
36
41
|
# --dry-run check the parameters, say what would be done, do nothing
|
|
37
42
|
#
|
|
43
|
+
# AFTER --local, going online is one command: `sudo clavarium online --relay
|
|
44
|
+
# HOST --domain NAME` or `sudo clavarium online --port --domain NAME` (see
|
|
45
|
+
# deploy/online.mjs); `sudo clavarium key` shows the public keys again.
|
|
46
|
+
#
|
|
38
47
|
# RUN AGAIN on an installation, it installs the version asked for beside the
|
|
39
48
|
# one that runs and switches to it. An installation made before releases
|
|
40
49
|
# existed (code copied into /opt/clavarium) is moved to them; the old copy is
|
|
@@ -102,7 +111,7 @@ bad() { echo "install: $*" >&2; echo "usage: install.sh --domain NAME --operator
|
|
|
102
111
|
refuse() { echo "install: REFUSED: $*" >&2; exit 78; }
|
|
103
112
|
step() { echo "== $*"; }
|
|
104
113
|
|
|
105
|
-
DOMAIN=""; OPERATORS=""; PORT="7717"; CORE_ONLY="no"; DRY="no"; RELAY=""; RELAY_PORT="22"; LOCAL="no"
|
|
114
|
+
DOMAIN=""; OPERATORS=""; PORT="7717"; CORE_ONLY="no"; DRY="no"; RELAY=""; RELAY_PORT="22"; LOCAL="no"; KEEP_RELEASE="no"; RELAY_INSTALLED="no"
|
|
106
115
|
VERSION=""; FROM_DIR=""; UPDATES=""; WINDOW=""; CHANNEL=""
|
|
107
116
|
while [ $# -gt 0 ]; do
|
|
108
117
|
case "$1" in
|
|
@@ -118,6 +127,8 @@ while [ $# -gt 0 ]; do
|
|
|
118
127
|
--updates) UPDATES="${2:-}"; shift 2 || bad "--updates needs a value" ;;
|
|
119
128
|
--update-window) WINDOW="${2:-}"; shift 2 || bad "--update-window needs a value" ;;
|
|
120
129
|
--channel) CHANNEL="${2:-}"; shift 2 || bad "--channel needs a value" ;;
|
|
130
|
+
--keep-release) KEEP_RELEASE="yes"; shift ;;
|
|
131
|
+
--relay-installed) RELAY_INSTALLED="yes"; shift ;;
|
|
121
132
|
--dry-run) DRY="yes"; shift ;;
|
|
122
133
|
*) bad "unknown parameter: $1" ;;
|
|
123
134
|
esac
|
|
@@ -178,7 +189,7 @@ would install Clavarium:
|
|
|
178
189
|
address $([ "$LOCAL" = "yes" ] && echo "http://127.0.0.1:$PORT, from this machine only (--local)" || echo "https://$DOMAIN")
|
|
179
190
|
operators $([ "$LOCAL" = "yes" ] && echo "whoever can open a page on this machine: no sign-in" || echo "$OPERATORS")
|
|
180
191
|
server 127.0.0.1:$PORT, as the account "$ACCOUNT", Node.js $NODE_VERSION
|
|
181
|
-
release $([ -n "$VERSION" ] && echo "clavarium@$VERSION from npm, checked (integrity, provenance from github.com/clavarium/clavarium, npm audit signatures)" || echo "the working tree $FROM_DIR, as it is (development: not checked)")
|
|
192
|
+
release $([ "$KEEP_RELEASE" = "yes" ] && echo "the one that runs, kept (--keep-release)" || [ -n "$VERSION" ] && echo "clavarium@$VERSION from npm, checked (integrity, provenance from github.com/clavarium/clavarium, npm audit signatures)" || echo "the working tree $FROM_DIR, as it is (development: not checked)")
|
|
182
193
|
updates $(case "$SHOW_MODE" in auto) echo "automatic, between ${SHOW_WINDOW%-*} and ${SHOW_WINDOW#*-}" ;; asap) echo "as soon as a version is out" ;; ask) echo "said in Settings, done when you say" ;; off) echo "off" ;; esac), channel $SHOW_CHANNEL
|
|
183
194
|
code $CODE/releases/<version>, $CODE/current (root) data $DATA folders $MNT (root's chain) vault key $VAULT
|
|
184
195
|
root rule $ACCOUNT may run $HOST_PROGRAM, and nothing else
|
|
@@ -209,7 +220,8 @@ export NEEDRESTART_SUSPEND=1
|
|
|
209
220
|
apt-get update -qq
|
|
210
221
|
# bindfs: the read-only views. nftables: the network rules. build-essential
|
|
211
222
|
# and python3: one module of the server (the terminal) is compiled here.
|
|
212
|
-
|
|
223
|
+
# qrencode: the public keys, shown as QR codes on a screen. openssh-client: the tunnel.
|
|
224
|
+
apt-get install -y -qq bindfs nftables build-essential python3 curl ca-certificates xz-utils sudo snapd openssl git qrencode openssh-client >/dev/null
|
|
213
225
|
snap list multipass >/dev/null 2>&1 || snap install multipass >/dev/null
|
|
214
226
|
|
|
215
227
|
step "account"
|
|
@@ -246,6 +258,11 @@ if [ ! -x "$NODE_DIR/bin/node" ]; then
|
|
|
246
258
|
mv -- "$NODE_DIR.partial" "$NODE_DIR"
|
|
247
259
|
fi
|
|
248
260
|
updater() { PATH="$NODE_DIR/bin:$PATH" "$NODE_DIR/bin/node" "$HERE/deploy/updater.mjs" "$@"; }
|
|
261
|
+
if [ "$KEEP_RELEASE" = "yes" ]; then
|
|
262
|
+
[ -L "$CODE/current" ] || refuse "--keep-release: no release runs here yet"
|
|
263
|
+
release="$(basename "$(readlink -f "$CODE/current")")"
|
|
264
|
+
echo " the release that runs is kept: $release"
|
|
265
|
+
else
|
|
249
266
|
if [ -n "$VERSION" ]; then
|
|
250
267
|
echo " clavarium@$VERSION from npm: fetched, checked, built, tried (a few minutes)"
|
|
251
268
|
release="$(updater prepare --version "$VERSION")" || refuse "clavarium@$VERSION was not installed (above: why)"
|
|
@@ -255,6 +272,7 @@ else
|
|
|
255
272
|
fi
|
|
256
273
|
systemctl stop clavarium.service 2>/dev/null || true
|
|
257
274
|
updater switch "$release"
|
|
275
|
+
fi
|
|
258
276
|
APP="$CODE/current/app"
|
|
259
277
|
NODE="$CODE/current/node/bin/node"
|
|
260
278
|
# The updater of the release now in place, for what follows and from now on.
|
|
@@ -268,6 +286,10 @@ install -o root -g root -m 0755 "$APP/deploy/git-read" "$GIT_READ"
|
|
|
268
286
|
|
|
269
287
|
step "the one privileged program"
|
|
270
288
|
install -o root -g root -m 0755 "$APP/deploy/clavarium-host" "$HOST_PROGRAM"
|
|
289
|
+
# The command for the keyboard (sudo clavarium online, key, relay): the
|
|
290
|
+
# program of the package, from the release that runs.
|
|
291
|
+
printf '#!/bin/sh\n# Clavarium: the command of the release that runs. Written by install.sh.\nexec %s %s "$@"\n' "$CODE/current/node/bin/node" "$CODE/current/app/deploy/clavarium.mjs" > /usr/local/bin/clavarium
|
|
292
|
+
chmod 0755 /usr/local/bin/clavarium
|
|
271
293
|
install -d -o root -g root -m 0755 "$ETC"
|
|
272
294
|
printf 'DATA=%s\nMNT=%s\n' "$DATA" "$MNT" > "$ETC/host.conf"; chmod 0644 "$ETC/host.conf"
|
|
273
295
|
rule="$(mktemp)"
|
|
@@ -368,20 +390,8 @@ updater prune
|
|
|
368
390
|
systemctl start clavarium-update.timer
|
|
369
391
|
echo " running: $(sed -n 's/^ "version": "\([^"]*\)",$/\1/p' "$APP/package.json" | head -1) ($release)"
|
|
370
392
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
echo "From another computer of yours: ssh -L $PORT:127.0.0.1:$PORT <you>@<this machine>, then the same address there."
|
|
374
|
-
echo "There is no sign-in: whoever can open a page on this machine is its operator."
|
|
375
|
-
echo "For real use, run it again with --domain and --operator."
|
|
376
|
-
exit 0
|
|
377
|
-
fi
|
|
378
|
-
if [ "$CORE_ONLY" = "yes" ]; then
|
|
379
|
-
echo "Clavarium is installed: 127.0.0.1:$PORT, for $OPERATORS."
|
|
380
|
-
echo "It answers nobody yet: put an authenticating proxy in front, for https://$DOMAIN."
|
|
381
|
-
exit 0
|
|
382
|
-
fi
|
|
383
|
-
|
|
384
|
-
# ------------------------------- THE EDGE ---------------------------------
|
|
393
|
+
# THE EDGE'S PROGRAMS: also in local mode, switched off, so that going online
|
|
394
|
+
# (sudo clavarium online) has them, and can test from the internet first.
|
|
385
395
|
# Downloads one file and refuses it unless it has the expected checksum.
|
|
386
396
|
fetch() { # <url> <sha256> <destination> [extra curl arguments...]
|
|
387
397
|
local url="$1" sum="$2" to="$3"; shift 3
|
|
@@ -390,6 +400,7 @@ fetch() { # <url> <sha256> <destination> [extra curl arguments...]
|
|
|
390
400
|
}
|
|
391
401
|
account() { id "$1" >/dev/null 2>&1 || useradd --system --home-dir "$2" --no-create-home --shell /usr/sbin/nologin "$1"; }
|
|
392
402
|
|
|
403
|
+
if [ "$LOCAL" = "yes" ] || [ "$CORE_ONLY" = "no" ]; then
|
|
393
404
|
step "edge: programs"
|
|
394
405
|
work="$(mktemp -d)"
|
|
395
406
|
if [ "$(/usr/local/bin/caddy version 2>/dev/null | cut -d' ' -f1)" != "v$CADDY_VERSION" ]; then
|
|
@@ -411,6 +422,29 @@ if [ "$(sha256sum /usr/local/bin/dex 2>/dev/null | cut -d' ' -f1)" != "$DEX_SHA2
|
|
|
411
422
|
install -o root -g root -m 0755 "$work/usr/local/bin/dex" /usr/local/bin/dex
|
|
412
423
|
fi
|
|
413
424
|
rm -rf -- "$work"
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
if [ "$LOCAL" = "yes" ]; then
|
|
428
|
+
echo
|
|
429
|
+
# The keys for going online, as text and as QR codes (a screen and a keyboard may be all there is).
|
|
430
|
+
/usr/local/bin/clavarium key || echo "install: the keys could not be shown: sudo clavarium key" >&2
|
|
431
|
+
echo
|
|
432
|
+
echo "Clavarium is installed, in local mode: http://127.0.0.1:$PORT (this machine only, no sign-in)"
|
|
433
|
+
echo "From another computer of yours: ssh -L $PORT:127.0.0.1:$PORT <you>@<this machine>"
|
|
434
|
+
echo
|
|
435
|
+
echo "NEXT, to go online (HTTPS, an administrator, a password shown once):"
|
|
436
|
+
echo " sudo clavarium online --relay RELAY_HOST --domain clav.example.com through a relay of yours"
|
|
437
|
+
echo " sudo clavarium online --port --domain clav.example.com or with ports 80 and 443 open here"
|
|
438
|
+
echo "The keys again: sudo clavarium key"
|
|
439
|
+
exit 0
|
|
440
|
+
fi
|
|
441
|
+
if [ "$CORE_ONLY" = "yes" ]; then
|
|
442
|
+
echo "Clavarium is installed: 127.0.0.1:$PORT, for $OPERATORS."
|
|
443
|
+
echo "It answers nobody yet: put an authenticating proxy in front, for https://$DOMAIN."
|
|
444
|
+
exit 0
|
|
445
|
+
fi
|
|
446
|
+
|
|
447
|
+
# ------------------------------- THE EDGE ---------------------------------
|
|
414
448
|
|
|
415
449
|
step "edge: accounts and settings"
|
|
416
450
|
account caddy /var/lib/caddy; account dex /var/lib/dex; account oauth2-proxy /nonexistent
|
|
@@ -657,7 +691,9 @@ if [ -n "$PASSWORDS" ]; then
|
|
|
657
691
|
else
|
|
658
692
|
echo "Operators and passwords: unchanged."
|
|
659
693
|
fi
|
|
660
|
-
if [ -n "$RELAY" ]; then
|
|
694
|
+
if [ -n "$RELAY" ] && [ "$RELAY_INSTALLED" = "yes" ]; then
|
|
695
|
+
echo "The relay $RELAY is set up; the tunnel comes up by itself. Its fingerprint: $RELAY_PRINT"
|
|
696
|
+
elif [ -n "$RELAY" ]; then
|
|
661
697
|
echo
|
|
662
698
|
echo "ON THE RELAY ($RELAY), run this one command; the tunnel then comes up by itself:"
|
|
663
699
|
echo " sudo deploy/relay.sh --key \"$TUNNEL_KEY\""
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/* GOING ONLINE FROM THE KEYBOARD: `sudo clavarium online`, `sudo clavarium key`,
|
|
2
|
+
* `sudo clavarium relay ...`. Run as root, on the Clavarium machine.
|
|
3
|
+
*
|
|
4
|
+
* An installation always ends in LOCAL MODE (this computer only, no sign-in).
|
|
5
|
+
* Going online means SERVER MODE: HTTPS, an administrator and a password, a
|
|
6
|
+
* domain, and one of two ways in:
|
|
7
|
+
*
|
|
8
|
+
* --relay HOST this machine is not reachable: it dials out to a small
|
|
9
|
+
* machine of yours that has a public address. This command
|
|
10
|
+
* sets that machine up ITSELF over SSH, with a "setup key" the
|
|
11
|
+
* operator puts once on an account of it that has sudo; the
|
|
12
|
+
* key is taken out again at the end (unless --keep-setup-key).
|
|
13
|
+
* --port this machine is reachable on 80 and 443 (a server at a host,
|
|
14
|
+
* or a router whose ports were opened).
|
|
15
|
+
*
|
|
16
|
+
* NOTHING IS CUT BEFORE A REAL TEST FROM THE INTERNET HAS PASSED: Let's
|
|
17
|
+
* Encrypt's staging service is asked for a certificate for the domain, so it
|
|
18
|
+
* really comes to this machine (through the relay, or through the open
|
|
19
|
+
* ports). On failure, the exact cause is said and the machine stays in local
|
|
20
|
+
* mode. Only then does install.sh switch it to server mode and print the
|
|
21
|
+
* administrator's password, once.
|
|
22
|
+
*
|
|
23
|
+
* Everything outside this file (processes, DNS, the terminal) goes through
|
|
24
|
+
* `sys`, so the tests can stand in for SSH, npm and DNS.
|
|
25
|
+
*/
|
|
26
|
+
import { spawn } from 'node:child_process';
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, mkdtempSync, renameSync, openSync, readSync, writeSync, closeSync } from 'node:fs';
|
|
28
|
+
import { promises as dns } from 'node:dns';
|
|
29
|
+
import { createInterface } from 'node:readline';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import { readConf } from './updater.mjs';
|
|
33
|
+
|
|
34
|
+
export const PATHS = {
|
|
35
|
+
access: '/var/lib/clavarium-relay-access',
|
|
36
|
+
tunnel: '/var/lib/clavarium-tunnel',
|
|
37
|
+
relayConf: '/etc/clavarium/relay.conf',
|
|
38
|
+
updateConf: '/etc/clavarium/update.conf',
|
|
39
|
+
unit: '/etc/systemd/system/clavarium.service',
|
|
40
|
+
code: '/opt/clavarium',
|
|
41
|
+
caddy: '/usr/local/bin/caddy',
|
|
42
|
+
};
|
|
43
|
+
export const STAGING = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
|
44
|
+
const DOMAIN = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}$/;
|
|
45
|
+
const HOST = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}$|^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
|
46
|
+
const EMAIL = /^[a-z0-9._+-]{1,64}@([a-z0-9-]{1,63}\.)+[a-z]{2,24}$/;
|
|
47
|
+
const USER = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
48
|
+
const PRINT = /^SHA256:[A-Za-z0-9+/]{43}$/;
|
|
49
|
+
const KEY = /^ssh-ed25519 [A-Za-z0-9+/]{68}( [A-Za-z0-9@._-]{1,64})?$/;
|
|
50
|
+
const KEEPER = 'clavarium-keeper';
|
|
51
|
+
const WAIT_MS = 30 * 60_000;
|
|
52
|
+
|
|
53
|
+
export class Bad extends Error {}
|
|
54
|
+
export class Refused extends Error {}
|
|
55
|
+
export class Failed extends Error {}
|
|
56
|
+
|
|
57
|
+
/* ------------------------------ parameters ------------------------------ */
|
|
58
|
+
export const USAGE = [
|
|
59
|
+
'usage: sudo clavarium online --relay HOST --domain NAME [--relay-user USER] [--relay-port N] [--admin EMAIL]',
|
|
60
|
+
' [--relay-fingerprint SHA256:...] [--keep-setup-key] [--yes]',
|
|
61
|
+
' or: sudo clavarium online --port --domain NAME [--admin EMAIL]',
|
|
62
|
+
].join('\n');
|
|
63
|
+
|
|
64
|
+
export function parseOnline(args) {
|
|
65
|
+
const o = { relay: '', user: 'root', relayPort: '22', domain: '', admin: '', fingerprint: '', keepSetupKey: false, yes: false, port: false };
|
|
66
|
+
const value = (i, name) => { const v = args[i + 1]; if (v === undefined || v.startsWith('--')) throw new Bad(`${name} needs a value`); return v; };
|
|
67
|
+
for (let i = 0; i < args.length; i++) {
|
|
68
|
+
const a = args[i];
|
|
69
|
+
switch (a) {
|
|
70
|
+
case '--relay': o.relay = value(i, a); i++; break;
|
|
71
|
+
case '--relay-user': o.user = value(i, a); i++; break;
|
|
72
|
+
case '--relay-port': o.relayPort = value(i, a); i++; break;
|
|
73
|
+
case '--domain': o.domain = value(i, a); i++; break;
|
|
74
|
+
case '--admin': o.admin = value(i, a); i++; break;
|
|
75
|
+
case '--relay-fingerprint': o.fingerprint = value(i, a); i++; break;
|
|
76
|
+
case '--keep-setup-key': o.keepSetupKey = true; break;
|
|
77
|
+
case '--yes': o.yes = true; break;
|
|
78
|
+
case '--port': o.port = true; break;
|
|
79
|
+
default: throw new Bad(`unknown parameter: ${a}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!o.relay === !o.port) throw new Bad('say how it will be reached: --relay HOST or --port');
|
|
83
|
+
if (!DOMAIN.test(o.domain)) throw new Bad('--domain is a host name, lower case, like clav.example.com');
|
|
84
|
+
o.admin ||= `admin@${o.domain}`;
|
|
85
|
+
if (!EMAIL.test(o.admin)) throw new Bad('--admin is an e-mail address, lower case');
|
|
86
|
+
if (o.relay) {
|
|
87
|
+
if (!HOST.test(o.relay)) throw new Bad('--relay is the name or the address of the relay machine');
|
|
88
|
+
if (!USER.test(o.user)) throw new Bad('--relay-user is an account name');
|
|
89
|
+
if (!/^\d{1,5}$/.test(o.relayPort) || +o.relayPort < 1 || +o.relayPort > 65535) throw new Bad('--relay-port is a port number');
|
|
90
|
+
if (o.fingerprint && !PRINT.test(o.fingerprint)) throw new Bad('--relay-fingerprint looks like SHA256:... (ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the relay)');
|
|
91
|
+
} else if (o.fingerprint || o.keepSetupKey || o.user !== 'root' || o.relayPort !== '22') throw new Bad('--relay-user, --relay-port, --relay-fingerprint and --keep-setup-key go with --relay');
|
|
92
|
+
return o;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* --------------------------------- keys --------------------------------- */
|
|
96
|
+
const keyFiles = (P) => ({
|
|
97
|
+
setup: join(P.access, 'setup-key'),
|
|
98
|
+
keeper: join(P.access, 'keeper-key'),
|
|
99
|
+
tunnel: join(P.tunnel, 'key'),
|
|
100
|
+
knownHosts: join(P.access, 'known_hosts'),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/* The three keys of this machine, made here if missing; private halves never leave it. */
|
|
104
|
+
export async function ensureKeys(sys, P = PATHS) {
|
|
105
|
+
const f = keyFiles(P);
|
|
106
|
+
mkdirSync(P.access, { recursive: true, mode: 0o700 });
|
|
107
|
+
for (const [file, name] of [[f.setup, 'clavarium-setup'], [f.keeper, KEEPER]]) {
|
|
108
|
+
if (existsSync(`${file}.pub`)) continue;
|
|
109
|
+
const r = await sys.run(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-C', name, '-f', file]);
|
|
110
|
+
if (r.code !== 0) throw new Failed(`the key ${name} could not be made: ${tail(r)}`);
|
|
111
|
+
}
|
|
112
|
+
if (!existsSync(`${f.tunnel}.pub`)) {
|
|
113
|
+
if ((await sys.run(['id', 'clavarium-tunnel'])).code !== 0) await sys.run(['useradd', '--system', '--home-dir', '/nonexistent', '--no-create-home', '--shell', '/usr/sbin/nologin', 'clavarium-tunnel']);
|
|
114
|
+
await sys.run(['install', '-d', '-o', 'clavarium-tunnel', '-g', 'clavarium-tunnel', '-m', '0700', P.tunnel]);
|
|
115
|
+
const r = await sys.run(['sudo', '-u', 'clavarium-tunnel', 'ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-C', 'clavarium-tunnel', '-f', f.tunnel]);
|
|
116
|
+
if (r.code !== 0) throw new Failed(`the tunnel key could not be made: ${tail(r)}`);
|
|
117
|
+
}
|
|
118
|
+
const read = (file) => readFileSync(`${file}.pub`, 'utf8').trim();
|
|
119
|
+
return { setup: read(f.setup), keeper: read(f.keeper), tunnel: read(f.tunnel) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function qr(sys, text) {
|
|
123
|
+
const r = await sys.run(['qrencode', '-t', 'ANSIUTF8', '-m', '2', '--', text]);
|
|
124
|
+
if (r.code === 0 && r.out) sys.print(r.out.replace(/\n$/, ''));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* `sudo clavarium key`: the public keys, as text and as QR codes for a phone. */
|
|
128
|
+
export async function showKeys(sys, P = PATHS, { which = 'all' } = {}) {
|
|
129
|
+
const keys = await ensureKeys(sys, P);
|
|
130
|
+
if (which === 'all' || which === 'setup') {
|
|
131
|
+
sys.print('SETUP KEY: for `clavarium online --relay`. Put it once on the relay, in');
|
|
132
|
+
sys.print('~/.ssh/authorized_keys of an account that has sudo. It is taken out at the end.');
|
|
133
|
+
sys.print(` ${keys.setup}`);
|
|
134
|
+
await qr(sys, keys.setup);
|
|
135
|
+
}
|
|
136
|
+
if (which === 'all' || which === 'tunnel') {
|
|
137
|
+
sys.print('TUNNEL KEY: only if you set up the relay yourself:');
|
|
138
|
+
sys.print(` sudo deploy/relay.sh --key "${keys.tunnel}"`);
|
|
139
|
+
await qr(sys, keys.tunnel);
|
|
140
|
+
}
|
|
141
|
+
sys.print('These are PUBLIC keys: showing them is safe.');
|
|
142
|
+
return keys;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ---------------------------------- SSH ---------------------------------- */
|
|
146
|
+
const sshOptions = (key, knownHosts, port) => [
|
|
147
|
+
'-o', 'BatchMode=yes', '-o', 'IdentitiesOnly=yes', '-o', `IdentityFile=${key}`,
|
|
148
|
+
'-o', `UserKnownHostsFile=${knownHosts}`, '-o', 'StrictHostKeyChecking=yes', '-o', 'ConnectTimeout=10', '-p', String(port),
|
|
149
|
+
];
|
|
150
|
+
const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
151
|
+
const tail = (r) => String(r.err || r.out || '').trim().split('\n').filter(Boolean).pop()?.slice(0, 300) ?? `exit ${r.code}`;
|
|
152
|
+
|
|
153
|
+
export function readRelayConf(P = PATHS) {
|
|
154
|
+
const conf = {};
|
|
155
|
+
try { for (const line of readFileSync(P.relayConf, 'utf8').split('\n')) { const m = /^([A-Z_]+)=(.*)$/.exec(line.trim()); if (m) conf[m[1]] = m[2]; } } catch { return null; }
|
|
156
|
+
return conf.HOST ? conf : null;
|
|
157
|
+
}
|
|
158
|
+
function writeRelayConf(P, conf) {
|
|
159
|
+
mkdirSync(join(P.relayConf, '..'), { recursive: true });
|
|
160
|
+
writeFileSync(`${P.relayConf}.new`, `# The relay of this Clavarium. Written by clavarium online.\n${Object.entries(conf).map(([k, v]) => `${k}=${v}`).join('\n')}\n`, { mode: 0o644 });
|
|
161
|
+
renameSync(`${P.relayConf}.new`, P.relayConf);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* The relay's own maintenance program, through the account that may run it only. */
|
|
165
|
+
export async function keeper(sys, P, conf, words, { stream = false } = {}) {
|
|
166
|
+
const f = keyFiles(P);
|
|
167
|
+
return sys.run(['ssh', ...sshOptions(f.keeper, f.knownHosts, conf.PORT), `${KEEPER}@${conf.HOST}`, words.join(' ')], { stream });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* The version of the relay program this Clavarium works with: the one relay.sh installs. */
|
|
171
|
+
export function relayVersion(P = PATHS) {
|
|
172
|
+
const text = readFileSync(join(P.code, 'current', 'app', 'deploy', 'relay.sh'), 'utf8');
|
|
173
|
+
return /^RELAY_VERSION="([^"]+)"$/m.exec(text)?.[1];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Takes the setup key out of the relay account it was put in, and checks it no longer opens it. */
|
|
177
|
+
export async function forgetSetupKey(sys, P, conf) {
|
|
178
|
+
const f = keyFiles(P);
|
|
179
|
+
const body = readFileSync(`${f.setup}.pub`, 'utf8').trim().split(' ')[1];
|
|
180
|
+
const at = [...sshOptions(f.setup, f.knownHosts, conf.PORT), `${conf.USER}@${conf.HOST}`];
|
|
181
|
+
const script = `f="$HOME/.ssh/authorized_keys"; [ -f "$f" ] || exit 0; t="$(mktemp)"; grep -vF -- ${quote(body)} "$f" > "$t" || true; cat "$t" > "$f"; rm -f -- "$t"`;
|
|
182
|
+
const r = await sys.run(['ssh', ...at, script]);
|
|
183
|
+
if (r.code !== 0 && !/Permission denied/i.test(r.err)) throw new Failed(`the setup key could not be taken out: ${tail(r)}`);
|
|
184
|
+
const again = await sys.run(['ssh', ...at, 'true']);
|
|
185
|
+
if (again.code === 0) throw new Failed(`the setup key still opens ${conf.USER}@${conf.HOST}: take it out of ~/.ssh/authorized_keys there`);
|
|
186
|
+
writeRelayConf(P, { ...conf, SETUP_KEY: 'removed' });
|
|
187
|
+
sys.print(`The setup key no longer opens ${conf.USER}@${conf.HOST}.`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ---------------------------------- DNS ---------------------------------- */
|
|
191
|
+
export function isPrivate(ip) {
|
|
192
|
+
return /^(10\.|127\.|169\.254\.|192\.168\.|0\.)/.test(ip) || /^172\.(1[6-9]|2\d|3[01])\./.test(ip)
|
|
193
|
+
|| /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip) || /^(::1$|f[cd]|fe80:)/i.test(ip);
|
|
194
|
+
}
|
|
195
|
+
export async function addresses(sys, name) {
|
|
196
|
+
if (/^([0-9]{1,3}\.){3}[0-9]{1,3}$/.test(name)) return [name];
|
|
197
|
+
return sys.resolve(name);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* What the DNS must say, checked before anything changes. */
|
|
201
|
+
export async function checkDns(sys, o) {
|
|
202
|
+
const got = await addresses(sys, o.domain);
|
|
203
|
+
if (o.relay) {
|
|
204
|
+
const relay = await addresses(sys, o.relay);
|
|
205
|
+
if (!relay.length) throw new Failed(`${o.relay} has no address in the DNS`);
|
|
206
|
+
if (!got.length) throw new Failed(`${o.domain} has no DNS record yet. Create it at your DNS provider:\n ${o.domain} A ${relay[0]}\n(then run this command again)`);
|
|
207
|
+
if (!got.some((a) => relay.includes(a))) throw new Failed(`${o.domain} points to ${got.join(', ')}, not to the relay (${relay.join(', ')}). Change the record:\n ${o.domain} A ${relay[0]}`);
|
|
208
|
+
sys.print(`DNS: ${o.domain} -> ${got.join(', ')}, the relay. Good.`);
|
|
209
|
+
return got;
|
|
210
|
+
}
|
|
211
|
+
if (!got.length) throw new Failed(`${o.domain} has no DNS record yet. Create one at your DNS provider: ${o.domain} A <the public address of this machine>`);
|
|
212
|
+
const pub = got.filter((a) => !isPrivate(a));
|
|
213
|
+
if (!pub.length) throw new Failed(`${o.domain} points to ${got.join(', ')}: a private address, which the internet cannot reach. Use the public one (your router's, or your server's)`);
|
|
214
|
+
sys.print(`DNS: ${o.domain} -> ${got.join(', ')}.`);
|
|
215
|
+
return got;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ------------------------ the test from the internet ------------------------ */
|
|
219
|
+
/* What Let's Encrypt (or Caddy) said, as the operator needs to read it. */
|
|
220
|
+
export function explain(error, { domain, port, relay }) {
|
|
221
|
+
const e = String(error);
|
|
222
|
+
const where = relay ? `the relay ${relay}` : 'this machine';
|
|
223
|
+
if (/address already in use/i.test(e)) return `port ${port} is already used by another program on this machine`;
|
|
224
|
+
if (/NXDOMAIN|no valid A records|DNS problem/i.test(e)) return `Let's Encrypt cannot find ${domain} in the DNS yet (${e.match(/DNS problem: [^;"]*/)?.[0] ?? 'DNS'}). Wait a few minutes after creating the record, then try again`;
|
|
225
|
+
if (/timeout|timed out|likely firewall/i.test(e)) return relay ? `port ${port} of ${relay} cannot be reached from the internet: open it in the relay's firewall (at its host, too)` : `port ${port} is not open on your router or firewall: Let's Encrypt waited and nothing answered`;
|
|
226
|
+
if (/connection refused/i.test(e)) return relay ? `port ${port} of ${relay} refuses connections: the relay does not pass it to the tunnel (sudo clavarium relay repair)` : `port ${port} reaches a machine that refuses it: forward it to this machine on your router`;
|
|
227
|
+
if (/unauthorized|invalid response|404|403/i.test(e)) return `${domain} leads to another machine than ${where}: check the DNS record`;
|
|
228
|
+
if (/rate ?limit|too many/i.test(e)) return 'Let\'s Encrypt says too many tries: wait an hour and try again';
|
|
229
|
+
return `the test failed: ${e.slice(0, 300)}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* One certificate asked of Let's Encrypt's STAGING service (no real
|
|
233
|
+
certificate, no limit worth worrying about), with one challenge only, so
|
|
234
|
+
that what fails says which port: http-01 comes on 80, tls-alpn-01 on 443. */
|
|
235
|
+
export async function acmeTest(sys, P, { domain, relay, challenge, timeoutMs = 150_000 }) {
|
|
236
|
+
const dir = mkdtempSync(join(sys.tmp ?? tmpdir(), 'clavarium-test-'));
|
|
237
|
+
const port = challenge === 'http' ? 80 : 443;
|
|
238
|
+
try {
|
|
239
|
+
writeFileSync(join(dir, 'Caddyfile'), [
|
|
240
|
+
'{', '\tadmin off', '\tpersist_config off', `\tstorage file_system ${join(dir, 'data')}`, '\tlog {', '\t\toutput stderr', '\t\tformat json', '\t}', '}',
|
|
241
|
+
`${domain} {`, relay ? '\tbind 127.0.0.1' : '',
|
|
242
|
+
'\ttls {', '\t\tissuer acme {', `\t\t\tdir ${STAGING}`, challenge === 'http' ? '\t\t\tdisable_tlsalpn_challenge' : '\t\t\tdisable_http_challenge', '\t\t}', '\t}',
|
|
243
|
+
'\trespond "Clavarium: test" 200', '}', '',
|
|
244
|
+
].join('\n'));
|
|
245
|
+
let finish;
|
|
246
|
+
const result = new Promise((resolve) => { finish = resolve; });
|
|
247
|
+
const timer = setTimeout(() => finish({ ok: false, why: `no answer from Let's Encrypt within ${Math.round(timeoutMs / 1000)} s: port ${port} probably does not reach ${relay ? 'the relay' : 'this machine'}` }), timeoutMs);
|
|
248
|
+
const proc = sys.start([P.caddy, 'run', '--config', join(dir, 'Caddyfile'), '--adapter', 'caddyfile'], {
|
|
249
|
+
env: { HOME: dir, XDG_DATA_HOME: dir, XDG_CONFIG_HOME: dir },
|
|
250
|
+
onLine: (line) => {
|
|
251
|
+
let j = null;
|
|
252
|
+
try { j = JSON.parse(line); } catch { /* not a log line */ }
|
|
253
|
+
if (j?.msg === 'certificate obtained successfully') finish({ ok: true });
|
|
254
|
+
else if (j && j.level === 'error' && /could not get certificate|obtaining certificate|job failed|obtain/i.test(j.msg ?? '')) finish({ ok: false, why: explain(j.error ?? j.msg, { domain, port, relay }) });
|
|
255
|
+
else if (!j && /address already in use|Error:/i.test(line)) finish({ ok: false, why: explain(line, { domain, port, relay }) });
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
proc.exited.then((r) => finish({ ok: false, why: `the test program stopped (exit ${r.code})` }));
|
|
259
|
+
const got = await result;
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
proc.stop();
|
|
262
|
+
await proc.exited;
|
|
263
|
+
return got;
|
|
264
|
+
} finally {
|
|
265
|
+
rmSync(dir, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function internetTest(sys, P, o) {
|
|
270
|
+
for (const challenge of ['http', 'tls']) {
|
|
271
|
+
const port = challenge === 'http' ? 80 : 443;
|
|
272
|
+
sys.print(`Test from the internet, port ${port}: Let's Encrypt (staging) comes to ${o.domain}...`);
|
|
273
|
+
const r = await acmeTest(sys, P, { domain: o.domain, relay: o.relay, challenge });
|
|
274
|
+
if (!r.ok) throw new Failed(`${r.why}.\nNothing was changed: this machine is still in local mode.`);
|
|
275
|
+
sys.print(` port ${port}: reached. Good.`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function portsFree(sys) {
|
|
280
|
+
for (const port of [80, 443]) {
|
|
281
|
+
const r = await sys.run(['ss', '-ltnpH', `sport = :${port}`]);
|
|
282
|
+
if (r.out.trim()) throw new Failed(`port ${port} is already used on this machine: ${r.out.trim().split('\n')[0].replace(/\s+/g, ' ')}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* ------------------------------ the relay path ------------------------------ */
|
|
287
|
+
async function scanRelay(sys, o, deadline) {
|
|
288
|
+
for (;;) {
|
|
289
|
+
const r = await sys.run(['ssh-keyscan', '-T', '10', '-t', 'ed25519', '-p', o.relayPort, o.relay]);
|
|
290
|
+
const line = r.out.split('\n').find((l) => / ssh-ed25519 [A-Za-z0-9+/=]+$/.test(l.trim()));
|
|
291
|
+
if (line) return line.trim();
|
|
292
|
+
if (sys.now() > deadline) throw new Failed(`${o.relay} does not answer on port ${o.relayPort} (SSH)`);
|
|
293
|
+
sys.print(`${o.relay} does not answer on port ${o.relayPort} yet; trying again...`);
|
|
294
|
+
await sys.sleep(5000);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function fingerprintOf(sys, P, line) {
|
|
299
|
+
const file = join(P.access, 'known_hosts.pending');
|
|
300
|
+
writeFileSync(file, `${line}\n`, { mode: 0o600 });
|
|
301
|
+
const r = await sys.run(['ssh-keygen', '-l', '-f', file]);
|
|
302
|
+
const print = /SHA256:[A-Za-z0-9+/]{43}/.exec(r.out)?.[0];
|
|
303
|
+
if (!print) throw new Failed(`the relay's key could not be read: ${tail(r)}`);
|
|
304
|
+
return { file, print };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function waitForSetupKey(sys, o, at, deadline) {
|
|
308
|
+
const check = o.user === 'root' ? 'true' : 'sudo -n true';
|
|
309
|
+
let said = '';
|
|
310
|
+
const say = (text) => { if (text !== said) { said = text; sys.print(text); } };
|
|
311
|
+
for (;;) {
|
|
312
|
+
const r = await sys.run(['ssh', ...at, `${o.user}@${o.relay}`, check]);
|
|
313
|
+
if (r.code === 0) { sys.print(`The setup key opens ${o.user}@${o.relay}${o.user === 'root' ? '' : ', with sudo'}.`); return; }
|
|
314
|
+
const err = r.err || '';
|
|
315
|
+
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...`);
|
|
316
|
+
else if (/Permission denied/i.test(err)) say(`Waiting for the setup key on ${o.user}@${o.relay} (Ctrl-C to stop)...`);
|
|
317
|
+
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
|
+
else say(`${o.relay} does not answer yet (${tail(r)}). Waiting...`);
|
|
319
|
+
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);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function setUpRelay(sys, P, o, keys) {
|
|
325
|
+
const f = keyFiles(P);
|
|
326
|
+
const conf = { HOST: o.relay, PORT: o.relayPort, USER: o.user };
|
|
327
|
+
const prior = readRelayConf(P);
|
|
328
|
+
/* Run again after the relay was set up (a test that failed, a DNS record
|
|
329
|
+
made late): the maintenance account answers, the setup key is not needed. */
|
|
330
|
+
if (prior && prior.HOST === o.relay && existsSync(f.knownHosts)) {
|
|
331
|
+
const r = await keeper(sys, P, { ...prior, PORT: o.relayPort }, ['version']);
|
|
332
|
+
if (r.code === 0 && /^\d/.test(r.out.trim())) { sys.print(`The relay ${o.relay} is set up already (clavarium-relay ${r.out.trim()}).`); return { ...prior, ...conf, SETUP_KEY: prior.SETUP_KEY ?? 'removed' }; }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
sys.print('');
|
|
336
|
+
sys.print(`1. Put this SETUP KEY on the relay, in ~${o.user}/.ssh/authorized_keys of ${o.relay}${o.user === 'root' ? '' : ` (${o.user} needs sudo without a password)`}:`);
|
|
337
|
+
sys.print(` ${keys.setup}`);
|
|
338
|
+
await qr(sys, keys.setup);
|
|
339
|
+
sys.print('(A public key: safe to show. It is taken out again at the end.)');
|
|
340
|
+
sys.print('');
|
|
341
|
+
const deadline = sys.now() + WAIT_MS;
|
|
342
|
+
const scanned = await scanRelay(sys, o, deadline);
|
|
343
|
+
const { file: pending, print } = await fingerprintOf(sys, P, scanned);
|
|
344
|
+
if (o.fingerprint && o.fingerprint !== print) {
|
|
345
|
+
rmSync(pending, { force: true });
|
|
346
|
+
throw new Refused(`${o.relay} answers with the key ${print}, not ${o.fingerprint}: it may not be your relay. Nothing was changed`);
|
|
347
|
+
}
|
|
348
|
+
await waitForSetupKey(sys, o, sshOptions(f.setup, pending, o.relayPort), deadline);
|
|
349
|
+
|
|
350
|
+
sys.print(`2. The relay's fingerprint: ${print}`);
|
|
351
|
+
if (o.fingerprint) sys.print(' It is the one given with --relay-fingerprint. Good.');
|
|
352
|
+
else if (o.yes) sys.print(' Accepted without a check (--yes).');
|
|
353
|
+
else {
|
|
354
|
+
sys.print(' To compare, on the relay: ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub');
|
|
355
|
+
if (!(await sys.ask(' Is it the same?'))) { rmSync(pending, { force: true }); throw new Refused('not confirmed: nothing was changed'); }
|
|
356
|
+
}
|
|
357
|
+
renameSync(pending, f.knownHosts);
|
|
358
|
+
writeFileSync(join(P.tunnel, 'known_hosts'), `${scanned}\n`, { mode: 0o644 });
|
|
359
|
+
await sys.run(['chown', 'clavarium-tunnel:clavarium-tunnel', join(P.tunnel, 'known_hosts')]);
|
|
360
|
+
|
|
361
|
+
sys.print('3. Setting up the relay (tunnel account, maintenance account, port rule, automatic updates)...');
|
|
362
|
+
const script = readFileSync(join(P.code, 'current', 'app', 'deploy', 'relay.sh'), 'utf8');
|
|
363
|
+
const sudo = o.user === 'root' ? '' : 'sudo -n ';
|
|
364
|
+
/* The script is read whole into a file before it runs: run from standard
|
|
365
|
+
input, a program it starts (apt-get...) could eat the rest of it. */
|
|
366
|
+
const runner = 'f="$(mktemp)"; cat > "$f"; bash "$f" "$@" </dev/null; s=$?; rm -f -- "$f"; exit $s';
|
|
367
|
+
const r = await sys.run(['ssh', ...sshOptions(f.setup, f.knownHosts, o.relayPort), `${o.user}@${o.relay}`,
|
|
368
|
+
`${sudo}bash -c ${quote(runner)} relay.sh --key ${quote(keys.tunnel)} --keeper-key ${quote(keys.keeper)}`], { input: script, stream: true });
|
|
369
|
+
if (r.code !== 0) throw new Failed(`the relay could not be set up (exit ${r.code}, above: why). This machine is still in local mode`);
|
|
370
|
+
const next = { ...conf, SETUP_KEY: 'kept' };
|
|
371
|
+
writeRelayConf(P, next);
|
|
372
|
+
const v = await keeper(sys, P, next, ['version']);
|
|
373
|
+
if (v.code !== 0) throw new Failed(`the relay's maintenance account does not answer: ${tail(v)}`);
|
|
374
|
+
sys.print(` The relay is set up (clavarium-relay ${v.out.trim()}).`);
|
|
375
|
+
if (o.keepSetupKey) sys.print(' The setup key stays on the relay (--keep-setup-key). Later: sudo clavarium relay forget-setup-key');
|
|
376
|
+
else await forgetSetupKey(sys, P, next);
|
|
377
|
+
return readRelayConf(P);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* The tunnel, for the test only: the service comes with server mode. */
|
|
381
|
+
function startTunnel(sys, P, o) {
|
|
382
|
+
const lines = [];
|
|
383
|
+
const proc = sys.start(['sudo', '-u', 'clavarium-tunnel', 'ssh', '-N', '-T', ...sshOptions(join(P.tunnel, 'key'), join(P.tunnel, 'known_hosts'), o.relayPort),
|
|
384
|
+
'-o', 'ExitOnForwardFailure=yes', '-R', '*:8443:127.0.0.1:443', '-R', '*:8080:127.0.0.1:80', `clavarium-tunnel@${o.relay}`], { onLine: (l) => lines.push(l) });
|
|
385
|
+
return { proc, lines };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* ------------------------------- the switch ------------------------------- */
|
|
389
|
+
function serverModeOf(P) {
|
|
390
|
+
try { return /--public-origin https:\/\/(\S+)/.exec(readFileSync(P.unit, 'utf8'))?.[1] ?? null; } catch { return null; }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function switchToServer(sys, P, o) {
|
|
394
|
+
const app = join(P.code, 'current', 'app');
|
|
395
|
+
const port = readConf(P.updateConf).PORT;
|
|
396
|
+
const argv = ['/bin/bash', join(app, 'deploy', 'install.sh'), '--keep-release', '--domain', o.domain, '--operator', o.admin, '--port', port];
|
|
397
|
+
if (o.relay) argv.push('--relay', o.relay, '--relay-port', o.relayPort, '--relay-installed');
|
|
398
|
+
sys.print('');
|
|
399
|
+
sys.print('Switching to server mode: HTTPS, sign-in, the administrator...');
|
|
400
|
+
const r = await sys.run(argv, { stream: true, inherit: true });
|
|
401
|
+
if (r.code !== 0) throw new Failed(`the switch to server mode failed (exit ${r.code}, above: why)`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* `sudo clavarium online ...` */
|
|
405
|
+
export async function online(o, sys, P = PATHS) {
|
|
406
|
+
const was = serverModeOf(P);
|
|
407
|
+
if (was) throw new Refused(`this machine is in server mode already: https://${was}`);
|
|
408
|
+
if (!existsSync(P.caddy)) throw new Failed('the HTTPS programs are not on this machine: run the installer again (curl -fsSL https://clavarium.org/install.sh | sudo bash)');
|
|
409
|
+
const keys = await ensureKeys(sys, P);
|
|
410
|
+
await checkDns(sys, o);
|
|
411
|
+
await portsFree(sys);
|
|
412
|
+
if (o.relay) await setUpRelay(sys, P, o, keys);
|
|
413
|
+
let tunnel = null;
|
|
414
|
+
try {
|
|
415
|
+
if (o.relay) {
|
|
416
|
+
tunnel = startTunnel(sys, P, o);
|
|
417
|
+
await sys.sleep(4000);
|
|
418
|
+
if (tunnel.proc.ended?.()) throw new Failed(`the tunnel to ${o.relay} did not come up: ${tunnel.lines.filter(Boolean).pop() ?? 'no reason given'}`);
|
|
419
|
+
}
|
|
420
|
+
await internetTest(sys, P, o);
|
|
421
|
+
} finally {
|
|
422
|
+
tunnel?.proc.stop();
|
|
423
|
+
}
|
|
424
|
+
await switchToServer(sys, P, o);
|
|
425
|
+
sys.print('');
|
|
426
|
+
sys.print(`Online: https://${o.domain} (sign in as ${o.admin}, with the password above)`);
|
|
427
|
+
if (o.relay) sys.print('The relay is watched from here: sudo clavarium relay status');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* `sudo clavarium relay VERB` */
|
|
431
|
+
export async function relayCommand(args, sys, P = PATHS) {
|
|
432
|
+
const conf = readRelayConf(P);
|
|
433
|
+
if (!conf) throw new Refused('this machine has no relay (sudo clavarium online --relay HOST --domain NAME)');
|
|
434
|
+
const [verb, ...rest] = args;
|
|
435
|
+
switch (verb) {
|
|
436
|
+
case 'forget-setup-key': return forgetSetupKey(sys, P, conf);
|
|
437
|
+
case 'status': case 'repair': case 'update-os': {
|
|
438
|
+
if (rest.length && !(verb === 'update-os' && rest.length === 1 && rest[0] === '--reboot')) throw new Bad(`relay ${verb} takes nothing`);
|
|
439
|
+
const r = await keeper(sys, P, conf, [verb, ...rest], { stream: true });
|
|
440
|
+
if (r.code !== 0) throw new Failed(`the relay answered with exit ${r.code}`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
case 'update-relay': {
|
|
444
|
+
const version = rest[0] ?? relayVersion(P);
|
|
445
|
+
if (!/^\d{1,4}\.\d{1,4}\.\d{1,6}(-[0-9A-Za-z.-]{1,40})?$/.test(version ?? '')) throw new Bad('relay update-relay [X.Y.Z]');
|
|
446
|
+
const r = await keeper(sys, P, conf, ['update-relay', version], { stream: true });
|
|
447
|
+
if (r.code !== 0) throw new Failed(`the relay did not update (exit ${r.code})`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
default: throw new Bad('usage: sudo clavarium relay status | repair | update-os [--reboot] | update-relay [X.Y.Z] | forget-setup-key');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* ---------------------------- the real world ---------------------------- */
|
|
455
|
+
export function realSys() {
|
|
456
|
+
return {
|
|
457
|
+
print: (text) => process.stdout.write(`${text}\n`),
|
|
458
|
+
now: () => Date.now(),
|
|
459
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
460
|
+
resolve: async (name) => {
|
|
461
|
+
const all = [];
|
|
462
|
+
for (const f of [dns.resolve4, dns.resolve6]) { try { all.push(...await f.call(dns, name)); } catch { /* none of that kind */ } }
|
|
463
|
+
return all;
|
|
464
|
+
},
|
|
465
|
+
run: (argv, { input, stream = false, inherit = false, env } = {}) => new Promise((resolve) => {
|
|
466
|
+
const child = spawn(argv[0], argv.slice(1), { env: env ? { ...process.env, ...env } : process.env, stdio: inherit ? 'inherit' : [input === undefined ? 'ignore' : 'pipe', stream ? 'inherit' : 'pipe', 'pipe'] });
|
|
467
|
+
let out = ''; let err = '';
|
|
468
|
+
child.stdout?.on('data', (d) => { out = (out + d).slice(-50_000); });
|
|
469
|
+
child.stderr?.on('data', (d) => { err = (err + d).slice(-50_000); if (stream) process.stderr.write(d); });
|
|
470
|
+
if (input !== undefined) child.stdin.end(input);
|
|
471
|
+
child.on('error', (e) => resolve({ code: 127, out, err: e.message }));
|
|
472
|
+
child.on('close', (code) => resolve({ code: code ?? 1, out, err }));
|
|
473
|
+
}),
|
|
474
|
+
start: (argv, { env, onLine }) => {
|
|
475
|
+
const child = spawn(argv[0], argv.slice(1), { env: env ? { ...process.env, ...env } : process.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
476
|
+
let ended = false;
|
|
477
|
+
for (const s of [child.stdout, child.stderr]) createInterface({ input: s }).on('line', (l) => onLine?.(l));
|
|
478
|
+
const exited = new Promise((resolve) => { child.on('error', () => { ended = true; resolve({ code: 127 }); }); child.on('close', (code) => { ended = true; resolve({ code: code ?? 1 }); }); });
|
|
479
|
+
return { exited, ended: () => ended, stop: () => { if (!ended) child.kill('SIGTERM'); } };
|
|
480
|
+
},
|
|
481
|
+
ask: async (question) => {
|
|
482
|
+
let fd;
|
|
483
|
+
try { fd = openSync('/dev/tty', 'r+'); } catch { return false; }
|
|
484
|
+
try {
|
|
485
|
+
writeSync(fd, `${question} [y/N] `);
|
|
486
|
+
const buf = Buffer.alloc(256); let s = '';
|
|
487
|
+
while (!s.includes('\n')) { const n = readSync(fd, buf, 0, buf.length, null); if (!n) break; s += buf.toString('utf8', 0, n); }
|
|
488
|
+
return /^y(es)?$/i.test(s.trim());
|
|
489
|
+
} finally { closeSync(fd); }
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
package/deploy/relay.sh
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
#
|
|
7
7
|
# sudo deploy/relay.sh --key "ssh-ed25519 AAAA... clavarium-tunnel"
|
|
8
8
|
#
|
|
9
|
-
# --key LINE
|
|
10
|
-
# --
|
|
9
|
+
# --key LINE the PUBLIC tunnel key of the home machine (`sudo clavarium key`)
|
|
10
|
+
# --keeper-key LINE the PUBLIC maintenance key of the home machine (optional)
|
|
11
|
+
# --dry-run check the parameters, say what would be done, do nothing
|
|
12
|
+
#
|
|
13
|
+
# `sudo clavarium online --relay HOST` at home runs this script here by itself,
|
|
14
|
+
# over SSH, with both keys.
|
|
11
15
|
#
|
|
12
16
|
# THE RELAY IS BLIND. It carries bytes: TLS starts in the browser and ends at
|
|
13
17
|
# home, so this machine never sees a page, a keystroke or a password. It holds
|
|
@@ -17,7 +21,12 @@
|
|
|
17
21
|
# - an account, "clavarium-tunnel", that can do ONE thing: hold two
|
|
18
22
|
# listening ports (8443, 8080) forwarded to home. No shell, no terminal,
|
|
19
23
|
# no command, no other forwarding;
|
|
20
|
-
# - a rule that sends what arrives on 443 and 80 to those two ports
|
|
24
|
+
# - a rule that sends what arrives on 443 and 80 to those two ports;
|
|
25
|
+
# - the maintenance program, clavarium-relay, fetched by THIS machine from
|
|
26
|
+
# npm at the version written below and checked against the registry's
|
|
27
|
+
# sha512; with --keeper-key, an account "clavarium-keeper" that may run
|
|
28
|
+
# that program, and nothing else (status, repair, updates);
|
|
29
|
+
# - Ubuntu's security updates, by themselves (unattended-upgrades).
|
|
21
30
|
#
|
|
22
31
|
# WHAT A BLIND RELAY CAN STILL DO, and how to close it: whoever controls this
|
|
23
32
|
# machine receives port 80 for your name, so it could answer a certificate
|
|
@@ -29,30 +38,38 @@
|
|
|
29
38
|
# ---------------------------------------------------------------------------
|
|
30
39
|
set -euo pipefail
|
|
31
40
|
|
|
41
|
+
# The version of the maintenance program this Clavarium works with.
|
|
42
|
+
RELAY_VERSION="0.1.0"
|
|
32
43
|
ACCOUNT="clavarium-tunnel"
|
|
33
44
|
TLS_PORT="8443"
|
|
34
45
|
HTTP_PORT="8080"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
PROGRAM="/usr/local/sbin/clavarium-relay"
|
|
47
|
+
# Tests run it on a folder that stands for "/".
|
|
48
|
+
ROOT="${CLAVARIUM_RELAY_ROOT:-}"
|
|
49
|
+
REGISTRY="https://registry.npmjs.org"
|
|
50
|
+
[ -z "$ROOT" ] || REGISTRY="${CLAVARIUM_RELAY_REGISTRY:-$REGISTRY}"
|
|
39
51
|
|
|
40
|
-
bad() { echo "relay: $*" >&2; echo "usage: relay.sh --key \"ssh-ed25519 AAAA... name\" [--dry-run]" >&2; exit 64; }
|
|
52
|
+
bad() { echo "relay: $*" >&2; echo "usage: relay.sh --key \"ssh-ed25519 AAAA... name\" [--keeper-key \"ssh-ed25519 AAAA... name\"] [--dry-run]" >&2; exit 64; }
|
|
41
53
|
refuse() { echo "relay: REFUSED: $*" >&2; exit 78; }
|
|
42
54
|
step() { echo "== $*"; }
|
|
43
55
|
|
|
44
|
-
KEY=""; DRY="no"
|
|
56
|
+
KEY=""; KEEPER_KEY=""; DRY="no"
|
|
45
57
|
while [ $# -gt 0 ]; do
|
|
46
58
|
case "$1" in
|
|
47
59
|
--key) KEY="${2:-}"; shift 2 || bad "--key needs a value" ;;
|
|
60
|
+
--keeper-key) KEEPER_KEY="${2:-}"; [ -n "$KEEPER_KEY" ] || bad "--keeper-key needs a value"; shift 2 ;;
|
|
48
61
|
--dry-run) DRY="yes"; shift ;;
|
|
49
62
|
*) bad "unknown parameter: $1" ;;
|
|
50
63
|
esac
|
|
51
64
|
done
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
65
|
+
# A key goes into a file that gives rights: it must be exactly a public key,
|
|
66
|
+
# on one line, with nothing that could add an option next to it.
|
|
67
|
+
for k in "$KEY" "$KEEPER_KEY"; do
|
|
68
|
+
case "$k" in *"PRIVATE KEY"*) bad "this is a PRIVATE key: it must never leave home. Give the public one (one line, starts with ssh-ed25519)" ;; esac
|
|
69
|
+
done
|
|
55
70
|
[[ "$KEY" =~ ^ssh-ed25519\ [A-Za-z0-9+/]{68}(\ [A-Za-z0-9@._-]{1,64})?$ ]] || bad "--key is one line: ssh-ed25519, the key, and optionally a name"
|
|
71
|
+
[ -z "$KEEPER_KEY" ] || [[ "$KEEPER_KEY" =~ ^ssh-ed25519\ [A-Za-z0-9+/]{68}(\ [A-Za-z0-9@._-]{1,64})?$ ]] || bad "--keeper-key is one line: ssh-ed25519, the key, and optionally a name"
|
|
72
|
+
[ "$KEY" != "$KEEPER_KEY" ] || bad "the tunnel and the maintenance keys are two different keys"
|
|
56
73
|
|
|
57
74
|
if [ "$DRY" = "yes" ]; then
|
|
58
75
|
cat <<PLAN
|
|
@@ -60,94 +77,65 @@ would make this machine a relay:
|
|
|
60
77
|
account $ACCOUNT: may hold ports $TLS_PORT and $HTTP_PORT forwarded to home, and nothing else
|
|
61
78
|
ports 443 -> $TLS_PORT, 80 -> $HTTP_PORT
|
|
62
79
|
sees nothing: TLS ends at home
|
|
80
|
+
program clavarium-relay@$RELAY_VERSION from npm, checked (sha512 of the registry)
|
|
81
|
+
maintenance $([ -n "$KEEPER_KEY" ] && echo "clavarium-keeper: may run clavarium-relay, and nothing else" || echo "none (no --keeper-key)")
|
|
82
|
+
updates Ubuntu's security updates, automatic
|
|
63
83
|
PLAN
|
|
64
84
|
exit 0
|
|
65
85
|
fi
|
|
66
86
|
|
|
67
|
-
[ "$(id -u)" -eq 0 ] || refuse "run it as root (sudo)"
|
|
87
|
+
[ -n "$ROOT" ] || [ "$(id -u)" -eq 0 ] || refuse "run it as root (sudo)"
|
|
68
88
|
# shellcheck disable=SC1091
|
|
69
|
-
. /etc/os-release
|
|
70
|
-
case "${
|
|
89
|
+
OS="$(. "$ROOT/etc/os-release" 2>/dev/null; printf '%s %s|%s' "${ID:-}" "${ID_LIKE:-}" "${PRETTY_NAME:-}")"
|
|
90
|
+
case "${OS%%|*}" in *ubuntu*|*debian*) : ;; *) refuse "this is ${OS#*|}; the relay installs on Ubuntu or Debian" ;; esac
|
|
71
91
|
command -v sshd >/dev/null 2>&1 || refuse "there is no SSH server on this machine (package openssh-server)"
|
|
72
|
-
grep -Eqs '^\s*Include\s+/etc/ssh/sshd_config\.d/' /etc/ssh/sshd_config || refuse "/etc/ssh/sshd_config does not include /etc/ssh/sshd_config.d: the account could not be bounded"
|
|
73
|
-
check="$(mktemp)"
|
|
74
|
-
|
|
92
|
+
grep -Eqs '^\s*Include\s+/etc/ssh/sshd_config\.d/' "$ROOT/etc/ssh/sshd_config" || refuse "/etc/ssh/sshd_config does not include /etc/ssh/sshd_config.d: the account could not be bounded"
|
|
93
|
+
check="$(mktemp)"
|
|
94
|
+
for k in "$KEY" "$KEEPER_KEY"; do
|
|
95
|
+
[ -n "$k" ] || continue
|
|
96
|
+
printf '%s\n' "$k" > "$check"
|
|
97
|
+
ssh-keygen -l -f "$check" >/dev/null 2>&1 || { rm -f -- "$check"; refuse "this is not a key the SSH tools recognise"; }
|
|
98
|
+
done
|
|
75
99
|
rm -f -- "$check"
|
|
76
100
|
for port in 443 80 "$TLS_PORT" "$HTTP_PORT"; do
|
|
77
101
|
# (On a second run, the tunnel itself holds two of them: that is the SSH server.)
|
|
78
102
|
if ss -ltnpH "sport = :$port" 2>/dev/null | grep -v '"sshd' | grep -q .; then refuse "something already listens on port $port here: a relay needs 443 and 80 for itself"; fi
|
|
79
103
|
done
|
|
80
104
|
|
|
81
|
-
step "
|
|
82
|
-
command -v
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
PermitOpen none
|
|
100
|
-
PermitTTY no
|
|
101
|
-
X11Forwarding no
|
|
102
|
-
AllowAgentForwarding no
|
|
103
|
-
PermitTunnel no
|
|
104
|
-
ForceCommand /bin/false
|
|
105
|
-
# A tunnel that died without saying so gives its ports back within minutes.
|
|
106
|
-
ClientAliveInterval 30
|
|
107
|
-
ClientAliveCountMax 3
|
|
108
|
-
# What follows this file in the server's settings is for everybody again.
|
|
109
|
-
Match all
|
|
110
|
-
SSHD
|
|
111
|
-
if ! sshd -t; then rm -f -- "$SSHD_FILE"; refuse "the SSH server refused these settings; they were taken out again"; fi
|
|
112
|
-
systemctl reload ssh.service 2>/dev/null || systemctl reload sshd.service
|
|
113
|
-
|
|
114
|
-
step "ports"
|
|
115
|
-
cat > "$RULES" <<NFT
|
|
116
|
-
# Clavarium relay: what arrives on 443 and 80 goes to the tunnel. Written by relay.sh.
|
|
117
|
-
table inet clavarium_relay
|
|
118
|
-
delete table inet clavarium_relay
|
|
119
|
-
table inet clavarium_relay {
|
|
120
|
-
chain arriving {
|
|
121
|
-
type nat hook prerouting priority dstnat;
|
|
122
|
-
tcp dport 443 redirect to :$TLS_PORT
|
|
123
|
-
tcp dport 80 redirect to :$HTTP_PORT
|
|
124
|
-
}
|
|
105
|
+
step "the maintenance program, from npm"
|
|
106
|
+
for tool in curl openssl tar; do command -v "$tool" >/dev/null 2>&1 || { export DEBIAN_FRONTEND=noninteractive; apt-get update -qq; apt-get install -y -qq curl openssl tar ca-certificates >/dev/null; break; }; done
|
|
107
|
+
# --- fetch_relay: shared, word for word, with deploy/relay.sh (a test says so) ---
|
|
108
|
+
# fetch_relay VERSION DIR: the package clavarium-relay@VERSION from the registry,
|
|
109
|
+
# checked against the registry's sha512, unpacked in DIR (DIR/package/...).
|
|
110
|
+
fetch_relay() {
|
|
111
|
+
local version="$1" dir="$2" meta integrity got proto="=https"
|
|
112
|
+
case "$REGISTRY" in file://*) proto="=file" ;; esac
|
|
113
|
+
meta="$(curl --proto "$proto" -fsSL --max-time 60 "$REGISTRY/clavarium-relay/$version")" || { echo "the registry did not answer for clavarium-relay@$version" >&2; return 1; }
|
|
114
|
+
printf '%s' "$meta" | grep -Eq "\"version\" *: *\"${version//./\\.}\"" || { echo "the registry has no clavarium-relay@$version" >&2; return 1; }
|
|
115
|
+
integrity="$(printf '%s' "$meta" | grep -Eo '"integrity" *: *"sha512-[A-Za-z0-9+/=]+"' | head -1 | sed 's/.*"\(sha512-[^"]*\)"$/\1/')"
|
|
116
|
+
[ -n "$integrity" ] || { echo "the registry gave no sha512 for clavarium-relay@$version" >&2; return 1; }
|
|
117
|
+
curl --proto "$proto" -fsSL --max-time 120 -o "$dir/relay.tgz" "$REGISTRY/clavarium-relay/-/clavarium-relay-$version.tgz" || { echo "the package could not be downloaded" >&2; return 1; }
|
|
118
|
+
got="sha512-$(openssl dgst -sha512 -binary "$dir/relay.tgz" | base64 -w0)"
|
|
119
|
+
[ "$got" = "$integrity" ] || { echo "the package is not the one the registry describes (sha512)" >&2; return 1; }
|
|
120
|
+
tar -xzf "$dir/relay.tgz" -C "$dir" --no-same-owner package/clavarium-relay package/package.json || { echo "the package does not hold the program" >&2; return 1; }
|
|
121
|
+
grep -Eq "\"name\" *: *\"clavarium-relay\"" "$dir/package/package.json" && grep -Eq "\"version\" *: *\"${version//./\\.}\"" "$dir/package/package.json" || { echo "the package is not clavarium-relay@$version" >&2; return 1; }
|
|
122
|
+
bash -n "$dir/package/clavarium-relay" || { echo "the program in the package does not parse" >&2; return 1; }
|
|
125
123
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
[Service]
|
|
134
|
-
Type=oneshot
|
|
135
|
-
RemainAfterExit=yes
|
|
136
|
-
ExecStart=/usr/sbin/nft -f $RULES
|
|
137
|
-
ExecStop=/usr/sbin/nft delete table inet clavarium_relay
|
|
124
|
+
# --- end of fetch_relay ---
|
|
125
|
+
work="$(mktemp -d)"
|
|
126
|
+
if ! fetch_relay "$RELAY_VERSION" "$work"; then rm -rf -- "$work"; refuse "clavarium-relay@$RELAY_VERSION could not be installed (above: why)"; fi
|
|
127
|
+
install -d "$ROOT$(dirname "$PROGRAM")"
|
|
128
|
+
install -m 0755 "$work/package/clavarium-relay" "$ROOT$PROGRAM"
|
|
129
|
+
rm -rf -- "$work"
|
|
138
130
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
systemctl daemon-reload
|
|
143
|
-
systemctl enable --quiet clavarium-relay-ports.service
|
|
144
|
-
systemctl restart clavarium-relay-ports.service
|
|
145
|
-
systemctl is-active --quiet clavarium-relay-ports.service || refuse "the port rule could not be loaded: journalctl -u clavarium-relay-ports"
|
|
131
|
+
step "accounts, rules, ports, updates"
|
|
132
|
+
if [ -n "$KEEPER_KEY" ]; then "$ROOT$PROGRAM" setup --key "$KEY" --keeper-key "$KEEPER_KEY"
|
|
133
|
+
else "$ROOT$PROGRAM" setup --key "$KEY"; fi
|
|
146
134
|
|
|
147
135
|
echo
|
|
148
136
|
echo "This machine is a relay. It waits for the home machine to dial in."
|
|
149
137
|
echo "Compare with what the home machine printed (it must be the same):"
|
|
150
|
-
echo " $(ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub | cut -d' ' -f2)"
|
|
138
|
+
echo " $(ssh-keygen -l -f "$ROOT/etc/ssh/ssh_host_ed25519_key.pub" | cut -d' ' -f2)"
|
|
151
139
|
echo "Point your name at this machine's public address (ports 443 and 80 must reach it)."
|
|
152
140
|
echo
|
|
153
141
|
echo "TO KEEP THIS RELAY BLIND, add a CAA record to your DNS that names your certificate"
|
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.7",
|
|
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.7",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|