feat: server-install.sh - install or update the server in one command: the latest release's binary, dist/ from main, the systemd service, nginx with a domain and certbot if wanted, a health check

This commit is contained in:
redtea committed 2026-10-16 20:47:26 +03:00
1 parent 86f7c96807
commit 667c6177f4
2 files changed
+333

No files matched your search

+310
View File
@@ -0,0 +1,310 @@
#!/bin/sh
# LPRA server: install or update in one command.
#
# wget -qO- https://raw.githubusercontent.com/iRedTea/lpra/main/server-install.sh | sudo sh
#
# Takes the server binary for this machine from the latest GitHub release and
# the agent binaries (dist/) from the main branch, puts them in /opt/lpra,
# sets up the systemd service and, if asked, nginx with a domain and a
# certificate from certbot. Run again to update: the binary and dist/ are
# replaced, the database, the service's settings and nginx stay.
#
# Questions are asked on the terminal even when the script comes through a
# pipe. Without a terminal the answers come from the environment (or their
# defaults): LPRA_PORT (8091), LPRA_NGINX (y/n), LPRA_DOMAIN, LPRA_EMAIL,
# LPRA_CERTBOT (y/n), LPRA_UFW (y/n). LPRA_REPO and LPRA_DIR change where from
# and where to.
set -eu
REPO=${LPRA_REPO:-iRedTea/lpra}
DIR=${LPRA_DIR:-/opt/lpra}
SERVICE=lpra-server
UNIT=/etc/systemd/system/$SERVICE.service
TUNNEL_PORT=8446
say() { printf '\033[1;34m>>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
# ---------- checks ----------
[ "$(id -u)" -eq 0 ] || die "нужен root: ... | sudo sh"
[ "$(uname -s)" = Linux ] || die "сервер ставится только на Linux"
command -v systemctl >/dev/null 2>&1 || die "нужен systemd"
case "$(uname -m)" in
x86_64 | amd64) ARCH=amd64 ;;
aarch64 | arm64) ARCH=arm64 ;;
*) die "архитектура $(uname -m) не поддерживается (есть amd64 и arm64)" ;;
esac
if command -v curl >/dev/null 2>&1; then
dl() { curl -fsSL --retry 3 -o "$2" "$1"; }
fetch() { curl -fsSL --retry 3 "$1"; }
elif command -v wget >/dev/null 2>&1; then
dl() { wget -q -O "$2" "$1"; }
fetch() { wget -q -O - "$1"; }
else
die "нужен curl или wget"
fi
# ---------- questions ----------
TTY=
if (: </dev/tty) 2>/dev/null; then TTY=/dev/tty; fi
# ask VAR "question" default: the answer from the terminal, else the
# environment's LPRA_<VAR>, else the default.
ask() {
var=$1 q=$2 def=$3
eval "env=\${LPRA_$var:-}"
[ -n "$env" ] && def=$env
ans=
if [ -n "$TTY" ]; then
if [ -n "$def" ]; then printf '%s [%s]: ' "$q" "$def" >"$TTY"; else printf '%s: ' "$q" >"$TTY"; fi
read -r ans <"$TTY" || ans=
fi
[ -n "$ans" ] || ans=$def
eval "$var=\$ans"
}
# confirm VAR "question" y|n: true for a yes.
confirm() {
ask "$1" "$2 (y/n)" "$3"
eval "v=\$$1"
case "$v" in [yYдД]*) return 0 ;; *) return 1 ;; esac
}
# ---------- download ----------
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT INT TERM
TAG=$(fetch "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null |
sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1) || TAG=
if [ -n "$TAG" ]; then
say "последний релиз: $TAG"
BIN_URL="https://github.com/$REPO/releases/download/$TAG/lpra-server-linux-$ARCH"
else
warn "не узнать номер релиза у GitHub API - беру latest"
BIN_URL="https://github.com/$REPO/releases/latest/download/lpra-server-linux-$ARCH"
fi
say "скачиваю сервер (linux/$ARCH)"
dl "$BIN_URL" "$TMP/lpra-server" || die "не скачался $BIN_URL"
chmod +x "$TMP/lpra-server"
say "скачиваю dist/ из ветки main"
mkdir -p "$TMP/dist"
URLS=$(fetch "https://api.github.com/repos/$REPO/contents/dist?ref=main" 2>/dev/null |
sed -n 's/.*"download_url": *"\([^"]*\)".*/\1/p') || URLS=
if [ -z "$URLS" ]; then
warn "не получить список dist/ у GitHub API - беру агентов linux amd64 и arm64"
URLS="https://raw.githubusercontent.com/$REPO/main/dist/agent-linux-amd64
https://raw.githubusercontent.com/$REPO/main/dist/agent-linux-arm64"
fi
for u in $URLS; do
f=${u##*/}
dl "$u" "$TMP/dist/$f" || die "не скачался $u"
case "$f" in agent-*) chmod +x "$TMP/dist/$f" ;; esac
printf ' %s\n' "$f"
done
# ---------- install ----------
UPDATE=
[ -f "$UNIT" ] && UPDATE=1
if [ -n "$UPDATE" ]; then
say "уже установлен - обновляю"
systemctl stop "$SERVICE" 2>/dev/null || true
fi
mkdir -p "$DIR"
mv -f "$TMP/lpra-server" "$DIR/lpra-server"
rm -rf "$DIR/dist.old"
[ -d "$DIR/dist" ] && mv "$DIR/dist" "$DIR/dist.old"
mv "$TMP/dist" "$DIR/dist"
rm -rf "$DIR/dist.old"
say "файлы в $DIR"
# The port: from the existing service on an update, asked otherwise.
PORT=
if [ -n "$UPDATE" ]; then
PORT=$(sed -n 's/.*-addr [^ ]*:\([0-9][0-9]*\).*/\1/p' "$UNIT" | head -n 1)
fi
if [ -z "$PORT" ]; then
ask PORT "порт сервера" 8091
case "$PORT" in '' | *[!0-9]*) die "порт - это число" ;; esac
fi
# ---------- nginx ----------
NGINX= DOMAIN= TLS=
if [ -n "$UPDATE" ]; then NGDEF=n; else NGDEF=y; fi
if confirm NGINX "настроить nginx с доменом" "$NGDEF"; then
if ! command -v nginx >/dev/null 2>&1; then
say "ставлю nginx"
if command -v apt-get >/dev/null 2>&1; then
{ apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nginx; } >/dev/null 2>&1
elif command -v dnf >/dev/null 2>&1; then
dnf install -y -q nginx >/dev/null 2>&1
elif command -v yum >/dev/null 2>&1; then
yum install -y -q nginx >/dev/null 2>&1
fi
fi
if command -v nginx >/dev/null 2>&1; then
ask DOMAIN "домен (например lpra.example.com)" ""
[ -n "$DOMAIN" ] || die "без домена nginx не настроить"
NGINX=1
if [ -d /etc/nginx/sites-available ]; then
CONF=/etc/nginx/sites-available/lpra
LINK=/etc/nginx/sites-enabled/lpra
else
CONF=/etc/nginx/conf.d/lpra.conf
LINK=
fi
cat >"$CONF" <<EOF
# LPRA (server-install.sh): $DOMAIN -> 127.0.0.1:$PORT
# Agents and terminals use WebSocket (the Upgrade headers) and long polling
# (no buffering, long timeouts).
map \$http_upgrade \$lpra_connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name $DOMAIN;
client_max_body_size 120m;
location / {
proxy_pass http://127.0.0.1:$PORT;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$lpra_connection_upgrade;
proxy_buffering off;
proxy_cache off;
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
EOF
[ -n "$LINK" ] && ln -sf "$CONF" "$LINK"
if ! nginx -t >/dev/null 2>&1; then nginx -t; die "nginx -t не прошёл, конфиг: $CONF"; fi
systemctl enable --now nginx >/dev/null 2>&1 || true
systemctl reload nginx
say "nginx: $DOMAIN -> 127.0.0.1:$PORT ($CONF)"
# A certificate, when certbot with its nginx plugin is there.
if command -v certbot >/dev/null 2>&1 && certbot plugins 2>/dev/null | grep -q nginx; then
if confirm CERTBOT "получить сертификат Let's Encrypt (certbot --nginx)" y; then
ask EMAIL "email для Let's Encrypt (можно пусто)" ""
if [ -n "$EMAIL" ]; then M="-m $EMAIL"; else M="--register-unsafely-without-email"; fi
# shellcheck disable=SC2086
if certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos --redirect $M; then
TLS=1
else
warn "certbot не смог - остаётся http. Домен должен указывать на этот сервер, порт 80 открыт"
fi
fi
else
warn "certbot с плагином nginx не найден - сертификат пропускаю (apt install certbot python3-certbot-nginx)"
fi
else
warn "nginx не установлен и не поставился - пропускаю"
fi
fi
# Not set up now, but by an earlier run: its domain, and https if certbot
# added it.
if [ -z "$NGINX" ]; then
for c in /etc/nginx/sites-available/lpra /etc/nginx/conf.d/lpra.conf; do
[ -f "$c" ] || continue
DOMAIN=$(sed -n 's/^[[:space:]]*server_name[[:space:]]\{1,\}\([^;[:space:]]*\).*/\1/p' "$c" | head -n 1)
grep -q 'listen.*443' "$c" && TLS=1
break
done
fi
# ---------- firewall ----------
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
if confirm UFW "ufw включён: открыть 80, 443 и туннель $TUNNEL_PORT tcp/udp" y; then
ufw allow 80/tcp >/dev/null
ufw allow 443/tcp >/dev/null
ufw allow $TUNNEL_PORT/tcp >/dev/null
ufw allow $TUNNEL_PORT/udp >/dev/null
if [ -z "$DOMAIN" ]; then ufw allow "$PORT/tcp" >/dev/null; fi
say "ufw: открыто"
fi
fi
# ---------- service ----------
# Behind nginx: local only. Without it the panel listens on every address.
# Cookies are Secure only when the panel is reached by https.
if [ -z "$UPDATE" ] || [ -n "$NGINX" ]; then
if [ -n "$NGINX" ]; then ADDR=127.0.0.1:$PORT; else ADDR=0.0.0.0:$PORT; fi
COOKIES=
[ -z "$TLS" ] && COOKIES='Environment=LPRA_INSECURE_COOKIES=1'
cat >"$UNIT" <<EOF
# LPRA server (server-install.sh). Its database and key: $DIR/lpra.db, $DIR/lpra.db.key
[Unit]
Description=LPRA remote access host (admin panel + agent rendezvous)
After=network-online.target
Wants=network-online.target
[Service]
WorkingDirectory=$DIR
ExecStart=$DIR/lpra-server -addr $ADDR -db $DIR/lpra.db -dist $DIR/dist
$COOKIES
Restart=always
RestartSec=3
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
# The database, and what ufw writes when the panel opens forwarded ports.
ReadWritePaths=$DIR -/etc/ufw -/run
[Install]
WantedBy=multi-user.target
EOF
fi
systemctl daemon-reload
systemctl enable "$SERVICE" >/dev/null 2>&1
systemctl restart "$SERVICE"
say "служба $SERVICE запущена"
# ---------- health check ----------
ok=
i=0
while [ $i -lt 20 ]; do
if fetch "http://127.0.0.1:$PORT/login" >/dev/null 2>&1; then ok=1; break; fi
i=$((i + 1))
sleep 1
done
[ -n "$ok" ] || die "сервер не отвечает на 127.0.0.1:$PORT - смотрите: journalctl -u $SERVICE -n 50"
say "сервер отвечает"
if [ -n "$NGINX" ]; then
if fetch "http://127.0.0.1/login" >/dev/null 2>&1 || [ -n "$TLS" ]; then say "nginx отвечает"; fi
fi
# ---------- done ----------
if [ -n "$TLS" ]; then URL=https://$DOMAIN
elif [ -n "$DOMAIN" ]; then URL=http://$DOMAIN
else
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
URL="http://${IP:-<IP сервера>}:$PORT"
fi
echo
say "готово: LPRA ${TAG:-} в $DIR"
if [ -z "$UPDATE" ]; then
echo " откройте $URL/setup - мастер создаст администратора"
else
echo " панель: $URL"
fi
if [ -z "$TLS" ]; then echo " панель без https: куки без Secure. Для работы через интернет лучше домен с сертификатом"; fi
echo " логи: journalctl -u $SERVICE -f"