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:
1 parent
86f7c96807
commit
667c6177f4
2 files changed
+333
No files matched your search
@@ -92,6 +92,29 @@ PostgreSQL: хост, порт, база, пользователь, пароль
|
||||
Получаются `bin/lpra-server*` и `dist/agent-linux-{amd64,arm64}`.
|
||||
Версия задаётся в одном месте - `VERSION.txt`, она вшивается в сервер и агент.
|
||||
|
||||
## Установка сервера одной командой
|
||||
На Linux-сервере (amd64 или arm64, systemd):
|
||||
```sh
|
||||
wget -qO- https://raw.githubusercontent.com/iRedTea/lpra/main/server-install.sh | sudo sh
|
||||
```
|
||||
Скрипт `server-install.sh`:
|
||||
- качает бинарник сервера под архитектуру машины из [последнего релиза](https://github.com/iRedTea/lpra/releases/latest)
|
||||
и папку `dist/` (бинарники агентов) из ветки `main`, кладёт всё в `/opt/lpra`;
|
||||
- ставит службу systemd `lpra-server` (порт спросит, по умолчанию `8091`);
|
||||
- по желанию настраивает nginx: спросит домен, создаст сайт на порт 80 (при необходимости поставит nginx),
|
||||
затем `certbot --nginx` - если certbot с плагином nginx не установлен, этот шаг пропускается;
|
||||
- если включён ufw - предложит открыть 80, 443 и туннель `8446` tcp/udp;
|
||||
- запускает службу, перезагружает nginx и проверяет, что сервер отвечает.
|
||||
|
||||
Вопросы задаются в терминале, даже когда скрипт идёт через `| sudo sh`. Без терминала ответы берутся
|
||||
из переменных: `LPRA_PORT`, `LPRA_NGINX` (y/n), `LPRA_DOMAIN`, `LPRA_EMAIL`, `LPRA_CERTBOT` (y/n), `LPRA_UFW` (y/n):
|
||||
```sh
|
||||
wget -qO- https://raw.githubusercontent.com/iRedTea/lpra/main/server-install.sh | sudo LPRA_NGINX=y LPRA_DOMAIN=lpra.example.com sh
|
||||
```
|
||||
Повторный запуск той же командой - **обновление**: бинарник и `dist/` заменяются, база (`/opt/lpra/lpra.db`
|
||||
и её ключ `lpra.db.key`), настройки службы и nginx остаются. После установки откройте `/setup` - мастер
|
||||
создаст администратора.
|
||||
|
||||
## Запуск сервера
|
||||
```sh
|
||||
./bin/lpra-server -addr 127.0.0.1:8091 -dist ./dist -db ./lpra.db
|
||||
|
||||
Executable
+310
@@ -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"
|
||||
Reference in new issue
Block a user