#!/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_ unset must still # serve every app on http://:. Left alone, a traefik=true port with # an empty $domain_full stamps Host(`.`) — a trailing-dot host that # matches nothing — and drags APP_URL to https://. 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://: 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_ rows have field 7 (traefik) == "true". Replaces the legacy # CFG__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="" }