#!/bin/bash # # LibrePortal Initialization Script # # Usage: ./init.sh [OPTIONS] init [password] [git_user] [git_token] [git_url] [unattended] [install_mode] # # OPTIONS: # --random-password Generate a random password automatically # --local Use local folder installation automatically # --unattended Run in unattended mode (skip confirmations) # --skip-os-update Skip operating system update # --skip-prereqs Skip installing prerequisite apps # # Examples: # ./init.sh --random-password --local init # ./init.sh --random-password --local --unattended init # ./init.sh --random-password --local --skip-os-update --skip-prereqs init # ./init.sh init mypassword myuser mytoken https://github.com/user/repo.git # RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' isSuccessful() { echo -e "${GREEN}✓ Success${NC} $1"; } isError() { echo -e "${RED}✗ Error${NC} $1"; } isNotice() { echo -e "${YELLOW}! Notice${NC} $1"; } isQuestion() { echo -e -n "${BLUE}❯ Question${NC} $1 "; } displayLibrePortalLogo() { local hbar; hbar=$(printf '═%.0s' $(seq 1 50)) printf '\n╔%s╗\n' "$hbar" printf '║%6s%s%8s║\n' '' '╦ ┬┌┐ ┬─┐┌─┐ ╭─╮ ╔═╗┌─┐┬─┐┌┬┐┌─┐┬' '' printf '║%6s%s%8s║\n' '' '║ │├┴┐├┬┘├┤ │◉│ ╠═╝│ │├┬┘ │ ├─┤│' '' printf '║%6s%s%6s║\n' '' '╩═╝┴└─┘┴└─└─┘ ╨─╨ ╩ └─┘┴└─ ┴ ┴ ┴┴─┘' '' printf '╚%s╝\n\n' "$hbar" } init_action="$1" for arg in "$@"; do case "$arg" in --*) ;; *) init_action="$arg"; break ;; esac done if [[ "${BASH_SOURCE[0]}" == "$0" ]] \ && [[ "$init_action" == "init" ]] \ && [[ "$LIBREPORTAL_SKIP_LOGO" != "1" ]]; then displayLibrePortalLogo fi unset init_action checkSuccess() { if [ $? -eq 0 ]; then isSuccessful "$1" else isError "$1" exit 1 fi } isHeader() { local title="$1" local width=52 local inner=$((width - 2)) local title_len=${#title} local total_pad=$((inner - title_len)) if (( total_pad < 0 )); then total_pad=0; fi local left_pad=$((total_pad / 2)) local right_pad=$((total_pad - left_pad)) local hbar hbar=$(printf '═%.0s' $(seq 1 "$inner")) printf '\n╔%s╗\n' "$hbar" printf '║%*s%s%*s║\n' "$left_pad" '' "$title" "$right_pad" '' printf '╚%s╝\n\n' "$hbar" } # Original parameters for backward compatibility param1="$1" # init to start script param2="$2" # password param3="$3" # git user param4="$4" # git token param5="$5" # git url param6="$6" # unattended param7="$7" # install mode (git/local) # Parse command line arguments init_random_password=false init_local_install=false init_unattended_mode=false init_skip_os_update=false init_skip_prereqs=false install_param="init" sudo_user_name=libreportal sshd_config="/etc/ssh/sshd_config" sudo_bashrc="/home/$sudo_user_name/.bashrc" hosts_file="/etc/hosts" hostname_file="/etc/hostname" fqdn_file="/root/libreportal-fqdn.txt" command_script="/usr/local/bin/libreportal" # Directories docker_dir="/docker" containers_dir="$docker_dir/containers/" ssl_dir="$docker_dir/ssl/" ssh_dir="$docker_dir/ssh/" wireguard_dir="$docker_dir/wireguard/" logs_dir="$docker_dir/logs/" configs_dir="$docker_dir/configs/" backup_dir="$docker_dir/backups" restore_dir="$docker_dir/restore" migrate_dir="$docker_dir/migrate" # Install Scripts script_dir="$docker_dir/install" install_configs_dir="$script_dir/configs/" install_containers_dir="$script_dir/containers/" install_scripts_dir="$script_dir/scripts/" # Parse flags init_shift_count=0 for ((i=1; i<=$#; i++)); do case "${!i}" in --random-password) init_random_password=true ((init_shift_count++)) ;; --local) init_local_install=true ((init_shift_count++)) ;; --unattended) init_unattended_mode=true ((init_shift_count++)) ;; --skip-os-update) init_skip_os_update=true ((init_shift_count++)) ;; --skip-prereqs) init_skip_prereqs=true ((init_shift_count++)) ;; esac done # Shift parsed flags to get positional parameters if [ $init_shift_count -gt 0 ]; then for ((i=1; i<=init_shift_count; i++)); do shift done # Reset positional parameters after shifting flags param1="$1" # init to start script param2="$2" # password param3="$3" # git user param4="$4" # git token param5="$5" # git url param6="$6" # unattended param7="$7" # install mode (git/local) fi # Apply flag logic if [ "$init_random_password" = true ]; then param2=$(openssl rand -base64 12 | tr -d '\n') isSuccessful "Generated password: $param2" fi if [ "$init_local_install" = true ]; then param7="local" fi # Auto-detect installation mode based on provided parameters if [[ -z "$param7" ]]; then # A reinstall that doesn't re-pass the git args must not silently # downgrade an existing git install to local (that disables the updater # and blanks the saved creds). Honor a git URL already saved from a # prior install — only fall back to local when there's no git history. saved_git_url=$(grep -E '^CFG_GIT_URL=' /docker/configs/general/general_install 2>/dev/null \ | sed -E 's/^[^=]+=([^[:space:]#]*).*/\1/') if [[ -n "$param3" || -n "$param4" || -n "$param5" ]]; then # Git parameters provided, set to git mode param7="git" isSuccessful "Auto-detected Git installation mode (git args provided)" elif [[ -n "$saved_git_url" && "$saved_git_url" != "changeme" && "$saved_git_url" != "empty" ]]; then # No git args this run, but a prior git install is on disk — keep it param7="git" isSuccessful "Auto-detected Git installation mode (existing git config detected)" else # No git parameters and no prior git config, set to local mode param7="local" isSuccessful "Auto-detected Local installation mode" fi fi initUpdateConfigOption() { local config_option="$1" local config_value="$2" local config_file="" for category_dir in "$configs_dir"/*; do if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then for config_file in "$category_dir"/*; do if [ -f "$config_file" ] && [[ ! "$config_file" =~ \.category$ ]]; then if grep -q "^$config_option=" "$config_file"; then local escaped_value=$(printf '%s\n' "$config_value" | sed -e 's/[\/&]/\\&/g') local original_line=$(grep "^$config_option=" "$config_file") local comment_part=$(echo "$original_line" | sed -n "s|^$config_option=[^#]*\(#.*\)|\1|p") if [[ -n "$comment_part" ]]; then sudo sed -i "s|^$config_option=.*|$config_option=$escaped_value $comment_part|" "$config_file" else sudo sed -i "s|^$config_option=.*|$config_option=$escaped_value|" "$config_file" fi source "$config_file" return 0 fi fi done fi done return 1 } if [[ -n "$param7" && -f "${configs_dir}general/general_install" ]]; then initUpdateConfigOption "CFG_INSTALL_MODE" "$param7" fi if [ "$init_unattended_mode" = true ]; then param6="true" fi if [ "$init_skip_os_update" = true ]; then isNotice "Skipping operating system update" fi if [ "$init_skip_prereqs" = true ]; then isNotice "Skipping prerequisite apps installation" fi # Get script directory for local installation (only if needed) if [ "$init_local_install" = true ]; then init_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" isNotice "Using script directory for local installation: $init_script_dir" fi detectLocalLibrePortal() { local check_dir # If --local flag is used, check script directory, otherwise check current directory if [ "$init_local_install" = true ]; then check_dir="$init_script_dir" echo "Checking script directory for LibrePortal structure..." else check_dir="$(pwd)" echo "Checking current directory for LibrePortal structure..." fi local required_dirs=("configs" "scripts" "containers") local required_files=("init.sh" "start.sh") local missing_count=0 # Check for required directories for dir in "${required_dirs[@]}"; do if [[ ! -d "$check_dir/$dir" ]]; then echo " Missing directory: $dir" ((missing_count++)) fi done # Check for required files for file in "${required_files[@]}"; do if [[ ! -f "$check_dir/$file" ]]; then echo " Missing file: $file" ((missing_count++)) fi done if [[ $missing_count -eq 0 ]]; then echo "Valid LibrePortal structure detected in $check_dir" echo "" return 0 else isNotice "Warning: $missing_count required items missing. Not a valid LibrePortal source." return 1 fi } copyFilesFromLocal() { local source_dir # If --local flag is used, use script directory, otherwise use current directory if [ "$init_local_install" = true ]; then source_dir="$init_script_dir" else source_dir="$(pwd)" fi isHeader "Copying from Local Directory" # Remove existing install directory sudo rm -rf "$script_dir" # Create install directory sudo mkdir -p "$script_dir" isNotice "Copying files from $source_dir to $script_dir..." # Copy all files while preserving structure if sudo cp -r "$source_dir"/* "$script_dir/" 2>/dev/null; then sudo cp -f "$script_dir/init.sh" /root/ setupConfigsFromRepo sudo chown -R $sudo_user_name:$sudo_user_name "$script_dir" isSuccessful "Files copied from local directory to '$script_dir'." else isError "Failed to copy files from local directory." exit 1 fi } resetConfigVars() { param2="" param3="" param4="" param5="" param7="" GIT_USER="" GIT_TOKEN="" GIT_URL="" INSTALL_MODE="" } initCheckConfigs() { # Check if any config files exist (new structure) or old config files local config_found=false # Check for new structure if [ -d "$configs_dir" ]; then for category_dir in "$configs_dir"/*; do if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then # Load new structure config files for config_file in "$category_dir"/*; do local should_load=true local filename=$(basename "$config_file") # Skip .category files and excluded files (hardcoded) if [[ "$config_file" =~ \.category$ ]]; then should_load=false fi if [ "$should_load" = true ]; then source "$config_file" config_found=true fi done fi done fi if [ "$config_found" = false ]; then isNotice "Configuration files do not exist, skipping init check" return fi get_cfg() { local var_name="$1" local var_value="${!var_name}" echo "$var_value" } [[ -z "$param2" ]] && param2=$(get_cfg CFG_LIBREPORTAL_USER_PASS) [[ -z "$param3" ]] && param3=$(get_cfg CFG_GIT_USER) [[ -z "$param4" ]] && param4=$(get_cfg CFG_GIT_KEY) [[ -z "$param5" ]] && param5=$(get_cfg CFG_GIT_URL) [[ -z "$param7" ]] && param7=$(get_cfg CFG_INSTALL_MODE) [[ "$param2" == "changeme" ]] && param2="" [[ "$param3" == "changeme" ]] && param3="" [[ "$param4" == "changeme" ]] && param4="" [[ "$param5" == "changeme" ]] && param5="" [[ "$param7" == "changeme" ]] && param7="" } validateUnattended() { if [[ -z "$param2" ]]; then isError "Password is required in unattended mode" exit 1 fi # If install mode is not specified, default to git if [[ -z "$param7" ]]; then param7="git" fi # For git installation, validate git parameters if [[ "$param7" == "git" ]]; then if [[ -z "$param5" ]]; then isError "Git repository URL is required in unattended mode" exit 1 fi if [[ -z "$param3" ]]; then param3="empty" param4="empty" fi if [[ "$param3" != "empty" && -z "$param4" ]]; then param4="empty" fi fi } initInputQuestions() { ### PASSWORD if [[ -z "$param2" ]]; then while true; do echo "" echo "LibrePortal User Password" read -p "Use custom (c) or randomized (r) password? (c/r): " choice case "$choice" in c) read -p "Enter custom password: " param2 [[ -z "$param2" ]] && echo "Password cannot be empty." || break ;; r) param2=$(openssl rand -base64 12) isSuccessful "Generated password: $param2" break ;; *) echo "Invalid option. Enter c or r." ;; esac done fi ### INSTALLATION METHOD if [[ -z "$param7" ]]; then echo "" echo "Installation Method:" # Check if local installation is available if detectLocalLibrePortal; then echo "1) Install from Git repository" echo "2) Install from local folder (current directory)" echo "" while true; do read -p "Choose option [1-2]: " install_choice case "$install_choice" in 1) param7="git" echo "Using Git repository installation." break ;; 2) param7="local" echo "Using local folder installation." break ;; *) echo "Invalid option. Choose 1 or 2." ;; esac done else echo "Local installation not available - missing required files/directories." echo "Defaulting to Git repository installation." param7="git" fi fi ### GIT USER (only for git installation) if [[ "$param7" == "git" && -z "$param3" ]]; then echo "" echo "Git Authentication Method:" echo "1) Login required (username + token)" echo "2) Authenticationless (public repos / SSH keys)" echo "" while true; do read -p "Choose option [1-2]: " git_auth_choice case "$git_auth_choice" in 1) read -p "Enter Git username: " param3 if [[ -z "$param3" ]]; then echo "Username cannot be empty for login authentication." else break fi ;; 2) param3="empty" param4="empty" echo "Using authenticationless Git access." break ;; *) echo "Invalid option. Choose 1 or 2." ;; esac done fi ### GIT TOKEN (only for git installation) if [[ "$param7" == "git" && "$param3" != "empty" && -z "$param4" ]]; then read -p "Enter Git token: " param4 [[ -z "$param4" ]] && param4="empty" fi ### GIT URL (only for git installation) if [[ "$param7" == "git" && -z "$param5" ]]; then while true; do echo "" read -p "Enter Git repository URL: " param5 [[ -z "$param5" ]] && echo "Git repository URL is required." || { # Validate and fix Git URL param5=$(validateAndFixGitUrl "$param5") echo "Using Git URL: $param5" break } done elif [[ "$param7" == "git" ]]; then # Also validate URL if provided as parameter param5=$(validateAndFixGitUrl "$param5") echo "Using Git URL: $param5" fi } cleanGitUrl() { local url="$1" url="${url#"${url%%[![:space:]]*}"}" url="${url%"${url##*[![:space:]]}"}" url="${url#https://}" url="${url#http://}" while [[ "$url" == */ ]]; do url="${url%/}"; done url="${url%.git}" echo "$url" } validateAndFixGitUrl() { local url="$1" url="${url#"${url%%[![:space:]]*}"}" url="${url%"${url##*[![:space:]]}"}" echo "$url" } initReloadConfigs() { for category_dir in "$configs_dir"/*; do if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then for config_file in "$category_dir"/*; do if [ -f "$config_file" ] && [[ ! "$config_file" =~ \.category$ ]]; then source "$config_file" fi done fi done # Check old config files (backward compatibility) for config_file in "$configs_dir"/config_*; do if [ -f "$config_file" ]; then source "$config_file" fi done } writeConfig() { # Defer config writes until the repo is cloned and /docker/configs is # populated by setupConfigsFromRepo. On a fresh install the file # below doesn't exist yet — initUpdateConfigs at the end of init.sh # is what actually persists everything. if [[ ! -f "${configs_dir}general/general_install" ]]; then return 0 fi [[ -n "$param2" ]] && initUpdateConfigOption "CFG_LIBREPORTAL_USER_PASS" "$param2" [[ -n "$param3" ]] && initUpdateConfigOption "CFG_GIT_USER" "$param3" [[ -n "$param4" ]] && initUpdateConfigOption "CFG_GIT_KEY" "$param4" [[ -n "$param5" ]] && initUpdateConfigOption "CFG_GIT_URL" "$param5" [[ -n "$param7" ]] && initUpdateConfigOption "CFG_INSTALL_MODE" "$param7" } initDisplayConfig() { GIT_USER="$param3" GIT_TOKEN="$param4" GIT_URL="$param5" INSTALL_MODE="$param7" isHeader "Configuration Summary" isNotice "LibrePortal User Password: [HIDDEN]" if [[ "$INSTALL_MODE" == "local" ]]; then isNotice "Git Username: [NOT APPLICABLE]" isNotice "Git Token: [NOT APPLICABLE]" else [[ "$GIT_USER" == "empty" ]] \ && isNotice "Git Username: [DISABLED]" \ || isNotice "Git Username: $GIT_USER" [[ "$GIT_TOKEN" == "empty" ]] \ && isNotice "Git Token: [DISABLED]" \ || isNotice "Git Token: [HIDDEN]" fi [[ "$INSTALL_MODE" == "local" ]] \ && isNotice "Installation Mode: Local Folder" \ || isNotice "Installation Mode: Git Repository" if [[ "$INSTALL_MODE" != "local" ]]; then isNotice "Git URL: $GIT_URL" fi echo "" if [[ "$param6" == "true" || "$param6" == "1" ]]; then isNotice "Unattended mode enabled — auto-accepting configuration" writeConfig fi if [[ "$INSTALL_MODE" != "local" ]]; then read -p "Are these details correct? (y/n): " confirm else confirm=y fi if [[ "$confirm" != "y" ]]; then echo "Restarting configuration.." resetConfigVars initCheckConfigs initInputQuestions initDisplayConfig else writeConfig isSuccessful "Configuration saved." fi } initOS() { isHeader "Updating Operating System" apt-get install sudo -y sudo apt-get update sudo apt-get dist-upgrade -y echo "" isSuccessful "OS Updated" } initPrerequires() { isHeader "Installing Prerequired Apps" # apache2-utils → htpasswd, used by hashPassword for fast local bcrypt. sudo apt-get install git zip curl sshpass dos2unix dnsutils apt-transport-https ca-certificates software-properties-common uidmap adduser apache2-utils restic -y TARGET_PATH="/usr/local/bin" CONFIG_FILE="$HOME/.bashrc" if ! echo "$PATH" | grep -q "$TARGET_PATH"; then echo "Adding $TARGET_PATH to PATH..." echo "export PATH=\$PATH:$TARGET_PATH" >> "$CONFIG_FILE" source "$CONFIG_FILE" echo "PATH updated successfully!" else echo "$TARGET_PATH is already in PATH." fi isSuccessful "Prerequisite apps installed." } initDocker() { isHeader "Installing Docker" if command -v docker &> /dev/null; then isSuccessful "Docker is already installed." else curl -fsSL https://get.docker.com | sh systemctl start docker systemctl enable docker isSuccessful "Docker has been installed successfully." fi } initUsers() { isHeader "Creating User Accounts" if id "$sudo_user_name" &>/dev/null; then isSuccessful "User $sudo_user_name already exists." else sudo useradd -s /bin/bash -d "/home/$sudo_user_name" -m -G sudo "$sudo_user_name" 2>/dev/null isNotice "Setting password for $sudo_user_name user." echo "$sudo_user_name:$param2" | sudo chpasswd sudo usermod -aG docker "$sudo_user_name" sudo systemctl restart docker isSuccessful "User $sudo_user_name created successfully." fi # Manager-user sudo lives in a validated /etc/sudoers.d drop-in, not appended # to /etc/sudoers — a malformed line in the main file locks out sudo entirely. # The grant is broad for now; this single drop-in is what gets tightened to a # scoped command allowlist once the runtime no longer needs broad root. local sudoers_dropin="/etc/sudoers.d/${sudo_user_name}" local sudoers_tmp sudoers_tmp=$(mktemp) printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$sudo_user_name" > "$sudoers_tmp" if sudo visudo -cf "$sudoers_tmp" >/dev/null 2>&1; then sudo install -m 0440 -o root -g root "$sudoers_tmp" "$sudoers_dropin" isSuccessful "Configured passwordless sudo for $sudo_user_name (/etc/sudoers.d/${sudo_user_name})." else isError "Refusing to install an invalid sudoers drop-in for $sudo_user_name." fi rm -f "$sudoers_tmp" initRootHelpers } # Install the root-owned privilege helpers. Under Model A the runtime runs AS the # manager, so the genuine-root operations it can't drop (the /docker ownership # model, /etc/resolv.conf edits, host SSH access) need root — but granting the # manager blanket `sudo chown/chmod/tee/sed/cp` would be root-equivalent. Each # helper does a FIXED, self-validated set of operations and lives root:root 0755 # where the manager can't edit it, so the scoped sudoers can allow each wholesale. # The manager name is baked in here (the manager can't change it); the sed is a # no-op on helpers without the placeholder. initRootHelpers() { local helper helper_src helper_dst helper_tmp for helper in libreportal-ownership libreportal-dns libreportal-ssh-access; do helper_src="$script_dir/scripts/system/$helper" helper_dst="/usr/local/sbin/$helper" if [[ ! -f "$helper_src" ]]; then isError "Root helper source missing ($helper_src) — skipping." continue fi helper_tmp=$(mktemp) sed "s/__MANAGER__/${sudo_user_name}/g" "$helper_src" > "$helper_tmp" if bash -n "$helper_tmp" 2>/dev/null; then sudo install -m 0755 -o root -g root "$helper_tmp" "$helper_dst" isSuccessful "Installed root-owned helper ($helper_dst)." else isError "Refusing to install a malformed root helper ($helper)." fi rm -f "$helper_tmp" done } initFolders() { isHeader "LibrePortal Folder Creation" folders=("$docker_dir" "$containers_dir" "$ssl_dir" "$ssh_dir" "$wireguard_dir" "$logs_dir" "$configs_dir" "$backup_dir" "$restore_dir" "$migrate_dir" "$script_dir") for folder in "${folders[@]}"; do if [ ! -d "$folder" ]; then sudo mkdir "$folder" sudo chown $sudo_user_name:$sudo_user_name "$folder" sudo chmod 750 "$folder" isSuccessful "Folder '$folder' created." fi done isSuccessful "All folders have been created." } setupConfigsFromRepo() { isNotice "Setting up configuration files from repository..." local src="$script_dir/configs" local dst="/docker/configs" if [[ ! -d "$src" ]]; then isError "Source configs directory missing: $src" isError "The clone in $script_dir didn't include a configs/ tree — aborting." exit 1 fi sudo mkdir -p "$dst" # No-clobber: only seed config files that don't already exist. On a fresh # install this copies the whole template; on a re-run/deploy it preserves the # user's live values (a plain cp -a here silently reset e.g. the Docker # install type back to the template default). New *keys* in existing files # are added separately by the add-only reconcile pass. if ! sudo cp -an "$src"/. "$dst"/; then isError "Failed to copy configs from $src to $dst — aborting." exit 1 fi sudo chown -R "$sudo_user_name":"$sudo_user_name" "$dst" if [[ ! -f "$dst/general/general_install" ]]; then isError "Configs were copied but $dst/general/general_install is missing." isError "Repository layout may have changed — fix and re-run." exit 1 fi isSuccessful "Configuration files copied from repository." isNotice "Applying initial configuration values..." [[ -n "$param2" ]] && initUpdateConfigOption "CFG_LIBREPORTAL_USER_PASS" "$param2" [[ -n "$param3" ]] && initUpdateConfigOption "CFG_GIT_USER" "$param3" [[ -n "$param4" ]] && initUpdateConfigOption "CFG_GIT_KEY" "$param4" [[ -n "$param5" ]] && initUpdateConfigOption "CFG_GIT_URL" "$param5" [[ -n "$param7" ]] && initUpdateConfigOption "CFG_INSTALL_MODE" "$param7" isSuccessful "Configuration setup complete." } initGIT() { isHeader "Git Clone / Update" # Handle local installation if [[ "$param7" == "local" ]]; then isNotice "Using local folder installation." copyFilesFromLocal return fi GIT_USER="$param3" GIT_TOKEN="$param4" GIT_URL="$param5" if [[ -z "$GIT_URL" ]]; then isError "Git URL is empty. Please provide a valid Git repository URL." exit 1 fi local clean_url clean_url=$(cleanGitUrl "$GIT_URL") if [[ -z "$clean_url" ]]; then isError "Could not normalize Git URL: $GIT_URL" exit 1 fi local auth="" if [[ -n "$GIT_USER" && "$GIT_USER" != "empty" ]]; then auth="${GIT_USER}:${GIT_TOKEN}@" fi isNotice "Cloning $clean_url into $script_dir ..." sudo rm -rf "$script_dir" local scheme cloned=false for scheme in https http; do if sudo -u "$sudo_user_name" git clone -q "${scheme}://${auth}${clean_url}.git" "$script_dir" 2>/dev/null; then cloned=true isSuccessful "Cloned via ${scheme^^}." break fi done if ! $cloned; then isError "Failed to clone $clean_url over HTTPS or HTTP." isError "Check the URL, credentials, and that the server is reachable." exit 1 fi sudo cp -f "$script_dir/init.sh" /root/ setupConfigsFromRepo sudo chown -R "$sudo_user_name":"$sudo_user_name" "$script_dir" } initLibrePortalCommand() { isHeader "Custom Command Setup" if ! grep -q "LibrePortal Command Start" $sudo_bashrc; then isNotice "Command maker not found. Removing old LibrePortal command." sed -i '/^libreportal() {$/,/^}$/d' $sudo_bashrc else isNotice "Command maker found. Removing old LibrePortal command." sed -i '/# LibrePortal Command Start/,/# LibrePortal Command End/d' $sudo_bashrc fi isNotice "Custom command 'libreportal' is not installed. Installing..." sudo rm -rf $command_script sudo tee -a "$command_script" >/dev/null <<'EOF' #!/usr/bin/env bash # LibrePortal Command Start # LibrePortal Command Version 1.4 CHECK_USER="libreportal" CURRENT_USER=$(whoami) # Check if the script is run by the specified user if [ "$CURRENT_USER" != "$CHECK_USER" ]; then echo "Script is NOT able to run from this user." echo "This script should ONLY be run as: $CHECK_USER" exit 1 fi command1="${1:-empty}" command2="${2:-empty}" command3="${3:-empty}" command4="${4:-empty}" command5="${5:-empty}" command6="${6:-empty}" command7="${7:-empty}" command8="${8:-empty}" command9="${9:-empty}" path="$PWD" reset_git_config() { echo "" echo "Resetting Git configuration for re-entry..." # Use dynamic config update commandUpdateConfigOption "CFG_GIT_USER" "changeme" commandUpdateConfigOption "CFG_GIT_KEY" "changeme" commandUpdateConfigOption "CFG_GIT_URL" "changeme" } # Helper function to load config files in the libreportal command commandReloadConfigs() { # Load new structure config files only for category_dir in /docker/configs/*; do if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then for config_file in "$category_dir"/*; do local should_load=true local filename=$(basename "$config_file") # Skip .category files and excluded files (hardcoded for now) if [[ "$config_file" =~ \.category$ ]]; then should_load=false fi if [ "$should_load" = true ]; then source "$config_file" fi done fi done } # Helper function to update config files in the libreportal command commandUpdateConfigOption() { local config_option="$1" local config_value="$2" for category_dir in /docker/configs/*; do if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then for config_file in "$category_dir"/*; do if [ -f "$config_file" ] && [[ ! "$config_file" =~ \.category$ ]]; then if grep -q "^$config_option=" "$config_file"; then # Escape special characters in the config value to prevent sed issues local escaped_value=$(printf '%s\n' "$config_value" | sed -e 's/[\/&]/\\&/g') # Extract the comment part first (everything after the first #) local original_line=$(grep "^$config_option=" "$config_file") local comment_part=$(echo "$original_line" | sed -n "s|^$config_option=[^#]*\(#.*\)|\1|p") # Replace the value, preserving comment if it existed if [[ -n "$comment_part" ]]; then sed -i "s|^$config_option=.*|$config_option=$escaped_value $comment_part|" "$config_file" else sed -i "s|^$config_option=.*|$config_option=$escaped_value|" "$config_file" fi source "$config_file" return 0 fi fi done fi done } update_config_values() { # Reload all config files to get current values commandReloadConfigs if [[ "$CFG_INSTALL_MODE" == "local" ]]; then [[ "$CFG_GIT_USER" != "empty" ]] && commandUpdateConfigOption "CFG_GIT_USER" "empty" [[ "$CFG_GIT_KEY" != "empty" ]] && commandUpdateConfigOption "CFG_GIT_KEY" "empty" [[ "$CFG_GIT_URL" != "empty" ]] && commandUpdateConfigOption "CFG_GIT_URL" "empty" commandReloadConfigs return 0 fi CFG_GIT_USER="${CFG_GIT_USER:-changeme}" if [[ -z "$CFG_GIT_USER" ]] || [[ "$CFG_GIT_USER" == "changeme" ]]; then while true; do echo "Please enter your Git username (Press Enter to set to 'empty'):" read -r NEW_GIT_USER if [[ -n "$NEW_GIT_USER" || "$NEW_GIT_USER" == "" ]]; then if [[ -z "$NEW_GIT_USER" ]]; then NEW_GIT_USER="empty" fi commandUpdateConfigOption "CFG_GIT_USER" "$NEW_GIT_USER" commandReloadConfigs echo "Updating Git Username to '$NEW_GIT_USER'" break fi done fi CFG_GIT_KEY="${CFG_GIT_KEY:-changeme}" # If Git user is disabled, force token to empty and skip prompt if [[ "$CFG_GIT_USER" == "empty" ]]; then commandUpdateConfigOption "CFG_GIT_KEY" "empty" commandReloadConfigs echo "Git authentication disabled; skipping Git token." else # Only prompt if token is unset or changeme if [[ -z "$CFG_GIT_KEY" || "$CFG_GIT_KEY" == "changeme" ]]; then while true; do echo "Please enter your Git access token (Press Enter to set to 'empty'):" read -rs NEW_GIT_KEY echo "" if [[ -z "$NEW_GIT_KEY" ]]; then NEW_GIT_KEY="empty" fi commandUpdateConfigOption "CFG_GIT_KEY" "$NEW_GIT_KEY" commandReloadConfigs echo "Git token updated." break done fi fi CFG_GIT_URL="${CFG_GIT_URL:-changeme}" if [[ -z "$CFG_GIT_URL" ]] || [[ "$CFG_GIT_URL" == "changeme" ]]; then while true; do echo "Please enter your Git repository URL:" read -rs NEW_GIT_URL if [[ -n "$NEW_GIT_URL" ]]; then commandUpdateConfigOption "CFG_GIT_URL" "$NEW_GIT_URL" commandReloadConfigs echo "Updating Git URL" break fi echo "Error: Git URL cannot be empty" done fi } commandCleanGitUrl() { local url="$1" url="${url#"${url%%[![:space:]]*}"}" url="${url%"${url##*[![:space:]]}"}" url="${url#https://}" url="${url#http://}" while [[ "$url" == */ ]]; do url="${url%/}"; done url="${url%.git}" echo "$url" } setup_repo() { while true; do update_config_values commandReloadConfigs CLEAN_GIT_URL=$(commandCleanGitUrl "$CFG_GIT_URL") local auth="" if [[ "$CFG_GIT_USER" != "empty" && -n "$CFG_GIT_USER" ]]; then auth="${CFG_GIT_USER}:${CFG_GIT_KEY}@" fi AUTH_HTTPS_REPO_URL="https://${auth}${CLEAN_GIT_URL}.git" AUTH_HTTP_REPO_URL="http://${auth}${CLEAN_GIT_URL}.git" echo "" echo "Configuration Summary:" echo "" if [[ "$CFG_INSTALL_MODE" == "local" ]]; then echo "Git Username: [NOT APPLICABLE]" echo "Git Token: [NOT APPLICABLE]" echo "Git URL: [NOT APPLICABLE]" else echo "Git Username: $CFG_GIT_USER" echo "Git Token: [HIDDEN]" echo "Git URL: $CLEAN_GIT_URL" fi echo "" read -p "Are these details correct? (y/n): " confirm_config if [[ "$confirm_config" == "y" ]]; then echo "Configuration confirmed." break else reset_git_config fi done } sync_configs_from_install() { local src="/docker/install/configs" local dst="/docker/configs" if [ ! -d "$src" ]; then echo "ERROR: $src missing — clone broken." return 1 fi mkdir -p "$dst" # No-clobber: preserve the user's live config values; only add new files. if ! cp -an "$src"/. "$dst"/; then echo "ERROR: Failed to sync configs from $src to $dst." return 1 fi chown -R libreportal:libreportal "$dst" if [ ! -f "$dst/general/general_install" ]; then echo "ERROR: $dst/general/general_install missing after sync." return 1 fi [ -n "$CFG_GIT_USER" ] && commandUpdateConfigOption "CFG_GIT_USER" "$CFG_GIT_USER" [ -n "$CFG_GIT_KEY" ] && commandUpdateConfigOption "CFG_GIT_KEY" "$CFG_GIT_KEY" [ -n "$CFG_GIT_URL" ] && commandUpdateConfigOption "CFG_GIT_URL" "$CFG_GIT_URL" commandUpdateConfigOption "CFG_INSTALL_MODE" "git" echo "SUCCESS: Configs synced and credentials re-applied." } clone_repo() { rm -rf /docker/install local clone_url if [ "$CFG_GIT_USER" != "empty" ]; then for clone_url in "$AUTH_HTTPS_REPO_URL" "$AUTH_HTTP_REPO_URL"; do if git clone -q "$clone_url" "/docker/install" 2>/dev/null; then sudo cp -f /docker/install/init.sh /root/ sync_configs_from_install || return 1 echo "SUCCESS: Clone complete. Run 'libreportal run' to continue." return 0 fi done echo "ERROR: Authentication failed. Please check your credentials." return 1 fi for clone_url in "https://${CLEAN_GIT_URL}.git" "http://${CLEAN_GIT_URL}.git"; do if git clone -q "$clone_url" "/docker/install" 2>/dev/null; then sudo cp -f /docker/install/init.sh /root/ sync_configs_from_install || return 1 echo "SUCCESS: Clone complete. Run 'libreportal run' to continue." return 0 fi done echo "ERROR: Anonymous clone failed." return 1 } clone_and_install() { commandReloadConfigs if [[ "$CFG_INSTALL_MODE" == "local" ]]; then echo "NOTICE: Local install detected — no Git remote to clone." return 0 fi update_config_values; setup_repo; clone_repo; } cd /docker/ if [[ $command1 == "reset" ]]; then clone_and_install elif [ -f "/docker/install/start.sh" ]; then chmod 0755 /docker/install/* cd /docker/install ./start.sh "$command1" "$command2" "$command3" "$command4" "$command5" "$command6" "$command7" "$command8" "$command9" else clone_and_install fi # LibrePortal Command End EOF sudo chmod +x $command_script sudo chown $sudo_user_name:$sudo_user_name $command_script source $sudo_bashrc } initUpdateConfigs() { isHeader "Updating Configs" initUpdateConfigOption "CFG_LIBREPORTAL_USER_PASS" "$param2" && isSuccessful "Updated Docker user password" initUpdateConfigOption "CFG_GIT_USER" "$param3" && isSuccessful "Updated Git Username" initUpdateConfigOption "CFG_GIT_KEY" "$param4" && isSuccessful "Updated Git Token" initUpdateConfigOption "CFG_GIT_URL" "$param5" && isSuccessful "Updated Git URL" initUpdateConfigOption "CFG_INSTALL_MODE" "$param7" && isSuccessful "Updated Installation Mode" isHeader "Verifying Saved Configuration" local cfg_file="/docker/configs/general/general_install" if [[ ! -f "$cfg_file" ]]; then isError "Expected $cfg_file is missing — install cannot proceed." exit 1 fi local saved_mode saved_user saved_url saved_key saved_mode=$(grep -E '^CFG_INSTALL_MODE=' "$cfg_file" | sed -E 's/^[^=]+=([^[:space:]#]*).*/\1/') saved_user=$(grep -E '^CFG_GIT_USER=' "$cfg_file" | sed -E 's/^[^=]+=([^[:space:]#]*).*/\1/') saved_url=$(grep -E '^CFG_GIT_URL=' "$cfg_file" | sed -E 's/^[^=]+=([^[:space:]#]*).*/\1/') saved_key=$(grep -E '^CFG_GIT_KEY=' "$cfg_file" | sed -E 's/^[^=]+=([^[:space:]#]*).*/\1/') isNotice "Mode: $saved_mode" isNotice "User: $saved_user" isNotice "URL: $saved_url" [[ -n "$saved_key" && "$saved_key" != "changeme" ]] \ && isNotice "Token: [SET]" \ || isNotice "Token: [EMPTY]" if [[ "$saved_mode" == "git" ]]; then local fail=0 if [[ -z "$saved_url" || "$saved_url" == "changeme" ]]; then isError "CFG_GIT_URL didn't persist — installer will re-prompt." fail=1 fi if [[ "$saved_user" != "empty" && ( -z "$saved_user" || "$saved_user" == "changeme" ) ]]; then isError "CFG_GIT_USER didn't persist — installer will re-prompt." fail=1 fi if (( fail )); then isError "Aborting before handoff so you can fix this once instead of retyping every install run." exit 1 fi fi isSuccessful "Configuration verified." } completeInitMessage() { isHeader "LibrePortal Initilization Complete" # Run LibrePortal install as the libreportal user isNotice "Starting LibrePortal installation as $sudo_user_name user..." # Switch to libreportal user and run the install command if sudo -u "$sudo_user_name" LIBREPORTAL_SKIP_LOGO=1 bash -c "libreportal run install"; then : else echo "" echo "⚠️ LibrePortal installation encountered issues." echo " You can manually run the installation with:" echo " sudo -u $sudo_user_name bash -c 'libreportal run install'" echo "" fi } # Only run the installer entrypoint (root check + init flow) when init.sh is # EXECUTED directly. When it's SOURCED — start.sh loads init.sh for its function # defs at runtime, and under Model A start.sh runs as the manager, not root — the # defs above are all that's wanted and this root check must NOT fire. [[ "${BASH_SOURCE[0]}" != "${0}" ]] && return 0 2>/dev/null if [[ $EUID -ne 0 ]]; then echo "This script must be run as root." exit 1 else if [[ "$param1" == "init" ]]; then # Always check existing config first initCheckConfigs if [[ "$param6" == "true" || "$param6" == "1" ]]; then # Validate unattended params validateUnattended initDisplayConfig # in unattended mode, it will auto-accept else # Interactive mode initInputQuestions initDisplayConfig fi # Common steps (run in both interactive and unattended after confirmation) if [ "$init_skip_os_update" != true ]; then initOS fi if [ "$init_skip_prereqs" != true ]; then initPrerequires fi initDocker initUsers initFolders initGIT initLibrePortalCommand initUpdateConfigs completeInitMessage fi fi