358 lines
14 KiB
Bash
358 lines
14 KiB
Bash
#!/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/tty
|
|
fi
|
|
valid_email "$EMAIL" || die "Invalid email: $EMAIL"
|
|
|
|
command -v docker >/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 <<EOF
|
|
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
for _ in \$(seq 1 60); do
|
|
if [[ \$(/usr/bin/docker inspect -f '{{.State.Running}}' ${CADDY_CONTAINER} 2>/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 <<EOF
|
|
[Unit]
|
|
Description=Caddy compatibility service for ${CADDY_CONTAINER}
|
|
Requires=docker.service
|
|
After=docker.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStart=/usr/local/bin/caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
ExecStart=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
ExecStop=/bin/true
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
cat > /etc/systemd/system/tproxy-caddy-relay-bridge.service <<EOF
|
|
[Unit]
|
|
Description=Private bridge from Docker Caddy to Telegram WEB Proxy relay
|
|
Requires=docker.service
|
|
After=docker.service network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/socat TCP-LISTEN:${RELAY_BRIDGE_PORT},bind=${CADDY_GATEWAY},fork,reuseaddr TCP:127.0.0.1:8080
|
|
Restart=always
|
|
RestartSec=2
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
cat > /etc/systemd/system/tproxy-caddy-panel-bridge.service <<EOF
|
|
[Unit]
|
|
Description=Private bridge from Docker Caddy to Telegram WEB Proxy panel
|
|
Requires=docker.service
|
|
After=docker.service network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/socat TCP-LISTEN:${PANEL_BRIDGE_PORT},bind=${CADDY_GATEWAY},fork,reuseaddr TCP:127.0.0.1:8090
|
|
Restart=always
|
|
RestartSec=2
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
install -d -m 0755 /etc/systemd/system/caddy.service.d
|
|
cat > /etc/systemd/system/caddy.service.d/tproxy.conf <<EOF
|
|
[Service]
|
|
Environment=TPROXY_HOSTNAME=${DOMAIN}
|
|
Environment=TPROXY_SITE_ROOT=/srv/tproxy-site
|
|
Environment=ACME_EMAIL=${EMAIL}
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now caddy.service tproxy-caddy-relay-bridge.service tproxy-caddy-panel-bridge.service
|
|
|
|
say "Adapting the pinned installer for the existing Docker Caddy"
|
|
python3 - "$SOURCE_DIR/install-webproxy-core.sh" "$SOURCE_DIR/install-panel.sh" "$CADDY_GATEWAY" "$RELAY_BRIDGE_PORT" "$PANEL_BRIDGE_PORT" <<'PY'
|
|
import re
|
|
import sys
|
|
|
|
core_path, panel_path, gateway, relay_port, panel_port = sys.argv[1:]
|
|
relay = f"{gateway}:{relay_port}"
|
|
panel = f"{gateway}:{panel_port}"
|
|
|
|
core = open(core_path, encoding="utf-8").read()
|
|
for old, new in (
|
|
("check_install_port 80 caddy", "check_install_port 80 docker-proxy"),
|
|
("check_install_port 443 caddy", "check_install_port 443 docker-proxy"),
|
|
):
|
|
if core.count(old) != 1:
|
|
raise SystemExit(f"Unexpected upstream core layout: {old}")
|
|
core = core.replace(old, new)
|
|
|
|
needle = ' "$CANONICAL_CADDY"\n\nif [[ "$REUSE_CADDY" == "1" ]]; then'
|
|
replacement = (
|
|
' "$CANONICAL_CADDY"\n'
|
|
f"sed -i 's#reverse_proxy 127.0.0.1:8080#reverse_proxy {relay}#g' \"$CANONICAL_CADDY\"\n\n"
|
|
'if [[ "$REUSE_CADDY" == "1" ]]; then'
|
|
)
|
|
if core.count(needle) != 1:
|
|
raise SystemExit("Could not locate the canonical Caddy patch point")
|
|
core = core.replace(needle, 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"
|
|
printf '\nThe panel installer will now ask only for its login and password.\n'
|
|
bash "$SOURCE_DIR/install-final.sh" </dev/tty
|
|
|
|
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"
|
|
curl --fail --silent --show-error --location \
|
|
--proto '=https' --proto-redir '=https' --tlsv1.2 \
|
|
--output "/srv/tproxy-site/${name}.new" "${ASSET_BASE_URL}/${name}"
|
|
[[ "$(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 <<EOF
|
|
Domain: ${DOMAIN}
|
|
Panel: https://${DOMAIN}${PANEL_PATH}/login
|
|
Primary secret: ${PRIMARY_SECRET}
|
|
Telegram link: tg://webproxy?server=${DOMAIN}&port=443&secret=${PRIMARY_SECRET}
|
|
Public site: https://${DOMAIN}/
|
|
Backup created before installation: ${BACKUP_DIR}
|
|
EOF
|
|
chmod 0600 /root/telegram-web-proxy-access.txt
|
|
|
|
INSTALL_COMPLETE=1
|
|
trap - ERR
|
|
cleanup
|
|
printf '\n============================================================\n'
|
|
printf ' Telegram WEB Proxy is ready\n'
|
|
printf '============================================================\n'
|
|
printf ' Site: https://%s/\n' "$DOMAIN"
|
|
printf ' Panel: https://%s%s/login\n' "$DOMAIN" "$PANEL_PATH"
|
|
printf ' Link: tg://webproxy?server=%s&port=443&secret=%s\n' "$DOMAIN" "$PRIMARY_SECRET"
|
|
printf '\nA private copy is saved in /root/telegram-web-proxy-access.txt\n'
|