diff --git a/core/cli_api.py b/core/cli_api.py index 1d61c66..44cfd6f 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -37,7 +37,7 @@ class Command(Enum): LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.sh') SERVER_INFO = os.path.join(SCRIPT_DIR, 'hysteria2', 'server_info.py') BACKUP_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'backup.py') - RESTORE_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'restore.sh') + RESTORE_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'restore.py') INSTALL_TELEGRAMBOT = os.path.join(SCRIPT_DIR, 'telegrambot', 'runbot.py') SHELL_SINGBOX = os.path.join(SCRIPT_DIR, 'singbox', 'singbox_shell.sh') SHELL_WEBPANEL = os.path.join(SCRIPT_DIR, 'webpanel', 'webpanel_shell.sh') @@ -192,7 +192,7 @@ def backup_hysteria2(): def restore_hysteria2(backup_file_path: str): '''Restores Hysteria configuration from the given backup file.''' try: - run_cmd(['bash', Command.RESTORE_HYSTERIA2.value, backup_file_path]) + run_cmd(['python3', Command.RESTORE_HYSTERIA2.value, backup_file_path]) except subprocess.CalledProcessError as e: raise Exception(f"Restore failed: {e}") except Exception as ex: diff --git a/core/scripts/hysteria2/restore.py b/core/scripts/hysteria2/restore.py new file mode 100644 index 0000000..a8a85b7 --- /dev/null +++ b/core/scripts/hysteria2/restore.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import shutil +import zipfile +import tempfile +import subprocess +import datetime +from pathlib import Path +from init_paths import * +from paths import * + +def run_command(command, capture_output=True, check=False): + """Run a shell command and return its output""" + result = subprocess.run( + command, + shell=True, + capture_output=capture_output, + text=True, + check=check + ) + if capture_output: + return result.returncode, result.stdout.strip() + return result.returncode, None + +def main(): + if len(sys.argv) < 2: + print("Error: Backup file path is required.") + return 1 + + backup_zip_file = sys.argv[1] + + if not os.path.isfile(backup_zip_file): + print(f"Error: Backup file not found: {backup_zip_file}") + return 1 + + if not backup_zip_file.lower().endswith('.zip'): + print("Error: Backup file must be a .zip file.") + return 1 + + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + restore_dir = f"/tmp/hysteria_restore_{timestamp}" + target_dir = "/etc/hysteria" + + try: + os.makedirs(restore_dir, exist_ok=True) + + try: + with zipfile.ZipFile(backup_zip_file) as zf: + zf.testzip() + zf.extractall(restore_dir) + except zipfile.BadZipFile: + print("Error: Invalid ZIP file.") + return 1 + except Exception as e: + print(f"Error: Could not extract the ZIP file: {e}") + return 1 + + expected_files = [ + "ca.key", + "ca.crt", + "users.json", + "config.json", + ".configs.env" + ] + + for file in expected_files: + file_path = os.path.join(restore_dir, file) + if not os.path.isfile(file_path): + print(f"Error: Required file '{file}' is missing from the backup.") + return 1 + + existing_backup_dir = f"/opt/hysbackup/restore_pre_backup_{timestamp}" + os.makedirs(existing_backup_dir, exist_ok=True) + + for file in expected_files: + source_file = os.path.join(target_dir, file) + dest_file = os.path.join(existing_backup_dir, file) + + if os.path.isfile(source_file): + try: + shutil.copy2(source_file, dest_file) + except Exception as e: + print(f"Error creating backup file before restore from '{source_file}': {e}") + return 1 + + for file in expected_files: + source_file = os.path.join(restore_dir, file) + dest_file = os.path.join(target_dir, file) + + try: + shutil.copy2(source_file, dest_file) + except Exception as e: + print(f"Error: replace Configuration Files '{file}': {e}") + shutil.rmtree(existing_backup_dir, ignore_errors=True) + return 1 + + config_file = os.path.join(target_dir, "config.json") + + if os.path.isfile(config_file): + print("Checking and adjusting config.json based on system state...") + + ret_code, networkdef = run_command("ip route | grep '^default' | awk '{print $5}'") + networkdef = networkdef.strip() + + if networkdef: + with open(config_file, 'r') as f: + config = json.load(f) + + for outbound in config.get('outbounds', []): + if outbound.get('name') == 'v4' and 'direct' in outbound: + current_v4_device = outbound['direct'].get('bindDevice', '') + + if current_v4_device != networkdef: + print(f"Updating v4 outbound bindDevice from '{current_v4_device}' to '{networkdef}'...") + outbound['direct']['bindDevice'] = networkdef + + with open(config_file, 'w') as f: + json.dump(config, f, indent=2) + + ret_code, _ = run_command("systemctl is-active --quiet wg-quick@wgcf.service", capture_output=False) + + if ret_code != 0: + print("wgcf service is NOT active. Removing warps outbound and any ACL rules...") + + with open(config_file, 'r') as f: + config = json.load(f) + + config['outbounds'] = [outbound for outbound in config.get('outbounds', []) + if outbound.get('name') != 'warps'] + + if 'acl' in config and 'inline' in config['acl']: + config['acl']['inline'] = [rule for rule in config['acl']['inline'] + if not rule.startswith('warps(')] + + with open(config_file, 'w') as f: + json.dump(config, f, indent=2) + + run_command("chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt", + capture_output=False) + run_command("chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt", + capture_output=False) + + ret_code, _ = run_command(f"python3 {CLI_PATH} restart-hysteria2", capture_output=False) + + if ret_code != 0: + print("Error: Restart service failed.") + return 1 + + print("Hysteria configuration restored and updated successfully.") + return 0 + + except Exception as e: + print(f"An unexpected error occurred: {e}") + return 1 + finally: + shutil.rmtree(restore_dir, ignore_errors=True) + if 'existing_backup_dir' in locals(): + shutil.rmtree(existing_backup_dir, ignore_errors=True) + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/core/scripts/hysteria2/restore.sh b/core/scripts/hysteria2/restore.sh deleted file mode 100644 index c581103..0000000 --- a/core/scripts/hysteria2/restore.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash -source /etc/hysteria/core/scripts/path.sh - -# Usage: ./restore.sh - -set -e - -BACKUP_ZIP_FILE="$1" -RESTORE_DIR="/tmp/hysteria_restore_$(date +%Y%m%d_%H%M%S)" -TARGET_DIR="/etc/hysteria" - -if [ -z "$BACKUP_ZIP_FILE" ]; then - echo "Error: Backup file path is required." - exit 1 -fi - -if [ ! -f "$BACKUP_ZIP_FILE" ]; then - echo "Error: Backup file not found: $BACKUP_ZIP_FILE" - exit 1 -fi - -if [[ "$BACKUP_ZIP_FILE" != *.zip ]]; then - echo "Error: Backup file must be a .zip file." - exit 1 -fi - -mkdir -p "$RESTORE_DIR" - -unzip -l "$BACKUP_ZIP_FILE" >/dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "Error: Invalid ZIP file." - rm -rf "$RESTORE_DIR" - exit 1 -fi - -unzip -o "$BACKUP_ZIP_FILE" -d "$RESTORE_DIR" >/dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "Error: Could not extract the ZIP file." - rm -rf "$RESTORE_DIR" - exit 1 -fi - -expected_files=( - "ca.key" - "ca.crt" - "users.json" - "config.json" - ".configs.env" -) - -for file in "${expected_files[@]}"; do - if [ ! -f "$RESTORE_DIR/$file" ]; then - echo "Error: Required file '$file' is missing from the backup." - rm -rf "$RESTORE_DIR" - exit 1 - fi - if [ ! -f "$RESTORE_DIR/$file" ]; then - echo "Error: '$file' in the backup is not a regular file." - rm -rf "$RESTORE_DIR" - exit 1 - fi -done - -timestamp=$(date +%Y%m%d_%H%M%S) -existing_backup_dir="/opt/hysbackup/restore_pre_backup_$timestamp" -mkdir -p "$existing_backup_dir" -for file in "${expected_files[@]}"; do - if [ -f "$TARGET_DIR/$file" ]; then - cp -p "$TARGET_DIR/$file" "$existing_backup_dir/$file" - if [ $? -ne 0 ]; then - echo "Error creating backup file before restore from '$TARGET_DIR/$file'." - exit 1 - fi - fi -done - -for file in "${expected_files[@]}"; do - cp -p "$RESTORE_DIR/$file" "$TARGET_DIR/$file" - if [ $? -ne 0 ]; then - echo "Error: replace Configuration Files '$file'." - rm -rf "$existing_backup_dir" - rm -rf "$RESTORE_DIR" - exit 1 - fi -done - - -CONFIG_FILE="$TARGET_DIR/config.json" - -if [ -f "$CONFIG_FILE" ]; then - echo "Checking and adjusting config.json based on system state..." - - networkdef=$(ip route | grep "^default" | awk '{print $5}') - - if [ -n "$networkdef" ]; then - current_v4_device=$(jq -r '.outbounds[] | select(.name=="v4") | .direct.bindDevice' "$CONFIG_FILE") - - if [ "$current_v4_device" != "$networkdef" ]; then - echo "Updating v4 outbound bindDevice from '$current_v4_device' to '$networkdef'..." - - tmpfile=$(mktemp) - jq --arg newdev "$networkdef" ' - .outbounds = (.outbounds | map( - if .name == "v4" then - .direct.bindDevice = $newdev - else - . - end - )) - ' "$CONFIG_FILE" > "$tmpfile" - - cat "$tmpfile" > "$CONFIG_FILE" - rm -f "$tmpfile" - fi - fi - - if ! systemctl is-active --quiet wg-quick@wgcf.service; then - echo "wgcf service is NOT active. Removing warps outbound and any ACL rules..." - - tmpfile=$(mktemp) - jq ' - .outbounds = (.outbounds | map(select(.name != "warps"))) | - .acl.inline = (.acl.inline | map( - select(test("^warps\\(") | not) - )) - ' "$CONFIG_FILE" > "$tmpfile" - - cat "$tmpfile" > "$CONFIG_FILE" - rm -f "$tmpfile" - fi -fi - -rm -rf "$RESTORE_DIR" -echo "Hysteria configuration restored and updated successfully." - -chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt -chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt - -python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "Error: Restart service failed'." - rm -rf "$existing_backup_dir" - exit 1 -fi - -if [[ "$existing_backup_dir" != "" ]]; then - rm -rf "$existing_backup_dir" -fi - -exit 0 \ No newline at end of file