diff --git a/install-shared-vps.sh b/install-shared-vps.sh new file mode 100644 index 0000000..b5da3b4 --- /dev/null +++ b/install-shared-vps.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +# Telegram WEB Proxy + WEB PANEL PROXY for a VPS where Caddy already runs +# inside Docker. The existing Caddyfile is preserved and extended with one +# additional hostname; no second public web server is installed. + +readonly INSTALLER_VERSION="1.0.0" +readonly PANEL_VERSION="v2.0.2" +readonly PANEL_ARCHIVE_URL="https://github.com/POLESNIESOVETI12/web-panel-proxy/archive/refs/tags/${PANEL_VERSION}.zip" +readonly PANEL_ARCHIVE_SHA256="ac2258323b71bde13eb45314a0a70449561774697c5e4d3603791e459c55e7bb" +readonly DEFAULT_ASSET_BASE_URL="https://git.chardb.ru/AlexChard/telegram-web-proxy/raw/branch/main/site" +readonly SITE_INDEX_SHA256="fd65d104b54a3c79c74cbc5a73dfd4e1504a01dffc2d5b453aa724c4b0445924" +readonly SITE_CSS_SHA256="511759e08f0ade1f1e96d46ca2a938a2d9ec15a40111c93a3df4c17be6ef5688" +readonly SITE_FAVICON_SHA256="b70149593f184d3acec15b23fa5f4226cb90782b87092b739977b8d7d67ede03" +readonly RELAY_BRIDGE_PORT="18080" +readonly PANEL_BRIDGE_PORT="18090" + +DOMAIN="gg.chardwork.ru" +EMAIL="" +CADDY_CONTAINER="adguard-caddy" +CADDY_STACK_DIR="/opt/stacks/adguard-dns" +ASSET_BASE_URL="${ASSET_BASE_URL:-$DEFAULT_ASSET_BASE_URL}" +WORK_DIR="" +BACKUP_DIR="" +CADDYFILE="" +DOH_URL="https://dns.chardwork.ru/dns-query?dns=AAABAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE" +DOH_BASELINE="" +INSTALL_COMPLETE=0 + +say() { printf '\n==> %s\n' "$*"; } +die() { printf '\nERROR: %s\n' "$*" >&2; exit 1; } + +usage() { + cat <<'EOF' +Usage: + sudo bash install-shared-vps.sh --domain gg.example.com --email admin@example.com + +Options: + --domain HOSTNAME Public WEB Proxy hostname. + --email ADDRESS Email used by Caddy/ACME. + --caddy-container NAME Existing Caddy container (default: adguard-caddy). + --caddy-stack DIR Existing Caddy stack directory. + --asset-base-url URL Alternate base URL for the static site files. +EOF +} + +valid_domain() { + [[ "$1" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] && + [[ "$1" == *.* ]] && [[ "$1" != *..* ]] +} + +valid_email() { + [[ "$1" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] +} + +cleanup() { + [[ -z "$WORK_DIR" || ! -d "$WORK_DIR" ]] || rm -rf -- "$WORK_DIR" +} + +rollback_caddy() { + local code=$? + trap - ERR + if [[ "$INSTALL_COMPLETE" != 1 && -n "$BACKUP_DIR" && -s "$BACKUP_DIR/Caddyfile" ]]; then + printf '\nInstallation stopped; restoring the previous Caddyfile...\n' >&2 + cp -a "$BACKUP_DIR/Caddyfile" "$CADDYFILE" + docker exec "$CADDY_CONTAINER" caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1 || true + docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1 || true + fi + cleanup + exit "$code" +} + +trap cleanup EXIT +trap rollback_caddy ERR + +while [[ $# -gt 0 ]]; do + case "$1" in + --domain) DOMAIN="${2:-}"; shift 2 ;; + --email) EMAIL="${2:-}"; shift 2 ;; + --caddy-container) CADDY_CONTAINER="${2:-}"; shift 2 ;; + --caddy-stack) CADDY_STACK_DIR="${2:-}"; shift 2 ;; + --asset-base-url) ASSET_BASE_URL="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "Unknown option: $1" ;; + esac +done + +[[ "$EUID" -eq 0 ]] || die "Run as root." +[[ "$(uname -m)" == "x86_64" ]] || die "An x86_64 VPS is required." +valid_domain "$DOMAIN" || die "Invalid domain: $DOMAIN" + +if [[ -z "$EMAIL" ]]; then + read -r -p "ACME email: " EMAIL /dev/null 2>&1 || die "Docker is required." +docker inspect "$CADDY_CONTAINER" >/dev/null 2>&1 || die "Caddy container '$CADDY_CONTAINER' was not found." +[[ "$(docker inspect -f '{{.State.Running}}' "$CADDY_CONTAINER")" == true ]] || die "Caddy container is not running." + +CADDYFILE="${CADDY_STACK_DIR}/Caddyfile" +[[ -s "$CADDYFILE" ]] || die "Existing Caddyfile was not found: $CADDYFILE" +[[ -s "${CADDY_STACK_DIR}/compose.yaml" ]] || die "Existing compose.yaml was not found: ${CADDY_STACK_DIR}/compose.yaml" + +# Choose the gateway of the Docker network that contains the running Caddy. +CADDY_GATEWAY="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{if .Gateway}}{{println .Gateway}}{{end}}{{end}}' "$CADDY_CONTAINER" | awk 'NF{print; exit}')" +[[ "$CADDY_GATEWAY" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || die "Could not determine the Caddy Docker gateway." + +for port in "$RELAY_BRIDGE_PORT" "$PANEL_BRIDGE_PORT" 2398 8080 8081 8090; do + if ss -lnt | grep -Eq "[:.]${port}\\b"; then + die "Required local port ${port} is already in use." + fi +done + +say "Checking DNS and the existing DoH service" +DNS_IP="$(getent ahostsv4 "$DOMAIN" | awk 'NR==1{print $1}')" +[[ -n "$DNS_IP" ]] || die "No IPv4 A record found for $DOMAIN." +VPS_IP="$(curl -4fsS --max-time 10 https://api.ipify.org)" +[[ "$DNS_IP" == "$VPS_IP" ]] || die "$DOMAIN resolves to $DNS_IP, but this VPS is $VPS_IP." +DOH_BASELINE="$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 8 "$DOH_URL" || true)" +[[ "$DOH_BASELINE" == 200 ]] || die "Existing DoH check failed before installation (HTTP ${DOH_BASELINE:-error})." + +say "Creating a recoverable backup" +BACKUP_DIR="/opt/backups/telegram-web-proxy/$(date -u +%Y%m%dT%H%M%SZ)" +install -d -m 0700 "$BACKUP_DIR" +cp -a "$CADDYFILE" "$BACKUP_DIR/Caddyfile" +cp -a "${CADDY_STACK_DIR}/compose.yaml" "$BACKUP_DIR/compose.yaml" +docker inspect "$CADDY_CONTAINER" > "$BACKUP_DIR/caddy-container-inspect.json" +chmod 0600 "$BACKUP_DIR"/* + +say "Installing verified dependencies" +export DEBIAN_FRONTEND=noninteractive +apt-get -o DPkg::Lock::Timeout=600 update +apt-get -o DPkg::Lock::Timeout=600 install -y --no-install-recommends \ + ca-certificates curl unzip socat python3 openssl git dnsutils nftables \ + build-essential libssl-dev util-linux zlib1g-dev + +WORK_DIR="$(mktemp -d /tmp/gg-web-proxy.XXXXXXXX)" +ARCHIVE="$WORK_DIR/web-panel-proxy.zip" +curl --fail --silent --show-error --location \ + --proto '=https' --proto-redir '=https' --tlsv1.2 \ + --output "$ARCHIVE" "$PANEL_ARCHIVE_URL" +[[ "$(sha256sum "$ARCHIVE" | awk '{print $1}')" == "$PANEL_ARCHIVE_SHA256" ]] || + die "WEB PANEL PROXY archive checksum mismatch." +unzip -q "$ARCHIVE" -d "$WORK_DIR/source" +SOURCE_DIR="$WORK_DIR/source/web-panel-proxy-${PANEL_VERSION#v}" +for file in install-final.sh install-webproxy-core.sh install-panel.sh uninstall-web-proxy.sh; do + [[ -s "$SOURCE_DIR/$file" ]] || die "Installation package is incomplete: $file" +done + +say "Preparing the Docker-Caddy compatibility layer" +install -d -m 0755 /etc/caddy /usr/local/lib/telegram-web-proxy +if [[ -e /etc/caddy/Caddyfile && ! -L /etc/caddy/Caddyfile ]]; then + die "/etc/caddy/Caddyfile already exists and is not a managed symlink." +fi +ln -sfn "$CADDYFILE" /etc/caddy/Caddyfile + +cat > /usr/local/bin/caddy </dev/null || true) == true ]]; then + break + fi + sleep 1 +done +[[ \$(/usr/bin/docker inspect -f '{{.State.Running}}' ${CADDY_CONTAINER} 2>/dev/null || true) == true ]] || { + echo "Caddy container ${CADDY_CONTAINER} is not running." >&2 + exit 1 +} +exec /usr/bin/docker exec ${CADDY_CONTAINER} caddy "\$@" +EOF +chmod 0755 /usr/local/bin/caddy + +cat > /etc/systemd/system/caddy.service < /etc/systemd/system/tproxy-caddy-relay-bridge.service < /etc/systemd/system/tproxy-caddy-panel-bridge.service < /etc/systemd/system/caddy.service.d/tproxy.conf < /etc/systemd/system/tproxy-panel.service.d/resources.conf <<'EOF' +[Service] +MemoryMax=128M +CPUQuota=25% +EOF +cat > /etc/systemd/system/tproxy-server.service.d/resources.conf <<'EOF' +[Service] +MemoryMax=256M +CPUQuota=100% +EOF +cat > /etc/systemd/system/mtproxy.service.d/resources.conf <<'EOF' +[Service] +MemoryMax=256M +CPUQuota=100% +EOF +systemctl daemon-reload +systemctl restart tproxy-panel.service tproxy-server.service mtproxy.service +systemctl reload caddy.service + +say "Final checks" +docker exec "$CADDY_CONTAINER" caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile +[[ "$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 10 "https://${DOMAIN}/")" == 200 ]] || die "Public site check failed." +[[ "$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 8 "$DOH_URL")" == 200 ]] || die "Existing DoH check failed after installation." +[[ "$(curl -fsS --max-time 5 http://127.0.0.1:8081/healthz)" == ok ]] || die "Relay health check failed." +[[ "$(curl -fsS --max-time 5 http://127.0.0.1:8081/readyz)" == ready ]] || die "Relay readiness check failed." + +PANEL_PATH="$(sed -n 's/^Environment=WEBPROXY_PANEL_PATH=//p' /etc/systemd/system/tproxy-panel.service | head -n1)" +PRIMARY_SECRET="$(cat /etc/web-proxy-panel/primary-secret)" +cat > /root/telegram-web-proxy-access.txt <