Merge pull request #137 from ReturnFI/beta

Features & Refactors
This commit is contained in:
Whispering Wind
2025-05-10 14:45:33 +03:30
committed by GitHub
19 changed files with 1154 additions and 585 deletions

View File

@ -1,10 +1,20 @@
## [1.9.1] - 2025-05-03 # [1.9.2] - 2025-05-10
### ✨ Changed ## ✨ Changed
🧠 **refactor:**: Implement server stats manager in Python
🧠 **refactor:**: Migrate IP address config management to Python ### ✨ Features
🧠 **refactor:**: Implement Hysteria backup functionality in Python 🔄 feat: Merge traffic collection with user kicking for efficient enforcement
🧠 **refactor:**: Transition Telegram bot service management to Python 🧪 feat(api): Add user existence validation to `add-user` endpoint
🧠 **refactor:**: Migrate WARP ACL configuration handling to Python 🖥️ feat(frontend): Improve user management UI and error feedback in `users.html`
🧠 **refactor:**: Implement WARP status management in Python
🧠 **refactor:**: Rewrite WARP setup and uninstallation scripts in Python3 ### 🧠 Refactors
📦 refactor: Migrate Hysteria restore functionality to Python
🌐 refactor: Implement SNI changer in Python
🛠️ refactor: Port TCP Brutal installation to Python
🎭 refactor: Rewrite masquerade management in Python3
⚙️ refactor(cli): Enhance error propagation and handling in CLI scripts
### 🛠️ System Enhancements
🧩 update: Improve upgrade script with better cronjob management
🛠️ enhance: Setup script with robust error handling and cleaner UX
📦 update: Drop Debian 11 support to focus on supported OS versions

View File

@ -227,7 +227,7 @@ def show_user_uri_json(usernames: list[str]):
@cli.command('traffic-status') @cli.command('traffic-status')
@click.option('--no-gui', is_flag=True, help='Retrieve traffic data without displaying output') @click.option('--no-gui', is_flag=True, help='Retrieve traffic data without displaying output and kick expired users')
def traffic_status(no_gui): def traffic_status(no_gui):
try: try:
cli_api.traffic_status(no_gui=no_gui) cli_api.traffic_status(no_gui=no_gui)

View File

@ -21,7 +21,7 @@ class Command(Enum):
UPDATE_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'update.py') UPDATE_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'update.py')
RESTART_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'restart.py') RESTART_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'restart.py')
CHANGE_PORT_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'change_port.py') CHANGE_PORT_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'change_port.py')
CHANGE_SNI_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'change_sni.sh') CHANGE_SNI_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'change_sni.py')
GET_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'get_user.py') GET_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'get_user.py')
ADD_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'add_user.py') ADD_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'add_user.py')
EDIT_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'edit_user.sh') EDIT_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'edit_user.sh')
@ -31,18 +31,18 @@ class Command(Enum):
WRAPPER_URI = os.path.join(SCRIPT_DIR, 'hysteria2', 'wrapper_uri.py') WRAPPER_URI = os.path.join(SCRIPT_DIR, 'hysteria2', 'wrapper_uri.py')
IP_ADD = os.path.join(SCRIPT_DIR, 'hysteria2', 'ip.py') IP_ADD = os.path.join(SCRIPT_DIR, 'hysteria2', 'ip.py')
MANAGE_OBFS = os.path.join(SCRIPT_DIR, 'hysteria2', 'manage_obfs.py') MANAGE_OBFS = os.path.join(SCRIPT_DIR, 'hysteria2', 'manage_obfs.py')
MASQUERADE_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'masquerade.sh') MASQUERADE_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'masquerade.py')
TRAFFIC_STATUS = 'traffic.py' # won't be called directly (it's a python module) TRAFFIC_STATUS = 'traffic.py' # won't be called directly (it's a python module)
UPDATE_GEO = os.path.join(SCRIPT_DIR, 'hysteria2', 'update_geo.py') UPDATE_GEO = os.path.join(SCRIPT_DIR, 'hysteria2', 'update_geo.py')
LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.sh') LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.sh')
SERVER_INFO = os.path.join(SCRIPT_DIR, 'hysteria2', 'server_info.py') SERVER_INFO = os.path.join(SCRIPT_DIR, 'hysteria2', 'server_info.py')
BACKUP_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'backup.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') INSTALL_TELEGRAMBOT = os.path.join(SCRIPT_DIR, 'telegrambot', 'runbot.py')
SHELL_SINGBOX = os.path.join(SCRIPT_DIR, 'singbox', 'singbox_shell.sh') SHELL_SINGBOX = os.path.join(SCRIPT_DIR, 'singbox', 'singbox_shell.sh')
SHELL_WEBPANEL = os.path.join(SCRIPT_DIR, 'webpanel', 'webpanel_shell.sh') SHELL_WEBPANEL = os.path.join(SCRIPT_DIR, 'webpanel', 'webpanel_shell.sh')
INSTALL_NORMALSUB = os.path.join(SCRIPT_DIR, 'normalsub', 'normalsub.sh') INSTALL_NORMALSUB = os.path.join(SCRIPT_DIR, 'normalsub', 'normalsub.sh')
INSTALL_TCP_BRUTAL = os.path.join(SCRIPT_DIR, 'tcp-brutal', 'install.sh') INSTALL_TCP_BRUTAL = os.path.join(SCRIPT_DIR, 'tcp-brutal', 'install.py')
INSTALL_WARP = os.path.join(SCRIPT_DIR, 'warp', 'install.py') INSTALL_WARP = os.path.join(SCRIPT_DIR, 'warp', 'install.py')
UNINSTALL_WARP = os.path.join(SCRIPT_DIR, 'warp', 'uninstall.py') UNINSTALL_WARP = os.path.join(SCRIPT_DIR, 'warp', 'uninstall.py')
CONFIGURE_WARP = os.path.join(SCRIPT_DIR, 'warp', 'configure.py') CONFIGURE_WARP = os.path.join(SCRIPT_DIR, 'warp', 'configure.py')
@ -83,24 +83,32 @@ class ScriptNotFoundError(HysteriaError):
# region Utils # region Utils
def run_cmd(command: list[str]) -> str | None: def run_cmd(command: list[str]) -> str:
''' '''
Runs a command and returns the output. Runs a command and returns its stdout if successful.
Could raise subprocess.CalledProcessError Raises CommandExecutionError if the command fails (non-zero exit code) or cannot be found.
''' '''
if (DEBUG) and not (Command.GET_USER.value in command or Command.LIST_USERS.value in command): if DEBUG:
print(' '.join(command)) print(f"Executing command: {' '.join(command)}")
try: try:
result = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=False) process = subprocess.run(command, capture_output=True, text=True, shell=False, check=False)
if result:
result = result.decode().strip() if process.returncode != 0:
return result error_output = process.stderr.strip() if process.stderr.strip() else process.stdout.strip()
except subprocess.CalledProcessError as e: if not error_output:
if DEBUG: error_output = f"Command exited with status {process.returncode} without specific error message."
raise CommandExecutionError(f'Command execution failed: {e}\nOutput: {e.output.decode()}')
else: detailed_error_message = f"Command '{' '.join(command)}' failed with exit code {process.returncode}: {error_output}"
return None raise CommandExecutionError(detailed_error_message)
return None
return process.stdout.strip() if process.stdout else ""
except FileNotFoundError as e:
raise ScriptNotFoundError(f"Script or command not found: {command[0]}. Original error: {e}")
except subprocess.TimeoutExpired as e:
raise CommandExecutionError(f"Command '{' '.join(command)}' timed out. Original error: {e}")
except OSError as e:
raise CommandExecutionError(f"OS error while trying to run command '{' '.join(command)}': {e}")
def generate_password() -> str: def generate_password() -> str:
@ -176,7 +184,7 @@ def change_hysteria2_sni(sni: str):
''' '''
Changes the SNI for Hysteria2. Changes the SNI for Hysteria2.
''' '''
run_cmd(['bash', Command.CHANGE_SNI_HYSTERIA2.value, sni]) run_cmd(['python3', Command.CHANGE_SNI_HYSTERIA2.value, sni])
def backup_hysteria2(): def backup_hysteria2():
@ -192,7 +200,7 @@ def backup_hysteria2():
def restore_hysteria2(backup_file_path: str): def restore_hysteria2(backup_file_path: str):
'''Restores Hysteria configuration from the given backup file.''' '''Restores Hysteria configuration from the given backup file.'''
try: 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: except subprocess.CalledProcessError as e:
raise Exception(f"Restore failed: {e}") raise Exception(f"Restore failed: {e}")
except Exception as ex: except Exception as ex:
@ -211,12 +219,12 @@ def disable_hysteria2_obfs():
def enable_hysteria2_masquerade(domain: str): def enable_hysteria2_masquerade(domain: str):
'''Enables masquerade for Hysteria2.''' '''Enables masquerade for Hysteria2.'''
run_cmd(['bash', Command.MASQUERADE_SCRIPT.value, '1', domain]) run_cmd(['python3', Command.MASQUERADE_SCRIPT.value, '1', domain])
def disable_hysteria2_masquerade(): def disable_hysteria2_masquerade():
'''Disables masquerade for Hysteria2.''' '''Disables masquerade for Hysteria2.'''
run_cmd(['bash', Command.MASQUERADE_SCRIPT.value, '2']) run_cmd(['python3', Command.MASQUERADE_SCRIPT.value, '2'])
def get_hysteria2_config_file() -> dict[str, Any]: def get_hysteria2_config_file() -> dict[str, Any]:
@ -363,8 +371,12 @@ def show_user_uri_json(usernames: list[str]) -> list[dict[str, Any]] | None:
def traffic_status(no_gui=False, display_output=True): def traffic_status(no_gui=False, display_output=True):
'''Fetches traffic status.''' if no_gui:
data = traffic.traffic_status(no_gui=True if not display_output else no_gui) data = traffic.traffic_status(no_gui=True)
traffic.kick_expired_users()
else:
data = traffic.traffic_status(no_gui=True if not display_output else no_gui)
return data return data
@ -431,7 +443,7 @@ def update_geo(country: str):
def install_tcp_brutal(): def install_tcp_brutal():
'''Installs TCP Brutal.''' '''Installs TCP Brutal.'''
run_cmd(['bash', Command.INSTALL_TCP_BRUTAL.value]) run_cmd(['python3', Command.INSTALL_TCP_BRUTAL.value])
def install_warp(): def install_warp():

View File

@ -0,0 +1,212 @@
#!/usr/bin/env python3
import os
import sys
import json
import time
import subprocess
import socket
from pathlib import Path
from init_paths import *
from paths import *
def run_command(command, capture_output=True, shell=True):
"""Run a shell command and return its output"""
result = subprocess.run(
command,
shell=shell,
capture_output=capture_output,
text=True
)
if capture_output:
return result.stdout.strip()
return None
def get_ip_from_domain(domain):
"""Get the first IPv4 address from a domain using dig"""
try:
output = run_command(f"dig +short {domain} A | head -n 1")
if output and is_valid_ipv4(output):
return output
except:
pass
return None
def is_valid_ipv4(ip):
"""Check if a string is a valid IPv4 address"""
try:
socket.inet_pton(socket.AF_INET, ip)
return True
except (socket.error, ValueError):
return False
def get_server_ip():
"""Get the server's public IP address"""
return run_command("curl -s -4 ifconfig.me")
def update_sni(sni):
if not sni:
print("Invalid SNI. Please provide a valid SNI.")
print(f"Example: {sys.argv[0]} yourdomain.com")
return 1
if os.path.isfile(CONFIG_ENV):
env_vars = {}
with open(CONFIG_ENV, 'r') as f:
for line in f:
if '=' in line:
name, value = line.strip().split('=', 1)
env_vars[name] = value
else:
print(f"Error: Config file {CONFIG_ENV} not found.")
return 1
server_ip = None
if 'IP4' in env_vars:
ip4 = env_vars['IP4']
if is_valid_ipv4(ip4):
server_ip = ip4
print(f"Using server IP from config: {server_ip}")
else:
domain_ip = get_ip_from_domain(ip4)
if domain_ip:
server_ip = domain_ip
print(f"Resolved domain {ip4} to IP: {server_ip}")
else:
server_ip = get_server_ip()
print(f"Could not resolve domain {ip4}. Using auto-detected server IP: {server_ip}")
else:
server_ip = get_server_ip()
print(f"Using auto-detected server IP: {server_ip}")
print(f"Checking if {sni} points to this server ({server_ip})...")
domain_ip = get_ip_from_domain(sni)
use_certbot = False
if not domain_ip:
print(f"Warning: Could not resolve {sni} to an IPv4 address.")
elif domain_ip == server_ip:
print(f"Success: {sni} correctly points to this server ({server_ip}).")
use_certbot = True
else:
print(f"Notice: {sni} points to {domain_ip}, not to this server ({server_ip}).")
os.chdir('/etc/hysteria/')
if use_certbot:
print(f"Using certbot to obtain a valid certificate for {sni}...")
certbot_output = run_command(f"certbot certificates")
if sni in certbot_output:
print(f"Certificate for {sni} already exists. Renewing...")
run_command(f"certbot renew --cert-name {sni}", capture_output=False)
else:
print(f"Requesting new certificate for {sni}...")
run_command(f"certbot certonly --standalone -d {sni} --non-interactive --agree-tos --email admin@{sni}",
capture_output=False)
run_command(f"cp /etc/letsencrypt/live/{sni}/fullchain.pem /etc/hysteria/ca.crt", capture_output=False)
run_command(f"cp /etc/letsencrypt/live/{sni}/privkey.pem /etc/hysteria/ca.key", capture_output=False)
print("Certificates successfully installed from Let's Encrypt.")
if os.path.isfile(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
config['tls']['insecure'] = False
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
print(f"TLS insecure flag set to false in {CONFIG_FILE}")
else:
print(f"Using self-signed certificate with openssl for {sni}...")
if os.path.exists("ca.key"):
os.remove("ca.key")
if os.path.exists("ca.crt"):
os.remove("ca.crt")
print(f"Generating CA key and certificate for SNI: {sni} ...")
run_command("openssl ecparam -genkey -name prime256v1 -out ca.key > /dev/null 2>&1", capture_output=False)
run_command(f"openssl req -new -x509 -days 36500 -key ca.key -out ca.crt -subj '/CN={sni}' > /dev/null 2>&1",
capture_output=False)
print(f"Self-signed certificate generated for {sni}")
if os.path.isfile(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
config['tls']['insecure'] = True
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
print(f"TLS insecure flag set to true in {CONFIG_FILE}")
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)
sha256 = run_command(
"openssl x509 -noout -fingerprint -sha256 -inform pem -in ca.crt | sed 's/.*=//;s///g'"
)
print(f"SHA-256 fingerprint generated: {sha256}")
if os.path.isfile(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
config['tls']['pinSHA256'] = sha256
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
print(f"SHA-256 updated successfully in {CONFIG_FILE}")
else:
print(f"Error: Config file {CONFIG_FILE} not found.")
return 1
sni_found = False
if os.path.isfile(CONFIG_ENV):
with open(CONFIG_ENV, 'r') as f:
lines = f.readlines()
with open(CONFIG_ENV, 'w') as f:
for line in lines:
if line.startswith('SNI='):
f.write(f'SNI={sni}\n')
sni_found = True
else:
f.write(line)
if not sni_found:
f.write(f'SNI={sni}\n')
print(f"Added new SNI entry to {CONFIG_ENV}")
else:
print(f"SNI updated successfully in {CONFIG_ENV}")
else:
with open(CONFIG_ENV, 'w') as f:
f.write(f'SNI={sni}\n')
print(f"Created {CONFIG_ENV} with new SNI.")
run_command(f"python3 {CLI_PATH} restart-hysteria2 > /dev/null 2>&1", capture_output=False)
print(f"Hysteria2 restarted successfully with new SNI: {sni}.")
if use_certbot:
print(f"✅ Valid Let's Encrypt certificate installed for {sni}")
print(" TLS insecure mode is now DISABLED")
else:
print(f"⚠️ Self-signed certificate installed for {sni}")
print(" TLS insecure mode is now ENABLED")
print(" (This certificate won't be trusted by browsers)")
return 0
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <sni>")
sys.exit(1)
sni = sys.argv[1]
sys.exit(update_sni(sni))

View File

@ -1,134 +0,0 @@
#!/bin/bash
source /etc/hysteria/core/scripts/path.sh
sni="$1"
if [ -f "$CONFIG_ENV" ]; then
source "$CONFIG_ENV"
else
echo "Error: Config file $CONFIG_ENV not found."
exit 1
fi
update_sni() {
local sni=$1
local server_ip
if [ -z "$sni" ]; then
echo "Invalid SNI. Please provide a valid SNI."
echo "Example: $0 yourdomain.com"
return 1
fi
if [ -n "$IP4" ]; then
if [[ $IP4 =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
server_ip="$IP4"
echo "Using server IP from config: $server_ip"
else
domain_ip=$(dig +short "$IP4" A | head -n 1)
if [ -n "$domain_ip" ]; then
server_ip="$domain_ip"
echo "Resolved domain $IP4 to IP: $server_ip"
else
server_ip=$(curl -s -4 ifconfig.me)
echo "Could not resolve domain $IP4. Using auto-detected server IP: $server_ip"
fi
fi
else
server_ip=$(curl -s -4 ifconfig.me)
echo "Using auto-detected server IP: $server_ip"
fi
echo "Checking if $sni points to this server ($server_ip)..."
domain_ip=$(dig +short "$sni" A | head -n 1)
if [ -z "$domain_ip" ]; then
echo "Warning: Could not resolve $sni to an IPv4 address."
use_certbot=false
elif [ "$domain_ip" = "$server_ip" ]; then
echo "Success: $sni correctly points to this server ($server_ip)."
use_certbot=true
else
echo "Notice: $sni points to $domain_ip, not to this server ($server_ip)."
use_certbot=false
fi
cd /etc/hysteria/ || exit
if [ "$use_certbot" = true ]; then
echo "Using certbot to obtain a valid certificate for $sni..."
if certbot certificates | grep -q "$sni"; then
echo "Certificate for $sni already exists. Renewing..."
certbot renew --cert-name "$sni"
else
echo "Requesting new certificate for $sni..."
certbot certonly --standalone -d "$sni" --non-interactive --agree-tos --email admin@"$sni"
fi
cp /etc/letsencrypt/live/"$sni"/fullchain.pem /etc/hysteria/ca.crt
cp /etc/letsencrypt/live/"$sni"/privkey.pem /etc/hysteria/ca.key
echo "Certificates successfully installed from Let's Encrypt."
if [ -f "$CONFIG_FILE" ]; then
jq '.tls.insecure = false' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
echo "TLS insecure flag set to false in $CONFIG_FILE"
fi
else
echo "Using self-signed certificate with openssl for $sni..."
rm -f ca.key ca.crt
echo "Generating CA key and certificate for SNI: $sni ..."
openssl ecparam -genkey -name prime256v1 -out ca.key >/dev/null 2>&1
openssl req -new -x509 -days 36500 -key ca.key -out ca.crt -subj "/CN=$sni" >/dev/null 2>&1
echo "Self-signed certificate generated for $sni"
if [ -f "$CONFIG_FILE" ]; then
jq '.tls.insecure = true' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
echo "TLS insecure flag set to true in $CONFIG_FILE"
fi
fi
chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt
chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt
sha256=$(openssl x509 -noout -fingerprint -sha256 -inform pem -in ca.crt | sed 's/.*=//;s///g')
echo "SHA-256 fingerprint generated: $sha256"
if [ -f "$CONFIG_FILE" ]; then
jq --arg sha256 "$sha256" '.tls.pinSHA256 = $sha256' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
echo "SHA-256 updated successfully in $CONFIG_FILE"
else
echo "Error: Config file $CONFIG_FILE not found."
return 1
fi
if [ -f "$CONFIG_ENV" ]; then
if grep -q "^SNI=" "$CONFIG_ENV"; then
sed -i "s/^SNI=.*$/SNI=$sni/" "$CONFIG_ENV"
echo "SNI updated successfully in $CONFIG_ENV"
else
echo "SNI=$sni" >> "$CONFIG_ENV"
echo "Added new SNI entry to $CONFIG_ENV"
fi
else
echo "SNI=$sni" > "$CONFIG_ENV"
echo "Created $CONFIG_ENV with new SNI."
fi
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
echo "Hysteria2 restarted successfully with new SNI: $sni."
if [ "$use_certbot" = true ]; then
echo "✅ Valid Let's Encrypt certificate installed for $sni"
echo " TLS insecure mode is now DISABLED"
else
echo "⚠️ Self-signed certificate installed for $sni"
echo " TLS insecure mode is now ENABLED"
echo " (This certificate won't be trusted by browsers)"
fi
}
update_sni "$sni"

View File

@ -80,12 +80,12 @@ install_hysteria() {
fi fi
chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh
chmod +x /etc/hysteria/core/scripts/hysteria2/kick.sh chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py
(crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status' >/dev/null 2>&1") | crontab - (crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status --no-gui'") | crontab -
(crontab -l ; echo "0 3 */3 * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py restart-hysteria2' >/dev/null 2>&1") | crontab - # (crontab -l ; echo "0 3 */3 * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py restart-hysteria2' >/dev/null 2>&1") | crontab -
(crontab -l ; echo "0 */6 * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria' >/dev/null 2>&1") | crontab - (crontab -l ; echo "0 */6 * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria' >/dev/null 2>&1") | crontab -
(crontab -l ; echo "*/1 * * * * /etc/hysteria/core/scripts/hysteria2/kick.sh >/dev/null 2>&1") | crontab - # (crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/scripts/hysteria2/kick.py' >/dev/null 2>&1") | crontab -
} }

View File

@ -0,0 +1,178 @@
#!/usr/bin/env python3
import os
import sys
import json
import time
import fcntl
import shutil
import datetime
from concurrent.futures import ThreadPoolExecutor
from init_paths import *
from paths import *
from hysteria2_api import Hysteria2Client
import logging
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format='%(asctime)s: [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger()
LOCKFILE = "/tmp/kick.lock"
BACKUP_FILE = f"{USERS_FILE}.bak"
MAX_WORKERS = 8
def acquire_lock():
try:
lock_file = open(LOCKFILE, 'w')
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
return lock_file
except IOError:
logger.warning("Another instance is already running. Exiting.")
sys.exit(1)
def kick_users(usernames, secret):
try:
client = Hysteria2Client(
base_url="http://127.0.0.1:25413",
secret=secret
)
client.kick_clients(usernames)
logger.info(f"Successfully kicked {len(usernames)} users: {', '.join(usernames)}")
return True
except Exception as e:
logger.error(f"Error kicking users: {str(e)}")
return False
def process_user(username, user_data, config_secret, users_data):
blocked = user_data.get('blocked', False)
if blocked:
logger.info(f"Skipping {username} as they are already blocked.")
return None
max_download_bytes = user_data.get('max_download_bytes', 0)
expiration_days = user_data.get('expiration_days', 0)
account_creation_date = user_data.get('account_creation_date')
current_download_bytes = user_data.get('download_bytes', 0)
current_upload_bytes = user_data.get('upload_bytes', 0)
total_bytes = current_download_bytes + current_upload_bytes
if not account_creation_date:
logger.info(f"Skipping {username} due to missing account creation date.")
return None
try:
current_date = datetime.datetime.now().timestamp()
creation_date = datetime.datetime.fromisoformat(account_creation_date.replace('Z', '+00:00'))
expiration_date = (creation_date + datetime.timedelta(days=expiration_days)).timestamp()
should_block = False
if max_download_bytes > 0 and total_bytes >= 0 and expiration_days > 0:
if total_bytes >= max_download_bytes or current_date >= expiration_date:
should_block = True
if should_block:
logger.info(f"Setting blocked=True for user {username}")
users_data[username]['blocked'] = True
return username
else:
logger.info(f"Skipping {username} due to invalid or missing data.")
return None
except Exception as e:
logger.error(f"Error processing user {username}: {str(e)}")
return None
return None
def main():
lock_file = acquire_lock()
try:
shutil.copy2(USERS_FILE, BACKUP_FILE)
logger.info(f"Created backup of users file at {BACKUP_FILE}")
try:
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
secret = config.get('trafficStats', {}).get('secret', '')
if not secret:
logger.error("No secret found in config file")
sys.exit(1)
except Exception as e:
logger.error(f"Failed to load config file: {str(e)}")
shutil.copy2(BACKUP_FILE, USERS_FILE)
sys.exit(1)
try:
with open(USERS_FILE, 'r') as f:
users_data = json.load(f)
logger.info(f"Loaded data for {len(users_data)} users")
except json.JSONDecodeError:
logger.error("Invalid users.json. Restoring backup.")
shutil.copy2(BACKUP_FILE, USERS_FILE)
sys.exit(1)
except Exception as e:
logger.error(f"Failed to load users file: {str(e)}")
shutil.copy2(BACKUP_FILE, USERS_FILE)
sys.exit(1)
users_to_kick = []
logger.info(f"Processing {len(users_data)} users in parallel with {MAX_WORKERS} workers")
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_user = {
executor.submit(process_user, username, user_data, secret, users_data): username
for username, user_data in users_data.items()
}
for future in future_to_user:
username = future.result()
if username:
users_to_kick.append(username)
logger.info(f"User {username} added to kick list")
if users_to_kick:
logger.info(f"Saving changes to users file for {len(users_to_kick)} blocked users")
for retry in range(3):
try:
with open(USERS_FILE, 'w') as f:
json.dump(users_data, f, indent=2)
break
except Exception as e:
logger.error(f"Failed to save users file (attempt {retry+1}): {str(e)}")
time.sleep(1)
if retry == 2:
raise
if users_to_kick:
logger.info(f"Kicking {len(users_to_kick)} users")
batch_size = 50
for i in range(0, len(users_to_kick), batch_size):
batch = users_to_kick[i:i+batch_size]
logger.info(f"Processing batch of {len(batch)} users")
kick_users(batch, secret)
for username in batch:
logger.info(f"Blocked and kicked user {username}")
else:
logger.info("No users to kick")
except Exception as e:
logger.error(f"An error occurred: {str(e)}")
logger.info("Restoring users file from backup")
shutil.copy2(BACKUP_FILE, USERS_FILE)
sys.exit(1)
finally:
fcntl.flock(lock_file, fcntl.LOCK_UN)
lock_file.close()
logger.info("Script completed")
if __name__ == "__main__":
main()

View File

@ -1,76 +0,0 @@
#!/bin/bash
source /etc/hysteria/core/scripts/path.sh
LOCKFILE="/tmp/kick.lock"
exec 200>$LOCKFILE
flock -n 200 || exit 1
LOGFILE="/var/log/kick.log"
BACKUP_FILE="${USERS_FILE}.bak"
cp "$USERS_FILE" "$BACKUP_FILE"
kick_user() {
local username=$1
local secret=$2
local kick_endpoint="http://127.0.0.1:25413/kick"
curl -s -H "Authorization: $secret" -X POST -d "[\"$username\"]" "$kick_endpoint"
}
SECRET=$(jq -r '.trafficStats.secret' "$CONFIG_FILE")
if ! jq empty "$USERS_FILE"; then
echo "$(date): [ERROR] Invalid users.json. Restoring backup." >> $LOGFILE
cp "$BACKUP_FILE" "$USERS_FILE"
exit 1
fi
handle_error() {
echo "$(date): [ERROR] An error occurred. Restoring backup." >> $LOGFILE
cp "$BACKUP_FILE" "$USERS_FILE"
exit 1
}
trap handle_error ERR
for USERNAME in $(jq -r 'keys[]' "$USERS_FILE"); do
BLOCKED=$(jq -r --arg user "$USERNAME" '.[$user].blocked // false' "$USERS_FILE")
if [ "$BLOCKED" == "true" ]; then
echo "$(date): [INFO] Skipping $USERNAME as they are already blocked." >> $LOGFILE
continue
fi
MAX_DOWNLOAD_BYTES=$(jq -r --arg user "$USERNAME" '.[$user].max_download_bytes // 0' "$USERS_FILE")
EXPIRATION_DAYS=$(jq -r --arg user "$USERNAME" '.[$user].expiration_days // 0' "$USERS_FILE")
ACCOUNT_CREATION_DATE=$(jq -r --arg user "$USERNAME" '.[$user].account_creation_date' "$USERS_FILE")
CURRENT_DOWNLOAD_BYTES=$(jq -r --arg user "$USERNAME" '.[$user].download_bytes // 0' "$USERS_FILE")
CURRENT_UPLOAD_BYTES=$(jq -r --arg user "$USERNAME" '.[$user].upload_bytes // 0' "$USERS_FILE")
TOTAL_BYTES=$((CURRENT_DOWNLOAD_BYTES + CURRENT_UPLOAD_BYTES))
if [ -z "$ACCOUNT_CREATION_DATE" ]; then
echo "$(date): [INFO] Skipping $USERNAME due to missing account creation date." >> $LOGFILE
continue
fi
CURRENT_DATE=$(date +%s)
EXPIRATION_DATE=$(date -d "$ACCOUNT_CREATION_DATE + $EXPIRATION_DAYS days" +%s)
if [ "$MAX_DOWNLOAD_BYTES" -gt 0 ] && [ "$TOTAL_BYTES" -ge 0 ] && [ "$EXPIRATION_DAYS" -gt 0 ]; then
if [ "$TOTAL_BYTES" -ge "$MAX_DOWNLOAD_BYTES" ] || [ "$CURRENT_DATE" -ge "$EXPIRATION_DATE" ]; then
for i in {1..3}; do
jq --arg user "$USERNAME" '.[$user].blocked = true' "$USERS_FILE" > temp.json && mv temp.json "$USERS_FILE" && break
sleep 1
done
kick_user "$USERNAME" "$SECRET"
echo "$(date): [INFO] Blocked and kicked user $USERNAME." >> $LOGFILE
fi
else
echo "$(date): [INFO] Skipping $USERNAME due to invalid or missing data." >> $LOGFILE
fi
done
# echo "$(date): [INFO] Kick script completed successfully." >> $LOGFILE
# exit 0

View File

@ -0,0 +1,93 @@
import json
import subprocess
import sys
from init_paths import *
from paths import *
def is_masquerade_enabled():
try:
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
return "masquerade" in config
except Exception as e:
print(f"Error reading config: {e}")
return False
def enable_masquerade(domain: str):
if is_masquerade_enabled():
print("Masquerade is already enabled.")
sys.exit(0)
url = f"https://{domain}"
try:
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
config["masquerade"] = {
"type": "proxy",
"proxy": {
"url": url,
"rewriteHost": True
},
"listenHTTP": ":80",
"listenHTTPS": ":443",
"forceHTTPS": True
}
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
print(f"Masquerade enabled with URL: {url}")
subprocess.run(["python3", CLI_PATH, "restart-hysteria2"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception as e:
print(f"Failed to enable masquerade: {e}")
sys.exit(1)
def remove_masquerade():
if not is_masquerade_enabled():
print("Masquerade is not enabled.")
sys.exit(0)
try:
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
config.pop("masquerade", None)
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
print("Masquerade removed from config.json")
subprocess.run(["python3", CLI_PATH, "restart-hysteria2"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception as e:
print(f"Failed to remove masquerade: {e}")
sys.exit(1)
def main():
if len(sys.argv) < 2:
print("Usage: python3 masquerade.py {1|2} [domain]")
print("1: Enable Masquerade [domain]")
print("2: Remove Masquerade")
sys.exit(1)
action = sys.argv[1]
if action == "1":
if len(sys.argv) < 3:
print("Error: Missing domain argument for enabling masquerade.")
sys.exit(1)
domain = sys.argv[2]
print(f"Enabling 'masquerade' with URL: {domain}...")
enable_masquerade(domain)
elif action == "2":
print("Removing 'masquerade' from config.json...")
remove_masquerade()
else:
print("Invalid option. Use 1 to enable or 2 to disable masquerade.")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,40 +0,0 @@
#!/bin/bash
source /etc/hysteria/core/scripts/path.sh
function is_masquerade_enabled() {
jq -e '.masquerade' $CONFIG_FILE > /dev/null 2>&1
}
function enable_masquerade() {
if is_masquerade_enabled; then
echo "Masquerade is already enabled."
exit 0
fi
url="https://$1"
jq --arg url "$url" '. + {masquerade: {type: "proxy", proxy: {url: $url, rewriteHost: true}, listenHTTP: ":80", listenHTTPS: ":443", forceHTTPS: true}}' $CONFIG_FILE > tmp.json && mv tmp.json $CONFIG_FILE
echo "Masquerade enabled with URL: $url"
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
}
function remove_masquerade() {
if ! is_masquerade_enabled; then
echo "Masquerade is not enabled."
exit 0
fi
jq 'del(.masquerade)' $CONFIG_FILE > tmp.json && mv tmp.json $CONFIG_FILE
echo "Masquerade removed from config.json"
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
}
if [[ "$1" == "1" ]]; then
echo "Enabling 'masquerade' with URL: $2..."
enable_masquerade "$2"
elif [[ "$1" == "2" ]]; then
echo "Removing 'masquerade' from config.json..."
remove_masquerade
else
echo "Usage: $0 {1|2} [domain]"
echo "1: Enable Masquerade [domain]"
echo "2: Remove Masquerade"
exit 1
fi

View File

@ -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())

View File

@ -1,150 +0,0 @@
#!/bin/bash
source /etc/hysteria/core/scripts/path.sh
# Usage: ./restore.sh <backup_zip_file>
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

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import os
import subprocess
def main():
print("Installing TCP Brutal...")
subprocess.run("bash -c \"$(curl -fsSL https://tcp.hy2.sh/)\"", shell=True)
os.system('clear')
print("TCP Brutal installation complete.")
if __name__ == "__main__":
main()

View File

@ -1,6 +0,0 @@
#!/bin/bash
echo "Installing TCP Brutal..."
bash <(curl -fsSL https://tcp.hy2.sh/)
sleep 3
clear
echo "TCP Brutal installation complete."

View File

@ -1,3 +1,4 @@
import json
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from .schema.user import UserListResponse, UserInfoResponse, AddUserInputBody, EditUserInputBody, UserUriResponse from .schema.user import UserListResponse, UserInfoResponse, AddUserInputBody, EditUserInputBody, UserUriResponse
@ -25,26 +26,35 @@ async def list_users_api():
raise HTTPException(status_code=400, detail=f'Error: {str(e)}') raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.post('/', response_model=DetailResponse) @router.post('/', response_model=DetailResponse, status_code=201)
async def add_user_api(body: AddUserInputBody): async def add_user_api(body: AddUserInputBody):
""" try:
Add a new user to the system. cli_api.get_user(body.username)
raise HTTPException(status_code=409,
Args: detail=f"User '{body.username}' already exists.")
body: An instance of AddUserInputBody containing the user's details. except cli_api.CommandExecutionError:
pass
Returns: except json.JSONDecodeError as e:
A DetailResponse with a message indicating the user has been added. raise HTTPException(status_code=500,
detail=f"{str(e)}")
Raises:
HTTPException: if an error occurs while adding the user.
"""
try: try:
cli_api.add_user(body.username, body.traffic_limit, body.expiration_days, body.password, body.creation_date) cli_api.add_user(body.username, body.traffic_limit, body.expiration_days, body.password, body.creation_date)
return DetailResponse(detail=f'User {body.username} has been added.') return DetailResponse(detail=f'User {body.username} has been added.')
except cli_api.CommandExecutionError as e:
if "User already exists" in str(e):
raise HTTPException(status_code=409,
detail=f"User '{body.username}' already exists.")
raise HTTPException(status_code=400,
detail=f'Failed to add user {body.username}: {str(e)}')
except cli_api.PasswordGenerationError as e:
raise HTTPException(status_code=500,
detail=f"Failed to generate password for user '{body.username}': {str(e)}")
except cli_api.InvalidInputError as e:
raise HTTPException(status_code=422, detail=str(e))
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f'Error: {str(e)}') raise HTTPException(status_code=500,
detail=f"An unexpected error occurred while adding user '{body.username}': {str(e)}")
@router.get('/{username}', response_model=UserInfoResponse) @router.get('/{username}', response_model=UserInfoResponse)

View File

@ -389,10 +389,11 @@
// Add User Form Submit // Add User Form Submit
$("#addUserForm").on("submit", function (e) { $("#addUserForm").on("submit", function (e) {
e.preventDefault(); e.preventDefault();
// Additional check before submitting (in case JS is disabled briefly)
if (!validateUsername($("#addUsername").val(), "addUsernameError")) { if (!validateUsername($("#addUsername").val(), "addUsernameError")) {
$("#addSubmitButton").prop("disabled", true);
return; return;
} }
$("#addSubmitButton").prop("disabled", true);
const formData = $(this).serializeArray(); const formData = $(this).serializeArray();
const jsonData = {}; const jsonData = {};
@ -406,31 +407,32 @@
contentType: "application/json", contentType: "application/json",
data: JSON.stringify(jsonData), data: JSON.stringify(jsonData),
success: function (response) { success: function (response) {
if (response.detail) { Swal.fire({
Swal.fire({ title: "Success!",
title: "Success!", text: response.detail || "User added successfully!",
text: response.detail, icon: "success",
icon: "success", confirmButtonText: "OK",
confirmButtonText: "OK", }).then(() => {
}).then(() => { location.reload();
location.reload(); });
});
} else {
Swal.fire({
title: "Error!",
text: response.error || "Failed to add user",
icon: "error",
confirmButtonText: "OK",
});
}
}, },
error: function () { error: function (jqXHR, textStatus, errorThrown) {
let errorMessage = "An error occurred while adding user.";
if (jqXHR.responseJSON && jqXHR.responseJSON.detail) {
errorMessage = jqXHR.responseJSON.detail;
} else if (jqXHR.status === 409) {
errorMessage = "User '" + jsonData.username + "' already exists.";
$("#addUsernameError").text(errorMessage);
} else if (jqXHR.status === 422) {
errorMessage = jqXHR.responseJSON.detail || "Invalid input provided.";
}
Swal.fire({ Swal.fire({
title: "Error!", title: "Error!",
text: "An error occurred while adding user", text: errorMessage,
icon: "error", icon: "error",
confirmButtonText: "OK", confirmButtonText: "OK",
}); });
$("#addSubmitButton").prop("disabled", false);
} }
}); });
}); });
@ -761,6 +763,14 @@
}); });
} }
$('#addUserModal').on('show.bs.modal', function (event) {
$('#addUsername').val('');
$('#addTrafficLimit').val('30');
$('#addExpirationDays').val('30');
$('#addUsernameError').text('');
$('#addSubmitButton').prop('disabled', true);
});
$("#searchButton").on("click", filterUsers); $("#searchButton").on("click", filterUsers);
$("#searchInput").on("keyup", filterUsers); $("#searchInput").on("keyup", filterUsers);

View File

@ -1,15 +1,49 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json import json
import os import os
import json import sys
import time
import fcntl
import shutil
import datetime
from concurrent.futures import ThreadPoolExecutor
from hysteria2_api import Hysteria2Client from hysteria2_api import Hysteria2Client
# Define static variables for paths and URLs
CONFIG_FILE = '/etc/hysteria/config.json' CONFIG_FILE = '/etc/hysteria/config.json'
USERS_FILE = '/etc/hysteria/users.json' USERS_FILE = '/etc/hysteria/users.json'
API_BASE_URL = 'http://127.0.0.1:25413' API_BASE_URL = 'http://127.0.0.1:25413'
LOCKFILE = "/tmp/kick.lock"
BACKUP_FILE = f"{USERS_FILE}.bak"
MAX_WORKERS = 8
# import logging
# logging.basicConfig(
# level=logging.INFO,
# format='%(asctime)s: [%(levelname)s] %(message)s',
# datefmt='%Y-%m-%d %H:%M:%S'
# )
# logger = logging.getLogger()
# null_handler = logging.NullHandler()
# logger.handlers = [null_handler]
def acquire_lock():
"""Acquires a lock file to prevent concurrent execution"""
try:
lock_file = open(LOCKFILE, 'w')
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
return lock_file
except IOError:
sys.exit(1)
def traffic_status(no_gui=False): def traffic_status(no_gui=False):
"""Updates and retrieves traffic statistics for all users.
Args:
no_gui (bool): If True, suppresses output to console
Returns:
dict: User data including upload/download bytes and status
"""
green = '\033[0;32m' green = '\033[0;32m'
cyan = '\033[0;36m' cyan = '\033[0;36m'
NC = '\033[0m' NC = '\033[0m'
@ -19,22 +53,24 @@ def traffic_status(no_gui=False):
config = json.load(config_file) config = json.load(config_file)
secret = config.get('trafficStats', {}).get('secret') secret = config.get('trafficStats', {}).get('secret')
except (json.JSONDecodeError, FileNotFoundError) as e: except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"Error: Failed to read secret from {CONFIG_FILE}. Details: {e}") if not no_gui:
return print(f"Error: Failed to read secret from {CONFIG_FILE}. Details: {e}")
return None
if not secret: if not secret:
print("Error: Secret not found in config.json") if not no_gui:
return print("Error: Secret not found in config.json")
return None
client = Hysteria2Client(base_url=API_BASE_URL, secret=secret) client = Hysteria2Client(base_url=API_BASE_URL, secret=secret)
try: try:
traffic_stats = client.get_traffic_stats(clear=True) traffic_stats = client.get_traffic_stats(clear=True)
online_status = client.get_online_clients() online_status = client.get_online_clients()
except Exception as e: except Exception as e:
print(f"Error communicating with Hysteria2 API: {e}") if not no_gui:
return print(f"Error communicating with Hysteria2 API: {e}")
return None
users_data = {} users_data = {}
if os.path.exists(USERS_FILE): if os.path.exists(USERS_FILE):
@ -42,8 +78,9 @@ def traffic_status(no_gui=False):
with open(USERS_FILE, 'r') as users_file: with open(USERS_FILE, 'r') as users_file:
users_data = json.load(users_file) users_data = json.load(users_file)
except json.JSONDecodeError: except json.JSONDecodeError:
print("Error: Failed to parse existing users data JSON file.") if not no_gui:
return print("Error: Failed to parse existing users data JSON file.")
return None
for user in users_data: for user in users_data:
users_data[user]["status"] = "Offline" users_data[user]["status"] = "Offline"
@ -79,6 +116,7 @@ def traffic_status(no_gui=False):
return users_data return users_data
def display_traffic_data(data, green, cyan, NC): def display_traffic_data(data, green, cyan, NC):
"""Displays traffic data in a formatted table"""
if not data: if not data:
print("No traffic data to display.") print("No traffic data to display.")
return return
@ -100,6 +138,7 @@ def display_traffic_data(data, green, cyan, NC):
print("-------------------------------------------------") print("-------------------------------------------------")
def format_bytes(bytes): def format_bytes(bytes):
"""Format bytes as human-readable string"""
if bytes < 1024: if bytes < 1024:
return f"{bytes}B" return f"{bytes}B"
elif bytes < 1048576: elif bytes < 1048576:
@ -111,5 +150,129 @@ def format_bytes(bytes):
else: else:
return f"{bytes / 1099511627776:.2f}TB" return f"{bytes / 1099511627776:.2f}TB"
def kick_users(usernames, secret):
"""Kicks specified users from the server"""
try:
client = Hysteria2Client(
base_url=API_BASE_URL,
secret=secret
)
client.kick_clients(usernames)
return True
except Exception:
return False
def process_user(username, user_data, config_secret, users_data):
"""Process a single user to check if they should be kicked"""
blocked = user_data.get('blocked', False)
if blocked:
return None
max_download_bytes = user_data.get('max_download_bytes', 0)
expiration_days = user_data.get('expiration_days', 0)
account_creation_date = user_data.get('account_creation_date')
current_download_bytes = user_data.get('download_bytes', 0)
current_upload_bytes = user_data.get('upload_bytes', 0)
total_bytes = current_download_bytes + current_upload_bytes
if not account_creation_date:
return None
try:
current_date = datetime.datetime.now().timestamp()
creation_date = datetime.datetime.fromisoformat(account_creation_date.replace('Z', '+00:00'))
expiration_date = (creation_date + datetime.timedelta(days=expiration_days)).timestamp()
should_block = False
if max_download_bytes > 0 and total_bytes >= 0 and expiration_days > 0:
if total_bytes >= max_download_bytes or current_date >= expiration_date:
should_block = True
if should_block:
users_data[username]['blocked'] = True
return username
except Exception:
return None
return None
def kick_expired_users():
"""Kicks users who have exceeded their data limits or whose accounts have expired"""
lock_file = acquire_lock()
try:
shutil.copy2(USERS_FILE, BACKUP_FILE)
try:
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
secret = config.get('trafficStats', {}).get('secret', '')
if not secret:
sys.exit(1)
except Exception:
shutil.copy2(BACKUP_FILE, USERS_FILE)
sys.exit(1)
try:
with open(USERS_FILE, 'r') as f:
users_data = json.load(f)
except json.JSONDecodeError:
shutil.copy2(BACKUP_FILE, USERS_FILE)
sys.exit(1)
except Exception:
shutil.copy2(BACKUP_FILE, USERS_FILE)
sys.exit(1)
users_to_kick = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_user = {
executor.submit(process_user, username, user_data, secret, users_data): username
for username, user_data in users_data.items()
}
for future in future_to_user:
username = future.result()
if username:
users_to_kick.append(username)
if users_to_kick:
for retry in range(3):
try:
with open(USERS_FILE, 'w') as f:
json.dump(users_data, f, indent=2)
break
except Exception:
time.sleep(1)
if retry == 2:
raise
if users_to_kick:
batch_size = 50
for i in range(0, len(users_to_kick), batch_size):
batch = users_to_kick[i:i+batch_size]
kick_users(batch, secret)
except Exception:
shutil.copy2(BACKUP_FILE, USERS_FILE)
sys.exit(1)
finally:
fcntl.flock(lock_file, fcntl.LOCK_UN)
lock_file.close()
if __name__ == "__main__": if __name__ == "__main__":
traffic_status() if len(sys.argv) > 1:
if sys.argv[1] == "kick":
kick_expired_users()
elif sys.argv[1] == "--no-gui":
traffic_status(no_gui=True)
kick_expired_users()
else:
print(f"Unknown argument: {sys.argv[1]}")
print("Usage: python traffic.py [kick|--no-gui]")
else:
traffic_status(no_gui=False)

View File

@ -1,70 +1,197 @@
#!/bin/bash #!/bin/bash
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[1;94m'
NC='\033[0m'
BOLD='\033[1m'
CHECK_MARK="[✓]"
CROSS_MARK="[✗]"
INFO_MARK="[i]"
WARNING_MARK="[!]"
log_info() {
echo -e "${BLUE}${INFO_MARK} ${1}${NC}"
}
log_success() {
echo -e "${GREEN}${CHECK_MARK} ${1}${NC}"
}
log_warning() {
echo -e "${YELLOW}${WARNING_MARK} ${1}${NC}"
}
log_error() {
echo -e "${RED}${CROSS_MARK} ${1}${NC}" >&2
}
handle_error() {
log_error "Error occurred at line $1"
exit 1
}
trap 'handle_error $LINENO' ERR
check_root() {
if [ "$(id -u)" -ne 0 ]; then
log_error "This script must be run as root."
exit 1
fi
log_info "Running with root privileges"
}
check_os_version() { check_os_version() {
local os_name os_version local os_name os_version
log_info "Checking OS compatibility..."
if [ -f /etc/os-release ]; then if [ -f /etc/os-release ]; then
os_name=$(grep '^ID=' /etc/os-release | cut -d= -f2) os_name=$(grep '^ID=' /etc/os-release | cut -d= -f2)
os_version=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"') os_version=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
else else
echo "Unsupported OS or unable to determine OS version." log_error "Unsupported OS or unable to determine OS version."
exit 1 exit 1
fi fi
if ! command -v bc &> /dev/null; then if ! command -v bc &> /dev/null; then
apt update && apt install -y bc log_info "Installing bc package..."
apt update -qq &> /dev/null && apt install -y -qq bc &> /dev/null
if [ $? -ne 0 ]; then
log_error "Failed to install bc package."
exit 1
fi
fi fi
if [[ "$os_name" == "ubuntu" && $(echo "$os_version >= 22" | bc) -eq 1 ]] || if [[ "$os_name" == "ubuntu" && $(echo "$os_version >= 22" | bc) -eq 1 ]] ||
[[ "$os_name" == "debian" && $(echo "$os_version >= 11" | bc) -eq 1 ]]; then [[ "$os_name" == "debian" && $(echo "$os_version >= 12" | bc) -eq 1 ]]; then
log_success "OS check passed: $os_name $os_version"
return 0 return 0
else else
echo "This script is only supported on Ubuntu 22+ or Debian 11+." log_error "This script is only supported on Ubuntu 22+ or Debian 12+."
exit 1 exit 1
fi fi
} }
if [ "$(id -u)" -ne 0 ]; then install_packages() {
echo "This script must be run as root." local REQUIRED_PACKAGES=("jq" "qrencode" "curl" "pwgen" "python3" "python3-pip" "python3-venv" "git" "bc" "zip" "cron" "lsof")
exit 1 local MISSING_PACKAGES=()
fi
check_os_version log_info "Checking required packages..."
REQUIRED_PACKAGES=("jq" "qrencode" "curl" "pwgen" "python3" "python3-pip" "python3-venv" "git" "bc" "zip" "cron" "lsof") for package in "${REQUIRED_PACKAGES[@]}"; do
MISSING_PACKAGES=() if ! command -v "$package" &> /dev/null; then
heavy_checkmark=$(printf "\xE2\x9C\x85") MISSING_PACKAGES+=("$package")
else
for package in "${REQUIRED_PACKAGES[@]}"; do log_success "Package $package is already installed"
if ! command -v "$package" &> /dev/null; then fi
MISSING_PACKAGES+=("$package")
else
echo "Install $package $heavy_checkmark"
fi
done
if [ ${#MISSING_PACKAGES[@]} -ne 0 ]; then
echo "The following packages are missing and will be installed: ${MISSING_PACKAGES[@]}"
apt update -qq && apt upgrade -y -qq
for package in "${MISSING_PACKAGES[@]}"; do
apt install -y -qq "$package" &> /dev/null && echo "Install $package $heavy_checkmark"
done done
else
echo "All required packages are already installed."
fi
git clone https://github.com/ReturnFI/Blitz /etc/hysteria if [ ${#MISSING_PACKAGES[@]} -ne 0 ]; then
log_info "Installing missing packages: ${MISSING_PACKAGES[*]}"
apt update -qq &> /dev/null || { log_error "Failed to update apt repositories"; exit 1; }
apt upgrade -y -qq &> /dev/null || { log_warning "Failed to upgrade packages, continuing..."; }
cd /etc/hysteria for package in "${MISSING_PACKAGES[@]}"; do
python3 -m venv hysteria2_venv log_info "Installing $package..."
source /etc/hysteria/hysteria2_venv/bin/activate if apt install -y -qq "$package" &> /dev/null; then
pip install -r requirements.txt &> /dev/null && echo "Install Python requirements ✅" log_success "Installed $package"
else
log_error "Failed to install $package"
exit 1
fi
done
else
log_success "All required packages are already installed."
fi
}
if ! grep -q "alias hys2='source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/menu.sh'" ~/.bashrc; then clone_repository() {
echo "alias hys2='source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/menu.sh'" >> ~/.bashrc log_info "Cloning Blitz repository..."
source ~/.bashrc
fi if [ -d "/etc/hysteria" ]; then
sleep 5 log_warning "Directory /etc/hysteria already exists."
cd /etc/hysteria read -p "Do you want to remove it and clone again? (y/n): " -n 1 -r
chmod +x menu.sh echo
./menu.sh if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf /etc/hysteria
else
log_info "Using existing directory."
return 0
fi
fi
if git clone https://github.com/ReturnFI/Blitz /etc/hysteria &> /dev/null; then
log_success "Repository cloned successfully"
else
log_error "Failed to clone repository"
exit 1
fi
}
setup_python_env() {
log_info "Setting up Python virtual environment..."
cd /etc/hysteria || { log_error "Failed to change to /etc/hysteria directory"; exit 1; }
if python3 -m venv hysteria2_venv &> /dev/null; then
log_success "Created Python virtual environment"
else
log_error "Failed to create Python virtual environment"
exit 1
fi
source /etc/hysteria/hysteria2_venv/bin/activate || { log_error "Failed to activate virtual environment"; exit 1; }
log_info "Installing Python requirements..."
if pip install -r requirements.txt &> /dev/null; then
log_success "Installed Python requirements"
else
log_error "Failed to install Python requirements"
exit 1
fi
}
add_alias() {
log_info "Adding 'hys2' alias to .bashrc..."
if ! grep -q "alias hys2='source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/menu.sh'" ~/.bashrc; then
echo "alias hys2='source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/menu.sh'" >> ~/.bashrc
log_success "Added 'hys2' alias to .bashrc"
else
log_info "Alias 'hys2' already exists in .bashrc"
fi
}
run_menu() {
log_info "Preparing to run menu..."
cd /etc/hysteria || { log_error "Failed to change to /etc/hysteria directory"; exit 1; }
chmod +x menu.sh || { log_error "Failed to make menu.sh executable"; exit 1; }
log_info "Starting menu..."
echo -e "\n${BOLD}${GREEN}======== Launching Blitz Menu ========${NC}\n"
./menu.sh
}
main() {
echo -e "\n${BOLD}${BLUE}======== Blitz Setup Script ========${NC}\n"
check_root
check_os_version
install_packages
clone_repository
setup_python_env
add_alias
source ~/.bashrc &> /dev/null || true
echo -e "\n${YELLOW}Starting Blitz in 3 seconds...${NC}"
sleep 3
run_menu
}
main

View File

@ -16,9 +16,20 @@ FILES=(
"/etc/hysteria/core/scripts/webpanel/Caddyfile" "/etc/hysteria/core/scripts/webpanel/Caddyfile"
) )
echo "Backing up and stopping all cron jobs" if crontab -l 2>/dev/null | grep -q "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py" || \
crontab -l > /tmp/crontab_backup crontab -l 2>/dev/null | grep -q "/etc/hysteria/core/scripts/hysteria2/kick.sh"; then
crontab -r
echo "Removing existing Hysteria cronjobs..."
crontab -l | grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status" | \
grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py restart-hysteria2" | \
grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria" | \
grep -v "/etc/hysteria/core/scripts/hysteria2/kick.sh" | \
crontab -
echo "Old Hysteria cronjobs removed successfully."
else
echo "No existing Hysteria cronjobs found. Skipping removal."
fi
echo "Backing up files to $TEMP_DIR" echo "Backing up files to $TEMP_DIR"
for FILE in "${FILES[@]}"; do for FILE in "${FILES[@]}"; do
@ -26,28 +37,6 @@ for FILE in "${FILES[@]}"; do
cp "$FILE" "$TEMP_DIR/$FILE" cp "$FILE" "$TEMP_DIR/$FILE"
done done
# echo "Checking and renaming old systemd service files"
# declare -A SERVICE_MAP=(
# ["/etc/systemd/system/hysteria-bot.service"]="hysteria-telegram-bot.service"
# ["/etc/systemd/system/singbox.service"]="hysteria-singbox.service"
# ["/etc/systemd/system/normalsub.service"]="hysteria-normal-sub.service"
# )
# for OLD_SERVICE in "${!SERVICE_MAP[@]}"; do
# NEW_SERVICE="/etc/systemd/system/${SERVICE_MAP[$OLD_SERVICE]}"
# if [[ -f "$OLD_SERVICE" ]]; then
# echo "Stopping old service: $(basename "$OLD_SERVICE")"
# systemctl stop "$(basename "$OLD_SERVICE")" 2>/dev/null
# echo "Renaming $OLD_SERVICE to $NEW_SERVICE"
# mv "$OLD_SERVICE" "$NEW_SERVICE"
# echo "Reloading systemd daemon"
# systemctl daemon-reload
# fi
# done
echo "Removing /etc/hysteria directory" echo "Removing /etc/hysteria directory"
rm -rf /etc/hysteria/ rm -rf /etc/hysteria/
@ -63,24 +52,6 @@ for FILE in "${FILES[@]}"; do
cp "$TEMP_DIR/$FILE" "$FILE" cp "$TEMP_DIR/$FILE" "$FILE"
done done
# CADDYFILE="/etc/hysteria/core/scripts/webpanel/Caddyfile"
# if [ -f "$CADDYFILE" ]; then
# echo "Updating Caddyfile port from 8080 to 28260"
# sed -i 's/\(:[[:space:]]*\)8080/\128260/g' "$CADDYFILE"
# sed -i 's/0\.0\.0\.0:8080/0.0.0.0:28260/g' "$CADDYFILE"
# sed -i 's/127\.0\.0\.1:8080/127.0.0.1:28260/g' "$CADDYFILE"
# if ! grep -q ':28260' "$CADDYFILE"; then
# echo "Warning: Caddyfile does not contain port 8080 in expected formats. Port replacement may have already been done."
# fi
# else
# echo "Error: Caddyfile not found at $CADDYFILE. Cannot update port."
# fi
CONFIG_ENV="/etc/hysteria/.configs.env" CONFIG_ENV="/etc/hysteria/.configs.env"
if [ ! -f "$CONFIG_ENV" ]; then if [ ! -f "$CONFIG_ENV" ]; then
echo ".configs.env not found, creating it with default values." echo ".configs.env not found, creating it with default values."
@ -140,9 +111,9 @@ chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt
chown -R hysteria:hysteria /etc/hysteria/core/scripts/singbox chown -R hysteria:hysteria /etc/hysteria/core/scripts/singbox
chown -R hysteria:hysteria /etc/hysteria/core/scripts/telegrambot chown -R hysteria:hysteria /etc/hysteria/core/scripts/telegrambot
echo "Setting execute permissions for user.sh and kick.sh" echo "Setting execute permissions for user.sh and kick.py"
chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh
chmod +x /etc/hysteria/core/scripts/hysteria2/kick.sh chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py
cd /etc/hysteria cd /etc/hysteria
python3 -m venv hysteria2_venv python3 -m venv hysteria2_venv
@ -155,7 +126,6 @@ systemctl restart hysteria-caddy.service
echo "Restarting other hysteria services" echo "Restarting other hysteria services"
systemctl restart hysteria-server.service systemctl restart hysteria-server.service
systemctl restart hysteria-telegram-bot.service systemctl restart hysteria-telegram-bot.service
# systemctl restart hysteria-singbox.service
systemctl restart hysteria-normal-sub.service systemctl restart hysteria-normal-sub.service
systemctl restart hysteria-webpanel.service systemctl restart hysteria-webpanel.service
@ -167,9 +137,20 @@ else
echo "Upgrade failed: hysteria-server.service is not active" echo "Upgrade failed: hysteria-server.service is not active"
fi fi
echo "Restoring cron jobs" echo "Adding new Hysteria cronjobs..."
crontab /tmp/crontab_backup if ! crontab -l 2>/dev/null | grep -q "python3 /etc/hysteria/core/cli.py traffic-status --no-gui"; then
rm /tmp/crontab_backup echo "Adding traffic-status cronjob..."
(crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status --no-gui'") | crontab -
else
echo "Traffic-status cronjob already exists. Skipping."
fi
if ! crontab -l 2>/dev/null | grep -q "python3 /etc/hysteria/core/cli.py backup-hysteria"; then
echo "Adding backup-hysteria cronjob..."
(crontab -l ; echo "0 */6 * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria' >/dev/null 2>&1") | crontab -
else
echo "Backup-hysteria cronjob already exists. Skipping."
fi
chmod +x menu.sh chmod +x menu.sh
./menu.sh ./menu.sh