Files
telegram-web-proxy-stack/install.sh
T

258 lines
7.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
hostname_value="${TPROXY_HOSTNAME:-}"
public_ip="${MTPROXY_PUBLIC_IP:-}"
npm_network="${NPM_NETWORK_NAME:-}"
assume_yes=false
usage() {
cat <<'USAGE'
Usage: ./install.sh [options]
Options:
--hostname DOMAIN Public proxy hostname
--public-ip IPV4 Public IPv4 address of this Docker host
--npm-network NAME Existing Docker network used by Nginx Proxy Manager
--yes Accept detected defaults without confirmation
-h, --help Show this help
Without options the installer asks only for values it cannot detect safely.
The WEB proxy secret and relay signing key are generated locally and are never
sent to Gitea or written to .env.
USAGE
}
while (($#)); do
case "$1" in
--hostname)
hostname_value="${2:-}"
shift 2
;;
--public-ip)
public_ip="${2:-}"
shift 2
;;
--npm-network)
npm_network="${2:-}"
shift 2
;;
--yes)
assume_yes=true
shift
;;
-h | --help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ "$(uname -s)" != Linux ]] || [[ "$(uname -m)" != x86_64 ]]; then
echo "This installer requires an x86_64 Linux Docker host." >&2
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
echo "Docker Engine with the Compose plugin is required." >&2
exit 1
fi
docker_command=(docker)
if ! docker info >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1 && sudo -n docker info >/dev/null 2>&1; then
docker_command=(sudo docker)
else
echo "The current user cannot access Docker. Run as root or configure Docker access." >&2
exit 1
fi
fi
if ! "${docker_command[@]}" compose version >/dev/null 2>&1; then
echo "The Docker Compose plugin is required." >&2
exit 1
fi
valid_hostname() {
local value=$1 label
[[ ${#value} -le 253 ]] || return 1
[[ "$value" == *.* ]] || return 1
[[ "$value" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || return 1
local IFS=.
read -r -a labels <<<"$value"
for label in "${labels[@]}"; do
[[ -n "$label" && ${#label} -le 63 ]] || return 1
[[ "$label" != -* && "$label" != *- ]] || return 1
done
}
valid_ipv4() {
local ip=$1 octet
local -a octets
local IFS=.
read -r -a octets <<<"$ip"
[[ ${#octets[@]} -eq 4 ]] || return 1
for octet in "${octets[@]}"; do
[[ "$octet" =~ ^[0-9]{1,3}$ ]] || return 1
((10#$octet <= 255)) || return 1
done
}
public_ipv4() {
local ip=$1
local -a octets
local IFS=.
valid_ipv4 "$ip" || return 1
read -r -a octets <<<"$ip"
((10#${octets[0]} != 0)) || return 1
((10#${octets[0]} != 10)) || return 1
((10#${octets[0]} != 127)) || return 1
! ((10#${octets[0]} == 100 && 10#${octets[1]} >= 64 && 10#${octets[1]} <= 127)) || return 1
! ((10#${octets[0]} == 169 && 10#${octets[1]} == 254)) || return 1
! ((10#${octets[0]} == 172 && 10#${octets[1]} >= 16 && 10#${octets[1]} <= 31)) || return 1
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 0 && (10#${octets[2]} == 0 || 10#${octets[2]} == 2))) || return 1
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 88 && 10#${octets[2]} == 99)) || return 1
! ((10#${octets[0]} == 192 && 10#${octets[1]} == 168)) || return 1
! ((10#${octets[0]} == 198 && (10#${octets[1]} == 18 || 10#${octets[1]} == 19))) || return 1
! ((10#${octets[0]} == 198 && 10#${octets[1]} == 51 && 10#${octets[2]} == 100)) || return 1
! ((10#${octets[0]} == 203 && 10#${octets[1]} == 0 && 10#${octets[2]} == 113)) || return 1
((10#${octets[0]} < 224)) || return 1
}
if [[ -z "$hostname_value" ]]; then
if [[ ! -t 0 ]]; then
echo "--hostname is required in non-interactive mode." >&2
exit 2
fi
read -r -p "Public hostname (for example, proxy.example.com): " hostname_value
fi
hostname_value="$(printf '%s' "$hostname_value" | tr '[:upper:]' '[:lower:]')"
if ! valid_hostname "$hostname_value"; then
echo "Invalid hostname: $hostname_value" >&2
exit 2
fi
if [[ -z "$public_ip" ]]; then
if [[ ! -t 0 ]]; then
echo "--public-ip is required in non-interactive mode." >&2
exit 2
fi
read -r -p "Public IPv4 of this Docker host: " public_ip
fi
if ! public_ipv4 "$public_ip"; then
echo "Not a usable public IPv4 address: $public_ip" >&2
exit 2
fi
if [[ -z "$npm_network" ]]; then
npm_container="$("${docker_command[@]}" ps --format '{{.Names}}' |
awk 'tolower($0) ~ /(nginx.*proxy|npm)/ { print; exit }')"
if [[ -n "$npm_container" ]]; then
npm_network="$("${docker_command[@]}" inspect \
--format '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}' \
"$npm_container" | awk '$0 != "bridge" && $0 != "host" && $0 != "none" { print; exit }')"
fi
fi
if [[ -z "$npm_network" ]]; then
for candidate in nginx-proxy-manager_default npm_default; do
if "${docker_command[@]}" network inspect "$candidate" >/dev/null 2>&1; then
npm_network=$candidate
break
fi
done
fi
if [[ -z "$npm_network" ]]; then
if [[ ! -t 0 ]]; then
echo "--npm-network is required when Nginx Proxy Manager cannot be detected." >&2
exit 2
fi
read -r -p "Existing Nginx Proxy Manager Docker network: " npm_network
fi
if ! "${docker_command[@]}" network inspect "$npm_network" >/dev/null 2>&1; then
echo "Docker network does not exist: $npm_network" >&2
exit 2
fi
repository="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
environment_file="$repository/.env"
temporary_environment="$(mktemp "$repository/.env.new.XXXXXX")"
trap 'rm -f "$temporary_environment"' EXIT
cat >"$temporary_environment" <<ENV
TPROXY_HOSTNAME=$hostname_value
MTPROXY_PUBLIC_IP=$public_ip
NPM_NETWORK_NAME=$npm_network
MTPROXY_WORKERS=1
MTPROXY_MAX_CONNECTIONS=4096
ENV
chmod 0600 "$temporary_environment"
mv -f "$temporary_environment" "$environment_file"
trap - EXIT
compose=("${docker_command[@]}" compose --env-file "$environment_file" -f "$repository/compose.yml")
"${compose[@]}" config --quiet
"${compose[@]}" build --pull
"${compose[@]}" up -d --remove-orphans
health=
for _ in $(seq 1 90); do
health="$("${docker_command[@]}" inspect \
--format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \
tg-web-proxy 2>/dev/null || true)"
if [[ "$health" == healthy ]]; then
break
fi
if [[ "$health" == unhealthy || "$health" == exited || "$health" == dead ]]; then
break
fi
sleep 2
done
if [[ "$health" != healthy ]]; then
echo "The proxy did not become healthy (state: ${health:-unknown})." >&2
"${docker_command[@]}" logs --tail 100 tg-web-proxy >&2 || true
exit 1
fi
client_secret="$("${docker_command[@]}" exec tg-web-proxy \
sh -c 'tr -d "\\r\\n" < /data/client-secret')"
if [[ ! "$client_secret" =~ ^[0-9a-f]{32}$ ]]; then
echo "The proxy is healthy, but its client secret could not be read." >&2
exit 1
fi
resolved_ip="$(getent ahostsv4 "$hostname_value" 2>/dev/null | awk 'NR == 1 { print $1 }')"
cat <<RESULT
Telegram WEB proxy is healthy.
Nginx Proxy Manager Proxy Host:
Domain: $hostname_value
Scheme: http
Forward host: tg-web-proxy
Forward port: 8088
WebSockets: enabled
Force SSL: enabled
HTTP/2: enabled
Telegram WEB proxy secret (store it securely):
$client_secret
Public IPv4: $public_ip
Detected DNS IPv4: ${resolved_ip:-not resolved}
The secret remains in the Docker volume tg_webproxy_data and is not stored in .env.
RESULT
if [[ -n "$resolved_ip" && "$resolved_ip" != "$public_ip" ]]; then
echo "WARNING: DNS currently resolves to $resolved_ip, not $public_ip." >&2
fi