clavarium 0.0.2 → 0.1.0-next.2

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +265 -6
  3. package/config/tools.json +172 -0
  4. package/deploy/appliance.sh +83 -0
  5. package/deploy/backup.sh +97 -0
  6. package/deploy/clavarium-host +720 -0
  7. package/deploy/clavarium.mjs +30 -0
  8. package/deploy/git-read +85 -0
  9. package/deploy/install.sh +659 -0
  10. package/deploy/move-to-mnt.sh +142 -0
  11. package/deploy/parley-archive.mjs +47 -0
  12. package/deploy/relay.sh +156 -0
  13. package/deploy/restore.sh +147 -0
  14. package/deploy/updater.mjs +600 -0
  15. package/npm-shrinkwrap.json +1951 -0
  16. package/package.json +53 -8
  17. package/server/alerts.mjs +83 -0
  18. package/server/answerer.mjs +326 -0
  19. package/server/api.mjs +458 -0
  20. package/server/bridge.mjs +271 -0
  21. package/server/capacity.mjs +220 -0
  22. package/server/conversations.mjs +895 -0
  23. package/server/dataformat.mjs +59 -0
  24. package/server/devices.mjs +257 -0
  25. package/server/door.mjs +235 -0
  26. package/server/files.mjs +455 -0
  27. package/server/gate.mjs +54 -0
  28. package/server/gitaccess.mjs +295 -0
  29. package/server/gitchecks.mjs +126 -0
  30. package/server/gitkeeper.mjs +719 -0
  31. package/server/gitpush.mjs +585 -0
  32. package/server/gitrunner.mjs +540 -0
  33. package/server/grants.mjs +110 -0
  34. package/server/guide.mjs +278 -0
  35. package/server/health.mjs +207 -0
  36. package/server/imap.mjs +75 -0
  37. package/server/mailbox.mjs +165 -0
  38. package/server/main.mjs +420 -0
  39. package/server/names.mjs +10 -0
  40. package/server/network.mjs +85 -0
  41. package/server/parley.mjs +62 -0
  42. package/server/resources.mjs +396 -0
  43. package/server/seats.mjs +24 -0
  44. package/server/secrets.mjs +118 -0
  45. package/server/setup.mjs +768 -0
  46. package/server/shelf.mjs +260 -0
  47. package/server/shortcuts.mjs +62 -0
  48. package/server/signin.mjs +185 -0
  49. package/server/smoke.mjs +26 -0
  50. package/server/store.mjs +48 -0
  51. package/server/talk.mjs +514 -0
  52. package/server/tmux.mjs +54 -0
  53. package/server/toolbox.mjs +168 -0
  54. package/server/tools.mjs +109 -0
  55. package/server/updates.mjs +116 -0
  56. package/server/vault.mjs +37 -0
  57. package/server/vm.mjs +139 -0
  58. package/server/voice.mjs +90 -0
  59. package/server/wakeups.mjs +320 -0
  60. package/web/app.js +4779 -0
  61. package/web/device.html +149 -0
  62. package/web/device.js +152 -0
  63. package/web/icon-192.png +0 -0
  64. package/web/icon-512.png +0 -0
  65. package/web/icon.svg +1 -0
  66. package/web/icons.js +9 -0
  67. package/web/index.html +784 -0
  68. package/web/links.js +995 -0
  69. package/web/manifest.webmanifest +30 -0
  70. package/web/prism-manual.js +3 -0
  71. package/web/style.css +1006 -0
  72. package/web/sw.js +26 -0
  73. package/web/voice-core.js +107 -0
  74. package/web/voice.js +56 -0
  75. package/keep.mjs +0 -8
@@ -0,0 +1,659 @@
1
+ #!/bin/bash
2
+ # ---------------------------------------------------------------------------
3
+ # INSTALLS CLAVARIUM ON THIS MACHINE. One command, parameters only: it asks
4
+ # nothing, and can be run again to upgrade.
5
+ #
6
+ # sudo deploy/install.sh --domain clav.example.com --operator you@example.com
7
+ #
8
+ # --domain NAME the address people will use (https://NAME)
9
+ # --operator EMAIL who may use it; repeat for several people
10
+ # --port N the loopback port of the server (default 7717)
11
+ # --relay HOST this machine is at home, with no public address: it
12
+ # dials out to a relay (see relay.sh) and is reached
13
+ # through it. Nothing here then listens to the outside.
14
+ # --relay-port N the SSH port of the relay (default 22)
15
+ # --core-only the server only: no edge in front. For a machine that
16
+ # has its own authenticating proxy.
17
+ # --local TO TRY IT, on a machine of your own, with no domain: no
18
+ # edge, no sign-in, no name. The page answers on this
19
+ # machine only (http://127.0.0.1:PORT), and whoever can
20
+ # open a page on this machine is its operator. Not for a
21
+ # machine other people use. Takes no --domain, --operator
22
+ # or --relay.
23
+ # --version X.Y.Z install exactly this version of the package "clavarium"
24
+ # from npm, checked (integrity, provenance from
25
+ # github.com/clavarium/clavarium, npm audit signatures).
26
+ # The default when this script comes from the package
27
+ # (npx clavarium install): its own version.
28
+ # --from-dir DIR FOR DEVELOPMENT: install the code of a working tree,
29
+ # as it is (not checked). The default when this script
30
+ # runs from a git working tree.
31
+ # --updates MODE auto (default): updates itself in the window below;
32
+ # asap: as soon as a version is out; ask: says so in
33
+ # Settings and waits; off. Changed later in Settings.
34
+ # --update-window HH:MM-HH:MM when "auto" may update (default 02:00-05:00)
35
+ # --channel C latest (default) or next (versions with a "-": tests)
36
+ # --dry-run check the parameters, say what would be done, do nothing
37
+ #
38
+ # RUN AGAIN on an installation, it installs the version asked for beside the
39
+ # one that runs and switches to it. An installation made before releases
40
+ # existed (code copied into /opt/clavarium) is moved to them; the old copy is
41
+ # removed only once the new one answers.
42
+ #
43
+ # WHAT IT SETS UP, and why it is safe to leave running:
44
+ # - an account, "clavarium", that owns its data and NOTHING ELSE: no shell,
45
+ # no password, not in any privileged group;
46
+ # - the ONE program it may run as root (clavarium-host), named alone in the
47
+ # rule that allows it. Everything that needs root goes through it;
48
+ # - the code in /opt/clavarium, owned by root: the server cannot rewrite
49
+ # itself. One folder per version (releases/<version>), the one that runs
50
+ # named by a link (current); the last two are kept to go back to. A timer
51
+ # looks for new versions and updates (deploy/updater.mjs), as --updates
52
+ # and Settings say;
53
+ # - the data in /home/clavarium, the account's home; the directories whose
54
+ # content gets mounted into VMs belong to root (see clavarium-host).
55
+ # UNDER /home ON PURPOSE: the VM manager is confined and can only share
56
+ # folders from there. Anywhere else, a folder "does not exist" for it
57
+ # (found on a real machine: nothing could be attached from /var/lib);
58
+ # - the key of the vault APART from the data (/var/lib/clavarium-vault): a
59
+ # copy of the data alone does not open the secrets;
60
+ # - THE EDGE, unless --core-only: the only thing that listens to the outside
61
+ # (TLS), the sign-in in front of everything, and a small identity provider
62
+ # that knows the operators. Each is a pinned release, checked against a
63
+ # checksum written here, and runs as its own account. Every operator gets
64
+ # a password made on this machine, SHOWN ONCE at the end.
65
+ #
66
+ # Exit codes: 0 done; 64 bad parameters; 78 this machine is refused.
67
+ # ---------------------------------------------------------------------------
68
+ set -euo pipefail
69
+
70
+ SUPPORTED="24.04 26.04"
71
+ ACCOUNT="clavarium"
72
+ CODE="/opt/clavarium"
73
+ DATA="/home/clavarium"
74
+ # What the machines are given -- the shared folders, their read-only views,
75
+ # the work folders and their disks, what was put aside: under /home too (the
76
+ # VM manager shares from there only), but on a chain of directories that ROOT
77
+ # owns from / down, so that the server cannot swap a path between a check and
78
+ # a mount (see clavarium-host, "held"). The server's own data stays in $DATA.
79
+ MNT="/home/clavarium-mnt"
80
+ VAULT="/var/lib/clavarium-vault"
81
+ # The git reader's sandbox: an empty root for it, and its one script.
82
+ RUNNER_ROOT="/var/lib/clavarium/runner-root"
83
+ GIT_READ="/usr/lib/clavarium/git-read"
84
+ ETC="/etc/clavarium"
85
+ HOST_PROGRAM="/usr/local/sbin/clavarium-host"
86
+ UNIT="/etc/systemd/system/clavarium.service"
87
+ UPDATE_CONF="$ETC/update.conf"
88
+ # The edge: exact releases, exact checksums (sha256 of what is downloaded).
89
+ CADDY_VERSION="2.11.4"
90
+ CADDY_SHA256="527fbf917c39189a1e3b31d34fa955601680b2d5c8055d2a87b8b9588dec7bb9"
91
+ PROXY_VERSION="v7.15.4"
92
+ PROXY_SHA256="4fbe902189aab713d9c0519b90a645032d4636ecb523dc36f5cc312d8ebef1e2"
93
+ # The identity provider publishes no archive, only a container image. What is
94
+ # fetched is the one layer of that image that holds the program, named by its
95
+ # checksum: no container tool is needed, and nothing else of the image is used.
96
+ DEX_VERSION="v2.45.1"
97
+ DEX_LAYER="680f1119fdbcdd7f0ad8985b2534ded871b08b5b77c40f337c38aebd062b59c2"
98
+ DEX_SHA256="217aa675738d1cbaf25fe8d7bc04cb00dbc60f91d8465b76b8d8af963d571337"
99
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
100
+
101
+ bad() { echo "install: $*" >&2; echo "usage: install.sh --domain NAME --operator EMAIL [--operator EMAIL...] [--port N] [--relay HOST [--relay-port N]] [--core-only] [--version X.Y.Z | --from-dir DIR] [--updates auto|asap|ask|off] [--update-window HH:MM-HH:MM] [--channel latest|next] [--dry-run]" >&2; echo " or: install.sh --local [--port N] [same options] [--dry-run] to try it on this machine, with no domain" >&2; exit 64; }
102
+ refuse() { echo "install: REFUSED: $*" >&2; exit 78; }
103
+ step() { echo "== $*"; }
104
+
105
+ DOMAIN=""; OPERATORS=""; PORT="7717"; CORE_ONLY="no"; DRY="no"; RELAY=""; RELAY_PORT="22"; LOCAL="no"
106
+ VERSION=""; FROM_DIR=""; UPDATES=""; WINDOW=""; CHANNEL=""
107
+ while [ $# -gt 0 ]; do
108
+ case "$1" in
109
+ --domain) DOMAIN="${2:-}"; shift 2 || bad "--domain needs a value" ;;
110
+ --operator) [ -n "${2:-}" ] || bad "--operator needs a value"; OPERATORS="${OPERATORS:+$OPERATORS,}$2"; shift 2 ;;
111
+ --port) PORT="${2:-}"; shift 2 || bad "--port needs a value" ;;
112
+ --relay) RELAY="${2:-}"; [ -n "$RELAY" ] || bad "--relay needs a value"; shift 2 ;;
113
+ --relay-port) RELAY_PORT="${2:-}"; shift 2 || bad "--relay-port needs a value" ;;
114
+ --core-only) CORE_ONLY="yes"; shift ;;
115
+ --local) LOCAL="yes"; shift ;;
116
+ --version) VERSION="${2:-}"; [ -n "$VERSION" ] || bad "--version needs a value"; shift 2 ;;
117
+ --from-dir) FROM_DIR="${2:-}"; [ -n "$FROM_DIR" ] || bad "--from-dir needs a value"; shift 2 ;;
118
+ --updates) UPDATES="${2:-}"; shift 2 || bad "--updates needs a value" ;;
119
+ --update-window) WINDOW="${2:-}"; shift 2 || bad "--update-window needs a value" ;;
120
+ --channel) CHANNEL="${2:-}"; shift 2 || bad "--channel needs a value" ;;
121
+ --dry-run) DRY="yes"; shift ;;
122
+ *) bad "unknown parameter: $1" ;;
123
+ esac
124
+ done
125
+
126
+ # Every value ends up in a unit file and on a command line: checked here.
127
+ if [ "$LOCAL" = "yes" ]; then
128
+ # To try it: nothing of the outside is set up, so nothing of it is asked.
129
+ [ -z "$DOMAIN" ] && [ -z "$OPERATORS" ] && [ -z "$RELAY" ] || bad "--local is this machine only: it takes no --domain, --operator or --relay"
130
+ CORE_ONLY="yes"
131
+ else
132
+ [[ "$DOMAIN" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}$ ]] || bad "--domain is a host name, lower case, like clav.example.com"
133
+ [ -n "$OPERATORS" ] || bad "at least one --operator is needed: without one, nobody could come in"
134
+ IFS=',' read -r -a each <<< "$OPERATORS"
135
+ for operator in "${each[@]}"; do
136
+ [[ "$operator" =~ ^[a-z0-9._+-]{1,64}@([a-z0-9-]{1,63}\.)+[a-z]{2,24}$ ]] || bad "--operator is an e-mail address, lower case: $operator"
137
+ done
138
+ fi
139
+ [[ "$PORT" =~ ^[0-9]{4,5}$ ]] && [ "$PORT" -ge 1024 ] && [ "$PORT" -le 65535 ] || bad "--port is a number from 1024 to 65535"
140
+ if [ -n "$RELAY" ]; then
141
+ [[ "$RELAY" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}$|^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || bad "--relay is the name or the address of the relay machine"
142
+ [[ "$RELAY_PORT" =~ ^[0-9]{1,5}$ ]] && [ "$RELAY_PORT" -ge 1 ] && [ "$RELAY_PORT" -le 65535 ] || bad "--relay-port is a port number"
143
+ [ "$CORE_ONLY" = "no" ] || bad "--relay comes with the edge: it cannot go with --core-only"
144
+ fi
145
+
146
+ # WHAT IS INSTALLED: an exact version from npm, or a working tree (development).
147
+ [ -z "$VERSION" ] || [[ "$VERSION" =~ ^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,40})?$ ]] || bad "--version is an exact version, like 1.2.3 or 1.3.0-next.1"
148
+ [ -z "$VERSION" ] || [ -z "$FROM_DIR" ] || bad "--version (from npm) or --from-dir (a working tree), not both"
149
+ if [ -n "$FROM_DIR" ]; then
150
+ FROM_DIR="$(cd "$FROM_DIR" 2>/dev/null && pwd)" || bad "--from-dir is a folder that exists"
151
+ [ -f "$FROM_DIR/server/main.mjs" ] && [ -f "$FROM_DIR/package-lock.json" ] || bad "--from-dir is a working tree of Clavarium (server/main.mjs, package-lock.json)"
152
+ fi
153
+ if [ -z "$VERSION" ] && [ -z "$FROM_DIR" ]; then
154
+ if [ -e "$HERE/.git" ]; then FROM_DIR="$HERE"
155
+ else VERSION="$(sed -n 's/^ "version": "\([^"]*\)",$/\1/p' "$HERE/package.json" | head -1)"
156
+ [[ "$VERSION" =~ ^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,40})?$ ]] || refuse "the version of this package was not found in $HERE/package.json: give --version"
157
+ fi
158
+ fi
159
+ [ -z "$UPDATES" ] || [[ "$UPDATES" =~ ^(auto|asap|ask|off)$ ]] || bad "--updates is auto, asap, ask or off"
160
+ [ -z "$WINDOW" ] || { [[ "$WINDOW" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]$ ]] && [ "${WINDOW%-*}" != "${WINDOW#*-}" ]; } || bad "--update-window is two times, like 02:00-05:00"
161
+ [ -z "$CHANNEL" ] || [[ "$CHANNEL" =~ ^(latest|next)$ ]] || bad "--channel is latest or next"
162
+ # What the updates will be, as the settings file has them or as asked now.
163
+ had() { { sed -n "s/^$1=//p" "$UPDATE_CONF" 2>/dev/null || true; } | head -1; }
164
+ SHOW_MODE="${UPDATES:-$(had MODE)}"; SHOW_MODE="${SHOW_MODE:-auto}"
165
+ SHOW_WINDOW="${WINDOW:-$(had WINDOW)}"; SHOW_WINDOW="${SHOW_WINDOW:-02:00-05:00}"
166
+ case "$VERSION" in *-*) default_channel="next" ;; *) default_channel="latest" ;; esac
167
+ SHOW_CHANNEL="${CHANNEL:-$(had CHANNEL)}"; SHOW_CHANNEL="${SHOW_CHANNEL:-$default_channel}"
168
+
169
+ # The Node.js the server runs on: the same pinned version, with the same
170
+ # checksum, as the one put in the VMs. One place says which (server/tools.mjs).
171
+ NODE_VERSION="$(sed -n "s/^export const NODE_VERSION = '\(v[0-9.]*\)';$/\1/p" "$HERE/server/tools.mjs")"
172
+ NODE_SHA256="$(sed -n "s/^export const NODE_SHA256 = '\([0-9a-f]\{64\}\)';$/\1/p" "$HERE/server/tools.mjs")"
173
+ [ -n "$NODE_VERSION" ] && [ -n "$NODE_SHA256" ] || refuse "the pinned Node.js version was not found in $HERE/server/tools.mjs"
174
+
175
+ if [ "$DRY" = "yes" ]; then
176
+ cat <<PLAN
177
+ would install Clavarium:
178
+ address $([ "$LOCAL" = "yes" ] && echo "http://127.0.0.1:$PORT, from this machine only (--local)" || echo "https://$DOMAIN")
179
+ operators $([ "$LOCAL" = "yes" ] && echo "whoever can open a page on this machine: no sign-in" || echo "$OPERATORS")
180
+ 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)")
182
+ 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
+ code $CODE/releases/<version>, $CODE/current (root) data $DATA folders $MNT (root's chain) vault key $VAULT
184
+ root rule $ACCOUNT may run $HOST_PROGRAM, and nothing else
185
+ edge $([ "$LOCAL" = "yes" ] && echo "none (--local): no address in https, no sign-in" || [ "$CORE_ONLY" = "yes" ] && echo "none (--core-only)" || echo "caddy $CADDY_VERSION (TLS), oauth2-proxy $PROXY_VERSION (sign-in), dex $DEX_VERSION (who the operators are)")
186
+ reached $([ "$LOCAL" = "yes" ] && echo "from this machine only; from another one of yours, through ssh -L" || [ -n "$RELAY" ] && echo "through the relay $RELAY (this machine dials out; nothing here listens to the outside)" || echo "directly: ports 443 and 80 of this machine")
187
+ PLAN
188
+ exit 0
189
+ fi
190
+
191
+ [ "$(id -u)" -eq 0 ] || refuse "run it as root (sudo)"
192
+ # /etc/os-release is read in a subshell: it defines VERSION, NAME and more, which
193
+ # would overwrite this script's own (VERSION is the Clavarium version asked for).
194
+ # shellcheck disable=SC1091
195
+ OS_ID="$(. /etc/os-release 2>/dev/null; printf '%s' "${ID:-}")"
196
+ # shellcheck disable=SC1091
197
+ OS_VERSION_ID="$(. /etc/os-release 2>/dev/null; printf '%s' "${VERSION_ID:-}")"
198
+ # shellcheck disable=SC1091
199
+ OS_PRETTY="$(. /etc/os-release 2>/dev/null; printf '%s' "${PRETTY_NAME:-}")"
200
+ [ "$OS_ID" = "ubuntu" ] || refuse "this is ${OS_PRETTY:-not Ubuntu}; Clavarium installs on Ubuntu Server $SUPPORTED"
201
+ case " $SUPPORTED " in *" ${OS_VERSION_ID} "*) : ;; *) refuse "this is Ubuntu ${OS_VERSION_ID:-?}; supported: $SUPPORTED" ;; esac
202
+ [ "$(uname -m)" = "x86_64" ] || refuse "this machine is $(uname -m); only x86_64 is supported"
203
+ grep -Eq '(vmx|svm)' /proc/cpuinfo || echo "install: WARNING: this processor does not show virtualisation: VMs may not start." >&2
204
+
205
+ step "packages"
206
+ export DEBIAN_FRONTEND=noninteractive
207
+ # Installing packages must not stop to ask which services to restart, nor fill the screen with it.
208
+ export NEEDRESTART_SUSPEND=1
209
+ apt-get update -qq
210
+ # bindfs: the read-only views. nftables: the network rules. build-essential
211
+ # and python3: one module of the server (the terminal) is compiled here.
212
+ apt-get install -y -qq bindfs nftables build-essential python3 curl ca-certificates xz-utils sudo snapd openssl git >/dev/null
213
+ snap list multipass >/dev/null 2>&1 || snap install multipass >/dev/null
214
+
215
+ step "account"
216
+ id "$ACCOUNT" >/dev/null 2>&1 || useradd --system --home-dir "$DATA" --no-create-home --shell /usr/sbin/nologin "$ACCOUNT"
217
+ [ "$(getent passwd "$ACCOUNT" | cut -d: -f6)" = "$DATA" ] || usermod --home "$DATA" "$ACCOUNT"
218
+ install -d -o "$ACCOUNT" -g "$ACCOUNT" -m 0750 "$DATA"
219
+ install -d -o "$ACCOUNT" -g "$ACCOUNT" -m 0700 "$VAULT"
220
+ # What gets mounted into VMs is arranged by root only: see clavarium-host.
221
+ install -d -o root -g root -m 0755 "$MNT" "$MNT/resources" "$MNT/views" "$MNT/removed" "$MNT/work" "$MNT/work-disks"
222
+
223
+ step "code"
224
+ # No update while this runs: the timer and its service wait.
225
+ systemctl stop clavarium-update.timer clavarium-update.service 2>/dev/null || true
226
+ install -d -o root -g root -m 0755 "$CODE" "$CODE/releases" "$CODE/nodes"
227
+ # An installation from before releases: its code sits in $CODE itself. It
228
+ # keeps running while the new release is made beside it, and is removed only
229
+ # once the new one answers (below).
230
+ LEGACY="no"; [ -d "$CODE/server" ] && [ ! -L "$CODE/current" ] && LEGACY="yes"
231
+ # The Node.js the updater runs on to make the release (each release then has
232
+ # the one its own code pins, most often this one).
233
+ NODE_DIR="$CODE/nodes/$NODE_VERSION"
234
+ if [ ! -x "$NODE_DIR/bin/node" ]; then
235
+ rm -rf -- "$NODE_DIR.partial"
236
+ if [ -x "$CODE/node/bin/node" ] && [ "$("$CODE/node/bin/node" --version)" = "$NODE_VERSION" ]; then
237
+ cp -a "$CODE/node" "$NODE_DIR.partial"
238
+ else
239
+ archive="$(mktemp)"
240
+ curl --proto '=https' --tlsv1.2 -fsSL --max-time 600 -o "$archive" "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.xz"
241
+ echo "$NODE_SHA256 $archive" | sha256sum -c - >/dev/null || refuse "the Node.js archive does not have the expected checksum"
242
+ install -d "$NODE_DIR.partial"
243
+ tar -xJf "$archive" -C "$NODE_DIR.partial" --strip-components=1 --no-same-owner
244
+ rm -f -- "$archive"
245
+ fi
246
+ mv -- "$NODE_DIR.partial" "$NODE_DIR"
247
+ fi
248
+ updater() { PATH="$NODE_DIR/bin:$PATH" "$NODE_DIR/bin/node" "$HERE/deploy/updater.mjs" "$@"; }
249
+ if [ -n "$VERSION" ]; then
250
+ echo " clavarium@$VERSION from npm: fetched, checked, built, tried (a few minutes)"
251
+ release="$(updater prepare --version "$VERSION")" || refuse "clavarium@$VERSION was not installed (above: why)"
252
+ else
253
+ echo " the working tree $FROM_DIR (development): copied, built, tried"
254
+ release="$(updater prepare --from-dir "$FROM_DIR")" || refuse "the working tree was not installed (above: why)"
255
+ fi
256
+ systemctl stop clavarium.service 2>/dev/null || true
257
+ updater switch "$release"
258
+ APP="$CODE/current/app"
259
+ NODE="$CODE/current/node/bin/node"
260
+ # The updater of the release now in place, for what follows and from now on.
261
+ updater() { PATH="$CODE/current/node/bin:$PATH" "$NODE" "$APP/deploy/updater.mjs" "$@"; }
262
+
263
+ step "the git reader"
264
+ # The one way the server reads an agent's repository: a sandbox around one
265
+ # fixed script (clavarium-host "git read"). Its root is empty and root's.
266
+ install -d -o root -g root -m 0755 /var/lib/clavarium /usr/lib/clavarium "$RUNNER_ROOT"
267
+ install -o root -g root -m 0755 "$APP/deploy/git-read" "$GIT_READ"
268
+
269
+ step "the one privileged program"
270
+ install -o root -g root -m 0755 "$APP/deploy/clavarium-host" "$HOST_PROGRAM"
271
+ install -d -o root -g root -m 0755 "$ETC"
272
+ printf 'DATA=%s\nMNT=%s\n' "$DATA" "$MNT" > "$ETC/host.conf"; chmod 0644 "$ETC/host.conf"
273
+ rule="$(mktemp)"
274
+ printf '# Clavarium: the server account may run ONE program as root, and nothing else.\n%s ALL=(root) NOPASSWD: %s\n' "$ACCOUNT" "$HOST_PROGRAM" > "$rule"
275
+ visudo -cf "$rule" >/dev/null || refuse "the sudo rule did not pass its check"
276
+ install -o root -g root -m 0440 "$rule" /etc/sudoers.d/clavarium; rm -f -- "$rule"
277
+
278
+ step "what the machines are given, on root's chain"
279
+ # ONCE, from an earlier version: resources/, views/, work/, work-disks/ and
280
+ # removed/ move from $DATA to $MNT, with the server stopped (above); the
281
+ # machines are given back what they had, from the new places, through the
282
+ # program just installed. See deploy/move-to-mnt.sh.
283
+ bash "$APP/deploy/move-to-mnt.sh" --data "$DATA" --mnt "$MNT" --host "$HOST_PROGRAM" --node "$NODE"
284
+ # Each component from / to each of them belongs to root, nobody else may
285
+ # write it, and no access list says otherwise (ls marks one with a '+').
286
+ for at in /home "$MNT" "$MNT/resources" "$MNT/views" "$MNT/removed" "$MNT/work" "$MNT/work-disks"; do
287
+ [ "$(stat -c %u "$at")" = "0" ] && [ -d "$at" ] && [ ! -L "$at" ] || refuse "$at must be a directory of root"
288
+ case "$(stat -c %A "$at")" in ?????w????|????????w?) refuse "$at may be written by others than root" ;; esac
289
+ mode="$(ls -ld -- "$at" | cut -d' ' -f1)"; [ "${#mode}" -eq 10 ] || refuse "$at has an access list (getfacl): only root may write it"
290
+ done
291
+ # Settings that are not parameters (the speech-to-text endpoint): kept across upgrades.
292
+ [ -e "$ETC/server.env" ] || install -o root -g root -m 0600 /dev/null "$ETC/server.env"
293
+
294
+ step "service"
295
+ # NO private mount namespace here (PrivateTmp, ProtectHome, ProtectSystem...):
296
+ # the read-only views are mounts made on behalf of this service, and the VM
297
+ # manager must see them. In a private namespace it would see empty folders.
298
+ cat > "$UNIT" <<UNITFILE
299
+ [Unit]
300
+ Description=Clavarium - conversations as VMs
301
+ After=network-online.target snap.multipass.multipassd.service
302
+ Wants=network-online.target
303
+
304
+ [Service]
305
+ User=$ACCOUNT
306
+ Group=$ACCOUNT
307
+ WorkingDirectory=$CODE/current/app
308
+ EnvironmentFile=$ETC/server.env
309
+ ExecStart=$CODE/current/node/bin/node server/main.mjs --host "sudo -n $HOST_PROGRAM" --data $DATA --vault-key $VAULT/vault.key --port $PORT$([ "$LOCAL" = "yes" ] || echo " --operators $OPERATORS --public-origin https://$DOMAIN")
310
+ Restart=on-failure
311
+ RestartSec=3
312
+
313
+ [Install]
314
+ WantedBy=multi-user.target
315
+ UNITFILE
316
+
317
+ # THE UPDATES: their settings (kept from an earlier run unless asked here),
318
+ # and the timer that looks for a new version every 15 minutes. What it does
319
+ # is the updater's (deploy/updater.mjs), as root, through this service only.
320
+ updater set port "$PORT" >/dev/null
321
+ updater set mode "$SHOW_MODE" >/dev/null
322
+ updater set window "$SHOW_WINDOW" >/dev/null
323
+ updater set channel "$SHOW_CHANNEL" >/dev/null
324
+ cat > /etc/systemd/system/clavarium-update.service <<UPDATEUNIT
325
+ [Unit]
326
+ Description=Clavarium - looks for a new version, and updates as its settings say
327
+ After=network-online.target
328
+ Wants=network-online.target
329
+
330
+ [Service]
331
+ Type=oneshot
332
+ ExecStart=$CODE/current/node/bin/node $CODE/current/app/deploy/updater.mjs run
333
+ TimeoutStartSec=3600
334
+ UPDATEUNIT
335
+ cat > /etc/systemd/system/clavarium-update.timer <<'UPDATETIMER'
336
+ [Unit]
337
+ Description=Clavarium - looks for a new version every 15 minutes
338
+
339
+ [Timer]
340
+ OnBootSec=10min
341
+ OnUnitActiveSec=15min
342
+ RandomizedDelaySec=3min
343
+
344
+ [Install]
345
+ WantedBy=timers.target
346
+ UPDATETIMER
347
+ systemctl daemon-reload
348
+ systemctl enable --quiet clavarium.service
349
+ systemctl enable --quiet clavarium-update.timer
350
+ systemctl restart clavarium.service
351
+
352
+ step "checks"
353
+ for _ in $(seq 1 20); do
354
+ curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/" && break
355
+ sleep 1
356
+ done
357
+ systemctl is-active --quiet clavarium.service || refuse "the service did not stay up: journalctl -u clavarium"
358
+ curl -s -o /dev/null --max-time 5 "http://127.0.0.1:$PORT/" || refuse "the server does not answer on 127.0.0.1:$PORT"
359
+ sudo -u "$ACCOUNT" sudo -n -l "$HOST_PROGRAM" >/dev/null || refuse "the account cannot reach its one program"
360
+ if sudo -u "$ACCOUNT" sudo -n -l /bin/sh >/dev/null 2>&1; then refuse "the account can run more than its one program as root"; fi
361
+ # It answers: the copy of an installation from before releases goes, and
362
+ # releases beyond the last two. Then the timer may look for updates.
363
+ if [ "$LEGACY" = "yes" ]; then
364
+ rm -rf -- "$CODE/server" "$CODE/web" "$CODE/config" "$CODE/node_modules" "$CODE/node" "$CODE/package.json" "$CODE/package-lock.json"
365
+ echo " the earlier copy of the code in $CODE was removed: it runs from $CODE/current"
366
+ fi
367
+ updater prune
368
+ systemctl start clavarium-update.timer
369
+ echo " running: $(sed -n 's/^ "version": "\([^"]*\)",$/\1/p' "$APP/package.json" | head -1) ($release)"
370
+
371
+ if [ "$LOCAL" = "yes" ]; then
372
+ echo "Clavarium is installed, to try it: http://127.0.0.1:$PORT (from this machine only)"
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 ---------------------------------
385
+ # Downloads one file and refuses it unless it has the expected checksum.
386
+ fetch() { # <url> <sha256> <destination> [extra curl arguments...]
387
+ local url="$1" sum="$2" to="$3"; shift 3
388
+ curl --proto '=https' --tlsv1.2 -fsSL --max-time 600 "$@" -o "$to" "$url"
389
+ echo "$sum $to" | sha256sum -c - >/dev/null || refuse "not the expected checksum: $url"
390
+ }
391
+ account() { id "$1" >/dev/null 2>&1 || useradd --system --home-dir "$2" --no-create-home --shell /usr/sbin/nologin "$1"; }
392
+
393
+ step "edge: programs"
394
+ work="$(mktemp -d)"
395
+ if [ "$(/usr/local/bin/caddy version 2>/dev/null | cut -d' ' -f1)" != "v$CADDY_VERSION" ]; then
396
+ fetch "https://github.com/caddyserver/caddy/releases/download/v$CADDY_VERSION/caddy_${CADDY_VERSION}_linux_amd64.tar.gz" "$CADDY_SHA256" "$work/caddy.tgz"
397
+ tar -xzf "$work/caddy.tgz" -C "$work" --no-same-owner caddy
398
+ install -o root -g root -m 0755 "$work/caddy" /usr/local/bin/caddy
399
+ fi
400
+ if ! /usr/local/bin/oauth2-proxy --version 2>/dev/null | grep -q " $PROXY_VERSION "; then
401
+ fetch "https://github.com/oauth2-proxy/oauth2-proxy/releases/download/$PROXY_VERSION/oauth2-proxy-$PROXY_VERSION.linux-amd64.tar.gz" "$PROXY_SHA256" "$work/proxy.tgz"
402
+ tar -xzf "$work/proxy.tgz" -C "$work" --no-same-owner --strip-components=1
403
+ install -o root -g root -m 0755 "$work/oauth2-proxy" /usr/local/bin/oauth2-proxy
404
+ fi
405
+ if [ "$(sha256sum /usr/local/bin/dex 2>/dev/null | cut -d' ' -f1)" != "$DEX_SHA256" ]; then
406
+ token="$(curl --proto '=https' --tlsv1.2 -fsSL --max-time 60 'https://ghcr.io/token?scope=repository:dexidp/dex:pull' | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')"
407
+ [ -n "$token" ] || refuse "the registry gave no anonymous token for the identity provider"
408
+ fetch "https://ghcr.io/v2/dexidp/dex/blobs/sha256:$DEX_LAYER" "$DEX_LAYER" "$work/dex.tgz" -H "Authorization: Bearer $token"
409
+ tar -xzf "$work/dex.tgz" -C "$work" --no-same-owner usr/local/bin/dex
410
+ echo "$DEX_SHA256 $work/usr/local/bin/dex" | sha256sum -c - >/dev/null || refuse "the identity provider is not the expected program"
411
+ install -o root -g root -m 0755 "$work/usr/local/bin/dex" /usr/local/bin/dex
412
+ fi
413
+ rm -rf -- "$work"
414
+
415
+ step "edge: accounts and settings"
416
+ account caddy /var/lib/caddy; account dex /var/lib/dex; account oauth2-proxy /nonexistent
417
+ install -d -o caddy -g caddy -m 0700 /var/lib/caddy
418
+ install -d -o dex -g dex -m 0700 /var/lib/dex
419
+ install -d -o root -g root -m 0755 /etc/caddy /etc/oauth2-proxy
420
+ install -d -o root -g dex -m 0750 /etc/dex
421
+
422
+ # The identity file holds the operators and their passwords. It is written
423
+ # ONCE: running this script again keeps it, and the passwords with it.
424
+ PASSWORDS=""
425
+ if [ ! -e /etc/dex/config.yaml ]; then
426
+ client_secret="$(openssl rand -hex 32)"
427
+ cookie_secret="$(openssl rand -base64 32 | tr '+/' '-_')"
428
+ identity="$(mktemp)"
429
+ cat > "$identity" <<DEX
430
+ # Who may sign in to Clavarium on THIS machine. Written by install.sh.
431
+ issuer: https://$DOMAIN/dex
432
+ storage:
433
+ type: sqlite3
434
+ config:
435
+ file: /var/lib/dex/dex.db
436
+ web:
437
+ # Loopback only: the TLS front publishes it under /dex.
438
+ http: 127.0.0.1:5556
439
+ oauth2:
440
+ skipApprovalScreen: true
441
+ staticClients:
442
+ - id: clavarium
443
+ name: Clavarium
444
+ secret: $client_secret
445
+ redirectURIs:
446
+ - https://$DOMAIN/oauth2/callback
447
+ enablePasswordDB: true
448
+ staticPasswords:
449
+ DEX
450
+ for operator in "${each[@]}"; do
451
+ password="$(openssl rand -base64 18 | tr '+/' '-_')"
452
+ # On standard input: a password never sits on a command line.
453
+ hash="$(printf '%s\n' "$password" | /usr/local/bin/caddy hash-password)"
454
+ [[ "$hash" == \$2* ]] || refuse "the password could not be hashed"
455
+ printf ' - email: "%s"\n hash: "%s"\n username: "%s"\n userID: "%s"\n' \
456
+ "$operator" "$hash" "${operator%%@*}" "$(cat /proc/sys/kernel/random/uuid)" >> "$identity"
457
+ PASSWORDS="$PASSWORDS $operator $password"$'\n'
458
+ done
459
+ install -o root -g dex -m 0640 "$identity" /etc/dex/config.yaml; rm -f -- "$identity"
460
+
461
+ # The sign-in proxy talks to the identity provider on the loopback, by
462
+ # explicit addresses: it does not depend on this machine resolving its own
463
+ # public name, nor on the certificate being issued yet.
464
+ proxy="$(mktemp)"
465
+ cat > "$proxy" <<PROXY
466
+ provider = "oidc"
467
+ client_id = "clavarium"
468
+ client_secret = "$client_secret"
469
+ cookie_secret = "$cookie_secret"
470
+ oidc_issuer_url = "https://$DOMAIN/dex"
471
+ skip_oidc_discovery = true
472
+ login_url = "https://$DOMAIN/dex/auth"
473
+ redeem_url = "http://127.0.0.1:5556/dex/token"
474
+ oidc_jwks_url = "http://127.0.0.1:5556/dex/keys"
475
+ redirect_url = "https://$DOMAIN/oauth2/callback"
476
+ # Who is an operator is decided by the identity file, and again by the server.
477
+ email_domains = ["*"]
478
+ cookie_secure = true
479
+ cookie_samesite = "lax"
480
+ set_xauthrequest = true
481
+ reverse_proxy = true
482
+ skip_provider_button = true
483
+ # The operators were chosen by whoever installed this: no "grant access" page.
484
+ approval_prompt = "auto"
485
+ http_address = "127.0.0.1:4180"
486
+ upstreams = ["http://127.0.0.1:$PORT/"]
487
+ # A paired device does not sign in: its own door (/c/) checks its key. The
488
+ # identity headers are stripped there, so nobody can claim to be an operator.
489
+ skip_auth_routes = ["^/c/"]
490
+ skip_auth_strip_headers = true
491
+ PROXY
492
+ install -o root -g root -m 0600 "$proxy" /etc/oauth2-proxy/oauth2-proxy.cfg; rm -f -- "$proxy"
493
+ else
494
+ echo " the identity file exists: kept, with its operators and passwords."
495
+ echo " (to start again: remove /etc/dex/config.yaml and run this script again)"
496
+ sed -i "s#^upstreams = .*#upstreams = [\"http://127.0.0.1:$PORT/\"]#" /etc/oauth2-proxy/oauth2-proxy.cfg
497
+ fi
498
+
499
+ # Behind a relay, the front listens on the loopback only: the tunnel is the
500
+ # one way in.
501
+ BIND=""; [ -z "$RELAY" ] || BIND=" bind 127.0.0.1"
502
+ cat > /etc/caddy/Caddyfile <<CADDY
503
+ # The only thing on this machine that takes what comes from the outside.
504
+ $DOMAIN {
505
+ $BIND
506
+ tls {
507
+ issuer acme {
508
+ disable_tlsalpn_challenge
509
+ }
510
+ issuer internal
511
+ }
512
+ handle /dex/* {
513
+ reverse_proxy 127.0.0.1:5556
514
+ }
515
+ handle {
516
+ reverse_proxy 127.0.0.1:4180
517
+ }
518
+ }
519
+ CADDY
520
+
521
+ step "edge: services"
522
+ cat > /etc/systemd/system/caddy.service <<'CADDYUNIT'
523
+ [Unit]
524
+ Description=Caddy - the TLS front of this machine
525
+ After=network-online.target
526
+ Wants=network-online.target
527
+
528
+ [Service]
529
+ User=caddy
530
+ Group=caddy
531
+ Environment=XDG_DATA_HOME=/var/lib/caddy XDG_CONFIG_HOME=/var/lib/caddy
532
+ ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
533
+ ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile
534
+ AmbientCapabilities=CAP_NET_BIND_SERVICE
535
+ Restart=on-failure
536
+
537
+ [Install]
538
+ WantedBy=multi-user.target
539
+ CADDYUNIT
540
+ cat > /etc/systemd/system/dex.service <<'DEXUNIT'
541
+ [Unit]
542
+ Description=Dex - who the operators of this machine are
543
+ After=network.target
544
+
545
+ [Service]
546
+ User=dex
547
+ ExecStart=/usr/local/bin/dex serve /etc/dex/config.yaml
548
+ NoNewPrivileges=yes
549
+ ProtectSystem=strict
550
+ ProtectHome=yes
551
+ PrivateTmp=yes
552
+ ReadWritePaths=/var/lib/dex
553
+ Restart=on-failure
554
+
555
+ [Install]
556
+ WantedBy=multi-user.target
557
+ DEXUNIT
558
+ cat > /etc/systemd/system/oauth2-proxy.service <<'PROXYUNIT'
559
+ [Unit]
560
+ Description=oauth2-proxy - the sign-in in front of Clavarium
561
+ After=network-online.target dex.service
562
+ Wants=network-online.target
563
+ StartLimitIntervalSec=0
564
+
565
+ [Service]
566
+ User=oauth2-proxy
567
+ Group=oauth2-proxy
568
+ LoadCredential=oauth2-proxy.cfg:/etc/oauth2-proxy/oauth2-proxy.cfg
569
+ ExecStart=/usr/local/bin/oauth2-proxy --config %d/oauth2-proxy.cfg
570
+ Restart=on-failure
571
+ RestartSec=5
572
+ NoNewPrivileges=yes
573
+ ProtectSystem=strict
574
+ ProtectHome=yes
575
+ PrivateTmp=yes
576
+
577
+ [Install]
578
+ WantedBy=multi-user.target
579
+ PROXYUNIT
580
+ systemctl daemon-reload
581
+ systemctl enable --quiet caddy.service dex.service oauth2-proxy.service
582
+ systemctl restart dex.service caddy.service oauth2-proxy.service
583
+
584
+ step "edge: checks"
585
+ local_get() { curl -sk -o /dev/null -w '%{http_code}' --max-time 5 --resolve "$DOMAIN:443:127.0.0.1" "https://$DOMAIN$1" || true; }
586
+ for _ in $(seq 1 30); do
587
+ [ "$(local_get /dex/.well-known/openid-configuration)" = "200" ] && [ "$(local_get /oauth2/sign_in)" = "200" ] && break
588
+ sleep 2
589
+ done
590
+ for service in caddy dex oauth2-proxy; do
591
+ systemctl is-active --quiet "$service.service" || refuse "$service did not stay up: journalctl -u $service"
592
+ done
593
+ [ "$(local_get /dex/.well-known/openid-configuration)" = "200" ] || refuse "the identity provider does not answer through the front"
594
+ # Not signed in: the server must be out of reach.
595
+ case "$(local_get /api/conversations)" in 302|401|403) : ;; *) refuse "the server answers somebody who has not signed in" ;; esac
596
+
597
+ # ------------------------------ THE TUNNEL --------------------------------
598
+ TUNNEL_KEY=""; RELAY_PRINT=""
599
+ if [ -n "$RELAY" ]; then
600
+ step "tunnel to the relay"
601
+ apt-get install -y -qq openssh-client >/dev/null
602
+ account clavarium-tunnel /nonexistent
603
+ install -d -o clavarium-tunnel -g clavarium-tunnel -m 0700 /var/lib/clavarium-tunnel
604
+ # The private key is made here and never leaves this machine.
605
+ [ -e /var/lib/clavarium-tunnel/key ] || sudo -u clavarium-tunnel ssh-keygen -q -t ed25519 -N '' -C clavarium-tunnel -f /var/lib/clavarium-tunnel/key
606
+ # The relay's identity is taken once and then held: a different machine
607
+ # answering later at the same address is refused.
608
+ if [ ! -s /var/lib/clavarium-tunnel/known_hosts ]; then
609
+ scanned="$(ssh-keyscan -T 15 -t ed25519 -p "$RELAY_PORT" "$RELAY" 2>/dev/null || true)"
610
+ [ -n "$scanned" ] || refuse "the relay $RELAY does not answer on port $RELAY_PORT"
611
+ printf '%s\n' "$scanned" > /var/lib/clavarium-tunnel/known_hosts
612
+ chown clavarium-tunnel:clavarium-tunnel /var/lib/clavarium-tunnel/known_hosts
613
+ fi
614
+ cat > /etc/systemd/system/clavarium-tunnel.service <<TUNNEL
615
+ [Unit]
616
+ Description=Clavarium - the outgoing tunnel to the relay
617
+ After=network-online.target caddy.service
618
+ Wants=network-online.target
619
+ StartLimitIntervalSec=0
620
+
621
+ [Service]
622
+ User=clavarium-tunnel
623
+ Group=clavarium-tunnel
624
+ # Only TLS (443) and the certificate challenge (80) are carried.
625
+ ExecStart=/usr/bin/ssh -N -T -o BatchMode=yes -o IdentitiesOnly=yes -o IdentityFile=/var/lib/clavarium-tunnel/key -o UserKnownHostsFile=/var/lib/clavarium-tunnel/known_hosts -o StrictHostKeyChecking=yes -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ConnectTimeout=15 -R *:8443:127.0.0.1:443 -R *:8080:127.0.0.1:80 -p $RELAY_PORT clavarium-tunnel@$RELAY
626
+ Restart=always
627
+ RestartSec=5
628
+ NoNewPrivileges=yes
629
+ ProtectSystem=strict
630
+ ProtectHome=yes
631
+ PrivateTmp=yes
632
+
633
+ [Install]
634
+ WantedBy=multi-user.target
635
+ TUNNEL
636
+ systemctl daemon-reload
637
+ systemctl enable --quiet clavarium-tunnel.service
638
+ systemctl restart clavarium-tunnel.service
639
+ TUNNEL_KEY="$(cat /var/lib/clavarium-tunnel/key.pub)"
640
+ RELAY_PRINT="$(ssh-keygen -l -f /var/lib/clavarium-tunnel/known_hosts | cut -d' ' -f2 | head -1)"
641
+ fi
642
+
643
+ echo
644
+ echo "Clavarium is installed: https://$DOMAIN"
645
+ if [ -n "$PASSWORDS" ]; then
646
+ echo "Sign in with (shown ONCE, kept nowhere in clear):"
647
+ printf '%s' "$PASSWORDS"
648
+ else
649
+ echo "Operators and passwords: unchanged."
650
+ fi
651
+ if [ -n "$RELAY" ]; then
652
+ echo
653
+ echo "ON THE RELAY ($RELAY), run this one command; the tunnel then comes up by itself:"
654
+ echo " sudo deploy/relay.sh --key \"$TUNNEL_KEY\""
655
+ echo "It will print this, and it must be the same: $RELAY_PRINT"
656
+ echo "The name $DOMAIN must lead to the relay."
657
+ else
658
+ echo "The name $DOMAIN must lead to this machine (ports 443 and 80)."
659
+ fi