#!/bin/sh # LPRA server: install or update in one command. # # wget -qO- https://gitlab.com/itzRedTea/lpra/-/raw/main/server-install.sh | sudo sh # # Takes the server binary for this machine from the latest GitLab 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:-itzRedTea/lpra} GL=https://gitlab.com # The project in the API's paths: its path with "/" as %2F. API="$GL/api/v4/projects/$(printf '%s' "$REPO" | sed 's|/|%2F|g')" 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/null; then TTY=/dev/tty; fi # ask VAR "question" default [KEY]: the answer from the terminal, else the # environment's LPRA_ (KEY is VAR if not given), else the default. ask() { var=$1 q=$2 def=$3 key=${4:-$1} eval "env=\${LPRA_$key:-}" [ -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 KEY "question" y|n: true for a yes (LPRA_ without a terminal). # The answer stays apart from the script's own variables of that name. confirm() { ask _yn "$2 (y/n)" "$3" "$1" case "$_yn" in [yYдД]*) return 0 ;; *) return 1 ;; esac } # ---------- download ---------- TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT INT TERM TAG=$(fetch "$API/releases/permalink/latest" 2>/dev/null | tr ',' '\n' | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1) || TAG= if [ -n "$TAG" ]; then say "последний релиз: $TAG" BIN_URL="$GL/$REPO/-/releases/$TAG/downloads/lpra-server-linux-$ARCH" else warn "не узнать номер релиза у GitLab API - беру latest" BIN_URL="$GL/$REPO/-/releases/permalink/latest/downloads/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" # The files of dist/ on main, by the API's list of the folder. URLS=$(fetch "$API/repository/tree?path=dist&ref=main&per_page=100" 2>/dev/null | tr '{},' '\n\n\n' | sed -n 's|^"path": *"\(dist/[^"]*\)"$|'"$GL/$REPO"'/-/raw/main/\1|p') || URLS= if [ -z "$URLS" ]; then warn "не получить список dist/ у GitLab API - беру агентов linux amd64 и arm64" URLS="$GL/$REPO/-/raw/main/dist/agent-linux-amd64 $GL/$REPO/-/raw/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" < 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" </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:-}:$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"