The parser accepted five shapes. Three of them (9, 10, 11/12) differ only by trailing columns that have sane defaults, and those are worth keeping: 39 of the catalogue's descriptors stop at nine because they are non-Traefik ports — DNS, SMTP, WireGuard UDP — with no subdomain to state. A short row there is a complete row. The other two were different animals. The 8-column legacy layout has no login column and the 7-column one has no parent either, so they SHIFT every position rather than omitting a tail: whenever the length was misread, each field after the shift silently took its neighbour's value — a port's access type reading from its protocol, and so on. That is the same class of fault the word-splitting bug in this file just caused, and it is invisible when it happens. Nothing needs them. All 74 descriptors in the catalogue carry nine or more, as does every one on this install. So they are refused now, with a notice naming the offending key: a skipped port is visible, a mis-parsed one is not. Checked that skipping a row cannot misalign the parallel arrays — port_config_data and port_config_vars are appended before the branch, but neither is ever indexed alongside the others; the former is only tested for emptiness. Verified across every shape: 9, 10, 11 and 12 parse with the right defaults, a label containing spaces survives intact next to an empty trailing column, and both legacy layouts are refused rather than guessed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
198 lines
9.0 KiB
Bash
Executable File
198 lines
9.0 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Default app variable setups
|
|
initializeAppVariables()
|
|
{
|
|
app_name="$1"
|
|
|
|
if [[ "$app_name" == "" ]]; then
|
|
isError "Something went wrong...No app name provided..."
|
|
if [[ "$initial_command2" == "terminal" ]]; then
|
|
resetToMenu;
|
|
fi
|
|
fi
|
|
|
|
# Build variable names based on app_name
|
|
compose_setup_var="CFG_${app_name^^}_COMPOSE_FILE"
|
|
domain_var="CFG_${app_name^^}_DOMAIN"
|
|
whitelist_var="CFG_${app_name^^}_WHITELIST"
|
|
healthcheck_var="CFG_${app_name^^}_HEALTHCHECK"
|
|
authelia_var="CFG_${app_name^^}_AUTHELIA"
|
|
headscale_var="CFG_${app_name^^}_HEADSCALE"
|
|
app_category_var="CFG_${app_name^^}_CATEGORY"
|
|
app_title_var="CFG_${app_name^^}_TITLE"
|
|
|
|
# Access the variables using variable indirection
|
|
compose_setup="${!compose_setup_var}"
|
|
domain="${!domain_var}"
|
|
whitelist="${!whitelist_var}"
|
|
healthcheck="${!healthcheck_var}"
|
|
authelia_setup="${!authelia_var}"
|
|
headscale_setup="${!headscale_var}"
|
|
app_category="${!app_category_var}"
|
|
app_title="${!app_title_var}"
|
|
domain_var_name="CFG_DOMAIN_${domain}"
|
|
domain_full="${!domain_var_name}"
|
|
ssl_key=${domain_full}.key
|
|
ssl_crt=${domain_full}.crt
|
|
# host_setup (the app's primary/canonical FQDN) is derived from the primary
|
|
# Traefik port's subdomain, computed after the port arrays are parsed below.
|
|
|
|
# Port configuration variables
|
|
port_config_vars=()
|
|
port_config_data=()
|
|
# Arrays to hold parsed port configuration data
|
|
port_service_names=()
|
|
port_parent_services=()
|
|
port_data_tags=()
|
|
port_external_ports=()
|
|
port_internal_ports=()
|
|
port_access_types=()
|
|
port_protocols=()
|
|
port_traefik_managed=()
|
|
port_url_accessibles=()
|
|
port_login_requireds=()
|
|
port_labels=()
|
|
port_url_paths=()
|
|
port_subdomains=()
|
|
port_recommendeds=()
|
|
|
|
for i in {1..20}; do
|
|
port_config_vars+=("CFG_${app_name^^}_PORT_$i")
|
|
# Store actual config data for port allocation
|
|
local port_config_var="CFG_${app_name^^}_PORT_$i"
|
|
local port_config_value="${!port_config_var}"
|
|
if [[ -n "$port_config_value" ]]; then
|
|
port_config_data+=("$port_config_value")
|
|
|
|
# 12-col: parent|name|ext:int|access|proto|login|traefik|webui|label|url_path|subdomain|recommended
|
|
# 11-col: ... |subdomain (recommended defaults to the webui flag)
|
|
# 10-col: ... |url_path (subdomain empty -> app-name default)
|
|
# 9-col: parent|name|ext:int|access|proto|login|traefik|webui|label
|
|
#
|
|
# Nine is the floor. The trailing three are genuinely optional and
|
|
# have sane defaults, so a 9/10/11-column row is a complete row --
|
|
# 39 of the catalogue's descriptors stop at nine because they are
|
|
# non-Traefik ports (DNS, SMTP, WireGuard UDP) with no subdomain to
|
|
# state. Anything shorter is not a shorter row, it is a DIFFERENT
|
|
# layout, and is refused below.
|
|
# IFS-split, NOT ${v//|/ } with word-splitting. That idiom broke the
|
|
# format in two ways at once: a label containing a space became
|
|
# several fields ("Web Interface" -> label "Web", url_path
|
|
# "Interface"), and an EMPTY column collapsed instead of being kept,
|
|
# shifting every field after it. Stoat's LiveKit row parsed as
|
|
# label "LiveKit", url_path "voice/video", subdomain "(TCP",
|
|
# recommended "fallback)" — and Rocket.Chat's subdomain only landed
|
|
# correctly because the extra label word and the collapsed empty
|
|
# column happened to cancel out. The column COUNT was wrong too, so
|
|
# the 9/8/7-col branch below was chosen from an inflated number.
|
|
local parts=()
|
|
IFS='|' read -ra parts <<< "$port_config_value"
|
|
if [[ ${#parts[@]} -ge 9 ]]; then
|
|
local external_port="${parts[2]%%:*}"
|
|
local internal_port="${parts[2]##*:}"
|
|
port_parent_services+=("${parts[0]}")
|
|
port_service_names+=("${parts[1]}")
|
|
port_data_tags+=("PORTS_TAG_$i")
|
|
port_external_ports+=("$external_port")
|
|
port_internal_ports+=("$internal_port")
|
|
port_access_types+=("${parts[3]}")
|
|
port_protocols+=("${parts[4]}")
|
|
port_login_requireds+=("${parts[5]}")
|
|
port_traefik_managed+=("${parts[6]}")
|
|
port_url_accessibles+=("${parts[7]}")
|
|
port_labels+=("${parts[8]}")
|
|
port_url_paths+=("${parts[9]:-}")
|
|
port_subdomains+=("${parts[10]:-}")
|
|
# Recommended defaults to the webui flag when the column isn't present —
|
|
# matches the panel's expectation that webui ports are primary by default.
|
|
if [[ ${#parts[@]} -ge 12 ]]; then
|
|
port_recommendeds+=("${parts[11]}")
|
|
else
|
|
port_recommendeds+=("${parts[7]}")
|
|
fi
|
|
else
|
|
# The 8- and 7-column legacy shapes used to be accepted here.
|
|
# They do not merely omit trailing fields -- they SHIFT every
|
|
# position (8-col has no login column, 7-col has no parent
|
|
# either), so whenever the length was misread every field after
|
|
# the shift silently took its neighbour's value. That is exactly
|
|
# the class of fault this parser was just fixed for, and nothing
|
|
# ships in those shapes any more: all 74 descriptors in the
|
|
# catalogue, and every one on a live install, carry nine or more.
|
|
#
|
|
# So refuse and say which row, rather than guess a layout and
|
|
# hand back a port whose access or protocol came from the wrong
|
|
# column. A skipped row is visible; a mis-parsed one is not.
|
|
isNotice "Port config $port_config_var has ${#parts[@]} column(s); at least 9 are required (parent|name|ext:int|access|proto|login|traefik|webui|label). Skipping this port."
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Default Empty config options
|
|
if [ "$authelia_setup" == "" ]; then
|
|
authelia_setup=false
|
|
fi
|
|
if [ "$headscale_setup" == "" ]; then
|
|
headscale_setup=false
|
|
fi
|
|
if [ "$whitelist" == "" ]; then
|
|
whitelist=false
|
|
fi
|
|
if [ "$healthcheck" == "" ]; then
|
|
healthcheck=true
|
|
fi
|
|
|
|
# No domain configured -> no Traefik, whatever the per-port column says.
|
|
# LibrePortal is LAN/VPN-first: a box with CFG_DOMAIN_<n> unset must still
|
|
# serve every app on http://<ip>:<port>. Left alone, a traefik=true port with
|
|
# an empty $domain_full stamps Host(`<app>.`) — a trailing-dot host that
|
|
# matches nothing — and drags APP_URL to https://<app>. with it, breaking any
|
|
# app that builds its links from APP_URL (Bookstack, Nextcloud, Mastodon).
|
|
# Forcing the column false makes tagsProcessorPortRouterBlocks comment the
|
|
# router out entirely and lets tagsProcessorAppUrl fall through to the
|
|
# http://<ip>:<port> branch. The published host port is unaffected — access
|
|
# type, not the traefik flag, decides whether a port is allocated.
|
|
if [[ -z "$domain_full" ]]; then
|
|
local _nd
|
|
for _nd in "${!port_traefik_managed[@]}"; do
|
|
port_traefik_managed[$_nd]="false"
|
|
done
|
|
fi
|
|
|
|
# $public is derived from the port config — true iff any of this app's
|
|
# PORT_<n> rows have field 7 (traefik) == "true". Replaces the legacy
|
|
# CFG_<APP>_PUBLIC field, which was redundant with the per-port flag.
|
|
public="false"
|
|
local _t
|
|
for _t in "${port_traefik_managed[@]}"; do
|
|
[[ "$_t" == "true" ]] && { public="true"; break; }
|
|
done
|
|
|
|
# Primary/canonical host for this app: its first recommended Traefik port,
|
|
# else its first Traefik port. Feeds the legacy single DOMAINSUBNAME_DATA
|
|
# (app env vars), the app URL, and trusted-domains. Mirrors the per-port
|
|
# host rule (@/root -> apex, set -> sub.domain, empty -> app-name).
|
|
host_setup="${app_name}.${domain_full}"
|
|
local _i _primary=-1
|
|
for ((_i = 0; _i < ${#port_service_names[@]}; _i++)); do
|
|
[[ "${port_traefik_managed[$_i]}" == "true" ]] || continue
|
|
if [[ "${port_recommendeds[$_i]}" == "true" ]]; then _primary=$_i; break; fi
|
|
[[ $_primary -lt 0 ]] && _primary=$_i
|
|
done
|
|
if [[ $_primary -ge 0 ]]; then
|
|
local _sub="${port_subdomains[$_primary]}"
|
|
if [[ "$_sub" == "@" || "$_sub" == "root" ]]; then
|
|
host_setup="${domain_full}"
|
|
elif [[ -n "$_sub" ]]; then
|
|
host_setup="${_sub}.${domain_full}"
|
|
fi
|
|
fi
|
|
# Every branch above suffixes $domain_full, so with no domain configured they
|
|
# all yield a bare trailing dot ("bookstack."). That string is not a host, and
|
|
# it reaches DOMAINSUBNAME_TAG and the trusted-domains list regardless of the
|
|
# Traefik flag — so blank it rather than let a non-resolving name ship. Both
|
|
# consumers already treat empty as "no canonical host".
|
|
[[ -z "$domain_full" ]] && host_setup=""
|
|
}
|