The install hands the heavy setup to the manager (completeInitMessage: sudo -u libreportal 'libreportal run install') — creating the docker-install user, rootless setup, apt, sysctl — which needs broad root. initUsers was installing the SCOPED sudoers up front, so that handoff died with 'sudo: a password is required' on useradd. Fix: initUsers installs a temporary NOPASSWD: ALL for the install phase; completeInitMessage calls the new initScopedSudoers to tighten to the runtime allowlist only after the install succeeds (on failure, broad sudo is left so the manual 'libreportal run install' retry works). This restores the documented 'kill NOPASSWD:ALL AFTER the runtime is set up' ordering. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
1429 lines
48 KiB
Bash
Executable File
1429 lines
48 KiB
Bash
Executable File
#!/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"
|
||
# All LibrePortal executables installed outside /docker live together here
|
||
# (root-owned). The user-facing CLI is symlinked into $PATH from /usr/local/bin.
|
||
lp_lib_dir="/usr/local/lib/libreportal"
|
||
command_script="$lp_lib_dir/libreportal"
|
||
command_symlink="/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. Only when init.sh
|
||
# is EXECUTED directly (install time) — start.sh sources init.sh for its function
|
||
# definitions at runtime (Model A, as the manager), and this block must not run
|
||
# then (it would print a spurious "Auto-detected ..." and rewrite the config).
|
||
if [[ "${BASH_SOURCE[0]}" == "$0" && "$param1" != "uninstall" && -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
|
||
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
|
||
|
||
return 1
|
||
}
|
||
|
||
if [[ "${BASH_SOURCE[0]}" == "$0" && -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
|
||
# No -G sudo: the manager's privileges come from the scoped /etc/sudoers.d
|
||
# drop-in below (user-specific), not blanket sudo-group membership.
|
||
sudo useradd -s /bin/bash -d "/home/$sudo_user_name" -m "$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
|
||
# Install-phase sudo: the heavy install runs AS this user (see the handoff in
|
||
# completeInitMessage) and needs BROAD root — useradd for the docker-install
|
||
# user, rootless setup, apt, sysctl, etc. So grant a temporary validated
|
||
# NOPASSWD: ALL drop-in now (never appended to /etc/sudoers — a malformed main
|
||
# file locks out sudo entirely); completeInitMessage calls initScopedSudoers
|
||
# to tighten it to the scoped RUNTIME allowlist once the install succeeds.
|
||
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 install-phase sudo for $sudo_user_name (tightened after install)."
|
||
else
|
||
isError "Refusing to install an invalid sudoers drop-in for $sudo_user_name."
|
||
fi
|
||
rm -f "$sudoers_tmp"
|
||
|
||
initRootHelpers
|
||
}
|
||
|
||
# Tighten the manager's sudo from the install-phase NOPASSWD: ALL down to the
|
||
# scoped RUNTIME allowlist. Called AFTER the (manager-run) install phase, which
|
||
# needs the broad root this deliberately withholds. The runtime then reaches root
|
||
# ONLY via: the unprivileged docker-install user (data plane, rootless-confined),
|
||
# the root-owned /usr/local/lib/libreportal/ helpers (each a fixed, self-validated
|
||
# op the manager can't modify), and a fixed system-binary set. Excluded:
|
||
# bash/su + tee/cp/chmod/chown/sed/mv/rm/install (each root-equivalent). Also
|
||
# clears legacy broad grants (a NOPASSWD: ALL in the main /etc/sudoers, sudo-group
|
||
# membership). See FOOTPRINT.md.
|
||
initScopedSudoers()
|
||
{
|
||
local sudoers_dropin="/etc/sudoers.d/${sudo_user_name}"
|
||
local install_user
|
||
install_user=$(grep -h '^CFG_DOCKER_INSTALL_USER=' "$configs_dir/general/general_docker_install" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
|
||
install_user="${install_user:-${CFG_DOCKER_INSTALL_USER:-dockerinstall}}"
|
||
local sudoers_tmp
|
||
sudoers_tmp=$(mktemp)
|
||
cat > "$sudoers_tmp" <<EOF
|
||
# Scoped least-privilege grant for the LibrePortal manager. Generated by init.sh.
|
||
Cmnd_Alias LP_HELPERS = ${lp_lib_dir}/libreportal-ownership, \\
|
||
${lp_lib_dir}/libreportal-dns, \\
|
||
${lp_lib_dir}/libreportal-ssh-access, \\
|
||
${lp_lib_dir}/libreportal-socket, \\
|
||
${lp_lib_dir}/libreportal-svc, \\
|
||
${lp_lib_dir}/libreportal-bininstall, \\
|
||
${lp_lib_dir}/libreportal-appcfg
|
||
Cmnd_Alias LP_SYSTEM = /usr/bin/systemctl, /usr/sbin/ufw, /usr/local/bin/ufw-docker, \\
|
||
/usr/sbin/nft, /usr/sbin/sysctl, /sbin/sysctl, \\
|
||
/usr/bin/loginctl, /usr/sbin/service
|
||
${sudo_user_name} ALL=(${install_user}) NOPASSWD:SETENV: ALL
|
||
${sudo_user_name} ALL=(root) NOPASSWD: LP_HELPERS, LP_SYSTEM
|
||
EOF
|
||
if sudo visudo -cf "$sudoers_tmp" >/dev/null 2>&1; then
|
||
sudo install -m 0440 -o root -g root "$sudoers_tmp" "$sudoers_dropin"
|
||
# Clear legacy broad grants from older installs.
|
||
if sudo grep -qE "^${sudo_user_name}[[:space:]]+ALL=\(ALL\)[[:space:]]+NOPASSWD:[[:space:]]+ALL" /etc/sudoers 2>/dev/null; then
|
||
local main_tmp; main_tmp=$(mktemp)
|
||
sudo grep -vE "^${sudo_user_name}[[:space:]]+ALL=\(ALL\)[[:space:]]+NOPASSWD:[[:space:]]+ALL$" /etc/sudoers > "$main_tmp"
|
||
sudo visudo -cf "$main_tmp" >/dev/null 2>&1 && sudo cp "$main_tmp" /etc/sudoers
|
||
rm -f "$main_tmp"
|
||
fi
|
||
sudo gpasswd -d "$sudo_user_name" sudo >/dev/null 2>&1 || true
|
||
isSuccessful "Tightened $sudo_user_name sudo to the scoped runtime allowlist."
|
||
else
|
||
isError "Invalid scoped sudoers — left install-phase sudo in place for $sudo_user_name."
|
||
fi
|
||
rm -f "$sudoers_tmp"
|
||
}
|
||
|
||
# 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()
|
||
{
|
||
# One root-owned home for every LibrePortal executable installed outside
|
||
# /docker (the helpers here; the CLI wrapper lands here too, see the command
|
||
# setup). Root-owned 0755 = the manager can't tamper with the helpers it
|
||
# sudo's (the trust boundary the scoped sudoers relies on).
|
||
sudo install -d -m 0755 -o root -g root "$lp_lib_dir"
|
||
local helper helper_src helper_dst helper_tmp
|
||
for helper in libreportal-ownership libreportal-dns libreportal-ssh-access libreportal-socket libreportal-svc libreportal-bininstall libreportal-appcfg; do
|
||
helper_src="$script_dir/scripts/system/$helper"
|
||
helper_dst="$lp_lib_dir/$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..."
|
||
|
||
# The CLI wrapper lives alongside the root helpers in $lp_lib_dir; a symlink
|
||
# in /usr/local/bin keeps `libreportal` on $PATH.
|
||
sudo install -d -m 0755 -o root -g root "$lp_lib_dir"
|
||
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 root:root $command_script
|
||
# Put it on $PATH via a symlink (replaces any older real file at this path).
|
||
sudo ln -sfn "$command_script" "$command_symlink"
|
||
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
|
||
# Install done — tighten the manager's broad install-phase sudo down to
|
||
# the scoped runtime allowlist.
|
||
initScopedSudoers
|
||
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
|
||
}
|
||
|
||
# FULL uninstall: permanently remove everything LibrePortal placed on the host.
|
||
# Runs as root (the entrypoint root-check below enforces it). Order matters —
|
||
# containers run AS the docker-install user and the rootless daemon is that user's
|
||
# systemd --user service, so stop those BEFORE removing the users. Self-contained:
|
||
# uses only init.sh's inline helpers, so it still works as it deletes /docker.
|
||
# Keep in sync with FOOTPRINT.md.
|
||
runFullUninstall()
|
||
{
|
||
local mgr="${sudo_user_name:-libreportal}"
|
||
local iuser
|
||
iuser=$(grep -h '^CFG_DOCKER_INSTALL_USER=' /docker/configs/general/general_docker_install 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}')
|
||
iuser="${iuser:-dockerinstall}"
|
||
|
||
isHeader "LibrePortal — FULL Uninstall"
|
||
isError "This PERMANENTLY removes EVERYTHING — there is no undo:"
|
||
echo " - all containers + images + the rootless docker setup"
|
||
echo " - /docker (ALL app data, configs, database)"
|
||
echo " - the '$mgr' and '$iuser' users + their home directories"
|
||
echo " - /usr/local/lib/libreportal/ + the /usr/local/bin/libreportal command"
|
||
echo " - /etc/sudoers.d/$mgr, the systemd service, the sysctl drop-ins"
|
||
echo " - the restic / kopia / ufw-docker binaries LibrePortal installed"
|
||
echo ""
|
||
isNotice "LEFT IN PLACE: the docker engine, docker-compose, apt-installed deps,"
|
||
isNotice "and your SSH config (so you can't get locked out)."
|
||
echo ""
|
||
isQuestion "Type exactly DELETE LIBREPORTAL to confirm:"
|
||
local confirm; read -r confirm
|
||
if [[ "$confirm" != "DELETE LIBREPORTAL" ]]; then
|
||
isNotice "Aborted — nothing was removed."
|
||
return 0
|
||
fi
|
||
|
||
isHeader "Tearing down LibrePortal"
|
||
|
||
# 1. Stop + remove the task-processor service.
|
||
systemctl disable --now libreportal.service >/dev/null 2>&1
|
||
rm -f /etc/systemd/system/libreportal.service
|
||
systemctl daemon-reload >/dev/null 2>&1
|
||
isSuccessful "Stopped + removed the task-processor service"
|
||
|
||
# 2. Best-effort graceful container removal, then tear down the rootless
|
||
# user's session (stops the rootless dockerd + any survivors).
|
||
local uid; uid=$(id -u "$iuser" 2>/dev/null)
|
||
if [[ -n "$uid" ]]; then
|
||
sudo -u "$iuser" env XDG_RUNTIME_DIR="/run/user/$uid" \
|
||
DOCKER_HOST="unix:///run/user/$uid/docker.sock" \
|
||
bash -c 'docker ps -aq | xargs -r docker rm -f' >/dev/null 2>&1 || true
|
||
sudo -u "$iuser" env XDG_RUNTIME_DIR="/run/user/$uid" \
|
||
dockerd-rootless-setuptool.sh uninstall >/dev/null 2>&1 || true
|
||
loginctl disable-linger "$iuser" >/dev/null 2>&1 || true
|
||
loginctl terminate-user "$iuser" >/dev/null 2>&1 || true
|
||
pkill -9 -u "$iuser" >/dev/null 2>&1 || true
|
||
isSuccessful "Stopped containers + rootless docker for '$iuser'"
|
||
fi
|
||
|
||
# 3. Remove the out-of-/docker footprint (see FOOTPRINT.md).
|
||
rm -f /usr/local/bin/libreportal
|
||
rm -rf /usr/local/lib/libreportal
|
||
rm -f "/etc/sudoers.d/$mgr"
|
||
rm -f /etc/sysctl.d/99-libreportal-hardening.conf /etc/sysctl.d/99-libreportal-rootless.conf
|
||
sysctl --system >/dev/null 2>&1
|
||
rm -f /usr/local/bin/restic /usr/local/bin/kopia /usr/local/bin/ufw-docker
|
||
rm -f /root/init.sh
|
||
isSuccessful "Removed the system-integration footprint"
|
||
|
||
# 4. Remove all app data.
|
||
rm -rf /docker
|
||
isSuccessful "Removed /docker"
|
||
|
||
# 5. Remove the LibrePortal users + their subuid/subgid ranges + home dirs.
|
||
# Terminate each user's session/linger and kill its processes first, or
|
||
# `userdel -r` leaves the home behind ("user currently used"); rm -rf the
|
||
# home afterwards as a backstop.
|
||
local u
|
||
for u in "$mgr" "$iuser"; do
|
||
loginctl disable-linger "$u" >/dev/null 2>&1 || true
|
||
loginctl terminate-user "$u" >/dev/null 2>&1 || true
|
||
pkill -9 -u "$u" >/dev/null 2>&1 || true
|
||
userdel -r "$u" >/dev/null 2>&1 || true
|
||
[[ -n "$u" ]] && rm -rf "/home/$u"
|
||
done
|
||
sed -i "/^${mgr}:/d;/^${iuser}:/d" /etc/subuid /etc/subgid 2>/dev/null || true
|
||
isSuccessful "Removed users '$mgr' + '$iuser' (+ home dirs)"
|
||
|
||
isHeader "LibrePortal uninstalled"
|
||
isNotice "Left in place: docker engine, docker-compose, apt deps, SSH config."
|
||
}
|
||
|
||
# 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
|
||
elif [[ "$param1" == "uninstall" ]]; then
|
||
runFullUninstall
|
||
fi
|
||
fi
|