#!/usr/bin/env bash set -e SCRIPT_COMMIT_SHA="${SCRIPT_COMMIT_SHA:-__SCRIPT_COMMIT_SHA__}" SCRIPT_DIR="${PG_NODE_SCRIPT_DIR:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)}" SHARED_LIB_DIR="${SCRIPT_DIR}/lib" REQUIRED_SHARED_LIBS="common.sh system.sh docker.sh github.sh" # Running from a local checkout/bundle (libs sit next to this script) vs. an # installed copy (libs live under /usr/local/lib). Only the installed copy is # auto-refreshed below; a checkout's libs are used as-is. running_from_checkout=true if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then SHARED_LIB_DIR="/usr/local/lib/pasarguard-scripts/lib" running_from_checkout=false fi # Refresh every shared library from the repo into the install dir. All files are # downloaded to a staging dir first and only swapped in if EVERY download # succeeds, so a partial/failed refresh never leaves a half-updated set and any # existing copy is preserved on failure. bootstrap_pg_node_shared_libs() { local fetch_repo="PasarGuard/scripts" local bootstrap_dir="/usr/local/lib/pasarguard-scripts/lib" local tmp_dir="" local shared_lib="" tmp_dir=$(mktemp -d) || return 1 for shared_lib in $REQUIRED_SHARED_LIBS; do if ! curl -fsSL --retry 4 --retry-delay 2 --connect-timeout 20 "https://git.nimbaha.org/pgnode/github/${fetch_repo}/raw/main/lib/${shared_lib}" -o "$tmp_dir/$shared_lib"; then rm -rf "$tmp_dir" return 1 fi done mkdir -p "$bootstrap_dir" || { rm -rf "$tmp_dir" return 1 } for shared_lib in $REQUIRED_SHARED_LIBS; do if ! install -m 644 "$tmp_dir/$shared_lib" "$bootstrap_dir/$shared_lib"; then rm -rf "$tmp_dir" return 1 fi done rm -rf "$tmp_dir" SHARED_LIB_DIR="$bootstrap_dir" return 0 } # For an installed copy, always refresh the shared libraries from the repo so an # outdated copy can never be sourced (the files are small). Best-effort: if the # refresh fails (e.g. no network) any existing copy is kept and the presence # check below still guards against a genuinely missing library. if [ "$running_from_checkout" = false ]; then bootstrap_pg_node_shared_libs || true fi for shared_lib in $REQUIRED_SHARED_LIBS; do if [ ! -f "$SHARED_LIB_DIR/$shared_lib" ]; then printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/$shared_lib" >&2 exit 1 fi done # shellcheck source=lib/common.sh source "$SHARED_LIB_DIR/common.sh" # shellcheck source=lib/system.sh source "$SHARED_LIB_DIR/system.sh" # shellcheck source=lib/docker.sh source "$SHARED_LIB_DIR/docker.sh" # shellcheck source=lib/github.sh source "$SHARED_LIB_DIR/github.sh" # Validate a user-supplied instance name (--name). The value flows into # filesystem paths, the systemd unit (body + filename), sed/yq programs and # the network service's command word, so it must be restricted to a safe # character set to prevent path traversal and command/directive injection. validate_app_name() { local name="$1" [[ "$name" =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$ ]] } # Handle global options AUTO_CONFIRM=false APP_NAME="" CUSTOM_NAME_SET=false ARGS=() while [[ $# -gt 0 ]]; do case "$1" in -y | --yes) AUTO_CONFIRM=true shift ;; --name) if [[ -z "${2:-}" ]]; then echo "Error: --name requires a value." >&2 exit 1 fi if ! validate_app_name "$2"; then echo "Error: invalid --name '$2'. Use 1-63 chars: letters, digits, '_' or '-', starting with a letter or digit." >&2 exit 1 fi APP_NAME="$2" CUSTOM_NAME_SET=true shift 2 ;; --name=*) APP_NAME="${1#*=}" if [[ -z "$APP_NAME" ]]; then echo "Error: --name requires a value." >&2 exit 1 fi if ! validate_app_name "$APP_NAME"; then echo "Error: invalid --name '$APP_NAME'. Use 1-63 chars: letters, digits, '_' or '-', starting with a letter or digit." >&2 exit 1 fi CUSTOM_NAME_SET=true shift ;; *) ARGS+=("$1") shift ;; esac done set -- "${ARGS[@]}" COMMAND="${1:-}" # Fetch IP address from ifconfig.io API NODE_IP_V4=$(curl -s -4 --fail --max-time 5 ifconfig.io 2>/dev/null || echo "") NODE_IP_V6=$(curl -s -6 --fail --max-time 5 ifconfig.io 2>/dev/null || echo "") NODE_IP="${NODE_IP_V4:-}" if [ -z "$NODE_IP" ]; then NODE_IP="${NODE_IP_V6:-}" fi if [ -z "$NODE_IP" ]; then NODE_IP="127.0.0.1" fi if [[ "${1:-}" == "install" || "${1:-}" == "install-script" ]] && [ -z "${APP_NAME:-}" ]; then APP_NAME="pg-node" fi # Set script name if APP_NAME is not set if [ -z "${APP_NAME:-}" ]; then SCRIPT_NAME=$(basename "$0") APP_NAME="${SCRIPT_NAME%.*}" fi if [[ "$CUSTOM_NAME_SET" == true && "$COMMAND" =~ ^(install|install-script)$ ]]; then if command -v "$APP_NAME" >/dev/null 2>&1; then echo "Error: '$APP_NAME' is an existing Linux command. Please choose a different --name." >&2 exit 1 fi fi INSTALL_DIR="/opt" if [ -z "${APP_DIR:-}" ]; then if [ -d "$INSTALL_DIR/$APP_NAME" ]; then APP_DIR="$INSTALL_DIR/$APP_NAME" else APP_DIR="$INSTALL_DIR/$APP_NAME" fi fi DATA_DIR="${DATA_DIR:-/var/lib/$APP_NAME}" COMPOSE_FILE="$APP_DIR/docker-compose.yml" ENV_FILE="$APP_DIR/.env" SSL_CERT_FILE="$DATA_DIR/certs/ssl_cert.pem" SSL_KEY_FILE="$DATA_DIR/certs/ssl_key.pem" LAST_XRAY_CORES=5 FETCH_REPO="PasarGuard/scripts" NODE_SERVICE_REPO="PasarGuard/node-serviced" NODE_SERVICE_RELEASE_API="https://git.nimbaha.org/pgnode/api/repos/${NODE_SERVICE_REPO}/releases/latest" NODE_SERVICE_BINARY_NAME="node-serviced" # Configure service paths based on APP_NAME. set_service_paths() { SERVICE_NAME="${APP_NAME}-service" SERVICE_BINARY_PATH="/usr/local/bin/${SERVICE_NAME}" SERVICE_UNIT="/etc/systemd/system/${SERVICE_NAME}.service" } # Require systemd (systemctl) on the system. require_systemd() { if ! command -v systemctl >/dev/null 2>&1; then colorized_echo red "systemd is required to manage the service (systemctl not found)." exit 1 fi } # Check if the node systemd service is installed. service_installed() { if ! command -v systemctl >/dev/null 2>&1; then return 1 fi set_service_paths if [ -f "$SERVICE_UNIT" ] || systemctl list-unit-files | grep -q "^${SERVICE_NAME}.service"; then return 0 fi return 1 } # Restart the node systemd service if currently installed. restart_service_if_installed() { if ! service_installed; then return fi if [ "$(id -u)" != "0" ]; then colorized_echo yellow "$SERVICE_NAME is installed; run as root to restart it." return fi systemctl restart "$SERVICE_NAME" colorized_echo blue "$SERVICE_NAME service restarted." } # Update and restart the node systemd service if currently installed. update_service_if_installed() { if ! service_installed; then return fi if [ "$(id -u)" != "0" ]; then colorized_echo yellow "$SERVICE_NAME is installed; run as root to update/restart it." return fi install_node_service_script systemctl daemon-reload systemctl restart "$SERVICE_NAME" colorized_echo blue "$SERVICE_NAME service updated and restarted." } # Detect system platform architecture string for node-serviced releases. detect_node_serviced_platform() { local arch os platform os=$(uname -s 2>/dev/null || echo "") if [ "$os" != "Linux" ]; then colorized_echo red "Unsupported OS for node-serviced: $os" exit 1 fi arch=$(uname -m 2>/dev/null || echo "") case "$arch" in x86_64 | amd64) platform="Linux_x86_64" ;; aarch64 | arm64 | armv8* ) platform="Linux_arm64" ;; armv7l | armv7) platform="Linux_armv7" ;; armv6l | armv6) platform="Linux_armv6" ;; *) colorized_echo red "Unsupported architecture for node-serviced: $arch" exit 1 ;; esac echo "$platform" } # Display firewall hints for opening a given port and protocol. configure_firewall_for_port() { local port="$1" local proto="${2:-tcp}" local hint="If a firewall is enabled (e.g., UFW or firewalld), allow ${port}/${proto}." colorized_echo yellow "$hint" } # Download and install the pg-node CLI script to /usr/local/bin. install_node_script() { print_script_execution_header "pg-node" "$SCRIPT_COMMIT_SHA" "install" colorized_echo blue "Installing node script" TARGET_PATH="/usr/local/bin/$APP_NAME" TEMP_FILE=$(create_temp_file "pg-node-script" ".sh") # Download script to temp file first colorized_echo cyan " Downloading script from GitHub..." if ! github_download_file "$(github_raw_url "$FETCH_REPO" "pg-node.sh")" "$TEMP_FILE"; then colorized_echo red "✗ Failed to download script from $(github_raw_url "$FETCH_REPO" "pg-node.sh")" rm -f "$TEMP_FILE" exit 1 fi # Replace APP_NAME in the script - the script has APP_NAME="" on line 5 # We need to set it to the current APP_NAME value if grep -q "^APP_NAME=" "$TEMP_FILE"; then sed -i "s|^APP_NAME=.*|APP_NAME=\"$APP_NAME\"|" "$TEMP_FILE" fi install_shared_libs_from_repo "$FETCH_REPO" common.sh system.sh docker.sh github.sh # Remove old file if it exists if [ -f "$TARGET_PATH" ]; then colorized_echo cyan " Replacing existing script at $TARGET_PATH..." rm -f "$TARGET_PATH" fi # Move temp file to target location mv "$TEMP_FILE" "$TARGET_PATH" chmod 755 "$TARGET_PATH" # Verify the installation if [ -f "$TARGET_PATH" ] && [ -x "$TARGET_PATH" ]; then colorized_echo green "✓ node script installed successfully at $TARGET_PATH" else colorized_echo red "✗ Failed to install script - file may not be executable" exit 1 fi } # Download and install the node-serviced binary release from GitHub. install_node_service_script() { set_service_paths if ! command -v jq >/dev/null 2>&1; then detect_os install_package jq fi colorized_echo blue "Installing node-serviced binary" local platform release_json latest_tag latest_version asset_name asset_url tmp_dir archive_path platform=$(detect_node_serviced_platform) if ! release_json=$(curl -fsSL "$NODE_SERVICE_RELEASE_API"); then colorized_echo red "Failed to query latest node-serviced release from $NODE_SERVICE_RELEASE_API" exit 1 fi latest_tag=$(echo "$release_json" | jq -r '.tag_name // empty') latest_version="${latest_tag#v}" if [ -z "$latest_version" ] || [ "$latest_version" = "null" ]; then colorized_echo red "Failed to resolve latest node-serviced version from $NODE_SERVICE_RELEASE_API" exit 1 fi asset_name="${NODE_SERVICE_BINARY_NAME}_${latest_version}_${platform}.tar.gz" asset_url=$(echo "$release_json" | jq -r --arg name "$asset_name" '.assets[]? | select(.name==$name) | .browser_download_url' | head -n 1) if [ -z "$asset_url" ] || [ "$asset_url" = "null" ]; then colorized_echo red "node-serviced asset not found for platform $platform (expected $asset_name)" exit 1 fi tmp_dir=$(create_temp_dir "node-serviced") archive_path="${tmp_dir}/${asset_name}" colorized_echo cyan " Downloading ${asset_name}..." if ! curl -sSL "$asset_url" -o "$archive_path"; then colorized_echo red "Failed to download node-serviced from $asset_url" rm -rf "$tmp_dir" exit 1 fi colorized_echo cyan " Extracting node-serviced..." if ! tar -xzf "$archive_path" -C "$tmp_dir" "$NODE_SERVICE_BINARY_NAME" 2>/dev/null; then colorized_echo red "Failed to extract node-serviced binary from archive." rm -rf "$tmp_dir" exit 1 fi install -m 755 "${tmp_dir}/${NODE_SERVICE_BINARY_NAME}" "$SERVICE_BINARY_PATH" rm -rf "$tmp_dir" colorized_echo green "node-serviced installed successfully at $SERVICE_BINARY_PATH (v${latest_version})" } # Get a list of occupied ports get_occupied_ports() { if command -v ss &>/dev/null; then OCCUPIED_PORTS=$(ss -tuln | awk '{print $5}' | grep -Eo '[0-9]+$' | sort | uniq) elif command -v netstat &>/dev/null; then OCCUPIED_PORTS=$(netstat -tuln | awk '{print $4}' | grep -Eo '[0-9]+$' | sort | uniq) else colorized_echo yellow "Neither ss nor netstat found. Attempting to install net-tools." detect_os install_package net-tools if command -v netstat &>/dev/null; then OCCUPIED_PORTS=$(netstat -tuln | awk '{print $4}' | grep -Eo '[0-9]+$' | sort | uniq) else colorized_echo red "Failed to install net-tools. Please install it manually." exit 1 fi fi } # Function to check if a port is occupied is_port_occupied() { if echo "$OCCUPIED_PORTS" | grep -q -w "$1"; then return 0 else return 1 fi } # Function to detect if a string is an IP address (IPv4 or IPv6) is_ip_address() { local input="$1" # Check for IPv4 (e.g., 192.168.1.1) if [[ "$input" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then # Validate each octet is 0-255 IFS='.' read -ra octets <<< "$input" for octet in "${octets[@]}"; do if [[ $octet -lt 0 || $octet -gt 255 ]]; then return 1 fi done return 0 fi # Check for IPv6 (simplified check - contains colons and hex digits) if [[ "$input" =~ ^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$ ]] || [[ "$input" =~ ^:: ]] || [[ "$input" =~ :: ]]; then return 0 fi return 1 } # Function to normalize SAN entry (add DNS: or IP: prefix if missing) normalize_san_entry() { local entry="$1" local normalized="" # Remove leading/trailing whitespace entry=$(echo "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # If already has prefix, return as-is if [[ "$entry" =~ ^DNS:.+ ]]; then echo "$entry" return 0 elif [[ "$entry" =~ ^IP:.+ ]]; then echo "$entry" return 0 fi # Auto-detect and add prefix if is_ip_address "$entry"; then normalized="IP:$entry" else # Assume it's a domain name normalized="DNS:$entry" fi echo "$normalized" } # Validate a Subject Alternative Name (SAN) entry for certificate generation. validate_san_entry() { local entry="$1" # Remove leading/trailing whitespace entry=$(echo "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # Empty entry is invalid if [ -z "$entry" ]; then return 1 fi # Normalize the entry (add prefix if missing) local normalized normalized=$(normalize_san_entry "$entry") # Check if normalized entry is valid if [[ "$normalized" =~ ^DNS:.+ ]] || [[ "$normalized" =~ ^IP:.+ ]]; then return 0 else return 1 fi } # Generate a random UUID v4 using available system tools. generate_uuid_v4() { cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null || python -c "import uuid; print(uuid.uuid4())" 2>/dev/null } # Check if the installed openssl binary supports the -addext option. openssl_supports_addext() { openssl req -help 2>&1 | grep -q -- '-addext' } # Generate an EC self-signed certificate using openssl -addext. generate_self_signed_cert_with_addext() { local san_string="$1" openssl req -x509 -newkey ec \ -pkeyopt ec_paramgen_curve:P-256 \ -keyout "$SSL_KEY_FILE" \ -out "$SSL_CERT_FILE" -days 3650 -nodes \ -subj "/CN=$NODE_IP" \ -addext "subjectAltName = $san_string" >/dev/null 2>&1 } # Generate an EC self-signed certificate using a temporary openssl config file. generate_self_signed_cert_with_config() { local san_string="$1" local openssl_config="" local status=0 openssl_config=$(create_temp_file "pg-node-openssl" ".cnf") { echo "[req]" echo "distinguished_name = req_distinguished_name" echo "x509_extensions = v3_req" echo "prompt = no" echo "" echo "[req_distinguished_name]" echo "CN = $NODE_IP" echo "" echo "[v3_req]" echo "subjectAltName = $san_string" } >"$openssl_config" openssl req -x509 -newkey ec \ -pkeyopt ec_paramgen_curve:P-256 \ -keyout "$SSL_KEY_FILE" \ -out "$SSL_CERT_FILE" -days 3650 -nodes \ -config "$openssl_config" \ -extensions v3_req >/dev/null 2>&1 || status=$? rm -f "$openssl_config" return "$status" } # Generate a self-signed SSL/TLS certificate with Subject Alternative Names (SANs). gen_self_signed_cert() { local san_entries=("DNS:localhost" "IP:127.0.0.1") local extra_san="" local user_san_entries=() # Add IPv4 if it exists if [ -n "$NODE_IP_V4" ]; then san_entries+=("IP:$NODE_IP_V4") fi # Add IPv6 if it exists if [ -n "$NODE_IP_V6" ]; then san_entries+=("IP:$NODE_IP_V6") fi colorized_echo cyan "================================" colorized_echo cyan "Current SAN (Subject Alternative Name) entries:" for entry in "${san_entries[@]}"; do if [[ "$entry" =~ ^DNS: ]]; then colorized_echo green " ✓ DNS: ${entry#DNS:}" elif [[ "$entry" =~ ^IP: ]]; then colorized_echo green " ✓ IP: ${entry#IP:}" fi done colorized_echo cyan "================================" if [ -n "${INSTALL_SAN_ENTRIES:-}" ]; then extra_san="$INSTALL_SAN_ENTRIES" IFS=',' read -ra user_entries <<<"$extra_san" local valid_entries=() local invalid_entries=() for entry in "${user_entries[@]}"; do entry=$(echo "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ -n "$entry" ]; then if validate_san_entry "$entry"; then valid_entries+=("$(normalize_san_entry "$entry")") else invalid_entries+=("$entry") fi fi done if [ ${#invalid_entries[@]} -gt 0 ]; then colorized_echo red "ERROR: Invalid SAN entries provided via argument: ${invalid_entries[*]}" exit 1 fi user_san_entries=("${valid_entries[@]}") elif [ "$AUTO_CONFIRM" = true ]; then : else while true; do # Temporarily disable exit on error for user input set +e colorized_echo cyan "" colorized_echo yellow "You can add additional SAN entries (IP addresses or domain names)." colorized_echo yellow "Examples:" colorized_echo cyan " • IP addresses: 192.168.1.100, 203.0.113.45" colorized_echo cyan " • Domain names: node.example.com, vpn.mydomain.com" colorized_echo cyan " • Wildcard domains: *.example.com" colorized_echo cyan " • IPv6: 2001:db8::1" colorized_echo yellow "" read -rp "Enter additional SAN entries (comma separated), or press ENTER to keep current: " extra_san local read_status=$? set -e # Check if read was interrupted (Ctrl+C) if [ $read_status -ne 0 ]; then colorized_echo yellow "Input cancelled, using default SAN entries only" break fi if [[ -z "$extra_san" ]]; then break fi # Split input by comma and validate each entry IFS=',' read -ra user_entries <<<"$extra_san" local valid_entries=() local invalid_entries=() local skipped_entries=() colorized_echo cyan "Validating SAN entries..." for entry in "${user_entries[@]}"; do # Trim whitespace entry=$(echo "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ -z "$entry" ]; then skipped_entries+=("(empty)") continue fi if validate_san_entry "$entry"; then # Normalize the entry to get the proper format local normalized normalized=$(normalize_san_entry "$entry") valid_entries+=("$normalized") if [[ "$normalized" =~ ^DNS: ]]; then colorized_echo green " ✓ Valid: ${normalized#DNS:} (detected as DNS)" elif [[ "$normalized" =~ ^IP: ]]; then colorized_echo green " ✓ Valid: ${normalized#IP:} (detected as IP)" fi else invalid_entries+=("$entry") colorized_echo red " ✗ Invalid: '$entry'" colorized_echo yellow " → Please enter a valid IP address (e.g., 192.168.1.100) or domain name (e.g., node.example.com)" fi done if [ ${#skipped_entries[@]} -gt 0 ]; then colorized_echo yellow " ⚠ Skipped ${#skipped_entries[@]} empty entry/entries" fi if [ ${#invalid_entries[@]} -gt 0 ]; then colorized_echo red "" colorized_echo red "ERROR: ${#invalid_entries[@]} invalid SAN entry/entries found:" for invalid in "${invalid_entries[@]}"; do colorized_echo red " • '$invalid'" done colorized_echo yellow "" colorized_echo yellow "Valid format examples:" colorized_echo cyan " • IP addresses: 192.168.1.100, 203.0.113.45" colorized_echo cyan " • Domain names: node.example.com, vpn.mydomain.com" colorized_echo cyan " • Wildcard domains: *.example.com" colorized_echo cyan " • IPv6 addresses: 2001:db8::1, ::1" colorized_echo yellow "" colorized_echo yellow "Note: Enter IPs and domains directly (no DNS: or IP: prefix needed)." colorized_echo yellow "The script will automatically detect the type." colorized_echo yellow "" colorized_echo yellow "Please correct the invalid entries and try again." continue fi if [ ${#valid_entries[@]} -gt 0 ]; then user_san_entries=("${valid_entries[@]}") colorized_echo green "" colorized_echo green "✓ Successfully accepted ${#valid_entries[@]} SAN entry/entries" fi break done fi if [ ${#user_san_entries[@]} -gt 0 ]; then san_entries+=("${user_san_entries[@]}") fi # Join SAN entries into a comma-separated string and remove duplicates local san_string san_string=$(printf '%s\n' "${san_entries[@]}" | sort -u | paste -sd, - 2>/dev/null) # Check if san_string was created successfully if [ -z "$san_string" ]; then colorized_echo red "Error: Failed to process SAN entries" exit 1 fi # Display final SAN entries colorized_echo cyan "" colorized_echo cyan "Final SAN entries that will be used:" IFS=',' read -ra final_entries <<<"$san_string" for entry in "${final_entries[@]}"; do if [[ "$entry" =~ ^DNS: ]]; then colorized_echo green " • DNS: ${entry#DNS:}" elif [[ "$entry" =~ ^IP: ]]; then colorized_echo green " • IP: ${entry#IP:}" fi done colorized_echo cyan "" # Generate certificate colorized_echo blue "Generating self-signed certificate..." colorized_echo cyan " Command: openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 ..." if ! command -v openssl >/dev/null 2>&1; then colorized_echo yellow "OpenSSL not found. Attempting to install openssl." detect_os install_package openssl fi local cert_generated=false if openssl_supports_addext; then if generate_self_signed_cert_with_addext "$san_string"; then cert_generated=true fi else colorized_echo yellow " OpenSSL -addext is unavailable; using config-file SAN fallback." if generate_self_signed_cert_with_config "$san_string"; then cert_generated=true fi fi if [ "$cert_generated" = true ]; then # openssl -keyout preserves a pre-existing file's mode, so tighten the # private key to owner-only after every (re)generation. harden_secret_file "$SSL_KEY_FILE" colorized_echo green "✓ Certificate generated successfully!" colorized_echo green " Certificate: $SSL_CERT_FILE" colorized_echo green " Private Key: $SSL_KEY_FILE" else colorized_echo red "✗ Error: Failed to generate certificate" colorized_echo red " Please check that openssl is installed and you have write permissions." exit 1 fi } # Read multiline content or copy a file path into a target file. read_and_save_file() { local prompt_message=$1 local output_file=$2 local allow_file_input=$3 local first_line_read=0 # Check if the file exists before clearing it if [ -f "$output_file" ]; then : >"$output_file" fi colorized_echo cyan "$prompt_message" colorized_echo yellow "Press ENTER on a new line when finished: " while IFS= read -r line; do [[ -z $line ]] && break if [[ "$first_line_read" -eq 0 && "$allow_file_input" -eq 1 && -f "$line" ]]; then first_line_read=1 colorized_echo cyan " Detected file path, copying: $line" cp "$line" "$output_file" break fi echo "$line" >>"$output_file" done } # Replace the image tag of the node service in a compose file (e.g. :latest -> :dev). # Only a tag after the last path segment is replaced (an untagged image gets one), # so a registry port such as registry:5000/pasarguard/node is kept. Needs mikefarah yq v4. # Arguments: # $1 - Image tag (a release tag such as v0.5.4, or dev). # $2 - Path to the docker-compose.yml to edit in place. set_node_image_tag() { local tag="$1" local compose_file="$2" yq eval ".services[\"node\"].image = (.services[\"node\"].image | sub(\"(:[^/:]*)?$\"; \":${tag}\"))" -i "$compose_file" } # Point a fresh install's compose file at the requested image tag. latest is the # downloaded default, so nothing is rewritten; any other tag must be set, or the # install would finish on the wrong image, so a failed rewrite stops the install. # Arguments: # $1 - Requested node version (latest, dev or a release tag). # $2 - Path to the docker-compose.yml to edit in place. set_install_image_tag() { local node_version="$1" local compose_file="$2" [ "$node_version" = "latest" ] && return 0 colorized_echo cyan " Command: yq eval ...image = ...:${node_version}..." if set_node_image_tag "$node_version" "$compose_file"; then colorized_echo green " ✓ Docker image version set to: ${node_version}" else colorized_echo red " ✗ Failed to set the node image tag to ${node_version} (mikefarah yq v4 is required)" exit 1 fi } # Download compose files, set up certificates, configure .env, and deploy the node. install_node() { local node_version=$1 FILES_URL_PREFIX="https://git.nimbaha.org/pgnode/raw/PasarGuard/node/main" COMPOSE_FILES_URL_PREFIX="https://git.nimbaha.org/pgnode/raw/PasarGuard/scripts/main/docker-compose" colorized_echo blue "Creating directories..." colorized_echo cyan " Command: mkdir -p $DATA_DIR $DATA_DIR/certs $APP_DIR" mkdir -p "$DATA_DIR" mkdir -p "$DATA_DIR/certs" # The certs dir holds the TLS private key; keep it owner-only. chmod 700 "$DATA_DIR/certs" 2>/dev/null || true mkdir -p "$APP_DIR" colorized_echo green " ✓ Directories created" colorized_echo cyan "" colorized_echo yellow "A self-signed certificate will be generated by default." if [ "${INSTALL_SELF_SIGNED:-false}" = true ]; then use_public_cert="" elif [ -n "${INSTALL_CERT_PATH:-}" ] && [ -n "${INSTALL_KEY_PATH:-}" ]; then use_public_cert="y" elif [ "$AUTO_CONFIRM" = true ]; then use_public_cert="" else read -r -p "Do you want to use your own public certificate instead? (Y/n): " use_public_cert fi if [[ "$use_public_cert" =~ ^[Yy]$ ]]; then if [ -n "${INSTALL_CERT_PATH:-}" ] && [ -f "$INSTALL_CERT_PATH" ]; then cp "$INSTALL_CERT_PATH" "$SSL_CERT_FILE" colorized_echo blue "Certificate copied to $SSL_CERT_FILE" else read_and_save_file "Please paste the content OR the path to the Client Certificate file." "$SSL_CERT_FILE" 1 colorized_echo blue "Certificate saved to $SSL_CERT_FILE" fi if [ -n "${INSTALL_KEY_PATH:-}" ] && [ -f "$INSTALL_KEY_PATH" ]; then cp "$INSTALL_KEY_PATH" "$SSL_KEY_FILE" colorized_echo blue "Private key copied to $SSL_KEY_FILE" else read_and_save_file "Please paste the content OR the path to the Private Key file." "$SSL_KEY_FILE" 1 colorized_echo blue "Private key saved to $SSL_KEY_FILE" fi # cp/paste create the key with the default umask (0644); restrict it. harden_secret_file "$SSL_KEY_FILE" else gen_self_signed_cert colorized_echo blue "self-signed certificate successfully generated" fi if [ -n "${INSTALL_API_KEY:-}" ]; then API_KEY="$INSTALL_API_KEY" elif [ "$AUTO_CONFIRM" = true ]; then API_KEY="" else read -p "Enter your API Key (must be a valid UUID (any version), leave blank to auto-generate): " -r API_KEY fi if [[ -z "$API_KEY" ]]; then # Generate a valid UUIDv4 API_KEY=$(generate_uuid_v4) colorized_echo green "No API Key provided. A random UUID version 4 has been generated" fi if [ "${INSTALL_USE_REST:-}" = "true" ]; then USE_REST=1 elif [ "${INSTALL_USE_REST:-}" = "false" ]; then USE_REST=0 else if [ "$AUTO_CONFIRM" = true ]; then use_rest="" else read -p "GRPC is recommended by default. Do you want to use REST protocol instead? (y/N): " -r use_rest fi # Default to GRPC (the recommended default) when the user just presses ENTER if [[ "$use_rest" =~ ^[Yy]$ ]]; then USE_REST=1 else USE_REST=0 fi fi get_occupied_ports if [ -n "${INSTALL_SERVICE_PORT:-}" ]; then SERVICE_PORT="$INSTALL_SERVICE_PORT" if is_port_occupied "$SERVICE_PORT"; then colorized_echo red "Port $SERVICE_PORT is already in use." exit 1 fi if ! [[ "$SERVICE_PORT" -ge 1 && "$SERVICE_PORT" -le 65535 ]]; then colorized_echo red "Invalid port. Please enter a port between 1 and 65535." exit 1 fi elif [ "$AUTO_CONFIRM" = true ]; then SERVICE_PORT=62050 if is_port_occupied "$SERVICE_PORT"; then colorized_echo red "Port $SERVICE_PORT is already in use. Run without -y to choose another port." exit 1 fi else # Prompt user to enter the service port, ensuring the selected port is not already in use while true; do read -p "Enter the SERVICE_PORT (default 62050): " -r SERVICE_PORT if [[ -z "$SERVICE_PORT" ]]; then SERVICE_PORT=62050 fi if [[ "$SERVICE_PORT" -ge 1 && "$SERVICE_PORT" -le 65535 ]]; then if is_port_occupied "$SERVICE_PORT"; then colorized_echo red "Port $SERVICE_PORT is already in use. Please enter another port." else break fi else colorized_echo red "Invalid port. Please enter a port between 1 and 65535." fi done fi colorized_echo blue "Fetching .env and compose file" colorized_echo cyan " Command: curl -fsL $FILES_URL_PREFIX/.env.example -o $APP_DIR/.env" # Pre-create .env as 0600 (and tighten any pre-existing copy) so the node # API_KEY written below is never world-readable. harden_secret_file "$APP_DIR/.env" if curl -fsL "$FILES_URL_PREFIX/.env.example" -o "$APP_DIR/.env"; then colorized_echo green " ✓ File saved: $APP_DIR/.env" else colorized_echo red " ✗ Failed to download .env.example" exit 1 fi colorized_echo cyan " Command: curl -fsL $COMPOSE_FILES_URL_PREFIX/node.yml -o $APP_DIR/docker-compose.yml" if curl -fsL "$COMPOSE_FILES_URL_PREFIX/node.yml" -o "$APP_DIR/docker-compose.yml"; then colorized_echo green " ✓ File saved: $APP_DIR/docker-compose.yml" else colorized_echo red " ✗ Failed to download node.yml" exit 1 fi # Modifying .env file sed -i "s/^SERVICE_PORT *= *.*/SERVICE_PORT= ${SERVICE_PORT}/" "$APP_DIR/.env" sed -i "s/^API_KEY *= *.*/API_KEY= ${API_KEY}/" "$APP_DIR/.env" if [ "$USE_REST" -eq 1 ]; then sed -i 's/^# \(SERVICE_PROTOCOL *=.*\)/SERVICE_PROTOCOL= "rest"/' "$APP_DIR/.env" else sed -i 's/^# \(SERVICE_PROTOCOL *=.*\)/SERVICE_PROTOCOL= "grpc"/' "$APP_DIR/.env" fi colorized_echo green ".env file modified successfully" # Modifying compose file colorized_echo blue "Modifying docker-compose.yml..." service_name="node" if [ "$APP_NAME" != "pg-node" ]; then colorized_echo cyan " Command: yq eval ...container_name = \"$APP_NAME\"..." if yq eval ".services[\"$service_name\"].container_name = \"$APP_NAME\"" -i "$APP_DIR/docker-compose.yml" 2>/dev/null; then colorized_echo green " ✓ Container name set to: $APP_NAME" else colorized_echo yellow " ⚠ Failed to set container name (may not be critical)" fi fi container_path="" existing_volume=$(yq eval -r ".services[\"$service_name\"].volumes[0]" "$APP_DIR/docker-compose.yml" 2>/dev/null) if [ -n "$existing_volume" ] && [ "$existing_volume" != "null" ]; then # Extract container path (everything after the colon) if [[ "$existing_volume" == *:* ]]; then container_path="${existing_volume#*:}" else # If no colon found, use the existing volume as container path container_path="$existing_volume" fi fi # For custom names, keep host/container paths aligned to the APP_NAME data dir if [ "$APP_NAME" != "pg-node" ] || [ -z "$container_path" ]; then container_path="$DATA_DIR" fi colorized_echo cyan " Command: yq eval ...volumes[0] = \"${DATA_DIR}:${container_path}\"..." if yq eval ".services[\"$service_name\"].volumes[0] = \"${DATA_DIR}:${container_path}\"" -i "$APP_DIR/docker-compose.yml" 2>/dev/null; then colorized_echo green " ✓ Volume path configured: ${DATA_DIR}:${container_path}" else colorized_echo yellow " ⚠ Failed to configure volume (may not be critical)" fi # Keep SSL paths in .env aligned with the mapped volume (important for node-serviced on host) ssl_cert_env="${container_path}/certs/ssl_cert.pem" ssl_key_env="${container_path}/certs/ssl_key.pem" sed -i "s|^SSL_CERT_FILE *=.*|SSL_CERT_FILE= ${ssl_cert_env}|" "$APP_DIR/.env" sed -i "s|^SSL_KEY_FILE *=.*|SSL_KEY_FILE= ${ssl_key_env}|" "$APP_DIR/.env" set_install_image_tag "$node_version" "$APP_DIR/docker-compose.yml" if ! _load_node_image "$node_version"; then colorized_echo red "✗ Could not prepare the node image — see the error above." colorized_echo red " If the version is missing from the mirror, check https://git.nimbaha.org/pgnode/versions.json" exit 1 fi # Never let compose reach for a registry on an offline host. if command -v yq >/dev/null 2>&1; then yq eval '.services.node.pull_policy = "never"' -i "$APP_DIR/docker-compose.yml" 2>/dev/null || true fi # Final sync to ensure env has the correct SSL paths for custom names sync_env_ssl_paths colorized_echo green "✓ docker-compose.yml modified successfully" } # Remove the pg-node script from /usr/local/bin. uninstall_node_script() { if [ -f "/usr/local/bin/$APP_NAME" ]; then colorized_echo yellow "Removing node script" rm "/usr/local/bin/$APP_NAME" fi } # Remove the node-serviced binary from /usr/local/bin. uninstall_node_service_script() { set_service_paths if [ -f "$SERVICE_BINARY_PATH" ]; then colorized_echo yellow "Removing node-serviced binary" rm "$SERVICE_BINARY_PATH" fi } # Remove the node application directory. uninstall_node() { if [ -d "$APP_DIR" ]; then colorized_echo yellow "Removing directory: $APP_DIR" rm -r "$APP_DIR" fi } # Remove unused pasarguard/node Docker images. uninstall_node_docker_images() { local images images=$(docker images --format '{{.Repository}} {{.ID}}' | awk '$1 ~ /^pasarguard\/node(:|$)/ {print $2}' | sort -u) if [ -z "$images" ]; then colorized_echo yellow "pasarguard/node images not found" return 0 fi colorized_echo yellow "Checking pasarguard/node images for removal..." for image in $images; do if docker ps -a --filter "ancestor=$image" -q | grep -q .; then local container container=$(docker ps -a --filter "ancestor=$image" --format '{{.Names}}' | tr '\n' ' ') colorized_echo yellow "Skipping image $image (still used by: $container)" continue fi if docker rmi "$image" >/dev/null 2>&1; then colorized_echo yellow "Image $image removed" else colorized_echo yellow "Failed to remove image $image" fi done } # Remove the node data and certificates directory. uninstall_node_data_files() { if [ -d "$DATA_DIR" ]; then colorized_echo yellow "Removing directory: $DATA_DIR" rm -r "$DATA_DIR" fi } # Start node Docker Compose services in background. up_node() { compose_up } # Stop and remove node Docker Compose containers. down_node() { compose_down } # Display node Docker Compose logs without following. show_node_logs() { compose_logs } # Follow node Docker Compose logs in real time. follow_node_logs() { compose_logs_follow } # Update shared libraries and the node script from the remote repository. update_node_script() { colorized_echo blue "Updating node script" local backup_dir backup_dir=$(backup_scripts) if ! install_shared_libs_from_repo "$FETCH_REPO" common.sh system.sh docker.sh github.sh; then colorized_echo red "Failed to update shared libraries. Restoring from backup..." restore_scripts "$backup_dir" cleanup_backup "$backup_dir" exit 1 fi if ! github_install_script_from_repo "$FETCH_REPO" "pg-node.sh" "$APP_NAME"; then colorized_echo red "Failed to update node script. Restoring from backup..." restore_scripts "$backup_dir" cleanup_backup "$backup_dir" exit 1 fi cleanup_backup "$backup_dir" colorized_echo green "node script updated successfully" } # Pull latest Docker images for the node services. update_node() { # Offline mirror: `compose pull` always contacts the registry, so reload the # bundled image instead and only fall back to a real pull when it is absent. if ! _load_node_image "${1:-latest}"; then colorized_echo yellow "No bundled image available; falling back to a registry pull" $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" pull fi } # Check if node application directory exists. is_node_installed() { if [ -d $APP_DIR ]; then return 0 else return 1 fi } # Verify that the environment file exists. ensure_env_exists() { if [ ! -f "$ENV_FILE" ]; then colorized_echo red "Environment file not found at $ENV_FILE. Please install the node first." exit 1 fi } # Synchronize SSL certificate and key paths in .env with custom APP_NAME. sync_env_ssl_paths() { # Adjust SSL_CERT_FILE/SSL_KEY_FILE in .env if a custom APP_NAME still points to the default pg-node path if [ "$APP_NAME" = "pg-node" ]; then return fi if [ ! -f "$ENV_FILE" ]; then return fi local desired_cert="${DATA_DIR}/certs/ssl_cert.pem" local desired_key="${DATA_DIR}/certs/ssl_key.pem" local current_cert current_key updated=false current_cert=$(grep -E '^[[:space:]]*SSL_CERT_FILE[[:space:]]*=' "$ENV_FILE" | head -n1 | sed "s/^[[:space:]]*SSL_CERT_FILE[[:space:]]*=[[:space:]]*//;s/[\"']//g") current_key=$(grep -E '^[[:space:]]*SSL_KEY_FILE[[:space:]]*=' "$ENV_FILE" | head -n1 | sed "s/^[[:space:]]*SSL_KEY_FILE[[:space:]]*=[[:space:]]*//;s/[\"']//g") if [[ -z "$current_cert" || "$current_cert" =~ /var/lib/pg-node/ ]]; then sed -i "s|^[[:space:]]*SSL_CERT_FILE[[:space:]]*=.*|SSL_CERT_FILE= ${desired_cert}|" "$ENV_FILE" grep -q '^[[:space:]]*SSL_CERT_FILE[[:space:]]*=' "$ENV_FILE" || echo "SSL_CERT_FILE= ${desired_cert}" >>"$ENV_FILE" updated=true fi if [[ -z "$current_key" || "$current_key" =~ /var/lib/pg-node/ ]]; then sed -i "s|^[[:space:]]*SSL_KEY_FILE[[:space:]]*=.*|SSL_KEY_FILE= ${desired_key}|" "$ENV_FILE" grep -q '^[[:space:]]*SSL_KEY_FILE[[:space:]]*=' "$ENV_FILE" || echo "SSL_KEY_FILE= ${desired_key}" >>"$ENV_FILE" updated=true fi if [ "$updated" = true ]; then colorized_echo cyan "Updated SSL file paths in $ENV_FILE to match APP_NAME ($APP_NAME)." fi } # Check if node Docker containers are created or running. is_node_up() { if [ -z "$($COMPOSE -f $COMPOSE_FILE ps -q -a)" ]; then return 1 else return 0 fi } # Check that a node image version exists as a GitHub release of PasarGuard/node. # Arguments: # $1 - latest, pre-release, dev, or a release tag such as v0.5.4. # Globals: # node_version - for pre-release, set to the newer of the latest stable and latest pre-release tags. # Returns: # 0 if the version can be used, 1 otherwise. check_node_version_exists() { local version=$1 local repo_url="https://git.nimbaha.org/pgnode/api/repos/PasarGuard/node/releases" local repo_list_url="https://git.nimbaha.org/pgnode/api/_node_releases.json" # dev is the Docker image built from the node dev branch, not a GitHub release if [ "$version" == "dev" ]; then return 0 fi if [ "$version" == "latest" ]; then local latest_tag=$(curl -s ${repo_url}/latest | jq -r '.tag_name') # Check if there is any stable release of node v1 if [ "$latest_tag" == "null" ]; then return 1 fi return 0 fi if [ "$version" == "pre-release" ]; then local latest_stable_tag=$(curl -s "$repo_url/latest" | jq -r '.tag_name') local latest_pre_release_tag=$(curl -s "$repo_list_url" | jq -r '[.[] | select(.prerelease == true)][0].tag_name') # An unreachable or rate-limited API leaves these empty; treat that like "no release" [ -n "$latest_stable_tag" ] || latest_stable_tag="null" [ -n "$latest_pre_release_tag" ] || latest_pre_release_tag="null" if [ "$latest_stable_tag" == "null" ] && [ "$latest_pre_release_tag" == "null" ]; then return 1 # No releases found at all elif [ "$latest_stable_tag" == "null" ]; then node_version=$latest_pre_release_tag elif [ "$latest_pre_release_tag" == "null" ]; then node_version=$latest_stable_tag else # sort -V puts v1.0.0-rc.1 after v1.0.0, which would pick an older release # candidate over the stable release it led to. Compare the base versions # (the part before the first "-") instead: a pre-release wins only when its # base is newer, so the result does not depend on which sort -V is installed. local stable_base="${latest_stable_tag%%-*}" local pre_release_base="${latest_pre_release_tag%%-*}" local newest_base newest_base=$(printf "%s\n" "$stable_base" "$pre_release_base" | sort -V | tail -n 1) if [ "$pre_release_base" != "$stable_base" ] && [ "$newest_base" = "$pre_release_base" ]; then node_version=$latest_pre_release_tag else node_version=$latest_stable_tag fi fi return 0 fi # Check if the repos contains the version tag # Offline mirror: only the mirrored tags exist locally, so do not probe # the API for an arbitrary tag. _load_node_image fails with a clear message # if the requested version was not mirrored. if true; then return 0 else return 1 fi } # Validate node_version (format, then GitHub release) and print why it cannot be used. # Globals: # node_version - the requested version; pre-release is resolved to a release tag. # Returns: # 0 if the version can be used, 1 otherwise. validate_node_version() { if ! [[ "$node_version" == "latest" || "$node_version" == "pre-release" || "$node_version" == "dev" || "$node_version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then colorized_echo red "✗ Invalid version format. Please enter a valid version (e.g. v1.0.0)" return 1 fi if ! check_node_version_exists "$node_version"; then colorized_echo red "✗ Version $node_version does not exist. Please enter a valid version (e.g. v0.1.2)" return 1 fi # Never hand an empty or malformed resolved tag to the compose rewrite local release_tag_regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' if ! [[ "$node_version" == "latest" || "$node_version" == "dev" || "$node_version" =~ $release_tag_regex ]]; then colorized_echo red "✗ Could not resolve a node release tag (got '$node_version'). Check that api.github.com is reachable." return 1 fi return 0 } # Warn that the dev image is an unreleased build and how to leave it. print_dev_image_note() { colorized_echo yellow "Note: pasarguard/node:dev is an unreleased build of the node dev branch, meant for testing only." colorized_echo yellow " '$APP_NAME update' pulls the newest dev build; '$APP_NAME update --version latest' returns to the latest release." } # Execute node installation workflow with option parsing and setup. install_command() { check_running_as_root print_script_execution_header "pg-node" "$SCRIPT_COMMIT_SHA" "install" # Default values node_version="latest" node_version_set="false" # Parse options while [[ $# -gt 0 ]]; do key="$1" case $key in -v | --version) if [[ "$node_version_set" == "true" ]]; then colorized_echo red "Error: Cannot use --pre-release, --dev and --version options simultaneously." exit 1 fi node_version="$2" node_version_set="true" shift 2 ;; --pre-release) if [[ "$node_version_set" == "true" ]]; then colorized_echo red "Error: Cannot use --pre-release, --dev and --version options simultaneously." exit 1 fi node_version="pre-release" node_version_set="true" shift ;; --dev) if [[ "$node_version_set" == "true" ]]; then colorized_echo red "Error: Cannot use --pre-release, --dev and --version options simultaneously." exit 1 fi node_version="dev" node_version_set="true" shift ;; --name) # --name is handled globally; ignore here to prevent unknown option errors shift 2 ;; --override) INSTALL_OVERRIDE=true shift ;; --api-key) INSTALL_API_KEY="$2" shift 2 ;; --use-rest) INSTALL_USE_REST=true shift ;; --use-grpc) INSTALL_USE_REST=false shift ;; --service-port) INSTALL_SERVICE_PORT="$2" shift 2 ;; --cert-path) INSTALL_CERT_PATH="$2" shift 2 ;; --key-path) INSTALL_KEY_PATH="$2" shift 2 ;; --self-signed) INSTALL_SELF_SIGNED=true shift ;; --api-port) INSTALL_API_PORT="$2" shift 2 ;; --install-service) INSTALL_SERVICE_CHOICE="y" shift ;; --no-install-service) INSTALL_SERVICE_CHOICE="n" shift ;; --san-entries) INSTALL_SAN_ENTRIES="$2" shift 2 ;; *) echo "Unknown option: $1" exit 1 ;; esac done # Check if node is already installed if is_node_installed; then colorized_echo red "node is already installed at $APP_DIR" if [ "${INSTALL_OVERRIDE:-false}" = true ] || [ "$AUTO_CONFIRM" = true ]; then REPLY="y" else read -p "Do you want to override the previous installation? (y/n) " fi if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo red "Aborted installation" exit 1 fi fi detect_os if ! command -v jq >/dev/null 2>&1; then install_package jq fi if ! command -v curl >/dev/null 2>&1; then install_package curl fi if ! command -v docker >/dev/null 2>&1; then install_docker fi ensure_docker_compose if ! command -v yq >/dev/null 2>&1; then install_yq fi detect_compose # Check if the version is valid and exists if ! validate_node_version; then exit 1 fi colorized_echo cyan "================================" colorized_echo cyan "Installing PasarGuard Node" colorized_echo cyan "Version: $node_version" colorized_echo cyan "================================" if [ "$node_version" == "dev" ]; then print_dev_image_note fi install_node "$node_version" colorized_echo green "✓ Node installation completed for version: $node_version" install_node_script install_completion up_node show_node_logs local install_service_choice="" if [ -n "${INSTALL_SERVICE_CHOICE:-}" ]; then install_service_choice="$INSTALL_SERVICE_CHOICE" elif [ "$AUTO_CONFIRM" = true ]; then install_service_choice="y" else read -p "Do you want to install and start the systemd service for $APP_NAME? (Y/n): " install_service_choice fi if [[ -z "$install_service_choice" || "$install_service_choice" =~ ^[Yy]$ ]]; then install_service_command else colorized_echo yellow "Skipped installing systemd service for $APP_NAME." fi colorized_echo blue "================================" colorized_echo magenta " node is set up with the following IP: $NODE_IP and Port: $SERVICE_PORT." colorized_echo magenta "Please use the following Certificate in pasarguard Panel (it's located in ${DATA_DIR}/certs):" cat "$SSL_CERT_FILE" colorized_echo blue "================================" colorized_echo magenta "Next, use the API Key (UUID v4) in pasarguard Panel: " colorized_echo red "${API_KEY}" } # Uninstall node containers, configuration, scripts, and optionally data directories. uninstall_command() { check_running_as_root # Check if node is installed if ! is_node_installed; then colorized_echo red "node not installed!" exit 1 fi if [ "$AUTO_CONFIRM" = true ]; then REPLY="y" else read -p "Do you really want to uninstall node? (y/n) " fi if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo red "Aborted" exit 1 fi detect_compose if is_node_up; then down_node fi if service_installed; then uninstall_service_command fi uninstall_completion uninstall_node_script uninstall_node uninstall_node_docker_images if [ "$AUTO_CONFIRM" = true ]; then REPLY="y" else read -p "Do you want to remove node data files too ($DATA_DIR)? (y/n) " fi if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo green "node uninstalled successfully" else uninstall_node_data_files colorized_echo green "node uninstalled successfully" fi } # Start node services and optionally stream container logs. up_command() { # Display help message for up command options. help() { colorized_echo red "Usage: node up [options]" echo "" echo "OPTIONS:" echo " -h, --help display this help message" echo " -n, --no-logs do not follow logs after starting" } local no_logs=false while [[ "$#" -gt 0 ]]; do case "$1" in -n | --no-logs) no_logs=true ;; -h | --help) help exit 0 ;; *) echo "Error: Invalid option: $1" >&2 help exit 0 ;; esac shift done # Check if node is installed if ! is_node_installed; then colorized_echo red "node's not installed!" exit 1 fi detect_compose if is_node_up; then colorized_echo red "node's already up" exit 1 fi up_node if [ "$no_logs" = false ]; then follow_node_logs fi } # Stop running node services. down_command() { # Check if node is installed if ! is_node_installed; then colorized_echo red "node not installed!" exit 1 fi detect_compose if ! is_node_up; then colorized_echo red "node already down" exit 1 fi down_node } # Restart node services and restart systemd service if present. restart_command() { # Display help message for restart command options. help() { colorized_echo red "Usage: node restart [options]" echo echo "OPTIONS:" echo " -h, --help display this help message" echo " -n, --no-logs do not follow logs after starting" echo " --no-restart-service do not restart the systemd service (if installed)" } local no_logs=false local no_restart_service=false while [[ "$#" -gt 0 ]]; do case "$1" in -n | --no-logs) no_logs=true ;; --no-restart-service) no_restart_service=true ;; -h | --help) help exit 0 ;; *) echo "Error: Invalid option: $1" >&2 help exit 1 ;; esac shift done # Check if node is installed if ! is_node_installed; then colorized_echo red "node not installed!" exit 1 fi detect_compose down_node up_node if [ "$no_restart_service" = false ]; then restart_service_if_installed else colorized_echo yellow "Skipped restarting $SERVICE_NAME (due to --no-restart-service)" fi if [ "$no_logs" = false ]; then follow_node_logs fi } # Configure, install, and start the node systemd service unit. install_service_command() { check_running_as_root require_systemd set_service_paths while [[ $# -gt 0 ]]; do case "$1" in --api-port) INSTALL_API_PORT="$2" shift 2 ;; *) echo "Unknown option: $1" exit 1 ;; esac done detect_os if ! command -v jq >/dev/null 2>&1; then install_package jq fi if ! is_node_installed; then colorized_echo red "node not installed! Install it before setting up the service." exit 1 fi ensure_env_exists sync_env_ssl_paths get_occupied_ports local api_port existing_api_port="" local default_api_port=62051 if existing_api_port=$(grep -E '^API_PORT[[:space:]]*=' "$ENV_FILE" | head -n1 | sed 's/^API_PORT[[:space:]]*=[[:space:]]*//'); then existing_api_port=$(echo "$existing_api_port" | tr -d '"'\') fi if [[ "$existing_api_port" =~ ^[0-9]+$ ]] && [ "$existing_api_port" -ge 1 ] && [ "$existing_api_port" -le 65535 ]; then colorized_echo blue "Existing API_PORT found in $ENV_FILE: $existing_api_port" default_api_port="$existing_api_port" fi if [ -n "${INSTALL_API_PORT:-}" ]; then api_port="$INSTALL_API_PORT" if is_port_occupied "$api_port"; then colorized_echo red "Port $api_port is already in use." exit 1 fi if ! [[ "$api_port" =~ ^[0-9]+$ && "$api_port" -ge 1 && "$api_port" -le 65535 ]]; then colorized_echo red "Invalid port. Please enter a port between 1 and 65535." exit 1 fi elif [ "$AUTO_CONFIRM" = true ]; then api_port="$default_api_port" if is_port_occupied "$api_port"; then colorized_echo red "Port $api_port is already in use. Run without -y to choose another port." exit 1 fi else while true; do read -p "Enter the API_PORT for node service (default ${default_api_port}): " -r api_port if [[ -z "$api_port" ]]; then api_port="$default_api_port" fi if [[ "$api_port" =~ ^[0-9]+$ && "$api_port" -ge 1 && "$api_port" -le 65535 ]]; then if is_port_occupied "$api_port"; then colorized_echo red "Port $api_port is already in use. Please enter another port." else break fi else colorized_echo red "Invalid port. Please enter a port between 1 and 65535." fi done fi local api_port_comment="# API_PORT is used by the node service API ($APP_NAME)" if grep -q '^API_PORT[[:space:]]*=' "$ENV_FILE"; then sed -i "s/^API_PORT[[:space:]]*=.*/API_PORT= ${api_port}/" "$ENV_FILE" if ! grep -q '^# *API_PORT' "$ENV_FILE"; then sed -i "/^API_PORT[[:space:]]*=.*/i ${api_port_comment}" "$ENV_FILE" fi else { echo "" echo "$api_port_comment" echo "API_PORT= ${api_port}" } >>"$ENV_FILE" fi colorized_echo magenta "API_PORT selected: ${api_port}" configure_firewall_for_port "$api_port" "tcp" install_node_service_script colorized_echo blue "Creating systemd unit at $SERVICE_UNIT" cat >"$SERVICE_UNIT" </dev/null 2>&1 || true systemctl disable "$SERVICE_NAME" >/dev/null 2>&1 || true if [ -f "$SERVICE_UNIT" ]; then colorized_echo yellow "Removing systemd unit $SERVICE_UNIT" rm "$SERVICE_UNIT" fi uninstall_node_service_script systemctl daemon-reload colorized_echo green "$SERVICE_NAME service uninstalled." } # Start the node systemd service unit. service_start_command() { check_running_as_root require_systemd if ! service_installed; then colorized_echo red "Service not installed. Run service-install first." exit 1 fi systemctl start "$SERVICE_NAME" colorized_echo green "$SERVICE_NAME service started." } # Stop the running node systemd service unit. service_stop_command() { check_running_as_root require_systemd if ! service_installed; then colorized_echo red "Service not installed. Run service-install first." exit 1 fi systemctl stop "$SERVICE_NAME" colorized_echo green "$SERVICE_NAME service stopped." } # Update the node-serviced binary and restart the systemd service. service_update_command() { check_running_as_root require_systemd if ! service_installed; then colorized_echo red "Service not installed. Run service-install first." exit 1 fi install_node_service_script systemctl daemon-reload systemctl restart "$SERVICE_NAME" colorized_echo green "$SERVICE_NAME service updated and restarted." } # Display or follow journalctl logs for the node systemd service. service_logs_command() { require_systemd if ! service_installed; then colorized_echo red "Service not installed. Run service-install first." exit 1 fi local no_follow=false while [[ "$#" -gt 0 ]]; do case "$1" in -n | --no-follow) no_follow=true ;; -h | --help) colorized_echo red "Usage: $APP_NAME service-logs [options]" echo " -n, --no-follow Show logs without following" exit 0 ;; *) echo "Error: Invalid option: $1" >&2 exit 1 ;; esac shift done if [ "$no_follow" = true ]; then journalctl -u "$SERVICE_NAME" --no-pager else journalctl -u "$SERVICE_NAME" -f fi } # Restart the node systemd service. restart_service_command() { check_running_as_root require_systemd if ! service_installed; then colorized_echo red "Service not installed. Run service-install first." exit 1 fi restart_service_if_installed } # Display status of the node systemd service via systemctl. status_service_command() { require_systemd if ! service_installed; then colorized_echo red "Service not installed. Run service-install first." exit 1 fi systemctl status --no-pager "$SERVICE_NAME" } # Display status and individual container states of the node services. status_command() { # Check if node is installed if ! is_node_installed; then echo -n "Status: " colorized_echo red "Not Installed" exit 1 fi detect_compose if ! is_node_up; then echo -n "Status: " colorized_echo blue "Down" exit 1 fi echo -n "Status: " colorized_echo green "Up" json=$($COMPOSE -f $COMPOSE_FILE ps -a --format=json) services=$(echo "$json" | jq -r 'if type == "array" then .[] else . end | .Service') states=$(echo "$json" | jq -r 'if type == "array" then .[] else . end | .State') # Print out the service names and statuses for i in $(seq 0 $(expr $(echo $services | wc -w) - 1)); do service=$(echo $services | cut -d' ' -f $(expr $i + 1)) state=$(echo $states | cut -d' ' -f $(expr $i + 1)) echo -n "- $service: " if [ "$state" == "running" ]; then colorized_echo green $state else colorized_echo red $state fi done } # Display or follow Docker Compose logs for the node. logs_command() { # Display help message for logs command options. help() { colorized_echo red "Usage: node logs [options]" echo "" echo "OPTIONS:" echo " -h, --help display this help message" echo " -n, --no-follow do not show follow logs" } local no_follow=false while [[ "$#" -gt 0 ]]; do case "$1" in -n | --no-follow) no_follow=true ;; -h | --help) help exit 0 ;; *) echo "Error: Invalid option: $1" >&2 help exit 0 ;; esac shift done # Check if node is installed if ! is_node_installed; then colorized_echo red "node's not installed!" exit 1 fi detect_compose if ! is_node_up; then colorized_echo red "node is not up." exit 1 fi if [ "$no_follow" = true ]; then show_node_logs else follow_node_logs fi } # Update node script, completions, container images, and restart services. update_command() { check_running_as_root local no_update_service=false # Image tag to switch to (--dev or --version); empty keeps the tag in the compose file local node_version="" local node_version_set="false" # Parse args while [[ "$#" -gt 0 ]]; do case "$1" in --no-update-service) no_update_service=true shift ;; --dev) if [[ "$node_version_set" == "true" ]]; then colorized_echo red "Error: Cannot use --dev and --version options simultaneously." exit 1 fi node_version="dev" node_version_set="true" shift ;; -v | --version) if [[ "$node_version_set" == "true" ]]; then colorized_echo red "Error: Cannot use --dev and --version options simultaneously." exit 1 fi if [[ -z "${2:-}" ]]; then colorized_echo red "Error: --version requires a value." exit 1 fi node_version="$2" node_version_set="true" shift 2 ;; *) break ;; esac done # Check if node is installed if ! is_node_installed; then colorized_echo red "node not installed!" exit 1 fi if [ "$node_version_set" = true ]; then if ! command -v jq >/dev/null 2>&1; then detect_os install_package jq fi if ! command -v yq >/dev/null 2>&1; then install_yq fi if ! validate_node_version; then exit 1 fi fi detect_compose update_node_script uninstall_completion install_completion if [ "$node_version_set" = true ]; then # Only the image tag changes; .env, the API key and certificates stay as they are colorized_echo blue "Switching node image to: $node_version" if [ "$node_version" == "dev" ]; then print_dev_image_note fi if ! set_node_image_tag "$node_version" "$COMPOSE_FILE"; then colorized_echo red "✗ Failed to set the node image tag in $COMPOSE_FILE (mikefarah yq v4 is required)" exit 1 fi colorized_echo green "✓ Docker image version set to: $node_version" fi colorized_echo blue "Pulling latest version" update_node colorized_echo blue "Restarting node services" down_node up_node if [ "$no_update_service" = false ]; then update_service_if_installed else colorized_echo yellow "Skipped updating $SERVICE_NAME (due to --no-update-service)" fi colorized_echo blue "node updated successfully" } # Function to update the Xray core get_xray_core() { local requested_version="${1:-}" identify_the_operating_system_and_architecture if ! command -v curl >/dev/null 2>&1; then colorized_echo yellow "curl is required. Attempting to install curl." detect_os install_package curl fi # Systemd/non-TTY environments may not have TERM set; ignore clear failures to avoid exiting under set -e safe_clear() { clear 2>/dev/null || true; } safe_clear # Validate whether a specified Xray-core version exists on GitHub. validate_version() { local version="$1" local response local curl_exit_code # Use curl with timeout and error handling response=$(curl -s --max-time 10 --connect-timeout 5 "https://git.nimbaha.org/pgnode/api/repos/XTLS/Xray-core/releases/tags/$version" 2>&1) curl_exit_code=$? # Check if curl failed (network error, timeout, etc.) if [ $curl_exit_code -ne 0 ] || [ -z "$response" ]; then echo -e "\033[1;31mError: Failed to validate version. Network error or GitHub API unavailable.\033[0m" >&2 echo "network_error" return fi # Check if version exists if echo "$response" | grep -q '"message": "Not Found"'; then echo "invalid" else echo "valid" fi } # Display the interactive selection menu for available Xray-core versions. print_menu() { safe_clear echo -e "\033[1;32m==============================\033[0m" echo -e "\033[1;32m Xray-core Installer \033[0m" echo -e "\033[1;32m==============================\033[0m" current_version=$(get_current_xray_core_version) echo -e "\033[1;33m>>>> Current Xray-core version: \033[1;1m$current_version\033[0m" echo -e "\033[1;32m==============================\033[0m" echo -e "\033[1;33mAvailable Xray-core versions:\033[0m" for ((i = 0; i < ${#versions[@]}; i++)); do echo -e "\033[1;34m$((i + 1)):\033[0m ${versions[i]}" done echo -e "\033[1;32m==============================\033[0m" echo -e "\033[1;35mM:\033[0m Enter a version manually" echo -e "\033[1;31mQ:\033[0m Quit" echo -e "\033[1;32m==============================\033[0m" } latest_releases=$(curl -s "https://git.nimbaha.org/pgnode/api/repos/XTLS/Xray-core/releases?per_page=$LAST_XRAY_CORES") versions=($(echo "$latest_releases" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')) if [ ${#versions[@]} -eq 0 ]; then echo -e "\033[1;31mNo Xray-core releases found.\033[0m" exit 1 fi if [[ -n "$requested_version" ]]; then if [[ "$requested_version" == "latest" ]]; then selected_version=${versions[0]} else local validation_result validation_result=$(validate_version "$requested_version") if [ "$validation_result" == "valid" ]; then selected_version="$requested_version" elif [ "$validation_result" == "network_error" ]; then echo -e "\033[1;31mError: Failed to validate version due to network error. Please check your internet connection and try again.\033[0m" >&2 exit 1 else echo -e "\033[1;31mInvalid version or version does not exist: $requested_version. Please try again.\033[0m" >&2 exit 1 fi fi elif [ "$AUTO_CONFIRM" = true ]; then selected_version=${versions[0]} else while true; do print_menu read -p "Choose a version to install (1-${#versions[@]}), or press M to enter manually, Q to quit: " choice if [[ "$choice" =~ ^[1-9][0-9]*$ ]] && [ "$choice" -le "${#versions[@]}" ]; then choice=$((choice - 1)) selected_version=${versions[choice]} break elif [ "$choice" == "M" ] || [ "$choice" == "m" ]; then while true; do read -p "Enter the version manually (e.g., v1.2.3): " custom_version if [ "$(validate_version "$custom_version")" == "valid" ]; then selected_version="$custom_version" break 2 else echo -e "\033[1;31mInvalid version or version does not exist. Please try again.\033[0m" fi done elif [ "$choice" == "Q" ] || [ "$choice" == "q" ]; then echo -e "\033[1;31mExiting.\033[0m" exit 0 else echo -e "\033[1;31mInvalid choice. Please try again.\033[0m" sleep 2 fi done fi echo -e "\033[1;32mSelected version $selected_version for installation.\033[0m" if ! command -v unzip >/dev/null 2>&1; then echo -e "\033[1;33mInstalling required packages...\033[0m" detect_os install_package unzip fi mkdir -p "$DATA_DIR/xray-core" cd "$DATA_DIR/xray-core" xray_filename="Xray-linux-$ARCH.zip" xray_download_url="https://git.nimbaha.org/pgnode/github/XTLS/Xray-core/releases/download/${selected_version}/${xray_filename}" echo -e "\033[1;33mDownloading Xray-core version ${selected_version}...\033[0m" curl -fsSL "$xray_download_url" -o "$xray_filename" || die "Failed to download Xray-core from $xray_download_url" echo -e "\033[1;33mExtracting Xray-core...\033[0m" unzip -o "$xray_filename" >/dev/null 2>&1 || die "Failed to extract $xray_filename" rm -f "$xray_filename" } # Retrieve the currently installed Xray-core version from binary or running container. get_current_xray_core_version() { XRAY_BINARY="$DATA_DIR/xray-core/xray" if [ -f "$XRAY_BINARY" ]; then version_output=$("$XRAY_BINARY" -version 2>/dev/null) if [ $? -eq 0 ]; then version=$(echo "$version_output" | head -n1 | awk '{print $2}') echo "$version" return fi fi # If local binary is not found or failed, check in the Docker container CONTAINER_NAME="$APP_NAME" if docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then version_output=$(docker exec "$CONTAINER_NAME" xray -version 2>/dev/null) if [ $? -eq 0 ]; then # Extract the version number from the first line version=$(echo "$version_output" | head -n1 | awk '{print $2}') echo "$version (in container)" return fi fi echo "Not installed" } # Download, extract, configure, and install a chosen Xray-core binary release. update_core_command() { check_running_as_root local core_version_arg="" while [[ $# -gt 0 ]]; do case "$1" in -v | --version) if [[ -z "${2:-}" ]]; then colorized_echo red "Error: --version requires a value." exit 1 fi core_version_arg="$2" shift 2 ;; -h | --help) colorized_echo red "Usage: node core-update [--version VERSION]" echo " --version VERSION Install a specific Xray-core version (use 'latest' for newest release)" exit 0 ;; *) echo "Unknown option: $1" exit 1 ;; esac done get_xray_core "$core_version_arg" # Ensure volumes match DATA_DIR when custom name is used service_name="node" existing_volume=$(yq eval -r ".services[\"$service_name\"].volumes[0]" "$APP_DIR/docker-compose.yml") if [ -n "$existing_volume" ] && [ "$existing_volume" != "null" ]; then # Extract container path (everything after the colon) if [[ "$existing_volume" == *:* ]]; then container_path="${existing_volume#*:}" else # If no colon found, use the existing volume as container path container_path="$existing_volume" fi # Update volumes to use DATA_DIR (which is based on APP_NAME) yq eval ".services[\"$service_name\"].volumes[0] = \"${DATA_DIR}:${container_path}\"" -i "$APP_DIR/docker-compose.yml" # Set XRAY_EXECUTABLE_PATH to the container path, not host path sed -i "s|^# *XRAY_EXECUTABLE_PATH *=.*|XRAY_EXECUTABLE_PATH= ${container_path}/xray-core/xray|" "$APP_DIR/.env" grep -q '^XRAY_EXECUTABLE_PATH=' "$APP_DIR/.env" || echo "XRAY_EXECUTABLE_PATH= ${container_path}/xray-core/xray" >>"$APP_DIR/.env" else # Fallback to APP_NAME-based path if no volume mapping is detected local fallback_path="${DATA_DIR}/xray-core/xray" sed -i "s|^# *XRAY_EXECUTABLE_PATH *=.*|XRAY_EXECUTABLE_PATH= ${fallback_path}|" "$APP_DIR/.env" grep -q '^XRAY_EXECUTABLE_PATH=' "$APP_DIR/.env" || echo "XRAY_EXECUTABLE_PATH= ${fallback_path}" >>"$APP_DIR/.env" fi # Restart node colorized_echo red "Restarting node..." restart_command -n --no-restart-service colorized_echo blue "Installation of XRAY-CORE version $selected_version completed." } # Open docker-compose.yml in default editor. edit_command() { detect_os check_editor if [ -f "$COMPOSE_FILE" ]; then $EDITOR "$COMPOSE_FILE" else colorized_echo red "Compose file not found at $COMPOSE_FILE" exit 1 fi } # Open .env in default editor. edit_env_command() { detect_os check_editor if [ -f "$ENV_FILE" ]; then $EDITOR "$ENV_FILE" else colorized_echo red "Environment file not found at $ENV_FILE" exit 1 fi } # Generate bash auto-completion definition script. generate_bash_completion() { cat <<'EOF' _node_completions() { local cur cmds COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" cmds="up down restart status logs install update uninstall install-script uninstall-script core-update geofiles renew-cert version-script script-version edit edit-env completion service-install service-uninstall service-restart service-status service-logs service-update service-start service-stop" COMPREPLY=( $(compgen -W "$cmds" -- "$cur") ) return 0 } EOF echo "complete -F _node_completions node.sh" echo "complete -F _node_completions $APP_NAME" } # Generate zsh auto-completion definition script. generate_zsh_completion() { cat <"$bash_completion_file" chmod 644 "$bash_completion_file" colorized_echo green "✓ Bash completion installed to $bash_completion_file" mkdir -p "$zsh_completion_dir" generate_zsh_completion >"$zsh_completion_file" chmod 644 "$zsh_completion_file" colorized_echo green "✓ Zsh completion installed to $zsh_completion_file" } # Remove installed bash and zsh completion files. uninstall_completion() { local bash_completion_dir="/etc/bash_completion.d" local bash_completion_file="$bash_completion_dir/$APP_NAME" local zsh_completion_dir="/usr/local/share/zsh/site-functions" local zsh_completion_file="$zsh_completion_dir/_$APP_NAME" if [ -f "$bash_completion_file" ]; then rm "$bash_completion_file" colorized_echo yellow "Bash completion removed from $bash_completion_file" fi if [ -f "$zsh_completion_file" ]; then rm "$zsh_completion_file" colorized_echo yellow "Zsh completion removed from $zsh_completion_file" fi } # Display help and usage information for pg-node worker node commands and options. usage() { colorized_echo blue "================================" colorized_echo magenta " $APP_NAME Node CLI Help" colorized_echo blue "================================" colorized_echo cyan "Usage:" echo " $APP_NAME [command] [options]" echo colorized_echo cyan "Options:" colorized_echo yellow " -y, --yes $(tput sgr0)✓ Use default answers for all prompts" colorized_echo yellow " --name NAME $(tput sgr0)✓ Target a specific node instance" echo colorized_echo cyan "Commands:" colorized_echo yellow " up $(tput sgr0)✓ Start services" colorized_echo yellow " down $(tput sgr0)✓ Stop services" colorized_echo yellow " restart $(tput sgr0)✓ Restart services" colorized_echo yellow " status $(tput sgr0)✓ Show status" colorized_echo yellow " logs $(tput sgr0)✓ Show logs" colorized_echo yellow " install $(tput sgr0)✓ Install/reinstall node" colorized_echo yellow " update $(tput sgr0)✓ Update to latest version" colorized_echo yellow " uninstall $(tput sgr0)✓ Uninstall node" colorized_echo yellow " install-script $(tput sgr0)✓ Install node script" colorized_echo yellow " uninstall-script $(tput sgr0)✓ Uninstall node script" colorized_echo yellow " service-install $(tput sgr0)✓ Install and start pg-node-service (systemd)" colorized_echo yellow " service-uninstall $(tput sgr0)✓ Remove pg-node-service (systemd)" colorized_echo yellow " service-restart $(tput sgr0)✓ Restart pg-node-service (systemd)" colorized_echo yellow " service-status $(tput sgr0)✓ Show pg-node-service status" colorized_echo yellow " service-logs $(tput sgr0)✓ View systemd service logs" colorized_echo yellow " service-update $(tput sgr0)✓ Update pg-node-service script" colorized_echo yellow " service-start $(tput sgr0)✓ Start pg-node-service (systemd)" colorized_echo yellow " service-stop $(tput sgr0)✓ Stop pg-node-service" colorized_echo yellow " edit $(tput sgr0)✓ Edit docker-compose.yml (via nano or vi)" colorized_echo yellow " edit-env $(tput sgr0)✓ Edit .env file (via nano or vi)" colorized_echo yellow " core-update $(tput sgr0)✓ Update/Change Xray core" colorized_echo yellow " geofiles $(tput sgr0)✓ Download geoip and geosite files for specific regions" colorized_echo yellow " renew-cert $(tput sgr0)✓ Regenerate SSL/TLS certificate" colorized_echo yellow " version-script $(tput sgr0)✓ Show script version and commit" colorized_echo yellow " completion $(tput sgr0)✓ Install bash/zsh tab completion" echo colorized_echo cyan "Restart Options:" colorized_echo yellow " -n, --no-logs $(tput sgr0)✓ Do not follow logs after restart" colorized_echo yellow " --no-restart-service $(tput sgr0)✓ Skip restarting systemd service" colorized_echo cyan "Update Options:" colorized_echo yellow " --no-update-service $(tput sgr0)✓ Skip updating systemd service" colorized_echo yellow " --dev $(tput sgr0)✓ Switch to the dev image (unreleased, for testing)" colorized_echo yellow " -v, --version VERSION $(tput sgr0)✓ Switch to a release tag, latest or pre-release" colorized_echo cyan "Uninstall Options:" colorized_echo yellow " -y, --yes $(tput sgr0)✓ Auto-confirm uninstall and data removal" colorized_echo yellow " --name NAME $(tput sgr0)✓ Uninstall specific node instance" colorized_echo cyan "Install Options:" colorized_echo yellow " -v, --version VERSION $(tput sgr0)✓ Install specific version" colorized_echo yellow " --pre-release $(tput sgr0)✓ Install pre-release version" colorized_echo yellow " --dev $(tput sgr0)✓ Install the dev image (unreleased, for testing)" colorized_echo yellow " --name NAME $(tput sgr0)✓ Install with custom name" colorized_echo yellow " --override $(tput sgr0)✓ Override existing installation" colorized_echo yellow " --api-key KEY $(tput sgr0)✓ Set API Key" colorized_echo yellow " --use-rest $(tput sgr0)✓ Use REST protocol instead of GRPC" colorized_echo yellow " --use-grpc $(tput sgr0)✓ Use GRPC protocol (default)" colorized_echo yellow " --service-port PORT $(tput sgr0)✓ Set service port" colorized_echo yellow " --cert-path PATH $(tput sgr0)✓ Set public certificate path" colorized_echo yellow " --key-path PATH $(tput sgr0)✓ Set private key path" colorized_echo yellow " --self-signed $(tput sgr0)✓ Generate self-signed certificate" colorized_echo yellow " --api-port PORT $(tput sgr0)✓ Set API port for node service" colorized_echo yellow " --install-service $(tput sgr0)✓ Install systemd service" colorized_echo yellow " --no-install-service $(tput sgr0)✓ Skip systemd service installation" colorized_echo yellow " --san-entries ENTRIES $(tput sgr0)✓ Add SAN entries (comma separated)" colorized_echo cyan "Core-update Options:" colorized_echo yellow " --version VERSION $(tput sgr0)✓ Update Xray-core to specific version (use 'latest' for newest)" colorized_echo cyan "Service Logs Options:" colorized_echo yellow " -n, --no-follow $(tput sgr0)✓ Show logs once without following" echo colorized_echo cyan "Node Information:" colorized_echo magenta " Node IP: $NODE_IP_V4" local service_port="" local api_key="" if [ -f "$APP_DIR/.env" ]; then service_port=$(grep '^SERVICE_PORT[[:space:]]*=' "$APP_DIR/.env" 2>/dev/null | sed 's/^SERVICE_PORT[[:space:]]*=[[:space:]]*//' || true) api_key=$(grep '^API_KEY[[:space:]]*=' "$APP_DIR/.env" 2>/dev/null | sed 's/^API_KEY[[:space:]]*=[[:space:]]*//' || true) fi colorized_echo magenta " Service port: $service_port" colorized_echo magenta " Cert file path: $SSL_CERT_FILE" colorized_echo magenta " API Key : $api_key" echo current_version=$(get_current_xray_core_version) colorized_echo cyan "Current Xray-core version: " 1 # 1 for bold colorized_echo magenta "$current_version" 1 echo colorized_echo blue "=================================" echo } # Download regional geoip and geosite rule files and restart node services. geofiles_command() { check_running_as_root mkdir -p "$DATA_DIR/assets" local restart_needed=false local args_provided=false if [[ $# -eq 0 ]]; then colorized_echo blue "No region specified, defaulting to Iran geofiles..." set -- "--iran" fi while [[ $# -gt 0 ]]; do case "$1" in --iran) colorized_echo blue "Downloading Iran geofiles..." curl -sL "https://git.nimbaha.org/pgnode/github/Chocolate4U/Iran-v2ray-rules/releases/latest/download/geoip.dat" -o "$DATA_DIR/assets/geoip.dat" curl -sL "https://git.nimbaha.org/pgnode/github/Chocolate4U/Iran-v2ray-rules/releases/latest/download/geosite.dat" -o "$DATA_DIR/assets/geosite.dat" colorized_echo green "Iran geofiles downloaded to $DATA_DIR/assets" restart_needed=true args_provided=true shift ;; --russia) colorized_echo blue "Downloading Russia geofiles..." curl -sL "https://git.nimbaha.org/pgnode/github/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -o "$DATA_DIR/assets/geoip.dat" curl -sL "https://git.nimbaha.org/pgnode/github/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -o "$DATA_DIR/assets/geosite.dat" colorized_echo green "Russia geofiles downloaded to $DATA_DIR/assets" restart_needed=true args_provided=true shift ;; --china) colorized_echo blue "Downloading China geofiles..." curl -sL "https://git.nimbaha.org/pgnode/github/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -o "$DATA_DIR/assets/geoip.dat" curl -sL "https://git.nimbaha.org/pgnode/github/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -o "$DATA_DIR/assets/geosite.dat" colorized_echo green "China geofiles downloaded to $DATA_DIR/assets" restart_needed=true args_provided=true shift ;; *) colorized_echo red "Unknown option: $1" exit 1 ;; esac done if [ "$restart_needed" = true ]; then # Get the container path from the volume mapping service_name="node" existing_volume=$(yq eval -r ".services[\"$service_name\"].volumes[0]" "$APP_DIR/docker-compose.yml") if [ -n "$existing_volume" ] && [ "$existing_volume" != "null" ]; then # Extract container path (everything after the colon) if [[ "$existing_volume" == *:* ]]; then container_path="${existing_volume#*:}" # XRAY_ASSETS_PATH should point to the container path xray_assets_path="${container_path}/assets" else xray_assets_path="$DATA_DIR/assets" fi else xray_assets_path="$DATA_DIR/assets" fi sed -i "s|^# *XRAY_ASSETS_PATH *=.*|XRAY_ASSETS_PATH = $xray_assets_path|" "$ENV_FILE" grep -q '^XRAY_ASSETS_PATH =' "$ENV_FILE" || echo "XRAY_ASSETS_PATH = $xray_assets_path" >> "$ENV_FILE" colorized_echo blue "XRAY_ASSETS_PATH updated in $ENV_FILE" colorized_echo blue "Restarting node services..." restart_command -n --no-restart-service colorized_echo green "Geofiles updated and node restarted." else colorized_echo yellow "No geofiles specified for download." fi } # Backup existing certificates and generate a renewed self-signed TLS certificate. renew_cert_command() { check_running_as_root # Check if node is installed if ! is_node_installed; then colorized_echo red "✗ Node is not installed. Please install node first." exit 1 fi colorized_echo cyan "================================" colorized_echo cyan "Renewing SSL/TLS Certificate" colorized_echo cyan "================================" colorized_echo yellow "This will create a new SSL/TLS certificate for your node." # Check if existing certificate is self-signed (generated by script) local is_self_signed=false if [ -f "$SSL_CERT_FILE" ]; then # Check if certificate is self-signed (subject == issuer) local subject=$(openssl x509 -in "$SSL_CERT_FILE" -noout -subject 2>/dev/null | sed 's/^subject= *//') local issuer=$(openssl x509 -in "$SSL_CERT_FILE" -noout -issuer 2>/dev/null | sed 's/^issuer= *//') if [ "$subject" = "$issuer" ]; then is_self_signed=true fi fi # Only backup if it's a self-signed certificate (generated by script) if [ "$is_self_signed" = true ] && [ -f "$SSL_CERT_FILE" ]; then # Clean up old backups first (keep only the 2 most recent) local cert_backups=($(ls -t "${SSL_CERT_FILE}.backup."* 2>/dev/null | tail -n +3 2>/dev/null)) local key_backups=($(ls -t "${SSL_KEY_FILE}.backup."* 2>/dev/null | tail -n +3 2>/dev/null)) if [ ${#cert_backups[@]} -gt 0 ] || [ ${#key_backups[@]} -gt 0 ]; then colorized_echo blue "Cleaning up old backups (keeping 2 most recent)..." for backup in "${cert_backups[@]}"; do if [ -f "$backup" ]; then rm -f "$backup" 2>/dev/null && colorized_echo cyan " Removed old backup: $(basename "$backup")" fi done for backup in "${key_backups[@]}"; do if [ -f "$backup" ]; then rm -f "$backup" 2>/dev/null && colorized_echo cyan " Removed old backup: $(basename "$backup")" fi done fi # Create new backup local backup_cert="${SSL_CERT_FILE}.backup.$(date +%Y%m%d_%H%M%S)" local backup_key="${SSL_KEY_FILE}.backup.$(date +%Y%m%d_%H%M%S)" colorized_echo blue "Backing up existing self-signed certificate..." cp "$SSL_CERT_FILE" "$backup_cert" 2>/dev/null || true if [ -f "$SSL_KEY_FILE" ]; then cp "$SSL_KEY_FILE" "$backup_key" 2>/dev/null || true fi colorized_echo green " ✓ Backup created: $(basename "$backup_cert")" if [ -f "$backup_key" ]; then colorized_echo green " ✓ Backup created: $(basename "$backup_key")" fi elif [ -f "$SSL_CERT_FILE" ]; then # User-provided certificate - don't backup, just warn colorized_echo yellow "⚠ Existing certificate appears to be user-provided (not self-signed)." colorized_echo yellow " It will be replaced with a new self-signed certificate." if [ "$AUTO_CONFIRM" != true ]; then read -p "Continue? (y/N): " confirm if [[ ! "$confirm" =~ ^[Yy]$ ]]; then colorized_echo yellow "Cancelled." exit 0 fi fi fi # Generate new certificate gen_self_signed_cert # Ask user if they want to restart the node if docker ps --format '{{.Names}}' | grep -q "^$APP_NAME$"; then colorized_echo cyan "" colorized_echo yellow "The node needs to be restarted to apply the new certificate." local restart_choice="" if [ "$AUTO_CONFIRM" = true ]; then restart_choice="n" else read -p "Do you want to restart the node now? (y/N): " restart_choice fi if [[ "$restart_choice" =~ ^[Yy]$ ]]; then colorized_echo blue "Restarting node to apply new certificate..." restart_command -n --no-restart-service colorized_echo green "✓ Node restarted with new certificate" else colorized_echo yellow "Skipped restart. Please restart the node manually to apply the new certificate." colorized_echo yellow "You can restart it later with: $APP_NAME restart" fi fi colorized_echo cyan "" colorized_echo cyan "================================" colorized_echo green "✓ Certificate renewal completed!" colorized_echo cyan "================================" colorized_echo magenta "Please use the following Certificate in pasarguard Panel (it's located in ${DATA_DIR}/certs):" cat "$SSL_CERT_FILE" colorized_echo cyan "================================" restart_command } # Main CLI dispatch handler for pg-node. pg_node_main() { # Bring existing env SSL paths in line with the current APP_NAME (safe no-op if not installed/default) sync_env_ssl_paths case "$1" in install) shift install_command "$@" ;; update) shift update_command "$@" ;; uninstall) uninstall_command ;; up) shift up_command "$@" ;; down) down_command ;; restart) shift restart_command "$@" ;; status) status_command ;; logs) shift logs_command "$@" ;; core-update) shift update_core_command "$@" ;; geofiles) shift geofiles_command "$@" ;; renew-cert) shift renew_cert_command "$@" ;; install-script) install_node_script ;; uninstall-script) uninstall_node_script ;; service-install) shift install_service_command "$@" ;; service-uninstall) uninstall_service_command ;; service-restart) restart_service_command ;; service-status) status_service_command ;; service-logs) shift service_logs_command "$@" ;; service-update) service_update_command ;; service-start) service_start_command ;; service-stop) service_stop_command ;; edit) edit_command ;; edit-env) edit_env_command ;; version-script | script-version) print_script_execution_header "pg-node" "$SCRIPT_COMMIT_SHA" ;; completion) check_running_as_root install_completion colorized_echo cyan "" colorized_echo yellow "To activate completion in this session:" colorized_echo cyan " bash: source /etc/bash_completion.d/$APP_NAME" colorized_echo cyan " zsh : autoload -Uz compinit && compinit" colorized_echo yellow "Or simply restart your terminal." ;; *) usage ;; esac } if [ "${PG_NODE_SOURCE_ONLY:-false}" != "true" ]; then pg_node_main "$@" fi