diff --git a/install.sh b/install.sh index f81fa2d2..124ac5f3 100644 --- a/install.sh +++ b/install.sh @@ -22,8 +22,14 @@ PUBLISHED_SCRIPT_URL="${PUBLISHED_SCRIPT_URL:-https://s3.cloudyun.top/downloads/ V2BX_DETECTED=0 V2BX_CONFIG_PATH="" UNINSTALL_V2BX_DEFAULT="${UNINSTALL_V2BX_DEFAULT:-n}" -SCRIPT_VERSION="${SCRIPT_VERSION:-v1.2.4}" +SCRIPT_VERSION="${SCRIPT_VERSION:-v1.3.0}" declare -a V2BX_IMPORTED_NODE_IDS=() +declare -a EXISTING_IMPORTED_NODE_IDS=() +declare -a NODE_IDS=() +EXISTING_INSTALL=0 +EXISTING_CONFIG_SOURCE="" +PROMPT_FOR_CONFIG=1 +CONFIG_BACKUP_DIR="" echo -e "${GREEN}Welcome to singbox Release Installation Script${NC}" echo -e "${GREEN}Script version: ${SCRIPT_VERSION}${NC}" @@ -136,6 +142,279 @@ detect_v2bx() { return 0 } +detect_existing_installation() { + if [[ -x "$BINARY_PATH" || -f "$SERVICE_FILE" || -f "$CONFIG_BASE_FILE" || -f "$CONFIG_ROUTE_FILE" || -f "$CONFIG_OUTBOUNDS_FILE" || -f "$CONFIG_DIR/config.json" ]]; then + EXISTING_INSTALL=1 + fi + + if [[ -f "$CONFIG_BASE_FILE" ]]; then + EXISTING_CONFIG_SOURCE="$CONFIG_BASE_FILE" + elif [[ -f "$CONFIG_DIR/config.json" ]]; then + EXISTING_CONFIG_SOURCE="$CONFIG_DIR/config.json" + fi + + if [[ "$EXISTING_INSTALL" -eq 1 ]]; then + echo -e "${YELLOW}Detected existing sing-box installation. Switching to update flow...${NC}" + if [[ -n "$EXISTING_CONFIG_SOURCE" ]]; then + echo -e "${YELLOW}Existing config source: ${EXISTING_CONFIG_SOURCE}${NC}" + fi + fi +} + +load_existing_install_defaults() { + if [[ -z "$EXISTING_CONFIG_SOURCE" || ! -f "$EXISTING_CONFIG_SOURCE" ]]; then + return 1 + fi + + echo -e "${YELLOW}Loading defaults from existing sing-box config...${NC}" + EXISTING_IMPORTED_NODE_IDS=() + + local parsed="" + if command -v python3 >/dev/null 2>&1; then + parsed="$(python3 - "$EXISTING_CONFIG_SOURCE" <<'PY' +import json +import sys + +path = sys.argv[1] +with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + +xboard = {} +for service in data.get("services") or []: + if isinstance(service, dict) and service.get("type") == "xboard": + xboard = service + break + +print(f"PANEL_URL={xboard.get('panel_url', '')}") +print(f"PANEL_TOKEN={xboard.get('key', '')}") + +node_id = xboard.get("node_id") +if node_id not in (None, ""): + print(f"NODE_ID={node_id}") +for node in xboard.get("nodes") or []: + if isinstance(node, dict): + current_node_id = node.get("node_id") + if current_node_id not in (None, ""): + print(f"NODE_ID={current_node_id}") + +servers = ((data.get("dns") or {}).get("servers") or []) +dns_server = servers[0] if servers else {} +if isinstance(dns_server, dict): + print(f"DNS_MODE={dns_server.get('type', '')}") + print(f"DNS_SERVER={dns_server.get('server', '')}") + print(f"DNS_SERVER_PORT={dns_server.get('server_port', '')}") +PY +)" + elif command -v jq >/dev/null 2>&1; then + parsed="$(jq -r ' + ([.services[]? | select(.type == "xboard")] | .[0] // {}) as $xboard | + "PANEL_URL=" + ($xboard.panel_url // ""), + "PANEL_TOKEN=" + ($xboard.key // ""), + (if ($xboard.node_id // empty) != empty then "NODE_ID=" + ($xboard.node_id | tostring) else empty end), + (($xboard.nodes // [])[]? | "NODE_ID=" + (.node_id | tostring)), + (.dns.servers[0] // {}) as $dns | + "DNS_MODE=" + ($dns.type // ""), + "DNS_SERVER=" + ($dns.server // ""), + "DNS_SERVER_PORT=" + (($dns.server_port // "") | tostring) + ' "$EXISTING_CONFIG_SOURCE" 2>/dev/null || true)" + else + echo -e "${YELLOW}Neither python3 nor jq found, unable to auto-load existing config.${NC}" + return 1 + fi + + if [[ -z "$parsed" ]]; then + return 1 + fi + + local parsed_line + while IFS= read -r parsed_line; do + parsed_line="$(sanitize_value "$parsed_line")" + case "$parsed_line" in + PANEL_URL=*) + PANEL_URL="$(sanitize_value "${parsed_line#PANEL_URL=}")" + ;; + PANEL_TOKEN=*) + PANEL_TOKEN="$(sanitize_value "${parsed_line#PANEL_TOKEN=}")" + ;; + NODE_ID=*) + append_unique_node_id_from_existing "${parsed_line#NODE_ID=}" + ;; + DNS_MODE=*) + DNS_MODE="$(echo "${parsed_line#DNS_MODE=}" | tr '[:upper:]' '[:lower:]')" + ;; + DNS_SERVER=*) + DNS_SERVER="$(sanitize_value "${parsed_line#DNS_SERVER=}")" + ;; + DNS_SERVER_PORT=*) + DNS_SERVER_PORT="$(sanitize_value "${parsed_line#DNS_SERVER_PORT=}")" + ;; + esac + done <<< "$parsed" + + if [[ "${#EXISTING_IMPORTED_NODE_IDS[@]}" -gt 0 ]]; then + NODE_IDS=("${EXISTING_IMPORTED_NODE_IDS[@]}") + NODE_ID="${EXISTING_IMPORTED_NODE_IDS[0]}" + fi + + if [[ -n "${DNS_MODE:-}" ]]; then + DNS_MODE="$(sanitize_value "$DNS_MODE")" + fi + + if [[ -n "${PANEL_URL:-}" && -n "${PANEL_TOKEN:-}" && "${#NODE_IDS[@]}" -gt 0 && ( "${DNS_MODE:-}" == "local" || ( "${DNS_MODE:-}" == "udp" && -n "${DNS_SERVER:-}" && -n "${DNS_SERVER_PORT:-}" ) ) ]]; then + echo -e "${YELLOW}Loaded existing config: PanelURL=${PANEL_URL}, NodeIDs=$(IFS=,; echo "${NODE_IDS[*]}"), DNS=${DNS_MODE}${NC}" + return 0 + fi + + echo -e "${YELLOW}Existing config detected, but some fields are incomplete. The installer will ask for the missing values.${NC}" + return 1 +} + +append_unique_node_id_from_existing() { + local normalized_value + local node_id_part + local existing_node_id + + normalized_value="$(normalize_node_id_input "$1")" + if [[ -z "$normalized_value" ]]; then + return 0 + fi + + read -r -a NORMALIZED_NODE_ID_PARTS <<< "$normalized_value" + for node_id_part in "${NORMALIZED_NODE_ID_PARTS[@]}"; do + if ! [[ "$node_id_part" =~ ^[0-9]+$ ]]; then + continue + fi + for existing_node_id in "${EXISTING_IMPORTED_NODE_IDS[@]}"; do + if [[ "$existing_node_id" == "$node_id_part" ]]; then + node_id_part="" + break + fi + done + if [[ -n "$node_id_part" ]]; then + EXISTING_IMPORTED_NODE_IDS+=("$node_id_part") + fi + done +} + +ensure_config_backup_dir() { + if [[ -n "$CONFIG_BACKUP_DIR" ]]; then + return 0 + fi + CONFIG_BACKUP_DIR="$CONFIG_DIR/backup/$(date +%Y%m%d-%H%M%S)" + mkdir -p "$CONFIG_BACKUP_DIR" + echo -e "${YELLOW}Backing up existing configuration to ${CONFIG_BACKUP_DIR}${NC}" +} + +backup_path_if_exists() { + local path="$1" + if [[ ! -e "$path" ]]; then + return 0 + fi + ensure_config_backup_dir + cp -a "$path" "$CONFIG_BACKUP_DIR/" +} + +archive_legacy_config_json() { + local legacy_config_path="$CONFIG_DIR/config.json" + if [[ ! -f "$legacy_config_path" ]]; then + return 0 + fi + ensure_config_backup_dir + mv "$legacy_config_path" "$CONFIG_BACKUP_DIR/config.json.migrated" + echo -e "${YELLOW}Legacy single-file config has been archived to ${CONFIG_BACKUP_DIR}/config.json.migrated${NC}" +} + +extract_legacy_config_sections() { + local source_path="$1" + local route_written=0 + local outbound_written=0 + + if [[ ! -f "$source_path" ]]; then + return 1 + fi + + if command -v python3 >/dev/null 2>&1; then + local parsed + parsed="$(python3 - "$source_path" "$CONFIG_ROUTE_FILE" "$CONFIG_OUTBOUNDS_FILE" <<'PY' +import json +import sys + +source_path, route_path, outbounds_path = sys.argv[1:4] +with open(source_path, "r", encoding="utf-8") as f: + data = json.load(f) + +route_written = 0 +if isinstance(data.get("route"), dict): + with open(route_path, "w", encoding="utf-8") as f: + json.dump({"route": data["route"]}, f, ensure_ascii=False, indent=2) + f.write("\n") + route_written = 1 + +outbound_written = 0 +if isinstance(data.get("outbounds"), list): + with open(outbounds_path, "w", encoding="utf-8") as f: + json.dump({"outbounds": data["outbounds"]}, f, ensure_ascii=False, indent=2) + f.write("\n") + outbound_written = 1 + +print(f"ROUTE_WRITTEN={route_written}") +print(f"OUTBOUNDS_WRITTEN={outbound_written}") +PY +)" + while IFS= read -r parsed_line; do + case "$parsed_line" in + ROUTE_WRITTEN=1) route_written=1 ;; + OUTBOUNDS_WRITTEN=1) outbound_written=1 ;; + esac + done <<< "$parsed" + elif command -v jq >/dev/null 2>&1; then + if jq -e '.route | type == "object"' "$source_path" >/dev/null 2>&1; then + jq '{route: .route}' "$source_path" > "$CONFIG_ROUTE_FILE" + route_written=1 + fi + if jq -e '.outbounds | type == "array"' "$source_path" >/dev/null 2>&1; then + jq '{outbounds: .outbounds}' "$source_path" > "$CONFIG_OUTBOUNDS_FILE" + outbound_written=1 + fi + fi + + if [[ "$route_written" -eq 1 || "$outbound_written" -eq 1 ]]; then + echo -e "${YELLOW}Extracted legacy config sections from ${source_path}${NC}" + return 0 + fi + + return 1 +} + +write_default_route_config() { + cat > "$CONFIG_ROUTE_FILE" < "$CONFIG_OUTBOUNDS_FILE" < "$CONFIG_BASE_FILE" < "$CONFIG_ROUTE_FILE" < "$CONFIG_OUTBOUNDS_FILE" <