268 lines
9.9 KiB
Bash
Executable file
268 lines
9.9 KiB
Bash
Executable file
#!/bin/sh
|
|
set -eu
|
|
umask 077
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: ./scripts/new-docker-env.sh --advertise-ip IP [options]
|
|
|
|
Options:
|
|
--public-base-url URL
|
|
--public-web-base-url URL
|
|
--admin-bind-ip IP
|
|
--host-network Bind the monolith's media sockets directly on the host (default).
|
|
--bridge-network Publish a bounded TURN relay range through Docker.
|
|
--allow-insecure-development-auth
|
|
--output PATH
|
|
--help
|
|
EOF
|
|
}
|
|
|
|
die() {
|
|
printf 'new-docker-env: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
validate_http_url() {
|
|
name=$1
|
|
value=$2
|
|
case "$value" in
|
|
*[[:space:]]*) die "$name must not contain whitespace" ;;
|
|
esac
|
|
case "$value" in
|
|
http://*|https://*) ;;
|
|
*) die "$name must be an absolute HTTP(S) URL" ;;
|
|
esac
|
|
authority=${value#*://}
|
|
authority=${authority%%/*}
|
|
authority=${authority%%\?*}
|
|
authority=${authority%%\#*}
|
|
[ -n "$authority" ] || die "$name must include a host"
|
|
case "$authority" in
|
|
*@*) die "$name must not contain embedded credentials" ;;
|
|
esac
|
|
}
|
|
|
|
url_host() {
|
|
value=$1
|
|
authority=${value#*://}
|
|
authority=${authority%%/*}
|
|
authority=${authority%%\?*}
|
|
authority=${authority%%\#*}
|
|
case "$authority" in
|
|
\[*\]*) host=${authority#\[}; host=${host%%\]*} ;;
|
|
*) host=${authority%%:*} ;;
|
|
esac
|
|
printf '%s\n' "$host"
|
|
}
|
|
|
|
validate_ipv4() {
|
|
awk -F. '
|
|
NF != 4 { exit 1 }
|
|
{ for (i = 1; i <= 4; i++) if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1 }
|
|
' <<EOF
|
|
$1
|
|
EOF
|
|
}
|
|
|
|
validate_ipv6() {
|
|
case "$1" in *[!0-9A-Fa-f:.]*|'') return 1 ;; esac
|
|
if command -v perl >/dev/null 2>&1; then
|
|
perl -MSocket=AF_INET6,inet_pton -e 'exit inet_pton(AF_INET6, $ARGV[0]) ? 0 : 1' "$1"
|
|
return
|
|
fi
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
python3 -c 'import ipaddress,sys; ipaddress.IPv6Address(sys.argv[1])' "$1"
|
|
return
|
|
fi
|
|
die "validating an IPv6 address requires perl or python3"
|
|
}
|
|
|
|
random_hex() {
|
|
openssl rand -hex "$1"
|
|
}
|
|
|
|
script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
|
|
repo_root=$(CDPATH='' cd -- "$script_dir/.." && pwd)
|
|
template_path="$repo_root/deploy/docker/.env.example"
|
|
output_path="$repo_root/deploy/docker/.env"
|
|
|
|
advertise_ip=
|
|
public_base_url=
|
|
public_web_base_url=
|
|
admin_bind_ip=127.0.0.1
|
|
server_host_network=true
|
|
allow_insecure=false
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--advertise-ip)
|
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
|
advertise_ip=$2
|
|
shift 2
|
|
;;
|
|
--public-base-url)
|
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
|
public_base_url=$2
|
|
shift 2
|
|
;;
|
|
--public-web-base-url)
|
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
|
public_web_base_url=$2
|
|
shift 2
|
|
;;
|
|
--admin-bind-ip)
|
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
|
admin_bind_ip=$2
|
|
shift 2
|
|
;;
|
|
--host-network) server_host_network=true; shift ;;
|
|
--bridge-network) server_host_network=false; shift ;;
|
|
--allow-insecure-development-auth) allow_insecure=true; shift ;;
|
|
--output)
|
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
|
output_path=$2
|
|
shift 2
|
|
;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) die "unknown argument: $1" ;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$advertise_ip" ] || die "--advertise-ip is required"
|
|
[ -f "$template_path" ] || die "Docker environment template not found: $template_path"
|
|
[ ! -e "$output_path" ] || die "$output_path already exists; initialization never overwrites live credentials"
|
|
command -v openssl >/dev/null 2>&1 || die "openssl is required"
|
|
|
|
is_ipv6=false
|
|
case "$advertise_ip" in
|
|
*:*) validate_ipv6 "$advertise_ip" || die "--advertise-ip is not a valid IPv6 address"; is_ipv6=true ;;
|
|
*) validate_ipv4 "$advertise_ip" || die "--advertise-ip is not a valid IPv4 address" ;;
|
|
esac
|
|
case "$admin_bind_ip" in
|
|
*:*) validate_ipv6 "$admin_bind_ip" || die "--admin-bind-ip is not a valid IPv6 address" ;;
|
|
*) validate_ipv4 "$admin_bind_ip" || die "--admin-bind-ip is not a valid IPv4 address" ;;
|
|
esac
|
|
|
|
is_loopback=false
|
|
if [ "$is_ipv6" = true ]; then
|
|
case "$advertise_ip" in ::1|0:0:0:0:0:0:0:1) is_loopback=true ;; esac
|
|
else
|
|
case "$advertise_ip" in 127.*) is_loopback=true ;; esac
|
|
fi
|
|
|
|
if [ -z "$public_base_url" ]; then
|
|
[ "$is_loopback" = true ] || die "--public-base-url is required when --advertise-ip is not loopback"
|
|
if [ "$is_ipv6" = true ]; then public_base_url='http://[::1]:2401'; else public_base_url='http://127.0.0.1:2401'; fi
|
|
fi
|
|
[ -n "$public_web_base_url" ] || public_web_base_url=$public_base_url
|
|
validate_http_url "--public-base-url" "$public_base_url"
|
|
validate_http_url "--public-web-base-url" "$public_web_base_url"
|
|
|
|
if [ "$is_loopback" = false ] && [ "$allow_insecure" = false ]; then
|
|
die "Internet/LAN development-code auth requires --allow-insecure-development-auth; for production generate on loopback, then configure the webhook provider before startup"
|
|
fi
|
|
|
|
public_bind_ip=0.0.0.0
|
|
local_bind_ip=127.0.0.1
|
|
if [ "$is_ipv6" = true ]; then public_bind_ip=::; local_bind_ip=::1; fi
|
|
if [ "$is_loopback" = true ]; then
|
|
public_bind_ip=$advertise_ip
|
|
local_bind_ip=$advertise_ip
|
|
elif [ "${public_base_url%%:*}" = http ] && [ "$(url_host "$public_base_url")" = "$advertise_ip" ]; then
|
|
local_bind_ip=$advertise_ip
|
|
fi
|
|
|
|
public_listen_host=$public_bind_ip
|
|
local_listen_host=$local_bind_ip
|
|
server_health_url_host=$local_bind_ip
|
|
if [ "$is_ipv6" = true ]; then
|
|
public_listen_host="[$public_bind_ip]"
|
|
local_listen_host="[$local_bind_ip]"
|
|
server_health_url_host="[$local_bind_ip]"
|
|
fi
|
|
|
|
turn_enable=true
|
|
turn_advertise_ip=$advertise_ip
|
|
if [ "$is_ipv6" = true ]; then
|
|
turn_enable=false
|
|
turn_advertise_ip=127.0.0.1
|
|
fi
|
|
|
|
admin_health_ip=$admin_bind_ip
|
|
case "$admin_bind_ip" in 0.0.0.0) admin_health_ip=127.0.0.1 ;; ::) admin_health_ip=::1 ;; esac
|
|
admin_listen_host=$admin_bind_ip
|
|
case "$admin_bind_ip" in *:*) admin_listen_host="[$admin_bind_ip]" ;; esac
|
|
rtmp_host=$advertise_ip
|
|
[ "$is_ipv6" = true ] && rtmp_host="[$rtmp_host]"
|
|
|
|
build_commit=$(git -C "$repo_root" rev-parse HEAD 2>/dev/null || printf unknown)
|
|
build_branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || printf unknown)
|
|
build_tree_state=unknown
|
|
if git -C "$repo_root" status --porcelain >/dev/null 2>&1; then
|
|
if [ -n "$(git -C "$repo_root" status --porcelain)" ]; then build_tree_state=dirty; else build_tree_state=clean; fi
|
|
fi
|
|
|
|
POSTGRES_PASSWORD=$(random_hex 24)
|
|
TELESRV_BUILD_COMMIT=$build_commit
|
|
TELESRV_BUILD_BRANCH=$build_branch
|
|
TELESRV_BUILD_TREE_STATE=$build_tree_state
|
|
TELESRV_BUILD_DATE=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
TELESRV_POSTGRES_DSN="postgres://telesrv:$POSTGRES_PASSWORD@127.0.0.1:15432/telesrv_main?sslmode=disable"
|
|
TELESRV_REDIS_PASSWORD=$(random_hex 32)
|
|
TELESRV_ADMIN_API_TOKEN=$(random_hex 32)
|
|
TELESRV_ADMIN_UI_PASSWORD=$(random_hex 24)
|
|
TELESRV_ADMIN_SESSION_KEY=$(random_hex 32)
|
|
TELESRV_TURN_SECRET=$(random_hex 32)
|
|
TELESRV_OTP_WEBHOOK_SECRET=$(random_hex 32)
|
|
TELESRV_ALLOW_INSECURE_DEVELOPMENT_AUTH=$allow_insecure
|
|
[ "$is_loopback" = true ] && TELESRV_ALLOW_INSECURE_DEVELOPMENT_AUTH=true
|
|
TELESRV_ADVERTISE_IP=$advertise_ip
|
|
TELESRV_PUBLIC_BASE_URL=$public_base_url
|
|
TELESRV_PUBLIC_WEB_BASE_URL=$public_web_base_url
|
|
TELESRV_SERVER_HOST_NETWORK=$server_host_network
|
|
TELESRV_SFU_ADVERTISE_IP=$advertise_ip
|
|
TELESRV_TURN_ENABLE=$turn_enable
|
|
TELESRV_TURN_ADVERTISE_IP=$turn_advertise_ip
|
|
TELESRV_LIVESTREAM_RTMP_URL="rtmp://$rtmp_host:2400/live"
|
|
TELESRV_PUBLIC_BIND_IP=$public_bind_ip
|
|
TELESRV_PUBLIC_LISTEN_HOST=$public_listen_host
|
|
TELESRV_LOCAL_BIND_IP=$local_bind_ip
|
|
TELESRV_LOCAL_LISTEN_HOST=$local_listen_host
|
|
TELESRV_SERVER_HEALTH_IP=$local_bind_ip
|
|
TELESRV_SERVER_HEALTH_URL_HOST=$server_health_url_host
|
|
TELESRV_ADMIN_BIND_IP=$admin_bind_ip
|
|
TELESRV_ADMIN_LISTEN_HOST=$admin_listen_host
|
|
TELESRV_ADMIN_HEALTH_IP=$admin_health_ip
|
|
|
|
replacement_keys='TELESRV_BUILD_COMMIT TELESRV_BUILD_BRANCH TELESRV_BUILD_TREE_STATE TELESRV_BUILD_DATE POSTGRES_PASSWORD TELESRV_POSTGRES_DSN TELESRV_REDIS_PASSWORD TELESRV_ADMIN_API_TOKEN TELESRV_ADMIN_UI_PASSWORD TELESRV_ADMIN_SESSION_KEY TELESRV_TURN_SECRET TELESRV_OTP_WEBHOOK_SECRET TELESRV_ALLOW_INSECURE_DEVELOPMENT_AUTH TELESRV_ADVERTISE_IP TELESRV_PUBLIC_BASE_URL TELESRV_PUBLIC_WEB_BASE_URL TELESRV_SERVER_HOST_NETWORK TELESRV_SFU_ADVERTISE_IP TELESRV_TURN_ENABLE TELESRV_TURN_ADVERTISE_IP TELESRV_LIVESTREAM_RTMP_URL TELESRV_PUBLIC_BIND_IP TELESRV_PUBLIC_LISTEN_HOST TELESRV_LOCAL_BIND_IP TELESRV_LOCAL_LISTEN_HOST TELESRV_SERVER_HEALTH_IP TELESRV_SERVER_HEALTH_URL_HOST TELESRV_ADMIN_BIND_IP TELESRV_ADMIN_LISTEN_HOST TELESRV_ADMIN_HEALTH_IP'
|
|
export POSTGRES_PASSWORD TELESRV_BUILD_COMMIT TELESRV_BUILD_BRANCH TELESRV_BUILD_TREE_STATE TELESRV_BUILD_DATE
|
|
export TELESRV_POSTGRES_DSN TELESRV_REDIS_PASSWORD TELESRV_ADMIN_API_TOKEN TELESRV_ADMIN_UI_PASSWORD
|
|
export TELESRV_ADMIN_SESSION_KEY TELESRV_TURN_SECRET TELESRV_OTP_WEBHOOK_SECRET TELESRV_ALLOW_INSECURE_DEVELOPMENT_AUTH
|
|
export TELESRV_ADVERTISE_IP TELESRV_PUBLIC_BASE_URL TELESRV_PUBLIC_WEB_BASE_URL TELESRV_SERVER_HOST_NETWORK
|
|
export TELESRV_SFU_ADVERTISE_IP TELESRV_TURN_ENABLE TELESRV_TURN_ADVERTISE_IP TELESRV_LIVESTREAM_RTMP_URL
|
|
export TELESRV_PUBLIC_BIND_IP TELESRV_PUBLIC_LISTEN_HOST TELESRV_LOCAL_BIND_IP TELESRV_LOCAL_LISTEN_HOST
|
|
export TELESRV_SERVER_HEALTH_IP TELESRV_SERVER_HEALTH_URL_HOST TELESRV_ADMIN_BIND_IP TELESRV_ADMIN_LISTEN_HOST TELESRV_ADMIN_HEALTH_IP
|
|
|
|
output_dir=$(dirname -- "$output_path")
|
|
[ -d "$output_dir" ] || die "output directory does not exist: $output_dir"
|
|
temporary_path=$(mktemp "$output_path.tmp.XXXXXX")
|
|
cleanup() { [ ! -e "$temporary_path" ] || unlink "$temporary_path"; }
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
awk -v keys="$replacement_keys" '
|
|
BEGIN { count = split(keys, list, " "); for (i = 1; i <= count; i++) wanted[list[i]] = 1 }
|
|
{
|
|
separator = index($0, "=")
|
|
key = separator ? substr($0, 1, separator - 1) : ""
|
|
if (key in wanted) { print key "=" ENVIRON[key]; seen[key] = 1 } else print
|
|
}
|
|
END {
|
|
for (key in wanted) if (!(key in seen)) { print "template is missing " key > "/dev/stderr"; missing = 1 }
|
|
exit missing
|
|
}
|
|
' "$template_path" >"$temporary_path"
|
|
chmod 0600 "$temporary_path"
|
|
mv "$temporary_path" "$output_path"
|
|
trap - EXIT HUP INT TERM
|
|
printf 'Created %s with owner-only permissions.\n' "$output_path"
|