#!/usr/bin/env bash
# =============================================================================
#  start-nginx.sh - install (offline) and start the Nginx web server daemon
#                   on RHEL 9.6, ready for performance testing
# =============================================================================
#
#  USAGE (as root)
#      ./start-nginx.sh            install if needed, configure, start
#      ./start-nginx.sh start      same as above
#      ./start-nginx.sh stop       stop the nginx daemon
#      ./start-nginx.sh restart    stop, then start again
#      ./start-nginx.sh status     show whether Nginx is running
#      ./start-nginx.sh cleanup    undo every change made by this kit
#
#  WHAT "start" DOES, STEP BY STEP
#      1. Checks the operating system (RHEL 9 expected).
#      2. Installs nginx + httpd-tools (for the "ab" load tool) 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. Checks that the web port is free and allowed by SELinux.
#      4. Creates small test web pages in /usr/share/nginx/html/perf-test/.
#      5. Writes the test configuration /etc/nginx/nginx.conf
#         (the original is saved as nginx.conf.before-perf-test).
#      6. Raises the open-files limit of nginx.service (systemd drop-in).
#      7. Allows Nginx to act as a reverse proxy under SELinux
#         (boolean httpd_can_network_relay, until the next reboot only).
#      8. Checks the configuration with "nginx -t".
#      9. Starts the daemon with systemd (systemctl), then waits until the
#         first page is served.
#
#  "cleanup" undoes steps 4 to 7.
# =============================================================================

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

# Remembers that step 7 changed the SELinux boolean, so cleanup can undo it.
SELINUX_STATE_FILE="$KIT_DIR/.selinux-relay-was-off"

# Marker line written at the top of the kit's nginx.conf.
CONF_MARKER="# Created by the Nginx performance kit"


# ----------------------------------------------------------------------------
#  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 Nginx without internet access
# ----------------------------------------------------------------------------
install_nginx_offline() {
    if rpm -q nginx httpd-tools >/dev/null 2>&1; then
        log "Nginx is already installed: $(rpm -q nginx)"
        return
    fi

    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 Nginx 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).
        if [[ -n $NGINX_STREAM ]]; then
            log "Selecting Nginx version stream $NGINX_STREAM"
            dnf module enable -y "nginx:$NGINX_STREAM"
        fi
        log "Installing Nginx from the local dnf repositories"
        if ! dnf install -y nginx httpd-tools; then
            die "Could not install Nginx 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 nginx) and $(rpm -q httpd-tools)"
}


# ----------------------------------------------------------------------------
#  Step 3 - port checks
# ----------------------------------------------------------------------------

# Name of the program listening on TCP port $1, or nothing if the port is free.
port_owner() {
    ss -Hltnp "sport = :$1" 2>/dev/null |
        grep -o 'users:(("[^"]*"' | head -1 | cut -d'"' -f2 || true
}

# True when SELinux lets web servers use TCP port $1 (type http_port_t).
selinux_allows_web_port() {
    local port="$1"
    semanage port -l 2>/dev/null |
        awk -v port="$port" '
            $1 == "http_port_t" && $2 == "tcp" {
                # The rest of the line is a list such as "80, 81, 8000-8010".
                for (i = 3; i <= NF; i++) {
                    gsub(/,/, "", $i)
                    split($i, range, "-")
                    low = range[1]; high = (range[2] == "" ? range[1] : range[2])
                    if (port >= low + 0 && port <= high + 0) found = 1
                }
            }
            END { exit !found }'
}

check_ports() {
    local port owner
    for port in "$NGINX_PORT" "$BACKEND_PORT"; do
        owner="$(port_owner "$port")"
        if [[ -n $owner && $owner != nginx ]]; then
            die "Port $port is already used by '$owner'.
       Stop that program, or choose another port in settings.conf
       (NGINX_PORT / BACKEND_PORT), for example 8443 or 9000."
        fi

        if selinux_is_enabled && command -v semanage >/dev/null; then
            if ! selinux_allows_web_port "$port"; then
                local message="SELinux does not allow web servers on port $port.
       Allow it with:   semanage port -a -t http_port_t -p tcp $port"
                if [[ $(getenforce) == Enforcing ]]; then
                    die "$message"
                fi
                warn "$message"
            fi
        fi
    done
    log "Ports $NGINX_PORT (web) and $BACKEND_PORT (test backend) are available"
}


# ----------------------------------------------------------------------------
#  Step 4 - test web pages
# ----------------------------------------------------------------------------
create_test_content() {
    log "Creating test pages in $TEST_CONTENT_DIR"
    mkdir -p "$TEST_CONTENT_DIR"

    # Health page: start/test scripts read it to know Nginx is really serving.
    echo "nginx-perf-test-ok" > "$TEST_CONTENT_DIR/health.txt"

    # 1 KB HTML page - a typical small request (throughput / latency tests).
    {
        echo "<html><head><title>perf test</title></head><body><pre>"
        head -c 700 /dev/zero | tr '\0' 'x' | fold -w 70
        echo "</pre></body></html>"
    } > "$TEST_CONTENT_DIR/small.html"

    # About 100 KB of HTML text - like a real page (compression test).
    # A fixed random seed makes the page identical on every run.
    awk 'BEGIN {
        srand(42)
        print "<!DOCTYPE html><html><head><title>perf test - text page</title></head><body>"
        print "<h1>Monthly service report</h1><table>"
        for (row = 1; row <= 1000; row++) {
            printf "<tr><td>%05d</td><td>host-%03d.example.lan</td><td>%s</td><td>%d ms</td><td>%d requests</td></tr>\n",
                   row, int(rand() * 500), (rand() < 0.9 ? "OK" : "WARNING"),
                   int(rand() * 900) + 1, int(rand() * 100000)
        }
        print "</table></body></html>"
    }' > "$TEST_CONTENT_DIR/text.html"

    # 1 MB binary file - a download (transfer-rate test).
    head -c 1048576 /dev/urandom > "$TEST_CONTENT_DIR/large.bin"

    chmod 755 "$TEST_CONTENT_DIR"
    chmod 644 "$TEST_CONTENT_DIR"/*

    # Give the files the SELinux label Nginx is allowed to read.
    if command -v restorecon >/dev/null; then
        restorecon -R "$TEST_CONTENT_DIR"
    fi
}


# ----------------------------------------------------------------------------
#  Step 5 - Nginx configuration for the test
# ----------------------------------------------------------------------------
backup_original_config() {
    # Save the original only once, and never save the kit's own file.
    if [[ -f $NGINX_CONF_BACKUP ]]; then
        return
    fi
    if head -1 "$NGINX_CONF" | grep -qF "$CONF_MARKER"; then
        return
    fi
    cp -a "$NGINX_CONF" "$NGINX_CONF_BACKUP"
    log "Original configuration saved as $NGINX_CONF_BACKUP"
}

write_test_config() {
    backup_original_config

    local access_log_line="access_log off;"
    if [[ $TEST_ACCESS_LOG == on ]]; then
        access_log_line="access_log /var/log/nginx/perf-test-access.log main buffer=64k flush=5s;"
    fi

    log "Writing $NGINX_CONF"

    # Note: "\$" below writes a literal "$" (an Nginx variable) into the file.
    cat > "$NGINX_CONF" <<EOF
$CONF_MARKER ($KIT_DIR/start-nginx.sh).
# Values come from settings.conf. The original file is $NGINX_CONF_BACKUP;
# "./start-nginx.sh cleanup" puts it back.

user  nginx;
pid   $NGINX_PID_FILE;
error_log  $NGINX_ERROR_LOG;

# One master process + this many worker processes ("auto" = one per CPU core).
worker_processes  $WORKER_PROCESSES;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    # Connections each worker may have open at the same time.
    worker_connections  $WORKER_CONNECTIONS;
}

http {
    log_format  main  '\$remote_addr - \$remote_user [\$time_local] "\$request" '
                      '\$status \$body_bytes_sent "\$http_referer" '
                      '"\$http_user_agent" "\$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;

    # --- Fast file sending (RHEL defaults) -----------------------------------
    sendfile            on;     # the kernel copies files straight to the network
    tcp_nopush          on;     # send headers and file start in one packet
    tcp_nodelay         on;     # do not delay small packets

    # --- Keep-alive: reuse one TCP connection for many requests ---------------
    keepalive_timeout   $KEEPALIVE_TIMEOUT;
    keepalive_requests  $KEEPALIVE_REQUESTS;

    types_hash_max_size 4096;
    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Other web sites in /etc/nginx/conf.d/ keep working.
    include /etc/nginx/conf.d/*.conf;

    # --- The test backend, used by the "proxy" test ---------------------------
    upstream perf_backend {
        server     127.0.0.1:$BACKEND_PORT;
        keepalive  $UPSTREAM_KEEPALIVE;      # reuse connections to the backend
    }

    # --- Main web server ------------------------------------------------------
    server {
        listen       $NGINX_PORT backlog=$LISTEN_BACKLOG;
        server_name  $NGINX_HOST localhost _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        # Test pages: only reachable from this machine.
        location /perf-test/ {
            allow 127.0.0.1;
            allow ::1;
            deny  all;
            $access_log_line

            # Compress text answers for clients that ask for it.
            gzip             on;
            gzip_comp_level  $GZIP_LEVEL;
            gzip_min_length  1024;
            gzip_types       text/plain text/css text/xml application/json application/javascript;
            gzip_vary        on;
            gzip_http_version 1.0;   # also for HTTP/1.0 clients such as "ab"
        }

        # Live counters (connections, requests), read by the tests.
        location = /nginx-status {
            stub_status;
            allow 127.0.0.1;
            allow ::1;
            deny  all;
            access_log off;
        }

        # Reverse proxy: /perf-proxy/<file> is fetched from the test backend.
        location /perf-proxy/ {
            allow 127.0.0.1;
            allow ::1;
            deny  all;
            $access_log_line

            proxy_pass          http://perf_backend/perf-test/;
            proxy_http_version  1.1;             # needed for backend keep-alive
            proxy_set_header    Connection "";   # needed for backend keep-alive
            proxy_set_header    Host \$host;
        }
    }

    # --- Test backend: plays the part of an application server -----------------
    server {
        listen       127.0.0.1:$BACKEND_PORT backlog=$LISTEN_BACKLOG;
        server_name  perf-backend;
        root         /usr/share/nginx/html;
        access_log   off;

        location /perf-test/ { }
        location /           { return 404; }
    }
}
EOF

    if command -v restorecon >/dev/null; then
        restorecon "$NGINX_CONF"
    fi
}


# ----------------------------------------------------------------------------
#  Step 6 - open-files limit
# ----------------------------------------------------------------------------
#  Every connection is an open file. RHEL starts services with a limit of
#  1024, which is too low for thousands of connections. (Nginx's own
#  "worker_rlimit_nofile" would need an extra SELinux permission; the systemd
#  setting does not.)
write_systemd_dropin() {
    if ! has_systemd; then
        return      # without systemd, nginx_start raises the limit itself
    fi
    log "Writing $SYSTEMD_DROPIN (open-files limit $OPEN_FILES_LIMIT)"
    mkdir -p "$(dirname "$SYSTEMD_DROPIN")"
    cat > "$SYSTEMD_DROPIN" <<EOF
# Created by $KIT_DIR/start-nginx.sh. Delete this file to undo.
[Service]
LimitNOFILE=$OPEN_FILES_LIMIT
EOF
    systemctl daemon-reload
}


# ----------------------------------------------------------------------------
#  Step 7 - SELinux: allow Nginx to work as a reverse proxy
# ----------------------------------------------------------------------------
#  By default SELinux does not let a web server open connections to other
#  servers. The boolean httpd_can_network_relay allows exactly that. It is
#  switched on for the running system only; a reboot switches it off again.
allow_reverse_proxy_in_selinux() {
    if ! selinux_is_enabled; then
        return
    fi
    if [[ $(getsebool httpd_can_network_relay | awk '{print $3}') == on ]]; then
        return
    fi
    log "SELinux: switching on httpd_can_network_relay (until reboot) for the proxy test"
    setsebool httpd_can_network_relay on
    touch "$SELINUX_STATE_FILE"
    log "  To keep it after reboots:  setsebool -P httpd_can_network_relay on"
}


# ----------------------------------------------------------------------------
#  Step 8 - configuration check
# ----------------------------------------------------------------------------
check_config() {
    log "Checking Nginx configuration (nginx -t)"
    if ! "$NGINX_BIN" -t; then
        die "Nginx configuration has errors (see messages above)."
    fi
}


# ----------------------------------------------------------------------------
#  Step 9 - start the daemon
# ----------------------------------------------------------------------------
start_daemon() {
    # A full restart (not a reload) is needed so the new open-files limit
    # takes effect.
    if nginx_is_running; then
        log "Nginx is running - restarting it to apply the configuration"
        nginx_restart
    else
        log "Starting the Nginx daemon"
        nginx_start
    fi

    if ! wait_until_serving 30; then
        die "Nginx did not answer $HEALTH_URL within 30 s.
       Look at:  journalctl -u nginx   and   $NGINX_ERROR_LOG"
    fi
}

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

    echo
    ok "Nginx is running and serving pages."
    echo "    Version      : $("$NGINX_BIN" -v 2>&1 | awk -F'/' '{print $2}')"
    echo "    Master PID   : $(nginx_master_pid)"
    echo "    Workers      : $worker_count processes x $WORKER_CONNECTIONS connections"
    echo "    Test page    : $HEALTH_URL"
    echo "    Status page  : $STATUS_URL"
    echo "    Proxy test   : $PROXY_HEALTH_URL"
    echo "    SELinux      : $selinux"
    if has_systemd; then
        echo "    Managed by   : systemd  (systemctl status nginx)"
        echo "    Boot start   : $(systemctl is-enabled nginx 2>/dev/null || true)" \
             "  (to start at every boot: systemctl enable nginx)"
    else
        echo "    Managed by   : nginx -s (no systemd found, e.g. inside a container)"
    fi

    if ! curl --noproxy '*' -sf --max-time 2 "$PROXY_HEALTH_URL" >/dev/null; then
        warn "The proxy page does not answer yet; the \"proxy\" test will fail."
        warn "Check: $NGINX_ERROR_LOG  and  ausearch -m AVC -ts recent"
    fi
    echo
    echo "    Next step    : ./test-nginx.sh"
}


# ----------------------------------------------------------------------------
#  Other actions
# ----------------------------------------------------------------------------
show_status() {
    if nginx_is_serving; then
        ok "Nginx is running (master PID $(nginx_master_pid), $(nginx_worker_pids | wc -l) workers) and serving $HEALTH_URL"
        # The live counters are extra information; never fail on them.
        curl --noproxy '*' -s --max-time 2 "$STATUS_URL" | sed 's/^/    /' || true
    elif nginx_is_running; then
        warn "nginx processes exist, but $HEALTH_URL does not answer. Run: ./start-nginx.sh"
        exit 1
    else
        warn "Nginx is not running."
        exit 1
    fi
}

cleanup_test_setup() {
    if [[ -f $NGINX_CONF_BACKUP ]]; then
        log "Restoring the original $NGINX_CONF"
        mv -f "$NGINX_CONF_BACKUP" "$NGINX_CONF"
        command -v restorecon >/dev/null && restorecon "$NGINX_CONF"
    fi

    log "Removing $TEST_CONTENT_DIR"
    rm -rf "$TEST_CONTENT_DIR"

    if [[ -f $SYSTEMD_DROPIN ]]; then
        log "Removing $SYSTEMD_DROPIN"
        rm -f "$SYSTEMD_DROPIN"
        rmdir --ignore-fail-on-non-empty "$(dirname "$SYSTEMD_DROPIN")"
        systemctl daemon-reload
    fi

    if [[ -f $SELINUX_STATE_FILE ]]; then
        log "SELinux: switching httpd_can_network_relay back off"
        setsebool httpd_can_network_relay off || true
        rm -f "$SELINUX_STATE_FILE"
    fi

    if nginx_is_running; then
        nginx_stop
        if nginx_start; then
            ok "Nginx restarted with its original configuration."
        else
            warn "Nginx did not start with its original configuration (see $NGINX_ERROR_LOG)."
        fi
    fi
    ok "Cleanup finished."
}


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

    case "$action" in
        start)
            check_operating_system
            install_nginx_offline
            check_ports
            create_test_content
            write_test_config
            write_systemd_dropin
            allow_reverse_proxy_in_selinux
            check_config
            start_daemon
            print_summary
            ;;
        stop)
            nginx_stop
            ok "Nginx stopped."
            ;;
        restart)
            nginx_restart
            wait_until_serving 30 || die "Nginx did not come back. See $NGINX_ERROR_LOG"
            ok "Nginx restarted."
            ;;
        status)
            show_status
            ;;
        cleanup)
            cleanup_test_setup
            ;;
        *)
            echo "Usage: $0 [start|stop|restart|status|cleanup]"
            exit 2
            ;;
    esac
}

main "$@"
