315 lines
11 KiB
Bash
Executable File
315 lines
11 KiB
Bash
Executable File
#!/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/tty) 2>/dev/null; then TTY=/dev/tty; fi
|
||
|
||
# ask VAR "question" default [KEY]: the answer from the terminal, else the
|
||
# environment's LPRA_<KEY> (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_<KEY> 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" <<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"
|