#!/usr/bin/env bash set -e APP_NAME_FROM_ARG=0 INSTALL_DIR="/opt" NODE_DISCOVERY_BASE="/opt" SKIP_SERVICE_UPDATE=0 INSTALL_MODE_REQUESTED="" NODE_VERSION_REQUESTED="" NODE_VERSION_SET=0 REBECCA_NODE_SCRIPT_FLAVOR="${REBECCA_NODE_SCRIPT_FLAVOR:-binary}" REBECCA_NODE_SCRIPT_SOURCE_FILE="${REBECCA_NODE_SCRIPT_SOURCE_FILE:-rebecca-node-binary.sh}" SCRIPT_PATH="${BASH_SOURCE[0]}" SCRIPT_NAME="$(basename "$SCRIPT_PATH")" SCRIPT_BASENAME="${SCRIPT_NAME%.*}" SCRIPT_DEFAULT_APP_NAME="${REBECCA_NODE_DEFAULT_APP_NAME:-$SCRIPT_BASENAME}" case "$SCRIPT_DEFAULT_APP_NAME" in rebecca-node-binary|@|bash|sh) SCRIPT_DEFAULT_APP_NAME="rebecca-node" ;; esac declare -a DISCOVERED_NODE_PATHS=() declare -a DISCOVERED_NODE_NAMES=() ensure_valid_app_name() { local candidate="${APP_NAME:-$SCRIPT_DEFAULT_APP_NAME}" if ! [[ "$candidate" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then candidate="rebecca-node" echo "Invalid app name detected. Falling back to default: $candidate" fi APP_NAME="$candidate" } set_app_context() { if [ -z "$APP_NAME" ]; then APP_NAME="$SCRIPT_DEFAULT_APP_NAME" fi ensure_valid_app_name if [ -z "${APP_DIR:-}" ] || [ ! -d "$APP_DIR" ]; then if [ -d "$INSTALL_DIR/$APP_NAME" ]; then APP_DIR="$INSTALL_DIR/$APP_NAME" elif [ -d "$INSTALL_DIR/Rebecca-node" ]; then APP_DIR="$INSTALL_DIR/Rebecca-node" else APP_DIR="$INSTALL_DIR/$APP_NAME" fi fi DATA_DIR="/var/lib/$APP_NAME" DATA_MAIN_DIR="/var/lib/$APP_NAME" COMPOSE_FILE="$APP_DIR/docker-compose.yml" BRANCH_FILE="$APP_DIR/.branch" INSTALL_MODE_FILE="$APP_DIR/.install-mode" CERT_FILE="$DATA_DIR/cert.pem" CERT_KEY_FILE="$DATA_DIR/cert.key" ENV_FILE="$APP_DIR/.env" BINARY_BIN_DIR="$APP_DIR/bin" BINARY_NODE="$BINARY_BIN_DIR/rebecca-node" BINARY_METADATA_FILE="$APP_DIR/.binary-release.json" BINARY_SERVICE_UNIT="/etc/systemd/system/${APP_NAME}.service" } while [[ $# -gt 0 ]]; do key="$1" case $key in install|update|uninstall|up|down|restart|status|logs|core-update|install-script|update-script|uninstall-script|edit|script-install|script-update|script-uninstall|help) COMMAND="$1" shift # past argument ;; --mode) if [ -z "${2:-}" ]; then echo "Error: --mode requires docker or binary." exit 1 fi INSTALL_MODE_REQUESTED="${2:-}" shift 2 ;; --binary) INSTALL_MODE_REQUESTED="binary" shift ;; --docker|--dockerized) INSTALL_MODE_REQUESTED="docker" shift ;; --dev) if [ "$NODE_VERSION_SET" -eq 1 ] && [ "$NODE_VERSION_REQUESTED" != "dev" ]; then echo "Error: Cannot use --dev and --version options simultaneously." exit 1 fi NODE_VERSION_REQUESTED="dev" NODE_VERSION_SET=1 shift ;; --version) if [ "$NODE_VERSION_SET" -eq 1 ]; then echo "Error: Cannot use --dev and --version options simultaneously." exit 1 fi if [ -z "${2:-}" ]; then echo "Error: --version requires a value." exit 1 fi NODE_VERSION_REQUESTED="${2:-}" NODE_VERSION_SET=1 shift 2 ;; --name) if [[ "$COMMAND" == "install" || "$COMMAND" == "install-script" || "$COMMAND" == "script-install" ]]; then APP_NAME="$2" APP_NAME_FROM_ARG=1 shift # past argument else echo "Error: --name parameter is only allowed with 'install' or 'install-script' commands." exit 1 fi shift # past value ;; *) shift # past unknown argument ;; esac done # Ask the kernel for the address it would source traffic from; the old # ifconfig.io probe is unreachable from inside Iran. NODE_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}') # If the IPv4 retrieval is empty, attempt to retrieve the IPv6 address if [ -z "$NODE_IP" ]; then NODE_IP=$(ip -6 route get 2606:4700:4700::1111 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}') fi if [ "$APP_NAME_FROM_ARG" -eq 0 ]; then if [ -n "${REBECCA_NODE_APP_NAME:-}" ]; then APP_NAME="$REBECCA_NODE_APP_NAME" elif [[ "$COMMAND" == "install" || "$COMMAND" == "install-script" || "$COMMAND" == "script-install" ]]; then APP_NAME="$SCRIPT_DEFAULT_APP_NAME" elif [ -z "${APP_NAME:-}" ]; then APP_NAME="$SCRIPT_DEFAULT_APP_NAME" fi fi ensure_valid_app_name LAST_XRAY_CORES=5 REBECCA_REPO="${REBECCA_REPO:-rebeccapanel/Rebecca}" REBECCA_REF="${REBECCA_REF:-master}" REBECCA_SCRIPT_BASE_URL_EXPLICIT=0 if [ -n "${REBECCA_SCRIPT_BASE_URL+x}" ]; then REBECCA_SCRIPT_BASE_URL_EXPLICIT=1 fi REBECCA_MIRROR_BASE="${REBECCA_MIRROR_BASE:-https://git.nimbaha.org/rebecca}" REBECCA_SCRIPT_BASE_URL="${REBECCA_SCRIPT_BASE_URL:-${REBECCA_MIRROR_BASE}/scripts/rebecca}" REBECCA_NODE_RELEASE_REPO="${REBECCA_NODE_RELEASE_REPO:-rebeccapanel/Rebecca-node}" REBECCA_NODE_BINARY_DEV_BRANCH="${REBECCA_NODE_BINARY_DEV_BRANCH:-dev}" REBECCA_NODE_BINARY_DEV_RELEASE_TAG="${REBECCA_NODE_BINARY_DEV_RELEASE_TAG:-dev-binaries}" REBECCA_NODE_BINARY_WORKFLOW_NAME="${REBECCA_NODE_BINARY_WORKFLOW_NAME:-binary-build}" REBECCA_NODE_BINARY_ARTIFACT_PREFIX="${REBECCA_NODE_BINARY_ARTIFACT_PREFIX:-rebecca-node-binaries}" DEFAULT_XRAY_CORE_VERSION="${DEFAULT_XRAY_CORE_VERSION:-v26.5.9}" # Default node channel values BRANCH="master" SCRIPT_URL="$REBECCA_SCRIPT_BASE_URL/$REBECCA_NODE_SCRIPT_SOURCE_FILE" colorized_echo() { local color=$1 local text=$2 local style=${3:-0} # Default style is normal case $color in "red") printf "\e[${style};91m${text}\e[0m\n" ;; "green") printf "\e[${style};92m${text}\e[0m\n" ;; "yellow") printf "\e[${style};93m${text}\e[0m\n" ;; "blue") printf "\e[${style};94m${text}\e[0m\n" ;; "magenta") printf "\e[${style};95m${text}\e[0m\n" ;; "cyan") printf "\e[${style};96m${text}\e[0m\n" ;; *) echo "${text}" ;; esac } ui_is_tty() { [ -t 1 ] && [ -z "${NO_COLOR:-}" ] } ui_supports_cursor_motion() { ui_is_tty && [ "${TERM:-dumb}" != "dumb" ] } ui_terminal_columns() { local columns="${COLUMNS:-}" if ! [[ "$columns" =~ ^[0-9]+$ ]] || [ "$columns" -lt 20 ]; then columns="" if command -v tput >/dev/null 2>&1; then columns=$(tput cols 2>/dev/null || true) fi fi if ! [[ "$columns" =~ ^[0-9]+$ ]] || [ "$columns" -lt 20 ]; then columns=80 fi printf "%s" "$columns" } ui_color() { local code="$1" shift || true if ui_is_tty; then printf "\033[%sm%s\033[0m" "$code" "$*" else printf "%s" "$*" fi } ui_line() { ui_color "38;5;39" "────────────────────────────────────────────────────────────" printf "\n" } ui_header() { local title="$1" local subtitle="${2:-}" printf "\n" ui_color "38;5;45;1" "╭──────────────────────────────────────────────────────────╮" printf "\n " ui_color "38;5;231;1" "$title" printf "\n" if [ -n "$subtitle" ]; then printf " " ui_color "38;5;117" "$subtitle" printf "\n" fi ui_color "38;5;45;1" "╰──────────────────────────────────────────────────────────╯" printf "\n" } ui_section() { printf "\n" ui_color "38;5;45;1" "◆ $1" printf "\n" ui_line } ui_status_row() { local label="$1" local value="$2" printf " " ui_color "38;5;245" "$(printf '%-14s' "$label")" ui_color "38;5;231;1" "$value" printf "\n" } ui_menu_item() { local number="$1" local command="$2" local description="$3" local selected="${4:-0}" local columns command_width=20 description_width command_label description_text columns=$(ui_terminal_columns) if [ "$columns" -lt 30 ]; then command_width=$((columns - 10)) fi [ "$command_width" -lt 1 ] && command_width=1 description_width=$((columns - 10 - command_width)) printf -v command_label "%-${command_width}.${command_width}s" "$command" if [ "$description_width" -gt 0 ]; then description_text="${description:0:$description_width}" else description_text="" fi printf " " if [ "$selected" = "1" ]; then ui_color "38;5;16;48;5;45;1" " > " else printf " " fi ui_color "38;5;45;1" "$(printf '%2s' "$number")" printf " " if [ "$selected" = "1" ]; then ui_color "38;5;231;1" "$command_label" ui_color "38;5;231" "$description_text" else ui_color "38;5;231;1" "$command_label" ui_color "38;5;245" "$description_text" fi printf "\n" } ui_menu_category() { printf "\n" ui_color "38;5;117;1" " $1" printf "\n" } ui_clear() { if ui_is_tty; then printf "\033[H\033[2J" fi } ui_read_menu_choice() { local selected="$1" local total="$2" local key rest digits IFS= read -rsn1 key || return 1 case "$key" in "") echo "enter:$selected" return ;; $'\033') rest="" while [ "${#rest}" -lt 8 ] && IFS= read -rsn1 -t 0.05 key; do rest="${rest}${key}" case "$key" in [A-Za-z~]) break ;; esac done case "$rest" in *A) selected=$((selected - 1)) [ "$selected" -lt 1 ] && selected="$total" echo "move:$selected" return ;; *B) selected=$((selected + 1)) [ "$selected" -gt "$total" ] && selected=1 echo "move:$selected" return ;; esac echo "move:$selected" return ;; [0-9]) digits="$key" while IFS= read -rsn1 -t 0.35 rest; do case "$rest" in [0-9]) digits="${digits}${rest}" ;; "") break ;; *) break ;; esac done echo "value:$digits" return ;; q|Q) echo "quit:" return ;; *) IFS= read -r rest || true echo "value:${key}${rest}" return ;; esac } ui_spinner_run() { local message="$1" shift if ! ui_is_tty; then "$@" return $? fi local log_file log_file=$(mktemp) "$@" >"$log_file" 2>&1 & local pid=$! local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏") local i=0 while kill -0 "$pid" >/dev/null 2>&1; do printf "\r" ui_color "38;5;45;1" "${frames[$((i % ${#frames[@]}))]}" printf " %s" "$message" sleep 0.08 i=$((i + 1)) done local status=0 wait "$pid" || status=$? printf "\r\033[K" if [ "$status" -eq 0 ]; then ui_color "38;5;82;1" "✓" printf " %s\n" "$message" rm -f "$log_file" return 0 fi ui_color "38;5;196;1" "✗" printf " %s\n" "$message" tail -n 80 "$log_file" >&2 || true rm -f "$log_file" return "$status" } ensure_env_file() { mkdir -p "$(dirname "$ENV_FILE")" touch "$ENV_FILE" } set_env_value() { local key="$1" local value="$2" value=$(echo "$value" | sed 's/^"//;s/"$//') ensure_env_file if grep -qE "^[[:space:]]*${key}[[:space:]]*=" "$ENV_FILE" 2>/dev/null; then sed -i "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = \"${value}\"|" "$ENV_FILE" else echo "${key} = \"${value}\"" >> "$ENV_FILE" fi } get_env_value() { local key="$1" if [ ! -f "$ENV_FILE" ]; then return fi grep -E "^[[:space:]]*${key}[[:space:]]*=" "$ENV_FILE" 2>/dev/null \ | tail -n 1 \ | sed -E 's/^[^=]+=//; s/^[[:space:]]*//; s/[[:space:]]*$//; s/^"//; s/"$//' } extract_container_name() { local compose_file="$1" if [ ! -f "$compose_file" ]; then return fi local match match=$(grep -m1 "container_name" "$compose_file" 2>/dev/null | awk -F: '{gsub(/["[:space:]]/, "", $2); print $2}') if [ -n "$match" ]; then echo "$match" fi } add_discovered_node_instance() { local dir="$1" local name="$2" local existing for existing in "${DISCOVERED_NODE_PATHS[@]}"; do if [ "$existing" = "$dir" ]; then return fi done if [ -z "$name" ]; then name=$(basename "$dir") fi DISCOVERED_NODE_PATHS+=("$dir") DISCOVERED_NODE_NAMES+=("$name") } discover_node_instances() { DISCOVERED_NODE_PATHS=() DISCOVERED_NODE_NAMES=() while IFS= read -r -d '' compose; do if ! grep -qi "rebeccapanel/rebecca-node" "$compose"; then continue fi local dir name dir=$(dirname "$compose") name=$(extract_container_name "$compose") if [ -z "$name" ]; then name=$(basename "$dir") fi add_discovered_node_instance "$dir" "$name" done < <(find "$NODE_DISCOVERY_BASE" -mindepth 1 -maxdepth 2 -type f -name "docker-compose.yml" -print0 2>/dev/null || true) while IFS= read -r -d '' mode_file; do local dir dir=$(dirname "$mode_file") add_discovered_node_instance "$dir" "$(basename "$dir")" done < <(find "$NODE_DISCOVERY_BASE" -mindepth 1 -maxdepth 2 -type f -name ".install-mode" -print0 2>/dev/null || true) while IFS= read -r -d '' binary_file; do local dir dir=$(dirname "$(dirname "$binary_file")") add_discovered_node_instance "$dir" "$(basename "$dir")" done < <(find "$NODE_DISCOVERY_BASE" -mindepth 2 -maxdepth 3 -type f -path "*/bin/rebecca-node" -print0 2>/dev/null || true) } prompt_node_selection() { discover_node_instances local count=${#DISCOVERED_NODE_PATHS[@]} if [ "$count" -eq 0 ]; then colorized_echo red "No Rebecca-node installations detected under $NODE_DISCOVERY_BASE." colorized_echo yellow "Specify the node with --name or install the node first." exit 1 fi if [ "$count" -eq 1 ]; then APP_NAME="${DISCOVERED_NODE_NAMES[0]}" APP_DIR="${DISCOVERED_NODE_PATHS[0]}" return fi colorized_echo cyan "Select the Rebecca-node instance:" local idx=0 for dir in "${DISCOVERED_NODE_PATHS[@]}"; do local display="${DISCOVERED_NODE_NAMES[$idx]}" printf " %d) %s (%s)\n" $((idx + 1)) "$display" "$dir" idx=$((idx + 1)) done local selection while true; do read -rp "Choice [1-$count]: " selection if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le "$count" ]; then local chosen=$((selection - 1)) APP_NAME="${DISCOVERED_NODE_NAMES[$chosen]}" APP_DIR="${DISCOVERED_NODE_PATHS[$chosen]}" break fi echo "Invalid choice." done } set_app_context set_branch_variables() { local selected_branch="${1:-master}" case "$selected_branch" in dev|development) BRANCH="dev" IMAGE_TAG="dev" DOCKER_IMAGE="rebeccapanel/rebecca-node:dev" ;; *) BRANCH="master" IMAGE_TAG="latest" DOCKER_IMAGE="rebeccapanel/rebecca-node:latest" ;; esac SCRIPT_BRANCH="$BRANCH" if [ "$BRANCH" = "dev" ]; then REBECCA_REF="dev" else REBECCA_REF="${REBECCA_SCRIPT_REF:-master}" fi if [ "${REBECCA_SCRIPT_BASE_URL_EXPLICIT:-0}" != "1" ]; then REBECCA_SCRIPT_BASE_URL="${REBECCA_MIRROR_BASE}/scripts/rebecca" fi SCRIPT_URL="$REBECCA_SCRIPT_BASE_URL/$REBECCA_NODE_SCRIPT_SOURCE_FILE" } prompt_branch_selection() { local question if [[ "$BRANCH" == "dev" ]]; then question="Keep using the dev branch? (Y/n): " else question="Do you want to install Rebecca-node using the dev branch? (y/N): " fi read -p "$question" -r branch_answer if [[ "$BRANCH" == "dev" ]]; then if [[ -z "$branch_answer" || "$branch_answer" =~ ^[Yy]$ ]]; then set_branch_variables dev else set_branch_variables master fi else if [[ "$branch_answer" =~ ^[Yy]$ ]]; then set_branch_variables dev else set_branch_variables master fi fi colorized_echo blue "Selected branch: $BRANCH (image tag: $IMAGE_TAG)" } normalize_install_mode() { local mode mode=$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]') case "$mode" in docker|dockerized|compose) echo "docker" ;; binary|bin|native) echo "binary" ;; "") echo "" ;; *) colorized_echo red "Invalid install mode: $1" >&2 colorized_echo yellow "Valid modes are: docker, binary" >&2 exit 1 ;; esac } script_install_mode() { case "${REBECCA_NODE_SCRIPT_FLAVOR:-docker}" in docker|dockerized|compose) echo "docker" ;; binary|bin|native) echo "binary" ;; mixed|"") echo "" ;; *) colorized_echo red "Invalid node script flavor: $REBECCA_NODE_SCRIPT_FLAVOR" >&2 exit 1 ;; esac } get_install_mode() { if [ -f "$INSTALL_MODE_FILE" ]; then normalize_install_mode "$(tr -d '[:space:]' < "$INSTALL_MODE_FILE")" return fi if [ -x "$BINARY_NODE" ] || [ -f "$BINARY_SERVICE_UNIT" ]; then echo "binary" return fi if [ -f "$COMPOSE_FILE" ]; then echo "docker" return fi local forced_mode forced_mode=$(script_install_mode) if [ -n "$forced_mode" ]; then echo "$forced_mode" return fi echo "docker" } is_binary_install() { [ "$(get_install_mode)" = "binary" ] } select_install_mode() { local requested_mode local forced_mode forced_mode=$(script_install_mode) requested_mode=$(normalize_install_mode "${1:-${REBECCA_NODE_INSTALL_MODE:-}}") if [ -n "$forced_mode" ]; then if [ -n "$requested_mode" ] && [ "$requested_mode" != "$forced_mode" ]; then colorized_echo red "This script is dedicated to ${forced_mode} installs. Use the matching Rebecca-node script for $requested_mode." >&2 exit 1 fi echo "$forced_mode" return fi if [ -n "$requested_mode" ]; then echo "$requested_mode" return fi if [ ! -t 0 ]; then echo "docker" return fi colorized_echo cyan "Select Rebecca-node installation mode:" >&2 colorized_echo yellow " 1) Dockerized" >&2 colorized_echo yellow " 2) Binary (native systemd service, no Docker)" >&2 read -r -p "Install mode [1]: " install_mode_answer case "$install_mode_answer" in 2|binary|Binary|bin|native) echo "binary" ;; ""|1|docker|Docker|dockerized|compose) echo "docker" ;; *) colorized_echo red "Invalid install mode selection." exit 1 ;; esac } prompt_node_name() { if [ "$APP_NAME_FROM_ARG" -eq 1 ] || [ ! -t 0 ]; then return fi local default_name="${APP_NAME:-$SCRIPT_DEFAULT_APP_NAME}" local node_name while true; do read -r -p "Node name [$default_name]: " node_name node_name="${node_name:-$default_name}" if [[ "$node_name" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then APP_NAME="$node_name" APP_DIR="" set_app_context return fi colorized_echo red "Invalid node name. Use letters, numbers, hyphens, or underscores." done } ensure_script_matches_installed_mode() { local forced_mode local installed_mode forced_mode=$(script_install_mode) if [ -z "$forced_mode" ] || [ ! -d "$APP_DIR" ]; then return fi installed_mode=$(get_install_mode) if [ "$installed_mode" != "$forced_mode" ]; then colorized_echo red "This Rebecca-node installation is in ${installed_mode} mode, but ${0##*/} is the ${forced_mode} script." if [ "$installed_mode" = "binary" ]; then colorized_echo yellow "Use the Rebecca-node binary script." else colorized_echo yellow "Use the Rebecca-node Docker script." fi exit 1 fi } select_node_version() { local requested_version="${1:-}" local install_mode="${2:-docker}" if [ -n "$requested_version" ]; then SELECTED_NODE_VERSION="$requested_version" return fi if [ ! -t 0 ]; then SELECTED_NODE_VERSION="latest" return fi colorized_echo cyan "Select Rebecca-node release channel for ${install_mode} mode:" >&2 colorized_echo yellow " 1) latest" >&2 if [ "$install_mode" = "binary" ]; then colorized_echo yellow " 2) dev (latest successful binary build from ${REBECCA_NODE_BINARY_DEV_BRANCH})" >&2 else colorized_echo yellow " 2) dev (Docker image tag dev)" >&2 fi read -r -p "Release channel [1]: " node_version_answer case "$node_version_answer" in 2|dev|Dev) SELECTED_NODE_VERSION="dev" ;; ""|1|latest|Latest|stable|Stable) SELECTED_NODE_VERSION="latest" ;; *) colorized_echo red "Invalid release channel selection." exit 1 ;; esac } select_xray_core_version() { if [ -n "${XRAY_CORE_VERSION:-}" ]; then return fi if [ ! -t 0 ]; then XRAY_CORE_VERSION="$DEFAULT_XRAY_CORE_VERSION" return fi colorized_echo cyan "Select Xray-core version:" colorized_echo yellow " 1) Default ($DEFAULT_XRAY_CORE_VERSION)" colorized_echo yellow " 2) Latest Xray release" colorized_echo yellow " 3) Enter a version manually (for example, v26.5.9)" local choice custom_version while true; do read -r -p "Xray version [1]: " choice case "$choice" in ""|1|default) XRAY_CORE_VERSION="$DEFAULT_XRAY_CORE_VERSION" return ;; 2|latest) XRAY_CORE_VERSION="latest" return ;; 3|manual) read -r -p "Enter Xray version (vX.Y.Z): " custom_version if [[ "$custom_version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then XRAY_CORE_VERSION="$custom_version" return fi colorized_echo red "Invalid version. Use the vX.Y.Z format." ;; *) colorized_echo red "Invalid Xray version selection." ;; esac done } BRANCH="master" IMAGE_TAG="latest" SCRIPT_BRANCH="master" DOCKER_IMAGE="rebeccapanel/rebecca-node:latest" SCRIPT_URL="$REBECCA_SCRIPT_BASE_URL/$REBECCA_NODE_SCRIPT_SOURCE_FILE" if [ -f "$BRANCH_FILE" ]; then saved_branch=$(tr -d '[:space:]' < "$BRANCH_FILE") if [[ -n "$saved_branch" ]]; then set_branch_variables "$saved_branch" else set_branch_variables "$BRANCH" fi else set_branch_variables "$BRANCH" fi check_running_as_root() { if [ "$(id -u)" != "0" ]; then colorized_echo red "This command must be run as root." exit 1 fi } detect_os() { # Detect the operating system if [ -f /etc/lsb-release ]; then OS=$(lsb_release -si) elif [ -f /etc/os-release ]; then OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') elif [ -f /etc/redhat-release ]; then OS=$(cat /etc/redhat-release | awk '{print $1}') elif [ -f /etc/arch-release ]; then OS="Arch" else colorized_echo red "Unsupported operating system" exit 1 fi } remove_broken_xanmod_apt_sources() { local matches matches=$(grep -RIlE 'deb\.xanmod\.org|xanmod\.org' /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null || true) if [ -z "$matches" ]; then return 1 fi colorized_echo yellow "Removing broken XanMod apt source entries" while IFS= read -r file; do [ -n "$file" ] || continue case "$file" in /etc/apt/sources.list) sed -i.bak '/deb\.xanmod\.org/d;/xanmod\.org/d' "$file" ;; /etc/apt/sources.list.d/*) rm -f "$file" ;; esac done <<< "$matches" return 0 } apt_update_with_repo_repair() { local log_file log_file=$(mktemp) if DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a "$PKG_MANAGER" "$@" update -qq >"$log_file" 2>&1; then rm -f "$log_file" return 0 fi cat "$log_file" >&2 if grep -qiE 'deb\.xanmod\.org|xanmod.*release file|does not have a release file' "$log_file" && remove_broken_xanmod_apt_sources; then rm -f "$log_file" DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a "$PKG_MANAGER" "$@" update -qq return fi rm -f "$log_file" return 1 } detect_and_update_package_manager() { if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then PKG_MANAGER="apt-get" ui_spinner_run "Updating package index" apt_update_with_repo_repair elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then PKG_MANAGER="yum" ui_spinner_run "Updating package index" "$PKG_MANAGER" update -y -q ui_spinner_run "Installing EPEL repository" "$PKG_MANAGER" install -y -q epel-release elif [[ "$OS" == "Fedora"* ]]; then PKG_MANAGER="dnf" ui_spinner_run "Updating package index" "$PKG_MANAGER" update -q -y elif [[ "$OS" == "Arch"* ]]; then PKG_MANAGER="pacman" ui_spinner_run "Updating package index" "$PKG_MANAGER" -Sy --noconfirm --quiet elif [[ "$OS" == "openSUSE"* ]]; then PKG_MANAGER="zypper" ui_spinner_run "Updating package index" "$PKG_MANAGER" refresh --quiet else colorized_echo red "Unsupported operating system" exit 1 fi } detect_compose() { # Check if docker compose command exists if docker compose >/dev/null 2>&1; then COMPOSE='docker compose' elif docker-compose >/dev/null 2>&1; then COMPOSE='docker-compose' else colorized_echo red "docker compose not found" exit 1 fi } install_package_impl() { local PACKAGE="$1" if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $PKG_MANAGER -y -qq install "$PACKAGE" \ -o Dpkg::Options::="--force-confdef" \ -o Dpkg::Options::="--force-confold" elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then $PKG_MANAGER install -y -q "$PACKAGE" elif [[ "$OS" == "Fedora"* ]]; then $PKG_MANAGER install -y -q "$PACKAGE" elif [[ "$OS" == "Arch"* ]]; then $PKG_MANAGER -S --noconfirm --quiet "$PACKAGE" elif [[ "$OS" == "openSUSE"* ]]; then PKG_MANAGER="zypper" $PKG_MANAGER --quiet install -y "$PACKAGE" else colorized_echo red "Unsupported operating system" exit 1 fi } _rebecca_install_package () { if [ -z "$PKG_MANAGER" ]; then detect_and_update_package_manager fi local PACKAGE="$1" ui_spinner_run "Installing $PACKAGE" install_package_impl "$PACKAGE" } ensure_ov_binary_prerequisites() { detect_os local packages=() if ! command -v openvpn >/dev/null 2>&1; then packages+=("openvpn") fi if ! command -v nft >/dev/null 2>&1; then packages+=("nftables") fi if ! command -v ip >/dev/null 2>&1; then if [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]] || [[ "$OS" == "Fedora"* ]]; then packages+=("iproute") else packages+=("iproute2") fi fi for package in "${packages[@]}"; do install_package "$package" done if command -v modprobe >/dev/null 2>&1 && [ ! -c /dev/net/tun ]; then modprobe tun >/dev/null 2>&1 || colorized_echo yellow "Unable to load tun module automatically; OpenVPN needs /dev/net/tun." fi } ensure_haproxy_prerequisites() { if ! command -v haproxy >/dev/null 2>&1; then detect_os install_package haproxy fi } ensure_python3_venv() { detect_os if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then PY_VER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "3") install_package "python${PY_VER}-venv" || install_package python3-venv else install_package python3-venv fi } install_docker() { # Install Docker and Docker Compose using the official installation script colorized_echo blue "Installing Docker" curl -fsSL https://get.docker.com | sh colorized_echo green "Docker installed successfully" } detect_node_binary_arch() { case "$(uname -m)" in amd64|x86_64) echo "amd64" ;; arm64|aarch64) echo "arm64" ;; i386|i486|i586|i686) echo "386" ;; armv5l|armv5tel|armv5tejl) echo "armv5" ;; armv6l|armv6) echo "armv6" ;; armv7l|armv7) echo "armv7" ;; s390x) echo "s390x" ;; *) colorized_echo red "Rebecca-node binary install is not available for architecture: $(uname -m)" >&2 colorized_echo yellow "Use Dockerized install for this server." >&2 exit 1 ;; esac } get_node_binary_release_asset_metadata() { local node_version="$1" local binary_arch="$2" local release_api local release_payload local resolved_tag local node_asset_name local node_asset_url if [ "$node_version" = "latest" ]; then release_api="${REBECCA_MIRROR_BASE}/api/repos/${REBECCA_NODE_RELEASE_REPO}/releases/latest" else release_api="${REBECCA_MIRROR_BASE}/api/repos/${REBECCA_NODE_RELEASE_REPO}/releases/tags/${node_version}" fi local attempts=1 if [ "$node_version" = "latest" ]; then # GitHub can expose a release before its workflow assets finish uploading. attempts=5 fi for attempt in $(seq 1 "$attempts"); do release_payload=$(curl -fsSL "$release_api" 2>/dev/null) || release_payload="" resolved_tag=$(echo "$release_payload" | jq -r '.tag_name // empty') node_asset_name="rebecca-node-${resolved_tag}-linux-${binary_arch}" node_asset_url=$(echo "$release_payload" | jq -r --arg name "$node_asset_name" --arg arch "$binary_arch" ' [ .assets[]? | select( (.name == $name) or (.name | test("^rebecca-node-.*-linux-" + $arch + "$")) ) | select(.browser_download_url != null) ] | sort_by(if .name == $name then 0 else 1 end, .name) | .[0].browser_download_url // empty ') if [ -n "$node_asset_url" ] && [ "$node_asset_url" != "null" ]; then printf '%s|%s\n' "${resolved_tag:-$node_version}" "$node_asset_url" return 0 fi if [ "$attempt" -lt "$attempts" ]; then sleep 2 fi done if [ -z "$release_payload" ]; then colorized_echo red "Unable to read Rebecca-node release metadata: $release_api" >&2 else colorized_echo red "No Rebecca-node binary release assets found for linux-${binary_arch}." >&2 colorized_echo yellow "Use --dev after the dev binary workflow succeeds, or use Dockerized install." >&2 fi exit 1 } get_node_binary_dev_artifact_metadata() { local binary_arch="$1" local release_api local release_payload local release_asset_name local release_asset_url local release_target local workflow_runs_api local workflow_runs_payload local matching_runs local run_json local run_id local head_sha local artifacts_api local artifacts_payload local artifact_name local artifact_url local nightly_workflow local workflow_path release_asset_name="rebecca-node-dev-linux-${binary_arch}" release_api="https://api.github.com/repos/${REBECCA_NODE_RELEASE_REPO}/releases/tags/${REBECCA_NODE_BINARY_DEV_RELEASE_TAG}" if release_payload=$(curl -fsSL "$release_api" 2>/dev/null); then release_asset_url=$(echo "$release_payload" | jq -r --arg name "$release_asset_name" ' .assets[]? | select(.name == $name) | .browser_download_url ' | head -n 1) if [ -n "$release_asset_url" ] && [ "$release_asset_url" != "null" ]; then release_target=$(echo "$release_payload" | jq -r '.target_commitish // empty') if [[ "$release_target" =~ ^[0-9a-fA-F]{7,40}$ ]]; then printf '%s|%s\n' "dev-${release_target:0:7}" "$release_asset_url" else printf '%s|%s\n' "dev-${REBECCA_NODE_BINARY_DEV_BRANCH}" "$release_asset_url" fi return 0 fi fi nightly_workflow="$REBECCA_NODE_BINARY_WORKFLOW_NAME" case "$nightly_workflow" in *.yml|*.yaml) ;; *) nightly_workflow="${nightly_workflow}.yml" ;; esac workflow_path=".github/workflows/${nightly_workflow}" workflow_runs_api="https://api.github.com/repos/${REBECCA_NODE_RELEASE_REPO}/actions/runs?per_page=50" workflow_runs_payload=$(curl -fsSL "$workflow_runs_api") || { colorized_echo red "Unable to read Rebecca-node binary workflow metadata: $workflow_runs_api" >&2 exit 1 } matching_runs=$(echo "$workflow_runs_payload" | jq -c --arg branch "$REBECCA_NODE_BINARY_DEV_BRANCH" --arg workflow_path "$workflow_path" ' .workflow_runs[]? | select( .head_branch == $branch and (.event == "push" or .event == "workflow_dispatch") and .conclusion == "success" and .path == $workflow_path ) ') if [ -z "$matching_runs" ]; then colorized_echo red "No successful Rebecca-node binary workflow run was found on branch ${REBECCA_NODE_BINARY_DEV_BRANCH}." >&2 exit 1 fi while IFS= read -r run_json; do [ -n "$run_json" ] || continue run_id=$(echo "$run_json" | jq -r '.id // empty') head_sha=$(echo "$run_json" | jq -r '.head_sha // empty') artifacts_api="https://api.github.com/repos/${REBECCA_NODE_RELEASE_REPO}/actions/runs/${run_id}/artifacts" if ! artifacts_payload=$(curl -fsSL "$artifacts_api"); then colorized_echo yellow "Unable to read Rebecca-node binary artifacts for workflow run ${run_id}; checking an older successful run." >&2 continue fi artifact_name=$(echo "$artifacts_payload" | jq -r --arg preferred "${REBECCA_NODE_BINARY_ARTIFACT_PREFIX}-linux-${binary_arch}" --arg arch "linux-${binary_arch}" ' [ .artifacts[]? | select((.expired | not) and (.name == $preferred or ((.name | startswith("rebecca-node")) and (.name | contains($arch))))) ] | sort_by(if .name == $preferred then 0 else 1 end, .created_at) | .[0].name // empty ') if [ -n "$artifact_name" ]; then artifact_url="https://nightly.link/${REBECCA_NODE_RELEASE_REPO}/workflows/${nightly_workflow}/${REBECCA_NODE_BINARY_DEV_BRANCH}/${artifact_name}.zip" printf '%s|%s\n' "dev-${head_sha:0:7}" "$artifact_url" return 0 fi colorized_echo yellow "Rebecca-node binary workflow run ${run_id} has no usable linux-${binary_arch} artifact; checking an older successful run." >&2 done <<< "$matching_runs" colorized_echo red "No usable Rebecca-node linux-${binary_arch} dev artifact was found on branch ${REBECCA_NODE_BINARY_DEV_BRANCH}." >&2 colorized_echo yellow "The dev binary workflow must publish ${REBECCA_NODE_BINARY_ARTIFACT_PREFIX}-linux-${binary_arch} before this server can install the dev binary." >&2 exit 1 } get_node_binary_dev_artifact_metadata_for_version() { local requested_version="$1" local binary_arch="$2" local requested_sha="${requested_version#dev-}" local workflow_name="${REBECCA_NODE_BINARY_WORKFLOW_NAME}" local workflow_path local runs_url local run_json local run_id local head_sha local artifact_name if ! [[ "$requested_version" =~ ^dev-[0-9a-fA-F]{7,40}$ ]]; then colorized_echo red "Invalid Rebecca-node dev build: $requested_version" >&2 exit 1 fi case "$workflow_name" in *.yml|*.yaml) ;; *) workflow_name="${workflow_name}.yml" ;; esac workflow_path=".github/workflows/${workflow_name}" runs_url="https://api.github.com/repos/${REBECCA_NODE_RELEASE_REPO}/actions/runs?branch=${REBECCA_NODE_BINARY_DEV_BRANCH}&event=push&status=success&per_page=100" while IFS= read -r run_json; do [ -n "$run_json" ] || continue run_id=$(echo "$run_json" | jq -r '.id // empty') head_sha=$(echo "$run_json" | jq -r '.head_sha // empty') [ -n "$run_id" ] && [ -n "$head_sha" ] || continue [[ "$head_sha" == "$requested_sha"* ]] || continue [[ "$(echo "$run_json" | jq -r '.path // empty')" == "$workflow_path" ]] || continue artifact_name="${REBECCA_NODE_BINARY_ARTIFACT_PREFIX}-linux-${binary_arch}" printf '%s|%s\n' "$requested_version" "https://nightly.link/${REBECCA_NODE_RELEASE_REPO}/workflows/${workflow_name}/${head_sha}/${artifact_name}.zip" return 0 done < <(curl -fsSL "$runs_url" | jq -c '.workflow_runs[]?') colorized_echo red "Rebecca-node dev build $requested_version was not found in successful workflow runs." >&2 exit 1 } write_node_binary_release_metadata() { local resolved_version="$1" local binary_arch="$2" local asset_url="$3" jq -n \ --arg image "rebecca-node (binary)" \ --arg tag "$resolved_version" \ --arg asset_url "$asset_url" \ --arg arch "linux-${binary_arch}" \ --arg node_binary "$BINARY_NODE" \ --arg installed_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ '{ install_mode: "binary", image: $image, tag: $tag, asset_url: $asset_url, arch: $arch, node_binary: $node_binary, installed_at: $installed_at }' > "$BINARY_METADATA_FILE" } create_binary_rebecca_node_service() { cat > "$BINARY_SERVICE_UNIT" < "$bundle_file" echo -e "Paste the Node install bundle from the panel, press ENTER on a new line when finished: " while IFS= read -r line || [ -n "$line" ]; do line="${line%$'\r'}" if [[ -z $line ]]; then if [ "$bundle_started" -eq 0 ]; then break fi if grep -q -- "-----END CERTIFICATE-----" "$bundle_file" && grep -Eq -- "-----END( [^-]+)? PRIVATE KEY-----" "$bundle_file"; then bundle_completed=1 break fi continue fi bundle_started=1 echo "$line" >>"$bundle_file" if grep -q -- "-----END CERTIFICATE-----" "$bundle_file" && grep -Eq -- "-----END( [^-]+)? PRIVATE KEY-----" "$bundle_file"; then bundle_completed=1 break fi done if [ "$bundle_completed" -ne 1 ]; then colorized_echo red "Node install bundle is incomplete. Paste the full bundle shown by the panel." rm -f "$bundle_file" exit 1 fi awk 'BEGIN{capture=0} /-----BEGIN CERTIFICATE-----/{capture=1} capture{print} /-----END CERTIFICATE-----/{exit}' "$bundle_file" >"$CERT_FILE" awk 'BEGIN{capture=0} /-----BEGIN( [^-]+)? PRIVATE KEY-----/{capture=1} capture{print} /-----END( [^-]+)? PRIVATE KEY-----/{exit}' "$bundle_file" >"$CERT_KEY_FILE" rm -f "$bundle_file" if ! grep -q -- "-----END CERTIFICATE-----" "$CERT_FILE"; then colorized_echo red "The bundle does not contain a valid PEM certificate." rm -f "$CERT_FILE" "$CERT_KEY_FILE" exit 1 fi if ! grep -Eq -- "-----END( [^-]+)? PRIVATE KEY-----" "$CERT_KEY_FILE"; then colorized_echo red "The bundle does not contain a valid PEM private key." rm -f "$CERT_FILE" "$CERT_KEY_FILE" exit 1 fi chmod 600 "$CERT_KEY_FILE" colorized_echo green "Node certificate bundle saved to $CERT_FILE and $CERT_KEY_FILE" } configure_binary_node_env() { mkdir -p "$DATA_DIR" "$APP_DIR" echo "$BRANCH" > "$BRANCH_FILE" if [ ! -s "$CERT_FILE" ] || [ ! -s "$CERT_KEY_FILE" ]; then rm -f "$CERT_FILE" "$CERT_KEY_FILE" read_node_certificate_bundle fi get_occupied_ports SERVICE_PORT=$(prompt_node_port_setting "SERVICE_PORT" "SERVICE_PORT" "62050") set_env_value "SERVICE_HOST" "0.0.0.0" set_env_value "SERVICE_PORT" "$SERVICE_PORT" XRAY_API_PORT=$(prompt_node_port_setting "XRAY_API_PORT" "XRAY_API_PORT" "62051" "$SERVICE_PORT") set_env_value "XRAY_API_HOST" "127.0.0.1" set_env_value "XRAY_API_PORT" "$XRAY_API_PORT" set_env_value "REBECCA_DATA_DIR" "$DATA_DIR" set_env_value "REBECCA_NODE_APP_NAME" "$APP_NAME" set_env_value "SSL_CLIENT_CERT_FILE" "$CERT_FILE" set_env_value "SSL_CERT_FILE" "$CERT_FILE" set_env_value "SSL_KEY_FILE" "$CERT_KEY_FILE" set_env_value "XRAY_EXECUTABLE_PATH" "$DATA_DIR/xray-core/xray" set_env_value "XRAY_ASSETS_PATH" "$DATA_DIR/xray-core" } normalize_node_dev_artifact() { local tmp_dir="$1" local binary_arch="$2" local candidate if [ -f "$tmp_dir/rebecca-node" ]; then chmod +x "$tmp_dir/rebecca-node" return 0 fi while IFS= read -r archive; do [ -n "$archive" ] || continue tar -xzf "$archive" -C "$tmp_dir" >/dev/null 2>&1 || true done < <(find "$tmp_dir" -maxdepth 3 -type f \( -name "*.tar.gz" -o -name "*.tgz" \) 2>/dev/null) candidate=$( find "$tmp_dir" -maxdepth 5 -type f \ \( -name "rebecca-node" -o -name "rebecca-node*linux-${binary_arch}" -o -name "rebecca-node-*" \) \ ! -name "*.sha256" ! -name "*.zip" ! -name "*.tar.gz" ! -name "*.tgz" 2>/dev/null \ | while IFS= read -r file; do size=$(wc -c < "$file" 2>/dev/null || echo 0) printf '%s\t%s\n' "$size" "$file" done \ | sort -nr \ | cut -f2- \ | head -n 1 ) if [ -n "$candidate" ]; then install -m 755 "$candidate" "$tmp_dir/rebecca-node" fi } install_binary_rebecca_node() { local node_version="$1" local configure="${2:-1}" local binary_arch local resolved_version local node_asset_url local artifact_url local tmp_dir local package_path detect_os for package in curl jq unzip; do if ! command -v "$package" >/dev/null 2>&1; then install_package "$package" fi done ensure_ov_binary_prerequisites ensure_haproxy_prerequisites binary_arch=$(detect_node_binary_arch) tmp_dir=$(mktemp -d) if [ -n "${REBECCA_NODE_BINARY_OVERRIDE:-}" ]; then if [ ! -f "$REBECCA_NODE_BINARY_OVERRIDE" ]; then colorized_echo red "REBECCA_NODE_BINARY_OVERRIDE must point to an existing file." >&2 rm -rf "$tmp_dir" exit 1 fi ui_spinner_run "Installing Rebecca-node custom binary" install -m 755 "$REBECCA_NODE_BINARY_OVERRIDE" "$tmp_dir/rebecca-node" resolved_version="${REBECCA_NODE_BINARY_OVERRIDE_VERSION:-custom}" artifact_url="local-override" elif [[ "$node_version" =~ ^dev-[0-9a-fA-F]{7,40}$ ]]; then IFS='|' read -r resolved_version artifact_url < <(get_node_binary_dev_artifact_metadata_for_version "$node_version" "$binary_arch") package_path="$tmp_dir/rebecca-node-binaries.zip" ui_spinner_run "Downloading Rebecca-node dev binary artifact" curl -fL "$artifact_url" -o "$package_path" ui_spinner_run "Extracting Rebecca-node dev artifact" unzip -j -o "$package_path" -d "$tmp_dir" normalize_node_dev_artifact "$tmp_dir" "$binary_arch" elif [ "$node_version" = "dev" ]; then IFS='|' read -r resolved_version artifact_url < <(get_node_binary_dev_artifact_metadata "$binary_arch") if [[ "$artifact_url" == *.zip ]]; then package_path="$tmp_dir/rebecca-node-binaries.zip" ui_spinner_run "Downloading Rebecca-node dev binary artifact" curl -fL "$artifact_url" -o "$package_path" ui_spinner_run "Extracting Rebecca-node dev artifact" unzip -j -o "$package_path" -d "$tmp_dir" normalize_node_dev_artifact "$tmp_dir" "$binary_arch" else ui_spinner_run "Downloading Rebecca-node dev binary" curl -fL "$artifact_url" -o "$tmp_dir/rebecca-node" chmod +x "$tmp_dir/rebecca-node" fi else IFS='|' read -r resolved_version node_asset_url < <(get_node_binary_release_asset_metadata "$node_version" "$binary_arch") ui_spinner_run "Downloading Rebecca-node binary" curl -fL "$node_asset_url" -o "$tmp_dir/rebecca-node" fi if [ ! -f "$tmp_dir/rebecca-node" ]; then colorized_echo red "Downloaded binary package is incomplete; rebecca-node is missing." >&2 rm -rf "$tmp_dir" exit 1 fi mkdir -p "$BINARY_BIN_DIR" "$DATA_DIR" "$APP_DIR" install -m 755 "$tmp_dir/rebecca-node" "$BINARY_NODE" if [ "$configure" = "1" ]; then configure_binary_node_env install_latest_xray_for_binary_node elif [ ! -x "$DATA_DIR/xray-core/xray" ]; then install_latest_xray_for_binary_node fi write_node_binary_release_metadata "${resolved_version:-$node_version}" "$binary_arch" "${artifact_url:-${node_asset_url:-}}" echo "binary" > "$INSTALL_MODE_FILE" create_binary_rebecca_node_service rm -rf "$tmp_dir" colorized_echo green "Rebecca-node binary files installed successfully" } install_rebecca_node_script() { TARGET_PATH="/usr/local/bin/$APP_NAME" TEMP_SCRIPT=$(mktemp) if ! ui_spinner_run "Downloading $APP_NAME command script" curl -fsSL "$SCRIPT_URL" -o "$TEMP_SCRIPT"; then colorized_echo red "Failed to download script from $SCRIPT_URL" rm -f "$TEMP_SCRIPT" exit 1 fi if head -n 1 "$TEMP_SCRIPT" | grep -qi "/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 } prompt_node_port_setting() { local key="$1" local label="$2" local fallback="$3" local other_port="${4:-}" local current_port local value current_port=$(get_env_value "$key") fallback="${current_port:-$fallback}" while true; do printf "Enter the %s (default %s): " "$label" "$fallback" >&2 IFS= read -r value value="${value:-$fallback}" if ! [[ "$value" =~ ^[0-9]+$ ]] || [ "$value" -lt 1 ] || [ "$value" -gt 65535 ]; then colorized_echo red "Invalid port. Please enter a port between 1 and 65535." >&2 elif [ -n "$other_port" ] && [ "$value" -eq "$other_port" ]; then colorized_echo red "Port $value cannot be the same as SERVICE_PORT. Please enter another port." >&2 elif is_port_occupied "$value" && [ "$value" != "$current_port" ]; then colorized_echo red "Port $value is already in use. Please enter another port." >&2 else echo "$value" return 0 fi done } install_rebecca_node() { # Fetch releases mkdir -p "$DATA_DIR" mkdir -p "$APP_DIR" mkdir -p "$DATA_MAIN_DIR" echo "$BRANCH" > "$BRANCH_FILE" rm -f "$CERT_FILE" "$CERT_KEY_FILE" read_node_certificate_bundle get_occupied_ports # Prompt the user to enter ports with occupation check 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 while true; do read -p "Enter the XRAY_API_PORT (default 62051): " -r XRAY_API_PORT if [[ -z "$XRAY_API_PORT" ]]; then XRAY_API_PORT=62051 fi if [[ "$XRAY_API_PORT" -ge 1 && "$XRAY_API_PORT" -le 65535 ]]; then if is_port_occupied "$XRAY_API_PORT"; then colorized_echo red "Port $XRAY_API_PORT is already in use. Please enter another port." elif [[ "$XRAY_API_PORT" -eq "$SERVICE_PORT" ]]; then colorized_echo red "Port $XRAY_API_PORT cannot be the same as SERVICE_PORT. Please enter another port." else break fi else colorized_echo red "Invalid port. Please enter a port between 1 and 65535." fi done colorized_echo blue "Generating compose file" # Write content to the file cat > "$COMPOSE_FILE" </dev/null 2>&1 || true rm -f "$BINARY_SERVICE_UNIT" systemctl daemon-reload fi if [ -d "$APP_DIR" ]; then colorized_echo yellow "Removing directory: $APP_DIR" rm -r "$APP_DIR" fi } uninstall_rebecca_node_docker_images() { images=$(docker images | grep rebecca-node | awk '{print $3}') if [ -n "$images" ]; then colorized_echo yellow "Removing Docker images of Rebecca-node" for image in $images; do if docker rmi "$image" >/dev/null 2>&1; then colorized_echo yellow "Image $image removed" fi done fi } uninstall_rebecca_node_data_files() { if [ -d "$DATA_DIR" ]; then colorized_echo yellow "Removing directory: $DATA_DIR" rm -r "$DATA_DIR" fi } up_rebecca_node() { if is_binary_install; then systemctl enable --now "$APP_NAME.service" return fi $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" up -d --remove-orphans } down_rebecca_node() { if is_binary_install; then systemctl stop "$APP_NAME.service" return fi $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" down } stop_rebecca_node_for_uninstall() { if [ "$install_mode" != "binary" ]; then detect_compose down_rebecca_node elif is_rebecca_node_up; then down_rebecca_node fi } show_rebecca_node_logs() { if is_binary_install; then journalctl -u "$APP_NAME.service" --no-pager return fi $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs } follow_rebecca_node_logs() { if is_binary_install; then journalctl -u "$APP_NAME.service" -f return fi $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs -f } update_rebecca_node_script() { colorized_echo blue "Updating $APP_NAME script from $SCRIPT_URL" install_rebecca_node_script } reexec_updated_node_script() { local target_path="/usr/local/bin/$APP_NAME" local args=("update") if [ "${REBECCA_NODE_SKIP_REEXEC:-0}" = "1" ]; then return fi if [ ! -x "$target_path" ]; then return fi if [ "$NODE_VERSION_SET" -eq 1 ]; then case "${NODE_VERSION_REQUESTED:-}" in dev) args+=("--dev") ;; "") args+=("--version" "latest") ;; *) args+=("--version" "$NODE_VERSION_REQUESTED") ;; esac fi colorized_echo blue "Reloading updated $APP_NAME script" REBECCA_NODE_SKIP_REEXEC=1 exec "$target_path" "${args[@]}" } update_rebecca_node() { local requested_version="${1:-}" if is_binary_install; then local node_version="${requested_version:-latest}" if [ -z "$requested_version" ] && [ "$BRANCH" = "dev" ]; then node_version="dev" fi install_binary_rebecca_node "$node_version" "0" return fi if [ -n "$requested_version" ]; then case "$requested_version" in dev) set_branch_variables dev ;; latest|"") set_branch_variables master ;; *) set_branch_variables master DOCKER_IMAGE="rebeccapanel/rebecca-node:${requested_version}" ;; esac echo "$BRANCH" > "$BRANCH_FILE" if [ -f "$COMPOSE_FILE" ]; then sed -i "s|^[[:space:]]*image:.*rebeccapanel/rebecca-node.*| image: $DOCKER_IMAGE|" "$COMPOSE_FILE" fi fi $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" pull } is_rebecca_node_installed() { if [ -d "$APP_DIR" ]; then return 0 else return 1 fi } is_rebecca_node_up() { if is_binary_install; then systemctl is-active --quiet "$APP_NAME.service" return fi if [ -z "${COMPOSE:-}" ]; then return 1 fi if [ -z "$($COMPOSE -f $COMPOSE_FILE ps -q -a)" ]; then return 1 else return 0 fi } install_command() { check_running_as_root local install_mode local node_version prompt_node_name # Check if rebecca is already installed if is_rebecca_node_installed; then colorized_echo red "Rebecca-node is already installed at $APP_DIR" read -p "Do you want to override the previous installation? (y/n) " if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo red "Aborted installation" exit 1 fi fi install_mode=$(select_install_mode "$INSTALL_MODE_REQUESTED") if [ "$NODE_VERSION_SET" -eq 1 ]; then node_version="$NODE_VERSION_REQUESTED" else select_node_version "" "$install_mode" node_version="$SELECTED_NODE_VERSION" fi case "$node_version" in dev) set_branch_variables dev ;; latest|"") set_branch_variables master node_version="latest" ;; *) set_branch_variables master ;; esac colorized_echo blue "Selected install mode: $install_mode" colorized_echo blue "Selected release channel: $node_version" if [ "$install_mode" = "binary" ]; then select_xray_core_version colorized_echo blue "Selected Xray-core version: $XRAY_CORE_VERSION" 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 [ "$install_mode" = "docker" ]; then if ! command -v docker >/dev/null 2>&1; then install_docker fi detect_compose fi install_rebecca_node_script if [ "$install_mode" = "binary" ]; then install_binary_rebecca_node "$node_version" "1" else install_rebecca_node echo "docker" > "$INSTALL_MODE_FILE" fi up_rebecca_node SERVICE_PORT="${SERVICE_PORT:-$(get_env_value "SERVICE_PORT")}" XRAY_API_PORT="${XRAY_API_PORT:-$(get_env_value "XRAY_API_PORT")}" echo "Use your IP: $NODE_IP and control port: $SERVICE_PORT to setup your Rebecca Main Panel" colorized_echo yellow "Run '$APP_NAME logs' if you want to follow live node logs." } uninstall_command() { check_running_as_root local install_mode install_mode=$(get_install_mode) local node_exists=0 if is_rebecca_node_installed; then node_exists=1 fi local service_exists=0 if [ -f "$BINARY_SERVICE_UNIT" ]; then service_exists=1 fi if [ "$node_exists" -eq 0 ] && [ "$service_exists" -eq 0 ]; then colorized_echo red "Rebecca-node not installed!" exit 1 fi read -p "Do you really want to uninstall Rebecca-node? (y/n) " if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo red "Aborted" exit 1 fi if [ "$node_exists" -eq 1 ]; then stop_rebecca_node_for_uninstall fi uninstall_rebecca_node_script if [ "$node_exists" -eq 1 ]; then uninstall_rebecca_node if [ "$install_mode" != "binary" ]; then uninstall_rebecca_node_docker_images fi read -p "Do you want to remove Rebecca-node data files too ($DATA_DIR)? (y/n) " if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo green "Rebecca-node uninstalled successfully" else uninstall_rebecca_node_data_files colorized_echo green "Rebecca-node uninstalled successfully" fi else colorized_echo green "Rebecca-node service/scripts removed" fi } up_command() { help() { colorized_echo red "Usage: rebecca-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 rebecca-node is installed if ! is_rebecca_node_installed; then colorized_echo red "Rebecca-node's not installed!" exit 1 fi if ! is_binary_install; then detect_compose fi if is_rebecca_node_up; then colorized_echo red "Rebecca-node's already up" exit 1 fi up_rebecca_node if [ "$no_logs" = false ]; then follow_rebecca_node_logs fi } down_command() { # Check if rebecca-node is installed if ! is_rebecca_node_installed; then colorized_echo red "Rebecca-node not installed!" exit 1 fi if ! is_binary_install; then detect_compose fi if ! is_rebecca_node_up; then colorized_echo red "Rebecca-node already down" exit 1 fi down_rebecca_node } restart_command() { help() { colorized_echo red "Usage: rebecca-node restart [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 rebecca-node is installed if ! is_rebecca_node_installed; then colorized_echo red "Rebecca-node not installed!" exit 1 fi if ! is_binary_install; then detect_compose fi down_rebecca_node up_rebecca_node } status_command() { # Check if rebecca-node is installed if ! is_rebecca_node_installed; then echo -n "Status: " colorized_echo red "Not Installed" exit 1 fi if ! is_binary_install; then detect_compose fi if ! is_rebecca_node_up; then echo -n "Status: " colorized_echo blue "Down" exit 1 fi echo -n "Status: " colorized_echo green "Up" if is_binary_install; then systemctl status "$APP_NAME.service" --no-pager return fi 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 } logs_command() { help() { colorized_echo red "Usage: rebecca-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 rebecca is installed if ! is_rebecca_node_installed; then colorized_echo red "Rebecca-node's not installed!" exit 1 fi if ! is_binary_install; then detect_compose fi if ! is_rebecca_node_up; then colorized_echo red "Rebecca-node is not up." exit 1 fi if [ "$no_follow" = true ]; then show_rebecca_node_logs else follow_rebecca_node_logs fi } update_command() { check_running_as_root local node_version="" if ! is_rebecca_node_installed; then colorized_echo red "Rebecca-node not installed!" exit 1 fi if [ "$NODE_VERSION_SET" -eq 1 ]; then node_version="${NODE_VERSION_REQUESTED:-latest}" case "$node_version" in dev) set_branch_variables dev ;; latest|"") set_branch_variables master node_version="latest" ;; *) set_branch_variables master DOCKER_IMAGE="rebeccapanel/rebecca-node:${node_version}" ;; esac echo "$BRANCH" > "$BRANCH_FILE" fi if ! is_binary_install; then detect_compose fi update_rebecca_node_script reexec_updated_node_script if is_binary_install; then colorized_echo blue "Updating Rebecca-node binary files" else colorized_echo blue "Pulling node image $DOCKER_IMAGE" fi update_rebecca_node "$node_version" colorized_echo blue "Restarting Rebecca-node services" down_rebecca_node up_rebecca_node colorized_echo blue "Rebecca-node updated successfully" } identify_the_operating_system_and_architecture() { if [[ "$(uname)" == 'Linux' ]]; then case "$(uname -m)" in 'i386' | 'i686') ARCH='32' ;; 'amd64' | 'x86_64') ARCH='64' ;; 'armv5tel') ARCH='arm32-v5' ;; 'armv6l') ARCH='arm32-v6' grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' ;; 'armv7' | 'armv7l') ARCH='arm32-v7a' grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' ;; 'armv8' | 'aarch64') ARCH='arm64-v8a' ;; 'mips') ARCH='mips32' ;; 'mipsle') ARCH='mips32le' ;; 'mips64') ARCH='mips64' lscpu | grep -q "Little Endian" && ARCH='mips64le' ;; 'mips64le') ARCH='mips64le' ;; 'ppc64') ARCH='ppc64' ;; 'ppc64le') ARCH='ppc64le' ;; 'riscv64') ARCH='riscv64' ;; 's390x') ARCH='s390x' ;; *) echo "error: The architecture is not supported." exit 1 ;; esac else echo "error: This operating system is not supported." exit 1 fi } # Function to update the Xray core get_xray_core() { identify_the_operating_system_and_architecture select_xray_core_version selected_version="$XRAY_CORE_VERSION" if ! command -v unzip >/dev/null 2>&1; then detect_os install_package unzip fi mkdir -p "$DATA_MAIN_DIR/xray-core" cd "$DATA_MAIN_DIR/xray-core" xray_filename="Xray-linux-$ARCH.zip" if [ "$selected_version" = "latest" ]; then xray_download_url="https://github.com/XTLS/Xray-core/releases/latest/download/${xray_filename}" else xray_download_url="https://github.com/XTLS/Xray-core/releases/download/${selected_version}/${xray_filename}" fi colorized_echo blue "Downloading Xray-core ${selected_version}" if ! curl -fL "$xray_download_url" -o "$xray_filename"; then rm -f "$xray_filename" colorized_echo red "Failed to download Xray-core ${selected_version}." exit 1 fi colorized_echo blue "Installing Xray-core ${selected_version}" if ! unzip -o "$xray_filename" >/dev/null 2>&1; then rm -f "$xray_filename" colorized_echo red "Failed to extract Xray-core ${selected_version}." exit 1 fi rm -f "$xray_filename" } get_current_xray_core_version() { XRAY_BINARY="$DATA_MAIN_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 command -v docker >/dev/null 2>&1 && 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" } install_yq() { if command -v yq &>/dev/null; then colorized_echo green "yq is already installed." return fi identify_the_operating_system_and_architecture local base_url="${REBECCA_MIRROR_BASE}/bin" local yq_binary="" case "$ARCH" in '64' | 'x86_64') yq_binary="yq_linux_amd64" ;; 'arm32-v7a' | 'arm32-v6' | 'arm32-v5' | 'armv7l') yq_binary="yq_linux_arm" ;; 'arm64-v8a' | 'aarch64') yq_binary="yq_linux_arm64" ;; '32' | 'i386' | 'i686') yq_binary="yq_linux_386" ;; *) colorized_echo red "Unsupported architecture: $ARCH" exit 1 ;; esac local yq_url="${base_url}/${yq_binary}" colorized_echo blue "Downloading yq from ${yq_url}..." if ! command -v curl &>/dev/null && ! command -v wget &>/dev/null; then colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl." install_package curl || { colorized_echo red "Failed to install curl. Please install curl or wget manually." exit 1 } fi if command -v curl &>/dev/null; then if curl -L "$yq_url" -o /usr/local/bin/yq; then chmod +x /usr/local/bin/yq colorized_echo green "yq installed successfully!" else colorized_echo red "Failed to download yq using curl. Please check your internet connection." exit 1 fi elif command -v wget &>/dev/null; then if wget -O /usr/local/bin/yq "$yq_url"; then chmod +x /usr/local/bin/yq colorized_echo green "yq installed successfully!" else colorized_echo red "Failed to download yq using wget. Please check your internet connection." exit 1 fi fi if ! echo "$PATH" | grep -q "/usr/local/bin"; then export PATH="/usr/local/bin:$PATH" fi hash -r if command -v yq &>/dev/null; then colorized_echo green "yq is ready to use." elif [ -x "/usr/local/bin/yq" ]; then colorized_echo yellow "yq is installed at /usr/local/bin/yq but not found in PATH." colorized_echo yellow "You can add /usr/local/bin to your PATH environment variable." else colorized_echo red "yq installation failed. Please try again or install manually." exit 1 fi } update_core_command() { check_running_as_root get_xray_core if is_binary_install; then set_env_value "XRAY_EXECUTABLE_PATH" "$DATA_MAIN_DIR/xray-core/xray" set_env_value "XRAY_ASSETS_PATH" "$DATA_MAIN_DIR/xray-core" colorized_echo red "Restarting Rebecca-node..." systemctl restart "$APP_NAME.service" colorized_echo blue "Installation of XRAY-CORE version $selected_version completed." return fi if ! command -v yq &>/dev/null; then echo "yq is not installed. Installing yq..." install_yq fi if ! grep -q 'XRAY_EXECUTABLE_PATH: "/var/lib/rebecca-node/xray-core/xray"' "$COMPOSE_FILE"; then yq eval '.services."rebecca-node".environment.XRAY_EXECUTABLE_PATH = "/var/lib/rebecca-node/xray-core/xray"' -i "$COMPOSE_FILE" fi if ! yq eval ".services.\"rebecca-node\".volumes[] | select(. == \"${DATA_MAIN_DIR}:/var/lib/rebecca-node\")" "$COMPOSE_FILE" &>/dev/null; then yq eval ".services.\"rebecca-node\".volumes += \"${DATA_MAIN_DIR}:/var/lib/rebecca-node\"" -i "$COMPOSE_FILE" fi # Restart Rebecca-node colorized_echo red "Restarting Rebecca-node..." $APP_NAME restart -n colorized_echo blue "Installation of XRAY-CORE version $selected_version completed." } check_editor() { if [ -z "$EDITOR" ]; then if command -v nano >/dev/null 2>&1; then EDITOR="nano" elif command -v vi >/dev/null 2>&1; then EDITOR="vi" else detect_os install_package nano EDITOR="nano" fi fi } edit_command() { detect_os check_editor if is_binary_install; then ensure_env_file $EDITOR "$ENV_FILE" return fi if [ -f "$COMPOSE_FILE" ]; then $EDITOR "$COMPOSE_FILE" else colorized_echo red "Compose file not found at $COMPOSE_FILE" exit 1 fi } get_node_current_version() { local version="" if [ -f "$BINARY_METADATA_FILE" ]; then version=$(sed -nE 's/.*"tag"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$BINARY_METADATA_FILE" | head -n 1) fi if [ -z "$version" ] && [ -f "$BRANCH_FILE" ]; then version=$(tr -d '[:space:]' < "$BRANCH_FILE") fi printf '%s\n' "${version:-unknown}" } get_node_service_status() { if is_rebecca_node_up; then echo "running" else echo "stopped" fi } print_node_menu_status_summary() { local service_port xray_api_port service_port=$(get_env_value "SERVICE_PORT") xray_api_port=$(get_env_value "XRAY_API_PORT") service_port="${service_port:-62050}" xray_api_port="${xray_api_port:-62051}" ui_status_row "Version" "$(get_node_current_version)" ui_status_row "Service" "$(get_node_service_status)" ui_status_row "Mode" "$(get_install_mode)" ui_status_row "Node IP" "${NODE_IP:-unknown}" ui_status_row "Service port" "$service_port" ui_status_row "Xray API" "$xray_api_port" ui_status_row "Cert" "$CERT_FILE" } usage() { colorized_echo blue "================================" colorized_echo magenta " $APP_NAME Node CLI Help" colorized_echo blue "================================" colorized_echo cyan "Usage:" echo " $APP_NAME [command]" echo colorized_echo cyan "Commands:" colorized_echo yellow " up – Start services" colorized_echo yellow " down – Stop services" colorized_echo yellow " restart – Restart services" colorized_echo yellow " status – Show status" colorized_echo yellow " logs – Show logs" colorized_echo yellow " install - Install/reinstall Rebecca-node" colorized_echo yellow " update - Update to latest/dev or a specific version" colorized_echo yellow " uninstall - Uninstall Rebecca-node" colorized_echo blue " script-install - Install Rebecca-node script" colorized_echo blue " script-update - Update Rebecca-node CLI script" colorized_echo blue " script-uninstall - Uninstall Rebecca-node script" colorized_echo yellow " edit - Edit docker-compose.yml or binary .env (via nano or vi)" colorized_echo yellow " core-update – Update/Change Xray core" echo colorized_echo cyan "Node Information:" colorized_echo magenta " Cert file path: $CERT_FILE" colorized_echo magenta " Node IP: $NODE_IP" echo colorized_echo cyan "Install/update options:" case "$(script_install_mode)" in docker) colorized_echo magenta " This script installs Docker node mode only." ;; binary) colorized_echo magenta " This script installs binary node mode only." ;; esac colorized_echo magenta " --dev or --version vX.Y.Z" 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 DEFAULT_SERVICE_PORT="62050" DEFAULT_XRAY_API_PORT="62051" if [ -f "$COMPOSE_FILE" ]; then SERVICE_PORT=$(awk -F': ' '/SERVICE_PORT:/ {gsub(/"/, "", $2); print $2}' "$COMPOSE_FILE") XRAY_API_PORT=$(awk -F': ' '/XRAY_API_PORT:/ {gsub(/"/, "", $2); print $2}' "$COMPOSE_FILE") elif [ -f "$ENV_FILE" ]; then SERVICE_PORT=$(get_env_value "SERVICE_PORT") XRAY_API_PORT=$(get_env_value "XRAY_API_PORT") fi SERVICE_PORT=${SERVICE_PORT:-$DEFAULT_SERVICE_PORT} XRAY_API_PORT=${XRAY_API_PORT:-$DEFAULT_XRAY_API_PORT} colorized_echo cyan "Ports:" colorized_echo magenta " Control port: $SERVICE_PORT" colorized_echo magenta " Local Xray API port: $XRAY_API_PORT" colorized_echo blue "=================================" echo } menu_commands() { echo "up down restart status logs install update uninstall script-install script-update script-uninstall core-update edit help" } menu_category_for() { case "$1" in up|down|restart|status|logs) echo "Node runtime" ;; install|update|uninstall) echo "Install and update" ;; script-install|script-update|script-uninstall) echo "Script management" ;; core-update|edit) echo "Tools" ;; *) echo "Help" ;; esac } menu_description_for() { case "$1" in up) echo "Start services" ;; down) echo "Stop services" ;; restart) echo "Restart services" ;; status) echo "Show status" ;; logs) echo "Show logs" ;; install) echo "Install/reinstall Rebecca-node" ;; update) echo "Update to latest version" ;; uninstall) echo "Uninstall Rebecca-node" ;; script-install) echo "Install Rebecca-node script" ;; script-update) echo "Update Rebecca-node CLI script" ;; script-uninstall) echo "Uninstall Rebecca-node script" ;; core-update) echo "Update/Change Xray core" ;; edit) echo "Edit docker-compose.yml or binary .env" ;; help) echo "Show this help message" ;; *) echo "" ;; esac } print_menu() { local selected="${1:-0}" local previous_category="" local idx=1 local cmd category desc is_selected columns tip_width tip ui_header "$APP_NAME" "Rebecca-node control center" ui_section "Status" print_node_menu_status_summary ui_section "Actions" for cmd in $(menu_commands); do category=$(menu_category_for "$cmd") if [ "$category" != "$previous_category" ]; then ui_menu_category "$category" previous_category="$category" fi desc=$(menu_description_for "$cmd") is_selected=0 [ "$idx" -eq "$selected" ] && is_selected=1 ui_menu_item "$idx" "$cmd" "$desc" "$is_selected" idx=$((idx + 1)) done printf "\n" columns=$(ui_terminal_columns) tip_width=$((columns - 1)) tip="Tip: arrow keys move, Enter selects, q exits; numbers and commands also work." ui_color "38;5;245" "${tip:0:$tip_width}" printf "\n" echo } ui_menu_lines_below_item() { local target="$1" local idx=1 lines=4 previous_category="" cmd category for cmd in $(menu_commands); do category=$(menu_category_for "$cmd") if [ "$idx" -gt "$target" ]; then [ "$category" != "$previous_category" ] && lines=$((lines + 2)) lines=$((lines + 1)) fi previous_category="$category" idx=$((idx + 1)) done printf "%s" "$lines" } ui_redraw_menu_item() { local index="$1" selected="$2" distance local commands=($(menu_commands)) local command="${commands[$((index - 1))]}" distance=$(ui_menu_lines_below_item "$index") printf "\033[%sA\r\033[2K" "$distance" ui_menu_item "$index" "$command" "$(menu_description_for "$command")" "$selected" if [ "$distance" -gt 1 ]; then printf "\033[%sB\r" "$((distance - 1))" fi } ui_menu_prompt() { local columns prompt columns=$(ui_terminal_columns) if [ "$columns" -lt 30 ]; then prompt="Select: " elif [ "$columns" -lt 55 ]; then prompt="Select (arrows/Enter/number): " else prompt="Select option (arrow keys, Enter, number, command): " fi prompt="${prompt:0:$((columns - 1))}" ui_color "38;5;45;1" "$prompt" } map_choice_to_command() { local commands=($(menu_commands)) if [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le "${#commands[@]}" ]; then echo "${commands[$(($1 - 1))]}" return fi echo "$1" } read_menu_command() { MENU_COMMAND="" if ! ui_supports_cursor_motion; then print_menu ui_color "38;5;45;1" "Select option" printf " " ui_color "38;5;245" "(number or command): " read -r user_choice [ -z "$user_choice" ] && return 1 MENU_COMMAND=$(map_choice_to_command "$user_choice") return fi local commands=($(menu_commands)) local selected=1 local action kind value mapped previous_selected ui_clear print_menu "$selected" ui_menu_prompt while true; do action=$(ui_read_menu_choice "$selected" "${#commands[@]}") || return 1 kind="${action%%:*}" value="${action#*:}" case "$kind" in move) previous_selected="$selected" selected="$value" if [ "$selected" -ne "$previous_selected" ]; then ui_redraw_menu_item "$previous_selected" 0 ui_redraw_menu_item "$selected" 1 printf "\r\033[2K" ui_menu_prompt fi ;; enter) MENU_COMMAND="${commands[$(($value - 1))]}" printf "\n" return ;; value) mapped=$(map_choice_to_command "$value") [ -n "$mapped" ] && MENU_COMMAND="$mapped" printf "\n" return ;; quit) printf "\n" return 1 ;; esac done } dispatch_command() { local cmd="$1" shift || true case "$cmd" in help|install|install-script|script-install|update-script|script-update|uninstall-script|script-uninstall) ;; *) ensure_script_matches_installed_mode ;; esac case "$cmd" in install) if [ ! -t 0 ] && [ -r /dev/tty ]; then install_command for each base URL read from stdin; print the first that answers. _pg_ir_pick() { local suffix="$1" base while IFS= read -r base; do [ -n "$base" ] || continue if curl -fsSL --connect-timeout 6 --max-time 12 -o /dev/null -r 0-0 "$base$suffix" 2>/dev/null; then printf '%s\n' "$base" return 0 fi done return 1 } _pg_ir_backup() { mkdir -p "$IR_SRC_BACKUP" || return 1 rm -rf "$IR_SRC_BACKUP"/* 2>/dev/null if [ -f /etc/apt/sources.list ]; then cp -a /etc/apt/sources.list "$IR_SRC_BACKUP/sources.list" || return 1 fi if [ -d /etc/apt/sources.list.d ]; then mkdir -p "$IR_SRC_BACKUP/sources.list.d" cp -a /etc/apt/sources.list.d/. "$IR_SRC_BACKUP/sources.list.d/" 2>/dev/null || true fi if [ -d /etc/yum.repos.d ]; then mkdir -p "$IR_SRC_BACKUP/yum.repos.d" cp -a /etc/yum.repos.d/. "$IR_SRC_BACKUP/yum.repos.d/" 2>/dev/null || true fi return 0 } _pg_ir_restore() { [ "$_pg_ir_mirror_active" = 1 ] || return 0 _pg_ir_mirror_active=0 trap - EXIT INT TERM if [ -d "$IR_SRC_BACKUP/sources.list.d" ]; then rm -rf /etc/apt/sources.list.d mkdir -p /etc/apt/sources.list.d cp -a "$IR_SRC_BACKUP/sources.list.d/." /etc/apt/sources.list.d/ 2>/dev/null || true fi if [ -f "$IR_SRC_BACKUP/sources.list" ]; then cp -a "$IR_SRC_BACKUP/sources.list" /etc/apt/sources.list 2>/dev/null || true fi if [ -d "$IR_SRC_BACKUP/yum.repos.d" ]; then rm -rf /etc/yum.repos.d mkdir -p /etc/yum.repos.d cp -a "$IR_SRC_BACKUP/yum.repos.d/." /etc/yum.repos.d/ 2>/dev/null || true fi # Deliberately no `apt-get update` here: the original repositories are # unreachable by definition, so refreshing would just hang the exit. colorized_echo green "Package repositories restored to their original configuration." } _pg_ir_enable() { [ "$_pg_ir_mirror_active" = 1 ] && return 0 [ -n "${OS:-}" ] || detect_os local codename base sec rv arch comps codename=$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"') if [ -z "$codename" ]; then colorized_echo yellow "No release codename found; cannot select an Iranian mirror." return 1 fi if [ "$OS" = "Ubuntu" ] || [ "${OS#Debian}" != "$OS" ]; then if [ "$OS" = "Ubuntu" ]; then base=$(_pg_ir_pick "/dists/$codename/Release" <<'CAND' https://mirror.arvancloud.ir/ubuntu https://repo.iut.ac.ir/ubuntu https://mirror.iranserver.com/ubuntu CAND ) else base=$(_pg_ir_pick "/dists/$codename/Release" <<'CAND' https://mirror.arvancloud.ir/debian https://repo.iut.ac.ir/debian CAND ) sec=$(_pg_ir_pick "/dists/$codename-security/Release" <<'CAND' https://mirror.arvancloud.ir/debian-security CAND ) fi if [ -z "$base" ]; then colorized_echo yellow "No reachable Iranian mirror for $OS $codename." return 1 fi _pg_ir_backup || return 1 mkdir -p /etc/apt/sources.list.d # Third-party lists may point abroad as well; park them for the duration. find /etc/apt/sources.list.d -maxdepth 1 -name '*.list' -exec rm -f {} + 2>/dev/null || true if [ "$OS" = "Ubuntu" ]; then cat > /etc/apt/sources.list < /etc/apt/sources.list <> /etc/apt/sources.list fi fi _pg_ir_mirror_active=1 trap '_pg_ir_restore' EXIT INT TERM colorized_echo yellow "Package repositories temporarily switched to $base; they will be restored on exit." apt-get update -qq >/dev/null 2>&1 || true return 0 fi if _pg_ir_is_redhat; then rv=$(rpm -E %{rhel} 2>/dev/null) case "$(uname -m)" in x86_64|amd64) arch=x86_64 ;; aarch64|arm64) arch=aarch64 ;; *) return 1 ;; esac [ -n "$rv" ] || return 1 base=$(_pg_ir_pick "/almalinux/$rv/BaseOS/$arch/os/repodata/repomd.xml" <<'CAND' https://mirror.iranserver.com/almalinux https://mirror.arvancloud.ir/almalinux CAND ) if [ -z "$base" ]; then colorized_echo yellow "No reachable Iranian mirror for $OS $rv." return 1 fi _pg_ir_backup || return 1 mkdir -p /etc/yum.repos.d rm -f /etc/yum.repos.d/*.repo cat > /etc/yum.repos.d/ir-temp-mirror.repo </dev/null 2>&1 || true return 0 fi colorized_echo yellow "No Iranian mirror recipe for $OS." return 1 } # Only reached when the normal repositories failed. PKG_MANAGER is reset # so the retry refreshes the index against the mirror instead of reusing # the list it just failed to fetch. install_package() { local PACKAGE="$1" if _rebecca_install_package "$PACKAGE"; then return 0 fi colorized_echo yellow "Package repositories are unreachable; retrying through an Iranian mirror." _pg_ir_enable || return 1 PKG_MANAGER="" _rebecca_install_package "$PACKAGE" } dispatch_command "$COMMAND"