#!/usr/bin/env bash
# =============================================================================
#  start-ftp.sh - install (offline) and start the vsftpd FTP server daemon
#                 on RHEL 9.6, ready for performance testing
# =============================================================================
#
#  USAGE (as root)
#      ./start-ftp.sh            install if needed, configure, start
#      ./start-ftp.sh start      same as above
#      ./start-ftp.sh stop       stop the test FTP server
#      ./start-ftp.sh restart    stop, then start again
#      ./start-ftp.sh status     show whether the FTP server is running
#      ./start-ftp.sh reset      empty the upload folder and the transfer log
#      ./start-ftp.sh cleanup    stop and remove everything the kit created
#
#  WHAT "start" DOES, STEP BY STEP
#      1. Checks the operating system (RHEL 9 expected).
#      2. Installs the vsftpd RPM if missing, WITHOUT internet:
#           a) from RPM files in ./rpms/  (see download-rpms.sh), or
#           b) from a local dnf repository already set up on the host
#              (for example the mounted RHEL 9.6 DVD / ISO).
#      3. Creates the test user "ftpperf" with a random password.
#      4. Creates the FTP folder /var/ftp/perf-test with the test files.
#      5. Lets SELinux allow the uploads (only while the machine is running).
#      6. Writes the vsftpd configuration, login (PAM) rules and user list.
#      7. Creates the systemd service vsftpd-perf-test.service.
#      8. Starts the daemon, waits for its greeting and checks a login.
#
#  SAFE FOR A HOST THAT ALREADY RUNS FTP
#      The normal vsftpd.service and /etc/vsftpd/vsftpd.conf are not changed.
#      The test server listens only on 127.0.0.1 port 2121, so it cannot be
#      reached from the network. "cleanup" removes everything from steps 3-7.
# =============================================================================

set -euo pipefail
source "$(dirname -- "${BASH_SOURCE[0]}")/lib/common.sh"


# ----------------------------------------------------------------------------
#  Step 1 - operating system check
# ----------------------------------------------------------------------------
check_operating_system() {
    # /etc/os-release defines NAME, VERSION_ID, ...
    source /etc/os-release

    if [[ ${VERSION_ID%%.*} != 9 ]]; then
        die "This kit is made for RHEL 9. Found: $PRETTY_NAME"
    fi
    if [[ $ID != rhel || $VERSION_ID != 9.6 ]]; then
        warn "Target is RHEL 9.6; this host is '$PRETTY_NAME'. Continuing."
    fi
    log "Operating system: $PRETTY_NAME"
}


# ----------------------------------------------------------------------------
#  Step 2 - install the FTP server without internet access
# ----------------------------------------------------------------------------
install_vsftpd_offline() {
    if rpm -q vsftpd >/dev/null 2>&1; then
        log "FTP server is already installed: $(rpm -q vsftpd)"
    else
        local rpm_dir="$KIT_DIR/rpms"

        if compgen -G "$rpm_dir/*.rpm" >/dev/null; then
            # a) RPM files shipped next to this script. All other repositories
            #    are disabled so dnf never tries to reach the internet.
            log "Installing vsftpd from RPM files in $rpm_dir"
            dnf install -y --disablerepo='*' "$rpm_dir"/*.rpm
        else
            # b) A local repository already configured on this host (RHEL DVD).
            log "Installing vsftpd from the local dnf repositories"
            if ! dnf install -y vsftpd; then
                die "Could not install vsftpd offline.
       Either copy RPMs into $rpm_dir (see download-rpms.sh),
       or mount the RHEL 9.6 DVD and configure it as a local repository."
            fi
        fi
        ok "Installed $(rpm -q vsftpd)"
    fi

    # The load generator needs Python 3, part of every RHEL 9 installation.
    command -v python3 >/dev/null || die "python3 not found (package python3)."
}


# ----------------------------------------------------------------------------
#  Step 3 - the test user
# ----------------------------------------------------------------------------
#  A system user whose shell is /sbin/nologin: it can log in to the test FTP
#  server, but not to a shell (SSH, console) and not to the normal vsftpd
#  (RHEL's /etc/pam.d/vsftpd accepts only users with a real shell).
create_test_user() {
    if id "$TEST_USER" >/dev/null 2>&1; then
        log "Test user '$TEST_USER' already exists"
        # Make sure home folder and shell are the ones the kit expects.
        usermod --home "$FTP_ROOT" --shell /sbin/nologin "$TEST_USER" 2>/dev/null || true
    else
        log "Creating test user '$TEST_USER' (no shell login possible)"
        useradd --system --no-create-home --home-dir "$FTP_ROOT" \
                --shell /sbin/nologin --comment "FTP performance test user" "$TEST_USER"
    fi

    # A random password, readable by root only. The load generator reads it.
    if [[ ! -s $TEST_PASSWORD_FILE ]]; then
        (umask 077; head -c 18 /dev/urandom | base64 | tr -d '/+=' > "$TEST_PASSWORD_FILE")
    fi
    chmod 600 "$TEST_PASSWORD_FILE"
    echo "$TEST_USER:$(cat "$TEST_PASSWORD_FILE")" | chpasswd
}


# ----------------------------------------------------------------------------
#  Step 4 - FTP folder and test files
# ----------------------------------------------------------------------------
#     /var/ftp/perf-test/            (root, read-only: vsftpd requires this for chroot)
#       download/big.bin             BIG_FILE_MB megabytes
#       download/small/file-*.bin    SMALL_FILE_COUNT files of SMALL_FILE_KB kilobytes
#       upload/                      owned by the test user, the only writable folder
create_test_files() {
    local download_dir="$FTP_ROOT/download"
    local small_dir="$download_dir/small"
    local upload_dir="$FTP_ROOT/upload"

    install -d -o root -g root -m 755 "$FTP_ROOT" "$download_dir"
    install -d -o "$TEST_USER" -g "$TEST_USER" -m 755 "$upload_dir"

    # One big file (for the download test). Recreated only if the size changed.
    local big_file="$download_dir/big.bin"
    local big_bytes=$(( BIG_FILE_MB * 1024 * 1024 ))
    if [[ $(stat -c %s "$big_file" 2>/dev/null || echo 0) != "$big_bytes" ]]; then
        log "Creating $big_file ($BIG_FILE_MB MB)"
        dd if=/dev/zero of="$big_file" bs=1M count="$BIG_FILE_MB" status=none
    fi

    # Many small files (small-file and listing tests). Recreated if count or size changed.
    local small_bytes=$(( SMALL_FILE_KB * 1024 ))
    local existing_count=0 first_size=0
    if [[ -d $small_dir ]]; then
        existing_count="$(find "$small_dir" -name 'file-*.bin' | wc -l)"
        first_size="$(stat -c %s "$small_dir/file-00001.bin" 2>/dev/null || echo 0)"
    fi
    if [[ $existing_count != "$SMALL_FILE_COUNT" || $first_size != "$small_bytes" ]]; then
        log "Creating $SMALL_FILE_COUNT small files of $SMALL_FILE_KB KB in $small_dir"
        rm -rf "$small_dir"
        install -d -o root -g root -m 755 "$small_dir"
        local i
        for (( i = 1; i <= SMALL_FILE_COUNT; i++ )); do
            head -c "$small_bytes" /dev/urandom > "$small_dir/$(printf 'file-%05d.bin' "$i")"
        done
    fi
    chmod -R a+r "$download_dir"
    log "Test files ready in $FTP_ROOT ($(du -sh "$FTP_ROOT" | cut -f1))"
}


# ----------------------------------------------------------------------------
#  Step 5 - SELinux
# ----------------------------------------------------------------------------
#  vsftpd runs in the SELinux domain ftpd_t. It may READ files labelled
#  public_content_t (the default for /var/ftp) without any change.
#  To WRITE, the folder needs the label public_content_rw_t, and the
#  boolean ftpd_anon_write must be on. The kit switches the boolean on only
#  until the next reboot (no "-P"), remembers the old value, and "cleanup"
#  puts it back.
configure_selinux() {
    if ! selinux_is_on; then
        log "SELinux is disabled - nothing to configure"
        return
    fi

    restorecon -R "$FTP_ROOT"
    chcon -R -t public_content_rw_t "$FTP_ROOT/upload"

    local current
    current="$(getsebool ftpd_anon_write | awk '{ print $3 }')"
    if [[ $current == off ]]; then
        log "SELinux: switching boolean ftpd_anon_write on (until reboot) so uploads work"
        setsebool ftpd_anon_write on
        # Remember to switch it off again in "cleanup".
        grep -q '^ftpd_anon_write_was=' "$TEST_STATE_FILE" 2>/dev/null ||
            echo "ftpd_anon_write_was=off" >> "$TEST_STATE_FILE"
    fi
    log "SELinux mode: $(getenforce)"
}


# ----------------------------------------------------------------------------
#  Step 6 - vsftpd configuration, login rules, user list
# ----------------------------------------------------------------------------
write_test_config() {
    log "Writing $TEST_CONF"
    cat > "$TEST_CONF" <<EOF
# =============================================================================
#  Created by $KIT_DIR/start-ftp.sh
#  Values come from settings.conf. "./start-ftp.sh cleanup" removes this file.
#  Used only by $TEST_SERVICE.service, never by the normal vsftpd.service.
#  All options: man vsftpd.conf
# =============================================================================

# --- Where to listen ---------------------------------------------------------
# One stand-alone daemon on the loopback address only (not reachable from
# the network). systemd keeps it in the foreground (background=NO).
listen=YES
listen_ipv6=NO
listen_address=$FTP_ADDRESS
listen_port=$FTP_PORT
background=NO

# --- Who may log in ----------------------------------------------------------
# Only the local user(s) in the user list, checked by our own PAM rules.
# Every user is locked into its home folder ($FTP_ROOT).
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=022
pam_service_name=$TEST_SERVICE
userlist_enable=YES
userlist_deny=NO
userlist_file=$TEST_USER_LIST
chroot_local_user=YES

# --- Data connections --------------------------------------------------------
# Passive mode: the client opens each data connection to a port in this range.
pasv_enable=YES
pasv_min_port=$PASV_MIN_PORT
pasv_max_port=$PASV_MAX_PORT
use_sendfile=YES

# --- Limits ------------------------------------------------------------------
# All test clients come from 127.0.0.1, so the per-address limit
# (default 50) is switched off (0 = no limit).
max_clients=$MAX_CLIENTS
max_per_ip=0

# --- Options that affect performance (see PERFORMANCE-METRICS.md) ----------
isolate_network=$ISOLATE_NETWORK
reverse_lookup_enable=$REVERSE_LOOKUP

# --- Logging -----------------------------------------------------------------
xferlog_enable=$XFERLOG_ENABLE
xferlog_std_format=YES
xferlog_file=$TEST_LOG
dirmessage_enable=YES
EOF

    log "Writing $TEST_PAM_FILE"
    cat > "$TEST_PAM_FILE" <<EOF
#%PAM-1.0
# Created by $KIT_DIR/start-ftp.sh - login rules for $TEST_SERVICE only.
# The same as RHEL's /etc/pam.d/vsftpd, except:
#   - only users in $TEST_USER_LIST are accepted
#   - no pam_shells check, so the test user can keep the shell /sbin/nologin
session    optional     pam_keyinit.so    force revoke
auth       required     pam_listfile.so item=user sense=allow file=$TEST_USER_LIST onerr=fail
auth       include      password-auth
account    include      password-auth
session    required     pam_loginuid.so
session    include      password-auth
EOF

    log "Writing $TEST_USER_LIST"
    echo "$TEST_USER" > "$TEST_USER_LIST"

    touch "$TEST_LOG"
    chmod 600 "$TEST_CONF" "$TEST_USER_LIST" "$TEST_LOG"

    # Give the new files the SELinux labels vsftpd is allowed to use.
    if command -v restorecon >/dev/null; then
        restorecon "$TEST_CONF" "$TEST_PAM_FILE" "$TEST_USER_LIST" "$TEST_PASSWORD_FILE" "$TEST_LOG"
    fi
}

# Stop early with a clear message if another program already uses the port.
check_port_is_free() {
    ftp_is_running && return 0                   # our own server: fine, it is restarted
    if ss -Hltn "( sport = :$FTP_PORT )" | grep -q .; then
        die "Port $FTP_PORT is already in use by another program:
$(ss -Hltnp "( sport = :$FTP_PORT )")
       Choose another FTP_PORT in settings.conf."
    fi
}


# ----------------------------------------------------------------------------
#  Step 7 - systemd service
# ----------------------------------------------------------------------------
#  It has no [Install] section on purpose: a test server should not start
#  at boot. Start it with ./start-ftp.sh when you want to test.
write_systemd_unit() {
    has_systemd || return 0

    log "Writing systemd service $TEST_UNIT_FILE"
    cat > "$TEST_UNIT_FILE" <<EOF
# Created by $KIT_DIR/start-ftp.sh - "./start-ftp.sh cleanup" removes it.
[Unit]
Description=vsftpd FTP server for performance testing (FTP perf kit)
Documentation=man:vsftpd(8) man:vsftpd.conf(5)
After=network.target

[Service]
Type=simple
ExecStart=$VSFTPD $TEST_CONF
EOF

    if command -v restorecon >/dev/null; then
        restorecon "$TEST_UNIT_FILE"
    fi
    systemctl daemon-reload
}


# ----------------------------------------------------------------------------
#  Step 8 - start the daemon and check it
# ----------------------------------------------------------------------------
start_daemon() {
    if ftp_is_running; then
        log "The test FTP server is running - restarting it to apply the configuration"
        ftp_restart
    else
        log "Starting the FTP server daemon"
        ftp_start
    fi

    if ! wait_until_serving 30; then
        if has_systemd; then
            die "The FTP server does not answer within 30 s.
       Look at:  journalctl -u $TEST_SERVICE"
        else
            die "The FTP server does not answer within 30 s.
       Run it in the foreground to see why:  $VSFTPD $TEST_CONF"
        fi
    fi
}

# Log in once as the test user, to be sure the whole setup works.
check_login() {
    local output
    output="$(run_load_generator hold --sessions 1 --hold-seconds 0 2>&1)"
    if ! grep -q '^sessions_open *= *1' <<< "$output"; then
        die "The FTP server answers, but the test user cannot log in:
$(grep 'error example' <<< "$output")
       Check the PAM rules ($TEST_PAM_FILE) and, with SELinux, the audit log:
       ausearch -m avc -ts recent"
    fi
    log "Test login as '$TEST_USER' works"
}

print_summary() {
    local selinux
    selinux="$(getenforce 2>/dev/null || echo 'not available')"

    echo
    ok "The FTP server is running and accepts logins."
    echo "    Version      : $(vsftpd_version)"
    echo "    Main PID     : $(ftp_main_pid)"
    echo "    Listens on   : $FTP_ADDRESS port $FTP_PORT (this machine only)"
    echo "    Passive ports: $PASV_MIN_PORT - $PASV_MAX_PORT"
    echo "    Test user    : $TEST_USER (password in $TEST_PASSWORD_FILE)"
    echo "    FTP folder   : $FTP_ROOT"
    echo "    Config file  : $TEST_CONF"
    echo "    Transfer log : $TEST_LOG"
    echo "    SELinux      : $selinux"
    if has_systemd; then
        echo "    Managed by   : systemd  (systemctl status $TEST_SERVICE)"
    else
        echo "    Managed by   : PID file $TEST_PIDFILE (no systemd, e.g. inside a container)"
    fi
    echo
    echo "    Try it       : curl -u $TEST_USER:\$(cat $TEST_PASSWORD_FILE) ftp://$FTP_ADDRESS:$FTP_PORT/download/"
    echo "    Next step    : ./test-ftp.sh"
}


# ----------------------------------------------------------------------------
#  Other actions
# ----------------------------------------------------------------------------
show_status() {
    if ftp_is_serving; then
        local processes
        processes="$(vsftpd_all_pids | wc -l)"
        ok "The test FTP server is running (PID $(ftp_main_pid)) on $FTP_ADDRESS:$FTP_PORT"
        echo "    vsftpd processes : $processes (1 listener + 2 per connected client)"
        echo "    Upload folder    : $(du -sh "$FTP_ROOT/upload" | cut -f1)"
        echo "    Transfer log     : $(du -h "$TEST_LOG" | cut -f1)"
    elif ftp_is_running; then
        warn "vsftpd is running, but does not answer. Run: ./start-ftp.sh"
        exit 1
    else
        warn "The test FTP server is not running."
        exit 1
    fi
}

# Remove uploaded test files and empty the transfer log.
reset_test_data() {
    log "Emptying $FTP_ROOT/upload and $TEST_LOG"
    find "$FTP_ROOT/upload" -mindepth 1 -delete 2>/dev/null || true
    : > "$TEST_LOG"
    ok "Upload folder and transfer log emptied."
}

cleanup_test_setup() {
    log "Stopping the test FTP server"
    ftp_stop

    log "Removing the service, configuration, login rules and transfer log"
    rm -f "$TEST_UNIT_FILE" "$TEST_CONF" "$TEST_PAM_FILE" "$TEST_USER_LIST" \
          "$TEST_PASSWORD_FILE" "$TEST_LOG" "$TEST_PIDFILE"
    has_systemd && systemctl daemon-reload

    log "Removing the FTP folder $FTP_ROOT"
    rm -rf "$FTP_ROOT"

    if id "$TEST_USER" >/dev/null 2>&1; then
        log "Removing the test user '$TEST_USER'"
        userdel "$TEST_USER"
    fi

    # Put the SELinux boolean back the way it was before start-ftp.sh.
    if grep -q '^ftpd_anon_write_was=off' "$TEST_STATE_FILE" 2>/dev/null && selinux_is_on; then
        log "SELinux: switching boolean ftpd_anon_write off again"
        setsebool ftpd_anon_write off
    fi
    rm -f "$TEST_STATE_FILE"

    ok "Cleanup finished. (The vsftpd RPM stays installed; remove it with: dnf remove vsftpd)"
}


# ----------------------------------------------------------------------------
#  Main
# ----------------------------------------------------------------------------
main() {
    local action="${1:-start}"
    require_root "$@"

    case "$action" in
        start)
            check_operating_system
            install_vsftpd_offline
            create_test_user
            create_test_files
            configure_selinux
            write_test_config
            check_port_is_free
            write_systemd_unit
            start_daemon
            check_login
            print_summary
            ;;
        stop)
            ftp_stop
            ok "The test FTP server is stopped."
            ;;
        restart)
            ftp_restart
            wait_until_serving 30 || die "The FTP server did not come back. Run ./start-ftp.sh"
            ok "The test FTP server restarted."
            ;;
        status)
            show_status
            ;;
        reset)
            reset_test_data
            ;;
        cleanup)
            cleanup_test_setup
            ;;
        *)
            echo "Usage: $0 [start|stop|restart|status|reset|cleanup]"
            exit 2
            ;;
    esac
}

main "$@"
