#!/bin/sh
# #
#   @app                ConfigServer Security & Firewall (CSF)
#                       Login Failure Daemon (LFD)
#   @website            https://configserver.dev
#   @docs               https://docs.configserver.dev
#   @download           https://download.configserver.dev
#   @repo               https://github.com/Aetherinox/csf-firewall
#   @copyright          Copyright (C) 2025-2026 Aetherinox
#                       Copyright (C) 2006-2025 Jonathan Michaelson
#                       Copyright (C) 2006-2025 Way to the Web Ltd.
#   @license            GPLv3
#   @updated            02.12.2026
#   
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 3 of the License, or (at
#   your option) any later version.
#   
#   This program is distributed in the hope that it will be useful, but
#   WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
#   General Public License for more details.
#   
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, see <https://www.gnu.org/licenses>.
# #

# #
#   Global variables
#       must remain POSIX compatible
# #

# set -eu

# #
#   Directory where this script lives
# #

OLDPWD=$(pwd)                                       # save current working directory
cd "$(dirname "$0")" || exit 1                      # change to the dir where the script resides
SCRIPT_DIR=$(pwd)                                   # get absolute path
cd "$OLDPWD" || exit 1                              # restore previous working directory

# #
#   Define › General
# #

APP_NAME="ConfigServer Security & Firewall"
APP_NAME_SHORT="CSF"
APP_DESC="Robust linux iptables/nftables firewall"
APP_REPO="https://github.com/aetherinox/csf-firewall"
APP_LINK_DOCS="https://docs.configserver.dev"
APP_LINK_DOWNLOAD="https://download.configserver.dev"
APP_LINK_DISCORD="https://discord.configserver.dev"
FILE_INSTALL_TXT="install.txt"

# #
#   Define › Files & Dirs
# #

CSF_ETC="/etc/csf"
CSF_VAR="/var/lib/csf"
CSF_USR="/usr/local/csf"
CSF_BIN="/usr/local/csf/bin"
CSF_TPL="/usr/local/csf/tpl"
CSF_CONF="/etc/csf/csf.conf"
CSF_CHOWN_GENERAL="root:root"
CSF_WEBMIN_SHARE_HOME="/usr/share/webmin"
CSF_WEBMIN_SHARE_DEST="${CSF_WEBMIN_SHARE_HOME}/csf"
CSF_WEBMIN_LIBEXEC_HOME="/usr/libexec/webmin"
CSF_WEBMIN_LIBEXEC_DEST="${CSF_WEBMIN_LIBEXEC_HOME}/csf"
CSF_WEBMIN_TARBALL="/usr/local/csf/csfwebmin.tgz"
CSF_WEBMIN_SYMBOLIC="${CSF_ETC}/csfwebmin.tgz"
CSF_WEBMIN_SRC="webmin"
CSF_WEBMIN_ETC="/etc/webmin"
CSF_WEBMIN_FILE_ACL="${CSF_WEBMIN_ETC}/webmin.acl"
CSF_WEBMIN_ACL_USER="root"
CSF_WEBMIN_ACL_MODULE="csf"
CSF_CWP_FOLD_SRC="csf"
CSF_CWP_PATH_DESIGN="/usr/local/cwpsrv/htdocs/admin/design/csf"
CSF_CRON_CSGET_SRC="csget.pl"
CSF_CRON_CSGET_DEST="/etc/cron.daily/csget"
CSF_CRON_CSGET_LOG="/var/log/csf/csget_daemon.log"
CSF_AUTO_GENERIC="auto.generic.pl"
CSF_AUTO_CWP="auto.cwp.pl"
CSF_AUTO_VESTA="auto.vesta.pl"
CSF_AUTO_CYBERPANEL="auto.cyberpanel.pl"
CSF_AUTO_DIRECTADMIN="auto.directadmin.pl"
CSF_AUTO_INTERWORX="auto.interworx.pl"
CSF_AUTO_CPANEL="auto.pl"

# #
#   Define › Server
# #

SERVER_HOST=$(hostname -f 2>/dev/null || hostname)

# #
#   Define › Files
# #

app_file_this=$(basename "$0")                                          # global.sh         (with ext)
app_file_bin="${app_file_this%.*}"                                      # global            (without ext)

# #
#   Define › Folders
# #

app_dir_this="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)"           # path where script was last found in
app_dir_this_usr="${PWD}"                                               # path where script is called from

# #
#   Define › Current version
# #

VERSION_FILE="$SCRIPT_DIR/version.txt"

# #
#   Extract ver from version.txt; fallback 'unknown'
# #

APP_VERSION=$( [ -f "$VERSION_FILE" ] && grep -v '^[[:space:]]*$' "$VERSION_FILE" | sed -n '1s/^[[:space:]]*//;s/[[:space:]]*$//p' || true )
: "${APP_VERSION:=unknown}"

# #
#   Define › Icons
# #

icoSheckmark='✔'   # ✔ $'\u2714'
icoXmark='✗'       # ❌ $'\u274C'

# #
#   Define › Colors
#   
#   Use the color table at:
#       - https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
# #

esc=$(printf '\033')
end="${esc}[0m"
bgEnd="${esc}[49m"
fgEnd="${esc}[39m"
bold="${esc}[1m"
dim="${esc}[2m"
underline="${esc}[4m"
blink="${esc}[5m"
white="${esc}[97m"
black="${esc}[0;30m"
redl="${esc}[0;91m"
redd="${esc}[38;5;196m"
magental="${esc}[38;5;197m"
magentad="${esc}[38;5;161m"
fuchsial="${esc}[38;5;206m"
fuchsiad="${esc}[38;5;199m"
bluel="${esc}[38;5;33m"
blued="${esc}[38;5;27m"
greenl="${esc}[38;5;47m"
greend="${esc}[38;5;35m"
orangel="${esc}[38;5;208m"
oranged="${esc}[38;5;202m"
yellowl="${esc}[38;5;226m"
yellowd="${esc}[38;5;214m"
greyl="${esc}[38;5;250m"
greym="${esc}[38;5;244m"
greyd="${esc}[38;5;240m"
navy="${esc}[38;5;62m"
olive="${esc}[38;5;144m"
peach="${esc}[38;5;204m"
cyan="${esc}[38;5;6m"
bgVerbose="${esc}[1;38;5;15;48;5;125m"
bgDebug="${esc}[1;38;5;15;48;5;237m"
bgInfo="${esc}[1;38;5;15;48;5;27m"
bgOk="${esc}[1;38;5;15;48;5;64m"
bgWarn="${esc}[1;38;5;16;48;5;214m"
bgDanger="${esc}[1;38;5;15;48;5;202m"
bgError="${esc}[1;38;5;15;48;5;160m"

# #
#   Define › Logging functions
#   
#   verbose "This is an verbose message"
#   debug "This is an debug message"
#   info "This is an info message"
#   ok "This is an ok message"
#   warn "This is a warn message"
#   danger "This is a danger message"
#   error "This is an error message"
# #

verbose( )
{
    case "${argVerbose:-0}" in
        1|true|TRUE|yes|YES)
            printf '\033[0m\r%-42s %-65s\n' "   ${bgVerbose} VRBO ${end}" "${greym} $1 ${end}"
            ;;
    esac
}

debug( )
{
    if [ "$argDevEnabled" = "true" ] || [ "$argDryrun" = "true" ]; then
        printf '\033[0m\r%-42s %-65s\n' "   ${bgDebug} DBUG ${end}" "${greym} $1 ${end}"
    fi
}

info( )
{
    printf '\033[0m\r%-41s %-65s\n' "   ${bgInfo} INFO ${end}" "${greym} $1 ${end}"
}

ok( )
{
    printf '\033[0m\r%-41s %-65s\n' "   ${bgOk} PASS ${end}" "${greym} $1 ${end}"
}

warn( )
{
    printf '\033[0m\r%-42s %-65s\n' "   ${bgWarn} WARN ${end}" "${greym} $1 ${end}"
}

danger( )
{
    printf '\033[0m\r%-42s %-65s\n' "   ${bgDanger} DNGR ${end}" "${greym} $1 ${end}"
}

error( )
{
    printf '\033[0m\r%-42s %-65s\n' "   ${bgError} FAIL ${end}" "${greym} $1 ${end}"
}

label( )
{
    printf '\033[0m\r%-31s %-65s\n' "   ${greyd}        ${end}" "${greyd} $1 ${end}"
}

print( )
{
    echo "${greym}$1${end}"
}

# #
#   truncate text; add ...
#   
#   @usage
#       truncate "This is a long string" 10 "..."
# #

truncate()
{
    text=$1
    maxlen=$2
    suffix=${3:-}

    len=$(printf %s "${text}" | wc -c | tr -d '[:space:]')

    if [ "${len}" -gt "${maxlen}" ]; then
        printf '%s%s\n' "$(printf %s "${text}" | cut -c1-"${maxlen}")" "${suffix}"
    else
        printf '%s\n' "${text}"
    fi
}

# #
#   Print › Demo Notifications
#   
#   Outputs a list of example notifications
#   
#   @usage          demoNoti
# #

demoNoti()
{
    verbose "This is an verbose message"
    debug "This is an debug message"
    info "This is an info message"
    ok "This is an ok message"
    warn "This is a warn message"
    danger "This is a danger message"
    error "This is an error message"
}

# #
#   Print › Line
#   
#   Prints single line horizontal line, no text
#   
#   @usage          prin0
# #

prin0()
{
    _p0_indent="  "
    _p0_box_width=110
    _p0_line_width=$(( _p0_box_width + 2 ))

    _p0_line=""
    i=1
    while [ "$i" -le "${_p0_line_width}" ]; do
        _p0_line="${_p0_line}─"
        i=$(( i + 1 ))
    done

    printf '\n'
    printf "%b%s%s%b\n" "${greyd}" "${_p0_indent}" "${_p0_line}" "${reset}"
    printf '\n'

    unset _p0_indent _p0_box_width _p0_line_width _p0_line i
}

# #
#   Print › Box › Single
#   
#   Prints single line with a box surrounding it.
#   
#   @usage          prinb "${APP_NAME_SHORT:-CSF} › Customize csf.config"
# #

prinb()
{
    _prinb_title="$*"
    _prinb_indent="   "                                                         # Left padding
    _prinb_padding=6                                                            # Extra horizontal space around text
    _prinb_title_length=${#_prinb_title}
    _prinb_inner_width=$(( _prinb_title_length + _prinb_padding ))
    _prinb_box_width=110

    # Minimum width for aesthetics
    if [ "$_prinb_inner_width" -lt "$_prinb_box_width" ]; then
        _prinb_inner_width=$_prinb_box_width
    fi

    # Horizontal border
    _prinb_line=""
    i=1
    while [ "$i" -le "$_prinb_inner_width" ]; do
        _prinb_line="${_prinb_line}─"
        i=$(( i + 1 ))
    done

    # Draw box
    printf '\n'
    printf '\n'
    printf "%b%s┌%s┐\n" "${greym}" "$_prinb_indent" "$_prinb_line"
    printf "%b%s│  %-${_prinb_inner_width}s \n" "${greym}" "$_prinb_indent" "$_prinb_title"
    printf "%b%s└%s┘%b\n" "${greym}" "$_prinb_indent" "$_prinb_line" "${reset}"
    printf '\n'

    unset _prinb_title _prinb_indent _prinb_padding \
          _prinb_title_length _prinb_inner_width _prinb_box_width \
          _prinb_line i
}

# #
#   Print › Box › Paragraph
#   
#   Places an ASCII box around text. Supports multi-lines with \n.
#   
#   Determines the character count if color codes are used and ensures that the box borders are aligned properly.
#   
#   If using emojis; adjust the spacing so that the far-right line will align with the rest. Add the number of spaces
#   to increase the value, which is represented with a number enclosed in square brackets.
#     [1]           add 1 space to the right.
#     [2]           add 2 spaces to the right.
#     [-1]          remove 1 space to the right (needed for some emojis depending on if the emoji is 1 or 2 bytes)
#   
#   @usage          prinp "Certificate Generation Successful" "Your new certificate and keys have been generated successfully.\n\nYou can find them in the ${greenl}${app_dir_output}${greyd} folder."
#                   prinp "🎗️[1]  ${file_domain_base}" "The following description will show on multiple lines with a ASCII box around it."
#                   prinp "📄[-1] File Overview" "The following list outlines the files that you have generated using this utility, and what certs/keys may be missing."
#                   prinp "➡️[15]  ${bluel}Paths${end}"
# #

prinp()
{
    local title="$1"
    shift
    local text="$*"

    local indent="  "
    local box_width=110
    local pad=1

    local content_width=$(( box_width ))
    local inner_width=$(( box_width - pad*2 ))

    print
    print

    local hline
    hline=$(printf '─%.0s' $(seq 1 "$content_width"))

    printf "${greyd}%s┌%s┐\n" "$indent" "$hline"

    # #
    #   Title
    #   
    #   Extract optional [N] adjustment from title (signed integer), portably
    # #

    local emoji_adjust=0
    local display_title="$title"

    # #
    #   Get content inside first [...] (if present)
    # #

    if printf '%s\n' "$title" | grep -q '\[[[:space:]]*[-0-9][-0-9[:space:]]*\]'; then

        # #
        #   Extract numeric inside brackets (allow optional leading -)
        #   - use sed to capture first bracketed token, then strip non-digit except leading -
        # #

        local bracket
        bracket=$(printf '%s' "$title" | sed -n 's/.*\[\([-0-9][-0-9]*\)\].*/\1/p')

        # #
        #   Validate numeric and assign, otherwise fallback to 0
        # #
    
        if printf '%s\n' "$bracket" | grep -qE '^-?[0-9]+$'; then
            emoji_adjust=$bracket
        else
            emoji_adjust=0
        fi

        # #
        #   Remove the first [...] token from the display_title
        # #
    
        display_title=$(printf '%s' "$title" | sed 's/\[[^]]*\]//')
    fi

    # #
    #   Sanity: ensure emoji_adjust is a decimal integer so math works
    # #

    case "$emoji_adjust" in
        ''|*[!0-9-]*)
            emoji_adjust=0
            ;;
    esac

    local title_width=$(( content_width - pad ))

    # #
    #   Account for emoji adjustment in visible length calculation
    # #
  
    local title_vis_len=$(( ${#display_title} - emoji_adjust ))
    printf "${greyd}%s│%*s${bluel}%s${greyd}%*s│\n" \
        "$indent" "$pad" "" "$display_title" "$(( title_width - title_vis_len ))" ""

    # #
    #   Only render body text if provided
    # #

    if [ -n "$text" ]; then
        printf "${greyd}%s│%-${content_width}s│\n" "$indent" ""

        # #
        #   Convert literal \n to real newlines
        # #

        text=$(printf "%b" "$text")

        # #
        #   Handle each line with ANSI-aware wrapping and true padding
        # #

        printf "%s" "$text" | while IFS= read -r line || [ -n "$line" ]; do

        # #
        #   Blank line
        # #
    
        if [ -z "$line" ]; then
            printf "${greyd}%s│%-*s│\n" "$indent" "$content_width" ""
            continue
        fi

        local out="" word
        for word in $line; do
            # #
            #   Strip ANSI for visible width
            # #
        
            local vis_out vis_len vis_word
            vis_out=$(printf "%s" "$out" | sed 's/\x1B\[[0-9;]*[A-Za-z]//g')
            vis_word=$(printf "%s" "$word" | sed 's/\x1B\[[0-9;]*[A-Za-z]//g')
            vis_len=$(( ${#vis_out} + ( ${#vis_out} > 0 ? 1 : 0 ) + ${#vis_word} ))

            if [ -z "$out" ]; then
                out="$word"
            elif [ $vis_len -le $inner_width ]; then
                out="$out $word"
            else
                # #
                #   Print and pad manually based on visible length
                # #

                local vis_len_full
                vis_len_full=$(printf "%s" "$out" | sed 's/\x1B\[[0-9;]*[A-Za-z]//g' | wc -c | tr -d ' ')
                local pad_spaces=$(( inner_width - vis_len_full ))
                [ $pad_spaces -lt 0 ] && pad_spaces=0
                printf "${greyd}%s│%*s%s%*s│\n" "$indent" "$pad" "" "$out" "$(( pad + pad_spaces ))" ""
                out="$word"
            fi
        done

        # #
        #   Final flush line
        # #
    
        if [ -n "$out" ]; then
            local vis_len_full pad_spaces
            vis_len_full=$(printf "%s" "$out" | sed 's/\x1B\[[0-9;]*[A-Za-z]//g' | wc -c | tr -d ' ')
            pad_spaces=$(( inner_width - vis_len_full ))
            [ $pad_spaces -lt 0 ] && pad_spaces=0
            printf "${greyd}%s│%*s%s%*s│\n" "$indent" "$pad" "" "$out" "$(( pad + pad_spaces ))" ""
        fi

        done
    fi

    printf "${greyd}%s└%s┘${reset}\n" "$indent" "$hline"
    print
}

# #
#   Define › Logging › Verbose
# #

log()
{
    case "${argVerbose:-0}" in
        1|true|TRUE|yes|YES)
            verbose "$@"
            ;;
    esac
}

# #
#   Check Sudo
# #

check_sudo( )
{
    if [ "$(id -u)" != "0" ]; then
        error "    ❌ Must run script with ${redl}sudo"
        exit 1
    fi
}

# #
#   Run Command
#   
#   Added when dryrun mode was added to the install.sh.
#   Allows for a critical command to be skipped if in --dryrun mode.
#       Throws a debug message instead of executing.
#   
#   argDryrun comes from global export in csf/install.sh
#   
#   @usage          run /sbin/chkconfig csf off
#                   run echo "ConfigServer"
#                   run chmod -v 700 "./${CSF_AUTO_GENERIC}"
# #

run()
{
    if [ "${argDryrun}" = "true" ]; then
        debug "    Drymode (skip): $*"
    else
        debug "    Run: $*"
        "$@"
    fi
}

# #
#   Copy If Missing
#   Copies a src file to dest only if missing
#   
#   @arg            src                         File to copy
#   @arg            dest                        Where to copy file
#   @usage			copy_if_missing "install.cpanel.sh" "csf cPanel installer"
#   @todo           deprecate in favor of func 'copi'
# #

copy_if_missing( )
{
    src="$1"
    dest="$2"

    if [ ! -e "${dest}" ]; then
        if cp -avf "${src}" "${dest}"; then
            ok "    Copied ${greenl}${src}${greym} to ${greenl}${dest}${greym} "
        else
            error "    ❌ Cannot copy ${redl}${src}${greym} to ${redl}${dest}${greym}"
            exit 1
        fi
    else
        status "    Already existing copy ${bluel}${src}${greym} to ${bluel}${dest}${greym}"
    fi
}

# #
#   Special copy: copy to dest or dest.new if dest exists
#   
#   @todo           deprecate in favor of func 'copi'
# #

copy_or_new( )
{
    src="$1"
    dest="$2"

    if [ ! -e "${dest}" ]; then
        if cp -avf "${src}" "${dest}"; then
            ok "    Copied ${greenl}${src}${greym} to ${greenl}${dest}${greym} "
        else
            error "    ❌ Cannot copy ${redl}${src}${greym} to ${redl}${dest}${greym}"
            exit 1
        fi
    else
        if cp -avf "${src}" "${dest}.new"; then
            ok "    Copied ${greenl}${src}${greym} to ${greenl}${dest}.new${greym} (destination already existed) "
        else
            error "    ❌ Cannot copy ${redl}${src}${greym} to ${redl}$des.newt${greym}"
            exit 1
        fi
    fi
}

# #
#   Copy
#   
#   Custom copy function to replace 'cp'; handles copy a bit differently but with the
#   same behavior.
#   
#   @desc           › Auto detects file, symlink, or directory copying
#                   › Preserve permissions & timestamps (uses `cp -p`)
#                   › Supports copying into directories
#                   › Supports copying to directory paths ending in '/' or '/.'
#                   › Supports copying entire directory trees (recursive)
#                   › Returns non-zero on error (never partially copies silently)
#   
#   @usage          › Copy file into existing directory:
#                       copi 'csfpre.sh' '/usr/local/csf/bin/'
#   
#                   › Copy file and force 'directory intent' using trailing slash:
#                       copi 'csfpre.sh' '/usr/local/csf/bin/'
#                       copi 'csfpre.sh' '/usr/local/csf/bin/.'
#   
#                   › Copy file to specific destination filename:
#                       copi 'config.txt' '/etc/csf/config.txt'
#   
#                   › Copy directory (full tree) to a new location:
#                       copi './tpl' '/usr/local/csf/tpl'
#   
#                   › Copy directory into another directory:
#                       copi './webmin' '/var/lib/csf/'
#   
#                   › Error handling:
#                       copi '/missing/badfile' '/tmp/'             # returns `3` (source missing)
#                       copi '/etc/passwd' '/root/badfolder'        # returns `1` or `2` depending on mkdir/cp errors
#   
#   @return codes   0   Success
#                   1   Failed to create destination directories
#                   2   Failed file copy
#                   3   Source does not exist / unsupported type
#   
#   @motes          - `/path/to/dir/`       treated as directory
#                   - `/path/to/dir/.`      treated as directory
#                   - Destination directories auto-created when required
#                   - Directory copies preserve directory structure and timestamps
# #

copi()
{
    src="$1"
    dest="$2"

    # #
    #   Source must exist
    # #

    if [ ! -e "${src}" ]; then
        warn "    No such file or directory ${yellowl}${src}${greym}"
        return 3
    fi

    # #
    #   File || symlink copy mode
    # #

    if [ -f "${src}" ] || [ -L "${src}" ]; then

        # #
        #   If dest is an existing dir; copy into it
        # #
    
        if [ -d "${dest}" ]; then
            fname=$(basename "${src}")
            label "     Copying ${navy}${fname}${greym} › ${navy}${dest}${greym}"
            cp -p "${src}" "${dest}/$fname" || return 2
            return 0
        fi

        # #
        #   Handle directory intent:
        #       ends in '/' OR ends in '/.'
        # #

        case "${dest}" in
            */ | */.)
                # Normalize "/." → "/"
                d="${dest%/.}"

                mkdir -p "${d}" || return 1
                fname=$(basename "${src}")
                label "     Copying ${navy}${fname}${greym} › ${navy}${d}${greym}"
                cp -p "${src}" "${d}/${fname}" || return 2
                return 0
                ;;
        esac

        # #
        #   Otherwise treat dest as a file path
        # #
    
        label "     Copying ${navy}${src}${greym} › ${navy}${dest}${greym}"
        cp -p "${src}" "${dest}" || return 2
        return 0
    fi

    # #
    #   Directory copy mode
    # #

    if [ -d "${src}" ]; then
        mkdir -p "${dest}" || return 1

        (
            cd "${src}" || exit 1

            # #
            #   Recreate directory tree
            # #

            find . -type d -print | while IFS= read -r d; do
                mkdir -p "${dest}/${d}" || { error "    Failed mkdir ${redl}${dest}/${d}"; exit 1; }
            done

            # #
            #   Copy files
            # #

            find . -type f -print | while IFS= read -r f; do
                label "     Copying ${navy}${f}${greym} › ${navy}${dest}${greym}"
                mkdir -p "${dest}/$(dirname "${f}")"
                cp -p "${f}" "${dest}/${f}" || { error "    Failed copying ${redl}${f}"; exit 1; }
            done
        )
        return $?
    fi

    # #
    #   Unknown type
    # #

    warn "    No such file or directory ${yellowl}${src}${greym}"
    return 3
}

# #
#   Get UI bind address, and credentials from CSF config
# #

get_csf_ui_info()
{
    if [ ! -f "${CSF_CONF}" ]; then
        return 1
    fi

    UI=""
    UI_PORT=""
    UI_IP=""
    UI_USER=""
    UI_PASS=""

    # Extract config data using awk
    awk '
        /^[[:space:]]*UI[[:space:]]*=/ { gsub(/"/,"",$3); print "UI="$3 }
        /^[[:space:]]*UI_PORT[[:space:]]*=/ { gsub(/"/,"",$3); print "PORT="$3 }
        /^[[:space:]]*UI_IP[[:space:]]*=/ { gsub(/"/,"",$3); print "IP="$3 }
        /^[[:space:]]*UI_USER[[:space:]]*=/ { gsub(/"/,"",$3); print "USER="$3 }
        /^[[:space:]]*UI_PASS[[:space:]]*=/ { gsub(/"/,"",$3); print "PASS="$3 }
    ' "${CSF_CONF}" > /tmp/csf_ui_values.$$ 

    # Read values in current shell
    while IFS='=' read -r key val
    do
        case "${key}" in
            UI)   UI="${val}" ;;
            PORT) UI_PORT="${val}" ;;
            IP)   UI_IP="${val}" ;;
            USER) UI_USER="${val}" ;;
            PASS) UI_PASS="${val}" ;;
        esac
    done < /tmp/csf_ui_values.$$

    rm -f /tmp/csf_ui_values.$$

    # Disabled; return empty
    if [ "${UI}" != "1" ]; then
        printf '%s\n' ""
        return 0
    fi

    # Fallbacks
    [ -z "${UI_IP}" ] && UI_IP="127.0.0.1"
    [ -z "${UI_PORT}" ] && UI_PORT="6666"

    # Output: "IP:PORT USER PASS"
    printf '%s:%s %s %s\n' "${UI_IP}" "${UI_PORT}" "${UI_USER}" "${UI_PASS}"
}
