Files
Qortal-Nextcloud-Integration/scripts/select-host-service-ports.sh

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