diff --git a/CHANGELOG.md b/CHANGELOG.md index ac63849..d37cf83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,32 +7,39 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] -### Removed -- Path B install pipeline (setup, privileged-setup, user-operations, - services-improved, lib/privileges.sh) -- dead code, never wired in +--- + +## [1.0.1] - 2026-06-10 + +Stabilization pass: bug fixes, security hardening, and codebase consolidation +on the Path A install pipeline. + +### Fixed +- Infinite recursion in get_timezone_mount() and get_gpu_devices() on Linux (B1) +- ((x++)) abort under set -e across hops and install (B5) +- Glob stored as string breaking multi-user directory detection (B3) +- Missing hops_service_definitions.sh reference in firewall setup (B4) +- Linux-only guard missing from hops entry point (B6) +- eval echo "~$SUDO_USER" replaced with getent passwd in uninstall (B3/S5) ### Changed -- Version reset to 1.0.0 +- Duplicate log, error_exit, warning, success, info removed from hops, + uninstall, and install; single canonical copies in lib/common.sh (A5, Q1) +- Duplicate validate_password, generate_secure_password, create_docker_networks, + validate_timezone removed from install; lib/security.sh, lib/docker.sh, + lib/validation.sh are now the sole definitions (A5) +- lib/docker.sh, lib/validation.sh, lib/security.sh use LIB_DIR instead of + SCRIPT_DIR so sourcing them inside a function does not clobber the caller +- validate_timezone updated to warn-and-default instead of error_exit +- validate_password updated to handle empty input (return 3) +- uninstall sources lib/common.sh directly, allowing standalone execution + +### Removed +- Path B install pipeline (setup, privileged-setup, user-operations, + services-improved, lib/privileges.sh) -- dead code, never wired in (A1, A3) --- ## [1.0.0] - TBD -Full rewrite and stabilization of the Path A install pipeline. - -### Fixed -- Infinite recursion in get_timezone_mount() and get_gpu_devices() on Linux -- ((x++)) abort under set -e across hops and install -- Glob stored as string breaking multi-user directory detection -- Missing hops_service_definitions.sh reference in firewall setup - -### Security -- Replace broken AES-GCM encryption with supported cipher -- Move passphrases off command line (use fd-based passphrase input) -- Remove committed default Authelia credential -- Use mktemp for temp files instead of predictable /tmp paths - -### Changed -- Single canonical service catalog (services) -- Latest image tags throughout -- lib/secrets.sh wired into install flow for .env encryption at rest +Full rewrite establishing the Path A install pipeline. diff --git a/TODO.md b/TODO.md index d627056..111a07f 100644 --- a/TODO.md +++ b/TODO.md @@ -160,12 +160,16 @@ Generated by codebase audit (2026-06-10). Ranked by severity. - Fix passphrase-on-command-line exposure (S1, S2). - Wire encrypt/decrypt calls into `install` flow. -### A5 -- `hops` duplicates functions from `lib/common.sh` [HIGH] -- DO FIRST +### A5 -- `hops` duplicates functions from `lib/common.sh` [HIGH] -- RESOLVED - `log`, `error_exit`, `warning`, `success`, `info`, `validate_timezone`, - `validate_password`, `generate_secure_password`, `create_docker_networks`, - `get_service_port/image` are all defined twice (or three times). -- Fix: source `lib/common.sh` from `hops` and remove local duplicates. -- Must be done before bug fixes to avoid patching the same logic in multiple places. + `validate_password`, `generate_secure_password`, `create_docker_networks` + removed from `hops`, `uninstall`, and `install`. Canonical copies kept in + `lib/common.sh`, `lib/security.sh`, `lib/validation.sh`, `lib/docker.sh`. +- Fixed `lib/docker.sh`, `lib/validation.sh`, `lib/security.sh` to use `LIB_DIR` + instead of `SCRIPT_DIR` so sourcing them inside a function doesn't clobber + the caller's `SCRIPT_DIR`. +- `validate_timezone` updated to warn-and-default instead of error_exit. +- `validate_password` updated to handle empty input (return 3). ### A6 -- Caddy is unreachable via the menu [LOW] - `services` defines `generate_caddy` but the `select_services` menu in @@ -236,10 +240,8 @@ Generated by codebase audit (2026-06-10). Ranked by severity. ## CODE QUALITY -### Q1 -- Three separate error-handling implementations [HIGH] -- DO FIRST -- `hops`, `uninstall`, and `lib/common.sh` each define their own `error_exit` - and `log` with different formats. Consolidate in `lib/common.sh`. -- Covered by A5; tracked here for completeness. +### Q1 -- Three separate error-handling implementations [HIGH] -- RESOLVED +- Covered by A5; all resolved. ### Q2 -- `set -e` + intentional non-zero returns is a minefield [MED] - `validate_password` returns 1/2/3, `check_port` returns 1 -- these work only @@ -262,15 +264,15 @@ Generated by codebase audit (2026-06-10). Ranked by severity. ### Cleanup first (do before any bug fixes) 1. [DONE] Delete Path B files (A1/A3) -2. Consolidate duplicate functions into `lib/common.sh` (A5, Q1) -- one copy to fix +2. [DONE] Consolidate duplicate functions into `lib/common.sh` (A5, Q1) 3. Reconcile `lib/docker.sh` service maps with `services` (A2) -- one catalog to fix 4. Remove debug echo statements from `lib/system.sh` (Q3) -- reduce noise ### Bug fixes -5. Fix B1 (infinite recursion in `services`) -- unblocks all Linux installs -6. Fix B5 (`((x++))` under `set -e`) -- prevents silent aborts -7. Fix B3 (glob directory detection) -- fixes multi-user and uninstall -8. Fix B4 (wrong filename in firewall setup) +5. [DONE] Fix B1 (infinite recursion in `services`) +6. [DONE] Fix B5 (`((x++))` under `set -e`) +7. [DONE] Fix B3 (glob directory detection) +8. [DONE] Fix B4 (wrong filename in firewall setup) 9. Fix B7 (intra-selection port collision detection) ### Security pass diff --git a/hops b/hops index 85f9dee..e0a39de 100755 --- a/hops +++ b/hops @@ -12,8 +12,7 @@ if [[ "$(uname -s)" != "Linux" ]]; then exit 1 fi -# Script version and metadata -readonly SCRIPT_VERSION="1.0.0" +# Script metadata (SCRIPT_VERSION defined in lib/common.sh) readonly SCRIPT_NAME="HOPS" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -22,53 +21,15 @@ readonly INSTALLER_SCRIPT="$SCRIPT_DIR/install" readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/uninstall" readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/services" -# Load system utilities +# Load shared utilities +source "$SCRIPT_DIR/lib/common.sh" source "$SCRIPT_DIR/lib/system.sh" -# Color codes are defined in lib/common.sh - -# Logging setup (will be set by setup_logging) -LOG_DIR="" -LOG_FILE="" - # Initialize logging init_logging() { setup_logging "hops-main" } -# Logging function -log() { - local message="$1" - local timestamp="$(date '+%Y-%m-%d %T')" - - if [[ -w "$LOG_FILE" ]]; then - echo "$timestamp - $message" >> "$LOG_FILE" - fi - - echo -e "$message" -} - -# Error handling -error_exit() { - log "${RED}❌ ERROR: $1${NC}" - exit 1 -} - -# Warning function -warning() { - log "${YELLOW}⚠️ WARNING: $1${NC}" -} - -# Success function -success() { - log "${GREEN}✅ $1${NC}" -} - -# Info function -info() { - log "${BLUE}ℹ️ $1${NC}" -} - # Clear screen and show header show_header() { clear @@ -632,7 +593,7 @@ update_hops() { # Source the updated script to get new version if [[ -f "$SCRIPT_DIR/hops" ]]; then - local new_version=$(grep '^readonly SCRIPT_VERSION=' "$SCRIPT_DIR/hops" | cut -d'"' -f2) + local new_version=$(grep '^readonly SCRIPT_VERSION=' "$SCRIPT_DIR/lib/common.sh" | cut -d'"' -f2) success "Updated to version $new_version" fi diff --git a/install b/install index 552f422..d7eccac 100755 --- a/install +++ b/install @@ -8,12 +8,14 @@ install_hops() { set -e # Script version for update tracking - local SCRIPT_VERSION="1.0.0" local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - # Load system utilities + # Load shared utilities source "$SCRIPT_DIR/lib/common.sh" source "$SCRIPT_DIR/lib/system.sh" + source "$SCRIPT_DIR/lib/validation.sh" + source "$SCRIPT_DIR/lib/security.sh" + source "$SCRIPT_DIR/lib/docker.sh" # -------------------------------------------- # LOGGING SETUP @@ -174,8 +176,7 @@ EOF if [[ "$keep_tz" =~ ^[Nn]$ ]]; then echo -e "Enter timezone (e.g., America/New_York, Europe/London): " read -r user_timezone - validate_timezone "$user_timezone" - TIMEZONE="$user_timezone" + TIMEZONE=$(validate_timezone "$user_timezone") else TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "America/New_York") fi @@ -210,42 +211,6 @@ EOF log " AppData: $APPDATA_DIR" } - # -------------------------------------------- - # VALIDATION FUNCTIONS - # -------------------------------------------- - validate_timezone() { - if ! timedatectl list-timezones | grep -qx "$1" 2>/dev/null; then - log "⚠️ Timezone '$1' invalid, defaulting to 'America/New_York'" - TIMEZONE="America/New_York" - fi - } - - validate_password() { - local password="$1" - local min_length="${2:-12}" - - if [[ -z "$password" ]]; then - echo -e "\n🔐 Password must meet these requirements:" - echo " • Minimum $min_length characters" - echo " • At least one uppercase letter" - echo " • At least one lowercase letter" - echo " • At least one number" - echo " • At least one special character" - return 3 - fi - - if [[ ${#password} -lt $min_length ]]; then - return 1 - fi - - if [[ ! "$password" =~ [A-Z] ]] || [[ ! "$password" =~ [a-z] ]] || \ - [[ ! "$password" =~ [0-9] ]] || [[ ! "$password" =~ [^A-Za-z0-9] ]]; then - return 2 - fi - - return 0 - } - check_port() { local PORT=$1 local SERVICE=$2 @@ -315,42 +280,6 @@ EOF error_exit "No Docker Compose detected. Please install Docker first." } - # -------------------------------------------- - # IMPROVED PASSWORD GENERATION - # -------------------------------------------- - generate_secure_password() { - local length="${1:-16}" - local max_attempts=5 - local attempt=1 - - while [[ $attempt -le $max_attempts ]]; do - # Generate password with mixed case, numbers, and symbols - local password=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-${length}) - - # Ensure it meets complexity requirements - if validate_password "$password" "$length"; then - echo "$password" - return 0 - fi - - attempt=$((attempt + 1)) - done - - # Fallback: construct a guaranteed compliant password - local upper=$(generate_chars 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 2) - local lower=$(generate_chars 'abcdefghijklmnopqrstuvwxyz' 4) - local digits=$(generate_chars '0123456789' 2) - local symbols=$(generate_chars '!@#$%^&*' 2) - local remaining_length=$((length - 10)) - - if [[ $remaining_length -gt 0 ]]; then - local remaining=$(generate_chars 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' $remaining_length) - shuffle_string "${upper}${lower}${digits}${symbols}${remaining}" - else - shuffle_string "${upper}${lower}${digits}${symbols}" - fi - } - # -------------------------------------------- # ENVIRONMENT FILE GENERATION # -------------------------------------------- @@ -642,22 +571,6 @@ EOF create_docker_networks } - # -------------------------------------------- - # NETWORK CREATION - # -------------------------------------------- - create_docker_networks() { - log "🌐 Creating Docker networks..." - - # Create traefik network if it doesn't exist - if ! docker network ls --format "{{.Name}}" | grep -q "^traefik$"; then - if docker network create traefik 2>/dev/null; then - log "✅ Created traefik network" - else - log "⚠️ Could not create traefik network (may already exist)" - fi - fi - } - # -------------------------------------------- # ENHANCED DEPLOYMENT WITH ROLLBACK # -------------------------------------------- diff --git a/lib/common.sh b/lib/common.sh index 9106e07..a3398ec 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -2,7 +2,6 @@ # HOPS - Common Utility Functions # Shared functions for logging, error handling, and UI -# Version: 1.0.0 # Prevent multiple sourcing if [[ -n "${HOPS_COMMON_LOADED:-}" ]]; then @@ -10,6 +9,8 @@ if [[ -n "${HOPS_COMMON_LOADED:-}" ]]; then fi readonly HOPS_COMMON_LOADED=1 +readonly SCRIPT_VERSION="1.0.1" + # Color codes for output readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' diff --git a/lib/docker.sh b/lib/docker.sh index a513b7c..1e6a1a4 100644 --- a/lib/docker.sh +++ b/lib/docker.sh @@ -5,8 +5,8 @@ # Version: 1.0.0 # Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$LIB_DIR/common.sh" # Service definitions with pinned versions declare -A HOPS_SERVICES=( diff --git a/lib/security.sh b/lib/security.sh index cfa52b5..d6cd3fd 100644 --- a/lib/security.sh +++ b/lib/security.sh @@ -5,14 +5,24 @@ # Version: 1.0.0 # Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$LIB_DIR/common.sh" # Password validation validate_password() { local password="$1" local min_length="${2:-12}" - + + if [[ -z "$password" ]]; then + echo -e "\n Password must meet these requirements:" + echo " - Minimum $min_length characters" + echo " - At least one uppercase letter" + echo " - At least one lowercase letter" + echo " - At least one number" + echo " - At least one special character" + return 3 + fi + # Check minimum length if [[ ${#password} -lt $min_length ]]; then debug "Password too short: ${#password} < $min_length" diff --git a/lib/validation.sh b/lib/validation.sh index d432261..c5366a0 100644 --- a/lib/validation.sh +++ b/lib/validation.sh @@ -5,8 +5,8 @@ # Version: 1.0.0 # Source common functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common.sh" +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$LIB_DIR/common.sh" # Validate and sanitize directory path validate_directory_path() { @@ -60,24 +60,18 @@ validate_directory_path() { echo "$path" } -# Validate timezone +# Validate timezone -- returns the timezone string, defaulting to America/New_York if invalid validate_timezone() { local timezone="$1" - - if [[ -z "$timezone" ]]; then - error_exit "Timezone cannot be empty" + local default="America/New_York" + + if [[ -z "$timezone" ]] || \ + [[ ! "$timezone" =~ ^[A-Za-z_]+(/[A-Za-z_]+)*$ ]] || \ + ! timedatectl list-timezones 2>/dev/null | grep -qx "$timezone"; then + warning "Timezone '${timezone}' invalid, defaulting to '${default}'" + timezone="$default" fi - - # Basic format validation - if [[ ! "$timezone" =~ ^[A-Za-z_]+(/[A-Za-z_]+)*$ ]]; then - error_exit "Invalid timezone format: $timezone" - fi - - # Check if timezone file exists - if [[ ! -f "/usr/share/zoneinfo/$timezone" ]]; then - error_exit "Unknown timezone: $timezone" - fi - + echo "$timezone" } diff --git a/uninstall b/uninstall index d0ecec9..a3d5fc2 100755 --- a/uninstall +++ b/uninstall @@ -8,29 +8,15 @@ uninstall_hops() { set +e # Script version for consistency - local SCRIPT_VERSION="1.0.0" + local _UNINSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + # Load shared utilities if not already loaded (allows standalone execution) + source "$_UNINSTALL_DIR/lib/common.sh" # -------------------------------------------- # LOGGING SETUP # -------------------------------------------- - local LOG_DIR="/var/log/hops" - local LOG_FILE="$LOG_DIR/hops-uninstall-$(date +%Y%m%d-%H%M%S).log" - mkdir -p "$LOG_DIR" - touch "$LOG_FILE" - - log() { - echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE" - } - - error_exit() { - log "❌ ERROR: $1" - log "❌ Uninstallation failed. Check logs at: $LOG_FILE" - exit 1 - } - - warning() { - log "⚠️ WARNING: $1" - } + setup_logging "hops-uninstall" # -------------------------------------------- # HEADER