feat: publish generic Telegram WEB proxy stack
This commit is contained in:
Executable
+257
@@ -0,0 +1,257 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user