#!/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}" SITE_SOURCE_DIR="${SITE_SOURCE_DIR:-}" 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 PANEL_ADMIN="admin" PANEL_PASSWORD="$(openssl rand -base64 24 | tr -d '\n' | tr '/+' '_-')" [[ ${#PANEL_PASSWORD} -ge 20 ]] || die "Could not generate the panel password." 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/web-proxy-panel/install-credentials chmod 0600 /etc/web-proxy-panel/install-credentials unset PANEL_PASS fi''' credentials_replacement = '''else PANEL_ADMIN="${WEBPROXY_PANEL_ADMIN:-admin}" PANEL_PASS="${WEBPROXY_PANEL_PASSWORD:?WEBPROXY_PANEL_PASSWORD is required}" [[ ${#PANEL_PASS} -ge 12 ]] || die "Generated panel password is too short." printf '%s\\n%s\\n' "$PANEL_ADMIN" "$PANEL_PASS" > /etc/web-proxy-panel/install-credentials chmod 0600 /etc/web-proxy-panel/install-credentials unset PANEL_PASS echo " Generated panel credentials received securely." fi''' if core.count(credentials_block) != 1: raise SystemExit("Could not locate the panel credentials block") core = core.replace(credentials_block, credentials_replacement) open(core_path, "w", encoding="utf-8").write(core) panel_src = open(panel_path, encoding="utf-8").read() old_route = '" reverse_proxy 127.0.0.1:8090\\n"' new_route = f'" reverse_proxy {panel}\\n"' if panel_src.count(old_route) != 1: raise SystemExit("Could not locate the panel Caddy route") panel_src = panel_src.replace(old_route, new_route) old_match = r"r'(?m)^\s*reverse_proxy 127\.0\.0\.1:8080\s*\{'" escaped = re.escape(relay) new_match = "r'(?m)^\\s*reverse_proxy " + escaped + r"\s*\{'" if panel_src.count(old_match) != 1: raise SystemExit("Could not locate the relay Caddy matcher") panel_src = panel_src.replace(old_match, new_match) open(panel_path, "w", encoding="utf-8").write(panel_src) PY chmod 0700 "$SOURCE_DIR"/*.sh say "Installing the proxy and its private management panel" WEBPROXY_PANEL_ADMIN="$PANEL_ADMIN" \ WEBPROXY_PANEL_PASSWORD="$PANEL_PASSWORD" \ bash "$SOURCE_DIR/install-final.sh" say "Installing the neutral Gradient Garden site" install -d -o root -g tproxy -m 0750 /srv/tproxy-site download_site_file() { local name="$1" expected="$2" if [[ -n "$SITE_SOURCE_DIR" ]]; then [[ -f "$SITE_SOURCE_DIR/$name" ]] || die "Local site file is missing: $SITE_SOURCE_DIR/$name" cp "$SITE_SOURCE_DIR/$name" "/srv/tproxy-site/${name}.new" else curl --fail --silent --show-error --location \ --proto '=https' --proto-redir '=https' --tlsv1.2 \ --output "/srv/tproxy-site/${name}.new" "${ASSET_BASE_URL}/${name}" fi [[ "$(sha256sum "/srv/tproxy-site/${name}.new" | awk '{print $1}')" == "$expected" ]] || die "Site file checksum mismatch: $name" mv "/srv/tproxy-site/${name}.new" "/srv/tproxy-site/${name}" } download_site_file index.html "$SITE_INDEX_SHA256" download_site_file styles.css "$SITE_CSS_SHA256" download_site_file favicon.svg "$SITE_FAVICON_SHA256" chown root:tproxy /srv/tproxy-site/index.html /srv/tproxy-site/styles.css /srv/tproxy-site/favicon.svg chmod 0640 /srv/tproxy-site/index.html /srv/tproxy-site/styles.css /srv/tproxy-site/favicon.svg install -d -m 0755 /etc/systemd/system/tproxy-panel.service.d /etc/systemd/system/tproxy-server.service.d /etc/systemd/system/mtproxy.service.d cat > /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 <