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}`.
|
Получаются `bin/lpra-server*` и `dist/agent-linux-{amd64,arm64}`.
|
||||||
Версия задаётся в одном месте - `VERSION.txt`, она вшивается в сервер и агент.
|
Версия задаётся в одном месте - `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
|
```sh
|
||||||
./bin/lpra-server -addr 127.0.0.1:8091 -dist ./dist -db ./lpra.db
|
./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