Files
lpra/server-install.sh
T

311 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"