481 lines
15 KiB
Bash
Executable File
481 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
env_file=""
|
|
mode="nossl"
|
|
compose_file=""
|
|
skip_when_running="0"
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: ./scripts/select-host-service-ports.sh --env-file <path> [--mode ssl|nossl] [--compose-file <path>] [--skip-when-running]
|
|
|
|
Auto-selects free host listen ports for enabled service profiles and updates the env file in-place.
|
|
USAGE
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--env-file)
|
|
env_file="${2:-}"
|
|
shift 2
|
|
;;
|
|
--env-file=*)
|
|
env_file="${1#*=}"
|
|
shift
|
|
;;
|
|
--mode)
|
|
mode="${2:-}"
|
|
shift 2
|
|
;;
|
|
--mode=*)
|
|
mode="${1#*=}"
|
|
shift
|
|
;;
|
|
--compose-file)
|
|
compose_file="${2:-}"
|
|
shift 2
|
|
;;
|
|
--compose-file=*)
|
|
compose_file="${1#*=}"
|
|
shift
|
|
;;
|
|
--skip-when-running)
|
|
skip_when_running="1"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "${env_file}" ]]; then
|
|
echo "Missing --env-file" >&2
|
|
usage
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "${env_file}" ]]; then
|
|
echo "Env file not found: ${env_file}" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${mode}" != "ssl" && "${mode}" != "nossl" ]]; then
|
|
echo "Invalid mode: ${mode}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
read_kv() {
|
|
local key="$1"
|
|
local line
|
|
line="$(grep -m1 -E "^${key}=" "${env_file}" || true)"
|
|
if [[ -z "${line}" ]]; then
|
|
return 1
|
|
fi
|
|
echo "${line#*=}"
|
|
}
|
|
|
|
set_kv() {
|
|
local key="$1"
|
|
local value="$2"
|
|
local esc
|
|
esc="${value//\\/\\\\}"
|
|
esc="${esc//&/\\&}"
|
|
esc="${esc//|/\\|}"
|
|
if grep -q -E "^${key}=" "${env_file}"; then
|
|
sed -i -E "s|^${key}=.*|${key}=${esc}|" "${env_file}"
|
|
else
|
|
echo "${key}=${value}" >> "${env_file}"
|
|
fi
|
|
}
|
|
|
|
is_valid_port() {
|
|
local port="$1"
|
|
[[ "${port}" =~ ^[0-9]+$ ]] || return 1
|
|
(( port >= 1 && port <= 65535 ))
|
|
}
|
|
|
|
normalize_bind_host() {
|
|
local host="$1"
|
|
if [[ -z "${host}" || "${host}" == "0.0.0.0" || "${host}" == "::" ]]; then
|
|
echo "*"
|
|
else
|
|
echo "${host}"
|
|
fi
|
|
}
|
|
|
|
port_in_use() {
|
|
local host="$1"
|
|
local port="$2"
|
|
|
|
if ! command -v ss >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ -n "${host}" && "${host}" != "0.0.0.0" && "${host}" != "::" ]]; then
|
|
if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "(^|[\\[\\]:])${host}:${port}$|:${port}$"; then
|
|
return 0
|
|
fi
|
|
if ss -H -lun 2>/dev/null | awk '{print $5}' | grep -Eq "(^|[\\[\\]:])${host}:${port}$|:${port}$"; then
|
|
return 0
|
|
fi
|
|
else
|
|
if ss -H -ltn "( sport = :${port} )" 2>/dev/null | grep -q .; then
|
|
return 0
|
|
fi
|
|
if ss -H -lun "( sport = :${port} )" 2>/dev/null | grep -q .; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
declare -a reserved_bindings=()
|
|
|
|
is_reserved() {
|
|
local host="$1"
|
|
local port="$2"
|
|
local normalized host_part port_part entry
|
|
normalized="$(normalize_bind_host "${host}")"
|
|
for entry in "${reserved_bindings[@]}"; do
|
|
host_part="${entry%%:*}"
|
|
port_part="${entry##*:}"
|
|
if [[ "${port_part}" != "${port}" ]]; then
|
|
continue
|
|
fi
|
|
if [[ "${normalized}" == "*" || "${host_part}" == "*" || "${normalized}" == "${host_part}" ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
reserve_binding() {
|
|
local host="$1"
|
|
local port="$2"
|
|
reserved_bindings+=("$(normalize_bind_host "${host}"):${port}")
|
|
}
|
|
|
|
port_unavailable() {
|
|
local host="$1"
|
|
local port="$2"
|
|
if is_reserved "${host}" "${port}"; then
|
|
return 0
|
|
fi
|
|
if port_in_use "${host}" "${port}"; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
find_free_port() {
|
|
local host="$1"
|
|
local start_port="$2"
|
|
local candidate
|
|
|
|
for ((candidate=start_port; candidate<=65535; candidate++)); do
|
|
if ! port_unavailable "${host}" "${candidate}"; then
|
|
echo "${candidate}"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
assign_port_key() {
|
|
local key="$1"
|
|
local host="$2"
|
|
local default_port="$3"
|
|
local label="$4"
|
|
local current_value desired start_port selected old_value
|
|
|
|
current_value="$(read_kv "${key}" || true)"
|
|
old_value="${current_value}"
|
|
if ! is_valid_port "${current_value}"; then
|
|
current_value="${default_port}"
|
|
fi
|
|
|
|
desired="${current_value}"
|
|
start_port="${default_port}"
|
|
if is_valid_port "${desired}"; then
|
|
start_port="${desired}"
|
|
fi
|
|
|
|
if port_unavailable "${host}" "${desired}"; then
|
|
selected="$(find_free_port "${host}" "${start_port}" || true)"
|
|
if [[ -z "${selected}" ]]; then
|
|
echo "Unable to find a free host port for ${key} (${label})" >&2
|
|
exit 1
|
|
fi
|
|
set_kv "${key}" "${selected}"
|
|
echo "Auto-selected ${key}=${selected} (${label})"
|
|
desired="${selected}"
|
|
elif [[ "${old_value}" != "${desired}" ]]; then
|
|
# Normalize invalid/missing values to the chosen default/current.
|
|
set_kv "${key}" "${desired}"
|
|
fi
|
|
|
|
reserve_binding "${host}" "${desired}"
|
|
}
|
|
|
|
reserve_existing_port_key() {
|
|
local key="$1"
|
|
local host="$2"
|
|
local current_value
|
|
|
|
current_value="$(read_kv "${key}" || true)"
|
|
if is_valid_port "${current_value}"; then
|
|
reserve_binding "${host}" "${current_value}"
|
|
fi
|
|
}
|
|
|
|
service_running() {
|
|
local service="$1"
|
|
if [[ "${skip_when_running}" != "1" || -z "${compose_file}" || ! -f "${compose_file}" ]]; then
|
|
return 1
|
|
fi
|
|
docker compose -f "${compose_file}" --env-file "${env_file}" ps --status running -q "${service}" 2>/dev/null | grep -q .
|
|
}
|
|
|
|
assign_service_port_key() {
|
|
local service="$1"
|
|
local key="$2"
|
|
local host="$3"
|
|
local default_port="$4"
|
|
local label="$5"
|
|
|
|
if service_running "${service}"; then
|
|
reserve_existing_port_key "${key}" "${host}"
|
|
return 0
|
|
fi
|
|
|
|
assign_port_key "${key}" "${host}" "${default_port}" "${label}"
|
|
}
|
|
|
|
normalize_running_stack_conflicts() {
|
|
local compose_profiles current_http_port current_talk_signaling_port replacement_http_port
|
|
compose_profiles="$(trim_csv "$(read_kv "COMPOSE_PROFILES" || true)")"
|
|
|
|
if [[ "${mode}" != "nossl" ]]; then
|
|
return 0
|
|
fi
|
|
if [[ ",${compose_profiles}," != *",talk,"* && ",${compose_profiles}," != *",turn,"* && ",${compose_profiles}," != *",janus,"* ]]; then
|
|
return 0
|
|
fi
|
|
|
|
current_http_port="$(read_kv "DEVPROD_HTTP_PORT" || true)"
|
|
current_talk_signaling_port="$(read_kv "TALK_SIGNALING_PORT" || true)"
|
|
if ! is_valid_port "${current_http_port}" || ! is_valid_port "${current_talk_signaling_port}"; then
|
|
return 0
|
|
fi
|
|
if [[ "${current_http_port}" != "${current_talk_signaling_port}" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
reserve_binding "0.0.0.0" "${current_talk_signaling_port}"
|
|
replacement_http_port="$(find_free_port "127.0.0.1" "${current_http_port}" || true)"
|
|
if [[ -z "${replacement_http_port}" ]]; then
|
|
echo "Unable to find a replacement DEVPROD_HTTP_PORT while avoiding TALK_SIGNALING_PORT=${current_talk_signaling_port}" >&2
|
|
exit 1
|
|
fi
|
|
set_kv "DEVPROD_HTTP_PORT" "${replacement_http_port}"
|
|
echo "Adjusted DEVPROD_HTTP_PORT=${replacement_http_port} to avoid TALK_SIGNALING_PORT=${current_talk_signaling_port} on a running no-SSL cloud."
|
|
}
|
|
|
|
profile_enabled() {
|
|
local name="$1"
|
|
[[ ",${compose_profiles}," == *",${name},"* ]]
|
|
}
|
|
|
|
trim_csv() {
|
|
local raw="$1"
|
|
local cleaned
|
|
cleaned="$(echo "${raw}" | tr -d '[:space:]')"
|
|
cleaned="${cleaned#,}"
|
|
cleaned="${cleaned%,}"
|
|
echo "${cleaned}"
|
|
}
|
|
|
|
bind_host_for() {
|
|
local key="$1"
|
|
local default_host="$2"
|
|
local value
|
|
value="$(read_kv "${key}" || true)"
|
|
if [[ -z "${value}" ]]; then
|
|
echo "${default_host}"
|
|
else
|
|
echo "${value}"
|
|
fi
|
|
}
|
|
|
|
compute_public_url() {
|
|
local domain="$1"
|
|
local https_port="$2"
|
|
if [[ -z "${domain}" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
if [[ "${https_port}" == "443" ]]; then
|
|
echo "https://${domain}"
|
|
else
|
|
echo "https://${domain}:${https_port}"
|
|
fi
|
|
}
|
|
|
|
if [[ "${skip_when_running}" == "1" && -n "${compose_file}" && -f "${compose_file}" ]]; then
|
|
existing_stack_ids="$(docker compose -f "${compose_file}" --env-file "${env_file}" ps -q 2>/dev/null || true)"
|
|
if [[ -n "${existing_stack_ids}" ]]; then
|
|
normalize_running_stack_conflicts
|
|
echo "Compose project already running; checking ports for newly enabled services only."
|
|
fi
|
|
fi
|
|
|
|
refresh_public_urls_from_https_port() {
|
|
local nextcloud_domain broker_domain collabora_domain signaling_domain https_port
|
|
local nextcloud_public_url broker_public_url collabora_public_url signaling_public_url
|
|
|
|
nextcloud_domain="$(read_kv "NEXTCLOUD_DOMAIN" || true)"
|
|
broker_domain="$(read_kv "BROKER_DOMAIN" || true)"
|
|
collabora_domain="$(read_kv "COLLABORA_DOMAIN" || true)"
|
|
signaling_domain="$(read_kv "SIGNALING_DOMAIN" || true)"
|
|
https_port="$(read_kv "CADDY_HTTPS_PORT" || true)"
|
|
if ! is_valid_port "${https_port}"; then
|
|
return
|
|
fi
|
|
|
|
nextcloud_public_url="$(compute_public_url "${nextcloud_domain}" "${https_port}")"
|
|
broker_public_url="$(compute_public_url "${broker_domain}" "${https_port}")"
|
|
collabora_public_url="$(compute_public_url "${collabora_domain}" "${https_port}")"
|
|
signaling_public_url="$(compute_public_url "${signaling_domain}" "${https_port}")"
|
|
|
|
if [[ -n "${nextcloud_public_url}" ]]; then
|
|
set_kv "NEXTCLOUD_PUBLIC_URL" "${nextcloud_public_url}"
|
|
set_kv "OIDC_REDIRECT_URI_ALLOWLIST" "${nextcloud_public_url}/apps/user_oidc/code"
|
|
fi
|
|
if [[ -n "${broker_public_url}" ]]; then
|
|
set_kv "OIDC_ISSUER" "${broker_public_url}"
|
|
fi
|
|
if [[ -n "${collabora_public_url}" ]]; then
|
|
set_kv "COLLABORA_PUBLIC_URL" "${collabora_public_url}"
|
|
fi
|
|
if [[ -n "${signaling_public_url}" ]]; then
|
|
set_kv "TALK_SIGNALING_PUBLIC_URL" "${signaling_public_url}"
|
|
fi
|
|
}
|
|
|
|
sync_stun_server_to_turn_port() {
|
|
local talk_domain nextcloud_domain signaling_domain talk_turn_host talk_janus_stun_server stun_server turn_server turn_port stun_host turn_host host_fqdn host_short host_fqdn_local host_short_local
|
|
nextcloud_domain="$(read_kv "NEXTCLOUD_DOMAIN" || true)"
|
|
signaling_domain="$(read_kv "SIGNALING_DOMAIN" || true)"
|
|
talk_domain="${signaling_domain:-${nextcloud_domain}}"
|
|
talk_turn_host="$(read_kv "TALK_TURN_HOST" || true)"
|
|
if [[ -z "${talk_turn_host}" ]]; then
|
|
talk_turn_host="${nextcloud_domain}"
|
|
if [[ -n "${talk_turn_host}" ]]; then
|
|
set_kv "TALK_TURN_HOST" "${talk_turn_host}"
|
|
fi
|
|
fi
|
|
turn_port="$(read_kv "TALK_TURN_PORT" || true)"
|
|
talk_janus_stun_server="$(read_kv "TALK_JANUS_STUN_SERVER" || true)"
|
|
stun_server="$(read_kv "TALK_STUN_SERVER" || true)"
|
|
turn_server="$(read_kv "TALK_TURN_SERVER" || true)"
|
|
host_fqdn="$(hostname -f 2>/dev/null || true)"
|
|
host_short="$(hostname -s 2>/dev/null || true)"
|
|
host_fqdn_local="${host_fqdn}.local"
|
|
host_short_local="${host_short}.local"
|
|
|
|
if [[ -z "${talk_turn_host}" ]] || ! is_valid_port "${turn_port}"; then
|
|
return
|
|
fi
|
|
|
|
if [[ -z "$(read_kv "TALK_JANUS_STUN_PORT" || true)" || "$(read_kv "TALK_JANUS_STUN_PORT" || true)" == "3478" || "${talk_janus_stun_server}" == "${talk_domain}" || "${talk_janus_stun_server}" == "${talk_turn_host}" || "${talk_janus_stun_server}" == "${host_fqdn}" || "${talk_janus_stun_server}" == "${host_short}" || "${talk_janus_stun_server}" == "${host_fqdn_local}" || "${talk_janus_stun_server}" == "${host_short_local}" || "${talk_janus_stun_server}" == "localhost" || "${talk_janus_stun_server}" == "127.0.0.1" || "${talk_janus_stun_server}" == "stun.nextcloud.com" ]]; then
|
|
set_kv "TALK_JANUS_STUN_PORT" "${turn_port}"
|
|
fi
|
|
if [[ -z "${talk_janus_stun_server}" || "${talk_janus_stun_server}" == "${talk_domain}" || "${talk_janus_stun_server}" == "${talk_turn_host}" || "${talk_janus_stun_server}" == "${host_fqdn}" || "${talk_janus_stun_server}" == "${host_short}" || "${talk_janus_stun_server}" == "${host_fqdn_local}" || "${talk_janus_stun_server}" == "${host_short_local}" || "${talk_janus_stun_server}" == "localhost" || "${talk_janus_stun_server}" == "127.0.0.1" || "${talk_janus_stun_server}" == "stun.nextcloud.com" ]]; then
|
|
set_kv "TALK_JANUS_STUN_SERVER" "${talk_turn_host}"
|
|
fi
|
|
|
|
if [[ -z "${turn_server}" ]]; then
|
|
set_kv "TALK_TURN_SERVER" "${talk_turn_host}:${turn_port}"
|
|
else
|
|
turn_host="${turn_server%%:*}"
|
|
if [[ "${turn_host}" == "${talk_domain}" || "${turn_host}" == "${talk_turn_host}" || "${turn_host}" == "${host_fqdn}" || "${turn_host}" == "${host_short}" || "${turn_host}" == "${host_fqdn_local}" || "${turn_host}" == "${host_short_local}" || "${turn_host}" == "localhost" || "${turn_host}" == "127.0.0.1" ]]; then
|
|
set_kv "TALK_TURN_SERVER" "${talk_turn_host}:${turn_port}"
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "${stun_server}" || "${stun_server}" == "stun.nextcloud.com:443" ]]; then
|
|
set_kv "TALK_STUN_SERVER" "${talk_turn_host}:${turn_port}"
|
|
return
|
|
fi
|
|
|
|
stun_host="${stun_server%%:*}"
|
|
if [[ "${stun_host}" == "${talk_domain}" || "${stun_host}" == "${talk_turn_host}" || "${stun_host}" == "${host_fqdn}" || "${stun_host}" == "${host_short}" || "${stun_host}" == "${host_fqdn_local}" || "${stun_host}" == "${host_short_local}" || "${stun_host}" == "localhost" || "${stun_host}" == "127.0.0.1" ]]; then
|
|
set_kv "TALK_STUN_SERVER" "${talk_turn_host}:${turn_port}"
|
|
fi
|
|
}
|
|
|
|
compose_profiles="$(trim_csv "$(read_kv "COMPOSE_PROFILES" || true)")"
|
|
talk_profile_active="0"
|
|
if profile_enabled "talk" || profile_enabled "turn" || profile_enabled "janus"; then
|
|
talk_profile_active="1"
|
|
talk_signaling_bind_host="$(bind_host_for "TALK_SIGNALING_BIND_HOST" "0.0.0.0")"
|
|
set_kv "TALK_SIGNALING_BIND_HOST" "${talk_signaling_bind_host}"
|
|
assign_service_port_key "talk_hpb" "TALK_SIGNALING_PORT" "${talk_signaling_bind_host}" "8081" "Talk signaling"
|
|
set_kv "TALK_SIGNALING_CONTAINER_PORT" "8081"
|
|
if [[ -z "$(read_kv "TALK_ETURNAL_EPMD_PORT" || true)" ]]; then
|
|
set_kv "TALK_ETURNAL_EPMD_PORT" "3470"
|
|
fi
|
|
set_kv "TALK_SIGNALING_UPSTREAM_PORT" "$(read_kv "TALK_SIGNALING_PORT" || true)"
|
|
fi
|
|
|
|
ssl_http_before="$(read_kv "CADDY_HTTP_PORT" || true)"
|
|
ssl_https_before="$(read_kv "CADDY_HTTPS_PORT" || true)"
|
|
|
|
if [[ "${mode}" == "nossl" ]]; then
|
|
assign_service_port_key "app" "DEVPROD_HTTP_PORT" "$(bind_host_for "DEVPROD_HTTP_BIND_HOST" "127.0.0.1")" "8081" "Nextcloud HTTP"
|
|
assign_service_port_key "broker" "DEVPROD_BROKER_PORT" "$(bind_host_for "DEVPROD_BROKER_BIND_HOST" "127.0.0.1")" "3001" "OIDC broker"
|
|
else
|
|
assign_port_key "CADDY_HTTP_PORT" "0.0.0.0" "80" "Caddy HTTP"
|
|
assign_port_key "CADDY_HTTPS_PORT" "0.0.0.0" "443" "Caddy HTTPS"
|
|
fi
|
|
|
|
if profile_enabled "external-auth"; then
|
|
assign_service_port_key "external_auth" "EXTERNAL_AUTH_PORT" "127.0.0.1" "3191" "External auth"
|
|
fi
|
|
|
|
if profile_enabled "office"; then
|
|
assign_service_port_key "collabora" "COLLABORA_PORT" "$(bind_host_for "COLLABORA_BIND_HOST" "127.0.0.1")" "9980" "Collabora"
|
|
fi
|
|
|
|
if [[ "${talk_profile_active}" == "1" ]]; then
|
|
talk_turn_bind_host="$(read_kv "TALK_TURN_BIND_HOST" || true)"
|
|
talk_turn_bind_host="${talk_turn_bind_host:-0.0.0.0}"
|
|
talk_signaling_port="$(read_kv "TALK_SIGNALING_PORT" || true)"
|
|
if [[ -n "${talk_signaling_port}" ]] && port_in_use "" "${talk_signaling_port}"; then
|
|
echo "Note: Talk signaling uses host port ${talk_signaling_port} and it appears in use; this may be expected if talk_hpb is already running."
|
|
fi
|
|
assign_service_port_key "talk_hpb" "TALK_TURN_PORT" "${talk_turn_bind_host}" "3478" "Talk TURN"
|
|
sync_stun_server_to_turn_port
|
|
fi
|
|
|
|
if profile_enabled "recording"; then
|
|
assign_service_port_key "talk_recording" "TALK_RECORDING_PORT" "$(bind_host_for "TALK_RECORDING_BIND_HOST" "127.0.0.1")" "1234" "Talk recording"
|
|
fi
|
|
|
|
ssl_http_after="$(read_kv "CADDY_HTTP_PORT" || true)"
|
|
ssl_https_after="$(read_kv "CADDY_HTTPS_PORT" || true)"
|
|
if [[ "${mode}" == "ssl" && ( "${ssl_http_before}" != "${ssl_http_after}" || "${ssl_https_before}" != "${ssl_https_after}" ) ]]; then
|
|
refresh_public_urls_from_https_port
|
|
echo "Updated public HTTPS URL settings to match Caddy HTTPS port ${ssl_https_after}."
|
|
fi
|
|
|
|
exit 0
|