Ctrl K
ComunidadGuías…

hshqlib-dev.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash
HSHQ_LIB_SCRIPT_VERSION=250
LOG_LEVEL=info

# Copyright (C) 2023 HomeServerHQ <drdoug@homeserverhq.com>
#
# 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 <http://www.gnu.org/licenses/>.

set -e

function init()
{
  # Version 74 Update
  # Changing to the home directory fixes issues with docker compose (v2)
  # since the command 'stat .' (inside of docker compose) results in a
  # permission denied error when running script from root as another user,
  # i.e. 'sudo -u <USER> bash hshq.sh'
  # Basically, docker compose is dependent on which directory it is
  # executed from, regardless if a specified file is in a readable directory.
  cd ~
  IS_STACK_DEBUG=false
  USERNAME=$(id -u -n)
  PRIOR_HSHQ_VERSION=0
  LAST_RELAYSERVER_VERSION_UPDATE=239
  IS_AUTO_INSTALL=false
  HSHQ_INSTALL_VARIANT=base
  loadVersionVars
  IS_RUST_UTILS=false
  if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26\.04. ]]; then
    IS_RUST_UTILS=true
  fi
  IS_DESKTOP_ENV=false
  if [ -d $HOME/Desktop ]; then
    IS_DESKTOP_ENV=true
  fi
  IS_CONSOLE_ENV=false
  # Leave this set to true for a bit, to allow for users to update.
  # Eventually, we'll turn it back off and only restart as needed.
  IS_RESTART_SCRIPTSERVER=true
  LOCK_UTILS_FILENAME=lockUtils.sh
  LOCKHOLDER_FILENAME=lastCaller
  ALL_LOCKS="hshqopen networkchecks"
  CONFIG_FILE_DEFAULT_FILENAME=config.cnf
  ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME=config.enc
  ENCRYPTED_CONFIG_FILE_BACKUP_FILENAME=config-enc.bak
  ENC_PW_FILE=/tmp/encpw.enc
  TMP_PW_CHECKDIR=/tmp/pwcheckdir
  IS_CONFIG_FILE_UNENCRYPTED=false
  RS_INSTALL_SETUP_SCRIPT_NAME=setupRS.sh
  RS_INSTALL_FRESH_SCRIPT_NAME=installRS.sh
  RS_INSTALL_TRANSFER_SCRIPT_NAME=transferRS.sh
  RS_INSTALL_VALIDATION_SCRIPT_NAME=validateRS.sh
  RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME=validateLib.sh
  RS_UPDATE_UTILS_SCRIPT_NAME=rsUtilsScript.sh
  RS_UPDATE_SCRIPT_NAME=rsUpdateScript.sh
  NUKE_SCRIPT_NAME=nuke.sh
  RELAYSERVER_HSHQ_FULL_LOG_NAME=hshqRSInstall.log
  RELAYSERVER_HSHQ_TIMESTAMP_LOG_NAME=hshqRSInstallTS.log
  RELAYSERVER_NOT_READY_FILE=notready
  RELAYSERVER_SCRIPTS_UPLOADED_FILE=uploaded
  RELAYSERVER_INSTALL_COMPLETE_FILE=installed
  RELAYSERVER_INSTALL_FAILED_FILE=failed
  RELAYSERVER_SWAP_SIZE=2G
  HOMESERVER_SWAP_SIZE=16G
  HSHQ_LOG_FILE=/var/log/hshq.log
  HSHQ_INSTALL_NOTES_FILENAME='HomeServer_Install_Notes.txt'
  SCRIPTSERVER_FULL_STACKLIST_FILENAME=fullStackList.txt
  SCRIPTSERVER_OPTIONAL_STACKLIST_FILENAME=optionalStackList.txt
  SCRIPTSERVER_UPDATE_STACKLIST_FILENAME=updateStackList.txt
  SCRIPTSERVER_REDIS_STACKLIST_FILENAME=redisStackList.txt
  ALLDBS_IMPORT_FILENAME=DBCImport.txt
  DNS_ZONE_FILENAME_BASE=DNSZoneInfo
  DOCKER_VERSION_UBUNTU_2204=5:29.2.1-1~ubuntu.22.04~jammy
  DOCKER_VERSION_UBUNTU_2404=5:29.4.0-1~ubuntu.24.04~noble
  DOCKER_VERSION_UBUNTU_2604=5:29.4.0-1~ubuntu.24.04~noble
  DOCKER_VERSION_DEBIAN_12=5:29.1.4-1~debian.12~bookworm
  DOCKER_VERSION_CHECK_UBUNTU_2204=29.2.1
  DOCKER_VERSION_CHECK_UBUNTU_2404=29.4.0
  DOCKER_VERSION_CHECK_UBUNTU_2604=29.4.0
  DOCKER_VERSION_CHECK_DEBIAN_12=29.1.4
  NET_PRIVATEIP_BRIDGE_NAME=brdockpriv
  NET_PRIVATEIP_SUBNET=172.16.1.0/24
  NET_PRIVATEIP_SUBNET_PREFIX=172.16.1
  NET_MAILU_EXT_BRIDGE_NAME=brmailuext
  NET_MAILU_EXT_SUBNET=172.16.2.0/24
  NET_MAILU_EXT_SUBNET_PREFIX=172.16.2
  NET_MAILU_INT_BRIDGE_NAME=brmailuint
  NET_MAILU_INT_SUBNET=172.16.3.0/24
  NET_MAILU_INT_SUBNET_PREFIX=172.16.3
  NET_WEBPROXY_BRIDGE_NAME=brdockproxy
  NET_WEBPROXY_SUBNET=172.16.4.0/22
  NET_WEBPROXY_SUBNET_PREFIX=172.16.4
  NET_EXTERNAL_BRIDGE_NAME=brdockext
  NET_EXTERNAL_SUBNET=172.16.8.0/21
  NET_EXTERNAL_SUBNET_PREFIX=172.16.8
  NET_INTERNALMAIL_BRIDGE_NAME=brdockmail
  NET_INTERNALMAIL_SUBNET=172.16.16.0/22
  NET_INTERNALMAIL_SUBNET_PREFIX=172.16.16
  NET_DBS_BRIDGE_NAME=brdockdbs
  NET_DBS_SUBNET=172.16.20.0/22
  NET_DBS_SUBNET_PREFIX=172.16.20
  NET_LDAP_BRIDGE_NAME=brdockldap
  NET_LDAP_SUBNET=172.16.24.0/22
  NET_LDAP_SUBNET_PREFIX=172.16.24
  NET_AIPRIVATE_SUBNET=172.16.28.0/22
  NET_AIPRIVATE_SUBNET_PREFIX=172.16.28
  NET_AIPRIVATE_BRIDGE_NAME=brdockaipriv
  NET_AILIMITED_SUBNET=172.16.32.0/24
  NET_AILIMITED_SUBNET_PREFIX=172.16.32
  NET_AILIMITED_BRIDGE_NAME=brdockailim
  RELAYSERVER_NET_EXTERNAL_BRIDGE_NAME=brdockext
  RELAYSERVER_NET_EXTERNAL_SUBNET=172.16.1.0/24
  RELAYSERVER_NET_EXTERNAL_SUBNET_PREFIX=172.16.1
  RELAYSERVER_NET_WEBPROXY_BRIDGE_NAME=brdockproxy
  RELAYSERVER_NET_WEBPROXY_SUBNET=172.16.2.0/24
  RELAYSERVER_NET_WEBPROXY_SUBNET_PREFIX=172.16.2
  RELAYSERVER_MAIL_RELAY_BRIDGE_NAME=brdockmrelay
  RELAYSERVER_MAIL_RELAY_SUBNET=172.16.3.0/24
  RELAYSERVER_MAIL_RELAY_SUBNET_PREFIX=172.16.3
  RELAYSERVER_CLIENT_DNS_BRIDGE_NAME=brdockcdns
  RELAYSERVER_CLIENT_DNS_SUBNET=172.16.4.0/24
  RELAYSERVER_CLIENT_DNS_SUBNET_PREFIX=172.16.4
  ADGUARD_DNS_PORT=53
  CADDY_HTTP_PORT=80
  CADDY_HTTPS_PORT=443
  COTURN_PRIMARY_PORT=3478
  COTURN_SECONDARY_PORT=5349
  COTURN_COMMS_MIN_PORT=14100
  COTURN_COMMS_MAX_PORT=14200
  JELLYFIN_PORT=7359
  JITSI_COLIBRI_PORT=8020
  JITSI_JVB_PORT=4443
  JITSI_MEET_PORT=10000
  KHOJ_VNC_PORT=55901
  MAILU_PORT_1=25
  MAILU_PORT_2=110
  MAILU_PORT_3=143
  MAILU_PORT_4=465
  MAILU_PORT_5=587
  MAILU_PORT_6=993
  MAILU_PORT_7=995
  PEERTUBE_RDP_PORT=1935
  QBITTORRENT_PORT=6881
  SYNCTHING_DISC_PORT=21027
  SYNCTHING_SYNC_PORT=22000
  SYNCTHING_LOCAL_WEB_PORT=8384
  UPNP_PORT=1900
  VNC_SERVER_PORT=5901
  WAZUH_PORT_1=1514
  WAZUH_PORT_2=1515
  WAZUH_PORT_3=514
  WAZUH_PORT_4=55000
  WAZUH_PORT_5=9200
  WAZUH_AGENT_VERSION=4.14.7-1
  PAPERLESS_ADMIN_ID=2
  DEFAULT_UNFOUND_IP_ADDRESS=169.254.84.48
  DEFAULT_UNFOUND_IP_SUBNET=169.254.0.0/16
  MAX_DOCKER_PULL_TRIES=20
  MENU_WIDTH=69
  MENU_HEIGHT=24
  MENU_INT_HEIGHT=10
  ENABLE_STACK_DELETE=false
  SUDO_NORMAL_TIMEOUT=15
  SUDO_LONG_TIMEOUT=1440
  SUDO_LONG_TIMEOUT_FILENAME=sudohshqinstall
  SUDO_MAX_RETRIES=20
  MAILX_TIMEOUT=15
  NETPLAN_ORIGIN_HINT=88-hshq
  NETPLAN_APPLY_WAIT=5
  MIN_NETWORKCHECK_INTERVAL=5
  STACK_VERSION_PREFIX=#HSHQManaged
  HSHQ_ADMIN_NAME="HSHQ Admin"
  IS_HSHQ_DEV_FILENAME=hshq.dev
  IS_HSHQ_TEST_FILENAME=hshq.test
  IS_HSHQ_DEV_TEST=false
  if [ -f $HOME/hshq/$IS_HSHQ_DEV_FILENAME ] || [ -f $HOME/hshq/$IS_HSHQ_TEST_FILENAME ]; then
    IS_HSHQ_DEV_TEST=true
  fi
  HSHQ_WRAP_URL=https://homeserverhq.com/hshq.sh
  HSHQ_WRAP_VER_URL=https://homeserverhq.com/ver/getwrapversion
  HSHQ_WRAP_DEV_URL=https://homeserverhq.com/hshq-dev.sh
  HSHQ_WRAP_DEV_VER_URL=https://homeserverhq.com/ver/getwrapversion-dev
  HSHQ_WRAP_TEST_URL=https://homeserverhq.com/hshq-test.sh
  HSHQ_WRAP_TEST_VER_URL=https://homeserverhq.com/ver/getwrapversion-test
  HSHQ_LIB_URL=https://homeserverhq.com/hshqlib.sh
  HSHQ_LIB_VER_URL=https://homeserverhq.com/ver/getversion
  HSHQ_LIB_DEV_URL=https://homeserverhq.com/hshqlib-dev.sh
  HSHQ_LIB_DEV_VER_URL=https://homeserverhq.com/ver/getversion-dev
  HSHQ_LIB_TEST_URL=https://homeserverhq.com/hshqlib-test.sh
  HSHQ_LIB_TEST_VER_URL=https://homeserverhq.com/ver/getversion-test
  HSHQ_RELEASES_URL=https://homeserverhq.com/releases
  HSHQ_SIG_BASE_URL=https://homeserverhq.com/signatures/
  HSHQ_GPG_FINGERPRINT=5B9C33067C71ABCFCE1ACF8A7F46128ABB7C1E42
  HSHQ_GPG_FINGERPRINT_SHORT=7F46128ABB7C1E42
  HSHQ_LIB_TMP=$HOME/hshqlib.tmp
  HSHQ_WRAP_TMP=$HOME/hshqwrap.tmp
  HSHQ_LIB_FILENAME=hshqlib.sh
  HSHQ_NEW_LIB_FILENAME=hshqlib.new
  HSHQ_WRAP_FILENAME=hshq.sh
  HSHQ_ASSETS_GIT_REPO=https://github.com/homeserverhq/assets.git
  APPLICATION_FIRST_LINE="###################### Application Begin #######################"
  APPLICATION_LAST_LINE="####################### Application End ########################"
  INVITATION_FIRST_LINE="####################### Invitation Begin #######################"
  INVITATION_LAST_LINE="######################## Invitation End ########################"
  initServiceDefaults
  loadPinnedDockerImages
  loadDirectoryStructure
  CONFIG_FILE_DEFAULT_LOCATION=$HSHQ_CONFIG_DIR
  HSHQ_DB=$HSHQ_CONFIG_DIR/hshq.db
  HSHQ_INSTALL_CFG=$HSHQ_BASE_DIR/installConfig.txt
  HSHQ_CUSTOM_PRE_IPTABLES_SCRIPT=$HSHQ_SCRIPTS_DIR/root/userCustomPreIPTables.sh
  HSHQ_CUSTOM_POST_IPTABLES_SCRIPT=$HSHQ_SCRIPTS_DIR/root/userCustomPostIPTables.sh
  HSHQ_PLAINTEXT_ROOT_CONFIG=$HSHQ_CONFIG_DIR/ptRootConfig.conf
  HSHQ_PLAINTEXT_USER_CONFIG=$HSHQ_CONFIG_DIR/ptUserConfig.conf
  SYSTEM_STATE_FILENAME=$HSHQ_CONFIG_DIR/systemstate
  SS_INIT=init
  SS_INSTALLING=installing
  SS_RESTORING=restoring
  SS_BOOTING=booting
  SS_TRANSFERRING=transferring
  SS_UPDATING=updating
  SS_REMOVING=removing
  SS_RUNNING=running
  RESTORE_SCREEN_NAME=hshqRestore
  UTILS_LIST="wget|wget whiptail|whiptail awk|gawk screen|screen pwgen|pwgen argon2|argon2 dig|dnsutils htpasswd|apache2-utils ssh|ssh sshd|openssh-server sshpass|sshpass wg|wireguard-tools qrencode|qrencode openssl|openssl faketime|faketime bc|bc sipcalc|sipcalc jq|jq git|git http|httpie sqlite3|sqlite3 curl|curl sha1sum|coreutils lsb_release|lsb-release nano|nano cron|cron ping|iputils-ping route|net-tools grepcidr|grepcidr networkd-dispatcher|networkd-dispatcher certutil|libnss3-tools update-ca-certificates|ca-certificates gpg|gnupg python3|python3 pip3|python3-pip unzip|unzip hwinfo|hwinfo netplan|netplan.io netplan|openvswitch-switch uuidgen|uuid-runtime aa-enforce|apparmor-utils logrotate|logrotate yq|yq iwlist|wireless-tools sudo|sudo gcc|build-essential gio|libglib2.0-bin ffmpeg|ffmpeg php|php-cli gnupg2|gnupg2 apt-transport-https|apt-transport-https software-properties-common|software-properties-common python3-werkzeug|python3-werkzeug inotifywait|inotify-tools"
  DESKTOP_APT_LIST=""
  APT_REMOVE_LIST="xxd needrestart bsd-mailx"
  RELAYSERVER_UTILS_LIST="curl|curl awk|gawk whiptail|whiptail nano|nano screen|screen htpasswd|apache2-utils pwgen|pwgen git|git http|httpie jq|jq sqlite3|sqlite3 wg|wireguard-tools qrencode|qrencode route|net-tools sipcalc|sipcalc mailx|mailutils ipset|ipset uuidgen|uuid-runtime grepcidr|grepcidr networkd-dispatcher|networkd-dispatcher aa-enforce|apparmor-utils logrotate|logrotate"
  hshqlogo=$(cat << EOFLG
#===============================================================#
# ▀█  █  ▄▄  ▄▄ ▄▄ ▄▄▄ █▀▀▀█ ▄▄▄ ▄▄▄  ▄   ▄ ▄▄▄ ▄▄▄  █  █ █▀▀█  #
#  █▀▀█ █  █ █ █ █ ▄▄  ▀▀▀▄▄ ▄▄  ▄▄▄▀ ▀▄ ▄▀ ▄▄  ▄▄▄▀ █▀▀█ █  █  #
#  █  █ ▀▄▄▀ █   █ ▄▄▄ █▄▄▄█ ▄▄▄ ▄▄▄▄  ▀█▀  ▄▄▄ ▄▄▄▄ █  █ █▄▄█▄ #
#===============================================================#

EOFLG
  )
  hshqwarninglogo=$(cat << EOFLG
#===============================================================#
# ▀█  █  ▄▄  ▄▄ ▄▄ ▄▄▄ █▀▀▀█ ▄▄▄ ▄▄▄  ▄   ▄ ▄▄▄ ▄▄▄  █  █ █▀▀█  #
#  █▀▀█ █  █ █ █ █ ▄▄  ▀▀▀▄▄ ▄▄  ▄▄▄▀ ▀▄ ▄▀ ▄▄  ▄▄▄▀ █▀▀█ █  █  #
#  █  █ ▀▄▄▀ █   █ ▄▄▄ █▄▄▄█ ▄▄▄ ▄▄▄▄  ▀█▀  ▄▄▄ ▄▄▄▄ █  █ █▄▄█▄ #
#===============================================================#
        ***** WARNING - Config File is UNENCRYPTED! *****        
EOFLG
  )
  if [ -d $HSHQ_SCRIPTS_DIR/source ]; then
    for curSource in $HSHQ_SCRIPTS_DIR/source/*; do
      if [ -f "$curSource" ]; then
        source "$curSource"
      fi
    done
  fi
}

function main()
{
  init
  checkWrapperVersion
  IS_CONSOLE_ENV=true
  IS_PERFORM_INSTALL=false
  IS_NEW_LIB=false
  CONNECTING_IP=""
  IS_PERFORM_RESTORE=false
  IS_PERFORM_UPDATE=false
  case "$1" in
    "install")
      IS_PERFORM_INSTALL=true
      CONNECTING_IP=$2
      ;;
    "-a")
      CONNECTING_IP=$2
      ;;
    "run")
      IS_NEW_LIB=$2
      CONNECTING_IP=$3
      ;;
    "restore")
      CONNECTING_IP=$2
      IS_PERFORM_RESTORE=true
      ;;
    "update")
      IS_PERFORM_UPDATE=true
      ;;
    *)
      ;;
  esac
  if [ -z "$CONNECTING_IP" ]; then
    CONNECTING_IP=$(getConnectingIPAddress)
  fi
  if [ -z "$CONNECTING_IP" ] && [ -f ~/hshq/cip.txt ]; then
    CONNECTING_IP=$(cat ~/hshq/cip.txt)
  fi
  if [ "$IS_PERFORM_UPDATE" = "true" ]; then
    setSystemState $SS_UPDATING
    checkUpdateVersion
    moveScriptServerPerformUpdate
    if [ "$IS_RESTART_SCRIPTSERVER" = "true" ]; then
      sudo systemctl daemon-reload
      sudo systemctl restart runScriptServer.service
    fi
    setSystemState $SS_RUNNING
    return
  fi
  if ! [ -z "$USER_SUDO_PW" ]; then
    sudo -k
    echo "$USER_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      USER_SUDO_PW=""
    fi
  fi
  if [ "$IS_PERFORM_INSTALL" = "true" ]; then
    mkdir -p $TMP_PW_CHECKDIR
    read -r -s -p "" sudoPWKey
    USER_SUDO_PW="$(openssl enc -d -aes256 -pbkdf2 -in $ENC_PW_FILE -pass pass:$sudoPWKey)"
    rm -f $ENC_PW_FILE
    refreshSudo
    rm -fr $TMP_PW_CHECKDIR
    clear
    stty echo
    if [ "$USERNAME" = "root" ]; then
      strErr="main - Cannot install as root user, exiting..."
      logHSHQEvent error "$strErr"
      echo "$strErr"
      exit 1
    fi
    performBaseInstallation
    exit 0
  fi
  if [ "$IS_PERFORM_RESTORE" = "true" ]; then
    mkdir -p $TMP_PW_CHECKDIR
    read -r -s -p "" sudoPWKey
    USER_SUDO_PW="$(openssl enc -d -aes256 -pbkdf2 -in $ENC_PW_FILE -pass pass:$sudoPWKey)"
    rm -f $ENC_PW_FILE
    refreshSudo
    rm -fr $TMP_PW_CHECKDIR
    clear
    stty echo
    if [ "$USERNAME" = "root" ]; then
      echo "Cannot install as root user, exiting..."
      exit 1
    fi
    performFullRestore
    exit 0
  fi
  set +e
  # Mitigate any race conditions after an update
  sleep 2
  tgLock="$(tryGetLock hshqopen User-Console)"
  # Additional measures against race conditions
  if ! [ "$tgLock" = "true" ]; then
    sleep 2
    tgLock="$(tryGetLock hshqopen User-Console)"
  fi
  if ! [ "$tgLock" = "true" ]; then
    checkRes="$(getLockOpenMsg hshqopen)"
    showNoYesMessageBox "Script Running" "It appears that this script is already running in another session ($checkRes). This situation could occur if another scheduled process is currently running or the script exited prematurely with an error. Unknown and damaging consequences can occur if running two different instances at the same time. Are you certain that you wish to continue?"
    mbres=$?
    if [ $mbres -eq 0 ]; then
      exit
    fi
    sudo -k
    USER_SUDO_PW=""
    refreshSudo
    if [ $? -ne 0 ]; then
      exit 3
    fi
    forceGetLock hshqopen "User-Console"
  fi
  set -e
  checkConfigAvailable
  is_hshq_installed=false
  if ! [ -z "$CONFIG_FILE" ] && [ -f $CONFIG_FILE ]; then
    isInstalled=$(getConfigVarFromFile IS_INSTALLED $CONFIG_FILE)
    if [ "$isInstalled" = "true" ]; then
      is_hshq_installed=true
      IS_CONFIG_FILE_UNENCRYPTED=true
      set +e
      fixConfigV130
      set -e
      source $CONFIG_FILE
    else
      is_hshq_installed=false
    fi
  elif ! [ -z "$ENC_CONFIG_FILE" ] && [ -f $ENC_CONFIG_FILE ]; then
    # Encrypted file found, assume installed.
    is_hshq_installed=true
  fi
  set +e
  mainMenuResult=1
  while  [[ $mainMenuResult -ne 0 ]]
  do
    if [ "$is_hshq_installed" = "true" ]; then
      if [ "$IS_NEW_LIB" = "true" ]; then
        checkLoadConfig
      fi
      showInstalledMenu
    else
      showNotInstalledMenu
    fi
    mainMenuResult=$?
  done
  performExitFunctions true
}

function showNotInstalledMenu()
{
  set +e
  checkSupportedHostOS
  chmod 544 ~/hshq.sh
  refreshSudo
  setSudoTimeoutInstall
  if [[ "$(isProgramInstalled sshd)" = "false" ]]; then
    sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
    echo "Installing openssh-server, please wait..."
    performAptInstall openssh-server > /dev/null 2>&1
  fi
  notinstalledmenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$notinstalledmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Perform Base Installation" \
  "2" "Edit Configuration" \
  "3" "Install Dependencies" \
  "4" "Update APT Mirror" \
  "5" "Uninstall and Remove Everything" \
  "6" "Restore From Backup" \
  "7" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    menures=0
  fi
  set -e
  case $menures in
    0)
      return 0 ;;
    1)
      menuPerformBaseInstall ;;
    2)
      checkWorkingInternet
      checkUpdateAptSources false
      set -e
      checkLoadConfig
      initConfig
      nano $CONFIG_FILE
      loadConfigVars true
      set +e
      return 1 ;;
    3)
      checkWorkingInternet
      checkUpdateAptSources false
      set -e
      installDependencies
      pullBaseServicesDockerImages
      sudo reboot ;;
    4)
      set +e
      checkWorkingInternet
      refreshSudo
      checkUpdateAptSources true
      return 1 ;;
    5)
      nukeHSHQ
      set +e
      return 1 ;;
    6)
      checkWorkingInternet
      checkUpdateAptSources false
      showRestoreMenu
      set +e
      return 1 ;;
    7)
      return 0 ;;
  esac
}

function menuPerformBaseInstall()
{
  checkWorkingInternet
  checkUpdateAptSources false
  clear
  checkDockerPre
  set -e
  clear
  echo "Please wait..."
  checkLoadConfig
  initConfig
  initCertificateAuthority
  if [ "$IS_DESKTOP_ENV" = "true" ]; then
    PRIMARY_VPN_SETUP_TYPE=none
    updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE
  else
    setupVPNConnection
    if [ $? -ne 0 ]; then
      exit
    fi
  fi
  if [ "$IS_INSTALLED" = "true" ] || [ "$IS_INSTALLING" = "true" ]; then
    showMessageBox "Error" "Already installed or existing installation is in progress."
    set +e
    exit
  fi
  initInstallation
}

function checkDockerPre()
{
  set +e
  sudo docker ps > /dev/null 2>&1
  if [ $? -ne 0 ]; then
    if [ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]; then
      # Docker must be installed and system rebooted
      showMessageBox "WARNING" "It appears that docker is not installed, and the OS Linux version is Debian 12. Due to recent changes, docker must be installed beforehand and the system rebooted in order to continue. After the system has rebooted, please log back in with this same user ($USERNAME) and restart the installation (bash hshq.sh). Press okay to proceed."
      upgradeHostOS
      sudo DEBIAN_FRONTEND=noninteractive apt dist-upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'
      installDocker
      sudo reboot
    fi
  fi
}

function upgradeHostOS()
{
  if [ "$IS_DISABLE_HOST_UPGRADE" = "true" ]; then
    return
  fi
  sudo DEBIAN_FRONTEND=noninteractive apt update
  sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'
}

function checkWorkingInternet()
{
  set +e
  timeout 10 ping -c 1 1.1.1.1 > /dev/null 2>&1
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "You do not appear to have a working internet connection. Please check your network settings and try again."
    exit 2
  fi
}

function verifyAptSource()
{
  if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then
    if test -f /etc/apt/sources.list; then
      echo "true"
      return
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    if test -f /etc/apt/sources.list.d/ubuntu.sources; then
      echo "true"
      return
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    if test -f /etc/apt/sources.list.d/ubuntu.sources; then
      echo "true"
      return
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    if test -f /etc/apt/sources.list; then
      echo "true"
      return
    fi
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    if test -f /etc/apt/sources.list.d/official-package-repositories.list; then
      echo "true"
      return
    fi
  fi
  echo "false"
}

function backupOrigAptSource()
{
  if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then
    if ! test -f /etc/apt/sources.list.orig; then
      sudo cp /etc/apt/sources.list /etc/apt/sources.list.orig
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    if ! test -f /etc/apt/sources.list.d/ubuntu.sources.orig; then
      sudo cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.orig
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    if ! test -f /etc/apt/sources.list.d/ubuntu.sources.orig; then
      sudo cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.orig
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    if ! test -f /etc/apt/sources.list.orig; then
      sudo cp /etc/apt/sources.list /etc/apt/sources.list.orig
    fi
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    if ! test -f /etc/apt/sources.list.d/official-package-repositories.list.orig; then
      sudo cp /etc/apt/sources.list.d/official-package-repositories.list /etc/apt/sources.list.d/official-package-repositories.list.orig
    fi
  fi
}

function checkIfOrigAptSource()
{
  if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then
    if test -f /etc/apt/sources.list.orig; then
      echo "true"
    else
      echo "false"
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    if test -f /etc/apt/sources.list.d/ubuntu.sources.orig; then
      echo "true"
    else
      echo "false"
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    if test -f /etc/apt/sources.list.d/ubuntu.sources.orig; then
      echo "true"
    else
      echo "false"
    fi
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    if test -f /etc/apt/sources.list.orig; then
      echo "true"
    else
      echo "false"
    fi
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    if test -f /etc/apt/sources.list.d/official-package-repositories.list.orig; then
      echo "true"
    else
      echo "false"
    fi
  fi
}

function getDefaultAptSource()
{
  if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then
    grep "^deb" /etc/apt/sources.list.orig | head -n 1 | cut -d" " -f2
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    grep "^URIs" /etc/apt/sources.list.d/ubuntu.sources.orig | head -n 1 | cut -d" " -f2
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    grep "^URIs" /etc/apt/sources.list.d/ubuntu.sources.orig | head -n 1 | cut -d" " -f2
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    grep "^deb" /etc/apt/sources.list.orig | head -n 1 | cut -d" " -f2
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    grep "^URIs" /etc/apt/sources.list.d/official-package-repositories.list.orig | head -n 1 | cut -d" " -f2
  fi
}

function backupAptSource()
{
  if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then
    sudo cp -f /etc/apt/sources.list /etc/apt/sources.list.backup
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    sudo cp -f /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.backup
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    sudo cp -f /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.backup
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    sudo cp -f /etc/apt/sources.list /etc/apt/sources.list.backup
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    sudo cp -f /etc/apt/sources.list.d/official-package-repositories.list /etc/apt/sources.list.d/official-package-repositories.list.backup
  fi
}

function removeBackupAptSource()
{
  if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then
    sudo rm -f /etc/apt/sources.list.backup
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    sudo rm -f /etc/apt/sources.list.d/ubuntu.sources.backup
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    sudo rm -f /etc/apt/sources.list.d/ubuntu.sources.backup
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    sudo rm -f /etc/apt/sources.list.backup
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    sudo rm -f /etc/apt/sources.list.d/official-package-repositories.list.backup
  fi
}

function restoreBackupAptSource()
{
  if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then
    sudo cp -f /etc/apt/sources.list.backup /etc/apt/sources.list
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    sudo cp -f /etc/apt/sources.list.d/ubuntu.sources.backup /etc/apt/sources.list.d/ubuntu.sources
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    sudo cp -f /etc/apt/sources.list.d/ubuntu.sources.backup /etc/apt/sources.list.d/ubuntu.sources
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    sudo cp -f /etc/apt/sources.list.backup /etc/apt/sources.list
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    sudo cp -f /etc/apt/sources.list.d/official-package-repositories.list.backup /etc/apt/sources.list.d/official-package-repositories.list
  fi
}

function restoreOrigAptSource()
{
  if ([ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]) || ([ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]); then
    sudo cp -f /etc/apt/sources.list.orig /etc/apt/sources.list
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    sudo cp -f /etc/apt/sources.list.d/ubuntu.sources.orig /etc/apt/sources.list.d/ubuntu.sources
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    sudo cp -f /etc/apt/sources.list.d/ubuntu.sources.orig /etc/apt/sources.list.d/ubuntu.sources
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    sudo cp -f /etc/apt/sources.list.orig /etc/apt/sources.list
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    sudo cp -f /etc/apt/sources.list.d/official-package-repositories.list.orig /etc/apt/sources.list.d/official-package-repositories.list
  fi
}

function replaceAptMirror()
{
  aptUrl="$1"
  if [ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]; then
    sudo sed -i "/security/! s|deb [a-z]*://[^ ]* |deb ${aptUrl} |g" /etc/apt/sources.list
    sudo sed -i "/security/! s|deb-src [a-z]*://[^ ]* |deb-src ${aptUrl} |g" /etc/apt/sources.list
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    sudo sed -i "s|deb [a-z]*://[^ ]* |deb ${aptUrl} |g" /etc/apt/sources.list
    sudo sed -i "s|deb-src [a-z]*://[^ ]* |deb-src ${aptUrl} |g" /etc/apt/sources.list
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24. ]]; then
    sudo sed -i "s|^URIs.*|URIs: ${aptUrl} |g" /etc/apt/sources.list.d/ubuntu.sources
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26. ]]; then
    sudo sed -i "s|^URIs.*|URIs: ${aptUrl} |g" /etc/apt/sources.list.d/ubuntu.sources
  elif [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]]; then
    sudo sed -i "s|deb [a-z]*://[^ ]* |deb ${aptUrl} |g" /etc/apt/sources.list
    sudo sed -i "s|deb-src [a-z]*://[^ ]* |deb-src ${aptUrl} |g" /etc/apt/sources.list
  elif [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    sudo sed -i "/linuxmint/! s|deb [a-z]*://[^ ]* |deb ${aptUrl} |g" /etc/apt/sources.list.d/official-package-repositories.list
  fi
}

function checkUpdateAptSources()
{
  isForceCheck="$1"
  set +e
  if [ "$IS_AUTO_INSTALL" = "true" ]; then
    return
  fi
  if ! [ "$(verifyAptSource)" = "true" ]; then
    showMessageBox "ERROR" "There was a problem with your apt sources list. Proceeding with current settings..."
    return
  fi
  if [ "$(checkIfOrigAptSource)" = "true" ] && ! [ "$isForceCheck" = "true" ]; then
    performAptUpdate false
    return
  fi
  if ! [ "$isForceCheck" = "true" ]; then
    showYesNoMessageBox "Find Mirrors" "Would you like to check for nearby apt mirrors? This process will take around 1-2 minutes to complete, but it will likely save a lot more time in the future when performing Linux host updates."
    if [ $? -ne 0 ]; then
      backupOrigAptSource
      performAptUpdate false
      return
    fi
  fi
  backupOrigAptSource
  default_apt_src="$(getDefaultAptSource)"
  backupAptSource
  echo "========================================================================"
  echo "  Calculating the closest apt mirrors."
  echo "  This could take a minute or two, please wait..."
  echo "========================================================================"
  mirrorResults="$(getFastestMirrors)"
  if [ -z "$mirrorResults" ]; then
    showMessageBox "ERROR" "There was a problem obtaining mirror info, continuing with current settings..."
    removeBackupAptSource
    performAptUpdate false
    return
  fi
  OLDIFS=$IFS
  IFS=$(echo -en "\n\b")
  mirrorResultsArr=($(echo "$mirrorResults"))
  IFS=$OLDIFS
  aptupdatemenu=$(cat << EOF

$(getLogo)

Select your desired apt sources mirror from the list below.
To keep the current settings, select Cancel.
EOF
)
  uas_menu_items=( --title "Select Mirror" --radiolist "$aptupdatemenu" $(($MENU_HEIGHT+2)) $MENU_WIDTH $MENU_INT_HEIGHT )
  curNum=1
  for curMirror in "${mirrorResultsArr[@]}"
  do
    uas_menu_items+=( "$(echo $curMirror | xargs)" )
    uas_menu_items+=( "|" )
    if [ $curNum -eq 1 ]; then
      uas_menu_items+=( "on" )
    else
      uas_menu_items+=( "off" )
    fi
    ((curNum++))
  done
  while true;
    do
    selItem=$(whiptail "${uas_menu_items[@]}" 3>&1 1>&2 2>&3)
    if [ $? -ne 0 ]; then
      restoreBackupAptSource
      removeBackupAptSource
      performAptUpdate false
      return
    fi
    selAptSource=$(echo "$selItem" | xargs | rev | cut -d" " -f1 | rev)
    replaceAptMirror "$selAptSource"
    tmpOutput=/tmp/aptOut.txt
    rm -f $tmpOutput
    performAptUpdate false 2>&1 | tee $tmpOutput
    grep -e "^Err:" -e "Failed" $tmpOutput > /dev/null 2>&1
    chkRes=$?
    rm -f $tmpOutput
    if [ $chkRes -eq 0 ]; then
      errmenu=$(cat << EOF

$(getLogo)

There were some errors found with the selected mirror.
Select Retry to select a different mirror, or select
Revert to restore the original default.
EOF
      )
      restoreOrigAptSource
      if (whiptail --title "Error" --yesno "$errmenu" $MENU_HEIGHT $MENU_WIDTH --yes-button "Retry" --no-button "Revert" ); then
        continue
      else
        performAptUpdate false
        break
      fi
    else
      echo -e "========================================================================\n"
      echo -e "The selected mirror appears to be in good working order. (See above)\n"
      isBreakMainLoop=false
      while true;
      do
        echo "Enter 'confirm': To proceed with this option"
        echo "        'retry': To select a different mirror from the list"
        echo "       'revert': To restore the prior settings and continue"
        echo "         'orig': To restore the original default and continue"
        echo "         'exit': To restore the prior settings and exit the HSHQ utility"
        echo
        read -r -p "Your selection: " selMirrorOption
        if [ "$selMirrorOption" = "confirm" ]; then
          isBreakMainLoop=true
          break
        elif [ "$selMirrorOption" = "retry" ]; then
          restoreBackupAptSource
          isBreakMainLoop=false
          break
        elif [ "$selMirrorOption" = "revert" ]; then
          restoreBackupAptSource
          isBreakMainLoop=true
          performAptUpdate false
          break
        elif [ "$selMirrorOption" = "orig" ]; then
          restoreOrigAptSource
          isBreakMainLoop=true
          performAptUpdate false
          break
        elif [ "$selMirrorOption" = "exit" ]; then
          restoreBackupAptSource
          removeBackupAptSource
          performExitFunctions true
          exit
        else
          echo "Unknown response, please retry."
        fi
      done
      if [ "$isBreakMainLoop" = "true" ]; then
        break
      fi
    fi
  done
  removeBackupAptSource
}

function performAptUpdate()
{
  clear
  isQuietUpdate="$1"
  if [ "$isQuietUpdate" = "true" ]; then
    echo "Performing apt update, please wait..."
    sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null
  else
    sudo DEBIAN_FRONTEND=noninteractive apt update
  fi
}

function getFastestMirrors()
{
  # Some of the concepts of this function were adopted from
  # https://github.com/vegardit/fast-apt-mirror.sh/blob/v1/fast-apt-mirror.sh
  set +e
  distID=""
  last_modified_path=""
  if [ "$DISTRO_ID" = "debian" ]; then
    distID="debian"
  elif [ "$DISTRO_ID" = "ubuntu" ] || [ "$DISTRO_ID" = "linuxmint" ]; then
    distID="ubuntu"
  else
    # Fatal
    exit 1
  fi
  cd ~
  backupOrigAptSource
  default_mirror="$(getDefaultAptSource)"
  which netselect > /dev/null 2>&1
  if [ $? -ne 0 ]; then
    netselect_version='0.3.ds1-30.1'
    wget http://ftp.debian.org/debian/pool/main/n/netselect/netselect_${netselect_version}_${DISTRO_ARCH}.deb > /dev/null 2>&1
    sudo apt install ./netselect_${netselect_version}_${DISTRO_ARCH}.deb > /dev/null 2>&1
    rm -f ./netselect_${netselect_version}_${DISTRO_ARCH}.deb
  fi
  rm -f /tmp/mirrors.txt
  outputMirrorsList
  OLDIFS=$IFS
  IFS=$(echo -en "\n\b")
  fastestMirrorsArr=($(sudo netselect -s50 -t4 $(grep "^$distID" /tmp/mirrors.txt | cut -d" " -f2) 2>/dev/null))
  IFS=$OLDIFS
  rm -f /tmp/mirrors.txt
  mirrorsDomainList=$(echo "$default_mirror" | tr -s "/" | cut -d"/" -f2)
  mirrorsURLsList="$default_mirror"
  maxMirrors=15
  curMirrorCount=1
  curDTSeconds=$(date +%s)
  last_modified_path="ls-lR.gz"
  for curMirror in "${fastestMirrorsArr[@]}"
  do
    curURL=$(echo "$curMirror" | xargs | cut -d" " -f2)
    curDomain=$(echo "$curURL" | tr -s "/" | cut -d"/" -f2)
    echo "$mirrorsDomainList" | grep "$curDomain" > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      mirrorsDomainList="$mirrorsDomainList $curDomain"
      # Check last modified date
      lastModified=$(curl --max-time 3 -sSfL --head "$curURL${last_modified_path}" 2>/dev/null | grep -i "last-modified" | cut -d" " -f2- | LANG=C date -f- -u +%s)
      if ! [ -z "$lastModified" ] && [ "$(checkValidNumber $lastModified)" = "true" ] && [ $lastModified -gt 0 ]; then
        secsDiff=$(($curDTSeconds-$lastModified))
        # 2592000 is equivalent to last 30 days
        if [ $secsDiff -ge 2592000 ]; then
          continue
        fi
      else
        continue
      fi
      mirrorsURLsList="$mirrorsURLsList\n$curURL"
      ((curMirrorCount++))
      if [ $curMirrorCount -ge $maxMirrors ]; then
        break
      fi
    fi
  done
  echo -e "$mirrorsURLsList" | xargs -P 5 -i bash -c "curl -r 0-5120000 --max-time 5 -sf -w '%{speed_download} {}\n' -o /dev/null {}ls-lR.gz 2>/dev/null" | awk '$1 > 0' | awk '{printf "%.2f MB/s %s\n",$1/1048576,$2}' | sort -rg | head -n 7
}

function outputMirrorsList()
{
  # We'll just manage the list right here
  # to reduce the potential points of failure
  cat <<EOFML > /tmp/mirrors.txt
debian http://ftp.am.debian.org/debian/
debian http://ftp.au.debian.org/debian/
debian http://ftp.at.debian.org/debian/
debian http://ftp.by.debian.org/debian/
debian http://ftp.be.debian.org/debian/
debian http://ftp.br.debian.org/debian/
debian http://ftp.bg.debian.org/debian/
debian http://ftp.ca.debian.org/debian/
debian http://ftp.cl.debian.org/debian/
debian http://ftp.cn.debian.org/debian/
debian http://ftp.hr.debian.org/debian/
debian http://ftp.cz.debian.org/debian/
debian http://ftp.dk.debian.org/debian/
debian http://ftp.fi.debian.org/debian/
debian http://ftp.fr.debian.org/debian/
debian http://ftp2.de.debian.org/debian/
debian http://ftp.de.debian.org/debian/
debian http://ftp.hk.debian.org/debian/
debian http://ftp.is.debian.org/debian/
debian http://ftp.it.debian.org/debian/
debian http://ftp.jp.debian.org/debian/
debian http://ftp.kr.debian.org/debian/
debian http://ftp.lt.debian.org/debian/
debian http://ftp.md.debian.org/debian/
debian http://ftp.nl.debian.org/debian/
debian http://ftp.nc.debian.org/debian/
debian http://ftp.nz.debian.org/debian/
debian http://ftp.no.debian.org/debian/
debian http://ftp.pl.debian.org/debian/
debian http://ftp.pt.debian.org/debian/
debian http://ftp.sk.debian.org/debian/
debian http://ftp.si.debian.org/debian/
debian http://ftp.es.debian.org/debian/
debian http://ftp.se.debian.org/debian/
debian http://ftp.ch.debian.org/debian/
debian http://ftp.tw.debian.org/debian/
debian http://ftp.tr.debian.org/debian/
debian http://ftp.uk.debian.org/debian/
debian http://ftp.us.debian.org/debian/
debian http://mirror.abmanagement.al/debian/
debian http://debian.unnoba.edu.ar/debian/
debian http://mirror.sitsa.com.ar/debian/
debian http://mirrors.asnet.am/debian/
debian http://debian.mirror.digitalpacific.com.au/debian/
debian http://debian.mirror.serversaustralia.com.au/debian/
debian http://mirror.aarnet.edu.au/debian/
debian http://mirror.amaze.com.au/debian/
debian http://mirror.gsl.icu/debian/
debian http://mirror.linux.org.au/debian/
debian http://mirror.overthewire.com.au/debian/
debian http://mirror.realcompute.io/debian/
debian http://mirrors.xtom.au/debian/
debian http://debian.anexia.at/debian/
debian http://debian.lagis.at/debian/
debian http://debian.mur.at/debian/
debian http://debian.sil.at/debian/
debian http://ftp.tu-graz.ac.at/mirror/debian/
debian http://mirror.alwyzon.net/debian/
debian http://mirror.ourhost.az/debian/
debian http://mirror.limda.net/debian/
debian http://mirror.xeonbd.com/debian/
debian http://ftp.byfly.by/debian/
debian http://mirror.datacenter.by/debian/
debian http://debian-mirror.behostings.net/debian/
debian http://ftp.belnet.be/debian/
debian http://mirror.as35701.net/debian/
debian http://mirror.unix-solutions.be/debian/
debian http://debian.c3sl.ufpr.br/debian/
debian http://debian.pop-sc.rnp.br/debian/
debian http://mirror.blue3.com.br/debian/
debian http://mirrors.ic.unicamp.br/debian/
debian http://mirror.uepg.br/debian/
debian http://mirror.ufscar.br/debian/
debian http://debian.a1.bg/debian/
debian http://debian.ipacct.com/debian/
debian http://debian.ludost.net/debian/
debian http://debian.mnet.bg/debian/
debian http://debian.telecoms.bg/debian/
debian http://ftp.uni-sofia.bg/debian/
debian http://mirrors.netix.net/debian/
debian http://mirror.telepoint.bg/debian/
debian http://mirror.sabay.com.kh/debian/
debian http://debian.linux.n0c.ca/debian/
debian http://debian.mirror.rafal.ca/debian/
debian http://mirror.csclub.uwaterloo.ca/debian/
debian http://mirror.dst.ca/debian/
debian http://mirror.estone.ca/debian/
debian http://mirror.it.ubc.ca/debian/
debian http://debian-mirror.puq.apoapsis.cl/debian/
debian http://debian.redlibre.cl/debian/
debian http://mirror.hnd.cl/debian/
debian http://mirror.insacom.cl/debian/
debian http://mirror.ufro.cl/debian/
debian http://mirror.lzu.edu.cn/debian/
debian http://mirror.nju.edu.cn/debian/
debian http://mirror.nyist.edu.cn/debian/
debian http://mirrors.163.com/debian/
debian http://mirrors.bfsu.edu.cn/debian/
debian http://mirrors.hit.edu.cn/debian/
debian http://mirrors.jlu.edu.cn/debian/
debian http://mirrors.neusoft.edu.cn/debian/
debian http://mirrors.tuna.tsinghua.edu.cn/debian/
debian http://mirrors.ustc.edu.cn/debian/
debian http://mirrors.zju.edu.cn/debian/
debian http://mirrors.ucr.ac.cr/debian/
debian http://debian.carnet.hr/debian/
debian http://debian.iskon.hr/debian/
debian http://debian.superhosting.cz/debian/
debian http://ftp.cvut.cz/debian/
debian http://ftp.debian.cz/debian/
debian http://ftp.sh.cvut.cz/debian/
debian http://ftp.zcu.cz/debian/
debian http://merlin.fit.vutbr.cz/debian/
debian http://mirror.dkm.cz/debian/
debian http://mirror.it4i.cz/debian/
debian http://mirrors.nic.cz/debian/
debian http://mirror.one.com/debian/
debian http://mirrors.dotsrc.org/debian/
debian http://mirrors.rackhosting.com/debian/
debian http://mirrors.xtom.ee/debian/
debian http://debian.web.trex.fi/debian/
debian http://www.nic.funet.fi/debian/
debian http://apt.tetaneutral.net/debian/
debian http://debian.apt-mirror.de/debian/
debian http://debian.mirrors.ovh.net/debian/
debian http://debian.obspm.fr/debian/
debian http://debian.polytech-lille.fr/debian/
debian http://debian.proxad.net/debian/
debian http://debian.univ-tlse2.fr/debian/
debian http://deb-mir1.naitways.net/debian/
debian http://deb.syxpi.fr/debian/
debian http://ftp.ec-m.fr/debian/
debian http://ftp.lip6.fr/pub/linux/distributions/debian/
debian http://ftp.rezopole.net/debian/
debian http://ftp.univ-pau.fr/linux/mirrors/debian/
debian http://ftp.u-picardie.fr/debian/
debian http://ftp.u-strasbg.fr/debian/
debian http://miroir.univ-lorraine.fr/debian/
debian http://mirror.debian.ikoula.com/debian/
debian http://mirror.gitoyen.net/debian/
debian http://mirror.ibcp.fr/debian/
debian http://mirror.johnnybegood.fr/debian/
debian http://mirror.plusserver.com/debian/debian/
debian http://mirrors.ircam.fr/pub/debian/
debian http://debian.grena.ge/debian/
debian http://debian.charite.de/debian/
debian http://debian.inf.tu-dresden.de/debian/
debian http://debian.intergenia.de/debian/
debian http://debian.mirror.iphh.net/debian/
debian http://debian.mirror.lrz.de/debian/
debian http://debian.netcologne.de/debian/
debian http://debian.tu-bs.de/debian/
debian http://de.mirrors.clouvider.net/debian/
debian http://drmirror.physi.uni-heidelberg.de/debian/
debian http://ftp.fau.de/debian/
debian http://ftp.gwdg.de/debian/
debian http://ftp.halifax.rwth-aachen.de/debian/
debian http://ftp.hosteurope.de/mirror/ftp.debian.org/debian/
debian http://ftp-stud.hs-esslingen.de/debian/
debian http://ftp.stw-bonn.de/debian/
debian http://ftp.tu-chemnitz.de/debian/
debian http://ftp.tu-clausthal.de/debian/
debian http://ftp.uni-bayreuth.de/debian/
debian http://ftp.uni-hannover.de/debian/debian/
debian http://ftp.uni-kl.de/debian/
debian http://ftp.uni-mainz.de/debian/
debian http://ftp.uni-stuttgart.de/debian/
debian http://ftp.wrz.de/debian/
debian http://mirror.23m.com/debian/
debian http://mirror.creoline.net/debian/
debian http://mirror.de.leaseweb.net/debian/
debian http://mirror.dogado.de/debian/
debian http://mirror.eu.oneandone.net/debian/
debian http://mirror.informatik.tu-freiberg.de/debian/
debian http://mirror.ipb.de/debian/
debian http://mirror.netzwerge.de/debian/
debian http://mirror.plusline.net/debian/
debian http://mirrors.xtom.de/debian/
debian http://mirror.united-gameserver.de/debian/
debian http://mirror.wtnet.de/debian/
debian http://packages.hs-regensburg.de/debian/
debian http://pubmirror.plutex.de/debian/
debian http://debian.otenet.gr/debian/
debian http://mirror.xtom.com.hk/debian/
debian http://ftp.bme.hu/debian/
debian http://repo.jztkft.hu/debian/
debian http://is.mirror.flokinet.net/debian/
debian http://mirror.cse.iitk.ac.in/debian/
debian http://mirror.nitc.ac.in/debian/
debian http://mirrors.iitd.ac.in/debian/
debian http://kartolo.sby.datautama.net.id/debian/
debian http://kebo.pens.ac.id/debian/
debian http://mirror.poliwangi.ac.id/debian/
debian http://mirror.unair.ac.id/debian/
debian http://mr.heru.id/debian/
debian http://mirror.aminidc.com/debian/
debian http://mirrors.pardisco.co/debian/
debian http://debian.interhost.co.il/debian/
debian http://debian.connesi.it/debian/
debian http://debian.dynamica.it/debian/
debian http://debian.mirror.garr.it/debian/
debian http://ftp.linux.it/debian/
debian http://giano.com.dist.unige.it/debian/
debian http://mirror.units.it/debian/
debian http://debian-mirror.sakura.ne.jp/debian/
debian http://ftp.jaist.ac.jp/debian/
debian http://ftp.kddilabs.jp/pub/debian/
debian http://ftp.nara.wide.ad.jp/debian/
debian http://ftp.riken.jp/Linux/debian/debian/
debian http://ftp.yz.yamagata-u.ac.jp/debian/
debian http://mirrors.xtom.jp/debian/
debian http://repo.jing.rocks/debian/
debian http://mirror.hoster.kz/debian/
debian http://mirror.ps.kz/debian/
debian http://debian.mirror.ac.ke/debian/
debian http://debian.mirror.liquidtelecom.com/debian/
debian http://ftp.kaist.ac.kr/debian/
debian http://ftp.lanet.kr/debian/
debian http://mirror.siwoo.org/debian/
debian http://debian.koyanet.lv/debian/
debian http://mirror.cloudhosting.lv/debian/
debian http://mirror.veesp.com/debian/
debian http://debian.mirror.vu.lt/debian/
debian http://mirror.litnet.lt/debian/
debian http://mirror.vpsnet.com/debian/
debian http://debian.mirror.root.lu/debian/
debian http://mirror.a1.mk/debian/
debian http://mirror-mk.interspace.com/debian/
debian http://lidsol.fi-b.unam.mx/debian/
debian http://mirror.as43289.net/debian/
debian http://mirror.marwan.ma/debian/
debian http://debian.snt.utwente.nl/debian/
debian http://mirror.ams.macarne.com/debian/
debian http://mirror.bgp.rodeo/debian/
debian http://mirror.duocast.net/debian/
debian http://mirror.i3d.net/debian/
debian http://mirror.nforce.com/debian/
debian http://mirror.nl.cdn-perfprod.com/debian/
debian http://mirror.nl.datapacket.com/debian/
debian http://mirror.nl.leaseweb.net/debian/
debian http://mirror.nl.mirhosting.net/debian/
debian http://mirrors.hostiserver.com/debian/
debian http://mirrors.xtom.nl/debian/
debian http://mirror.tngnet.com/debian/
debian http://mirror.vpgrp.io/debian/
debian http://nl.mirror.flokinet.net/debian/
debian http://nl.mirrors.clouvider.net/debian/
debian http://debian.nautile.nc/debian/
debian http://mirror.lagoon.nc/debian/
debian http://linux.purple-cat.net/debian/
debian http://mirror.fsmg.org.nz/debian/
debian http://ftp.uio.no/debian/
debian http://ftp.agh.edu.pl/debian/
debian http://ftp.psnc.pl/debian/
debian http://ftp.task.gda.pl/debian/
debian http://debian.uevora.pt/debian/
debian http://ftp.eq.uc.pt/software/Linux/debian/
debian http://ftp.rnl.tecnico.ulisboa.pt/pub/debian/
debian http://mirror.leitecastro.com/debian/
debian http://mirrors.upr.edu/debian/
debian http://debian.mithril.re/debian/
debian http://mirror.flo.c-f.ro/debian/
debian http://mirror.linux.ro/debian/
debian http://mirror.ro.cdn-perfprod.com/debian/
debian http://mirrors.hosterion.ro/debian/
debian http://mirrors.hostico.ro/debian/
debian http://mirrors.nav.ro/debian/
debian http://mirrors.nxthost.com/debian/
debian http://mirrors.pidginhost.com/debian/
debian http://ro.mirror.flokinet.net/debian/
debian http://ftp.psn.ru/debian/
debian http://mirror.corbina.net/debian/
debian http://mirror.docker.ru/debian/
debian http://mirror.hyperdedic.ru/debian/
debian http://mirror.kpfu.ru/debian/
debian http://mirrors.powernet.com.ru/debian/
debian http://mirror.truenetwork.ru/debian/
debian http://repository.su/debian/
debian http://mirror.pmf.kg.ac.rs/debian/
debian http://mirror.coganng.com/debian/
debian http://mirror.djvg.sg/debian/
debian http://mirror.sg.gs/debian/
debian http://ossmirror.mycloud.services/debian/
debian http://deb.bbxnet.sk/debian/
debian http://ftp.antik.sk/debian/
debian http://ftp.debian.sk/debian/
debian http://debian.mirror.ac.za/debian/
debian http://ftp.is.co.za/debian/
debian http://debian.grn.cat/debian/
debian http://debian.redimadrid.es/debian/
debian http://debian.redparra.com/debian/
debian http://debian.uvigo.es/debian/
debian http://ftp.caliu.cat/debian/
debian http://ftp.cica.es/debian/
debian http://ftp.udc.es/debian/
debian http://mirror.raiolanetworks.com/debian/
debian http://repo.ifca.es/debian/
debian http://softlibre.unizar.es/debian/
debian http://ulises.hostalia.com/debian/
debian http://debian.lth.se/debian/
debian http://debian.mirror.su.se/debian/
debian http://ftp.acc.umu.se/debian/
debian http://ftpmirror1.infania.net/debian/
debian http://mirror.braindrainlan.nu/debian/
debian http://mirrors.glesys.net/debian/
debian http://debian.ethz.ch/debian/
debian http://linuxsoft.cern.ch/debian/
debian http://mirror1.infomaniak.com/debian/
debian http://mirror2.infomaniak.com/debian/
debian http://mirror.init7.net/debian/
debian http://mirror.iway.ch/debian/
debian http://mirror.sinavps.ch/debian/
debian http://pkg.adfinis-on-exoscale.ch/debian/
debian http://debian.ccns.ncku.edu.tw/debian/
debian http://debian.csie.ntu.edu.tw/debian/
debian http://debian.cs.nycu.edu.tw/debian/
debian http://ftp.tku.edu.tw/debian/
debian http://mirror.twds.com.tw/debian/
debian http://opensource.nchc.org.tw/debian/
debian http://tw1.mirror.blendbyte.net/debian/
debian http://ftp.debianclub.org/debian/
debian http://mirror.applebred.net/debian/
debian http://mirror.kku.ac.th/debian/
debian http://mirrors.cloudforest.co.th/debian/
debian http://ftp.linux.org.tr/debian/
debian http://debian.netforce.hosting/debian/
debian http://debian.volia.net/debian/
debian http://distrohub.kyiv.ua/debian/
debian http://mirror.mirohost.net/debian/
debian http://mirror.ukrnames.com/debian/
debian http://debian.mirrors.uk2.net/debian/
debian http://debian.mirror.uk.sargasso.net/debian/
debian http://free.hands.com/debian/
debian http://ftp.ticklers.org/debian/
debian http://mirror.cov.ukservers.com/debian/
debian http://mirror.lchost.net/debian/
debian http://mirror.mythic-beasts.com/debian/
debian http://mirror.ox.ac.uk/debian/
debian http://mirror.positive-internet.com/debian/
debian http://mirrors.coreix.net/debian/
debian http://mirrorservice.org/sites/ftp.debian.org/debian/
debian http://mirror.vinehost.net/debian/
debian http://uk.mirrors.clouvider.net/debian/
debian http://atl.mirrors.clouvider.net/debian/
debian http://debian-archive.trafficmanager.net/debian/
debian http://debian.cc.lehigh.edu/debian/
debian http://debian.csail.mit.edu/debian/
debian http://debian.cs.binghamton.edu/debian/
debian http://debian.mirror.constant.com/debian/
debian http://debian.osuosl.org/debian/
debian http://debian.uchicago.edu/debian/
debian http://la.mirrors.clouvider.net/debian/
debian http://lethe.chinstrap.org/debian/
debian http://mirror.0x626b.com/debian/
debian http://mirror.cogentco.com/debian/
debian http://mirror.dal.nexril.net/debian/
debian http://mirror.keystealth.org/debian/
debian http://mirror.rustytel.net/debian/
debian http://mirrors.accretive-networks.net/debian/
debian http://mirrors.bloomu.edu/debian/
debian http://mirrors.bmcc.edu/debian/
debian http://mirrors.iu13.net/debian/
debian http://mirrors.lug.mtu.edu/debian/
debian http://mirrors.mwcampbell.us/debian/
debian http://mirrors.namecheap.com/debian/
debian http://mirrors.ocf.berkeley.edu/debian/
debian http://mirror.steadfast.net/debian/
debian http://mirrors.vcea.wsu.edu/debian/
debian http://mirrors.wikimedia.org/debian/
debian http://mirrors.xtom.com/debian/
debian http://mirror.timkevin.us/debian/
debian http://mirror.us.leaseweb.net/debian/
debian http://mirror.us.mirhosting.net/debian/
debian http://mirror.us.oneandone.net/debian/
debian http://nyc.mirrors.clouvider.net/debian/
debian http://plug-mirror.rcac.purdue.edu/debian/
debian http://repo.ialab.dsu.edu/debian/
debian http://us.mirror.ahrefs.org/debian/
debian http://debian.repo.cure.edu.uy/debian/
debian http://debian.xtdv.net/debian/
debian http://mirror.bizflycloud.vn/debian/
ubuntu http://mirror.sitsa.com.ar/ubuntu/
ubuntu http://ubuntu.zero.com.ar/ubuntu/
ubuntu http://ubuntu.unc.edu.ar/ubuntu/
ubuntu http://mirrors.asnet.am/ubuntu/
ubuntu http://ubuntu.mirror.gnc.am/ubuntu/
ubuntu http://mirrors.teamcloud.am/mirrors/ubuntu/
ubuntu http://mirror.aarnet.edu.au/pub/ubuntu/archive/
ubuntu http://mirror.internet.asn.au/pub/ubuntu/archive/
ubuntu http://mirror.datamossa.io/ubuntu/archive/
ubuntu http://mirror.realcompute.io/ubuntu/
ubuntu http://ubuntu.mirror.serversaustralia.com.au/ubuntu/
ubuntu http://ftp.iinet.net.au/pub/ubuntu/
ubuntu ftp://ftp.iinet.net.au/pub/ubuntu
ubuntu http://mirror.as24220.net/pub/ubuntu-archive/
ubuntu ftp://mirror.as24220.net/pub/ubuntu-archive/
ubuntu http://mirror.as24220.net/pub/ubuntu/
ubuntu ftp://mirror.as24220.net/pub/ubuntu/
ubuntu http://mirror.internode.on.net/pub/ubuntu/ubuntu/
ubuntu ftp://mirror.internode.on.net/pub/ubuntu/ubuntu/
ubuntu http://mirror.netspace.net.au/pub/ubuntu/
ubuntu ftp://mirror.netspace.net.au/pub/ubuntu/
ubuntu http://mirror.overthewire.com.au/ubuntu/
ubuntu ftp://mirror.overthewire.com.au/ubuntu/
ubuntu http://mirror.alwyzon.net/ubuntu/
ubuntu http://mirror.easyname.at/ubuntu-archive/
ubuntu ftp://mirror.easyname.at/ubuntu-archive/
ubuntu http://ubuntu.anexia.at/ubuntu/
ubuntu http://mirror.kumi.systems/ubuntu/
ubuntu ftp://mirror.kumi.systems/ubuntu/
ubuntu http://mirror.kumi.systems/ubuntu-ports/
ubuntu ftp://mirror.kumi.systems/ubuntu-ports/
ubuntu http://ubuntu.lagis.at/ubuntu/
ubuntu ftp://ubuntu.lagis.at/ubuntu/
ubuntu http://mirror.datacenter.az/ubuntu/
ubuntu http://mirror.ourhost.az/ubuntu/
ubuntu http://aze.archive.ubuntu.com/ubuntu/
ubuntu ftp://aze.archive.ubuntu.com/ubuntu/
ubuntu http://mirror.yer.az/ubuntu/archive/
ubuntu http://mirror.gotmyhost.com/ubuntu-archive/
ubuntu http://mirror.limda.net/ubuntu-archive/
ubuntu http://mirror.xeonbd.com/ubuntu-archive/
ubuntu http://ftp.byfly.by/ubuntu/
ubuntu ftp://ftp.byfly.by/ubuntu/
ubuntu http://mirror.datacenter.by/ubuntu/
ubuntu ftp://mirror.datacenter.by/ubuntu/
ubuntu http://ftp.belnet.be/ubuntu/
ubuntu ftp://ftp.belnet.be/ubuntu/
ubuntu http://mirror.unix-solutions.be/ubuntu/
ubuntu http://mirrors-ubuntu.behostings.com/ubuntu/
ubuntu http://archive.ubuntu.mirror.ba/ubuntu/
ubuntu http://ubuntu-mirror.bhtelecom.ba/ubuntu/
ubuntu http://mirror.uepg.br/ubuntu/
ubuntu http://ubuntu.c3sl.ufpr.br/ubuntu/
ubuntu ftp://ubuntu.c3sl.ufpr.br/ubuntu/
ubuntu http://ubuntu.mirror.letscloud.io/
ubuntu http://mirror.ufam.edu.br/ubuntu/
ubuntu http://mirror.ufscar.br/ubuntu/
ubuntu http://mirror.unesp.br/ubuntu/
ubuntu http://sft.if.usp.br/ubuntu/
ubuntu ftp://sft.if.usp.br/ubuntu/
ubuntu http://ubuntu-archive.locaweb.com.br/ubuntu/
ubuntu http://ubuntu.letscloud.io/ubuntu/
ubuntu http://ubuntu.mti.mt.gov.br/
ubuntu http://mirror.telepoint.bg/ubuntu/
ubuntu ftp://mirror.telepoint.bg/ubuntu/
ubuntu http://mirrors.neterra.net/ubuntu/archive/
ubuntu ftp://mirrors.neterra.net/ubuntu/archive/
ubuntu http://mirrors.netix.net/ubuntu/archive/
ubuntu ftp://mirrors.netix.net/ubuntu/archive/
ubuntu http://ubuntu.ipacct.com/ubuntu/
ubuntu ftp://ubuntu.ipacct.com/ubuntu/
ubuntu http://mirrors.storpool.com/ubuntu/archive/
ubuntu http://ubuntu.linux-bg.org/ubuntu/
ubuntu ftp://ubuntu.linux-bg.org/ubuntu/
ubuntu http://mirror.bg.host.ag/ubuntu/
ubuntu ftp://mirror.bg.host.ag/ubuntu/
ubuntu http://mirror.sabay.com.kh/ubuntu/
ubuntu http://mirrors.layeronline.com/ubuntu/
ubuntu http://ubuntu.linux.n0c.ca/ubuntuarchive/
ubuntu http://mirror.csclub.uwaterloo.ca/ubuntu/
ubuntu ftp://mirror.csclub.uwaterloo.ca/ubuntu/
ubuntu http://mirror.it.ubc.ca/ubuntu/
ubuntu http://mirror.reenigne.net/ubuntu/
ubuntu http://muug.ca/mirror/ubuntu/
ubuntu ftp://ftp.muug.ca/mirror/ubuntu/
ubuntu http://mirror.0xem.ma/ubuntu/
ubuntu http://mirror.hep.gg/ubuntu/
ubuntu http://archive.ubuntu.mirror.rafal.ca/ubuntu/
ubuntu ftp://archive.ubuntu.mirror.rafal.ca/ubuntu/
ubuntu http://gpl.savoirfairelinux.net/pub/mirrors/ubuntu/
ubuntu http://mirror.ca-tr.kamatera.com/ubuntu/
ubuntu http://mirror.rcg.sfu.ca/mirror/ubuntu/
ubuntu http://ubuntu.mirror.globo.tech/
ubuntu ftp://ubuntu.mirror.globo.tech/
ubuntu http://mirror.its.dal.ca/ubuntu/
ubuntu ftp://mirror.its.dal.ca/ubuntu/
ubuntu http://ubuntu.mirror.rafal.ca/ubuntu/
ubuntu http://mirror.hnd.cl/ubuntu/
ubuntu http://ubuntu-mirror.puq.apoapsis.cl/ubuntu/
ubuntu http://mirror.uchile.cl/ubuntu/
ubuntu http://elmirror.cl/ubuntu/
ubuntu http://mirror.ufro.cl/ubuntu/
ubuntu http://mirror1.grupocg.cl/ubuntu/
ubuntu http://mirrors.tuna.tsinghua.edu.cn/ubuntu/
ubuntu http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports/
ubuntu http://mirror.nju.edu.cn/ubuntu/
ubuntu http://mirror.nyist.edu.cn/ubuntu/
ubuntu http://mirror.nyist.edu.cn/ubuntu-ports/
ubuntu http://mirrors.ustc.edu.cn/ubuntu/
ubuntu http://mirrors.sdu.edu.cn/ubuntu/
ubuntu http://repo.huaweicloud.com/ubuntu/
ubuntu http://ftp.sjtu.edu.cn/ubuntu/
ubuntu ftp://ftp.sjtu.edu.cn/ubuntu/
ubuntu http://mirror.bjtu.edu.cn/ubuntu/
ubuntu http://mirrors.cn99.com/ubuntu/
ubuntu http://mirrors.huaweicloud.com/repository/ubuntu/
ubuntu http://mirrors.hust.edu.cn/ubuntu/
ubuntu http://mirrors.jcut.edu.cn/ubuntu/
ubuntu http://mirrors.jlu.edu.cn/ubuntu/
ubuntu http://mirrors.jxust.edu.cn/ubuntu/
ubuntu http://mirrors.njupt.edu.cn/ubuntu/
ubuntu http://mirrors.sjtug.sjtu.edu.cn/ubuntu/
ubuntu http://mirrors.sohu.com/ubuntu/
ubuntu ftp://mirrors.sohu.com/ubuntu/
ubuntu http://mirrors.xjtu.edu.cn/ubuntu/
ubuntu http://mirrors.yun-idc.com/ubuntu/
ubuntu ftp://mirrors.yun-idc.com/ubuntu/
ubuntu http://mirrors.zju.edu.cn/ubuntu/
ubuntu http://mirrors.cqu.edu.cn/ubuntu/
ubuntu http://edgeuno-bog2.mm.fcix.net/ubuntu/
ubuntu http://mirror.unimagdalena.edu.co/ubuntu/
ubuntu http://mirrors.atlas.net.co/ubuntu/
ubuntu http://mirrors.united.cd/ubuntu/
ubuntu http://ubuntu.ucr.ac.cr/ubuntu/
ubuntu ftp://ubuntu.ucr.ac.cr/ubuntu/
ubuntu http://ubuntu.grad.hr/ubuntu/
ubuntu ftp://ubuntu.grad.hr/ubuntu/
ubuntu http://mirror.library.ucy.ac.cy/linux/ubuntu/archive/
ubuntu http://cz.archive.ubuntu.com/ubuntu/
ubuntu http://ftp.linux.cz/pub/linux/ubuntu/
ubuntu ftp://ftp.linux.cz/pub/linux/ubuntu/
ubuntu http://ftp.sh.cvut.cz/ubuntu/
ubuntu ftp://ftp.sh.cvut.cz/ubuntu/
ubuntu http://mirror.it4i.cz/ubuntu/
ubuntu http://ftp.cvut.cz/ubuntu/
ubuntu ftp://ftp.cvut.cz/ubuntu/
ubuntu http://ucho.ignum.cz/ubuntu/
ubuntu ftp://ucho.ignum.cz/ubuntu/
ubuntu http://mirrors.dotsrc.org/ubuntu/
ubuntu ftp://mirrors.dotsrc.org/ubuntu/
ubuntu http://mirrors.fibianet.dk/ubuntu/
ubuntu http://mirror.group.one/ubuntu/
ubuntu http://mirror.one.com/ubuntu/
ubuntu ftp://mirror.one.com/ubuntu/
ubuntu http://ftp.klid.dk/ftp/ubuntu/
ubuntu ftp://ftp.klid.dk/ubuntu/
ubuntu http://mirror.cedia.org.ec/ubuntu/
ubuntu ftp://mirror.cedia.org.ec/ubuntu/
ubuntu http://mirrors.xtom.ee/ubuntu/
ubuntu http://mirror.5i.fi/ubuntu/
ubuntu http://ubuntu.web.trex.fi/ubuntu/
ubuntu ftp://mirror.clientvps.com/ubuntu/
ubuntu http://mirror.fi.ossplanet.net/ubuntu/
ubuntu http://mirror.ubuntu.ikoula.com/
ubuntu http://ubuntu.lafibre.info/ubuntu/
ubuntu http://ubuntu.univ-reims.fr/ubuntu/
ubuntu ftp://ubuntu.univ-reims.fr/ubuntu/
ubuntu http://ubuntu.mirrors.ovh.net/ubuntu/
ubuntu http://mirror.plusserver.com/ubuntu/ubuntu/
ubuntu http://miroir.univ-lorraine.fr/ubuntu/
ubuntu http://mirror.its-tps.fr/ubuntu/
ubuntu http://mirror.johnnybegood.fr/ubuntu/
ubuntu http://mirrors.ircam.fr/pub/ubuntu/archive/
ubuntu ftp://mirrors.ircam.fr/pub/ubuntu/archive/
ubuntu http://ubuntu.syxpi.fr/ubuntu/
ubuntu http://ubuntu.univ-nantes.fr/ubuntu/
ubuntu ftp://ubuntu.univ-nantes.fr/ubuntu/
ubuntu http://ftp.u-picardie.fr/mirror/ubuntu/ubuntu/
ubuntu http://distrib-coffee.ipsl.jussieu.fr/pub/linux/ubuntu/
ubuntu ftp://distrib-coffee.ipsl.jussieu.fr/pub/linux/ubuntu/
ubuntu http://ubuntu.grena.ge/ubuntu/
ubuntu http://ftp.uni-stuttgart.de/ubuntu/
ubuntu http://ftp.halifax.rwth-aachen.de/ubuntu/
ubuntu ftp://ftp.halifax.rwth-aachen.de/ubuntu/
ubuntu http://mirror.dogado.de/ubuntu/
ubuntu http://mirror.netcologne.de/ubuntu/
ubuntu ftp://mirror.netcologne.de/ubuntu/
ubuntu http://ubuntu.mirror.lrz.de/ubuntu/
ubuntu http://de.mirrors.clouvider.net/ubuntu/
ubuntu http://debian.charite.de/ubuntu/
ubuntu http://ftp.stw-bonn.de/ubuntu/
ubuntu ftp://ftp.stw-bonn.de/ubuntu/
ubuntu http://mirror.creoline.net/ubuntu/
ubuntu http://mirror.de.leaseweb.net/ubuntu/
ubuntu ftp://mirror.de.leaseweb.net/ubuntu/
ubuntu http://mirror.ipb.de/ubuntu/
ubuntu http://mirror.wtnet.de/ubuntu/
ubuntu http://mirrors.xtom.de/ubuntu/
ubuntu http://ubuntu.mirror.tudos.de/ubuntu/
ubuntu http://ftp.fau.de/ubuntu/
ubuntu ftp://ftp.fau.de/ubuntu/
ubuntu http://drmirror.physi.uni-heidelberg.de/ubuntu/
ubuntu http://ftp-stud.hs-esslingen.de/ubuntu/
ubuntu ftp://ftp-stud.hs-esslingen.de/ubuntu/
ubuntu http://mirror.netzwerge.de/ubuntu/
ubuntu http://mirror.serverloft.eu/ubuntu/ubuntu/
ubuntu http://artfiles.org/ubuntu.com/
ubuntu ftp://artfiles.org/ubuntu.com/
ubuntu ftp://ftp.fu-berlin.de/linux/ubuntu/
ubuntu http://ftp.rz.tu-bs.de/pub/mirror/ubuntu-packages/
ubuntu ftp://ftp.rz.tu-bs.de/pub/mirror/ubuntu-packages/
ubuntu http://ftp.tu-chemnitz.de/pub/linux/ubuntu-ports/
ubuntu ftp://ftp.tu-chemnitz.de/pub/linux/ubuntu-ports/
ubuntu http://ftp.tu-ilmenau.de/mirror/ubuntu/
ubuntu ftp://ftp.tu-ilmenau.de/mirror/ubuntu/
ubuntu http://ftp.uni-bayreuth.de/linux/ubuntu/ubuntu/
ubuntu ftp://ftp.uni-bayreuth.de/pub/linux/ubuntu/ubuntu/
ubuntu http://ftp.uni-mainz.de/ubuntu/
ubuntu ftp://ftp.uni-mainz.de/ubuntu/
ubuntu http://ftp5.gwdg.de/pub/linux/debian/ubuntu/
ubuntu ftp://ftp5.gwdg.de/pub/linux/debian/ubuntu/
ubuntu http://mirror.23m.com/ubuntu/
ubuntu http://mirror.daniel-jost.net/ubuntu/
ubuntu ftp://mirror.daniel-jost.net/ubuntu/
ubuntu http://mirror.eu-fr.kamatera.com/ubuntu/
ubuntu http://mirror.informatik.tu-freiberg.de/ubuntu/
ubuntu http://mirror.kamp.de/ubuntu/
ubuntu http://mirror.kkg.berlin/ubuntu/
ubuntu http://mirror.liquidrp.de/ubuntu/
ubuntu http://mirror.scaleuptech.com/ubuntu/
ubuntu ftp://mirror.scaleuptech.com/ubuntu/
ubuntu http://mirror.thunderhosting.cloud/ubuntu/
ubuntu http://mirror.united-gameserver.de/ubuntu/
ubuntu http://packages.oth-regensburg.de/ubuntu/
ubuntu http://suse.uni-leipzig.de/pub/releases.ubuntu.com/ubuntu/
ubuntu ftp://suse.uni-leipzig.de/pub/releases.ubuntu.com/ubuntu/
ubuntu http://ftp.hosteurope.de/mirror/archive.ubuntu.com/
ubuntu ftp://ftp.hosteurope.de/mirror/archive.ubuntu.com/
ubuntu http://ftp.tu-chemnitz.de/pub/linux/ubuntu/
ubuntu http://ftp.cc.uoc.gr/mirrors/linux/ubuntu/packages/
ubuntu ftp://ftp.cc.uoc.gr/mirrors/linux/ubuntu/packages/
ubuntu http://ftp.ntua.gr/ubuntu/
ubuntu ftp://ftp.ntua.gr/ubuntu/
ubuntu http://ubuntu.otenet.gr/
ubuntu ftp://ftp.otenet.gr/ubuntu/
ubuntu http://mirror.greennet.gl/ubuntu/
ubuntu http://hk.mirrors.thegigabit.com/ubuntu/
ubuntu http://mirror-hk.koddos.net/ubuntu/
ubuntu http://mirror.01link.hk/ubuntu/
ubuntu http://mirror.as.kamatera.com/ubuntu/
ubuntu http://repo.jztkft.hu/ubuntu/
ubuntu http://mirror.niif.hu/ubuntu/
ubuntu http://quantum-mirror.hu/mirrors/pub/ubuntu/
ubuntu http://mirrors.sth.sze.hu/ubuntu/
ubuntu http://ubuntu.hysing.is/ubuntu/
ubuntu http://is.mirror.flokinet.net/ubuntu/
ubuntu http://mirrors.opensource.is/ubuntu/
ubuntu http://repos.del.extreme-ix.org/ubuntu/
ubuntu http://in.mirror.coganng.com/ubuntu/
ubuntu ftp://in.mirror.coganng.com/ubuntu/
ubuntu http://in.mirror.coganng.com/ubuntu-ports/
ubuntu ftp://in.mirror.coganng.com/ubuntu-ports/
ubuntu http://mirror.bharatdatacenter.com/ubuntu/
ubuntu ftp://mirror.bharatdatacenter.com/ubuntu/
ubuntu http://mirror.cse.iitk.ac.in/ubuntu/
ubuntu ftp://mirror.cse.iitk.ac.in/ubuntu/
ubuntu http://mirror.ietlucknow.ac.in/ubuntu/
ubuntu http://mirror.nitc.ac.in/ubuntu/
ubuntu http://mirrors.nxtgen.com/ubuntu-mirror/ubuntu/
ubuntu http://repo.extreme-ix.org/ubuntu/
ubuntu http://ftp.iitm.ac.in/ubuntu/
ubuntu ftp://ftp.iitm.ac.in/ubuntu
ubuntu http://cdn.repo.cloudeka.id/ubuntu/
ubuntu http://cdn.ubuntu.repo.cloudeka.id/ubuntu/
ubuntu http://linux.domainesia.com/ubuntu/ubuntu-archive/
ubuntu http://mirror.faizuladib.com/ubuntu/
ubuntu http://mirror.repository.id/ubuntu/
ubuntu http://mr.heru.id/ubuntu/
ubuntu http://mirror.deace.id/ubuntu/
ubuntu http://mirror-gw.elastis.id/ubuntu/
ubuntu http://mirror.biznetgio.com/ubuntu/
ubuntu http://mirror.cepatcloud.id/ubuntu/
ubuntu http://mirror.citrahost.com/ubuntu/
ubuntu http://mirror.dewabiz.com/ubuntu/
ubuntu http://mirror.gi.co.id/ubuntu/
ubuntu http://mirror.papua.go.id/ubuntu/
ubuntu http://mirror.poliwangi.ac.id/ubuntu/
ubuntu http://mirror.unair.ac.id/ubuntu/
ubuntu http://repo.ugm.ac.id/ubuntu/
ubuntu http://repo.usk.ac.id/ubuntu/
ubuntu http://suro.ubaya.ac.id/ubuntu/
ubuntu ftp://suro.ubaya.ac.id/mirror/ubuntu/
ubuntu http://archive.ubuntu.mirrors.zagrio.net/
ubuntu ftp://mirrors.zagrio.net/ubuntu/archive/
ubuntu http://mirror.iranserver.com/ubuntu/
ubuntu http://ir.archive.ubuntu.com/ubuntu/
ubuntu ftp://ir.archive.ubuntu.com/ubuntu/
ubuntu http://ubuntu.mobinhost.com/ubuntu/
ubuntu ftp://ubuntu.mobinhost.com/ubuntu/
ubuntu http://ubuntu.pishgaman.net/ubuntu/
ubuntu http://mirror.aminidc.com/ubuntu/
ubuntu ftp://mirror.aminidc.com/ubuntu/
ubuntu http://mirrors.pardisco.co/ubuntu/
ubuntu http://archive.ubuntu.petiak.ir/ubuntu/
ubuntu ftp://archive.ubuntu.petiak.ir/ubuntu/
ubuntu http://ubuntu.pars.host/
ubuntu http://ubuntu.parsvds.com/ubuntu/
ubuntu http://mirror.faraso.org/ubuntu/
ubuntu http://mirrors.ubuntu.dimit.cloud/
ubuntu http://repo.iut.ac.ir/repo/Ubuntu/
ubuntu http://mirror.webworld.ie/ubuntu/
ubuntu http://mirror.il-ha.kamatera.com/ubuntu/
ubuntu http://ubuntu-archive.interhost.co.il/ubuntu/
ubuntu http://mirror.il-pt.kamatera.com/ubuntu/
ubuntu http://mirror.il-rh.kamatera.com/ubuntu/
ubuntu http://mirror.il-ta.kamatera.com/ubuntu/
ubuntu http://mirror.il.kamatera.com/ubuntu/
ubuntu http://rep-ubuntu-il.upress.io/ubuntu/
ubuntu http://mirror.isoc.org.il/pub/ubuntu/
ubuntu ftp://mirror.isoc.org.il/ubuntu/
ubuntu http://it1.mirror.vhosting-it.com/ubuntu/
ubuntu http://it2.mirror.vhosting-it.com/ubuntu/
ubuntu http://giano.com.dist.unige.it/ubuntu/
ubuntu http://ftp.udx.icscoe.jp/Linux/ubuntu/
ubuntu http://mirror.hashy0917.net/ubuntu/
ubuntu http://mirror.hashy0917.net/ubuntu-ports/
ubuntu http://archive.g4t1.pro/ubuntu/
ubuntu http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu/
ubuntu http://linux.yz.yamagata-u.ac.jp/ubuntu/
ubuntu http://mirror.nishi.network/ubuntu/
ubuntu http://mirror.nishi.network/ubuntu-ports/
ubuntu http://repo.jing.rocks/ubuntu/
ubuntu http://repo.jing.rocks/ubuntu-ports/
ubuntu http://ftp.jaist.ac.jp/pub/Linux/ubuntu/
ubuntu ftp://ftp.jaist.ac.jp/pub/Linux/ubuntu/
ubuntu http://ftp.riken.jp/Linux/ubuntu/
ubuntu ftp://ftp.riken.jp/Linux/ubuntu/
ubuntu http://ubuntutym.u-toyama.ac.jp/ubuntu/
ubuntu http://ftp.sakura.ad.jp/ubuntu/
ubuntu http://jp.mirror.coganng.com/ubuntu/
ubuntu ftp://jp.mirror.coganng.com/ubuntu/
ubuntu http://jp.mirror.coganng.com/ubuntu-ports/
ubuntu ftp://jp.mirror.coganng.com/ubuntu-ports/
ubuntu http://mirror.hoster.kz/ubuntu/
ubuntu ftp://mirror.hoster.kz/ubuntu/
ubuntu http://mirror.ps.kz/ubuntu/
ubuntu ftp://mirror.ps.kz/ubuntu/
ubuntu http://mirror.amuksa.com/ubuntu/
ubuntu http://mirror.kakao.com/ubuntu/
ubuntu http://mirror.krfoss.org/ubuntu/
ubuntu http://mirror.siwoo.org/ubuntu/
ubuntu http://ftp.daum.net/ubuntu/
ubuntu ftp://ftp.daum.net/ubuntu/
ubuntu http://ftp.lanet.kr/ubuntu/
ubuntu ftp://ftp.lanet.kr/ubuntu/
ubuntu http://ftp.lanet.kr/ubuntu-ports/
ubuntu ftp://ftp.lanet.kr/ubuntu-ports/
ubuntu http://mirror.jeonnam.school/ubuntu/
ubuntu http://mirror.zzunipark.com/ubuntu/
ubuntu http://mirror.zzunipark.com/ubuntu-ports/
ubuntu http://mirror.yuki.net.uk/ubuntu-ports/
ubuntu http://mir.linux.kg/ubuntu/
ubuntu http://mirror.cloudhosting.lv/ubuntu/
ubuntu http://ubuntu-arch.linux.edu.lv/ubuntu/
ubuntu ftp://ubuntu-arch.linux.edu.lv/lv.archive.ubuntu.com/ubuntu/
ubuntu http://ubuntu.koyanet.lv/ubuntu/
ubuntu http://mirror.bacloud.com/ubuntu-mirror/archive/
ubuntu http://mirror.vpsnet.com/ubuntu/
ubuntu http://ubuntu.mirror.vu.lt/ubuntu/
ubuntu ftp://ubuntu.mirror.vu.lt/ubuntu/
ubuntu http://ftp.litnet.lt/ubuntu/
ubuntu ftp://ftp.litnet.lt/ubuntu/
ubuntu http://ubuntu.mirror.root.lu/ubuntu/
ubuntu ftp://mirror.root.lu/ubuntu/ubuntu/
ubuntu http://mirror.a1.mk/ubuntu/
ubuntu http://mirror.t-home.mk/ubuntu/
ubuntu http://ubuntu.dts.mg/ubuntu/
ubuntu http://mirrors.gbnetwork.com/ubuntu/
ubuntu http://mirrors.ipserverone.com/ubuntu/
ubuntu http://ubuntu.mirror.thegigabit.com/
ubuntu http://ubuntu.tuxuri.com/ubuntu/
ubuntu http://ubuntu-mirror.cloud.mu/ubuntu/
ubuntu http://ubuntu-mirror.cloud.mu/ubuntu-ports/
ubuntu http://mirror.as43289.net/ubuntu/
ubuntu ftp://mirror.as43289.net/ubuntu/
ubuntu http://mirror.ihost.md/ubuntu/
ubuntu http://mirrors.mivocloud.com/ubuntu/
ubuntu ftp://mirrors.mivocloud.com/ubuntu/
ubuntu http://mirror.hosthink.net/ubuntu/
ubuntu http://mirror.datacenter.mn/ubuntu/
ubuntu http://mirror.marwan.ma/ubuntu/
ubuntu http://mirrors.ygn.mmix.net.mm/ubuntu/
ubuntu http://download.nust.na/pub/ubuntu/ubuntu/
ubuntu http://ubuntu.ntc.net.np/ubuntu/
ubuntu http://mirror.ams.macarne.com/ubuntu/
ubuntu http://mirror.i3d.net/pub/ubuntu/
ubuntu http://mirror.nl.datapacket.com/ubuntu/
ubuntu http://nl.archive.ubuntu.com/ubuntu/
ubuntu http://ftp.nluug.nl/os/Linux/distr/ubuntu/
ubuntu ftp://ftp.nluug.nl/pub/os/Linux/distr/ubuntu/
ubuntu http://ftp.snt.utwente.nl/pub/os/linux/ubuntu/
ubuntu ftp://ftp.snt.utwente.nl/pub/os/linux/ubuntu/
ubuntu http://mirror.lyrahosting.com/ubuntuarchive/
ubuntu ftp://mirror.lyrahosting.com/ubuntuarchive/
ubuntu http://mirror.nforce.com/pub/linux/ubuntu/
ubuntu ftp://mirror.nforce.com/pub/linux/ubuntu/
ubuntu http://mirror.nl.altushost.com/ubuntu/
ubuntu http://mirror.nl.leaseweb.net/ubuntu/
ubuntu ftp://mirror.nl.leaseweb.net/ubuntu/
ubuntu http://mirror.serverion.com/ubuntu/
ubuntu ftp://mirror.serverion.com/ubuntu/
ubuntu http://mirror.vpgrp.io/ubuntu/
ubuntu http://mirrors.xtom.nl/ubuntu/
ubuntu http://nl.mirrors.clouvider.net/ubuntu/
ubuntu http://ubuntu.mirror.intermax.nl/
ubuntu http://ubuntu.mirror.true.nl/ubuntu/
ubuntu http://ubuntu.mirror.wearetriple.com/archive/
ubuntu http://mirror.transip.net/ubuntu/ubuntu/
ubuntu ftp://mirror.transip.net/ubuntu/ubuntu/
ubuntu http://mirrors.as215248.net/ubuntu/
ubuntu http://nl3.archive.ubuntu.com/ubuntu/
ubuntu http://ftp.tudelft.nl/archive.ubuntu.com/
ubuntu ftp://ftp.tudelft.nl/pub/Linux/archive.ubuntu.com/
ubuntu http://mirror.eu.kamatera.com/ubuntu/
ubuntu http://mirror.hostnet.nl/ubuntu/archive/
ubuntu http://mirror.nl.as215296.net/ubuntu/
ubuntu http://mirror.previder.nl/ubuntu/
ubuntu http://mirrors.evoluso.com/ubuntu/
ubuntu http://nl.mirror.flokinet.net/ubuntu/
ubuntu http://osmirror.rug.nl/ubuntu/
ubuntu ftp://osmirror.rug.nl/ubuntu/
ubuntu ftp://ftpserv.tudelft.nl/pub/Linux/archive.ubuntu.com/
ubuntu http://ubuntu.lagoon.nc/ubuntu/
ubuntu ftp://ubuntu.lagoon.nc/ubuntu/
ubuntu http://archive.ubuntu.nautile.nc/ubuntu/
ubuntu http://mirror.fsmg.org.nz/ubuntu/
ubuntu http://ubuntu.btit.nz/ubuntu/
ubuntu ftp://ubuntu.btit.nz/ubuntu/
ubuntu http://no.mirrors.blix.com/ubuntu/
ubuntu http://no.archive.ubuntu.com/ubuntu/
ubuntu http://ubuntu.hi.no/archive/
ubuntu http://ftp.uninett.no/ubuntu/
ubuntu ftp://ftp.uninett.no/ubuntu/
ubuntu http://ubuntu.uib.no/archive/
ubuntu ftp://ubuntu.uib.no/pub/Linux/Distributions/ubuntu/archive/
ubuntu http://mirror.virtury.com/ubuntu/
ubuntu http://mirror.rise.ph/ubuntu/
ubuntu ftp://mirror.rise.ph/ubuntu/
ubuntu http://ftp.icm.edu.pl/pub/Linux/ubuntu/
ubuntu ftp://ftp.icm.edu.pl/pub/Linux/ubuntu/
ubuntu http://ftp.psnc.pl/linux/ubuntu/
ubuntu http://ubuntu.task.gda.pl/ubuntu/
ubuntu ftp://ubuntu.task.gda.pl/ubuntu/
ubuntu http://ftp.agh.edu.pl/ubuntu/
ubuntu ftp://ftp.agh.edu.pl/ubuntu/
ubuntu http://mirror.chmuri.net/ubuntu/
ubuntu http://ubuntu.man.lodz.pl/ubuntu/
ubuntu ftp://ubuntu.man.lodz.pl/ubuntu/
ubuntu http://mirror.leitecastro.com/ubuntu/
ubuntu http://mirrors.ptisp.pt/ubuntu/
ubuntu http://mirrors.up.pt/ubuntu/
ubuntu ftp://mirrors.up.pt/pub/ubuntu/
ubuntu http://cesium.di.uminho.pt/pub/ubuntu-archive/
ubuntu ftp://cesium.di.uminho.pt/pub/ubuntu-archive/
ubuntu http://archive.ubuntumirror.dei.uc.pt/ubuntu/
ubuntu ftp://archive.ubuntumirror.dei.uc.pt/ubuntu/
ubuntu http://ftp.rnl.tecnico.ulisboa.pt/pub/ubuntu/archive/
ubuntu ftp://ftp.rnl.tecnico.ulisboa.pt/pub/ubuntu/archive/
ubuntu http://glua.ua.pt/pub/ubuntu/
ubuntu ftp://glua.ua.pt/pub/ubuntu/
ubuntu http://mirrors.webhs.pt/ubuntu/
ubuntu http://mirrors.hosterion.ro/ubuntu/
ubuntu http://mirror.efect.ro/ubuntu/archive/
ubuntu http://mirrors.nav.ro/ubuntu/
ubuntu ftp://mirrors.nav.ro/ubuntu/
ubuntu http://mirror.flo.c-f.ro/ubuntu/
ubuntu http://mirror.flokinet.net/ubuntu/
ubuntu http://mirrors.chroot.ro/ubuntu/
ubuntu ftp://mirrors.chroot.ro/ubuntu/
ubuntu http://mirrors.hostico.ro/ubuntu/archive/
ubuntu http://mirrors.pidginhost.com/ubuntu/
ubuntu ftp://mirrors.pidginhost.com/ubuntu/
ubuntu http://ubuntu.mirrors.linux.ro/archive/
ubuntu ftp://ftp.linux.ro/ubuntu/archive/
ubuntu http://mirror.linux-ia64.org/ubuntu/
ubuntu http://mirror.neftm.ru/ubuntu/
ubuntu http://mirror.timeweb.ru/ubuntu/
ubuntu http://mirror.truenetwork.ru/ubuntu/
ubuntu ftp://mirror.truenetwork.ru/ubuntu/
ubuntu http://mirror.docker.ru/ubuntu/
ubuntu http://mirror.corbina.net/ubuntu/
ubuntu ftp://mirror.corbina.net/ubuntu/
ubuntu http://mirror.hyperdedic.ru/ubuntu/
ubuntu http://mirror.yandex.ru/ubuntu/
ubuntu ftp://mirror.yandex.ru/ubuntu/
ubuntu http://mirrors.powernet.com.ru/ubuntu/
ubuntu ftp://mirrors.powernet.com.ru/pub/ubuntu/archive/
ubuntu http://mirror.logol.ru/ubuntu/
ubuntu ftp://mirror.logol.ru/ubuntu/
ubuntu http://mirror.maeen.sa/apt-mirror/
ubuntu http://mirror1.sox.rs/ubuntu/ubuntu/
ubuntu http://ossmirror.mycloud.services/os/linux/ubuntu/
ubuntu http://mirror.sg.gs/ubuntu/
ubuntu http://kambing.uu.sg/ubuntu/
ubuntu http://mirror.0x.sg/ubuntu/
ubuntu ftp://mirror.0x.sg/ubuntu/
ubuntu http://mirror.soonkeat.sg/ubuntu/
ubuntu http://mirror.aktkn.sg/ubuntu/
ubuntu http://mirror.coganng.com/ubuntu/
ubuntu ftp://mirror.coganng.com/ubuntu/
ubuntu http://mirror.coganng.com/ubuntu-ports/
ubuntu ftp://mirror.coganng.com/ubuntu-ports/
ubuntu http://sg-mirrors.vhost.vn/ubuntu/
ubuntu http://mirror.vnet.sk/ubuntu/
ubuntu http://ftp.energotel.sk/pub/linux/ubuntu/
ubuntu ftp://ftp.energotel.sk/pub/linux/ubuntu/
ubuntu http://tux.rainside.sk/ubuntu/
ubuntu ftp://tux.rainside.sk/ubuntu/
ubuntu http://ftp.arnes.si/pub/mirrors/ubuntu/
ubuntu ftp://ftp.arnes.si/mirrors/ubuntu/
ubuntu http://ubuntu.mirror.ac.za/ubuntu/
ubuntu ftp://ubuntu.mirror.ac.za/ubuntu/
ubuntu http://mirror.hostafrica.co.za/ubuntu/
ubuntu http://ubuntu.mirror.rain.co.za/ubuntu/
ubuntu http://mirror.raiolanetworks.com/ubuntu/
ubuntu http://ftp.udc.es/ubuntu/
ubuntu ftp://ftp.udc.es/ubuntu/
ubuntu http://ubuntu.cica.es/ubuntu/
ubuntu ftp://ubuntu.cica.es/ubuntu/
ubuntu http://ftp.caliu.cat/pub/distribucions/ubuntu/archive/
ubuntu http://ftp.csuc.cat/ubuntu/archive/
ubuntu ftp://ftp.csuc.cat/ubuntu/archive/
ubuntu http://ubuntu.uvigo.es/
ubuntu ftp://ftp.uvigo.es/ubuntu/
ubuntu http://dafi.inf.um.es/ubuntu/
ubuntu ftp://dafi.inf.um.es/ubuntu/
ubuntu http://ubuntu.grn.cat/ubuntu/
ubuntu ftp://ubuntu.grn.cat/ubuntu/
ubuntu http://ftp.acc.umu.se/ubuntu/
ubuntu ftp://ftp.acc.umu.se/ubuntu/
ubuntu http://ftp.ludd.ltu.se/mirrors/ubuntu/
ubuntu http://mirror.bahnhof.net/ubuntu/
ubuntu ftp://mirror.bahnhof.net/ubuntu/
ubuntu http://mirror.zetup.net/ubuntu/
ubuntu http://ftp.lysator.liu.se/ubuntu/
ubuntu ftp://ftp.lysator.liu.se/ubuntu/
ubuntu http://ftpmirror1.infania.net/ubuntu/
ubuntu ftp://ftpmirror1.infania.net/ubuntu/
ubuntu http://mirror.siati.ch/ubuntu-archive/
ubuntu http://mirror.infomaniak.ch/ubuntu/
ubuntu ftp://mirror.infomaniak.ch/ubuntu/
ubuntu http://mirror.init7.net/ubuntu/
ubuntu http://mirror.solnet.ch/ubuntu/
ubuntu http://pkg.adfinis-on-exoscale.ch/ubuntu/
ubuntu http://ubuntu.ethz.ch/ubuntu/
ubuntu ftp://ubuntu.ethz.ch/ubuntu/
ubuntu http://mirror.gofoss.xyz/ubuntu/
ubuntu http://mirror.gofoss.xyz/ubuntu-ports/
ubuntu http://pkg.adfinis.com/ubuntu/
ubuntu http://mirror.twds.com.tw/ubuntu/
ubuntu http://mirror.twds.com.tw/ubuntu-ports/
ubuntu http://free.nchc.org.tw/ubuntu/
ubuntu ftp://free.nchc.org.tw/ubuntu
ubuntu http://ftp.tw.debian.org/ubuntu/
ubuntu ftp://ftp.tw.debian.org/ubuntu/
ubuntu http://ubuntu.ccns.ncku.edu.tw/ubuntu/
ubuntu http://ftp.ubuntu-tw.net/ubuntu/
ubuntu ftp://ftp.ubuntu-tw.net/ubuntu/
ubuntu http://mirror.ossplanet.net/ubuntu/
ubuntu ftp://mirror.ossplanet.net/ubuntu/
ubuntu http://ftp.tku.edu.tw/ubuntu/
ubuntu ftp://ftp.tku.edu.tw/ubuntu/
ubuntu http://tw1.mirror.blendbyte.net/ubuntu/
ubuntu http://ubuntu.cs.nctu.edu.tw/ubuntu/
ubuntu ftp://ubuntu.cs.nctu.edu.tw/ubuntu/
ubuntu http://ftp.mirror.tw/pub/ubuntu/ubuntu/
ubuntu ftp://ftp.mirror.tw/pub/ubuntu/ubuntu/
ubuntu http://mirror.aptus.co.tz/pub/ubuntuarchive/
ubuntu http://deb-mirror.habari.co.tz/ubuntu/
ubuntu http://mirror.kku.ac.th/ubuntu/
ubuntu ftp://mirror.kku.ac.th/ubuntu/
ubuntu http://mirrors.cloudforest.co.th/ubuntu/
ubuntu http://mirror1.totbb.net/ubuntu/
ubuntu http://mirrors.bangmod.cloud/ubuntu/
ubuntu http://mirror1.ku.ac.th/ubuntu/
ubuntu http://mirror.ati.tn/ubuntu/
ubuntu http://mirror.ekiphost.com/ubuntu/
ubuntu http://mirror.sh.com.tr/ubuntu/
ubuntu http://mirror.domainhizmetleri.com/ubuntu/
ubuntu http://mirror.verinomi.com/ubuntu/ubuntu-archive/
ubuntu http://mirror.kapteyan.com.tr/ubuntu/
ubuntu http://mirror.rabisu.com/ubuntu/ubuntu-archive/
ubuntu http://mirror.renu.ac.ug/ubuntu/
ubuntu http://ubuntu.ip-connect.vn.ua/
ubuntu ftp://ubuntu.ip-connect.vn.ua/mirror/ubuntu/
ubuntu http://ubuntu.mirrors.omnilance.com/ubuntu/
ubuntu http://distrohub.kyiv.ua/ubuntu/
ubuntu http://mirror.mirohost.net/ubuntu/
ubuntu ftp://mirror.mirohost.net/ubuntu/
ubuntu http://ubuntu.netforce.hosting/ubuntu/
ubuntu ftp://ubuntu.netforce.hosting/ubuntu/
ubuntu http://ubuntu.org.ua/ubuntu/
ubuntu ftp://ubuntu.org.ua/ubuntu/
ubuntu http://mirror.cov.ukservers.com/ubuntu/
ubuntu ftp://mirror.cov.ukservers.com/ubuntu/
ubuntu http://mirror.katapult.io/ubuntu/
ubuntu http://mirror.server.net/ubuntu/
ubuntu http://mirror.vorboss.net/ubuntu-archive/
ubuntu http://mirrors.ukfast.co.uk/sites/archive.ubuntu.com/
ubuntu ftp://mirrors.ukfast.co.uk/archive.ubuntu.com/
ubuntu http://www.mirrorservice.org/sites/archive.ubuntu.com/ubuntu/
ubuntu ftp://ftp.mirrorservice.org/sites/archive.ubuntu.com/ubuntu/
ubuntu http://uk.mirrors.clouvider.net/ubuntu/
ubuntu http://archive.ubuntu.moon127.net/
ubuntu http://mirror.as29550.net/archive.ubuntu.com/
ubuntu ftp://mirror.as29550.net/archive.ubuntu.com/
ubuntu http://mirror.bytemark.co.uk/ubuntu/
ubuntu ftp://mirror.bytemark.co.uk/ubuntu/
ubuntu http://mirror.eu-lo.kamatera.com/ubuntu/
ubuntu http://mirror.freethought-internet.co.uk/ubuntu/
ubuntu ftp://mirror.freethought-internet.co.uk/ubuntu/
ubuntu http://mirror.ox.ac.uk/sites/archive.ubuntu.com/ubuntu/
ubuntu ftp://mirror.ox.ac.uk/sites/archive.ubuntu.com/ubuntu/
ubuntu http://mirror.vinehost.net/ubuntu/
ubuntu ftp://mirror.vinehost.net/ubuntu/
ubuntu http://mirrors.coreix.net/ubuntu/
ubuntu http://mirrors.gethosted.online/ubuntu/
ubuntu ftp://mirrors.gethosted.online/ubuntu/
ubuntu http://mirrors.melbourne.co.uk/ubuntu/
ubuntu ftp://mirrors.melbourne.co.uk/ubuntu/
ubuntu http://ports.ubuntu.moon127.net/
ubuntu http://ubuntu.mirrors.uk2.net/ubuntu/
ubuntu ftp://ubuntu.mirrors.uk2.net/ubuntu/
ubuntu http://archive.ubuntu.com/ubuntu/
ubuntu ftp://archive.ubuntu.com/ubuntu/
ubuntu http://mirror.enzu.com/ubuntu/
ubuntu ftp://mirror.enzu.com/ubuntu/
ubuntu http://mirror.pilotfiber.com/ubuntu/
ubuntu http://dal.mirrors.serverside.com/ubuntu/
ubuntu http://mirror.math.princeton.edu/pub/ubuntu/
ubuntu http://mirror.pit.teraswitch.com/ubuntu/
ubuntu http://mirror.us.mirhosting.net/ubuntu/
ubuntu ftp://mirror.us.mirhosting.net/ubuntu/
ubuntu http://mirrors.iu13.net/ubuntu/
ubuntu http://atl.mirrors.clouvider.net/ubuntu/
ubuntu http://dal.mirrors.clouvider.net/ubuntu/
ubuntu http://la.mirrors.clouvider.net/ubuntu/
ubuntu http://mirror.arizona.edu/ubuntu/
ubuntu http://mirror.brightridge.com/ubuntuarchive/
ubuntu http://mirror.hoobly.com/ubuntu/
ubuntu http://mirror.its.umich.edu/ubuntu/
ubuntu http://mirror.linux.ncsu.edu/ubuntu/
ubuntu http://mirror.lstn.net/ubuntu/
ubuntu http://mirror.nodesdirect.com/ubuntu/
ubuntu ftp://mirror.nodesdirect.com/ubuntu/
ubuntu http://mirror.servaxnet.com/ubuntu/
ubuntu http://mirror.timkevin.us/ubuntu/
ubuntu http://mirror.ubuntu.serverforge.org/
ubuntu http://mirror.umd.edu/ubuntu/
ubuntu http://mirror.us.leaseweb.net/ubuntu/
ubuntu ftp://mirror.us.leaseweb.net/ubuntu/
ubuntu http://mirrors.bloomu.edu/ubuntu/
ubuntu ftp://mirrors.bloomu.edu/ubuntu/
ubuntu http://mirrors.egr.msu.edu/ubuntu/
ubuntu http://mirrors.rit.edu/ubuntu/
ubuntu ftp://mirrors.rit.edu/ubuntu/
ubuntu http://mirrors.usinternet.com/ubuntu/archive/
ubuntu http://mirrors.xtom.com/ubuntu/
ubuntu http://nyc.mirrors.clouvider.net/ubuntu/
ubuntu http://plug-mirror.rcac.purdue.edu/ubuntu/
ubuntu ftp://plug-mirror.rcac.purdue.edu/ubuntu/
ubuntu http://ubuntu.cs.utah.edu/ubuntu/
ubuntu ftp://ubuntu.cs.utah.edu/ubuntu/ubuntu/
ubuntu http://ubuntu.mirror.constant.com/
ubuntu http://ubuntu.mirror.shastacoe.net/ubuntu/
ubuntu http://mirrors.wikimedia.org/ubuntu/
ubuntu http://mirrors.gigenet.com/ubuntu/
ubuntu http://mirror.rustytel.net/ubuntu/
ubuntu http://mirror.siena.edu/ubuntu/
ubuntu http://mirrors.cmich.edu/ubuntu/
ubuntu http://mirrors.us.kernel.org/ubuntu/
ubuntu ftp://mirrors.us.kernel.org/ubuntu/
ubuntu http://repo.ialab.dsu.edu/ubuntu/
ubuntu http://ubuntu.osuosl.org/ubuntu/
ubuntu ftp://ubuntu.osuosl.org/pub/ubuntu/
ubuntu http://ftp.usf.edu/pub/ubuntu/
ubuntu ftp://ftp.usf.edu/pub/ubuntu/
ubuntu http://mirror.cc.vt.edu/pub2/ubuntu/
ubuntu ftp://mirror.cc.vt.edu/pub2/ubuntu/
ubuntu http://mirror.clarkson.edu/ubuntu/
ubuntu http://mirror.cogentco.com/pub/linux/ubuntu/
ubuntu http://mirror.cs.jmu.edu/pub/ubuntu/
ubuntu http://mirror.dal.nexril.net/ubuntu/
ubuntu http://mirror.math.ucdavis.edu/ubuntu/
ubuntu http://mirror.metrocast.net/ubuntu/
ubuntu http://mirror.mia.velocihost.net/ubuntu/
ubuntu http://mirror.mrjester.net/ubuntu/archive/
ubuntu http://mirror.nerdmilio.com/ubuntu/
ubuntu http://mirror.steadfastnet.com/ubuntu/
ubuntu http://mirror.team-cymru.com/ubuntu/
ubuntu ftp://mirror.team-cymru.com/ubuntu/
ubuntu http://mirror.team-cymru.org/ubuntu/
ubuntu ftp://mirror.team-cymru.org/ubuntu/
ubuntu http://mirror.uoregon.edu/ubuntu/
ubuntu ftp://mirror.uoregon.edu/ubuntu/
ubuntu http://mirror.us-midwest-1.nexcess.net/ubuntu/
ubuntu http://mirror.us-ny2.kamatera.com/ubuntu/
ubuntu http://mirror.us-sc.kamatera.com/ubuntu/
ubuntu http://mirror.us-tx.kamatera.com/ubuntu/
ubuntu http://mirrors.accretive-networks.net/ubuntu/
ubuntu http://mirrors.arpnetworks.com/Ubuntu/
ubuntu http://mirrors.cat.pdx.edu/ubuntu/
ubuntu http://mirrors.liquidweb.com/ubuntu/
ubuntu http://mirrors.lug.mtu.edu/ubuntu/
ubuntu ftp://mirrors.lug.mtu.edu/ubuntu/
ubuntu http://mirrors.maine.edu/ubuntu/
ubuntu http://mirrors.mit.edu/ubuntu/
ubuntu http://mirrors.namecheap.com/ubuntu/
ubuntu ftp://mirrors.namecheap.com/ubuntu/
ubuntu http://mirrors.ocf.berkeley.edu/ubuntu/
ubuntu ftp://mirrors.ocf.berkeley.edu/ubuntu/
ubuntu http://mirrors.ocf.berkeley.edu/ubuntu-ports/
ubuntu http://mirrors.sarak.as/ubuntu/
ubuntu ftp://mirrors.sarak.as/ubuntu/
ubuntu http://mirrors.sonic.net/ubuntu/
ubuntu ftp://mirrors.sonic.net/ubuntu/
ubuntu http://mirrors.xmission.com/ubuntu/
ubuntu ftp://mirrors.xmission.com/ubuntu/
ubuntu http://pubmirrors.dal.corespace.com/ubuntu/
ubuntu ftp://pubmirrors.dal.corespace.com/ubuntu/
ubuntu http://ubuntu.mirror.frontiernet.net/ubuntu/
ubuntu ftp://ubuntu.mirror.frontiernet.net/ubuntu/
ubuntu http://ubuntu.mirrors.pair.com/archive/
ubuntu ftp://ubuntu.mirrors.pair.com/archive/
ubuntu http://ubuntu.phoenixnap.com/ubuntu/
ubuntu http://www.gtlib.gatech.edu/pub/ubuntu/
ubuntu ftp://ftp.gtlib.gatech.edu/pub/ubuntu
ubuntu http://mirror.d.umn.edu/ubuntu/
ubuntu http://mirrors.vcea.wsu.edu/ubuntu/
ubuntu http://ubuntu.securedservers.com/
ubuntu http://ftp.ussg.iu.edu/linux/ubuntu/
ubuntu http://mirror.vcu.edu/pub/gnu+linux/ubuntu/
ubuntu ftp://mirror.vcu.edu/pub/gnu+linux/ubuntu/
ubuntu http://ubuntu.repo.cure.edu.uy/ubuntu/
ubuntu http://repos.interior.edu.uy/ubuntu/
ubuntu http://mirror.dc.uz/ubuntu/
ubuntu ftp://mirror.dc.uz/ubuntu/
ubuntu http://mirror.azvps.vn/ubuntu/
ubuntu http://ubuntu.vpsttt.com/ubuntu/
ubuntu http://opensource.xtdv.net/ubuntu/
ubuntu http://mirror.bizflycloud.vn/ubuntu/
ubuntu http://mirror.clearsky.vn/ubuntu/
ubuntu http://mirror.vietnix.vn/ubuntu/
ubuntu http://mirror.viettelcloud.vn/ubuntu/
ubuntu http://mirrors.tino.org/ubuntu/
ubuntu http://vn-mirrors.techhost.vn/ubuntu/
ubuntu http://vn-mirrors.vhost.vn/ubuntu/
ubuntu http://mirrors.nhanhoa.com/ubuntu/
EOFML
}

function getLogo()
{
  if [ "$IS_INSTALLED" = "true" ] && [ "$IS_CONFIG_FILE_UNENCRYPTED" = "true" ]; then
    echo "$hshqwarninglogo"
  else
    echo "$hshqlogo"
  fi
}

function showInstalledMenu()
{
  installedmenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$installedmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Services" \
  "2" "System Utils" \
  "3" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    menures=0
  fi
  case $menures in
    0)
	  return 0 ;;
    1)
      checkLoadConfig
      showStacksMenu
      set +e
      return 1 ;;
    2)
      set +e
      showAllUtilsMenu
      set +e
      return 1 ;;
#    2)
#      checkLoadConfig
#      if ! [ "$IS_INSTALLED" = "true" ]; then
#        showMessageBox "System Not Installed" "You must perform a system installation first before using this utility."
#        exit
#      fi
#      set +e
#      showNetworkMenu
#      set +e
#      return 1 ;;
#    2)
#      showHSHQUtilsMenu
#      set +e
#       return 1 ;;
#    3)
#      showSysUtilsMenu
#      set +e
#       return 1 ;;
#    4)
#       return 0 ;;
  esac
}

function showRestoreMenu()
{
  set +e
  precheckRestoreResult="$(performRestorePrecheck)" 
  if ! [ -z "$precheckRestoreResult" ]; then
    showMessageBox "ERROR" "$precheckRestoreResult"
    return
  fi
  checkPerformSystemPrep
  if [ $? -ne 0 ]; then
    return
  fi
  restoremenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$restoremenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Restore From Encrypted Duplicati Backup" \
  "2" "Restore From Unencrypted Backup" \
  "3" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    menures=0
  fi
  case $menures in
    0)
      return 0 ;;
    1)
      showRestoreEncryptedMenu ;;
    2)
      showRestoreUnencryptedMenu ;;
    3)
      return 0 ;;
  esac
}

function showRestoreEncryptedMenu()
{
  restoremenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$restoremenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Mount Drive" \
  "2" "Select Directory" \
  "3" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    menures=0
  fi
  case $menures in
    0)
      return 0 ;;
    1)
      showRestoreMountDriveMenu ;;
    2)
      showRestoreSelectEncryptedDirectoryMenu ;;
    3)
      return 0 ;;
  esac
}

function showRestoreUnencryptedMenu()
{
  set +e
  performRestorePostcheck
  if [ $? -ne 0 ]; then
    return
  fi
  confirmRestore=$(promptUserInputMenu "" "Confirm" "The main restore process is ready to start. This will take awhile to complete, as all of the docker images must be re-downloaded. You will be prompted shortly for your config decrypt password. Enter the word 'restore' below to continue:")
  if [ $? -ne 0 ] || [ -z "$confirmRestore" ] || ! [ "$confirmRestore" = "restore" ]; then
    showMessageBox "Incorrect Confirmation" "The text did not match, returning..."
    return
  fi
  cp $HSHQ_LIB_SCRIPT $HOME/$HSHQ_LIB_FILENAME
  rm -fr $HSHQ_DATA_DIR/*
  mv $HSHQ_RESTORE_DIR/* $HSHQ_DATA_DIR/
  rm -fr $HSHQ_RESTORE_DIR
  # Only restore hshqlib script if version is less than 85
  hshqlib_orig_version=$(sed -n 2p $HSHQ_LIB_SCRIPT | cut -d"=" -f2)
  if [ $hshqlib_orig_version -lt 85 ]; then
    mv $HOME/$HSHQ_LIB_FILENAME $HSHQ_LIB_SCRIPT
  else
    rm -f $HOME/$HSHQ_LIB_FILENAME
  fi
  scName=$RESTORE_SCREEN_NAME
  initScreen $scName "bash $HSHQ_LIB_SCRIPT restore $CONNECTING_IP"
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "There was a problem starting the restore process, exiting..."
    exit 1
  fi
  screen -r $scName
  exit 0
}

function performFullRestore()
{
  refreshSudo
  createHSHQLog
  loadConfigConsole
  forceGetLock networkchecks performFullRestore
  setSystemState $SS_RESTORING
  IS_INSTALLED=false
  IS_INSTALLING=true
  is_draw_pb=false
  set +e
  origUsername=$(echo $HSHQ_BASE_DIR | cut -d"/" -f3)
  curUID=$(id -u)
  curGID=$(id -g)
  if ! [ "$USERNAME" = "$origUsername" ] || ! [ "$curUID" = "$USERID" ] || ! [ "$curGID" = "$GROUPID" ]; then
    msgmenu=$(cat << EOF

$(getLogo)
 The current username(UID:GID) does not match the original.

   Current: $USERNAME ($curUID:$curGID)
   Original: $origUsername ($USERID:$GROUPID)

 The restore cannot be performed.
 Please ensure to have the exact same username
 and UID:GID as the Original.

EOF
    )
    whiptail --title "ERROR" --msgbox "$msgmenu" $MENU_HEIGHT $MENU_WIDTH
    return
  fi
  setSudoTimeoutInstall
  mkdir -p $HSHQ_NONBACKUP_DIR
  mkdir -p $HSHQ_BUILD_DIR
  # Set hostname
  new_hostname="HomeServer-$(echo $HOMESERVER_DOMAIN | sed 's/\./-/g')"
  if [ -z "$(cat /etc/hosts | grep $new_hostname)" ]; then
    echo "127.0.1.1 $new_hostname" | sudo tee -a /etc/hosts
  fi
  sudo hostnamectl set-hostname $new_hostname
  rm -f $HOME/dead.letter
  installDependencies
  echo "Pulling docker images..."
  restorePullDockerImages
  rm -f $HOME/script-server.zip
  # Set timezone
  sudo timedatectl set-timezone "$TZ"
  set +e
  # Setup primary network interface
  prevPrimaryIface=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ConnectionType = 'primary' and NetworkType = 'home_network';")
  isFound=false
  availInterfacesArr=($(echo "$(getAllNetworkInterfacesCSV)" | tr "," "\n"))
  for curInterface in "${availInterfacesArr[@]}"
  do
    if [ "$prevPrimaryIface" = "$curInterface" ]; then
      isFound=true
      break
    fi
  done
  if [ "$isFound" = "false" ]; then
    default_iface=$(getDefaultIface)
    checkDB=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where InterfaceName = '$default_iface';")
    if [ -z "$checkDB" ]; then
      addHSInterface $default_iface true
    else
      sudo sqlite3 $HSHQ_DB "update connections set ConnectionType = 'secondary' where NetworkType = 'home_network';"
      sudo sqlite3 $HSHQ_DB "update connections set ConnectionType = 'primary' where InterfaceName = '$default_iface';"
    fi
  fi
  set -e
  # This should cover all possibilities
  curPrimaryIfaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ConnectionType = 'primary' and NetworkType = 'home_network';")
  curPrimaryIfaceIP=$(sqlite3 $HSHQ_DB "select IPAddress from connections where ConnectionType = 'primary' and NetworkType = 'home_network';")
  HOMESERVER_HOST_PRIMARY_INTERFACE_NAME=$curPrimaryIfaceName
  updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_NAME $HOMESERVER_HOST_PRIMARY_INTERFACE_NAME
  HOMESERVER_HOST_PRIMARY_INTERFACE_IP=$curPrimaryIfaceIP
  updatePlaintextRootConfigVar HOMESERVER_HOST_PRIMARY_INTERFACE_IP $HOMESERVER_HOST_PRIMARY_INTERFACE_IP
  find $HSHQ_SSL_DIR -name "*-ca.crt" -print0 | xargs -0 -I {} sudo cp {} /usr/local/share/ca-certificates/
  sudo update-ca-certificates
  CURRENT_SSH_PORT=$(sudo grep "^Port\|^#Port" /etc/ssh/sshd_config | cut -d" " -f2)
  if [ -z "$CURRENT_SSH_PORT" ]; then
    CURRENT_SSH_PORT=22
  fi
  # Change SSH port on host
  sudo sed -i "s|^#*Port .*$|Port ${SSH_PORT}|g" /etc/ssh/sshd_config
  sudo sed -i "s|^Port .*$|Port ${SSH_PORT}|g" /etc/ssh/sshd_config
  updateMOTD
  performSuggestedSecUpdates
  installMailUtils
  performClearIPTables true
  checkUpdateAllIPTables performFullRestore
  outputScripts
  sudo cp $HSHQ_WIREGUARD_DIR/vpn/*.conf /etc/wireguard/ > /dev/null 2>&1
  performAptInstall python3-tornado > /dev/null 2>&1
  sudo rm -f /etc/systemd/system/runScriptServer.service
  sudo ln -s $HSHQ_SCRIPTS_DIR/root/runScriptServer.service /etc/systemd/system/runScriptServer.service
  sudo systemctl daemon-reload
  sudo systemctl enable runScriptServer
  sudo systemctl start runScriptServer
  set -e
  if [ -d $HSHQ_STACKS_DIR/wazuh ]; then
    installWazuhAgent
  fi
  set +e
  sudo docker ps -q | xargs sudo docker stop > /dev/null 2>&1
  sudo docker container prune -f > /dev/null 2>&1
  createDockerNetworks
  enableAllWGVPNInterfaces
  createWGDockerNetworks
  outputEnvPortainer
  startPortainer
  docker volume create --driver local -o device=$HSHQ_STACKS_DIR/caddy-common/primary-certs -o o=bind -o type=none caddy-primary-certs
  createClientDNSNetworksOnRestore
  prepAdguardInstallation
  restartAllStacks "duplicati,syncthing" true doNothing false doNothing
  sudo docker container prune -f
  addDomainAndWildcardAdguardHS $HOMESERVER_DOMAIN $HOMESERVER_HOST_PRIMARY_INTERFACE_IP
  # Add RelayServer fingerprint
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
    perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "echo \"Successfully connected to RelayServer! \"" -f
  fi
  wgDockInternetUpAll
  postInstallation restore
  setSystemState $SS_RUNNING
  performExitFunctions
  releaseLock networkchecks performFullRestore false
  set +e
  sudo systemctl daemon-reload
  sudo systemctl restart ssh
  sudo systemctl restart sshd
  clear
  echo -e "\n\n\n\n################################################################\n"
  echo -e "  HomeServer Restore Process Complete! \n"
  echo -e "  The Duplicati and Syncthing stacks have been intentionally"
  echo -e "  skipped from the stack restart process, as you may still"
  echo -e "  need to re-configure your backup drives, etc. You may also"
  echo -e "  have to manually restart some stacks due to any issues in"
  echo -e "  the docker startup process. Double check everything is"
  echo -e "  working correctly in Portainer."
  echo -e "                  ---------------------                  "
  echo -e "  If the ip address of the primary network interface is"
  echo -e "  different, then the caddy-home-xxx stack likely had"
  echo -e "  issues when restarting the stack. However, the networking"
  echo -e "  checks background process will automatically resolve that"
  echo -e "  issue within a minute or two.\n"
  echo -e "                  ***** IMPORTANT *****                  "
  echo -e "  If you use Duplicati as your primary backup method, then"
  echo -e "  the first thing you need to do after starting the Duplicati"
  echo -e "  stack is to run the 'Recreate (delete and repair)' function."
  echo -e "  This can be found by going to your HSHQ Backup, then go to"
  echo -e "  Advanced -> Database. DO NOT run the 'Repair' function,"
  echo -e "  even if prompted. It will cause permanent data loss to your"
  echo -e "  backup history, and you will have to either recover the"
  echo -e "  missing files or start a fresh new backup profile." 
  echo -e "\n################################################################\n\n"
  read -r -p "  Press enter to continue."
  screen -S $RESTORE_SCREEN_NAME -X quit
}

function restorePullDockerImages()
{
  set +e
  OLDIFS=$IFS
  IFS=$(echo -en "\n\b")
  imgList=($(sudo find $HSHQ_STACKS_DIR/portainer/compose -name docker-compose.yml -print0 | xargs -0 sudo grep -hr "^[[:blank:]]*image:"))
  imgOnlyList=()
  for curImgItem in "${imgList[@]}"
  do
    imgOnlyList+=($(echo "$curImgItem" | xargs | cut -d" " -f2))
  done
  IFS=$OLDIFS
  uniqs_arr=($(for tmpImg in "${imgOnlyList[@]}"; do echo "${tmpImg}"; done | sort -u))
  for curImg in "${uniqs_arr[@]}"
  do
    echo "Pulling $curImg..."
    buildOrPullImage "$curImg" 5 > /dev/null 2>&1
  done
  # Create nonbackup directories
  stackList=($(sudo find $HSHQ_STACKS_DIR/portainer/compose -name docker-compose.yml -print0 | xargs -0 sudo grep -hr "^#HSHQManaged" | cut -d" " -f2))
  for curStackItem in "${stackList[@]}"
  do
    checkCreateNonbackupDirByStack "$curStackItem"
  done
  restoreRemainingNonBackupDirectories
}

function restoreRemainingNonBackupDirectories()
{
  set +e
  OLDIFS=$IFS
  IFS=$(echo -en "\n\b")
  dirList=($(sudo find $HSHQ_STACKS_DIR/portainer/compose -name docker-compose.yml -print0 | xargs -0 sudo grep -hr "\${PORTAINER_HSHQ_NONBACKUP_DIR}" | cut -d"}" -f2 | cut -d":" -f1 | cut -d" " -f1))
  for curDir in "${dirList[@]}"
  do
    mkdir -p ${HSHQ_NONBACKUP_DIR}$curDir
  done
  IFS=$OLDIFS
}

function showRestoreMountDriveMenu()
{
  findmnt | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR. Please unmount this filesystem first, returning..."
    return
  fi
  dbackmenu=$(cat << EOF

$(getLogo)
EOF
)
  OLDIFS=$IFS
  IFS=$(echo -en "\n\b")
  diskarr=($(sudo hwinfo --disk --short | tail +2))
  curListNum=1
  scsbm_menu_items=( --title "Select Disk/Partition" --radiolist "$dbackmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT )
  for curDisk in "${diskarr[@]}"
  do
    curDiskID=$(echo "$curDisk" | xargs | cut -d" " -f1)
    curDiskName=$(echo "$curDisk" | xargs | cut -d" " -f2-)
    findmnt | grep $curDiskID > /dev/null 2>&1
    if [ $? -eq 0 ]; then
      continue
    fi
    curDiskPartitions=($(lsblk --noheadings --raw -o NAME,SIZE,TYPE $curDiskID))
    for curDiskPart in "${curDiskPartitions[@]}"
    do
      curDiskPartName=$(echo $curDiskPart | cut -d" " -f1)
      curDiskPartSize=$(echo $curDiskPart | cut -d" " -f2)
      curDiskPartType=$(echo $curDiskPart | cut -d" " -f3)
      if [ "$curDiskPartType" = "part" ]; then
        scsbm_menu_items+=( "$curDiskPartName  $curDiskPartSize  ($curDiskName)" )
        scsbm_menu_items+=( "|" )
        scsbm_menu_items+=( "off" )
        ((curListNum++))
      fi
    done
  done
  IFS=$OLDIFS
  if [ $curListNum -le 1 ]; then
    showMessageBox "ERROR" "There are no available disks for this operation, returning..."
    return
  fi
  while true;
  do
    selDiskItem=$(whiptail "${scsbm_menu_items[@]}" 3>&1 1>&2 2>&3)
    if [ $? -ne 0 ]; then
      return
    fi
    if [ -z "$selDiskItem" ]; then
      showMessageBox "ERROR" "You did not make a selection, please try again."
    else
      break
    fi
  done
  selDisk=$(echo "$selDiskItem" | cut -d" " -f1)
  if [ -z "$selDisk" ]; then
    showMessageBox "ERROR" "No partition was selected, returning..."
    return
  fi
  mountDir=$(promptUserInputMenu "$HSHQ_BACKUP_DIR" "Select Directory" "Enter the directory where you would like to mount this partition:")
  if [ "$mountDir" = "$HSHQ_BACKUP_DIR" ]; then
    mkdir -p $HSHQ_BACKUP_DIR
  fi
  if [ $(checkValidDirectory "$mountDir") = "false" ]; then
    showMessageBox "ERROR" "Invalid directory name, returning..."
    return
  fi
  findmnt | grep "$mountDir" > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    showMessageBox "ERROR" "There is already a disk mounted to this directory, returning..."
    return
  fi
  if [ -d "$mountDir" ]; then
    if ! [ -z "$(ls -A $mountDir)" ]; then
      showMessageBox "ERROR" "The selected directory is not empty, returning..."
      return
    fi
  fi
  if ! [ -d "$mountDir" ]; then
    showYesNoMessageBox "ERROR" "This directory does not exist, would you like to create it now?"
    if [ $? -ne 0 ]; then
      return
    fi
    mkdir -p $mountDir
    if [ $? -ne 0 ]; then
      showMessageBox "ERROR" "There was an error creating this directory, likely a permission error, returning..."
      return
    fi
  fi
  sudo mount /dev/$selDisk $mountDir
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "There was an error mounting this partition, returning..."
    return
  fi
  showRestoreDuplicatiRestoreMenu "$mountDir" "/dev/$selDisk"
}

function showRestoreSelectEncryptedDirectoryMenu()
{
  backupDir=$(promptUserInputMenu "$HSHQ_BACKUP_DIR" "Select Directory" "Enter the top-level directory of the encrypted data:")
  showRestoreDuplicatiRestoreMenu "$backupDir"
}

function showRestoreDuplicatiRestoreMenu()
{
  set +e
  backupDirTL="$1"
  umountDisk="$2"
  curListNum=0
  dbackupmenu=$(cat << EOF

$(getLogo)
EOF
)
  OLDIFS=$IFS
  IFS=$(echo -en "\n\b")
  dupback_menu_items=( --title "Select Directory" --radiolist "$dbackupmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT )
  dirarr=($(sudo ls $backupDirTL))
  cd ~
  for curDir in "${dirarr[@]}"
  do
    if [ "$curDir" = "lost+found" ]; then
      continue
    fi
    if sudo test -d "$backupDirTL/$curDir"; then
      dupback_menu_items+=( "$curDir" )
      dupback_menu_items+=( "|" )
      dupback_menu_items+=( "off" )
      ((curListNum++))
    fi
  done
  IFS=$OLDIFS
  if [ $curListNum -lt 1 ]; then
    showMessageBox "ERROR" "There are no subdirectories for this operation, returning..."
    return
  fi
  selBackupDir=$(whiptail "${dupback_menu_items[@]}" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    return
  fi
  if [ -z "$selBackupDir" ]; then
    showMessageBox "ERROR" "No directory was selected, returning..."
    return
  fi
  if [ -d "$HSHQ_RESTORE_DIR" ]; then
    if ! [ -z "$(ls -A $HSHQ_RESTORE_DIR)" ]; then
      showMessageBox "ERROR" "The restore directory ($HSHQ_RESTORE_DIR) is not empty, returning..."
      return 1
    fi
  else
    mkdir -p $HSHQ_RESTORE_DIR
  fi
  dup_pw=$(promptPasswordMenu "Enter Passphrase" "Enter the passphrase that you used to encrypt your Duplicati backup data: ")
  confirmRestore=$(promptUserInputMenu "" "Confirm" "The decryption process is ready to start. It could take anywhere from 15 minutes to many hours to decrypt the data from the encrypted state, depending on how much data. After this step, you will be prompted for additional information. So check back in an hour or so to complete the full server restore process. Enter the word 'decrypt' below to continue:")
  if [ $? -ne 0 ] || [ -z "$confirmRestore" ] || ! [ "$confirmRestore" = "decrypt" ]; then
    showMessageBox "Incorrect Confirmation" "The text did not match, returning..."
    return
  fi
  sudo rm -fr /tmp/dupconfig
  mkdir /tmp/dupconfig
  docker container stop duplicati-restore > /dev/null 2>&1
  docker container rm duplicati-restore > /dev/null 2>&1
  docker run -d --name=duplicati-restore -v /tmp/dupconfig:/config -v $backupDirTL/$selBackupDir:/backup -v $HSHQ_RESTORE_DIR:/restore $(getScriptImageByContainerName duplicati) > /dev/null 2>&1
  echo "Recreating duplicati database..."
  #docker exec -it duplicati-restore bash -c "mono /app/duplicati/Duplicati.CommandLine.exe repair /backup --dbpath=/config/hshqrestore.sqlite --passphrase=$dup_pw"
  docker exec -it duplicati-restore bash -c "/app/duplicati/duplicati-cli repair /backup --dbpath=/config/hshqrestore.sqlite --passphrase=$dup_pw"
  is_repair_dup_error=$?
  if [ $is_repair_dup_error -ne 0 ]; then
    docker container stop duplicati-restore > /dev/null 2>&1
    docker container rm duplicati-restore > /dev/null 2>&1
    sudo rm -fr /tmp/dupconfig
    if ! [ -z "$umountDisk" ]; then
      echo "Unmounting disk ($umountDisk)..."
      sudo umount $umountDisk
    fi
    showMessageBox "ERROR" "There was a problem recreating the database, the password may be incorrect. Returning..."
    return 2
  fi
  echo "Database recreated, restoring files..."
  #docker exec -it duplicati-restore bash -c "mono /app/duplicati/Duplicati.CommandLine.exe restore /backup --dbpath=/config/hshqrestore.sqlite --restore-path=/restore --overwrite=true --restore-permissions --passphrase=$dup_pw"
  docker exec -it duplicati-restore bash -c "/app/duplicati/duplicati-cli restore /backup --dbpath=/config/hshqrestore.sqlite --restore-path=/restore --overwrite=true --restore-permissions --passphrase=$dup_pw"
  docker container stop duplicati-restore > /dev/null 2>&1
  docker container rm duplicati-restore > /dev/null 2>&1
  sudo rm -fr /tmp/dupconfig
  echo "Data Successfully Restored!"
  if ! [ -z "$umountDisk" ]; then
    echo "Unmounting disk ($umountDisk)..."
    sudo umount $umountDisk
  fi
  showRestoreUnencryptedMenu
}

function performRestorePrecheck()
{
  if [ -d $HSHQ_ASSETS_DIR ]; then
    echo "The HSHQ assets directory ($HSHQ_ASSETS_DIR) already exists. Cannot perform restore."
  fi
  if [ -d $HSHQ_CONFIG_DIR ]; then
    echo "The HSHQ config directory ($HSHQ_CONFIG_DIR) already exists. Cannot perform restore."
  fi
  if [ -d $HSHQ_RELAYSERVER_DIR ]; then
    echo "The HSHQ relayserver directory ($HSHQ_RELAYSERVER_DIR) already exists. Cannot perform restore."
  fi
  if [ -d $HSHQ_SCRIPTS_DIR ]; then
    echo "The HSHQ scripts directory ($HSHQ_SCRIPTS_DIR) already exists. Cannot perform restore."
  fi
  if [ -d $HSHQ_SECRETS_DIR ]; then
    echo "The HSHQ secrets directory ($HSHQ_SECRETS_DIR) already exists. Cannot perform restore."
  fi
  if [ -d $HSHQ_SSL_DIR ]; then
    echo "The HSHQ ssl directory ($HSHQ_SSL_DIR) already exists. Cannot perform restore."
  fi
  if [ -d $HSHQ_STACKS_DIR ]; then
    echo "The HSHQ stacks directory ($HSHQ_STACKS_DIR) already exists. Cannot perform restore."
  fi
  if [ -d $HSHQ_WIREGUARD_DIR ]; then
    echo "The HSHQ wireguard directory ($HSHQ_WIREGUARD_DIR) already exists. Cannot perform restore."
  fi
}

function performRestorePostcheck()
{
  if ! [ -d "$HSHQ_RESTORE_DIR" ]; then
    showMessageBox "ERROR" "The restore directory ($HSHQ_RESTORE_DIR) does not exist, returning..."
    return 1
  fi
  if ! [ -d "$HSHQ_RESTORE_DIR/assets" ]; then
    showMessageBox "ERROR" "The restore (assets) directory ($HSHQ_RESTORE_DIR/assets) does not exist, returning..."
    return 1
  fi
  if ! [ -d "$HSHQ_RESTORE_DIR/config" ]; then
    showMessageBox "ERROR" "The restore (config) directory ($HSHQ_RESTORE_DIR/config) does not exist, returning..."
    return 1
  fi
  if ! [ -d "$HSHQ_RESTORE_DIR/lib" ]; then
    showMessageBox "ERROR" "The restore (lib) directory ($HSHQ_RESTORE_DIR/lib) does not exist, returning..."
    return 1
  fi
  if ! [ -d "$HSHQ_RESTORE_DIR/scripts" ]; then
    showMessageBox "ERROR" "The restore (scripts) directory ($HSHQ_RESTORE_DIR/scripts) does not exist, returning..."
    return 1
  fi
  if ! [ -d "$HSHQ_RESTORE_DIR/secrets" ]; then
    showMessageBox "ERROR" "The restore (secrets) directory ($HSHQ_RESTORE_DIR/secrets) does not exist, returning..."
    return 1
  fi
  if ! [ -d "$HSHQ_RESTORE_DIR/ssl" ]; then
    showMessageBox "ERROR" "The restore (ssl) directory ($HSHQ_RESTORE_DIR/ssl) does not exist, returning..."
    return 1
  fi
  if ! [ -d "$HSHQ_RESTORE_DIR/stacks" ]; then
    showMessageBox "ERROR" "The restore (stacks) directory ($HSHQ_RESTORE_DIR/stacks) does not exist, returning..."
    return 1
  fi
  if ! [ -d "$HSHQ_RESTORE_DIR/wireguard" ]; then
    showMessageBox "ERROR" "The restore (wireguard) directory ($HSHQ_RESTORE_DIR/wireguard) does not exist, returning..."
    return 1
  fi
}

function createClientDNSNetworksOnRestore()
{
  echo "Creating ClientDNS networks..."
  if [ -z "$PORTAINER_TOKEN" ]; then
    PORTAINER_TOKEN="$(getPortainerToken -u $PORTAINER_ADMIN_USERNAME -p $PORTAINER_ADMIN_PASSWORD)"
  fi
  rsi_numTries=1
  rsi_totalTries=5
  rsi_retVal=1
  rstackIDsQry=""
  while [ $rsi_numTries -le $rsi_totalTries ]
  do
    rstackIDsQry=$(http --check-status --ignore-stdin --verify=no --timeout=300 --print="b" GET https://127.0.0.1:$PORTAINER_LOCAL_HTTPS_PORT/api/stacks?filters={\"EndpointId\":$PORTAINER_ENDPOINT_ID} "Authorization: Bearer $PORTAINER_TOKEN")
    rsi_retVal=$?
    if [ $rsi_retVal -eq 0 ]; then
      break
    fi
    ((rsi_numTries++))
  done
  if [ $rsi_retVal -ne 0 ]; then
    echo "ERROR: Could not get list of stack IDs in Portainer..." 1>&2
    return
  fi
  rstackIDs=($(echo $rstackIDsQry | jq -r '.[] | select(.Status == 1) | .Id'))
  rstackNames=($(echo $rstackIDsQry | jq -r '.[] | select(.Status == 1) | .Name'))
  numItems=$((${#rstackIDs[@]} - 1))
  for curID in $(seq 0 $numItems);
  do
    echo ${rstackNames[$curID]} | grep "^clientdns-" > /dev/null 2>&1
    if [ $? -eq 0 ]; then
      echo "Found ClientDNS stack: (${rstackIDs[$curID]}) ${rstackNames[$curID]}"
      cdns_stack_name=$(echo echo ${rstackNames[$curID]} | cut -d"-" -f2)
      clientdns_subnet=$(getConfigVarFromFile CLIENTDNS_SUBNET_PREFIX $HSHQ_STACKS_DIR/portainer/compose/${rstackIDs[$curID]}/stack.env root).0/24
      docker network create -o com.docker.network.bridge.name=brcd-${cdns_stack_name} --driver=bridge --subnet $clientdns_subnet cdns-${cdns_stack_name} > /dev/null
    fi
  done
}

function showAllUtilsMenu()
{
  set +e
  utilmenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$utilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Edit Encrypted Config File" \
  "2" "Edit Plaintext Root Config File" \
  "3" "Edit Plaintext User Config File" \
  "4" "Update APT Mirror" \
  "5" "Release All Locks" \
  "6" "Update IP and Refresh Firewall" \
  "7" "Configure Simple Backup" \
  "8" "Uninstall and Remove Everything" \
  "9" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    menures=0
  fi
  set -e
  case $menures in
    0)
      set +e
	  return 1 ;;
    1)
      set +e
      showYesNoMessageBox "WARNING" "Be VERY careful editing this file. You could very easily break things. The configuration file will be opened with the nano utility, so ensure you know how to use this before proceeding.\n\nDo you wish to continue?"
      if [ $? -ne 0 ]; then
        return 1
      fi
      checkLoadConfig
      nano $CONFIG_FILE
      loadConfigVars true
      set +e
      return 1 ;;
    2)
      set +e
      showYesNoMessageBox "WARNING" "Be VERY careful editing this file. You could very easily break things. The configuration file will be opened with the nano utility, so ensure you know how to use this before proceeding.\n\nDo you wish to continue?"
      if [ $? -ne 0 ]; then
        return 1
      fi
      checkLoadConfig
      sudo nano $HSHQ_PLAINTEXT_ROOT_CONFIG
      loadConfigVars true
      set +e
      return 1 ;;
    3)
      set +e
      showYesNoMessageBox "WARNING" "Be VERY careful editing this file. You could very easily break things. The configuration file will be opened with the nano utility, so ensure you know how to use this before proceeding.\n\nDo you wish to continue?"
      if [ $? -ne 0 ]; then
        return 1
      fi
      checkLoadConfig
      nano $HSHQ_PLAINTEXT_USER_CONFIG
      loadConfigVars true
      set +e
      return 1 ;;
    4)
      set +e
      refreshSudo
      checkUpdateAptSources true
      return 1 ;;
    5)
      set +e
      sudo -k
      USER_SUDO_PW=""
      refreshSudo
      releaseAllLocks true
      return 1 ;;
    6)
      checkLoadConfig
      checkHostAllInterfaceIPChanges true false User-Console
      checkUpdateAllIPTables User-Console
      set +e
      return 1 ;;
    7)
      checkLoadConfig
      showSimpleBackupMenu
      set +e
      return 1 ;;
    8)
      nukeHSHQ ;;
    9)
	  return 0 ;;
  esac
}

function showHSHQUtilsMenu()
{
  set +e
  utilmenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$utilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Edit Encrypted Config File" \
  "2" "Edit Plaintext Root Config File" \
  "3" "Edit Plaintext User Config File" \
  "5" "Reset Caddy Data" \
  "6" "Restart All Stacks" \
  "7" "Email Vaultwarden Credentials" \
  "8" "Email Root CA" \
  "9" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    menures=0
  fi
  set -e
  case $menures in
    0)
      set +e
	  return 1 ;;
    1)
      checkLoadConfig
      nano $CONFIG_FILE
      loadConfigVars true
      set +e
      return 1 ;;
    2)
      checkLoadConfig
      sudo nano $HSHQ_PLAINTEXT_ROOT_CONFIG
      loadConfigVars true
      set +e
      return 1 ;;
    3)
      checkLoadConfig
      nano $HSHQ_PLAINTEXT_USER_CONFIG
      loadConfigVars true
      set +e
      return 1 ;;
    4)
      checkLoadConfig
      if ! [ "$IS_INSTALLED" = "true" ]; then
        showMessageBox "System Not Installed" "You must perform a system installation first before using this utility."
        exit
      fi
      generateCertDialog
      set +e
      return 1 ;;
    5)
      checkLoadConfig
      if ! [ "$IS_INSTALLED" = "true" ]; then
        showMessageBox "System Not Installed" "You must perform a system installation first before using this utility."
        exit
      fi
      showResetCaddyMenu
      set +e
      return 1 ;;
    6)
      checkLoadConfig
      restartAllStacksDialog ;;
    7)
      checkLoadConfig
      emailVaultwardenCredentials ;;
    8)
      checkLoadConfig
      sendRootCAEmail false ;;
    9)
	  return 0 ;;
  esac
}

function showSysUtilsMenu()
{
  set +e
  utilmenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$utilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Update Linux OS and Reboot" \
  "2" "Download All Docker Images" \
  "3" "Release All Locks" \
  "4" "Check For IP Address Changes" \
  "5" "Refresh Firewall" \
  "6" "Configure Simple Backup" \
  "7" "Uninstall and Remove Everything" \
  "8" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    menures=0
  fi
  set -e
  case $menures in
    0)
      set +e
	  return 1 ;;
    1)
      performExitFunctions true
      set +e
      sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y && sudo reboot ;;
    2)
      pullDockerImages
      set +e
      return 1 ;;
    3)
      sudo -k
      set +e
      USER_SUDO_PW=""
      refreshSudo
      releaseAllLocks
      return 1 ;;
    4)
      checkLoadConfig
      checkHostAllInterfaceIPChanges true false User-Console
      set +e
      return 1 ;;
    5)
      checkLoadConfig
      checkUpdateAllIPTables User-Console
      set +e
      return 1 ;;
    6)
      checkLoadConfig
      showSimpleBackupMenu
      set +e
      return 1 ;;
    7)
      nukeHSHQ ;;
    8)
	  return 0 ;;
  esac
}

function showResetCaddyMenu()
{
  set +e
  caddymenu=$(cat << EOF

$(getLogo)
Select the Caddy instances that you wish to reset:

EOF
  )
  caddy_arr=($(docker ps -a --filter name=caddy- --format "{{.Names}}"))
  menu_items=""
  for curcaddy in "${caddy_arr[@]}"
  do
    menu_items=${menu_items}"$curcaddy | OFF "
  done
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
    menu_items=${menu_items}"RelayServer | OFF "
  fi
  sel_caddy=($(whiptail --title "Select Instances" --checklist "$caddymenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3))
  showYesNoMessageBox "Confirm Reset" "Perform this action on the selected Caddy server(s), Continue?"
  if [ $? -ne 0 ]; then
    set -e
    return 0
  fi
  set -e
  for curcaddy in "${sel_caddy[@]}"
  do
    curcaddy=$(echo $curcaddy | tr -d \")
    if [ "$curcaddy" = "RelayServer" ]; then
      resetCaddyDataRelayServer true
    else
      sudo $HSHQ_SCRIPTS_DIR/userasroot/resetCaddyContainer.sh $curcaddy
    fi
  done
}

function checkPerformSystemPrep()
{
  isPrepped=true
  for util in $UTILS_LIST; do
    if [[ "$(isProgramInstalled $util)" = "false" ]]; then
      isPrepped=false
      break
    fi
  done
  if [ "$isPrepped" = "false" ]; then
    showYesNoMessageBox "Prep System" "The system must be prepped with updates and required utilities. The system will automatically reboot upon completion and you will need to re-run this utility. Do you wish to continue?"
    if [ $? -ne 0 ]; then
      return 1
    fi
    sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y
    installDependencies
    pullBaseServicesDockerImages
    sudo reboot
  fi
}

function checkWrapperVersion()
{
  set +e
  curWrapV=$(sed -n 2p $HSHQ_WRAP_SCRIPT | cut -d"=" -f2)
  if ! [ -z "$curWrapV" ]; then
    if [ $curWrapV -lt 5 ]; then
      showMessageBox "Update Wrapper" "It appears you are using a deprecated version of the wrapper script. Please run the command: 'wget -q4N https://homeserverhq.com/hshq.sh' to obtain the latest version as soon as possible."
    fi
  fi
  set -e
}

function loadVersionVars()
{
  DISTRO_ID=$(cat /etc/*-release 2> /dev/null | grep "^ID=" | cut -d"=" -f2 | xargs | tr '[:upper:]' '[:lower:]')
  DISTRO_NAME=$(cat /etc/*-release 2> /dev/null | grep "^NAME=" | cut -d"=" -f2 | xargs)
  DISTRO_VERSION=$(cat /etc/*-release 2> /dev/null | grep "^VERSION=" | cut -d"=" -f2 | xargs)
  DISTRO_CODENAME=$(cat /etc/*-release 2> /dev/null | grep "^VERSION_CODENAME=" | cut -d"=" -f2 | xargs)
  DISTRO_ARCH=$(dpkg --print-architecture)
}

function checkSupportedHostOS()
{
  IS_DISTRO_EXP=false
  if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^22\.04. ]]; then
    return
  fi
  if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^24\.04. ]]; then
    return
  fi
  if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^26\.04. ]]; then
    return
  fi
  if [ "$DISTRO_ID" = "ubuntu" ] && [[ "$DISTRO_VERSION" =~ ^1\.1. ]] && [ "$DISTRO_NAME" = "AnduinOS" ]; then
    return
  fi
  if [ "$DISTRO_ID" = "debian" ] && [[ "$DISTRO_VERSION" =~ ^12. ]]; then
    return
  fi
  if [ "$DISTRO_ID" = "linuxmint" ] && [[ "$DISTRO_VERSION" =~ ^22. ]]; then
    return
  fi
  if [ "$IS_DISTRO_EXP" = "true" ]; then
    showYesNoMessageBox "Confirm Experimental" "This distribution of Linux is on the experimental list. It has not yet been thoroughly tested. Do you wish to continue?"
    if [ $? -eq 0 ]; then
      touch $HOME/hshq/$IS_HSHQ_TEST_FILENAME
      IS_HSHQ_DEV_TEST=true
      return
    fi
  else
    echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
    echo "@                   Unsupported Host Operating System                  @"
    echo "@                                                                      @"
    echo "@ This installation only supports the following Linux distribution(s): @"
    echo "@                                                                      @"
    echo "@  - Ubuntu 22.04 (Jammy Jellyfish)                                    @"
    echo "@  - Ubuntu 24.04 (Noble Numbat)                                       @"
    echo "@  - AnduinOS 1.1 (Noble Numbat)                                       @"
    echo "@  - Debian 12 (Bookworm)                                              @"
    echo "@  - Mint 22 (Wilma)                                                   @"
    echo "@                                                                      @"
    echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
  fi
  releaseLock hshqopen "checkSupportedHostOS" false
  exit 2
}

function updateSysctl()
{
  isPerfUpdate=$1
  set +e
  sudo rm -f /etc/sysctl.d/88-hshq.conf
  sudo tee /etc/sysctl.d/88-hshq.conf >/dev/null <<EOFSC
kernel.panic = 10
fs.file-max = 10000000
fs.nr_open = 10000000
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches = 524288
vm.swappiness = 10
net.core.somaxconn = 65536
net.mptcp.enabled = 0
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_loose = 0
net.netfilter.nf_conntrack_tcp_timeout_established = 1800
net.netfilter.nf_conntrack_tcp_timeout_close = 10
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 10
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 20
net.netfilter.nf_conntrack_tcp_timeout_last_ack = 20
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 20
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 20
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 10
net.ipv4.route.flush = 1
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_rfc1337 = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5
net.ipv4.tcp_timestamps = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.ip_forward = 1
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1

# Reserve for Coturn and any other ports above 10000
net.ipv4.ip_local_reserved_ports = $JITSI_MEET_PORT,$KHOJ_VNC_PORT,$COTURN_COMMS_MIN_PORT-$COTURN_COMMS_MAX_PORT,$SYNCTHING_SYNC_PORT,$SYNCTHING_DISC_PORT,$WAZUH_PORT_4

# See https://community.home-assistant.io/t/zeroconf-error/153883

# Bigger buffers (to make 40Gb more practical). These are maximums, but the default is unaffected.
net.core.wmem_max=268435456
net.core.rmem_max=268435456
net.core.netdev_max_backlog=10000

# Force IGMP v2 (required by CBF switch)
net.ipv4.conf.all.force_igmp_version=2
net.ipv4.conf.default.force_igmp_version=2

# Increase the ARP cache table
net.ipv4.neigh.default.gc_thresh3=4096
net.ipv4.neigh.default.gc_thresh2=2048
net.ipv4.neigh.default.gc_thresh1=1024

# Increase number of multicast groups permitted
net.ipv4.igmp_max_memberships=1024

# See https://documentation.wazuh.com/current/deployment-options/docker/docker-installation.html
vm.max_map_count = 262144

EOFSC
  if ! [ "$isPerfUpdate" = "false" ]; then
    sudo sysctl --system > /dev/null 2>&1
  fi
}

function createSwapfile()
{
  set +e
  sudo grep -q swap /etc/fstab > /dev/null 2>&1
  if [ $? -ne 0 ] && ! [ -f /swapfile ]; then
    sudo fallocate -l $HOMESERVER_SWAP_SIZE /swapfile
    sudo chmod 600 /swapfile
    sudo mkswap /swapfile
    sudo swapon /swapfile
    echo "# Swap file" | sudo tee -a /etc/fstab > /dev/null 2>&1
    echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1
  fi
}

function updateLogrotateConfig()
{
  ulrc_curE=${-//[^e]/}
  set +e
  # Set max size of syslog to 5G
  grep maxsize /etc/logrotate.d/rsyslog > /dev/null 2>&1
  if [ $? -ne 0 ]; then
    sudo sed -i "/weekly$/a\        maxsize 5G" /etc/logrotate.d/rsyslog
  fi
  # Move logrotate into hourly
  if [ -f /etc/cron.daily/logrotate ]; then
    sudo mv /etc/cron.daily/logrotate /etc/cron.hourly
  fi
  # Set conditions for docker container logs
  sudo tee /etc/logrotate.d/docker >/dev/null <<EOFLR
/var/log/docker/*.log {
    weekly
    maxsize 100M
    rotate 4
    missingok
    notifempty
}
EOFLR
  
  sudo systemctl restart logrotate
  if ! [ -z "$ulrc_curE" ]; then
    set -e
  fi
}

function updateMOTD()
{
  cat <<EOFMD > $HOME/88-hshq
#!/bin/bash

echo
echo "  #===============================================================#"
echo "  # ▀█  █  ▄▄  ▄▄ ▄▄ ▄▄▄ █▀▀▀█ ▄▄▄ ▄▄▄  ▄   ▄ ▄▄▄ ▄▄▄  █  █ █▀▀█  #"
echo "  #  █▀▀█ █  █ █ █ █ ▄▄  ▀▀▀▄▄ ▄▄  ▄▄▄▀ ▀▄ ▄▀ ▄▄  ▄▄▄▀ █▀▀█ █  █  #"
echo "  #  █  █ ▀▄▄▀ █   █ ▄▄▄ █▄▄▄█ ▄▄▄ ▄▄▄▄  ▀█▀  ▄▄▄ ▄▄▄▄ █  █ █▄▄█▄ #"
echo "  #===============================================================#"
echo

echo "      Linux OS:  \$(lsb_release -d | cut -d":" -f2 | cut -d"(" -f1 | xargs)"
if [ -f $HSHQ_LIB_SCRIPT ]; then
  echo "  HSHQ Version:  \$(sed -n 2p $HSHQ_LIB_SCRIPT | cut -d'=' -f2)"
fi
printf "  Memory Usage:  %.1f%% of \$(free -h | awk  '/Mem:/{print \$2}')\n" \$((10000 - \$((10**4 * \$(grep "MemAvailable" /proc/meminfo | xargs | cut -d" " -f2) / \$(grep "MemTotal" /proc/meminfo | xargs | cut -d" " -f2)))))e-2
totSwap=\$(grep "SwapTotal" /proc/meminfo | xargs | cut -d" " -f2)
if [ \$totSwap -gt 0 ]; then
  printf "    Swap Usage:  %.1f%% of \$(free -h | awk  '/Swap:/{print \$2}')\n" \$((10000 - \$((10**4 * \$(grep "SwapFree" /proc/meminfo | xargs | cut -d" " -f2) / \$totSwap))))e-2
fi

echo
echo "  Disks: "
echo "  -----------------------------------------------------------------"
echo "  Filesystem            Size    Used    Avail   Use%    Mounted on"
echo "  -----------------------------------------------------------------"
df -hP | grep "^/dev" | awk '{printf "  %-21s %-7s %-7s %-7s %-7s %-8s\n", \$1, \$2, \$3, \$4, \$5, \$6}'
echo

# Let's only show zombie count if greater than 10
zombie_count=\$(ps aux | grep "defunct" | wc -l)
if [ \$zombie_count -gt 10 ]; then
  echo "  => There are \$zombie_count zombie processes."
  echo
fi

echo "  Enter 'bash $HSHQ_WRAP_FILENAME' to run the HomeServerHQ script."
echo
EOFMD
  chmod 755 $HOME/88-hshq
  sudo chown root:root $HOME/88-hshq
  sudo chmod -x /etc/update-motd.d/*
  sudo mv $HOME/88-hshq /etc/update-motd.d/
  if [ -f /etc/motd ]; then
    sudo mv /etc/motd /etc/motd.old
  fi
}

function performSuggestedSecUpdates()
{
  disa_curE=${-//[^e]/}
  set +e
  # This function continues the process of reducing the failed checks in the Wazuh SCA scan
  # It's mostly low hanging fruit, but reducing the list of exceptions will help to focus
  # on the more important ones.
  grep "enabled=1" /etc/default/apport > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    sudo sed -i "s/enabled=1/enabled=0/" /etc/default/apport
    sudo systemctl stop apport
    sudo systemctl --now disable apport
    sudo systemctl daemon-reload
    #sudo systemctl mask apport
  fi
  echo "Authorized uses only." | sudo tee /etc/issue > /dev/null 2>&1
  echo "Authorized uses only." | sudo tee /etc/issue.net > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y telnet > /dev/null 2>&1
  # Assume the caller will restart sshd - don't want to risk breaking the installation process
  sudo sed -i "s/^#MaxAuthTries.*/MaxAuthTries 4/" /etc/ssh/sshd_config
  sudo sed -i "s/^#Banner none.*/Banner \/etc\/issue.net/" /etc/ssh/sshd_config
  sudo sed -i "s/^#MaxStartups.*/MaxStartups 10:30:60/" /etc/ssh/sshd_config
  sudo sed -i "s/^#LoginGraceTime.*/LoginGraceTime 60/" /etc/ssh/sshd_config
  sudo sed -i "s/^#AllowTcpForwarding yes.*/AllowTcpForwarding no/" /etc/ssh/sshd_config
  sudo chown root:root /etc/crontab
  sudo chmod og-rwx /etc/crontab
  sudo chown root:root /etc/cron.hourly/
  sudo chmod og-rwx /etc/cron.hourly/
  sudo chown root:root /etc/cron.daily/
  sudo chmod og-rwx /etc/cron.daily/
  sudo chown root:root /etc/cron.weekly/
  sudo chmod og-rwx /etc/cron.weekly/
  sudo chown root:root /etc/cron.monthly/
  sudo chmod og-rwx /etc/cron.monthly/
  sudo chown root:root /etc/cron.d/
  sudo chmod og-rwx /etc/cron.d/
  if ! [ -z "$disa_curE" ]; then
    set -e
  fi
}

function performAptInstall()
{
  if [ "$1" = "yq" ]; then
    installYQ
    return
  fi
  sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' $1
}

function installYQ()
{
  sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
  sudo chmod a+x /usr/local/bin/yq
}

function installDependencies()
{
  set +e
  createHSHQLog
  sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1
  upgradeHostOS
  sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1
  # Install utils
  installHostNTPServer
  for util in $UTILS_LIST; do
    if [[ "$(isProgramInstalled $util)" = "false" ]]; then
      lib_name=$(echo $util | cut -d"|" -f2)
      echo "Installing $lib_name, please wait..."
      performAptInstall $lib_name
    fi
  done
  if ! [ -f /etc/rsyslog.d/docker-logs.conf ]; then
    # Install Rsyslog
    sudo systemctl status rsyslog > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      echo "Installing rsyslog, please wait..."
      performAptInstall rsyslog
      sudo systemctl enable rsyslog
      sudo systemctl start rsyslog
    fi
  fi
  # Some host tuning
  updateSysctl true
  updateLogrotateConfig
  sudo sed -i "s|^ClientAliveInterval .*$|ClientAliveInterval 15|g" /etc/ssh/sshd_config
  sudo sed -i "s|^ClientAliveCountMax .*$|ClientAliveCountMax 3|g" /etc/ssh/sshd_config
  sudo sed -i "s|^PermitEmptyPasswords .*$|PermitEmptyPasswords no|g" /etc/ssh/sshd_config
  sudo sed -i "s|^#*ClientAliveInterval .*$|ClientAliveInterval 15|g" /etc/ssh/sshd_config
  sudo sed -i "s|^#*ClientAliveCountMax .*$|ClientAliveCountMax 3|g" /etc/ssh/sshd_config
  sudo sed -i "s|^#*PermitEmptyPasswords .*$|PermitEmptyPasswords no|g" /etc/ssh/sshd_config
  sudo sed -i "s/^#DefaultLimitNOFILE.*/DefaultLimitNOFILE=65536:524288/g" /etc/systemd/system.conf
  sudo sed -i "s/^#DefaultLimitNOFILE.*/DefaultLimitNOFILE=65536:524288/g" /etc/systemd/user.conf
  sudo sed -i "s/^DefaultLimitNOFILE.*/DefaultLimitNOFILE=65536:524288/g" /etc/systemd/system.conf
  sudo sed -i "s/^DefaultLimitNOFILE.*/DefaultLimitNOFILE=65536:524288/g" /etc/systemd/user.conf
  sudo grep "* - nofile" /etc/security/limits.conf >/dev/null
  if [ $? -ne 0 ]; then
    echo "* - nofile 65536" | sudo tee -a /etc/security/limits.conf >/dev/null
  fi
  sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target > /dev/null 2>&1
  if [ "$IS_DESKTOP_ENV" = "true" ]; then
    for util in $DESKTOP_APT_LIST; do
      if [[ "$(isProgramInstalled $util)" = "false" ]]; then
        lib_name=$(echo $util | cut -d"|" -f2)
        echo "Installing $lib_name, please wait..."
        performAptInstall $lib_name
      fi
    done
  fi
  installDocker
  curl -fsSL https://gvisor.dev/archive.key | sudo gpg --yes --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list > /dev/null 2>&1
  sudo apt-get update > /dev/null 2>&1
  performAptInstall runsc > /dev/null 2>&1
  for rem_util in $APT_REMOVE_LIST; do
    sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y $rem_util
  done
  sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y
  set -e
}

function removeBSDMailx()
{
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y bsd-mailx
  sudo tee /etc/apt/preferences.d/bsd-mailx >/dev/null <<EOFSM
Package: bsd-mailx
Pin: release *
Pin-Priority: -1
EOFSM
  sudo apt update > /dev/null 2>&1
}

function installMailUtils()
{
  set +e
  echo "Removing bsd-mailx, please wait..."
  removeBSDMailx
  echo "Installing ssmtp, please wait..."
  performAptInstall ssmtp
  echo "Installing mailx, please wait..."
  performAptInstall mailutils
  sudo tee /etc/ssmtp/ssmtp.conf >/dev/null <<EOFSM
root=$EMAIL_ADMIN_EMAIL_ADDRESS
mailhub=${SUB_POSTFIX}.${HOMESERVER_DOMAIN}:$MAILU_PORT_5
hostname=$(cat /etc/hostname)
TLS_CA_FILE=/etc/ssl/certs/ca-certificates.crt
UseSTARTTLS=yes
FromLineOverride=no
AuthUser=$EMAIL_SMTP_EMAIL_ADDRESS
AuthPass=$EMAIL_SMTP_PASSWORD
EOFSM
  sudo tee /etc/ssmtp/revaliases >/dev/null <<EOFSM
root:$EMAIL_SMTP_EMAIL_ADDRESS
$USERNAME:$EMAIL_SMTP_EMAIL_ADDRESS
EOFSM
  getent group mailsenders >/dev/null || sudo groupadd mailsenders 
  sudo usermod -aG mailsenders $USERNAME
  sudo chown root:mailsenders /usr/bin/mail.mailutils
  sudo update-alternatives --set mailx /usr/bin/mail.mailutils
}

function installSQLite344()
{
  if [ -d $HSHQ_NONBACKUP_DIR/sqlite344 ]; then
    return
  fi
  echo "Installing sqlite (v3.44) from source, this will take a few minutes..."
  cd $HSHQ_NONBACKUP_DIR
  wget https://www.sqlite.org/2023/sqlite-autoconf-3440000.tar.gz > /dev/null 2>&1
  tar xvfz sqlite-autoconf-3440000.tar.gz > /dev/null 2>&1
  rm -f sqlite-autoconf-3440000.tar.gz
  sudo mv sqlite-autoconf-3440000 sqlite344
  cd sqlite344
  ./configure > /dev/null 2>&1
  make -j $(nproc) > /dev/null 2>&1
  sudo make install > /dev/null 2>&1
  cd ~
  $HSHQ_NONBACKUP_DIR/sqlite344/sqlite3 --version > /dev/null 2>&1
  rtv=$?
  if [ $rtv -ne 0 ]; then
    return $rtv
  else
    echo "Sqlite (v3.44) installation complete!"
  fi
}

function checkLoadConfig()
{
  set -e
  if ! [ -z "$ENC_CONFIG_FILE" ] && ! [ -f "$ENC_CONFIG_FILE" ]; then
    ENC_CONFIG_FILE=""
  fi
  if [ -z "$CONFIG_FILE" ] || ! [ -f "$CONFIG_FILE" ]; then
    DEF_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME
    if [ -f $DEF_CONFIG_FILE ]; then
      CONFIG_FILE=$DEF_CONFIG_FILE
      ENC_CONFIG_FILE=""
    else
      CONFIG_FILE=""
    fi
  fi
  if [ -z "$ENC_CONFIG_FILE" ] && [ -z "$CONFIG_FILE" ]; then
    if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME ]; then
      ENC_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME
    fi
  fi
  set +e
  if ! [ -z "$ENC_CONFIG_FILE" ]; then
    showYesNoMessageBox "Encrypted Config Found" "Encrypted configuration file found. Do you wish to decrypt it?"
    if [ $? -ne 0 ]; then
      releaseLock hshqopen "checkLoadConfig" false
      exit 1
    fi
    # Prompt for config password, test it.
    # Check if sudo, if not, get it and test it.
    # If either fails, exit.
    confirmUnlockCreds
    if [ $? -ne 0 ]; then
      releaseLock hshqopen "checkLoadConfig" false
      exit 1
    fi
    decryptConfigFile "$ENC_CONFIG_FILE"
    ENC_CONFIG_FILE=""
  fi
  confirmUnlockCreds
  if [ $? -ne 0 ]; then
    releaseLock hshqopen "checkLoadConfig" false
    exit 1
  fi
  set -e
  if ! [ -z "$CONFIG_FILE" ] && [ -f $CONFIG_FILE ]; then
    loadConfigVars true
  fi
}

function loadConfigConsole()
{
  ENC_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME
  if ! [ -f $ENC_CONFIG_FILE ]; then
    echo "Config file does not exist, exiting..."
    exit 1
  fi
  if ! [ -z "$USER_CONFIG_PW" ]; then
    checkDecryptConfigFile > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      USER_CONFIG_PW=""
    fi
  fi
  while [ -z "$USER_CONFIG_PW" ]
  do
    read -r -s -p "Enter your config decrypt password: " USER_CONFIG_PW
    retVal=$?
    if [ $retVal -ne 0 ]; then
      return 3
    fi
    checkDecryptConfigFile > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      echo "The password is incorrect, please re-enter it."
      USER_CONFIG_PW=""
    fi
  done
  decryptAndLoad false
}

function confirmUnlockCreds()
{
  set +e
  refreshSudo
  if [ $? -ne 0 ]; then
    return 3
  fi
  if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME ]; then
    CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME
    IS_CONFIG_FILE_UNENCRYPTED=true
    return
  fi
  if ! [ -f $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME ]; then
    # Assume this is pre-install, so just return
    return
  fi
  promptTestDecryptConfigFile
  if [ $? -ne 0 ]; then
    return 3
  fi
}

function refreshSudo()
{
  rsu_curE=${-//[^e]/}
  set +e
  sudo -k
  if ! [ -z "$USER_SUDO_PW" ]; then
    echo "$USER_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1
    if [ $? -ne 0 ]; then
      USER_SUDO_PW=""
    fi
  fi
  while [ -z "$USER_SUDO_PW" ]
  do
    if [ "$IS_CONSOLE_ENV" = "true" ]; then
      USER_SUDO_PW=$(promptPasswordMenu "Enter Password" "Enter the sudo password for $USERNAME: ")
      retVal=$?
      if [ $retVal -ne 0 ]; then
        exit 3
      fi
      echo "$USER_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1
      if [ $? -ne 0 ]; then
        showMessageBox "Incorrect Password" "The password is incorrect, please re-enter it."
        USER_SUDO_PW=""
      fi
    else
      echo
      read -r -s -p "Enter the sudo password for $USERNAME: " USER_SUDO_PW
      echo
      echo "$USER_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1
      if [ $? -ne 0 ]; then
        echo "The password is incorrect, please re-enter it."
        USER_SUDO_PW=""
      fi
    fi
  done
  if ! [ -z "$rsu_curE" ]; then
    set -e
  fi
}

function checkConfigAvailable()
{
  if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME ]; then
    CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$CONFIG_FILE_DEFAULT_FILENAME
    IS_CONFIG_FILE_UNENCRYPTED=true
    return
  fi
  if [ -f $CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME ]; then
    ENC_CONFIG_FILE=$CONFIG_FILE_DEFAULT_LOCATION/$ENCRYPTED_CONFIG_FILE_DEFAULT_FILENAME
    IS_CONFIG_FILE_UNENCRYPTED=false
    return
  fi
}

function initConfig()
{
  if [ "$IS_INSTALLING" = "true" ]; then
    showMessageBox "Active Installation" "There is an active installation in progress, exiting..."
    exit 1
  fi
  if [ "$IS_INSTALLED" = "true" ]; then
    return 0
  fi
  if [[ "$(isProgramInstalled sqlite3)" = "false" ]]; then
    echo "Installing sqlite3, please wait..."
    performAptInstall sqlite3 > /dev/null 2>&1
  fi
  if [[ "$(isProgramInstalled sipcalc)" = "false" ]]; then
    echo "Installing sipcalc, please wait..."
    performAptInstall sipcalc > /dev/null 2>&1
  fi
  if [[ "$(isProgramInstalled jq)" = "false" ]]; then
    echo "Installing jq, please wait..."
    performAptInstall jq > /dev/null 2>&1
  fi
  if [ -z "$CONFIG_FILE" ]; then
    # Create new directories and config file
    createInitialEnv
    loadConfigVars true
  fi
  set +e
  if [ -z "$IS_ACCEPT_DEFAULTS" ]; then
    showYesNoMessageBox "Accept Defaults?" "Do you wish to use defaults where applicable?"
    mbres=$?
    if [ $mbres -eq 0 ]; then
      IS_ACCEPT_DEFAULTS=yes
    else
      IS_ACCEPT_DEFAULTS=no
    fi
  fi
  total_ram=$(free --giga | grep "Mem:" | xargs | cut -d" " -f2)
  if [ $total_ram -lt 4 ]; then
    showMessageBox "Insufficient Memory" "Total memory is $total_ram GB. You must have at least 4 GB to perform the installation, exiting..."
    exit 1
  elif [ $total_ram -lt 8 ]; then
    showYesNoMessageBox "Insufficient Memory" "Total memory is $total_ram GB. You should have at least 8 GB to perform the installation. The process could fail or your server could crash. Are you sure you want to continue?"
    mbres=$?
    if [ $mbres -ne 0 ]; then
      exit 1
    fi
  fi
  # Need to allow for docker images already downloaded, so look at total disk space.
  # Obviously 120 != 150, but this allows a little leeway.
  # The partitioning structure could be different than expected, so user can bypass this warning.
  total_disk_space=$(($(df / | tail -1 | xargs | cut -d" " -f2) / 1048576))
  if [ $total_disk_space -lt 11 ]; then
    showMessageBox "Insufficient Disk Space" "You must have at least 16GB of disk space to perform the base installation, exiting..."
    exit 1
  elif [ $total_disk_space -lt 120 ]; then
    showYesNoMessageBox "Insufficient Disk Space" "Total size of disk is $total_disk_space GB. You should have at least 150 GB of available space to perform the installation. The process could fail or your server could crash. Are you sure you want to continue?"
    mbres=$?
    if [ $mbres -ne 0 ]; then
      exit 1
    fi
  fi
  if [[ "$(isProgramInstalled grepcidr)" = "false" ]]; then
    echo "Installing grepcidr, please wait..."
    performAptInstall grepcidr > /dev/null 2>&1
  fi
  is_add_error=false
  default_iface=$(getDefaultIface)
  add_interface=""
  while [ -z "$add_interface" ]
  do
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ] && [ "$is_add_error" = "false" ]; then
      add_interface=$default_iface
    else
      add_interface=$(promptUserInputMenu "$default_iface" "Enter Primary Interface" "Enter the primary network interface. The interface for the default route has been entered for you.")
      if [ $? -ne 0 ]; then
        exit 5
      fi
      if [ -z "$add_interface" ]; then
        showMessageBox "Interface Error" "The interface cannot be empty."
        add_interface=""
        continue
      elif [ $(checkValidStringUpperLowerNumbers "$add_interface" "-") = "false" ]; then
        showMessageBox "Interface Error" "The interface contains invalid characters. It must consist of a-z (upper or lowercase), 0-9, and/or -"
        add_interface=""
        continue
      elif [ $(getStringLength "$add_interface") -gt 15 ]; then
        showMessageBox "Interface Error" "The interface name is too long, it must be 15 characters or less."
        add_interface=""
        continue
      fi
    fi
    set +e
    addHSInterface $add_interface true
    retVal=$?
    if [ $retVal -ne 0 ] && [ $retVal -ne 1 ]; then
      ip_addr=$(getIPAddressOfInterface $add_interface)
      is_private=$(checkIsIPPrivate "$ip_addr")
      chk_subnet=$(getSubnetOfInterface "$add_interface" "$ip_addr" "$is_private")
      if ! [ -z "$chk_subnet" ] && [ "$(checkNetworkIntersect $chk_subnet 10.0.0.0/8)" = "true" ] && [ $(echo "$chk_subnet" | cut -d"/" -f2) -eq 8 ]; then
        showMessageBox "Default Interface Error" "You are using the ENTIRE 10.0.0.0/8 network for your home network - a terribly poor design choice. You will not be able to host a VPN or network with anyone. If you want to bypass these safeguards and proceed, then change your settings in /etc/netplan to a static IP and a smaller network and restart the installation."
        exit 6
      else
        showMessageBox "Default Interface Error" "There was an error with the default interface"
      fi
      add_interface=""
      is_add_error=true
    fi
    set -e
  done
  if [ "$(checkIsIPPrivate $HOMESERVER_HOST_PRIMARY_INTERFACE_IP)" = "false" ]; then
    if [ -z "$CONNECTING_IP" ]; then
      showMessageBox "Error with Connecting IP" "Could not determine connecting IP, exiting..."
      exit 1
    else
      addHomeNetIP ${CONNECTING_IP}/32 false
    fi
  fi
  if [ -z "$USERID" ]; then
    USERID=$(id -u)
    updatePlaintextRootConfigVar USERID $USERID
  fi
  if [ -z "$GROUPID" ]; then
    GROUPID=$(id -g)
    updatePlaintextRootConfigVar GROUPID $GROUPID
  fi
  if [ -z "$XDG_RUNTIME_DIR" ]; then
    XDG_RUNTIME_DIR="/run/user/$USERID"
    updateConfigVar XDG_RUNTIME_DIR $XDG_RUNTIME_DIR
  fi
  if ! [ "$IS_AUTO_INSTALL" = "true" ] && [ "$HSHQ_APP_TYPE" = "home" ]; then
    selecttypemenu=$(cat << EOF

$(getLogo)

Is this server for your home or business?
EOF
)
    menures=$(whiptail --title "Select an option" --menu "$selecttypemenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
    "1" "Home" \
    "2" "Business" 3>&1 1>&2 2>&3)
    if [ $? -ne 0 ]; then
      menures=1
    fi
    case $menures in
      1)
        HSHQ_APP_TYPE=home ;;
      2)
        HSHQ_APP_TYPE=business ;;
    esac
  fi
  updatePlaintextRootConfigVar HSHQ_APP_TYPE $HSHQ_APP_TYPE
  if [ -z "$DISABLED_SERVICES" ]; then
    if [ "$HSHQ_APP_TYPE" = "business" ]; then
      if [ $total_ram -lt 8 ]; then
        DISABLED_SERVICES=$DS_MEM_LOW
      elif [ $total_ram -lt 12 ]; then
        DISABLED_SERVICES=$BDS_MEM_12
      elif [ $total_ram -lt 16 ]; then
        DISABLED_SERVICES=$BDS_MEM_16
      elif [ $total_ram -lt 22 ]; then
        DISABLED_SERVICES=$BDS_MEM_22
      elif [ $total_ram -lt 28 ]; then
        DISABLED_SERVICES=$BDS_MEM_28
      else
        DISABLED_SERVICES=$BDS_MEM_HIGH
      fi
    else
      if [ $total_ram -lt 8 ]; then
        DISABLED_SERVICES=$DS_MEM_LOW
      elif [ $total_ram -lt 12 ]; then
        DISABLED_SERVICES=$DS_MEM_12
      elif [ $total_ram -lt 16 ]; then
        DISABLED_SERVICES=$DS_MEM_16
      elif [ $total_ram -lt 22 ]; then
        DISABLED_SERVICES=$DS_MEM_22
      elif [ $total_ram -lt 28 ]; then
        DISABLED_SERVICES=$DS_MEM_28
      else
        DISABLED_SERVICES=$DS_MEM_HIGH
      fi
    fi
    updatePlaintextRootConfigVar DISABLED_SERVICES $DISABLED_SERVICES
  fi
  replaceType="HomeServer"
  if [ "$HSHQ_APP_TYPE" = "business" ]; then
    replaceType="company"
  fi
  while [ -z "$HOMESERVER_DOMAIN" ]
  do
	HOMESERVER_DOMAIN=$(promptUserInputMenu "" "Enter Domain" "Enter your $replaceType domain name, i.e. example.com. You must own this domain to route external emails and/or post services on the public internet: ")
	if [ -z "$HOMESERVER_DOMAIN" ]; then
	  showMessageBox "Domain Empty" "The domain cannot be empty"
    elif [ $(checkValidString "$HOMESERVER_DOMAIN" ".-") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The domain contains invalid character(s). It must consist of a-z (lowercase), 0-9, and/or -."
      HOMESERVER_DOMAIN=""
    elif [ $(checkValidBaseDomain "$HOMESERVER_DOMAIN") = "false" ]; then
      showMessageBox "Invalid Domain Name" "Invalid domain name. The base domain must be of the format 'example.com'. It cannot start or end with a period."
      HOMESERVER_DOMAIN=""
    elif [ $(getDomainTLD "$HOMESERVER_DOMAIN") = "test" ]; then
      showMessageBox "Invalid Domain" "The specific TLD .test actually causes some issues. Change it to .tester or something else."
      HOMESERVER_DOMAIN=""
	fi
  done
  updatePlaintextRootConfigVar HOMESERVER_DOMAIN $HOMESERVER_DOMAIN
  new_hostname="HomeServer-$(echo $HOMESERVER_DOMAIN | sed 's/\./-/g')"
  if [ -z "$(cat /etc/hosts | grep $new_hostname)" ]; then
    echo "127.0.1.1 $new_hostname" | sudo tee -a /etc/hosts
  fi
  sudo hostnamectl set-hostname $new_hostname
  rm -f $HOME/dead.letter
  EXT_DOMAIN_PREFIX=""
  while [ -z "$EXT_DOMAIN_PREFIX" ]
  do
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      EXT_DOMAIN_PREFIX="external"
    else
      EXT_DOMAIN_PREFIX=$(promptUserInputMenu "external" "External Domain Prefix" "Enter the prefix for your domain to be reached externally:")
    fi
	if [ -z "$EXT_DOMAIN_PREFIX" ]; then
	  showMessageBox "Prefix Empty" "The Prefix cannot be empty"
    elif [ $(checkValidString "$EXT_DOMAIN_PREFIX" "-") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The prefix contains invalid character(s). It must consist of a-z (lowercase), 0-9, and/or -"
      EXT_DOMAIN_PREFIX=""
	else
	  updatePlaintextRootConfigVar EXT_DOMAIN_PREFIX $EXT_DOMAIN_PREFIX
	fi
  done
  INT_DOMAIN_PREFIX=""
  while [ -z "$INT_DOMAIN_PREFIX" ]
  do
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      INT_DOMAIN_PREFIX="internal"
    else
      INT_DOMAIN_PREFIX=$(promptUserInputMenu "internal" "Internal Domain Prefix" "Enter the prefix for your RelayServer to be reached internally (if applicable):")
    fi
	if [ -z "$INT_DOMAIN_PREFIX" ]; then
	  showMessageBox "Prefix Empty" "The Prefix cannot be empty"
    elif [ $(checkValidString "$INT_DOMAIN_PREFIX" "-") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The prefix contains invalid character(s). It must consist of a-z (lowercase), 0-9, and/or -"
      INT_DOMAIN_PREFIX=""
	else
	  updatePlaintextRootConfigVar INT_DOMAIN_PREFIX $INT_DOMAIN_PREFIX
	fi
  done
  while [ -z "$HOMESERVER_NAME" ]
  do
	HOMESERVER_NAME=$(promptUserInputMenu "" "Enter Name" "Enter your $replaceType name with desired formatting, i.e. capitalization, spaces, etc. No special characters except for commas, hyphens, or periods. This will appear in window titles, certificates, among other things: ")
	if [ -z "$HOMESERVER_NAME" ]; then
	  showMessageBox "Name Empty" "The name cannot be empty"
    elif [ $(checkValidStringUpperLowerNumbers "$HOMESERVER_NAME" "[:space:],.-") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The name contains invalid character(s)."
      HOMESERVER_NAME=""
	fi
  done
  updatePlaintextRootConfigVar HOMESERVER_NAME "$HOMESERVER_NAME"
  while [ -z "$HOMESERVER_ABBREV" ]
  do
    HOMESERVER_ABBREV=$(promptUserInputMenu "" "Enter Abbrev" "Enter an abbreviation for your $replaceType, 3 to 10 lowercase alpha-numeric characters. This will be used as a default prefix for admin usernames as well as some other uses: ")
    if [ -z "$HOMESERVER_ABBREV" ]; then
      showMessageBox "Abbrev Empty" "The abbreviation cannot be empty."
    elif [ $(checkValidString "$HOMESERVER_ABBREV") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The abbreviation contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9"
      HOMESERVER_ABBREV=""
    elif [ ${#HOMESERVER_ABBREV} -lt 3 ] || [ ${#HOMESERVER_ABBREV} -gt 10 ]; then
      showMessageBox "Invalid Length" "The abbreviation must be 3 to 10 characters"
      HOMESERVER_ABBREV=""
    fi
  done
  updatePlaintextRootConfigVar HOMESERVER_ABBREV $HOMESERVER_ABBREV
  while [ -z "$HOMESERVER_CURRENCY_CODE" ]
  do
    if [ "$IS_AUTO_INSTALL" = "true" ]; then
      HOMESERVER_CURRENCY_CODE="USD"
      break
    fi
	HOMESERVER_CURRENCY_CODE=$(promptUserInputMenu "USD" "Enter Currency Code" "Enter your currency code: ")
	if [ -z "$HOMESERVER_CURRENCY_CODE" ]; then
	  showMessageBox "Code Empty" "The code cannot be empty"
    elif [ $(checkValidCurrencyCode "$HOMESERVER_CURRENCY_CODE") = "false" ]; then
      showMessageBox "Invalid Code" "The code is invalid."
      HOMESERVER_CURRENCY_CODE=""
	fi
  done
  updatePlaintextRootConfigVar HOMESERVER_CURRENCY_CODE "$HOMESERVER_CURRENCY_CODE"
  priorTZ=$(readlink /etc/localtime | sed 's|.*/zoneinfo/||')
  while [ -z "$TZ" ]
  do
    TZ=$(promptUserInputMenu "$priorTZ" "Enter Time Zone" "Enter your time zone. For a list of all time zones, visit https://en.wikipedia.org/wiki/List_of_tz_database_time_zones and enter the value in the [TZ database name] column: ")
    if [ $? -ne 0 ]; then
      exit 2
    fi
    if [ -z "$TZ" ]; then
      showMessageBox "Time Zone Empty" "The time zone cannot be empty"
    elif [ $(checkValidStringUpperLowerNumbers "$TZ" "/_+-") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The timezone contains invalid character(s)."
      TZ=""
    else
      set +e
      sudo timedatectl set-timezone "$TZ" > /dev/null 2>&1
      if [ $? -ne 0 ]; then
        showMessageBox "Invalid Time Zone" "The time zone is invalid. Please enter a valid time zone."
        TZ=""
        sudo timedatectl set-timezone "$priorTZ" > /dev/null 2>&1
      fi
      set -e
    fi
  done
  updatePlaintextRootConfigVar TZ $TZ
  echo "$TZ" | sudo tee /etc/timezone > /dev/null 2>&1
  sudo chmod 0644 /etc/timezone
  set +e
  CURRENT_SSH_PORT=$(grep "^Port" /etc/ssh/sshd_config)
  if [ $? -eq 0 ]; then
    CURRENT_SSH_PORT=$(echo $CURRENT_SSH_PORT | cut -d " " -f2)
    if ! [ "$CURRENT_SSH_PORT" = "22" ]; then
      SSH_PORT=$CURRENT_SSH_PORT
    fi
  else
    CURRENT_SSH_PORT=22
  fi
  updatePlaintextRootConfigVar CURRENT_SSH_PORT $CURRENT_SSH_PORT
  while [ -z "$SSH_PORT" ]
  do
    SSH_PORT=$(promptUserInputMenu $((9000 + $RANDOM % 999)) "Enter SSH Port" "It is highly advised to change your default SSH port (22), since bots will constantly probe port 22. A random port between 9000-9999 has been generated for you. Ensure you remember this port in order to log back in.")
    if [ -z "$SSH_PORT" ]; then
      showMessageBox "SSH Port Empty" "The SSH port cannot be empty"
    elif [ "$(checkValidNumber $SSH_PORT)" = "false" ]; then
      showMessageBox "Invalid Character(s)" "The port contains invalid character(s). It must be a number."
      SSH_PORT=""
    elif [ $SSH_PORT -eq 22 ]; then
      # No change, we'll allow it
      showMessageBox "Huh" "Okay, whatever...you do you."
      SSH_PORT="22"
    elif [ $SSH_PORT -lt 1024 ] || [ $SSH_PORT -gt 65536 ]; then
      showMessageBox "Invalid Character(s)" "The port number is invalid, it must be between 1024-65536."
      SSH_PORT=""
    else
      checkAvailableHomeServerPort $SSH_PORT
      if [ $? -ne 0 ]; then
        showMessageBox "Invalid Port" "The port is invalid. It either conflicts with an existing used port or is less than 1024."
        SSH_PORT=""
      fi
    fi
  done
  updatePlaintextRootConfigVar SSH_PORT $SSH_PORT
  # Change SSH port on host
  sudo sed -i "s|^#*Port .*$|Port ${SSH_PORT}|g" /etc/ssh/sshd_config
  sudo sed -i "s|^Port .*$|Port ${SSH_PORT}|g" /etc/ssh/sshd_config
  while [ -z "$PORTAINER_LOCAL_HTTPS_PORT" ] || [ "$PORTAINER_LOCAL_HTTPS_PORT" = "$SSH_PORT" ]
  do
    tmp_port=$((9000 + $RANDOM % 999))
    while [ "$tmp_port" = "$SSH_PORT" ]
    do
      tmp_port=$((9000 + $RANDOM % 999))
    done
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      PORTAINER_LOCAL_HTTPS_PORT=$tmp_port
    else
      PORTAINER_LOCAL_HTTPS_PORT=$(promptUserInputMenu $tmp_port "Enter Local Portainer Port" "Enter a local port to use for portainer. A random port between 9000-9999 has been generated for you.")
      if [ -z "$PORTAINER_LOCAL_HTTPS_PORT" ] || [ "$PORTAINER_LOCAL_HTTPS_PORT" = "$SSH_PORT" ]; then
        showMessageBox "Portainer Port Error" "The port cannot be empty or the same as the SSH port"
      elif [ "$(checkValidNumber $PORTAINER_LOCAL_HTTPS_PORT)" = "false" ]; then
        showMessageBox "Invalid Character(s)" "The port contains invalid character(s). It must be a number."
        PORTAINER_LOCAL_HTTPS_PORT=""
      elif [ $PORTAINER_LOCAL_HTTPS_PORT -lt 1024 ] || [ $PORTAINER_LOCAL_HTTPS_PORT -gt 65536 ]; then
        showMessageBox "Invalid Character(s)" "The port number is invalid, it must be between 1024-65536."
        PORTAINER_LOCAL_HTTPS_PORT=""
      fi
      if ! [ -z "$PORTAINER_LOCAL_HTTPS_PORT" ]; then
        checkAvailableHomeServerPort $PORTAINER_LOCAL_HTTPS_PORT
        if [ $? -ne 0 ]; then
          showMessageBox "Invalid Port" "The port is invalid. It either conflicts with an existing used port or is less than 1024."
          PORTAINER_LOCAL_HTTPS_PORT=""
        fi
      fi
    fi
    updatePlaintextRootConfigVar PORTAINER_LOCAL_HTTPS_PORT $PORTAINER_LOCAL_HTTPS_PORT
    DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT=$DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT,$PORTAINER_LOCAL_HTTPS_PORT
    updatePlaintextUserConfigVar DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT $DOCKERUSER_HOMESERVER_HOST_ALLOW_PORTS_DEFAULT
  done
  set -e
  # Do the boring things that must be done
  while ! [ "$IS_AUTO_INSTALL" = "true" ] && [ -z "$FIRST_USER_USERNAME" ]
  do
    FIRST_USER_USERNAME=$(promptUserInputMenu "" "Enter First User" "Enter the username for your first LDAP/email account: ")
    if [ -z "$FIRST_USER_USERNAME" ]; then
      showMessageBox "Username Empty" "The username cannot be empty"
    elif [ $(checkValidString "$FIRST_USER_USERNAME") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9"
      FIRST_USER_USERNAME=""
    fi
  done
  curFullName="$(echo $(getent passwd $USERNAME) | cut -d":" -f5 | cut -d"," -f1)"
  if [ -z "$curFullName" ]; then
    curFullName="${FIRST_USER_USERNAME^}"
  fi
  while ! [ "$IS_AUTO_INSTALL" = "true" ] && [ -z "$FIRST_USER_FIRSTNAME" ]
  do
    FIRST_USER_FIRSTNAME=$(promptUserInputMenu "$curFullName" "Enter First Name" "Enter the first name for your first LDAP/email user account: ")
    if [ -z "$FIRST_USER_FIRSTNAME" ]; then
      showMessageBox "Name Empty" "The name cannot be empty"
    elif [ $(checkValidStringUpperLowerNumbers "$FIRST_USER_FIRSTNAME" "[:space:].,-") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The name contains invalid character(s). It must consist of a-z, A-Z, 0-9, spaces, periods, commas, and/or hyphens."
      FIRST_USER_FIRSTNAME=""
    fi
  done
  while ! [ "$IS_AUTO_INSTALL" = "true" ] && [ -z "$FIRST_USER_LASTNAME" ]
  do
    FIRST_USER_LASTNAME=$(promptUserInputMenu "" "Enter Last Name" "Enter the last name for your first LDAP/email user account: ")
    if [ -z "$FIRST_USER_LASTNAME" ]; then
      showMessageBox "Name Empty" "The name cannot be empty"
    elif [ $(checkValidStringUpperLowerNumbers "$FIRST_USER_LASTNAME" "[:space:].,-") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The name contains invalid character(s). It must consist of a-z, A-Z, 0-9, spaces, periods, commas, and/or hyphens."
      FIRST_USER_LASTNAME=""
    fi
  done
  if ! [ "$IS_AUTO_INSTALL" = "true" ] && [ -z "$FIRST_USER_PASSWORD" ]; then
    tmp_pw1=""
    tmp_pw2=""
    set +e
    while [ -z "$tmp_pw1" ] || ! [ "$tmp_pw1" = "$tmp_pw2" ]
    do
      tmp_pw1=$(promptPasswordMenu "Create Password" "Create a password for your LDAP/email user ($FIRST_USER_USERNAME) account: ")
      if [ $? -ne 0 ]; then exit; fi
      if [ -z "$tmp_pw1" ]; then
        showMessageBox "Password Empty" "The password cannot be empty, please try again."
        tmp_pw1=abc
        tmp_pw2=def
        continue
      fi
      if [ $(checkValidPassword "$tmp_pw1" 8) = "false" ]; then
        showMessageBox "Password Invalid" "The password is too short or contains invalid characters. It must contain at least 8 characters and consist of uppercase letters, lowercase letters, and numbers. It can only contain the following symbols: ~!@#%^&*()-_+=<>?.,"
        tmp_pw1=abc
        tmp_pw2=def
        continue
      fi
      tmp_pw2=$(promptPasswordMenu "Confirm Password" "Enter the password again to confirm: ")
      if [ $? -ne 0 ]; then exit; fi
      if ! [ "$tmp_pw1" = "$tmp_pw2" ]; then
        showMessageBox "Password Mismatch" "The passwords do not match, please try again."
      fi
    done
    FIRST_USER_PASSWORD="$tmp_pw1"
    tmp_pw1=""
    tmp_pw2=""
    set -e
  fi
  while [ -z "$ADMIN_USERNAME_BASE" ] || [ "$ADMIN_USERNAME_BASE" = "admin" ]
  do
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      ADMIN_USERNAME_BASE=$HOMESERVER_ABBREV"admin"
    else
      ADMIN_USERNAME_BASE=$(promptUserInputMenu $HOMESERVER_ABBREV"admin" "Enter Admin Base" "Enter the base admin username (it cannot be \"admin\"): ")
    fi
    if [ $(checkValidString "$ADMIN_USERNAME_BASE") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9"
      ADMIN_USERNAME_BASE=""
    fi
    updatePlaintextRootConfigVar ADMIN_USERNAME_BASE $ADMIN_USERNAME_BASE
  done
  if [[ "$(isProgramInstalled pwgen)" = "false" ]]; then
    echo "Installing pwgen, please wait..."
    performAptInstall pwgen > /dev/null 2>&1
  fi
  if [[ "$(isProgramInstalled uuidgen)" = "false" ]]; then
    echo "Installing uuidgen, please wait..."
    performAptInstall uuid-runtime > /dev/null 2>&1
  fi
  while [ -z "$EMAIL_ADMIN_USERNAME" ]
  do
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      EMAIL_ADMIN_USERNAME="mailmanager"
    else
      EMAIL_ADMIN_USERNAME=$(promptUserInputMenu "mailmanager" "Enter Admin Email" "Enter the admin email username: ")
    fi
    if [ $(checkValidString "$EMAIL_ADMIN_USERNAME") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9"
      EMAIL_ADMIN_USERNAME=""
    fi
    updateConfigVar EMAIL_ADMIN_USERNAME $EMAIL_ADMIN_USERNAME
  done
  while [ -z "$EMAIL_JOINT_USERNAME" ]
  do
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      EMAIL_JOINT_USERNAME="info"
    else
      EMAIL_JOINT_USERNAME=$(promptUserInputMenu "info" "Enter Shared Email" "Enter the shared email username: ")
    fi
    if [ $(checkValidString "$EMAIL_JOINT_USERNAME") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The username contains invalid character(s). It must consist of a-z (lowercase) and/or 0-9"
      EMAIL_JOINT_USERNAME=""
    fi
    updateConfigVar EMAIL_JOINT_USERNAME $EMAIL_JOINT_USERNAME
  done
  initServicesCredentials
  addUserShareDirectories $NEXTCLOUD_ADMIN_USERNAME
  addUserShareDirectories $SPEAKR_ADMIN_USERNAME
}

function initInstallation()
{
  if [[ "$(isProgramInstalled screen)" = "false" ]]; then
    echo "Installing screen, please wait..."
    performAptInstall screen > /dev/null 2>&1
  fi
  if [[ "$(isProgramInstalled nano)" = "false" ]]; then
    echo "Installing nano, please wait..."
    performAptInstall nano > /dev/null 2>&1
  fi
  # Show info to user and prompt for confirmation
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
    rs_wg_user_conf=$(sudo cat $HSHQ_WIREGUARD_DIR/users/${RELAYSERVER_WG_VPN_NETNAME}-user1.conf)
  fi
  clear
  strInstallConfig=""
  strInstallConfig="${strInstallConfig}################################################################\n"
  strInstallConfig="${strInstallConfig}========================= General Info =========================\n"
  strInstallConfig="${strInstallConfig}- Root CA URL: http://$SUB_FILES.$HOMESERVER_DOMAIN/ca.crt\n"
  strInstallConfig="${strInstallConfig}- Home Page: https://$SUB_HSHQHOME.$HOMESERVER_DOMAIN\n"
  strInstallConfig="${strInstallConfig}- Email Admin Username: $EMAIL_ADMIN_EMAIL_ADDRESS\n"
  strInstallConfig="${strInstallConfig}- Email Admin Password: $EMAIL_ADMIN_PASSWORD\n"
  strInstallConfig="${strInstallConfig}- HomeServer Current SSH Port: $CURRENT_SSH_PORT\n"
  strInstallConfig="${strInstallConfig}- HomeServer New SSH Port: $SSH_PORT\n"
  strInstallConfig="${strInstallConfig}- Portainer IP URL: https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$PORTAINER_LOCAL_HTTPS_PORT\n"
  strInstallConfig="${strInstallConfig}- Portainer Username: $PORTAINER_ADMIN_USERNAME\n"
  strInstallConfig="${strInstallConfig}- Portainer Password: $PORTAINER_ADMIN_PASSWORD\n"
  strInstallConfig="${strInstallConfig}- Script-server IP URL: https://$HOMESERVER_HOST_PRIMARY_INTERFACE_IP:$SCRIPTSERVER_LOCALHOST_PORT\n"
  strInstallConfig="${strInstallConfig}- Script-server Username: $SCRIPTSERVER_ADMIN_USERNAME\n"
  strInstallConfig="${strInstallConfig}- Script-server Password: $SCRIPTSERVER_ADMIN_PASSWORD\n"
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
    strInstallConfig="${strInstallConfig}- RelayServer IP Address: $RELAYSERVER_SERVER_IP\n"
    strInstallConfig="${strInstallConfig}- RelayServer Current SSH Port: $RELAYSERVER_CURRENT_SSH_PORT\n"
    strInstallConfig="${strInstallConfig}- RelayServer New SSH Port: $RELAYSERVER_SSH_PORT\n"
    strInstallConfig="${strInstallConfig}- RelayServer Linux Username: $RELAYSERVER_REMOTE_USERNAME\n"
  fi
  strInstallConfig="${strInstallConfig}================================================================\n\n"
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] || [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then
    strInstallConfig="${strInstallConfig}$(getDNSRecordsInfo $HOMESERVER_DOMAIN)\n\n"
  fi
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
    strInstallConfig="${strInstallConfig}================= User WireGuard Configuration =================\n"
    strInstallConfig="${strInstallConfig}""""$rs_wg_user_conf""""\n"
    strInstallConfig="${strInstallConfig}================================================================\n\n"
  fi
  #strInstallConfig="${strInstallConfig}=========================== Root CA ============================\n"
  #strInstallConfig="${strInstallConfig}""""$(cat "$HSHQ_SSL_DIR/${CERTS_ROOT_CA_NAME}.crt")""""\n"
  #strInstallConfig="${strInstallConfig}================================================================\n"
  strInstallConfig="${strInstallConfig}                      INSTRUCTIONS                     \n"
  strInstallConfig="${strInstallConfig}The system is prepped for installation. It will take around\n"
  strInstallConfig="${strInstallConfig}10-15 minutes to perform the base installation, depending on\n"
  strInstallConfig="${strInstallConfig}hardware specs and download speeds. Copy all of this info\n"
  strInstallConfig="${strInstallConfig}before proceeding. Ensure to scroll to the top and retain\n"
  strInstallConfig="${strInstallConfig}everything between the ##### borders. Simply select the\n"
  strInstallConfig="${strInstallConfig}information with your mouse, and it will automatically be\n"
  strInstallConfig="${strInstallConfig}copied to the clipboard.\n\n"
  strInstallConfig="${strInstallConfig}The process will run continuously uninterrupted in a 'screen'\n"
  strInstallConfig="${strInstallConfig}If you lose connection, then enter 'screen -r hshqInstall' to\n"
  strInstallConfig="${strInstallConfig}resume the screen if you want to continue to view the process.\n"
  strInstallConfig="${strInstallConfig}To exit the screen, press CTRL-a, then release both keys,\n"
  strInstallConfig="${strInstallConfig}then press d to detach.\n\n"
  strInstallConfig="${strInstallConfig}The system will automatically reboot upon completion.\n\n"
  strInstallConfig="${strInstallConfig}If you want to completely remove everything, run the utility\n"
  strInstallConfig="${strInstallConfig}inside this script (System Utils -> Uninstall and Remove\n"
  strInstallConfig="${strInstallConfig}Everything) and follow the prompts. To do the same on the\n"
  strInstallConfig="${strInstallConfig}RelayServer, enter the command 'bash nuke.sh', and follow\n"
  strInstallConfig="${strInstallConfig}the prompts.\n\n"
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
    strInstallConfig="${strInstallConfig}The installation on the RelayServer will also automatically\n"
    strInstallConfig="${strInstallConfig}be initiated as well. Ensure to log in to the RelayServer\n"
    strInstallConfig="${strInstallConfig}as user ($RELAYSERVER_REMOTE_USERNAME) to monitor the progress of the installation,\n"
    strInstallConfig="${strInstallConfig}by entering the command 'screen -r hshqInstall'.\n\n"
  fi
  strInstallConfig="${strInstallConfig}################################################################\n\n"
  echo -e "${strInstallConfig}"
  while true;
  do
    echo -e "________________________________________________________________________\n"
    if [ "$IS_DESKTOP_ENV" = "true" ]; then
      hdir=/home/$USERNAME/Desktop
      is_keep_config=y
      echo -e "The above information has been saved to your Desktop,"
      echo -e "i.e. $hdir/$HSHQ_INSTALL_NOTES_FILENAME."
    else
      hdir=/home/$USERNAME
      echo -e "Do you want to retain the above information in a file in the home directory,"
      echo -e "i.e. ($hdir/$HSHQ_INSTALL_NOTES_FILENAME)? Note that this"
      echo -e "information is very sensitive and you should delete the file as soon"
      read -r -p "as you are finished with it. Enter 'y' or 'n': " is_keep_config
    fi
    if [ "$is_keep_config" = "y" ]; then
      final_prompt="After reading the above section, enter 'install' or 'exit': "
      rm -f "$hdir/$HSHQ_INSTALL_NOTES_FILENAME"
      echo -e "\n" > "$hdir/$HSHQ_INSTALL_NOTES_FILENAME"
      echo -e "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME"
      echo -e "@@@@@@@@     You should permanently delete this file    @@@@@@@@" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME"
      echo -e "@@@@@@@@       as soon as you are finished with it      @@@@@@@@" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME"
      echo -e "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n\n" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME"
      echo -e "${strInstallConfig}" >> "$hdir/$HSHQ_INSTALL_NOTES_FILENAME"
      chmod 0400 "$hdir/$HSHQ_INSTALL_NOTES_FILENAME"
      break
    elif [ "$is_keep_config" = "n" ]; then
      final_prompt="After reading/copying the above section, enter 'install' or 'exit': "
      break
    else
      echo "Unknown response, please try again."
    fi
  done
  while true;
  do
    if [ "$IS_AUTO_INSTALL" = "true" ]; then
      break
    fi
    echo -e "\n________________________________________________________________________\n"
    read -r -p "$final_prompt" is_install
    if [ "$is_install" = "install" ]; then
      break
    elif [ "$is_install" = "exit" ]; then
      echo "Exiting..."
      releaseLock hshqopen "initConfig" false
      exit 1
    else
      echo "Unknown response, please try again."
    fi
  done
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
    echo "Starting installation on RelayServer..."
    sleep 1
    isRSReady=false
    set +e
    countRSPreRetries=0
    maxRSPreRetries=30
    while true;
    do
      perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "sleep 1;if [ -f ~/$RELAYSERVER_NOT_READY_FILE ]; then exit 1; fi" -r 1 -b 1
      if [ $? -eq 0 ]; then
        break
      fi
      if [ $countRSPreRetries -ge $maxRSPreRetries ]; then
        echo "Max retries reached. Something has likely gone wrong with the RelayServer pre-installation script, exiting..."
        exit 2
      fi
      ((countRSPreRetries++))
      echo "($countRSPreRetries of $maxRSPreRetries) RelayServer not ready for installation, sleeping 30 seconds then will retry..."
      sleep 30
    done
    sleep 1
    perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "bash $RS_INSTALL_FRESH_SCRIPT_NAME -s" -f -i "$USER_RELAY_SUDO_PW"
  fi
  set -e
  refreshSudo
  echo "Starting installation on HomeServer..."
  sleep 1
  releaseLock hshqopen "preInstallation" false
  truncate -s 0 $HSHQ_LOG_FILE
  scName=hshqInstall
  initScreen $scName "bash $HSHQ_LIB_SCRIPT install $CONNECTING_IP"
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "There was a problem starting the installation, exiting..."
    exit 1
  fi
  screen -r $scName
  exit 0
}

function setSudoTimeoutInstall()
{
  sudo tee $HOME/$SUDO_LONG_TIMEOUT_FILENAME >/dev/null <<EOFSU
Defaults timestamp_timeout=$SUDO_LONG_TIMEOUT
EOFSU
  sudo chmod 0600 $HOME/$SUDO_LONG_TIMEOUT_FILENAME
  sudo mv $HOME/$SUDO_LONG_TIMEOUT_FILENAME /etc/sudoers.d/
}

function removeSudoTimeoutInstall()
{
  sudo rm -f /etc/sudoers.d/$SUDO_LONG_TIMEOUT_FILENAME
}

function performBaseInstallation()
{
  logHSHQEvent info "Installation Started!"
  checkRes="$(acquireAllLocks)"
  if ! [ -z "$checkRes" ]; then
    strErr="performBaseInstallation - The HSHQ script is already open or running in a different instance ($checkRes). If you are certain that it is not, then please remove the directories: /tmp/hshqopen and /tmp/networkchecks, then restart the installation."
    logHSHQEvent error "$strErr"
    echo "$strErr"
    exit 7
  fi
  set +e
  checkLoadConfig
  setSudoTimeoutInstall
  if [ "$IS_INSTALLED" = "true" ] || [ "$IS_INSTALLING" = "true" ]; then
    strErr="performBaseInstallation - Already installed or existing installation is in progress, exiting..."
    logHSHQEvent error "$strErr"
    echo "$strErr"
    exit 1
  fi
  IS_INSTALLING=true
  setSystemState $SS_INSTALLING
  updateConfigVar IS_INSTALLING $IS_INSTALLING
  set +e
  upgradeHostOS
  sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y
  set -e
  outputScripts
  echo "Setting MOTD..."
  updateMOTD
  createSwapfile
  performSuggestedSecUpdates
  logHSHQEvent info "performBaseInstallation - Install Dependencies"
  installMailUtils
  installDependencies
  clear
  echo -e "\n\n##########################################"
  echo -e "     Starting Base HSHQ Installation"
  echo -e "##########################################\n\n"
  enable_trapping
  setup_scroll_area
  is_draw_pb=true
  draw_progress_bar 0
  set +e
  if [ "$LOG_LEVEL" = "debug" ]; then
    echo "Clearing IPTables..."
  fi
  performClearIPTables true
  if [ "$LOG_LEVEL" = "debug" ]; then
    echo "performBaseInstallation-Early IPTables..."
  fi
  checkUpdateAllIPTables performBaseInstallation-Early
  performInstallVariantsPre
  set -e
  draw_progress_bar 5
  pullBaseServicesDockerImages
  draw_progress_bar 50
  logHSHQEvent info "performBaseInstallation - Init DH Params"
  initDHParams
  draw_progress_bar 53
  getUpdateAssets
  if [ $? -ne 0 ]; then
    echo "FATAL: Could not update image assets, exiting..."
    exit 9
  fi
  sudo mkdir -p /usr/share/icons/HSHQ
  sudo cp -f $HSHQ_ASSETS_DIR/icons/* /usr/share/icons/HSHQ/
  draw_progress_bar 55
  logHSHQEvent info "performBaseInstallation - Starting Stack Installs"
  installBaseStacks
  draw_progress_bar 80
  set +e
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ] || [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then
    if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
      logHSHQEvent info "performBaseInstallation - Configure Docker WireGuard Network"
      connectPrimaryInternet
    elif [ "$PRIMARY_VPN_SETUP_TYPE" = "join" ]; then
      dns_file=$HOME/dns.tmp
      if [ -f $dns_file ]; then
        docker container stop heimdall >/dev/null
        updateHomeServerDNS $dns_file
        rm -f $dns_file
        docker container start heimdall >/dev/null
      fi
    fi
    draw_progress_bar 81
    logHSHQEvent info "performBaseInstallation - Connect Services To VPN"
    connectPrimaryVPN true
    draw_progress_bar 83
    echo "Waiting 30s for services to initialize..."
    sleep 30
  fi
  draw_progress_bar 85
  clearQueryLogAndStatsAdguardHS
  set +e
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "host" ]; then
    curl https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN > /dev/null 2>&1
    if [ $? -eq 0 ]; then
      clearQueryLogAndStatsAdguardRS
    fi
  fi
  set -e
  logHSHQEvent info "performBaseInstallation - Post Installation"
  draw_progress_bar 86
  checkUpdateAllIPTables performBaseInstallation-Late
  set -e
  draw_progress_bar 90
  postInstallation install
}

function trustDesktopIcon()
{
  set +e
  dtIcon=$1
  which gio > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    gio set $HOME/Desktop/$dtIcon metadata::trusted true > /dev/null 2>&1
    gio set -t string $HOME/Desktop/$dtIcon metadata::xfce-exe-checksum "$(sha256sum $HOME/Desktop/$dtIcon | awk '{print $1}')" > /dev/null 2>&1
    dbus-launch --exit-with-session gio set $HOME/Desktop/$dtIcon metadata::trusted true > /dev/null 2>&1
    dbus-launch --exit-with-session gio set -t string $HOME/Desktop/$dtIcon metadata::xfce-exe-checksum "$(sha256sum $HOME/Desktop/$dtIcon | awk '{print $1}')" > /dev/null 2>&1
    nautilus -q > /dev/null 2>&1
  fi
}

function postInstallation()
{
  isInstallOrRestore="$1"
  refreshSudo
  echo "Performing post-installation tasks..."
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y avahi-daemon > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y avahi-autoipd > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt autoremove --purge -y
  if [ "$isInstallOrRestore" = "install" ]; then
    echo "Emailing Root CA..."
    sendRootCAEmail true
    draw_progress_bar 91
    if ! [ "$IS_AUTO_INSTALL" = "true" ]; then
      emailFormattedCredentials true
    fi
    draw_progress_bar 95
  fi
  # Need to wait until emails have been sent before changing permissions.
  sudo chmod 750 /usr/bin/mail.mailutils
  sudo rm -f /etc/skel/hshq.sh
  if [ "$IS_DESKTOP_ENV" = "true" ]; then
    echo "Configuring Desktop environment..."
    set +e
    if ! [ -f /usr/share/icons/HSHQ/Homepage.png ]; then
      sudo cp /usr/share/icons/HSHQ/HomepageHSHQ.png /usr/share/icons/HSHQ/Homepage.png
    fi
    rm -f ~/Desktop/InstallHSHQ.desktop
    rm -f ~/Desktop/HomePage.desktop
    cat <<EOFHP > ~/Desktop/HomePage.desktop
[Desktop Entry]
Name=$HOMESERVER_NAME
Exec=firefox $SUB_HSHQHOME.$HOMESERVER_DOMAIN
Comment=Opens your HomeServer home page
Terminal=false
Icon=/usr/share/icons/HSHQ/Homepage.png
Type=Application
Categories=HomeServerHQ
EOFHP
    chmod 755 ~/Desktop/HomePage.desktop
    rm -f ~/Desktop/HSHQConsole.desktop
    which terminator > /dev/null 2>&1
    if [ $? -eq 0 ]; then
      # Specify terminator as console if possible
      cat <<EOFHP > ~/Desktop/HSHQConsole.desktop
[Desktop Entry]
Name=HSHQ Console Util
Exec=/usr/bin/terminator -x bash -ic "~/hshq.sh"
Comment=Runs the HSHQ console-based management utility
Icon=/usr/share/icons/HSHQ/ConsoleHSHQ.png
Type=Application
Categories=HomeServerHQ
EOFHP
    else
      cat <<EOFHP > ~/Desktop/HSHQConsole.desktop
[Desktop Entry]
Name=HSHQ Console Util
Exec=bash -ic "~/hshq.sh"
Terminal=true
Comment=Runs the HSHQ console-based management utility
Icon=/usr/share/icons/HSHQ/ConsoleHSHQ.png
Type=Application
Categories=HomeServerHQ
EOFHP
    fi
    chmod 755 ~/Desktop/HSHQConsole.desktop
    rm -f ~/Desktop/HSHQScriptServer.desktop
    cat <<EOFHP > ~/Desktop/HSHQScriptServer.desktop
[Desktop Entry]
Name=HSHQ Web Util
Exec=firefox https://127.0.0.1:$SCRIPTSERVER_LOCALHOST_PORT
Comment=Runs the HSHQ web-based management utility
Terminal=false
Icon=/usr/share/icons/HSHQ/ScriptServerHSHQ.png
Type=Application
Categories=HomeServerHQ
EOFHP
    chmod 755 ~/Desktop/HSHQScriptServer.desktop
    rm -f ~/Desktop/HSHQHelp.desktop
    cat <<EOFHP > ~/Desktop/HSHQHelp.desktop
[Desktop Entry]
Name=Help!
Exec=firefox forum.homeserverhq.com
Comment=Ask for help on the HomeServerHQ Forum
Terminal=false
Icon=/usr/share/icons/HSHQ/HelpHSHQ.png
Type=Application
Categories=HomeServerHQ
EOFHP
    chmod 755 ~/Desktop/HSHQHelp.desktop
    trustDesktopIcon "HomePage.desktop"
    trustDesktopIcon "HSHQConsole.desktop"
    trustDesktopIcon "HSHQScriptServer.desktop"
    trustDesktopIcon "HSHQHelp.desktop"
    sudo mkdir -p /usr/share/applications
    sudo cp -f ~/Desktop/HomePage.desktop /usr/share/applications/
    sudo cp -f ~/Desktop/HSHQConsole.desktop /usr/share/applications/
    sudo cp -f ~/Desktop/HSHQScriptServer.desktop /usr/share/applications/
    sudo cp -f ~/Desktop/HSHQHelp.desktop /usr/share/applications/
    draw_progress_bar 96
    which gsettings > /dev/null 2>&1
    if [ $? -eq 0 ]; then
      # Spell check doesn't belong in a basic text editor,
      # especially if it's insanely intrusive.
      gsettings set org.gnome.TextEditor spellcheck false > /dev/null 2>&1
      # Just because
      gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' > /dev/null 2>&1
    fi
    replaceHomePageDesktopIcon $(pwgen -c -n 8 1)
    draw_progress_bar 97
    addRootCertificateToApps
    draw_progress_bar 98
    configureFirefox
    set -e
  fi
  draw_progress_bar 99
  initCronJobs
  removeSudoTimeoutInstall
  rm -f ~/dead.letter
  rm -f $HSHQ_BASE_DIR/cip.txt
  if [ "$isInstallOrRestore" = "install" ]; then
    performInstallVariantsPost
    addPrimaryUser "$LDAP_ADMIN_USER_USERNAME" "$LDAP_ADMIN_USER_PASSWORD" "${HOMESERVER_ABBREV^^}" "Admin" true
# > /dev/null 2>&1
    if ! [ -z "$FIRST_USER_USERNAME" ]; then
      echo "Adding first user($FIRST_USER_USERNAME)..."
      addPrimaryUser "$FIRST_USER_USERNAME" "$FIRST_USER_PASSWORD" "$FIRST_USER_FIRSTNAME" "$FIRST_USER_LASTNAME" false > /dev/null 2>&1
    fi
    IS_INSTALLED=true
    updateConfigVar IS_INSTALLED $IS_INSTALLED
    IS_INSTALLING=false
    updateConfigVar IS_INSTALLING $IS_INSTALLING
    encryptConfigFile
    draw_progress_bar 100
    destroy_scroll_area
    releaseLock hshqopen "postInstallation" false
    sanitizeHSHQLog
    setSystemState $SS_RUNNING
    logHSHQEvent info "Installation Complete!"
    echo -e "\n\n\n\n################################################################\n"
    echo "               HomeServer Installation Complete!"
    echo "     The system will automatically reboot in 60 seconds..."
    echo -e "\n################################################################\n\n"
    sleep 60
    logHSHQEvent info "postInstallation - Rebooting"
    sudo cp $HSHQ_LOG_FILE $HSHQ_BASE_DIR/hshqInstall.log
    sudo chown $USERNAME:$USERNAME $HSHQ_BASE_DIR/hshqInstall.log
    chmod 0400 $HSHQ_BASE_DIR/hshqInstall.log
    sudo truncate -s 0 $HSHQ_LOG_FILE
    sudo reboot
  fi
}

function sanitizeHSHQLog()
{
  sudo sed -i 's/\x1b//g' $HSHQ_LOG_FILE
  sudo sed -i 's/\^//g' $HSHQ_LOG_FILE
  sudo sed -i 's/\x1b//g' $HSHQ_LOG_FILE
  sudo sed -i "s|$USERNAME|hshquser|g" $HSHQ_LOG_FILE
  sudo sed -i "s|$LDAP_ADMIN_USER_USERNAME|hshqadmin|g" $HSHQ_LOG_FILE
  sudo sed -i "s|$HOMESERVER_DOMAIN|hshqexample.com|g" $HSHQ_LOG_FILE
}

function showSimpleBackupMenu()
{
  set +e
  utilmenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$utilmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Configure Backup" \
  "2" "Mount Existing Backup Disk" \
  "3" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ]; then
    menures=0
  fi
  case $menures in
    0)
	  return 1 ;;
    1)
      showConfigureSimpleBackupMenu
      return 1 ;;
    2)
      showMountBackupDriveMenu
      return 1 ;;
    3)
	  return 0 ;;
  esac
}

function showConfigureSimpleBackupMenu()
{
  set +e
  showYesNoMessageBox "Continue?" "This is a very basic utility to set up your local encrypted backup. It will search for any unmounted disks and prompt you with a list. After making your selection and confirming, it will wipe all data and partitions on that disk, create a single partition, automount the filesystem on boot, then configure your backup in Duplicati for this disk. There are infinite ways you can configure your backup, and infinite tools to accomplish it. This utility is merely one simple way. If you decide to manually set up your own backup process, the directory you need to focus on is $HSHQ_DATA_DIR. The full system can be restored from the contents of this directory. Do you wish to continue?"
  if [ $? -ne 0 ]; then
    return
  fi
  sudo cat /etc/fstab | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR in /etc/fstab. Please unmount this filesystem and remove it from /etc/fstab, returning..."
    return
  fi
  sudo findmnt | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR. Please unmount this filesystem first, returning..."
    return
  fi
  chkfstab=$(sudo findmnt --verify 2> /dev/null)
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "There is an error with /etc/fstab: ${chkfstab}\nPlease fix this issue and retry."
    return
  fi
  if ! sudo test -f $HSHQ_STACKS_DIR/duplicati/config/Duplicati-server.sqlite; then
    showMessageBox "ERROR" "Could not find the Duplicati database, returning..."
    return
  fi
  sudo sqlite3 $HSHQ_STACKS_DIR/duplicati/config/Duplicati-server.sqlite "select TargetURL from Backup;" | grep /backups/ > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    showMessageBox "ERROR" "There is already a backup in Duplicati that uses the /backups directory, returning..."
    return
  fi
  dtok="$(getDuplicatiToken)"
  if [ -z "$dtok" ]; then
    showMessageBox "ERROR" "The Duplicati auth token is empty, returning..."
    return
  fi
  dbackmenu=$(cat << EOF

$(getLogo)
EOF
)
  OLDIFS=$IFS
  IFS=$(echo -en "\n\b")
  diskarr=($(sudo hwinfo --disk --short | tail +2))
  curListNum=1
  scsbm_menu_items=( --title "Select Disk" --radiolist "$dbackmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT )
  for curDisk in "${diskarr[@]}"
  do
    curDiskID=$(echo "$curDisk" | xargs | cut -d" " -f1)
    curDiskName=$(echo "$curDisk" | xargs | cut -d" " -f2-)
    findmnt | grep $curDiskID > /dev/null 2>&1
    if [ $? -eq 0 ]; then
      continue
    fi
    curDiskSize=$(lsblk -o SIZE --noheadings --raw -d $curDiskID)
    curDiskParts=$(sudo partx -g $curDiskID 2> /dev/null | wc -l)
    scsbm_menu_items+=( "$curDiskID  $curDiskSize  ($curDiskParts Partitions)  $curDiskName" )
    scsbm_menu_items+=( "|" )
    scsbm_menu_items+=( "off" )
    ((curListNum++))
  done
  IFS=$OLDIFS
  if [ $curListNum -le 1 ]; then
    showMessageBox "ERROR" "There are no available disks for this operation, returning..."
    return
  fi
  while true;
  do
    selDiskItem=$(whiptail "${scsbm_menu_items[@]}" 3>&1 1>&2 2>&3)
    if [ $? -ne 0 ]; then
      return
    fi
    if [ -z "$selDiskItem" ]; then
      showMessageBox "ERROR" "You did not make a selection, press spacebar on your desired option. Please try again."
    else
      break
    fi
  done
  selDisk=$(echo "$selDiskItem" | cut -d" " -f1)
  if [ -z "$selDisk" ]; then
    showMessageBox "ERROR" "No disk was selected, returning..."
    return
  fi
  confirmFormat=$(promptUserInputMenu "" "Confirm Format" "WARNING - This operation will entirely ERASE the contents of the selected drive. To confirm, enter the word 'format' below:")
  if [ $? -ne 0 ]; then
    return
  fi
  if [ $? -ne 0 ] || [ -z "$confirmFormat" ] || ! [ "$confirmFormat" = "format" ]; then
    showMessageBox "Incorrect Confirmation" "The text did not match, returning..."
    return
  fi
  while true;
  do
    backupHour=$(promptUserInputMenu "" "Daily Backup Time" "Enter the hour of the day(0-23) that you wish to perform the daily backup. This can be modified later in Duplicati:")
    if [ $? -ne 0 ]; then
      return
    fi
    if [[ $backupHour =~ ^[0-9]+$ ]] && [ $backupHour -ge 0 ] && [ $backupHour -le 23 ]; then
      break
    else
      showMessageBox "Invalid Number" "The value is not a valid number."
    fi
  done
  tmp_pw1=""
  tmp_pw2=""
  while [ -z "$tmp_pw1" ] || ! [ "$tmp_pw1" = "$tmp_pw2" ]
  do
    tmp_pw1=$(promptPasswordMenu "Create Password" "Create a password to encrypt/decrypt your backup files. ENSURE you remember this or you will be IRREVERSIBLY locked out of your backup files (unless you have a quantum super-computer) and you will NOT be able to recover your data: ")
    if [ $? -ne 0 ]; then
      return
    fi
    if [ -z "$tmp_pw1" ]; then
      showMessageBox "Password Empty" "The password cannot be empty, please try again."
      continue
    fi
    if [ $(checkValidPassword "$tmp_pw1" 16) = "false" ]; then
      showMessageBox "Weak Password" "The password is invalid or is too weak. It must contain at least 16 characters and consist of uppercase letters, lowercase letters, and numbers. It can only contain the following symbols: ~!@#%^&*()-_+=<>?.,"
      tmp_pw1=""
      tmp_pw2=""
      continue
    fi
    tmp_pw2=$(promptPasswordMenu "Confirm Password" "Enter the password again to confirm: ")
    if [ $? -ne 0 ]; then
      return
    fi
    if ! [ "$tmp_pw1" = "$tmp_pw2" ]; then
      showMessageBox "Password Mismatch" "The passwords do not match, please try again."
    fi
  done
  backupPW="$tmp_pw1"
  tmp_pw1=""
  tmp_pw2=""
  confirmFinal=$(promptUserInputMenu "" "Confirm" "The backup configuration is ready. To perform all actions, including erasing and formatting the selected disk, enter the word 'confirm' below:")
  if [ $? -ne 0 ] || [ -z "$confirmFinal" ] || ! [ "$confirmFinal" = "confirm" ]; then
    showMessageBox "Incorrect Confirmation" "The text did not match, returning..."
    return
  fi
  docker container stop duplicati > /dev/null 2>&1
  docker container stop syncthing > /dev/null 2>&1
  sudo sfdisk --delete $selDisk > /dev/null
  retV=$?
  if [ $retV -ne 0 ] && [ $retV -ne 1 ]; then
    docker container start duplicati > /dev/null 2>&1
    docker container start syncthing > /dev/null 2>&1
    showMessageBox "ERROR" "Encountered an error attempting to delete the partitions from the disk, exiting..."
    return
  fi
  # A little sleep never hurts
  sleep 1
  echo ';' | sudo sfdisk $selDisk > /dev/null
  if [ $? -ne 0 ]; then
    docker container start duplicati > /dev/null 2>&1
    docker container start syncthing > /dev/null 2>&1
    showMessageBox "ERROR" "Encountered an error attempting to create the new partition, exiting..."
    return
  fi
  sleep 1
  newPart=$(sudo fdisk -l $selDisk | grep '^/dev' | cut -d' ' -f1)
  sleep 1
  sudo mkfs -F -t ext4 $newPart > /dev/null
  if [ $? -ne 0 ]; then
    docker container start duplicati > /dev/null 2>&1
    docker container start syncthing > /dev/null 2>&1
    showMessageBox "ERROR" "Encountered an error attempting to create the new filesystem, exiting..."
    return
  fi
  sleep 1
  sudo mount $newPart $HSHQ_BACKUP_DIR > /dev/null
  if [ $? -ne 0 ]; then
    docker container start duplicati > /dev/null 2>&1
    docker container start syncthing > /dev/null 2>&1
    showMessageBox "ERROR" "Encountered an error attempting to mount the filesystem, exiting..."
    return
  fi
  sleep 1
  sudo mkdir $HSHQ_BACKUP_DIR/$HOMESERVER_DOMAIN
  docker container start duplicati > /dev/null 2>&1
  docker container start syncthing > /dev/null 2>&1
  newPartID=$(sudo blkid -s UUID -o value $newPart)
  sudo cp /etc/fstab /etc/fstab.old
  echo "# Backup Drive" | sudo tee -a /etc/fstab > /dev/null 2>&1
  if [ -z "$newPartID" ]; then
    echo "$newPart $HSHQ_BACKUP_DIR ext4 defaults,nofail,x-systemd.device-timeout=15 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1
  else
    echo "UUID=$newPartID $HSHQ_BACKUP_DIR ext4 defaults,nofail,x-systemd.device-timeout=15 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1
  fi
  sudo systemctl daemon-reload > /dev/null 2>&1
  sudo findmnt --verify | grep "Success, no errors or warnings detected" > /dev/null 2>&1
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "There was a problem adding the requisite entry to /etc/fstab. Your backup disk will not be auto-mounted after a reboot."
    sudo mv /etc/fstab.old /etc/fstab
  else
    sudo rm -f /etc/fstab.old
  fi
  mydate=$(date --date="$(date +%Y-%m-%d) $backupHour:00")
  dtnow=$(date)
  if [ $(($(date -d "$dtnow" +%s) - $(date -d "$mydate" +%s))) -gt 0 ]; then
    mydate=$(date -d "$mydate + 1 day")
  fi
  utcdate=$(TZ="UTC" date --date="$mydate")
  cat <<EOFBU > $HOME/hshq-backup.json
{
  "CreatedByVersion": "2.0.7.1",
  "Schedule": {
    "Time": "$(TZ="UTC" date --date="$utcdate" +%FT%TZ)",
    "Repeat": "1D",
    "LastRun": "0001-01-01T05:51:00Z",
    "Rule": "AllowedWeekDays=Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday",
    "AllowedDays": [
      "mon",
      "tue",
      "wed",
      "thu",
      "fri",
      "sat",
      "sun"
    ]
  },
  "Backup": {
    "Name": "HSHQ Backup",
    "Description": "",
    "Tags": [],
    "TargetURL": "file:///backups/$HOMESERVER_DOMAIN",
    "Sources": [
      "/source/"
    ],
    "Settings": [
      {
        "Filter": "",
        "Name": "encryption-module",
        "Value": "aes",
        "Argument": null
      },
      {
        "Filter": "",
        "Name": "compression-module",
        "Value": "zip",
        "Argument": null
      },
      {
        "Filter": "",
        "Name": "dblock-size",
        "Value": "50mb",
        "Argument": null
      },
      {
        "Filter": "",
        "Name": "passphrase",
        "Value": "$backupPW",
        "Argument": null
      },
      {
        "Filter": "",
        "Name": "retention-policy",
        "Value": "1W:1D,4W:1W,12M:1M",
        "Argument": null
      },
      {
        "Filter": "",
        "Name": "ignore-advisory-locking",
        "Value": "true",
        "Argument": null
      }
    ],
    "Filters": [],
    "Metadata": {},
    "IsTemporary": false
  },
  "DisplayNames": {
    "/source/": "source"
  }
}
EOFBU
  sleep 3
  echo "Getting Duplicati auth token..."
  dtok="$(getDuplicatiToken)"
  if [ -z "$dtok" ]; then
    showMessageBox "ERROR" "ERROR: The Duplicati auth token is empty, returning..."
  else
    echo "Got token, importing backup configuration..."
  fi
  docker exec -i -e DTOK="$dtok" duplicati sh -c 'curl -sS --fail-with-body --max-time 300 -X POST -H "Authorization: Bearer $DTOK" -H "Content-Type: application/json" --data-binary @- http://localhost:8200/api/v1/backups' < "$HOME/hshq-backup.json"
  #http --check-status --ignore-stdin POST https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN/api/v1/backups "Authorization: Bearer $dtok" @$HOME/hshq-backup.json > /dev/null 2>&1
  #docker exec duplicati bash -c "mono /app/duplicati/Duplicati.CommandLine.ConfigurationImporter.exe /config/hshq-backup.json --import-metadata=false --server-datafolder=/config"
  uplRetval=$?
  sudo rm -f $HOME/hshq-backup.json
  if [ $uplRetval -ne 0 ]; then
    showMessageBox "ERROR" "ERROR: The backup job was not uploaded to Duplicati. Please report this error on Github or the Forum."
  else
    showMessageBox "SUCCESS" "Your backup is configured. It will run automatically starting at $mydate, or you can trigger it manually. You will recieve a daily email in your admin email account ($EMAIL_ADMIN_EMAIL_ADDRESS) after each backup. You can view/edit the configuration in https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN ."
  fi
}

function getDuplicatiToken()
{
  maxDupTries=10
  curDupTries=0
  isSuccess=false
  dupToken=""
  while [ $curDupTries -lt $maxDupTries ]
  do
    dupToken=$(docker exec -e DUPLICATI_ADMIN_PASSWORD="$DUPLICATI_ADMIN_PASSWORD" duplicati sh -c 'curl -sS --fail-with-body --max-time 300 -X POST -H "Content-Type: application/json" -d "$(jq -nc --arg p "$DUPLICATI_ADMIN_PASSWORD" "{password: \$p}")" http://localhost:8200/api/v1/auth/login' | jq -r .AccessToken)
    #dupToken=$(http --check-status --ignore-stdin --timeout=300 https://$SUB_DUPLICATI.$HOMESERVER_DOMAIN/api/v1/auth/login password=$DUPLICATI_ADMIN_PASSWORD | jq -r .AccessToken)
    if [ $? -eq 0 ] && ! [ -z "$dupToken" ]; then
      isSuccess=true
      break
    fi
    ((curDupTries++))
    sleep 3
  done
  if [ "$isSuccess" = "true" ]; then
    echo "$dupToken"
  fi
}

function showMountBackupDriveMenu()
{
  findmnt | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR. Please unmount this filesystem first, returning..."
    return
  fi
  sudo cat /etc/fstab | grep $HSHQ_BACKUP_DIR > /dev/null 2>&1
  if [ $? -eq 0 ]; then
    showMessageBox "ERROR" "There is already a filesystem mounted to $HSHQ_BACKUP_DIR in /etc/fstab. Please unmount this filesystem and remove it from /etc/fstab, returning..."
    return
  fi
  mkdir -p $HSHQ_BACKUP_DIR
  if ! [ -z "$(ls -A $HSHQ_BACKUP_DIR)" ]; then
    showMessageBox "ERROR" "The backup directory ($HSHQ_BACKUP_DIR) is not empty, returning..."
    return
  fi

  dbackmenu=$(cat << EOF

$(getLogo)
EOF
)
  OLDIFS=$IFS
  IFS=$(echo -en "\n\b")
  diskarr=($(sudo hwinfo --disk --short | tail +2))
  curListNum=1
  scsbm_menu_items=( --title "Select Disk/Partition" --radiolist "$dbackmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT )
  for curDisk in "${diskarr[@]}"
  do
    curDiskID=$(echo "$curDisk" | xargs | cut -d" " -f1)
    curDiskName=$(echo "$curDisk" | xargs | cut -d" " -f2-)
    findmnt | grep $curDiskID > /dev/null 2>&1
    if [ $? -eq 0 ]; then
      continue
    fi
    curDiskPartitions=($(lsblk --noheadings --raw -o NAME,SIZE,TYPE $curDiskID))
    for curDiskPart in "${curDiskPartitions[@]}"
    do
      curDiskPartName=$(echo $curDiskPart | cut -d" " -f1)
      curDiskPartSize=$(echo $curDiskPart | cut -d" " -f2)
      curDiskPartType=$(echo $curDiskPart | cut -d" " -f3)
      if [ "$curDiskPartType" = "part" ]; then
        scsbm_menu_items+=( "$curDiskPartName  $curDiskPartSize  ($curDiskName)" )
        scsbm_menu_items+=( "|" )
        scsbm_menu_items+=( "off" )
        ((curListNum++))
      fi
    done
  done
  IFS=$OLDIFS
  if [ $curListNum -le 1 ]; then
    showMessageBox "ERROR" "There are no available disks for this operation, returning..."
    return
  fi
  while true;
  do
    selDiskItem=$(whiptail "${scsbm_menu_items[@]}" 3>&1 1>&2 2>&3)
    if [ $? -ne 0 ]; then
      return
    fi
    if [ -z "$selDiskItem" ]; then
      showMessageBox "ERROR" "You did not make a selection, please try again."
    else
      break
    fi
  done
  selDisk=$(echo "$selDiskItem" | cut -d" " -f1)
  if [ -z "$selDisk" ]; then
    showMessageBox "ERROR" "No partition was selected, returning..."
    return
  fi
  sudo mount /dev/$selDisk $HSHQ_BACKUP_DIR > /dev/null
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "There was an error mounting this partition, returning..."
    return
  fi
  newPart=/dev/$selDisk
  newPartID=$(sudo blkid -s UUID -o value $newPart)
  sudo cp /etc/fstab /etc/fstab.old
  echo "# Backup Drive" | sudo tee -a /etc/fstab > /dev/null 2>&1
  if [ -z "$newPartID" ]; then
    echo "$newPart $HSHQ_BACKUP_DIR ext4 defaults,nofail,x-systemd.device-timeout=15 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1
  else
    echo "UUID=$newPartID $HSHQ_BACKUP_DIR ext4 defaults,nofail,x-systemd.device-timeout=15 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1
  fi
  sudo systemctl daemon-reload > /dev/null 2>&1
  sudo findmnt --verify | grep "Success, no errors or warnings detected" > /dev/null 2>&1
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "There was a problem adding the requisite entry to /etc/fstab. Your backup disk will not be auto-mounted after a reboot."
    sudo mv /etc/fstab.old /etc/fstab
  else
    sudo rm -f /etc/fstab.old
  fi
}

function initScreen()
{
  # This function may appear to be overly complex and
  # contrived, but the main purpose is to inject the
  # super-user (sudo) password into the running screen
  # with as minimal of a security footprint as possible.
  # There's also some race conditions to overcome, hence
  # the added complexity. This method avoids anything
  # (long-running) from showing up in the process list
  # (ps auxwwe).
  screenName="$1"
  screenCmd="$2"
  set +e
  dirtest1=/tmp/sctest1
  isScreenSuccess=false
  curScreenAttempt=0
  maxScreenAttempt=5
  screen -XS $screenName quit > /dev/null 2>&1
  # The screen utility has a problem with stuffing
  # special characters in strings, specifically
  # in this case when trying to inject the sudo
  # password. Thus, the easiest way to handle this 
  # is to create a temporary "safe" key, output
  # the encrypted password to file using the key
  # and transmit the key to the screen session.
  sudoPWKey=$(pwgen -c -n 32 1)
  echo -n "$USER_SUDO_PW" | openssl enc -e -aes256 -pbkdf2 -pass pass:$sudoPWKey > $ENC_PW_FILE
  while [ $curScreenAttempt -lt $maxScreenAttempt ]
  do
    ((curScreenAttempt++))
    rm -fr $dirtest1
    screen -L -Logfile $HSHQ_LOG_FILE -dmS $screenName
    screen -S $screenName -X stuff "stty -echo;mkdir $dirtest1\n"
    curCheckCount=0
    while ! [ -d $dirtest1 ] && [ $curCheckCount -lt 5 ]
    do
      ((curCheckCount++))
      sleep 1
    done
    if ! [ -d $dirtest1 ]; then
      screen -XS $screenName quit > /dev/null 2>&1
      continue
    fi
    screen -S $screenName -X stuff "$screenCmd\n"
    curCheckCount=0
    while ! [ -d $TMP_PW_CHECKDIR ] && [ $curCheckCount -lt 15 ]
    do
      ((curCheckCount++))
      sleep 1
    done
    screen -S $screenName -X stuff "$sudoPWKey\n"
    curCheckCount=0
    while [ -d $TMP_PW_CHECKDIR ] && [ $curCheckCount -lt 15 ]
    do
      ((curCheckCount++))
      sleep 1
    done
    rm -fr $dirtest1
    if [ -d $TMP_PW_CHECKDIR ]; then
      rm -fr $TMP_PW_CHECKDIR
      echo "The sudo password was not correctly transmitted to the screen session, exiting..."
      exit 5
    fi
    rm -fr $TMP_PW_CHECKDIR
    isScreenSuccess=true
    break
  done
  if ! [ "$isScreenSuccess" = "true" ]; then
    return 1
  fi
}

function performInstallVariantsPre()
{
  return
}

function performInstallVariantsPost()
{
  return
}

# Stacks Functions
function showStacksMenu()
{
  set +e
  if ! [ "$IS_INSTALLED" = "true" ]; then
    showMessageBox "System Not Installed" "You must perform a system installation first before using this utility."
    exit
  fi
  svcsmenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$svcsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
  "1" "Install Service(s) From List" \
  "2" "Install All Available Services" \
  "3" "Update Service(s) From List" \
  "4" "Update All Available Services" \
  "5" "Remove Service(s) From List" \
  "6" "Exit" 3>&1 1>&2 2>&3)
  if [ $? -ne 0 ] || [ $menures -eq 6 ]; then
    return
  fi
  set +e
  getUpdateAssets
  if [ $? -ne 0 ]; then
    showMessageBox "ERROR" "ERROR: Could not update image assets, returning..."
    return
  fi
  case $menures in
    0)
	  return 0 ;;
    1)
      installStacksFromList
      set +e
      return 1 ;;
    2)
      installAllAvailableStacks true
      set +e
      return 1 ;;
    3)
      performStackUpdatesFromList
      set +e
      return 1 ;;
    4)
      performAllAvailableStackUpdates true
      set +e
      return 1 ;;
    5)
      deleteStacksFromList
      set +e
      return 1 ;;
    6)
	  return 0 ;;
  esac
}

function installStacksFromList()
{
  set +e
  setSystemState $SS_INSTALLING
  sortedStackList=$(sortCSVList $HSHQ_OPTIONAL_STACKS)
  stackListArr=($(echo $sortedStackList | tr "," "\n"))
  menu_items=""
  for curStack in "${stackListArr[@]}"
  do
    if ! [ -d $HSHQ_STACKS_DIR/$curStack ]; then
      menu_items=${menu_items}"$curStack | OFF "
    fi
  done
  if [ -z "$menu_items" ]; then
    showMessageBox "No Available Services" "There are no available services to install."
    return
  fi
  selsvcsmenu=$(cat << EOF

$(getLogo)
Select the services that you wish to install:

EOF
  )
  sel_svcs=($(whiptail --title "Select Services" --checklist "$selsvcsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3))
  if [ -z "$sel_svcs" ]; then
    showMessageBox "Empty Selection" "You have not selected anything, returning to main menu..."
    return 0
  fi
  #showYesNoMessageBox "Perform Integration?" "Do you want to automatically integrate these services, i.e. within Caddy, Heimdall, UptimeKuma, etc.?"
  is_integrate=true
  #if [ $? -eq 0 ]; then
  #  is_integrate=true
  #else
  #  is_integrate=false
  #fi
  showYesNoMessageBox "Confirm Installation" "This selected service(s) will be installed, Continue?"
  if [ $? -ne 0 ]; then
    return
  fi
  setSudoTimeoutInstall
  for cur_svc in "${sel_svcs[@]}"
  do
    installStackByName ${cur_svc//\"} $is_integrate
  done
  removeSudoTimeoutInstall
  outputStackListsScriptServer
  setSystemState $SS_RUNNING
}

function installListOfServices()
{
  echo "Installing list of services, Start time: $(date '+%Y-%m-%d %H:%M:%S')"
  setSystemState $SS_INSTALLING
  stackListArr=($(echo "$1" | tr "," "\n"))
  setSudoTimeoutInstall
  for curStack in "${stackListArr[@]}"
  do
    installStackByName $curStack true
  done
  sudo systemctl daemon-reload
  sudo systemctl restart ntpsec
  docker container restart authelia > /dev/null 2>&1
  removeSudoTimeoutInstall
  outputStackListsScriptServer
  setSystemState $SS_RUNNING
  echo "Installing list of services, End time: $(date '+%Y-%m-%d %H:%M:%S')"
}

function installAllAvailableStacks()
{
  is_msgbox_prompt="$1"
  set +e
  echo "Installing all services, Start time: $(date '+%Y-%m-%d %H:%M:%S')"
  setSystemState $SS_INSTALLING
  echo "Pulling common images..."
  pullCommonImages
  stackListArr=($(echo $HSHQ_OPTIONAL_STACKS | tr "," "\n"))
  sel_svcs=()
  is_list_empty=true
  for curStack in "${stackListArr[@]}"
  do
    if ! [ -d $HSHQ_STACKS_DIR/$curStack ] && ! [ "$(isServiceDisabled $curStack)" = "true" ]; then
      sel_svcs+=($curStack)
      is_list_empty=false
    fi
  done
  if [ "$is_list_empty" = "true" ]; then
    if [ "$is_msgbox_prompt" = "true" ]; then
      showMessageBox "No Available Services" "There are no available services to install."
    fi
    return
  fi
  is_integrate=true
  if [ "$is_msgbox_prompt" = "true" ]; then
    #showYesNoMessageBox "Perform Integration?" "Do you want to automatically integrate these services, i.e. within Caddy, Heimdall, UptimeKuma, etc.?"
    #if [ $? -eq 0 ]; then
    #  is_integrate=true
    #else
    #  is_integrate=false
    #fi
    showYesNoMessageBox "Confirm Installation" "All available service(s) will be installed, Continue?"
    if [ $? -ne 0 ]; then
      return
    fi
  fi
  set -e
  setSudoTimeoutInstall
  for cur_svc in "${sel_svcs[@]}"
  do
    installStackByName $cur_svc $is_integrate
  done
  sudo systemctl restart ntpsec
  docker container restart authelia > /dev/null 2>&1
  removeSudoTimeoutInstall
  outputStackListsScriptServer
  setSystemState $SS_RUNNING
  echo "Installing all services, End time: $(date '+%Y-%m-%d %H:%M:%S')"
}

function getStacksToUpdate()
{
  set +e
  stacks_to_update_list=""
  # Special case for portainer compose
  check_stack_firstline=$(sudo sed -n 1p $HSHQ_STACKS_DIR/portainer/docker-compose.yml)
  if [ "$(checkStackHSHQManaged $check_stack_firstline)" = "true" ]; then
    check_stack_name=$(getStackNameFromComposeLine "$check_stack_firstline")
    if [ $? -eq 0 ]; then
      check_stack_version=$(getVersionFromComposeLine "$check_stack_firstline")
      if [ $? -eq 0 ]; then
        hshq_stack_ver=$(getScriptStackVersionNumber $(getScriptStackVersion $check_stack_name))
        if [ $? -eq 0 ]; then
          if [ $check_stack_version -lt $hshq_stack_ver ]; then
            stacks_to_update_list=${stacks_to_update_list}"${check_stack_name},"
          fi
        fi
      fi
    fi
  fi
  dirList=($(sudo ls $HSHQ_STACKS_DIR/portainer/compose/))
  for curDir in "${dirList[@]}"
  do
    check_stack_firstline=$(sudo sed -n 1p $HSHQ_STACKS_DIR/portainer/compose/$curDir/docker-compose.yml)
    if ! [ "$(checkStackHSHQManaged $check_stack_firstline)" = "true" ]; then
      continue
    fi
    check_stack_name=$(getStackNameFromComposeLine "$check_stack_firstline")
    if [ $? -ne 0 ]; then continue; fi
    check_stack_version=$(getVersionFromComposeLine "$check_stack_firstline")
    if [ $? -ne 0 ] || [ -z "$check_stack_version" ]; then continue; fi
    hshq_stack_ver=$(getScriptStackVersionNumber $(getScriptStackVersion $check_stack_name))
    if [ $? -ne 0 ] || [ -z "$hshq_stack_ver" ]; then continue; fi
    if [ $check_stack_version -lt $hshq_stack_ver ]; then
      stacks_to_update_list=${stacks_to_update_list}"${check_stack_name},"
    fi
    if [ $? -ne 0 ]; then continue; fi
  done
  if ! [ -z "$stacks_to_update_list" ]; then
    stacks_to_update_list=${stacks_to_update_list%?}
  fi
  sortCSVList "$stacks_to_update_list"
}

function performStackUpdatesFromList()
{
  set +e
  this_stack_list=$(getStacksToUpdate)
  stackListArr=($(echo $this_stack_list | tr "," "\n"))
  menu_items=""
  for curStack in "${stackListArr[@]}"
  do
    if [ -z "$curStack" ]; then
      continue
    fi
    menu_items=${menu_items}"$curStack | OFF "
  done
  if [ -z "$menu_items" ]; then
    showMessageBox "No Available Updates" "All services are updated."
    return
  fi
  selsvcsmenu=$(cat << EOF

$(getLogo)
Select the services that you wish to update:

EOF
  )
  sel_svcs=($(whiptail --title "Select Services" --checklist "$selsvcsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3))
  if [ -z "$sel_svcs" ]; then
    showMessageBox "Empty Selection" "You have not selected anything, returning to main menu..."
    return 0
  fi
  showYesNoMessageBox "Confirm Updates" "This selected service(s) will be updated, Continue?"
  if [ $? -ne 0 ]; then
    return
  fi
  update_list=""
  for cur_svc in "${sel_svcs[@]}"
  do
    update_list="${update_list}${cur_svc//\"},"
  done
  updateListOfStacks "$update_list"
}

function performAllAvailableStackUpdates()
{
  is_msgbox_prompt="$1"
  stacksToUpdate="$(getStacksToUpdate)"
  if [ -z "$stacksToUpdate" ]; then
    if [ "$is_msgbox_prompt" = "true" ]; then
      showMessageBox "No Available Updates" "All services are updated."
    else
      echo "All services are updated."
    fi
    return
  fi
  if [ "$is_msgbox_prompt" = "true" ]; then
    showYesNoMessageBox "Confirm Updates" "All available stacks will be updated, Continue?"
    if [ $? -ne 0 ]; then
      return
    fi
  fi
  updateListOfStacks "$stacksToUpdate"
}

function updateListOfStacks()
{
  echo "Updating list of services, Start time: $(date '+%Y-%m-%d %H:%M:%S')"
  stackListArr=($(echo "$1" | tr "," "\n"))
  stacks_need_update_list="$(getStacksToUpdate)"
  setSudoTimeoutInstall
  report_start_time=$(date)
  full_update_report=""
  full_update_report="${full_update_report}        Services that updated successfully:       \n"
  full_update_report="${full_update_report}--------------------------------------------------"
  upgrade_error_report="\n\n"
  upgrade_error_report="${upgrade_error_report}        Services that encountered errors:         \n"
  upgrade_error_report="${upgrade_error_report}--------------------------------------------------"
  set +e
  for cur_svc in "${stackListArr[@]}"
  do
    if [ -z "$cur_svc" ]; then continue; fi
    unset is_upgrade_error
    is_upgrade_error=""
    echo -e "\n\nUpdating ${cur_svc}..."
    if ! [ "$(isItemInCSVList $cur_svc $stacks_need_update_list)" = "true" ]; then
      continue
    fi
    performUpdateStackByName "$cur_svc"
    if [ "$is_upgrade_error" = "true" ]; then
      upgrade_error_report="${upgrade_error_report}\n$perform_update_report\n"
    else
      full_update_report="${full_update_report}\n$perform_update_report"
    fi
  done
  full_update_report="${full_update_report}\n${upgrade_error_report}"
  removeSudoTimeoutInstall
  report_end_time=$(date)
  full_update_report_header=""
  full_update_report_header="${full_update_report_header}==================================================\n"
  full_update_report_header="${full_update_report_header}             Services Upgrade Report              \n"
  full_update_report_header="${full_update_report_header}==================================================\n"
  full_update_report_header="${full_update_report_header}Start Time: $report_start_time\n"
  full_update_report_header="${full_update_report_header}  End Time: $report_end_time\n\n"
  sendEmail -s "Services Upgrade Report" -b "${full_update_report_header}$full_update_report" -f "$(getAdminEmailName) <$EMAIL_SMTP_EMAIL_ADDRESS>" -t $EMAIL_ADMIN_EMAIL_ADDRESS
  echo "Sending update results to email manager ($EMAIL_ADMIN_EMAIL_ADDRESS). Refer to this for further details."
  outputStackListsScriptServer
  echo "Updating list of services, End time: $(date '+%Y-%m-%d %H:%M:%S')"
}

function deleteStacksFromList()
{
  sortedStackList=$(sortCSVList $HSHQ_OPTIONAL_STACKS)
  stackListArr=($(echo $sortedStackList | tr "," "\n"))
  menu_items=""
  for curStack in "${stackListArr[@]}"
  do
    if [ -d $HSHQ_STACKS_DIR/$curStack ]; then
      menu_items=${menu_items}"$curStack | OFF "
    fi
  done
  if [ -z "$menu_items" ]; then
    showMessageBox "No Available Services" "There are no available services to remove."
    return
  fi
  selsvcsmenu=$(cat << EOF

$(getLogo)
Select the services that you wish to remove:

EOF
  )
  sel_svcs=($(whiptail --title "Select Services" --checklist "$selsvcsmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT $menu_items 3>&1 1>&2 2>&3))
  if [ -z "$sel_svcs" ]; then
    showMessageBox "Empty Selection" "You have not selected anything, returning to main menu..."
    return 0
  fi
  showYesNoMessageBox "Confirm Installation" "This selected service(s) will be removed, Continue?"
  if [ $? -ne 0 ]; then
    return
  fi
  del_stack_list=""
  for svc in "${sel_svcs[@]}"
  do
    del_stack_list="${del_stack_list}${svc//\"},"
    cur_svc=${svc//\"}
  done
  ENABLE_STACK_DELETE=true
  deleteListOfStacks false "$del_stack_list"
  ENABLE_STACK_DELETE=false
}

function deleteListOfStacks()
{
  is_force_delete_stack="$1"
  stackListArr=($(echo "$2" | tr "," "\n"))
  setSudoTimeoutInstall
  docker container stop uptimekuma >/dev/null
  docker container stop heimdall >/dev/null
  for cur_stack in "${stackListArr[@]}"
  do
    checkDeleteStackAndDirectory $cur_stack $cur_stack true "$is_force_delete_stack"
    for curSVC in "${SVCS_ARR[@]}"
    do
      cur_stack_name=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f1)
      if [ "${cur_stack_name//\"}" = "$cur_stack" ]; then
        user_type=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f4)
        subdom=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f6)
        deleteSvcAll "${user_type//\"}" "https://${subdom//\"}.$HOMESERVER_DOMAIN" false
      fi
    done
  done
  docker container start uptimekuma >/dev/null
  docker container start heimdall >/dev/null
  removeSudoTimeoutInstall
  restartAllCaddyContainers
  outputStackListsScriptServer
}

function clearRedisTempDataByStackName()
{
  stackName="$1"
  if ! [ -d $HSHQ_NONBACKUP_DIR/$stackName/redis ]; then
    echo "ERROR: Could not find corresponding redis directory"
    return
  fi
  stackStatus=$(getStackStatusByName "$stackName")
  if ! [ "$stackStatus" = "1" ]; then
    echo "WARNING: This stack is not currently running"
    sudo rm -fr $HSHQ_NONBACKUP_DIR/$stackName/redis/*
    return
  fi
  echo "Stopping $stackName..."
  startStopStack "$stackName" stop
  echo "Clearing redis data..."
  sudo rm -fr $HSHQ_NONBACKUP_DIR/$stackName/redis/*
  echo "Starting $stackName..."
  startStopStack "$stackName" start
  echo "Complete!"
}

# VPN Setup
function setupVPNConnection()
{
  if [[ "$(isProgramInstalled wg)" = "false" ]]; then
    echo "Installing WireGuard utils, please wait..."
    performAptInstall wireguard-tools > /dev/null 2>&1
  fi
  if [ "$PRIMARY_VPN_SETUP_TYPE" = "none" ]; then
    vpnmenu=$(cat << EOF

$(getLogo)
EOF
)
    menures=$(whiptail --title "Select an option for your PRIMARY VPN connection" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
    "1" "Host a VPN" \
    "2" "Setup Later" 3>&1 1>&2 2>&3)

    case $menures in
      1)
        PRIMARY_VPN_SETUP_TYPE=host
        setupHostedVPN
        if [ $? -ne 0 ]; then
          return 1
        fi
        ;;
      2)
        PRIMARY_VPN_SETUP_TYPE=none
        updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE
        # Determine if this is a private IP, and if so, let the user know about networking.
        showMsg="Since you will set up a RelayServer later, you will need to manually change the DNS server on any client devices to the IP address of this HomeServer ($HOMESERVER_HOST_PRIMARY_INTERFACE_IP), in order to access your services. "
        if [ "$(checkIsIPPrivate $HOMESERVER_HOST_PRIMARY_INTERFACE_IP)" = "true" ]; then
          showMsg=$showMsg"The client devices must be on the same local network (connected to the same router) as well."
        fi
        showMessageBox "DNS Server" "$showMsg"
        ;;
    esac
  fi
}

function setupJoinPrimaryVPN()
{
  vpnmenu=$(cat << EOF

$(getLogo)
EOF
)
  menures=$(whiptail --title "Select an option" --menu "$vpnmenu" $MENU_HEIGHT $MENU_WIDTH $MENU_INT_HEIGHT \
    "1" "Generate Join Application" \
    "2" "Submit Join Configuration" \
    "3" "Exit" 3>&1 1>&2 2>&3)

  case $menures in
    1)
      applyHomeServerPrimaryVPNConfig
      releaseLock hshqopen "setupJoinPrimaryVPN" false
      return 1 ;;
    2)
      createNetworkJoin false
      if [ $? -ne 0 ]; then
        return 1
      fi
      PRIMARY_VPN_SETUP_TYPE=join
      updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE ;;
    3)
      return 1 ;;
  esac
}

function initRelayServerCredentials()
{
  RELAYSERVER_WG_SV_IP=$(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f2- | rev).$(($(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f1 | rev)))
  updateConfigVar RELAYSERVER_WG_SV_IP $RELAYSERVER_WG_SV_IP
  if [ -z "$RELAYSERVER_WG_SV_PRIVATEKEY" ]; then
    RELAYSERVER_WG_SV_PRIVATEKEY=$(wg genkey)
    updateConfigVar RELAYSERVER_WG_SV_PRIVATEKEY $RELAYSERVER_WG_SV_PRIVATEKEY
    RELAYSERVER_WG_SV_PUBLICKEY=$(echo $RELAYSERVER_WG_SV_PRIVATEKEY | wg pubkey)
    updateConfigVar RELAYSERVER_WG_SV_PUBLICKEY $RELAYSERVER_WG_SV_PUBLICKEY
  fi
  RELAYSERVER_WG_SV_PRESHAREDKEY=$(wg genpsk)
  updateConfigVar RELAYSERVER_WG_SV_PRESHAREDKEY $RELAYSERVER_WG_SV_PRESHAREDKEY
  RELAYSERVER_WG_SV_CLIENTDNS_IP=$(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f2- | rev).$(($(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f1 | rev)-1))
  updateConfigVar RELAYSERVER_WG_SV_CLIENTDNS_IP $RELAYSERVER_WG_SV_CLIENTDNS_IP
  RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY=$(wg genkey)
  updateConfigVar RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY $RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY
  RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY=$(wg genpsk)
  updateConfigVar RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY $RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY
  RELAYSERVER_WG_SV_CLIENTDNS_PUBLICKEY=$(echo $RELAYSERVER_WG_SV_CLIENTDNS_PRIVATEKEY | wg pubkey)
  RELAYSERVER_WG_HS_IP=$(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f2- | rev).$(($(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f1 | rev)-2))
  updateConfigVar RELAYSERVER_WG_HS_IP $RELAYSERVER_WG_HS_IP
  RELAYSERVER_WG_HS_PRIVATEKEY=$(wg genkey)
  updateConfigVar RELAYSERVER_WG_HS_PRIVATEKEY $RELAYSERVER_WG_HS_PRIVATEKEY
  RELAYSERVER_WG_HS_PRESHAREDKEY=$(wg genpsk)
  updateConfigVar RELAYSERVER_WG_HS_PRESHAREDKEY $RELAYSERVER_WG_HS_PRESHAREDKEY
  RELAYSERVER_WG_HS_PUBLICKEY=$(echo $RELAYSERVER_WG_HS_PRIVATEKEY | wg pubkey)
  RELAYSERVER_WG_HS_CLIENTDNS_IP=$(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f2- | rev).$(($(sipcalc $PRIMARY_VPN_SUBNET | grep "^Usable range" | rev | cut -d" " -f1 | cut -d"." -f1 | rev)-3))
  updateConfigVar RELAYSERVER_WG_HS_CLIENTDNS_IP $RELAYSERVER_WG_HS_CLIENTDNS_IP
  RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY=$(wg genkey)
  updateConfigVar RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY $RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY
  RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY=$(wg genpsk)
  updateConfigVar RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY $RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY
  RELAYSERVER_WG_HS_CLIENTDNS_PUBLICKEY=$(echo $RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY | wg pubkey)
  RELAYSERVER_WG_INTERNET_HS_IP=$(getRandomWireGuardIP)
  RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY=$(wg genkey)
  updateConfigVar RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY $RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY
  RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY=$(wg genpsk)
  updateConfigVar RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY $RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY
  RELAYSERVER_WG_INTERNET_HS_PUBLICKEY=$(echo $RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY | wg pubkey)
  RELAYSERVER_WG_USER_IP=$(getRandomWireGuardIP)
  RELAYSERVER_WG_USER_PRIVATEKEY=$(wg genkey)
  RELAYSERVER_WG_USER_PRESHAREDKEY=$(wg genpsk)
  RELAYSERVER_WG_USER_PUBLICKEY=$(echo $RELAYSERVER_WG_USER_PRIVATEKEY | wg pubkey)
  SMTP_RELAY_HOST="[$SUB_POSTFIX.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN]:$MAILU_PORT_5"
  updateConfigVar SMTP_RELAY_HOST $SMTP_RELAY_HOST
  SMTP_RELAY_USERNAME=$HOMESERVER_DOMAIN
  updateConfigVar SMTP_RELAY_USERNAME $SMTP_RELAY_USERNAME
  SMTP_RELAY_PASSWORD=$(pwgen -c -n 32 1)
  updateConfigVar SMTP_RELAY_PASSWORD $SMTP_RELAY_PASSWORD
}

function outputHostedVPNConfigs()
{
  sudo rm -f $HSHQ_WIREGUARD_DIR/vpn/${RELAYSERVER_WG_VPN_NETNAME}.conf
  sudo tee $HSHQ_WIREGUARD_DIR/vpn/${RELAYSERVER_WG_VPN_NETNAME}.conf >/dev/null <<EOFCF
[Interface]
PrivateKey = $RELAYSERVER_WG_HS_PRIVATEKEY
Address = ${RELAYSERVER_WG_HS_IP}/32
MTU = $RELAYSERVER_CLIENT_DEFAULT_MTU
Table = off
PostUp = ip route add 10.0.0.0/8 dev %i table 1100; ip route add $PRIMARY_VPN_SUBNET dev %i; ip rule add fwmark 1100 table 1100 pref 20000; iptables -t mangle -A PREROUTING -i %i -j CONNMARK --set-mark 1100; iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark; iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff
PreDown = iptables -t mangle -D OUTPUT -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff; iptables -t mangle -D PREROUTING -j CONNMARK --restore-mark; iptables -t mangle -D PREROUTING -i %i -j CONNMARK --set-mark 1100; ip rule del fwmark 1100 table 1100 pref 20000; ip route del $PRIMARY_VPN_SUBNET dev %i; ip route del 10.0.0.0/8 dev %i table 1100

[Peer]
PublicKey = $RELAYSERVER_WG_SV_PUBLICKEY
PresharedKey = $RELAYSERVER_WG_HS_PRESHAREDKEY
AllowedIPs = 10.0.0.0/8
Endpoint = $RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN:$RELAYSERVER_WG_PORT
PersistentKeepalive = $RELAYSERVER_PERSISTENT_KEEPALIVE
EOFCF
  sudo chmod 0400 $HSHQ_WIREGUARD_DIR/vpn/${RELAYSERVER_WG_VPN_NETNAME}.conf
  sudo cp $HSHQ_WIREGUARD_DIR/vpn/${RELAYSERVER_WG_VPN_NETNAME}.conf /etc/wireguard/${RELAYSERVER_WG_VPN_NETNAME}.conf
  sudo rm -f $HSHQ_WIREGUARD_DIR/internet/${RELAYSERVER_WG_INTERNET_NETNAME}.conf
  tableid=$(getNextWGRoutingTableID)
  dockerNetworkName="dwg-${RELAYSERVER_WG_INTERNET_NETNAME}"
  sudo tee $HSHQ_WIREGUARD_DIR/internet/${RELAYSERVER_WG_INTERNET_NETNAME}.conf >/dev/null <<EOFCF
#NETNAME=$RELAYSERVER_WG_INTERNET_NETNAME
#ROUTING_TABLE_ID=$tableid
#DOCKER_NETWORK_NAME=$dockerNetworkName
#DOCKER_NETWORK_SUBNET=
#CLIENT_ADDRESS=${RELAYSERVER_WG_INTERNET_HS_IP}/32
#MTU=$RELAYSERVER_CLIENT_DEFAULT_MTU
#IS_ENABLED=true
#EXT_DOMAIN=$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN

[Interface]
PrivateKey = $RELAYSERVER_WG_INTERNET_HS_PRIVATEKEY

[Peer]
PublicKey = $RELAYSERVER_WG_SV_PUBLICKEY
PresharedKey = $RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY
AllowedIPs = $(getAllowedPublicIPs)
Endpoint = $RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN:$RELAYSERVER_WG_PORT
PersistentKeepalive = $RELAYSERVER_PERSISTENT_KEEPALIVE
EOFCF
  sudo chmod 0400 $HSHQ_WIREGUARD_DIR/internet/${RELAYSERVER_WG_INTERNET_NETNAME}.conf
  sudo rm -f $HSHQ_WIREGUARD_DIR/users/${RELAYSERVER_WG_VPN_NETNAME}-user1.conf
  sudo tee $HSHQ_WIREGUARD_DIR/users/${RELAYSERVER_WG_VPN_NETNAME}-user1.conf >/dev/null <<EOFCF
[Interface]
PrivateKey = $RELAYSERVER_WG_USER_PRIVATEKEY
Address = ${RELAYSERVER_WG_USER_IP}/32
MTU = $RELAYSERVER_CLIENT_DEFAULT_MTU
# DNS Servers (Use only one at a time)
# Home Network DNS Server
DNS = $RELAYSERVER_WG_HS_CLIENTDNS_IP
# VPN DNS Server
#DNS = $RELAYSERVER_WG_SV_CLIENTDNS_IP

[Peer]
PublicKey = $RELAYSERVER_WG_SV_PUBLICKEY
PresharedKey = $RELAYSERVER_WG_USER_PRESHAREDKEY
AllowedIPs = $(getAllowedPublicIPs $PRIMARY_VPN_SUBNET)
Endpoint = $RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN:$RELAYSERVER_WG_PORT
PersistentKeepalive = $RELAYSERVER_PERSISTENT_KEEPALIVE
EOFCF
  sudo chmod 0400 $HSHQ_WIREGUARD_DIR/users/${RELAYSERVER_WG_VPN_NETNAME}-user1.conf
  sudo rm -f $HSHQ_WIREGUARD_DIR/users/clientdns-user1.conf
  sudo tee $HSHQ_WIREGUARD_DIR/users/clientdns-user1.conf >/dev/null <<EOFCF
[Interface]
PrivateKey = $RELAYSERVER_WG_HS_CLIENTDNS_PRIVATEKEY
Address = ${RELAYSERVER_WG_HS_CLIENTDNS_IP}/32
MTU = $RELAYSERVER_CLIENT_DEFAULT_MTU

[Peer]
PublicKey = $RELAYSERVER_WG_SV_PUBLICKEY
PresharedKey = $RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY
AllowedIPs = 10.0.0.0/8
Endpoint = $RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN:$RELAYSERVER_WG_PORT
PersistentKeepalive = $RELAYSERVER_PERSISTENT_KEEPALIVE
EOFCF
  sudo chmod 0400 $HSHQ_WIREGUARD_DIR/users/clientdns-user1.conf
}

function insertSQLHostedVPN()
{
  curdt=$(getCurrentDate)
  sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated) values('WireGuardServer','$EMAIL_ADMIN_EMAIL_ADDRESS','wgserver','relayserver','$RELAYSERVER_WG_SV_PUBLICKEY','$RELAYSERVER_WG_SV_PRESHAREDKEY','$RELAYSERVER_WG_SV_IP',false,'$RELAYSERVER_WG_INTERFACE_NAME','$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN','$curdt');"
  sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated) values('RelayServerClientDNS','$EMAIL_ADMIN_EMAIL_ADDRESS','clientdns','relayserver','$RELAYSERVER_WG_SV_CLIENTDNS_PUBLICKEY','$RELAYSERVER_WG_SV_CLIENTDNS_PRESHAREDKEY','$RELAYSERVER_WG_SV_CLIENTDNS_IP',false,'wg0','$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN','$curdt');"
  db_id=$(sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated,Network_Subnet,IsExposeToNetwork,InputAllowPorts,DockerUserAllowPorts) values('Primary-VPN-${HOMESERVER_DOMAIN}','$EMAIL_ADMIN_EMAIL_ADDRESS','homeserver_vpn','primary','$RELAYSERVER_WG_HS_PUBLICKEY','$RELAYSERVER_WG_HS_PRESHAREDKEY','$RELAYSERVER_WG_HS_IP',false,'$RELAYSERVER_WG_VPN_NETNAME','$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN','$curdt','$PRIMARY_VPN_SUBNET',true,'$INPUT_PRIMARY_VPN_ALLOW_PORTS_DEFAULT','$DOCKERUSER_PRIMARY_VPN_ALLOW_PORTS_DEFAULT');select last_insert_rowid();")
  mail_host_id=$(sudo sqlite3 $HSHQ_DB "insert into mailhosts(MailHost) values('$SUB_POSTFIX.$HOMESERVER_DOMAIN');select last_insert_rowid();")
  sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into hsvpn_connections(ID,HomeServerName,IsPrimary,DomainName,ExternalPrefix,InternalPrefix,MailHostID,CA_Abbrev,CA_IP,CA_Subdomain,CA_URL,RS_VPN_IP,VPNRoutingTable) values($db_id,'$HOMESERVER_NAME',1,'$HOMESERVER_DOMAIN','$EXT_DOMAIN_PREFIX','$INT_DOMAIN_PREFIX',$mail_host_id,'$HOMESERVER_ABBREV','$RELAYSERVER_WG_HS_IP','$SUB_CADDY.$HOMESERVER_DOMAIN','https://$SUB_CADDY.$HOMESERVER_DOMAIN/acme/$HOMESERVER_ABBREV/directory' ,'$RELAYSERVER_WG_SV_IP',1100);"
  sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into mailhostmap(MailHostID,Domain,IsFirstDomain) values ($mail_host_id,'$HOMESERVER_DOMAIN',true);"
  sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated) values('clientdns-user1','$EMAIL_ADMIN_EMAIL_ADDRESS','clientdns','primary','$RELAYSERVER_WG_HS_CLIENTDNS_PUBLICKEY','$RELAYSERVER_WG_HS_CLIENTDNS_PRESHAREDKEY','$RELAYSERVER_WG_HS_CLIENTDNS_IP',false,'wg0','$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN','$curdt');"
  sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,InterfaceName,EndpointHostname,LastUpdated) values('Primary-Internet-${HOMESERVER_DOMAIN}','$EMAIL_ADMIN_EMAIL_ADDRESS','homeserver_internet','primary','$RELAYSERVER_WG_INTERNET_HS_PUBLICKEY','$RELAYSERVER_WG_INTERNET_HS_PRESHAREDKEY','$RELAYSERVER_WG_INTERNET_HS_IP',true,'$RELAYSERVER_WG_INTERNET_NETNAME','$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN','$curdt');"
  sudo sqlite3 $HSHQ_DB "insert into connections(Name,EmailAddress,ConnectionType,NetworkType,PublicKey,PresharedKey,IPAddress,IsInternet,EndpointHostname,LastUpdated) values('User-1','$EMAIL_ADMIN_EMAIL_ADDRESS','user','mynetwork','$RELAYSERVER_WG_USER_PUBLICKEY','$RELAYSERVER_WG_USER_PRESHAREDKEY','$RELAYSERVER_WG_USER_IP',true,'$RELAYSERVER_SUB_WG.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN','$curdt');"
  updateDeviceIPMappingFile
}

function updateHeimdallUptimeKumaRelayServer()
{
  docker container stop heimdall > /dev/null 2>&1
  docker container stop uptimekuma > /dev/null 2>&1
  insertEnableSvcHeimdall adguard "${FMLNAME_ADGUARD}" relayserver "https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "adguardhome.png" false "$(getHeimdallOrderFromSub $SUB_ADGUARD.$INT_DOMAIN_PREFIX relayserver)"
  insertEnableSvcUptimeKuma adguard "${FMLNAME_ADGUARD}-RelayServer" relayserver "https://$SUB_ADGUARD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false
  insertEnableSvcHeimdall clientdns "${FMLNAME_CLIENTDNS}" relayserver "https://$SUB_CLIENTDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "dnsmasq.png" false "$(getHeimdallOrderFromSub $SUB_CLIENTDNS.$INT_DOMAIN_PREFIX relayserver)"
  checkInsertServiceHeimdall caddy-dns "${FMLNAME_CADDYDNS}" relayserver "https://$SUB_CADDYDNS.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "dnsmasq.png" false 0 "$(getHeimdallOrderFromSub $SUB_CADDYDNS.$INT_DOMAIN_PREFIX relayserver)"
  insertEnableSvcHeimdall portainer "${FMLNAME_PORTAINER}" relayserver "https://$SUB_PORTAINER.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "portainer.png" false "$(getHeimdallOrderFromSub $SUB_PORTAINER.$INT_DOMAIN_PREFIX relayserver)"
  insertEnableSvcUptimeKuma portainer "${FMLNAME_PORTAINER}-RelayServer" relayserver "https://$SUB_PORTAINER.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false
  insertEnableSvcHeimdall rspamd "${FMLNAME_RSPAMD}" relayserver "https://$SUB_RSPAMD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "rspamd.png" false "$(getHeimdallOrderFromSub $SUB_RSPAMD.$INT_DOMAIN_PREFIX relayserver)"
  insertEnableSvcUptimeKuma rspamd "${FMLNAME_RSPAMD}-RelayServer" relayserver "https://$SUB_RSPAMD.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false
  insertEnableSvcHeimdall syncthing "${FMLNAME_SYNCTHING}" relayserver "https://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "syncthing.png" false "$(getHeimdallOrderFromSub $SUB_SYNCTHING.$INT_DOMAIN_PREFIX relayserver)"
  insertEnableSvcUptimeKuma syncthing "${FMLNAME_SYNCTHING}-RelayServer" relayserver "https://$SUB_SYNCTHING.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false
  checkInsertServiceHeimdall wgportal "${FMLNAME_WGPORTAL}" relayserver "https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "wgportal.png" false 0 "$(getHeimdallOrderFromSub $SUB_WGPORTAL.$INT_DOMAIN_PREFIX relayserver)"
  insertEnableSvcUptimeKuma wgportal "${FMLNAME_WGPORTAL}-RelayServer" relayserver "https://$SUB_WGPORTAL.$INT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false
  checkInsertServiceUptimeKuma filebrowser "${FMLNAME_FILEBROWSER}-RelayServer" relayserver "https://$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" false 0
  checkInsertServiceHeimdall filebrowser "${FMLNAME_FILEBROWSER}" relayserver "https://$SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "filebrowser.png" false 0 "$(getHeimdallOrderFromSub $SUB_FILEBROWSER.$EXT_DOMAIN_PREFIX relayserver)"
  docker container start heimdall > /dev/null 2>&1
  docker container start uptimekuma > /dev/null 2>&1
}

function prepSvcsHostedVPN()
{
  set +e
  echo "Updating Portainer and Jitsi..."
  updateSvcsIPChanges
  set +e
  echo "Updating IP tables..."
  checkUpdateAllIPTables prepSvcsHostedVPN
}

function webTransferHostedVPN()
{
  # These variables should already be set by Script-server
  # rs_cur_username
  # rs_external_ip
  # rs_cur_password
  # rs_new_password
  # rs_cur_ssh_port
  # rs_new_ssh_port
  set +e
  # Clear out any old hosts
  ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1
  ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SERVER_IP]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1
  ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$rs_external_ip]:$rs_cur_ssh_port" > /dev/null 2>&1
  ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$rs_external_ip]:$rs_new_ssh_port" > /dev/null 2>&1
  if ! [ "$rs_cur_username" = "root" ] && ! [ "$rs_cur_username" = "$RELAYSERVER_REMOTE_USERNAME" ]; then
    echo "ERROR: The username on the new RelayServer must match the existing username ($RELAYSERVER_REMOTE_USERNAME)"
    return 2
  fi
  rs_new_username="$RELAYSERVER_REMOTE_USERNAME"
  if ! [ "$rs_cur_username" = "root" ]; then
    rs_new_password="$rs_cur_password"
  fi
  # Login, upload check script, log back in and run check script
  # 1. Login
  echo "Logging into RelayServer..."
  perfRemoteAction -m ssh -p $rs_cur_ssh_port -s "$rs_cur_password" -o "-T -o ConnectTimeout=5 -o 'StrictHostKeyChecking accept-new'" -u "$rs_cur_username" -h "$rs_external_ip" -c "echo hello >/dev/null" -f
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was a problem logging in to the RelayServer, returning..."
    return 2
  fi
  # 2. Upload check script
  echo "Uploading validation script..."
  outputRelayServerValidationScript
  outputRelayServerInstallSetupScript
  perfRemoteAction -m scp -p $rs_cur_ssh_port -s "$rs_cur_password" -a $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME -u $rs_cur_username -h $rs_external_ip -c ":~" -f
  is_err=$?
  rm -f $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was an problem uploading the validation script to the RelayServer, returning..."
    return 2
  fi
  perfRemoteAction -m scp -p $rs_cur_ssh_port -s "$rs_cur_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $rs_cur_username -h $rs_external_ip -c ":~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME" -f
  is_err=$?
  rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was an problem uploading the lib script to the RelayServer, returning..."
    return 2
  fi
  # 3. Log back in and run check script
  echo "Performing pre-installation checks, please wait..."
  perfRemoteAction -m ssh -p $rs_cur_ssh_port -s "$rs_cur_password" -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_cur_username -h $rs_external_ip -c "bash ~/$RS_INSTALL_VALIDATION_SCRIPT_NAME -s" -f -i "$rs_new_password"
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was a problem with the RelayServer, see above."
    return 2
  fi
  echo "Testing RelayServer login with pub/priv key..."
  perfRemoteAction -m ssh -p $rs_cur_ssh_port -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $rs_external_ip -c "echo \"Successfully logged in to RelayServer with key! \"" -f
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "ERROR: Could not login to RelayServer with pub/priv key."
    return 2
  fi
  setSudoTimeoutInstall
  setSystemState $SS_TRANSFERRING
  # Pause syncthing RelayServer
  curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X PATCH -d "{\"paused\": true}" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices/$RELAYSERVER_SYNCTHING_DEVICE_ID
  echo "Compressing RelayServer data..."
  sudo tar cvzf $HOME/rsbackup.tar.gz -C $HSHQ_RELAYSERVER_DIR/ ./backup >/dev/null
  echo "Checking if RelayServer is ready..."
  countRSPreRetries=0
  maxRSPreRetries=20
  while true;
  do
    perfRemoteAction -m ssh -p $rs_cur_ssh_port -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "sleep 1;if [ -f ~/$RELAYSERVER_NOT_READY_FILE ]; then exit 1; fi" -r 1 -b 1
    if [ $? -eq 0 ]; then
      break
    fi
    if [ $countRSPreRetries -ge $maxRSPreRetries ]; then
      echo "Max retries reached. Something has likely gone wrong with the RelayServer pre-installation script, exiting..."
      return 2
    fi
    ((countRSPreRetries++))
    echo "($countRSPreRetries of $maxRSPreRetries) RelayServer not ready for installation, sleeping 30 seconds then will retry..."
    sleep 30
  done
  removeHomeNetIP ${RELAYSERVER_SERVER_IP}/32 false
  RELAYSERVER_SERVER_IP="$rs_external_ip"
  RELAYSERVER_CURRENT_SSH_PORT="$rs_cur_ssh_port"
  RELAYSERVER_SSH_PORT="$rs_new_ssh_port"
  outputRelayServerInstallSetupScript
  outputRelayServerInstallTransferScript
  perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_new_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $rs_new_username -h $rs_external_ip -c ":~/$RS_INSTALL_SETUP_SCRIPT_NAME" -f
  is_err=$?
  rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was an problem uploading the setup script to the RelayServer, returning..."
    return 2
  fi
  perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_new_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_TRANSFER_SCRIPT_NAME -u $rs_new_username -h $rs_external_ip -c ":~/$RS_INSTALL_TRANSFER_SCRIPT_NAME" -f
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was an problem uploading the transfer script to the RelayServer, returning..."
    return 2
  fi
  perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -a $HOME/rsbackup.tar.gz -u $rs_new_username -h $rs_external_ip -c ":/home/$RELAYSERVER_REMOTE_USERNAME" -f
  sudo rm -f $HOME/rsbackup.tar.gz
  echo "Initializing transfer process..."
  perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_new_password" -o "-tt -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "bash ~/$RS_INSTALL_TRANSFER_SCRIPT_NAME -i" -f -i "$rs_new_password" -r 1 -b 1
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "There was a problem initializing the transfer, see above."
    return $is_err
  fi
  echo "Transfer Initialization complete! Rebooting.."
  perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -s "$rs_new_password" -o "-tt -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "sudo reboot" -f -i "$rs_new_password"
  echo
  echo
  echo "============================================================"
  echo "                  RelayServer is rebooting...               "
  echo "           You can now modify your DNS Records to           "
  echo "          point to the new RelayServer's IP Address         "
  echo "============================================================"
  echo
  echo
  sleep 30
  countRSPreRetries=0
  maxRSPreRetries=20
  while true;
  do
    perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "echo hello >/dev/null" -r 1 -b 1
    if [ $? -eq 0 ]; then
      break
    fi
    if [ $countRSPreRetries -ge $maxRSPreRetries ]; then
      echo "Max retries reached. Something has likely gone wrong with the RelayServer transfer init script."
      read -r -p "Enter cancel to exit or any other key to retry: " raRetry
      if [ "$raRetry" = "cancel" ]; then
        return 2
      fi
    fi
    ((countRSPreRetries++))
    echo "($countRSPreRetries of $maxRSPreRetries) RelayServer not ready for transfer, sleeping 15 seconds then will retry..."
    sleep 15
  done
  echo "RelayServer has rebooted, finishing transfer..."
  perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -s "$rs_new_password" -o "-tt -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_new_username -h $rs_external_ip -c "bash ~/$RS_INSTALL_TRANSFER_SCRIPT_NAME -s" -f -i "$rs_new_password" -r 1 -b 1
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "There was a problem performing the transfer, see above."
    return $is_err
  fi
  echo "The RelayServer is fully prepped for transfer. If you haven't already updated your DNS records, ensure to do that now. Both A records should point to $rs_external_ip. Please also review the above logs to ensure there were no errors. If everything appears normal, then enter continue. If there are any errors, then enter cancel to halt the process."
  while true;
  do
    read -r -p "Enter continue or cancel: " raRetry
    if [ "$raRetry" = "cancel" ]; then
      return 2
    elif [ "$raRetry" = "continue" ]; then
      break
    else
      echo "Invalid input."
    fi
  done
  updateConfigVar RELAYSERVER_SERVER_IP $RELAYSERVER_SERVER_IP
  updateConfigVar RELAYSERVER_CURRENT_SSH_PORT $RELAYSERVER_CURRENT_SSH_PORT
  updateConfigVar RELAYSERVER_SSH_PORT $RELAYSERVER_SSH_PORT
  addHomeNetIP ${RELAYSERVER_SERVER_IP}/32 true
  addDomainAdguardHS "*.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "$RELAYSERVER_SERVER_IP"
  # Restart all vpn connections
  echo "Restarting all wg connections..."
  db_id=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_vpn' and NetworkType='primary';")
  ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;")
  sudo systemctl restart wg-quick@$ifaceName
  db_id=$(sqlite3 $HSHQ_DB "select ID from connections where ConnectionType='homeserver_internet' and NetworkType='primary';")
  ifaceName=$(sqlite3 $HSHQ_DB "select InterfaceName from connections where ID=$db_id;")
  sudo $HSHQ_WIREGUARD_DIR/scripts/wgDockInternet.sh $HSHQ_WIREGUARD_DIR/internet/${ifaceName}.conf restart
  clientdns_arr=($(docker ps -a --filter name=clientdns.*wireguard --format "{{.Names}}"))
  for curCDNS in "${clientdns_arr[@]}"
  do
    curStackName=$(echo "$curCDNS" | rev | cut -d"-" -f2- | rev)
    if ! [ -d "$HSHQ_STACKS_DIR/$curStackName" ] || ! [ -f "$HSHQ_STACKS_DIR/$curStackName/${curStackName}.conf" ]; then
      continue
    fi
    startStopStack "$curStackName" stop
    startStopStack "$curStackName" start
  done
  updateEndpointIPs
  jsonbody="{\"paused\": false}"
  curl -s -H "X-API-Key: $SYNCTHING_API_KEY" -X PATCH -d "$jsonbody" -k https://127.0.0.1:$SYNCTHING_LOCAL_WEB_PORT/rest/config/devices/$RELAYSERVER_SYNCTHING_DEVICE_ID
  docker container restart syncthing
  perfRemoteAction -m ssh -p $RELAYSERVER_SSH_PORT -o "-T -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN -c "echo \"Successful! IP Address is: \$(curl --silent https://api.ipify.org)\"" -f
  notifyMyNetworkTransferRelayServer
  removeSudoTimeoutInstall
  setSystemState $SS_RUNNING
}

function webSetupHostedVPN()
{
  set +e
  # These variables should already be set by Script-server
  # rs_name
  # rs_cur_username
  # rs_cur_password
  # rs_cur_ssh_port
  # rs_external_ip
  # rs_ledomains
  # rs_new_username
  # rs_new_password
  # rs_new_ssh_port
  # rs_primary_vpn_subnet
  case "$PRIMARY_VPN_SETUP_TYPE" in
    "host")
      echo "ERROR: You are already hosting a VPN, please remove the existing one first, returning..."
      return
    ;;
    "join")
      echo "ERROR: You have already joined an existing primary VPN. You must remove this first, returning..."
      return
    ;;
    "none"|"manual")
      PRIMARY_VPN_SETUP_TYPE=none
      updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE "$PRIMARY_VPN_SETUP_TYPE"
    ;;
    *)
      echo "ERROR: Unknown VPN option, returning..."
      return
    ;;
  esac
  pullImage $IMG_WIREGUARD
  if [ $? -ne 0 ]; then
    echo "ERROR: There was a problem pulling the WireGuard docker image..."
    return
  fi
  pullImage $IMG_DNSMASQ
  if [ $? -ne 0 ]; then
    echo "ERROR: There was a problem pulling the DNSMasq docker image..."
    return
  fi
  RELAYSERVER_WG_VPN_NETNAME="vpn-"${HOMESERVER_ABBREV:0:6}
  curNum=1
  while [ "$(checkInterfaceNameExists $RELAYSERVER_WG_VPN_NETNAME)" = "true" ] && [ $curNum -lt 100 ]
  do
    RELAYSERVER_WG_VPN_NETNAME="vpn-${HOMESERVER_ABBREV:0:5}${curNum}"
    ((curNum++))
  done
  if [ "$(checkInterfaceNameExists $RELAYSERVER_WG_VPN_NETNAME)" = "true" ]; then
    echo "ERROR: You have 99 other connections that start with ${HOMESERVER_ABBREV:0:5}?!? Returning..."
    return
  fi
  RELAYSERVER_WG_INTERNET_NETNAME="ext-${HOMESERVER_ABBREV:0:6}"
  curNum=1
  while [ "$(checkInterfaceNameExists $RELAYSERVER_WG_INTERNET_NETNAME)" = "true" ] && [ $curNum -lt 100 ]
  do
    RELAYSERVER_WG_INTERNET_NETNAME="ext-${HOMESERVER_ABBREV:0:5}${curNum}"
    ((curNum++))
  done
  if [ "$(checkInterfaceNameExists $RELAYSERVER_WG_INTERNET_NETNAME)" = "true" ]; then
    echo "ERROR: You have 99 other connections that start with ${HOMESERVER_ABBREV:0:5}?!? Returning..."
    return
  fi
  RELAYSERVER_EXT_EMAIL_HOSTNAME=$SUB_POSTFIX.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN
  if [ $(checkValidStringUpperLowerNumbers "$rs_name" "[:space:],.-") = "false" ]; then
    echo "ERROR: The selected RelayServer name contains invalid characters, returning..."
    return
  fi
  RELAYSERVER_NAME="$rs_name"
  if ! [ -z "$rs_ledomains" ] && [ $(checkValidString "$rs_ledomains" ",.-") = "false" ]; then
    echo "ERROR: The selected LetsEncrypt subdomains list contains invalid characters, returning..."
    return
  fi
  if [ "$rs_cur_username" = "root" ]; then
    if [ "$(checkValidUsername $rs_new_username)" = "false" ]; then
      echo "ERROR: The selected username is invalid. It must consists of a-z and/or 0-9. No special characters, the length must be between 4-32 characters, and it must begin with a letter, returning..."
      return
    fi
    if [ "$(checkValidPassword "$rs_new_password" 16)" = "false" ]; then
      echo "ERROR: The password is invalid or is too weak($rs_new_password). It must contain at least 16 characters and consist of uppercase letters, lowercase letters, and numbers. It can only contain the following symbols: ~!@#%^&*()-_+=<>?.,"
      return
    fi
    USER_RELAY_SUDO_PW="$rs_new_password"
  else
    rs_new_username="$rs_cur_username"
    USER_RELAY_SUDO_PW="$rs_cur_password"
  fi
  RELAYSERVER_LE_CERT_DOMAINS="$rs_ledomains"
  RELAYSERVER_REMOTE_USERNAME="$rs_new_username"
  RELAYSERVER_SERVER_IP="$rs_external_ip"
  RELAYSERVER_CURRENT_SSH_PORT="$rs_cur_ssh_port"
  if [ "$(checkValidIPAddress $rs_primary_vpn_subnet)" = "false" ]; then
    echo "ERROR: Invalid RelayServer IP address, returning..."
    return
  fi
  if ! [ "$(echo "$rs_primary_vpn_subnet" | cut -d"." -f1)" = "10" ]; then
    echo "ERROR: VPN subnet must be in the 10.0.0.0/8 range, returning..."
    return
  fi
  is_intersect="$(isNetworkIntersectOurNetworks ${rs_primary_vpn_subnet}/24 false true)"
  if ! [ -z "$is_intersect" ]; then
    echo "ERROR: VPN subnet collision - $is_intersect, returning..."
    return
  fi
  PRIMARY_VPN_SUBNET="${rs_primary_vpn_subnet}/24"
  PRIMARY_VPN_SETUP_TYPE=host
  setupPortForwardingDB
  chkSSHPort="$(checkPortForwardingIntersect tcp $rs_new_ssh_port $rs_new_ssh_port)"
  if ! [ -z "$chkSSHPort" ]; then
    sudo sqlite3 $HSHQ_DB "drop table portforwarding;"
    echo "ERROR: $chkSSHPort, returning..."
    return
  fi
  RELAYSERVER_SSH_PORT="$rs_new_ssh_port"
  RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=""
  tmp_port=$PORTAINER_LOCAL_HTTPS_PORT
  while [ -z "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT" ]
  do
    chkSSHPort="$(checkPortForwardingIntersect tcp $tmp_port $tmp_port)"
    if ! [ -z "$chkSSHPort" ]; then
      tmp_port=$((9000 + $RANDOM % 999))
    else
      RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=$tmp_port
      break
    fi
  done
  # Clear out any old hosts
  ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SUB_RELAYSERVER.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1
  ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[$RELAYSERVER_SERVER_IP]:$RELAYSERVER_SSH_PORT" > /dev/null 2>&1
  if [ -z "$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME" ]; then
    echo "Generating keys, please wait..."
    RELAYSERVER_SSH_PRIVATE_KEY_FILENAME=$HOMESERVER_ABBREV".key"
    updateConfigVar RELAYSERVER_SSH_PRIVATE_KEY_FILENAME $RELAYSERVER_SSH_PRIVATE_KEY_FILENAME
    rm -f $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME
    rm -f $HSHQ_CONFIG_DIR/${RELAYSERVER_SSH_PRIVATE_KEY_FILENAME}.pub
    RELAYSERVER_SSH_PRIVATE_KEY_PHRASE=$(pwgen -c -n 64 1)
    updateConfigVar RELAYSERVER_SSH_PRIVATE_KEY_PHRASE $RELAYSERVER_SSH_PRIVATE_KEY_PHRASE
    ssh-keygen -f $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME -N $RELAYSERVER_SSH_PRIVATE_KEY_PHRASE
    chmod 0400 $HSHQ_CONFIG_DIR/$RELAYSERVER_SSH_PRIVATE_KEY_FILENAME
    chmod 0400 $HSHQ_CONFIG_DIR/${RELAYSERVER_SSH_PRIVATE_KEY_FILENAME}.pub
  fi
  # Login, upload check script, log back in and run check script
  # 1. Login
  echo "Logging into RelayServer..."
  perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -o "-T -o ConnectTimeout=5 -o 'StrictHostKeyChecking accept-new'" -u "$rs_cur_username" -h "$RELAYSERVER_SERVER_IP" -c "echo hello >/dev/null" -f
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was a problem logging in to the RelayServer, returning..."
    return
  fi
  # 2. Upload check script
  echo "Uploading validation script..."
  outputRelayServerValidationScript
  outputRelayServerInstallSetupScript
  perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -a $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c ":~" -f
  is_err=$?
  rm -f $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was an problem uploading the validation script to the RelayServer, returning..."
    return
  fi
  perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c ":~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME" -f
  is_err=$?
  rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was an problem uploading the setup script to the RelayServer, returning..."
    return
  fi
  rm -f $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME
  # 3. Log back in and run check script
  if ! [ "$rs_cur_username" = "root" ]; then
    rs_new_password="$rs_cur_password"
  fi
  echo "Performing pre-installation checks, please wait..."
  perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -s "$rs_cur_password" -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $rs_cur_username -h $RELAYSERVER_SERVER_IP -c "bash ~/$RS_INSTALL_VALIDATION_SCRIPT_NAME -s" -f -i "$rs_new_password"
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "ERROR: There was a problem with the RelayServer, see above."
    return
  fi
  echo "Testing RelayServer login with pub/priv key..."
  perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "echo \"Successfully logged in to RelayServer with key! \"" -f
  is_err=$?
  if [ $is_err -ne 0 ]; then
    echo "ERROR: Could not login to RelayServer with pub/priv key."
    return
  fi
  echo "All is good, assigning values..."
  RELAYSERVER_HSHQ_BASE_DIR=/home/$RELAYSERVER_REMOTE_USERNAME/hshq
  RELAYSERVER_HSHQ_DATA_DIR=$RELAYSERVER_HSHQ_BASE_DIR/data
  RELAYSERVER_HSHQ_NONBACKUP_DIR=$RELAYSERVER_HSHQ_BASE_DIR/nonbackup
  RELAYSERVER_HSHQ_SCRIPTS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/scripts
  RELAYSERVER_HSHQ_SECRETS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/secrets
  RELAYSERVER_HSHQ_STACKS_DIR=$RELAYSERVER_HSHQ_DATA_DIR/stacks
  RELAYSERVER_HSHQ_SSL_DIR=$RELAYSERVER_HSHQ_DATA_DIR/ssl
  resetRelayServerData
  updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE "$PRIMARY_VPN_SETUP_TYPE"
  updateConfigVar RELAYSERVER_SSH_PORT $RELAYSERVER_SSH_PORT
  # SSH (do both protocols, even though only need TCP)
  sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-SSH', $RELAYSERVER_SSH_PORT, $RELAYSERVER_SSH_PORT, 0, 0, 'both','','','$curdt');"
  updateConfigVar RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT
  # Portainer (do both protocols, even though only need TCP)
  sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-Portainer', $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT, $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT, 0, 0, 'both','','','$curdt');"
  updateConfigVar RELAYSERVER_WG_VPN_NETNAME "$RELAYSERVER_WG_VPN_NETNAME"
  updateConfigVar RELAYSERVER_WG_INTERNET_NETNAME "$RELAYSERVER_WG_INTERNET_NETNAME"
  updateConfigVar RELAYSERVER_EXT_EMAIL_HOSTNAME $RELAYSERVER_EXT_EMAIL_HOSTNAME
  updateConfigVar RELAYSERVER_NAME "$RELAYSERVER_NAME"
  updateConfigVar RELAYSERVER_LE_CERT_DOMAINS $RELAYSERVER_LE_CERT_DOMAINS
  updateConfigVar RELAYSERVER_REMOTE_USERNAME $RELAYSERVER_REMOTE_USERNAME
  updateConfigVar RELAYSERVER_HSHQ_BASE_DIR $RELAYSERVER_HSHQ_BASE_DIR
  updateConfigVar RELAYSERVER_HSHQ_DATA_DIR $RELAYSERVER_HSHQ_DATA_DIR
  updateConfigVar RELAYSERVER_HSHQ_NONBACKUP_DIR $RELAYSERVER_HSHQ_NONBACKUP_DIR
  updateConfigVar RELAYSERVER_HSHQ_SCRIPTS_DIR $RELAYSERVER_HSHQ_SCRIPTS_DIR
  updateConfigVar RELAYSERVER_HSHQ_SECRETS_DIR $RELAYSERVER_HSHQ_SECRETS_DIR
  updateConfigVar RELAYSERVER_HSHQ_STACKS_DIR $RELAYSERVER_HSHQ_STACKS_DIR
  updateConfigVar RELAYSERVER_HSHQ_SSL_DIR $RELAYSERVER_HSHQ_SSL_DIR
  updateConfigVar RELAYSERVER_SERVER_IP $RELAYSERVER_SERVER_IP
  updateConfigVar RELAYSERVER_CURRENT_SSH_PORT $RELAYSERVER_CURRENT_SSH_PORT
  updatePlaintextRootConfigVar PRIMARY_VPN_SUBNET $PRIMARY_VPN_SUBNET
  leCertsArr=($(echo $RELAYSERVER_LE_CERT_DOMAINS | tr "," "\n"))
  sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from lecertdomains;"
  for leCert in "${leCertsArr[@]}"
  do
    sudo sqlite3 $HSHQ_DB "insert or ignore into lecertdomains(Domain,BaseDomain) values('$leCert','$HOMESERVER_DOMAIN');"
    curBaseDomain=$(getBaseDomain $leCert)
    curSubDomain=$(getSubDomain $leCert)
    if [ "$curBaseDomain" = "$HOMESERVER_DOMAIN" ]; then
      for curSVC in "${SVCS_ARR[@]}"
      do
        var_base=$(echo $curSVC | cut -d"=" -f1 | cut -d"_" -f 2-)
        subdom=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f6)
        subdom="${subdom//\"}"
        if [ "$subdom" = "$curSubDomain" ]; then
          var_name="SVCD_"$var_base
          var_value=$(getConfigVar $var_name)
          var_valueArr=($(echo $var_value | tr "," "\n"))
          var_valueArr[6]=le
          printf -v new_value '%s,' "${var_valueArr[@]}"
          new_value="$(echo "${new_value%,}")"
          updateConfigVar $var_name $new_value
        fi
      done
    fi
  done
  initRelayServerCredentials
  outputHostedVPNConfigs
  insertSQLHostedVPN
  # This upload must take place as soon as possible after the
  # validation check, as the RelayServer might be installing
  # docker and reboooting (required for debian 12 atm)
  # See "Performing pre-installation checks" comment above.
  echo "Generating RelayServer install scripts..."
  outputRelayServerInstallSetupScript
  outputRelayServerInstallFreshScript
  addDomainAdguardHS "*.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN" "$RELAYSERVER_SERVER_IP"
  perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c ":/home/$RELAYSERVER_REMOTE_USERNAME" -f
  perfRemoteAction -m scp -p $RELAYSERVER_CURRENT_SSH_PORT -a $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_FRESH_SCRIPT_NAME -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c ":/home/$RELAYSERVER_REMOTE_USERNAME" -f
  perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "touch ~/$RELAYSERVER_SCRIPTS_UPLOADED_FILE" -f
  rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME
  rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_FRESH_SCRIPT_NAME
  set +e
  sendDNSRecordsEmail $HOMESERVER_DOMAIN
  sleep 2
  prepSvcsHostedVPN
  set +e
  startStopStack mailu stop
  sleep 1
  echo "Generating mailu certs..."
  generateCert mail "$SMTP_HOSTNAME,$SUB_POSTFIX.$HOMESERVER_DOMAIN"
  echo "Setting mailu relay creds..."
  updateMailuStackRelayHost
  # Start Install
  set +e
  echo "Starting installation on RelayServer..."
  countRSPreRetries=0
  maxRSPreRetries=20
  while true;
  do
    perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10 -o 'StrictHostKeyChecking accept-new'" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "sleep 1;if [ -f ~/$RELAYSERVER_NOT_READY_FILE ]; then exit 1; fi" -r 1 -b 1
    if [ $? -eq 0 ]; then
      break
    fi
    if [ $countRSPreRetries -ge $maxRSPreRetries ]; then
      echo "Max retries reached. Something has likely gone wrong with the RelayServer pre-installation script, exiting..."
      exit 2
    fi
    ((countRSPreRetries++))
    echo "($countRSPreRetries of $maxRSPreRetries) RelayServer not ready for installation, sleeping 30 seconds then will retry..."
    sleep 30
  done
  perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-T -o ConnectTimeout=10" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "bash $RS_INSTALL_FRESH_SCRIPT_NAME -s" -i "$USER_RELAY_SUDO_PW" -f
  perfRemoteAction -m ssh -p $RELAYSERVER_CURRENT_SSH_PORT -o "-t -o ConnectTimeout=10" -u $RELAYSERVER_REMOTE_USERNAME -h $RELAYSERVER_SERVER_IP -c "export TERM=linux; screen -r hshqInstall" -r 1 -b 1
  echo "RelayServer is rebooting..."
  echo "Sleeping 10 seconds, then will attempt to connect..."
  sleep 10
  set +e
  connectPrimaryInternet
  set +e
  connectPrimaryVPN true
  set +e
  updateEndpointIPs
  set +e
  rs_wg_user_conf=$(sudo cat $HSHQ_WIREGUARD_DIR/users/${RELAYSERVER_WG_VPN_NETNAME}-user1.conf)
  wgconfig=""
  wgconfig="$wgconfig""============= User WireGuard Configuration ============\n"
  wgconfig="$wgconfig""""""$rs_wg_user_conf""""\n"
  wgconfig="$wgconfig""=======================================================\n"
  if [ "$IS_DESKTOP_ENV" = "true" ]; then
    wgconfigfile="$HOME/Desktop/User1_WireGuard.conf"
  else
    wgconfigfile="$HOME/User1_WireGuard.conf"
  fi
  rm -f $wgconfigfile
  echo -e "$wgconfig" > $wgconfigfile
}

function setupHostedVPN()
{
  echo "Checking for required utils, please wait..."
  if [[ "$(isProgramInstalled dig)" = "false" ]]; then
    echo "Installing dig, please wait..."
    performAptInstall dnsutils > /dev/null 2>&1
  fi
  if [[ "$(isProgramInstalled sshpass)" = "false" ]]; then
    echo "Installing sshpass, please wait..."
    performAptInstall sshpass > /dev/null 2>&1
  fi
  if [[ "$(isProgramInstalled sipcalc)" = "false" ]]; then
    echo "Installing sipcalc, please wait..."
    performAptInstall sipcalc > /dev/null 2>&1
  fi
  resetRelayServerData
  if [ -z "$RELAYSERVER_WG_VPN_NETNAME" ]; then
    RELAYSERVER_WG_VPN_NETNAME="vpn-"${HOMESERVER_ABBREV:0:6}
    curNum=1
    while [ "$(checkInterfaceNameExists $RELAYSERVER_WG_VPN_NETNAME)" = "true" ] && [ $curNum -lt 100 ]
    do
      RELAYSERVER_WG_VPN_NETNAME="vpn-${HOMESERVER_ABBREV:0:5}${curNum}"
      ((curNum++))
    done
    if [ "$(checkInterfaceNameExists $RELAYSERVER_WG_VPN_NETNAME)" = "true" ]; then
      showMessageBox "Too Many Connections" "You have 99 other connections that start with ${HOMESERVER_ABBREV:0:5}?!?"
      return 1
    fi
    updateConfigVar RELAYSERVER_WG_VPN_NETNAME "$RELAYSERVER_WG_VPN_NETNAME"
  fi
  if [ -z "$RELAYSERVER_WG_INTERNET_NETNAME" ]; then
    RELAYSERVER_WG_INTERNET_NETNAME="ext-${HOMESERVER_ABBREV:0:6}"
    curNum=1
    while [ "$(checkInterfaceNameExists $RELAYSERVER_WG_INTERNET_NETNAME)" = "true" ] && [ $curNum -lt 100 ]
    do
      RELAYSERVER_WG_INTERNET_NETNAME="ext-${HOMESERVER_ABBREV:0:5}${curNum}"
      ((curNum++))
    done
    if [ "$(checkInterfaceNameExists $RELAYSERVER_WG_INTERNET_NETNAME)" = "true" ]; then
      showMessageBox "Too Many Connections" "You have 99 other connections that start with ${HOMESERVER_ABBREV:0:5}?!?"
      return 1
    fi
    updateConfigVar RELAYSERVER_WG_INTERNET_NETNAME "$RELAYSERVER_WG_INTERNET_NETNAME"
  fi
  if [ -z "$IS_ACCEPT_DEFAULTS" ]; then
    set +e
    showYesNoMessageBox "Accept Defaults?" "Do you wish to use defaults where applicable?"
    mbres=$?
    if [ $mbres -eq 0 ]; then
      IS_ACCEPT_DEFAULTS=yes
    else
      IS_ACCEPT_DEFAULTS=no
    fi
    set -e
  fi
  set +e
  sudo sqlite3 $HSHQ_DB "drop table portforwarding;" > /dev/null 2>&1
  set -e
  setupPortForwardingDB
  RELAYSERVER_EXT_EMAIL_HOSTNAME=$SUB_POSTFIX.$EXT_DOMAIN_PREFIX.$HOMESERVER_DOMAIN
  updateConfigVar RELAYSERVER_EXT_EMAIL_HOSTNAME $RELAYSERVER_EXT_EMAIL_HOSTNAME
  RELAYSERVER_SSH_PORT=""
  while [ -z "$RELAYSERVER_SSH_PORT" ]
  do
	RELAYSERVER_SSH_PORT=$(promptUserInputMenu "$SSH_PORT" "Enter NEW RelayServer SSH Port" "Enter your desired SSH Port for your RelayServer. It is highly advised to change your default SSH port (22). Bots will constantly probe port 22:")
	if [ -z "$RELAYSERVER_SSH_PORT" ]; then
	  showMessageBox "SSH Port Empty" "The SSH port cannot be empty"
    elif [ "$(checkValidNumber $RELAYSERVER_SSH_PORT)" = "false" ]; then
      showMessageBox "Invalid Character(s)" "The port contains invalid character(s). It must be a number."
      RELAYSERVER_SSH_PORT=""
    elif [ $RELAYSERVER_SSH_PORT -lt 1024 ] || [ $RELAYSERVER_SSH_PORT -gt 65536 ]; then
      showMessageBox "Invalid Character(s)" "The port number is invalid, it must be between 1024-65536."
      RELAYSERVER_SSH_PORT=""
	fi
    if ! [ -z "$RELAYSERVER_SSH_PORT" ]; then
      chkSSHPort="$(checkPortForwardingIntersect tcp $RELAYSERVER_SSH_PORT $RELAYSERVER_SSH_PORT)"
      if ! [ -z "$chkSSHPort" ]; then
        showMessageBox "ERROR" "Error with port: $chkSSHPort"
        RELAYSERVER_SSH_PORT=""
      else
        updateConfigVar RELAYSERVER_SSH_PORT $RELAYSERVER_SSH_PORT
      fi
    fi
  done
  # SSH (do both protocols, even though only need TCP)
  sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-SSH', $RELAYSERVER_SSH_PORT, $RELAYSERVER_SSH_PORT, 0, 0, 'both','','','$curdt');"
  RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=""
  while [ -z "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT" ]
  do
    tmp_port=$((9000 + $RANDOM % 999))
    while [ "$tmp_port" = "$RELAYSERVER_SSH_PORT" ]
    do
      tmp_port=$((9000 + $RANDOM % 999))
    done
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT="$tmp_port"
    else
      RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=$(promptUserInputMenu "$tmp_port" "Enter RelayServer Portainer Port" "Enter a port to use for Portainer on the RelayServer. A random port between 9000-9999 has been generated for you.")
    fi
	if [ -z "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT" ]; then
	  showMessageBox "Port Empty" "The port cannot be empty"
    elif [ "$(checkValidNumber $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT)" = "false" ]; then
      showMessageBox "Invalid Character(s)" "The port contains invalid character(s). It must be a number."
      RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=""
    elif [ $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT -lt 1024 ] || [ $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT -gt 65536 ]; then
      showMessageBox "Invalid Character(s)" "The port number is invalid, it must be between 1024-65536."
      RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=""
	fi
    if ! [ -z "$RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT" ]; then
      chkSSHPort="$(checkPortForwardingIntersect tcp $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT)"
      if ! [ -z "$chkSSHPort" ]; then
        showMessageBox "ERROR" "Error with port: $chkSSHPort"
        RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT=""
      else
        updateConfigVar RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT
      fi
    fi
  done
  # Portainer (do both protocols, even though only need TCP)
  sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;insert into portforwarding(PFType, Name, ExtStart, ExtEnd, IntStart, IntEnd, Protocol, InternalHost, IPAddress, LastUpdated) values('System', 'RS-Portainer', $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT, $RELAYSERVER_PORTAINER_LOCAL_HTTPS_PORT, 0, 0, 'both','','','$curdt');"
  while [ -z "$RELAYSERVER_NAME" ]
  do
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      RELAYSERVER_NAME="$HOMESERVER_NAME RelayServer"
    else
      RELAYSERVER_NAME=$(promptUserInputMenu "$HOMESERVER_NAME RelayServer" "Enter RelayServer Name" "Enter the RelayServer Name:")
    fi
	if [ -z "$RELAYSERVER_NAME" ]; then
	  showMessageBox "RelayServer Name Empty" "The RelayServer Name cannot be empty"
    elif [ $(checkValidStringUpperLowerNumbers "$RELAYSERVER_NAME" "[:space:],.-") = "false" ]; then
      showMessageBox "Invalid Character(s)" "The RelayServer name contains invalid character(s). It must consist of upper/lowercase letters, numbers, spaces, commas, periods, or hyphens."
      RELAYSERVER_NAME=""
	else
	  updateConfigVar RELAYSERVER_NAME "$RELAYSERVER_NAME"
	fi
  done
  while [ "$RELAYSERVER_LE_CERT_DOMAINS" = "unset" ]
  do
    lecert_def=$(getLetsEncryptCertsDefault)
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      RELAYSERVER_LE_CERT_DOMAINS="$lecert_def"
    else
      RELAYSERVER_LE_CERT_DOMAINS=$(promptUserInputMenu "$lecert_def" "Enter LE Cert Subdomains" "Enter the subdomains for which the certificates will be managed by LetsEncrypt (comma-separated):")
	  if ! [ -z "$RELAYSERVER_LE_CERT_DOMAINS" ] && [ $(checkValidString "$RELAYSERVER_LE_CERT_DOMAINS" ",.-") = "false" ]; then
        showMessageBox "Invalid Character(s)" "The domain list contains invalid character(s). It must consist of a-z (lowercase), 0-9, -, and/or ."
        RELAYSERVER_LE_CERT_DOMAINS=unset
	  fi
    fi
  done
  updateConfigVar RELAYSERVER_LE_CERT_DOMAINS $RELAYSERVER_LE_CERT_DOMAINS
  leCertsArr=($(echo $RELAYSERVER_LE_CERT_DOMAINS | tr "," "\n"))
  isReload=false
  sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from lecertdomains;"
  for leCert in "${leCertsArr[@]}"
  do
    sudo sqlite3 $HSHQ_DB "insert or ignore into lecertdomains(Domain,BaseDomain) values('$leCert','$HOMESERVER_DOMAIN');"
    curBaseDomain=$(getBaseDomain $leCert)
    curSubDomain=$(getSubDomain $leCert)
    if [ "$curBaseDomain" = "$HOMESERVER_DOMAIN" ]; then
      for curSVC in "${SVCS_ARR[@]}"
      do
        var_base=$(echo $curSVC | cut -d"=" -f1 | cut -d"_" -f 2-)
        subdom=$(echo $curSVC | cut -d"=" -f2 | cut -d"," -f6)
        subdom="${subdom//\"}"
        if [ "$subdom" = "$curSubDomain" ]; then
          var_name="SVCD_"$var_base
          var_value=$(getConfigVar $var_name)
          var_valueArr=($(echo $var_value | tr "," "\n"))
          var_valueArr[6]=le
          printf -v new_value '%s,' "${var_valueArr[@]}"
          new_value="$(echo "${new_value%,}")"
          updateConfigVar $var_name $new_value
          isReload=true
        fi
      done
    fi
  done
  if [ "$isReload" = "true" ]; then
    loadSvcVars
  fi
  echo "Initializing RelayServer service credentials..."
  num_tries=1
  max_tries=100
  PRIMARY_VPN_SUBNET=""
  while [ -z "$PRIMARY_VPN_SUBNET" ] && [ $num_tries -lt $max_tries ]
  do
    ((num_tries++))
    if [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
      PRIMARY_VPN_SUBNET=10.$(( $RANDOM % 256 )).$(( $RANDOM % 256 )).0/24
    else
      PRIMARY_VPN_SUBNET=$(promptUserInputMenu "10.$(( $RANDOM % 256 )).$(( $RANDOM % 256 )).0/24" "Enter Subnet" "Enter the VPN Hosting Subnet (in CIDR): ")
    fi
	if [ -z "$PRIMARY_VPN_SUBNET" ] || [ "$(checkValidIPAddress $PRIMARY_VPN_SUBNET)" = "false" ]; then
	  showMessageBox "Invalid Subnet" "The VPN Subnet is invalid."
      PRIMARY_VPN_SUBNET=""
      continue
	fi
    is_intersect="$(isNetworkIntersectOurNetworks $PRIMARY_VPN_SUBNET false true)"
    if ! [ -z "$is_intersect" ]; then
      if ! [ "$IS_ACCEPT_DEFAULTS" = "yes" ]; then
        showMessageBox "Network Collision" "Network Collision: $is_intersect"
      fi
      PRIMARY_VPN_SUBNET=""
      continue
    fi
    set +e
    firstOct=$(echo "$PRIMARY_VPN_SUBNET" | cut -d"." -f1)
    if ! [ "$firstOct" = "10" ]; then
      showYesNoMessageBox "Confirm" "This subnet is not in the 10.0.0.0/8 range. It is highly recommended that you stay within this range. Are you sure you know what you are doing?"
      if [ $? -ne 0 ]; then
        PRIMARY_VPN_SUBNET=""
        continue
      fi
    fi
    vpnCIDR=$(echo "$PRIMARY_VPN_SUBNET" | cut -d"/" -f2-)
    if ! [ "$vpnCIDR" = "24" ]; then
      showYesNoMessageBox "The VPN subnet must be of size /24."
      PRIMARY_VPN_SUBNET=""
      continue
    fi
    set -e
	updatePlaintextRootConfigVar PRIMARY_VPN_SUBNET $PRIMARY_VPN_SUBNET
  done
  if [ -z "$PRIMARY_VPN_SUBNET" ]; then
    # We tried...
    showMessageBox "ERROR" "Could not allocate VPN network subnet, returning..."
    return 1
  fi
  initRelayServerCredentials
  outputHostedVPNConfigs
  insertSQLHostedVPN
  echo "Generating RelayServer install scripts..."
  outputRelayServerInstallSetupScript
  outputRelayServerInstallFreshScript
  set +e
  uploadVPNInstallScripts
  if [ $? -ne 0 ]; then
    sudo sqlite3 $HSHQ_DB "PRAGMA foreign_keys=ON;delete from lecertdomains;"
    return 1
  fi
  if [ "$IS_INSTALLED" = "true" ]; then
    echo "Preparing mailu, please wait..."
    startStopStack mailu stop
    sleep 5
    echo "Generating mailu certs..."
    generateCert mail "$SMTP_HOSTNAME,$SUB_POSTFIX.$HOMESERVER_DOMAIN"
    startStopStack mailu start
    prepSvcsHostedVPN
  fi
  updatePlaintextRootConfigVar PRIMARY_VPN_SETUP_TYPE $PRIMARY_VPN_SETUP_TYPE
}

function outputRelayServerValidationScript()
{
  # This function assumes rs_cur_username and rs_new_username have been defined before calling it.
  # It also assumes that the caller will send the current/new user's password immediately via
  # standard input.
  if [ -z "$rs_cur_username" ] || [ -z "$rs_new_username" ]; then
    # Fatal
    echo "RelayServer usernames undefined for script, exiting..."
    exit 5
  fi
  tee $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME >/dev/null <<EOFRS
#!/bin/bash

curUsername=$rs_cur_username
newUsername=$rs_new_username
pubkey="$(cat $HSHQ_CONFIG_DIR/${RELAYSERVER_SSH_PRIVATE_KEY_FILENAME}.pub)"

function main()
{
  while getopts ':ips' opt; do
    case "\$opt" in
      s)
        startValidation ;;
      p)
        getSuper ;;
      i)
        performDebian12PreInstall ;;
      ?|h)
        echo "Usage: \$(basename \$0)"
        exit 1 ;;
    esac
  done
  shift "\$((\$OPTIND -1))"
}

function startValidation()
{
  set +e
  which sudo > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    if [ "\$curUsername" = "root" ]; then
      DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
      dpkg --configure -a > /dev/null 2>&1
      DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' sudo > /dev/null 2>&1
    else
      echo "-----------------------------------------------------------------"
      echo "  ERROR: Sudo is not installed. Please either log in with"
      echo "  the root account or install it manually."
      echo "-----------------------------------------------------------------"
      exit 2
    fi
  fi
  if [ "\$curUsername" = "root" ]; then
    read -r -s -p "" USER_RELAY_SUDO_PW
    id "\$newUsername" > /dev/null 2>&1
    if [ \$? -eq 0 ]; then
      echo "-----------------------------------------------------------------"
      echo "  ERROR: User (\$newUsername) already exists."
      echo "-----------------------------------------------------------------"
      removeMyself
      exit 1
    fi
    checkForHSHQ
    checkForRunningDockerContainers
    checkForBoundPorts
    sudo useradd -m -G sudo -s /bin/bash "\$newUsername" > /dev/null 2>&1
    echo "\$newUsername:\$USER_RELAY_SUDO_PW" | sudo chpasswd > /dev/null 2>&1
  else
    read -r -s -p "" USER_RELAY_SUDO_PW
    echo "\$USER_RELAY_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1
    if [ \$? -ne 0 ]; then
      echo "-----------------------------------------------------------------"
      echo "  ERROR: The sudo password for \$curUsername is incorrect."
      echo "-----------------------------------------------------------------"
      removeMyself
      exit 2
    fi
    sudo bash -c "\$(declare -f checkForValidOSVersion); checkForValidOSVersion \$curUsername" 2> /dev/null
    if [ \$? -ne 0 ]; then
      removeMyself
      exit 2
    fi
    sudo bash -c "\$(declare -f checkForHSHQ); checkForHSHQ" 2> /dev/null
    if [ \$? -ne 0 ]; then
      removeMyself
      exit 2
    fi
    sudo bash -c "\$(declare -f checkForRunningDockerContainers); checkForRunningDockerContainers" 2> /dev/null
    if [ \$? -ne 0 ]; then
      removeMyself
      exit 2
    fi
    sudo bash -c "\$(declare -f checkBoundPort; declare -f checkForBoundPorts); checkForBoundPorts" 2> /dev/null
    if [ \$? -ne 0 ]; then
      removeMyself
      exit 2
    fi
  fi
  getent group docker >/dev/null || sudo groupadd docker > /dev/null 2>&1
  sudo usermod -aG docker \$newUsername > /dev/null 2>&1
  mkdir -p /home/\$newUsername/.ssh
  chmod 775 /home/\$newUsername/.ssh
  pubk=\$(echo "\$pubkey" | cut -d " " -f2)
  grep "\$pubk" /home/\$newUsername/.ssh/authorized_keys > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    echo "\$pubkey" >> /home/\$newUsername/.ssh/authorized_keys
  fi
  chown -R \$newUsername:\$newUsername /home/\$newUsername/.ssh
  rm -f /home/\$newUsername/$RS_INSTALL_SETUP_SCRIPT_NAME
  rm -f /home/\$newUsername/$RS_INSTALL_FRESH_SCRIPT_NAME
  checkPerformPreInstall
}

function checkForValidOSVersion()
{
  source /home/\$1/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME lib
  checkSupportedHostOS
  rtVal=\$?
  if [ \$rtVal -ne 0 ]; then
    exit \$rtVal
  fi
}

function checkForHSHQ()
{
  findhshq="\$(find /home -maxdepth 3 -type d -name hshq 2>/dev/null | head -n 1)"
  if ! [ -z "\$findhshq" ]; then
    echo "------------------------------------------------------------------------------------"
    echo "  ERROR: An hshq directory already exists: \$findhshq. You must log into the"
    echo "  RelayServer and run the nuke.sh script (bash nuke.sh) to clear everything out."
    echo "------------------------------------------------------------------------------------"
    exit 5
  fi
}

function checkForRunningDockerContainers()
{
  is_containers=\$(docker ps -q 2>/dev/null | head -n 1)
  if ! [ -z "\$is_containers" ]; then
    echo "------------------------------------------------------------------------------------"
    echo "  ERROR: There are currently running docker containers,"
    echo "  the server must be clear of all activity."
    echo "------------------------------------------------------------------------------------"
    exit 6
  fi
}

function checkForBoundPorts()
{
  checkBoundPort $MAILU_PORT_1
  checkBoundPort $MAILU_PORT_5
  checkBoundPort $CADDY_HTTP_PORT
  checkBoundPort $CADDY_HTTPS_PORT
  checkBoundPort $SYNCTHING_SYNC_PORT
  checkBoundPort $SYNCTHING_DISC_PORT
  checkBoundPort $SYNCTHING_LOCAL_WEB_PORT
  checkBoundPort $RELAYSERVER_WG_PORTAL_PORT
  checkBoundPort $RELAYSERVER_WG_PORT
}

function checkBoundPort()
{
  chkPort=\$1
  set +e
  sudo netstat -tulpn 2>/dev/null | grep :\$chkPort > /dev/null 2>&1
  if [ \$? -eq 0 ]; then
    echo "------------------------------------------------------------------------------------"
    echo "  A needed port is bound by another service: \$chkPort"
    echo "  The server must be clear of all services on the following port lists:"
    echo "  External - $MAILU_PORT_1, $CADDY_HTTP_PORT, $CADDY_HTTPS_PORT, $RELAYSERVER_WG_PORT"
    echo "  Internal - $MAILU_PORT_5, $SYNCTHING_SYNC_PORT, $SYNCTHING_DISC_PORT, $SYNCTHING_LOCAL_WEB_PORT, $RELAYSERVER_WG_PORTAL_PORT"
    echo "------------------------------------------------------------------------------------"
    exit 7
  fi
}

function removeMyself()
{
  rm -f ~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME
  rm -f \$0
}

function checkPerformPreInstall()
{
  sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
  sudo dpkg --configure -a > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1
  which screen > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    DEBIAN_FRONTEND=noninteractive sudo apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' screen > /dev/null 2>&1
  fi
  which pwgen > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    DEBIAN_FRONTEND=noninteractive sudo apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' pwgen > /dev/null 2>&1
  fi
  which netstat > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    DEBIAN_FRONTEND=noninteractive sudo apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' net-tools > /dev/null 2>&1
  fi
  docker ps > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    source ~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME lib
    if [ "\$DISTRO_ID" = "debian" ] && [[ "\$DISTRO_VERSION" =~ ^12. ]]; then
      echo "Installing docker on RelayServer and rebooting it..."
      # Must install docker and reboot
      touch /home/\$newUsername/$RELAYSERVER_NOT_READY_FILE
      scName=hshqPreInstall
      initScreen "\$scName"
      screen -S "\$scName" -X stuff "bash \$0 -i\n"
      return
    fi
  fi
  removeMyself
}

function performDebian12PreInstall()
{
  sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt dist-upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1
  source ~/$RS_INSTALL_VALIDATION_LIB_SCRIPT_NAME lib
  installDocker
  echo "Checking for uploaded scripts..."
  # Need to ensure other files have been uploading before rebooting.
  maxCount=120
  curCount=0
  while [ \$curCount -lt \$maxCount ]
  do
    if [ -f /home/\$newUsername/$RELAYSERVER_SCRIPTS_UPLOADED_FILE ]; then
      break
    fi
    sleep 5
    ((curCount++))
  done
  echo "Scripts found, rebooting..."
  rm -f /home/\$newUsername/$RELAYSERVER_SCRIPTS_UPLOADED_FILE
  removeMyself
  rm -f /home/\$newUsername/$RELAYSERVER_NOT_READY_FILE
  reboot
}

function getSuper()
{
  set +e
  stty -echo
  mkdir -p $TMP_PW_CHECKDIR
  read -r -s -p "" sudoPWKey
  USER_RELAY_SUDO_PW="\$(openssl enc -d -aes256 -pbkdf2 -in $ENC_PW_FILE -pass pass:\$sudoPWKey)"
  rm -f $ENC_PW_FILE
  echo "\$USER_RELAY_SUDO_PW" | sudo -S -v -p "" > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    exit 6
  fi
  rm -fr $TMP_PW_CHECKDIR
  clear
  stty echo
}

function initScreen()
{
  set +e
  screenName="\$1"
  dirtest1=/tmp/scPre1
  isScreenSuccess=false
  curScreenAttempt=0
  maxScreenAttempt=5
  screen -XS \$screenName quit > /dev/null 2>&1
  sudoPWKey=\$(pwgen -c -n 32 1)
  echo -n "\$USER_RELAY_SUDO_PW" | openssl enc -e -aes256 -pbkdf2 -pass pass:\$sudoPWKey > $ENC_PW_FILE
  while [ \$curScreenAttempt -lt \$maxScreenAttempt ]
  do
    ((curScreenAttempt++))
    rm -fr \$dirtest1
    rm -fr $TMP_PW_CHECKDIR
    screen -dmS \$screenName
    screen -S \$screenName -X stuff "stty -echo;mkdir \$dirtest1\n"
    curCheckCount=0
    while ! [ -d \$dirtest1 ] && [ \$curCheckCount -lt 5 ]
    do
      ((curCheckCount++))
      sleep 1
    done
    if ! [ -d \$dirtest1 ]; then
      screen -XS \$screenName quit > /dev/null 2>&1
      continue
    fi
    screen -S \$screenName -X stuff "bash \$0 -p\n"
    curCheckCount=0
    while ! [ -d $TMP_PW_CHECKDIR ] && [ \$curCheckCount -lt 15 ]
    do
      ((curCheckCount++))
      sleep 1
    done
    screen -S \$screenName -X stuff "\$sudoPWKey\n"
    curCheckCount=0
    while [ -d $TMP_PW_CHECKDIR ] && [ \$curCheckCount -lt 15 ]
    do
      ((curCheckCount++))
      sleep 1
    done
    rm -fr \$dirtest1
    if [ -d $TMP_PW_CHECKDIR ]; then
      rm -fr $TMP_PW_CHECKDIR
      echo "The sudo password was not correctly transmitted to the screen session, exiting..."
      exit 5
    fi
    rm -fr $TMP_PW_CHECKDIR
    isScreenSuccess=true
    break
  done
  USER_RELAY_SUDO_PW=""
  if ! [ "\$isScreenSuccess" = "true" ]; then
    return 1
  fi
}

main "\$@"
EOFRS
  chmod 600 $HOME/$RS_INSTALL_VALIDATION_SCRIPT_NAME
}

function outputRelayServerInstallSetupScript()
{
  rm -f $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME
  cat <<EOFRS > $HSHQ_RELAYSERVER_DIR/scripts/$RS_INSTALL_SETUP_SCRIPT_NAME
#!/bin/bash

set +e

function init()
{
  USERNAME=\$(id -u -n)
  RELAYSERVER_HSHQ_BASE_DIR=\$HOME/hshq
  RELAYSERVER_HSHQ_DATA_DIR=\$RELAYSERVER_HSHQ_BASE_DIR/data
  RELAYSERVER_HSHQ_NONBACKUP_DIR=\$RELAYSERVER_HSHQ_BASE_DIR/nonbackup
  RELAYSERVER_HSHQ_SCRIPTS_DIR=\$RELAYSERVER_HSHQ_DATA_DIR/scripts
  NET_EXTERNAL_SUBNET=$RELAYSERVER_NET_EXTERNAL_SUBNET
  NET_EXTERNAL_SUBNET_PREFIX=$RELAYSERVER_NET_EXTERNAL_SUBNET_PREFIX
  NET_WEBPROXY_SUBNET=$RELAYSERVER_NET_WEBPROXY_SUBNET
  NET_WEBPROXY_SUBNET_PREFIX=$RELAYSERVER_NET_WEBPROXY_SUBNET_PREFIX
  MAIL_RELAY_SUBNET=$RELAYSERVER_MAIL_RELAY_SUBNET
  MAIL_RELAY_SUBNET_PREFIX=$RELAYSERVER_MAIL_RELAY_SUBNET_PREFIX
  CLIENT_DNS_SUBNET=$RELAYSERVER_CLIENT_DNS_SUBNET
  CLIENT_DNS_SUBNET_PREFIX=$RELAYSERVER_CLIENT_DNS_SUBNET_PREFIX
  loadVersionVars
  IS_RUST_UTILS=false
  if [ "\$DISTRO_ID" = "ubuntu" ] && [[ "\$DISTRO_VERSION" =~ ^26\.04. ]]; then
    IS_RUST_UTILS=true
  fi
}

function setupMain()
{
  init
  echo "Running setup script..."
  rm -f ~/$RELAYSERVER_SCRIPTS_UPLOADED_FILE
  outputNukeScript
  new_hostname="RelayServer-$(echo $HOMESERVER_DOMAIN | sed 's/\./-/g')"
  if [ -z "\$(cat /etc/hosts | grep \$new_hostname)" ]; then
    echo "127.0.1.1 \$new_hostname" | sudo tee -a /etc/hosts
  fi
  sudo hostnamectl set-hostname \$new_hostname
  rm -f \$HOME/dead.letter
  # Ensure we can login during the installation process,
  # since the firewall will block the connection if we
  # haven't logged in before the firewall goes up.
  cur_ssh_port=\$(sudo grep ^Port /etc/ssh/sshd_config)
  if [ \$? -ne 0 ]; then
    cur_ssh_port=22
  else
    cur_ssh_port=\$(sudo grep ^Port /etc/ssh/sshd_config | xargs | cut -d" " -f2)
  fi
  if [[ "\$(isProgramInstalled iptables)" = "false" ]]; then
    sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
    performAptInstall iptables > /dev/null 2>&1
  fi
  sudo iptables -C INPUT -p tcp -m tcp --dport \$cur_ssh_port -j ACCEPT > /dev/null 2>&1 || sudo iptables -A INPUT -p tcp -m tcp --dport \$cur_ssh_port -j ACCEPT
  # Change SSH port on host
  sudo sed -i "s|^#*Port .*\$|Port $RELAYSERVER_SSH_PORT|g" /etc/ssh/sshd_config
  sudo sed -i "s|^Port .*\$|Port $RELAYSERVER_SSH_PORT|g" /etc/ssh/sshd_config
  # Some other SSH settings
  sudo sed -i "s|^ClientAliveInterval .*\$|ClientAliveInterval 15|g" /etc/ssh/sshd_config
  sudo sed -i "s|^ClientAliveCountMax .*\$|ClientAliveCountMax 3|g" /etc/ssh/sshd_config
  sudo sed -i "s|^PermitEmptyPasswords .*\$|PermitEmptyPasswords no|g" /etc/ssh/sshd_config
  sudo sed -i "s|^#*ClientAliveInterval .*\$|ClientAliveInterval 15|g" /etc/ssh/sshd_config
  sudo sed -i "s|^#*ClientAliveCountMax .*\$|ClientAliveCountMax 3|g" /etc/ssh/sshd_config
  sudo sed -i "s|^#*PermitEmptyPasswords .*\$|PermitEmptyPasswords no|g" /etc/ssh/sshd_config
  # Update sudoers file
  sudo sed -i '/\/userasroot/d' /etc/sudoers >/dev/null
  echo "\$USERNAME ALL=(ALL) NOPASSWD: \$RELAYSERVER_HSHQ_SCRIPTS_DIR/userasroot/*.sh" | sudo tee -a /etc/sudoers >/dev/null
  sudo sed -i '/timestamp_timeout/d' /etc/sudoers >/dev/null
  echo "Defaults timestamp_timeout=$SUDO_NORMAL_TIMEOUT" | sudo tee -a /etc/sudoers >/dev/null
  sudo sed -i '/logfile=/d' /etc/sudoers >/dev/null
  if [ "\$IS_RUST_UTILS" = "true" ]; then
    echo "authpriv.*    /var/log/sudo.log" | sudo tee /etc/rsyslog.d/50-sudo.conf >/dev/null
    sudo systemctl restart rsyslog
  else
    echo "Defaults logfile=/var/log/sudo.log" | sudo tee -a /etc/sudoers >/dev/null
  fi
  sudo sed -i '/includedir/d' /etc/sudoers >/dev/null
  echo "@includedir /etc/sudoers.d" | sudo tee -a /etc/sudoers >/dev/null
  # Set timezone
  sudo timedatectl set-timezone "$TZ"
  # Create swap file
  createSwapfile
  installDependencies
  createDockerNetworks
}

function createSwapfile()
{
  sudo grep swapfile /etc/fstab > /dev/null 2>&1
  if [ \$? -ne 0 ] && ! [ -f /swapfile ]; then
    sudo fallocate -l $RELAYSERVER_SWAP_SIZE /swapfile
    sudo chmod 600 /swapfile
    sudo mkswap /swapfile
    sudo swapon /swapfile
    echo "# Swap file" | sudo tee -a /etc/fstab > /dev/null 2>&1
    echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab > /dev/null 2>&1
  fi
}

function updateSysctl()
{
  set +e
  sudo tee /etc/sysctl.d/88-hshq.conf >/dev/null <<EOFSC
kernel.panic = 10
fs.file-max = 10000000
fs.nr_open = 10000000
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches = 524288
net.core.rmem_max = 4194304
net.core.wmem_max = 4194304
net.core.somaxconn = 65536
net.mptcp.enabled = 0
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_loose = 0
net.netfilter.nf_conntrack_tcp_timeout_established = 1800
net.netfilter.nf_conntrack_tcp_timeout_close = 10
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 10
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 20
net.netfilter.nf_conntrack_tcp_timeout_last_ack = 20
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 20
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 20
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 10
net.ipv4.route.flush = 1
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_rfc1337 = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5
net.ipv4.tcp_timestamps = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.ip_forward = 1
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOFSC

  sudo sysctl --system > /dev/null 2>&1
}

function updateMOTD()
{
  cat <<EOFMD > \$HOME/88-hshq
#!/bin/bash

echo
echo "  #===============================================================#"
echo "  # ▀█  █  ▄▄  ▄▄ ▄▄ ▄▄▄ █▀▀▀█ ▄▄▄ ▄▄▄  ▄   ▄ ▄▄▄ ▄▄▄  █  █ █▀▀█  #"
echo "  #  █▀▀█ █  █ █ █ █ ▄▄  ▀▀▀▄▄ ▄▄  ▄▄▄▀ ▀▄ ▄▀ ▄▄  ▄▄▄▀ █▀▀█ █  █  #"
echo "  #  █  █ ▀▄▄▀ █   █ ▄▄▄ █▄▄▄█ ▄▄▄ ▄▄▄▄  ▀█▀  ▄▄▄ ▄▄▄▄ █  █ █▄▄█▄ #"
echo "  #===============================================================#"
echo

echo "      Linux OS:  \\\$(lsb_release -d | cut -d":" -f2 | cut -d"(" -f1 | xargs)"
printf "  Memory Usage:  %.1f%% of \\\$(free -h | awk  '/Mem:/{print \\\$2}')\n" \\\$((10000 - \\\$((10**4 * \\\$(grep "MemAvailable" /proc/meminfo | xargs | cut -d" " -f2) / \\\$(grep "MemTotal" /proc/meminfo | xargs | cut -d" " -f2)))))e-2
totSwap=\\\$(grep "SwapTotal" /proc/meminfo | xargs | cut -d" " -f2)
if [ \\\$totSwap -gt 0 ]; then
  printf "    Swap Usage:  %.1f%% of \\\$(free -h | awk  '/Swap:/{print \\\$2}')\n" \\\$((10000 - \\\$((10**4 * \\\$(grep "SwapFree" /proc/meminfo | xargs | cut -d" " -f2) / \\\$totSwap))))e-2
fi

echo
echo "  Disks: "
echo "  -----------------------------------------------------------------"
echo "  Filesystem            Size    Used    Avail   Use%    Mounted on"
echo "  -----------------------------------------------------------------"
df -hP | grep "^/dev" | awk '{printf "  %-21s %-7s %-7s %-7s %-7s %-8s\n", \\\$1, \\\$2, \\\$3, \\\$4, \\\$5, \\\$6}'
echo

# Let's only show zombie count if greater than 10
zombie_count=\\\$(ps aux | grep "defunct" | wc -l)
if [ \\\$zombie_count -gt 10 ]; then
  echo "  => There are \\\$zombie_count zombie processes."
  echo
fi

EOFMD
  chmod 755 \$HOME/88-hshq
  sudo chown root:root \$HOME/88-hshq
  sudo chmod -x /etc/update-motd.d/*
  sudo mv \$HOME/88-hshq /etc/update-motd.d/
  if [ -f /etc/motd ]; then
    sudo mv /etc/motd /etc/motd.old
  fi
}

function performSuggestedSecUpdates()
{
  disa_curE=\${-//[^e]/}
  set +e
  grep "enabled=1" /etc/default/apport > /dev/null 2>&1
  if [ \$? -eq 0 ]; then
    sudo sed -i "s/enabled=1/enabled=0/g" /etc/default/apport
    sudo systemctl stop apport
    sudo systemctl --now disable apport
    sudo systemctl daemon-reload
  fi
  echo "Authorized uses only." | sudo tee /etc/issue > /dev/null 2>&1
  echo "Authorized uses only." | sudo tee /etc/issue.net > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y telnet > /dev/null 2>&1
  # Assume the caller will restart sshd - don't want to risk breaking the installation process
  sudo sed -i "s/^#MaxAuthTries.*/MaxAuthTries 4/" /etc/ssh/sshd_config
  sudo sed -i "s/^#Banner none.*/Banner \/etc\/issue.net/" /etc/ssh/sshd_config
  sudo sed -i "s/^#MaxStartups.*/MaxStartups 10:30:60/" /etc/ssh/sshd_config
  sudo sed -i "s/^#LoginGraceTime.*/LoginGraceTime 60/" /etc/ssh/sshd_config
  sudo sed -i "s/^#AllowTcpForwarding yes.*/AllowTcpForwarding no/" /etc/ssh/sshd_config
  sudo chown root:root /etc/crontab
  sudo chmod og-rwx /etc/crontab
  sudo chown root:root /etc/cron.hourly/
  sudo chmod og-rwx /etc/cron.hourly/
  sudo chown root:root /etc/cron.daily/
  sudo chmod og-rwx /etc/cron.daily/
  sudo chown root:root /etc/cron.weekly/
  sudo chmod og-rwx /etc/cron.weekly/
  sudo chown root:root /etc/cron.monthly/
  sudo chmod og-rwx /etc/cron.monthly/
  sudo chown root:root /etc/cron.d/
  sudo chmod og-rwx /etc/cron.d/
  if ! [ -z \$disa_curE ]; then
    set -e
  fi
}

function performAptInstall()
{
  sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' \$1
}

function installDependencies()
{
  UTILS_LIST="$RELAYSERVER_UTILS_LIST"
  APT_REMOVE_LIST="$APT_REMOVE_LIST"
  echo "Updating host..."
  sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1
  sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y needrestart > /dev/null 2>&1
  echo "Host update complete!"
  performAptInstall ssmtp > /dev/null 2>&1
  sudo tee /etc/ssmtp/ssmtp.conf >/dev/null <<EOFSM
root=$EMAIL_ADMIN_EMAIL_ADDRESS
mailhub=${SUB_POSTFIX}.${HOMESERVER_DOMAIN}:$MAILU_PORT_5
hostname=\$(cat /etc/hostname)
TLS_CA_FILE=/etc/ssl/certs/ca-certificates.crt
UseSTARTTLS=yes
FromLineOverride=no
AuthUser=$EMAIL_SMTP_EMAIL_ADDRESS
AuthPass=$EMAIL_SMTP_PASSWORD
EOFSM

  sudo tee /etc/ssmtp/revaliases >/dev/null <<EOFSM
root:$EMAIL_SMTP_EMAIL_ADDRESS
\$USERNAME:$EMAIL_SMTP_EMAIL_ADDRESS
EOFSM

  performAptInstall mailutils > /dev/null 2>&1
  getent group mailsenders >/dev/null || sudo groupadd mailsenders
  sudo usermod -aG mailsenders \$USERNAME
  sudo chown root:mailsenders /usr/bin/mail.mailutils
  sudo chmod 750 /usr/bin/mail.mailutils

  set +e
  # Install Rsyslog
  sudo systemctl status rsyslog > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    echo "Installing rsyslog, please wait..."
    performAptInstall rsyslog > /dev/null 2>&1
    sudo systemctl enable rsyslog
    sudo systemctl start rsyslog
  fi

  for util in \$UTILS_LIST; do
    if [[ "\$(isProgramInstalled \$util)" = "false" ]]; then
      lib_name=\$(echo \$util | cut -d"|" -f2)
      echo "Installing \$lib_name, please wait..."
      performAptInstall \$lib_name > /dev/null 2>&1
    fi
  done

  updateSysctl
  updateMOTD
  performSuggestedSecUpdates
  installDocker

  if sudo test -f /etc/logrotate.d/rsyslog; then
    # Set max size of syslog to 2G
    grep maxsize /etc/logrotate.d/rsyslog > /dev/null 2>&1
    if [ \$? -ne 0 ]; then
      sudo sed -i "/weekly\$/a\        maxsize 2G" /etc/logrotate.d/rsyslog
    fi
  fi
  # Move logrotate into hourly
  if sudo test -f /etc/cron.daily/logrotate; then
    sudo mv /etc/cron.daily/logrotate /etc/cron.hourly
  fi
  # Set conditions for docker container logs
  sudo tee /etc/logrotate.d/docker >/dev/null <<EOFLR
/var/log/docker/*.log {
    weekly
    maxsize 100M
    rotate 4
    missingok
    notifempty
}
EOFLR
  sudo systemctl restart logrotate

  for rem_util in \$APT_REMOVE_LIST; do
    sudo DEBIAN_FRONTEND=noninteractive apt remove --purge -y \$rem_util
  done
  sudo DEBIAN_FRONTEND=noninteractive apt autoremove -y
}

function loadVersionVars()
{
  DISTRO_ID=\$(cat /etc/*-release | grep "^ID=" | cut -d"=" -f2 | xargs | tr '[:upper:]' '[:lower:]')
  DISTRO_NAME=\$(cat /etc/*-release | grep "^NAME=" | cut -d"=" -f2 | xargs)
  DISTRO_VERSION=\$(cat /etc/*-release | grep "^VERSION=" | cut -d"=" -f2 | xargs)
  DISTRO_CODENAME=\$(cat /etc/*-release 2> /dev/null | grep "^VERSION_CODENAME=" | cut -d"=" -f2 | xargs)
}

function checkSupportedHostOS()
{
  if [ "\$DISTRO_ID" = "ubuntu" ] && [[ "\$DISTRO_VERSION" =~ ^22\.04. ]]; then
    return
  fi
  if [ "\$DISTRO_ID" = "ubuntu" ] && [[ "\$DISTRO_VERSION" =~ ^24\.04. ]]; then
    return
  fi
  if [ "\$DISTRO_ID" = "ubuntu" ] && [[ "\$DISTRO_VERSION" =~ ^26\.04. ]]; then
    return
  fi
  if [ "\$DISTRO_ID" = "debian" ] && [[ "\$DISTRO_VERSION" =~ ^12. ]]; then
    return
  fi
  if [ "\$DISTRO_ID" = "linuxmint" ] && [[ "\$DISTRO_VERSION" =~ ^22. ]]; then
    return
  fi
  echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
  echo "@                   Unsupported Host Operating System                  @"
  echo "@                                                                      @"
  echo "@ This installation only supports the following Linux distribution(s): @"
  echo "@                                                                      @"
  echo "@  - Ubuntu 22.04 (Jammy Jellyfish)                                    @"
  echo "@  - Ubuntu 24.04 (Noble Numbat)                                       @"
  echo "@  - Debian 12 (Bookworm)                                              @"
  echo "@  - Mint 22 (Wilma)                                                   @"
  echo "@                                                                      @"
  echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
  return 2
}

function outputDockerSettings()
{
  sudo mkdir -p /etc/docker
  sudo tee /etc/docker/daemon.json >/dev/null <<EOFRL
{
  "default-address-pools":
  [
    {"base":"$DOCKER_NETWORK_RESERVED_RANGE","size":$DOCKER_NETWORK_SIZE}
  ],
  "log-driver": "syslog",
  "log-opts": {
    "syslog-address": "unixgram:///dev/log",
    "tag": "docker/{{.Name}}"
  },
  "ipv6": false,
  "registry-mirrors": ["https://mirror.gcr.io"],
  "min-api-version": "1.24"
}
EOFRL
  set +e
  grep -q DockerDaemonLogFileName /etc/rsyslog.d/docker-logs.conf > /dev/null 2>&1
  if [ \$? -ne 0 ]; then
    sudo tee /etc/rsyslog.d/docker-logs.conf >/dev/null <<EOFRL
\\\$FileCreateMode 0644
\\\$template DockerDaemonLogFileName,"/var/log/docker/docker.log"
\\\$template DockerContainerLogFileName,"/var/log/docker/%SYSLOGTAG:R,ERE,1,FIELD:docker/(.*)\[--end:secpath-replace%.log"
if \\\$programname == 'dockerd' then {
  ?DockerDaemonLogFileName
  stop
}
if \\\$programname == 'containerd' then {
  ?DockerDaemonLogFileName
  stop
}
if \\\$programname == 'docker' then {
  if \\\$syslogtag contains 'docker/' then {
  ?DockerContainerLogFileName
  stop
  }
}
\\\$FileCreateMode 0600
EOFRL
  fi
}

function installDocker()
{
  set +e
  outputDockerSettings
  util="docker|docker"
  if [[ "\$(isProgramInstalled \$util)" = "true" ]]; then
    sudo systemctl restart rsyslog
    sudo systemctl restart docker
    return 0
  fi
  echo "Installing docker, please wait..."
  performAptInstall apt-transport-https > /dev/null 2>&1
  performAptInstall ca-certificates > /dev/null 2>&1
  performAptInstall curl > /dev/null 2>&1
  performAptInstall gnupg > /dev/null 2>&1
  performAptInstall gnupg2 > /dev/null 2>&1
  performAptInstall software-properties-common > /dev/null 2>&1
  performAptInstall lsb-release > /dev/null 2>&1
  case "\$DISTRO_ID" in
  "ubuntu")
    if [[ "\$DISTRO_VERSION" =~ ^22\.04. ]]; then
      installDockerUbuntu2204
    elif [[ "\$DISTRO_VERSION" =~ ^24\.04. ]]; then
      installDockerUbuntu2404
    elif [[ "\$DISTRO_VERSION" =~ ^26\.04. ]]; then
      installDockerUbuntu2604
    else
      echo "Linux version not found when installing Docker, exiting..."
      exit 5
    fi
    ;;
  "debian")
    if [[ "\$DISTRO_VERSION" =~ ^12. ]]; then
      installDockerDebian12
    else
      echo "Linux version not found when installing Docker, exiting..."
      exit 5
    fi
    ;;
  "linuxmint")
    if [[ "\$DISTRO_VERSION" =~ ^22. ]]; then
      installDockerUbuntu2404
    elif [[ "\$DISTRO_VERSION" =~ ^6. ]]; then
      installDockerDebian12
    else
      echo "Linux version not found when installing Docker, exiting..."
      exit 5
    fi
    ;;
  *)
    ;;
  esac
  # See https://www.portainer.io/blog/portainer-and-docker-26
  sudo apt-mark hold docker-ce
  sudo apt-mark hold docker-ce-cli
  sudo systemctl restart rsyslog
  sudo systemctl restart docker
  sudo usermod -aG docker \$USERNAME
}

function installDockerUbuntu2204()
{
  # Install Docker (https://docs.docker.com/engine/install/ubuntu/)
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg --yes
  echo "deb [arch=\$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
  echo "Installing docker..."
  sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2204 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2204 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1
}

function installDockerUbuntu2404()
{
  # Install Docker (https://docs.docker.com/engine/install/ubuntu/)
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \$(. /etc/os-release && echo "\$DISTRO_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
  echo "Installing docker..."
  sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2404 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2404 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1
}

function installDockerUbuntu2604()
{
  # Install Docker (https://docs.docker.com/engine/install/ubuntu/)
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \$(. /etc/os-release && echo "\$DISTRO_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
  echo "Installing docker..."
  sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_UBUNTU_2604 docker-ce-cli=$DOCKER_VERSION_UBUNTU_2604 containerd.io docker-buildx-plugin docker-compose-plugin > /dev/null 2>&1
}

function installDockerDebian12()
{
  # Install Docker (https://docs.docker.com/engine/install/debian/)
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "\$DISTRO_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  sudo DEBIAN_FRONTEND=noninteractive apt update > /dev/null 2>&1
  echo "Installing docker..."
  sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' docker-ce=$DOCKER_VERSION_DEBIAN_12 docker-ce-cli=$DOCKER_VERSION_DEBIAN_12 containerd.io docker-buildx-plugin docker-compose docker-compose-plugin > /dev/null 2>&1
}

function outputNukeScript()
{
  cat <<EOFNS > \$HOME/$NUKE_SCRIPT_NAME
HSHQ_BASE_DIR=\$RELAYSERVER_HSHQ_BASE_DIR
HSHQ_SCRIPTS_DIR=\$RELAYSERVER_HSHQ_SCRIPTS_DIR

set +e
function removeWGInterfaceQuick()
{
  config_filename=\\\$1
  sudo systemctl stop wg-quick@\\\${config_filename}.service
  sudo systemctl disable wg-quick@\\\${config_filename}.service
  sudo systemctl daemon-reload
  sudo rm -f /etc/wireguard/\\\${config_filename}.conf
}

function main()
{
  echo
  echo
  echo
  echo
  echo "============================================================"
  echo "This script will entirely remove HSHQ and all artifacts."
  echo "It will permanently delete ALL data"
  echo "It will not delete any previously downloaded docker images."
  echo "In order to remove them as well, type 'docker image prune -f'"
  echo "Please type 'nuclear' in order to continue."
  echo "============================================================"
  echo
  echo
  read -r -p "Type 'nuclear' (no quotes): " isnuke
  if ! [ "\\\$isnuke" = "nuclear" ]; then
    echo "String does not match, exiting..."
    exit 1
  fi
  sudo -k
  sudo crontab -r
  sudo doc