Repository navigation
Expand file tree
/
Copy pathinstall-server.sh
More file actions
397 lines (359 loc) · 17.8 KB
/
Copy pathinstall-server.sh
File metadata and controls
397 lines (359 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
#!/usr/bin/env bash
# raba server setup script (.internal/TECH_DOC.md project structure entry) --
# interactively prompts for config, writes .env, then installs either via
# Docker (pulls the published GHCR image) or Direct (downloads the
# published server binary, installs it as a systemd service). Needs
# .github/workflows/release.yml to have actually published something --
# won't work against a repo with no tagged releases yet.
#
# Usage (works today, no landing site needed -- same raw-GitHub-content
# reasoning as install.sh's own usage comment):
# curl -fsSL https://raw.githubusercontent.com/codad5/raba/master/install-server.sh | bash
#
# bash, not POSIX sh, unlike install.sh -- this script needs `read -p` for
# interactive prompts and associative-array-free but still fairly involved
# control flow; bash is a safe assumption on any server this script
# targets (systemd implies a modern Linux distro, which ships bash by
# default), unlike install.sh which specifically wants to run on whatever
# minimal shell a `curl | sh` one-liner lands in.
#
# Scope, deliberately simplified for this first version (each noted where
# it applies below): runs the systemd service as the invoking user rather
# than provisioning a dedicated unprivileged system account; the
# SNI-passthrough proxy path fills in the template and prints next steps
# rather than unattended-installing/configuring nginx or Caddy itself
# (distro package-manager differences make that a much bigger job than
# this pass covers).
set -euo pipefail
REPO="codad5/raba"
INSTALL_DIR="${RABA_SERVER_DIR:-$HOME/.raba-server}"
log() {
printf 'raba-install-server: %s\n' "$1"
}
fail() {
printf 'raba-install-server: error: %s\n' "$1" >&2
exit 1
}
# Downloads a file from install/templates/ in the repo. This script never
# assumes a local checkout is present -- its own primary documented usage
# is `curl | bash`, with nothing on disk beforehand -- so templates are
# always fetched fresh from GitHub's raw-content host, the same source
# install.sh/install.ps1 already download release binaries from.
fetch_template() {
local name="$1"
curl -fsSL "https://raw.githubusercontent.com/$REPO/master/install/templates/$name" \
|| fail "couldn't download template $name"
}
# `read -p` writes its prompt to stdout, which is fine interactively but
# would be silently swallowed if this whole script is itself piped from
# `curl` -- redirecting the prompt to the terminal directly (/dev/tty)
# instead of stdout is what makes `curl ... | bash` still able to prompt
# the user rather than hanging or reading prompts as if they were input.
prompt() {
local message="$1"
local default="${2:-}"
local reply
if [ -n "$default" ]; then
read -r -p "$message [$default]: " reply < /dev/tty
echo "${reply:-$default}"
else
read -r -p "$message: " reply < /dev/tty
echo "$reply"
fi
}
prompt_yes_no() {
local message="$1"
local default="${2:-n}"
local reply
read -r -p "$message [y/N]: " reply < /dev/tty
reply="${reply:-$default}"
case "$reply" in
y | Y | yes | Yes) return 0 ;;
*) return 1 ;;
esac
}
# Step 1: interactive config -- everything install-server.sh actually
# asks about, per .internal/TECH_DOC.md's own description of this script. JWT_SECRET
# is deliberately never one of these prompts (see write_env below).
collect_config() {
log "Let's set up your raba instance."
RABA_DOMAIN="$(prompt "Domain this server will be reachable at (e.g. tunnel.example.com)")"
[ -n "$RABA_DOMAIN" ] || fail "a domain is required"
USE_ACME=false
if prompt_yes_no "Set up automated TLS via Cloudflare DNS-01? (needs a Cloudflare API token scoped to this domain's zone)"; then
USE_ACME=true
RABA_DNS_API_TOKEN="$(prompt "Cloudflare API token (DNS-edit scope)")"
RABA_DNS_ZONE_ID="$(prompt "Cloudflare zone ID for $RABA_DOMAIN")"
RABA_ACME_EMAIL="$(prompt "Contact email for Let's Encrypt (renewal-failure notifications)")"
if prompt_yes_no "Use Let's Encrypt's staging environment instead of production? (untrusted certs, higher rate limits -- for testing only)"; then
RABA_ACME_STAGING=true
else
RABA_ACME_STAGING=false
fi
fi
ALLOW_SIGNUP=false
if prompt_yes_no "Allow public signup on this instance? (the very first account can always sign up regardless -- this only affects who can join after that)"; then
ALLOW_SIGNUP=true
fi
# raba's actual listener ports -- asked directly rather than inferred
# from a separate yes/no, since the port numbers themselves already
# say everything: choosing 443/80 means raba binds them directly (no
# proxy needed, it IS what's listening); choosing anything else means
# something else needs to own 443/80 and pass traffic through to
# whatever raba's on instead. Matters for BOTH install paths below,
# not just Direct -- Docker's port mapping and Direct's systemd
# AmbientCapabilities are two different mechanisms for the same
# underlying choice.
RABA_HTTPS_PORT="$(prompt "Port for raba's HTTPS listener (443 if this server is dedicated to raba alone)" "7222")"
RABA_HTTP_PORT="$(prompt "Port for raba's plain-HTTP redirect listener (80 if dedicated)" "8080")"
PROXY_CHOICE=""
if [ "$RABA_HTTPS_PORT" = "443" ]; then
log "raba will bind :443 directly. Since nothing else on this box can also bind :443 once it"
log "does, only do this if this server is dedicated to raba alone -- re-run this script and"
log "pick a different port later if you ever need to free :443 back up for something else."
if port_in_use 443; then
log "warning: something is already listening on :443 on this host -- raba may fail to"
log "start, or may conflict with whatever that is."
fi
else
log "raba's HTTPS listener will stay on :$RABA_HTTPS_PORT -- something needs to pass :443"
log "traffic through to it for the outside world to reach it on the standard port."
PROXY_CHOICE="$(prompt "Which reverse proxy do you use (or want) for that :443 passthrough? (nginx/caddy/none -- I'll set it up myself)" "nginx")"
fi
if [ "$RABA_HTTP_PORT" = "80" ]; then
log "raba will bind :80 directly for the plain-HTTP redirect listener."
if port_in_use 80; then
log "warning: something is already listening on :80 on this host -- raba may fail to"
log "start, or may conflict with whatever that is."
fi
fi
}
# Step 2: write .env -- JWT_SECRET is generated here, never prompted for
# above (.internal/TECH_DOC.md is explicit about this: a user-supplied session
# secret is a foot-gun, generating it ourselves is strictly safer).
#
# Idempotency: if an existing JWT_SECRET is already present in .env, it's
# reused rather than regenerated. Provisioning scripts should converge to
# the same state on a re-run, not repeat a disruptive action every time --
# silently rotating the session secret on every re-run would log out
# every active dashboard session each time this script runs, turning
# "update my server" into "log everyone out" by accident.
write_env() {
mkdir -p "$INSTALL_DIR"
local env_file="$INSTALL_DIR/.env"
local jwt_secret=""
if [ -f "$env_file" ]; then
jwt_secret="$(grep -m1 '^JWT_SECRET=' "$env_file" 2> /dev/null | cut -d= -f2-)"
fi
if [ -n "$jwt_secret" ]; then
log "reusing existing JWT_SECRET from $env_file"
else
jwt_secret="$(openssl rand -hex 32)"
fi
{
echo "RABA_DOMAIN=$RABA_DOMAIN"
echo "JWT_SECRET=$jwt_secret"
echo "ALLOW_SIGNUP_DEFAULT=$ALLOW_SIGNUP"
# Written explicitly (not left to the server's own built-in
# defaults) so the ports chosen in collect_config are visible and
# persisted, not just a one-time in-memory choice.
echo "RABA_HTTPS_PORT=$RABA_HTTPS_PORT"
echo "RABA_HTTP_PORT=$RABA_HTTP_PORT"
if [ "$USE_ACME" = true ]; then
echo "RABA_DNS_API_TOKEN=$RABA_DNS_API_TOKEN"
echo "RABA_DNS_ZONE_ID=$RABA_DNS_ZONE_ID"
echo "RABA_ACME_EMAIL=$RABA_ACME_EMAIL"
echo "RABA_DNS_PROVIDER=cloudflare"
echo "RABA_ACME_STAGING=$RABA_ACME_STAGING"
fi
} > "$env_file"
chmod 600 "$env_file"
log "wrote $env_file"
}
# Step 3a: Docker path -- pulls the image release.yml publishes rather
# than building from source (this script doesn't assume a full repo
# checkout is even present). Writes a minimal, self-contained
# docker-compose.yml instead of depending on the repo's own one.
install_docker() {
command -v docker > /dev/null || fail "docker not found -- install Docker first, or choose the Direct install path instead"
# Port mappings read from .env via docker compose's own variable
# substitution (${VAR:-default}), not hardcoded -- this script never
# actually prompts for/writes RABA_HTTPS_PORT and friends into .env,
# so they fall back to the server's built-in defaults today, but if
# someone edits .env afterward to change a port, this mapping must
# follow it or the container becomes unreachable on its old port
# while actually listening on the new one. Matches the repo's own
# root docker-compose.yml exactly. The heredoc delimiter is quoted
# ('COMPOSE', not COMPOSE) specifically so bash writes the ${...}
# syntax out literally instead of trying to expand it itself right
# now, in this script's own shell, where none of those vars are set
# -- which also means $REPO can't be interpolated directly here
# either, hence the __RABA_IMAGE__ placeholder + sed below, same
# pattern this repo already uses for raba.service.template.
cat > "$INSTALL_DIR/docker-compose.yml" << 'COMPOSE'
services:
raba:
image: __RABA_IMAGE__
restart: unless-stopped
env_file:
- .env
ports:
- "${RABA_HTTPS_PORT:-7222}:${RABA_HTTPS_PORT:-7222}"
- "${RABA_HTTP_PORT:-8080}:${RABA_HTTP_PORT:-8080}"
- "${RABA_TUNNEL_PORT:-4443}:${RABA_TUNNEL_PORT:-4443}"
- "${RABA_TCP_UDP_PORT_RANGE_START:-20000}-${RABA_TCP_UDP_PORT_RANGE_END:-21000}:${RABA_TCP_UDP_PORT_RANGE_START:-20000}-${RABA_TCP_UDP_PORT_RANGE_END:-21000}"
volumes:
- raba-data:/data
volumes:
raba-data:
COMPOSE
sed -i "s|__RABA_IMAGE__|ghcr.io/$REPO:latest|" "$INSTALL_DIR/docker-compose.yml"
log "pulling ghcr.io/$REPO:latest and starting..."
(cd "$INSTALL_DIR" && docker compose up -d)
log "running on :$RABA_HTTPS_PORT/:$RABA_HTTP_PORT -- check status with: docker compose -f $INSTALL_DIR/docker-compose.yml ps"
}
# Detects whether something is already listening on a given local TCP
# port -- used to decide whether raba can bind :443/:80 directly, or
# needs the SNI-passthrough proxy path instead. Prefers `ss` (modern,
# near-universal on current distros) and falls back to the older `netstat`
# for anything that doesn't have it.
port_in_use() {
local port="$1"
if command -v ss > /dev/null; then
ss -tln | grep -q ":$port "
elif command -v netstat > /dev/null; then
netstat -tln | grep -q ":$port "
else
return 1 # can't tell -- assume free rather than block the install
fi
}
# Step 3b: Direct (non-Docker) path -- downloads the plain server binary
# (linux-x86_64 only, matching release.yml's current scope) and installs
# it as a systemd service using the existing template.
install_direct() {
command -v systemctl > /dev/null || fail "systemd not found -- Direct install needs systemd; use the Docker path instead"
local asset="raba-server-linux-x86_64.tar.gz"
local url="https://github.com/$REPO/releases/latest/download/$asset"
local tmp_dir
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
log "downloading $url"
curl -fsSL "$url" -o "$tmp_dir/$asset" || fail "download failed -- does a release exist yet?"
tar xzf "$tmp_dir/$asset" -C "$tmp_dir"
mkdir -p "$INSTALL_DIR"
mv "$tmp_dir/server" "$INSTALL_DIR/raba-server"
chmod +x "$INSTALL_DIR/raba-server"
# Scope note (see the top-of-file comment): runs as whichever user
# invoked this script, not a dedicated unprivileged system account.
# AmbientCapabilities in the template still means this doesn't need
# to be root to bind :443/:80.
local service_user
service_user="$(whoami)"
local service_file="/etc/systemd/system/raba.service"
fetch_template "raba.service.template" \
| sed \
-e "s|__RABA_USER__|$service_user|g" \
-e "s|__RABA_WORKDIR__|$INSTALL_DIR|g" \
-e "s|__RABA_BINARY_PATH__|$INSTALL_DIR/raba-server|g" \
| sudo tee "$service_file" > /dev/null \
|| fail "couldn't write $service_file -- this needs sudo"
sudo systemctl daemon-reload
sudo systemctl enable --now raba.service
log "raba.service installed and started on :$RABA_HTTPS_PORT/:$RABA_HTTP_PORT -- check status with: systemctl status raba"
}
# Only called when RABA_HTTPS_PORT isn't 443 (collect_config's PROXY_CHOICE
# branch) -- generates the actual filled-in passthrough config for
# whichever proxy was chosen, instead of just pointing at a template file
# and leaving the user to fill it in themselves.
#
# :443 vs :80 are NOT the same problem: the SNI-passthrough trick
# (ssl_preread) this generates for :443 works because a TLS handshake's
# SNI is visible before decryption -- plain :80 traffic has no such thing
# to read, so a shared :80 needs an ordinary proxy_pass block instead, a
# structurally different config this does not generate (a known gap
# already documented in .internal/plan/dynamic-domains-tls.md).
setup_reverse_proxy() {
case "$PROXY_CHOICE" in
caddy | Caddy)
local out="$INSTALL_DIR/raba-caddy.conf"
fetch_template "Caddyfile.template" \
| sed -e "s|__RABA_DOMAIN__|$RABA_DOMAIN|g" -e "s|__RABA_INTERNAL_PORT__|$RABA_HTTPS_PORT|g" \
> "$out"
log "wrote $out for the :443 passthrough -- REQUIRES the third-party layer4 Caddy module"
log "(see the file's own comments). Add its contents to your Caddy config, then reload Caddy."
;;
none | None | "")
log "skipping :443 passthrough setup -- raba stays on :$RABA_HTTPS_PORT until you wire up"
log "a proxy yourself. Templates: install/templates/{nginx.stream.conf.template,Caddyfile.template}"
;;
*)
local out="$INSTALL_DIR/raba-nginx-stream.conf"
fetch_template "nginx.stream.conf.template" \
| sed "s|__RABA_INTERNAL_PORT__|$RABA_HTTPS_PORT|g" \
> "$out"
log "wrote $out for the :443 passthrough. Add (or confirm) an include line inside nginx's"
log "top-level 'stream' block, e.g.: stream { include $out; }"
log "then: nginx -t && systemctl reload nginx"
;;
esac
if [ "$RABA_HTTP_PORT" != "80" ]; then
log "note: the above only covers :443 -- :80 (raba's plain-HTTP redirect, currently on"
log ":$RABA_HTTP_PORT) needs a separate, ordinary proxy_pass block in $PROXY_CHOICE"
log "(SNI-passthrough doesn't apply to plain HTTP, there's no SNI in an unencrypted request)."
log "e.g. for nginx: server { listen 80; location / { proxy_pass http://127.0.0.1:$RABA_HTTP_PORT; } }"
fi
}
# Re-deploys onto an already-configured install without touching .env or
# repeating any prompt -- detects which install method was used
# previously (docker-compose.yml vs the systemd unit, the two are
# mutually exclusive outcomes of the same choice main() asks below) and
# just re-runs that same path. install_direct already re-downloads the
# binary and restarts the service unconditionally, so reusing it here
# for both fresh installs and updates is correct, not just convenient --
# one code path, not two that could drift apart.
update_existing() {
if [ -f "$INSTALL_DIR/docker-compose.yml" ]; then
log "existing Docker install detected -- pulling the latest image..."
(cd "$INSTALL_DIR" && docker compose pull && docker compose up -d)
log "updated -- check status with: docker compose -f $INSTALL_DIR/docker-compose.yml ps"
elif [ -f /etc/systemd/system/raba.service ]; then
log "existing Direct (systemd) install detected -- re-downloading and restarting..."
install_direct
else
fail "found $INSTALL_DIR/.env but neither docker-compose.yml nor a systemd unit -- can't tell which install method was used before. Remove $INSTALL_DIR/.env and re-run to start fresh."
fi
}
main() {
# Idempotency: a second run against an already-configured install
# updates in place by default, instead of silently re-prompting for
# (and overwriting) config that's already correct -- see write_env's
# own comment for why blindly overwriting would be actively harmful,
# not just redundant.
if [ -f "$INSTALL_DIR/.env" ]; then
log "existing installation detected at $INSTALL_DIR"
if prompt_yes_no "Update to the latest version using your existing config?" "y"; then
update_existing
exit 0
fi
log "proceeding with full reconfiguration (JWT_SECRET will still be preserved, see write_env)"
fi
collect_config
write_env
if prompt_yes_no "Install via Docker? (choose no for a Direct/systemd install instead)" "y"; then
install_docker
else
install_direct
fi
if [ -n "$PROXY_CHOICE" ]; then
setup_reverse_proxy
fi
log ""
log "Next: point $RABA_DOMAIN's DNS A/AAAA record at this server's public IP."
if [ "${USE_ACME:-false}" = "true" ]; then
log "TLS certs will be issued/renewed automatically once that DNS record resolves here."
else
log "No automated TLS was configured -- see docker/.env.example's RABA_TLS_CERT_PATH/RABA_TLS_KEY_PATH"
log "for the manual-certificate path, or re-run this script to set up Cloudflare DNS-01 instead."
fi
}
main