28
changelog
28
changelog
@ -1,10 +1,20 @@
|
||||
## [1.9.1] - 2025-05-03
|
||||
# [1.9.2] - 2025-05-10
|
||||
|
||||
### ✨ Changed
|
||||
🧠 **refactor:**: Implement server stats manager in Python
|
||||
🧠 **refactor:**: Migrate IP address config management to Python
|
||||
🧠 **refactor:**: Implement Hysteria backup functionality in Python
|
||||
🧠 **refactor:**: Transition Telegram bot service management to Python
|
||||
🧠 **refactor:**: Migrate WARP ACL configuration handling to Python
|
||||
🧠 **refactor:**: Implement WARP status management in Python
|
||||
🧠 **refactor:**: Rewrite WARP setup and uninstallation scripts in Python3
|
||||
## ✨ Changed
|
||||
|
||||
### ✨ Features
|
||||
🔄 feat: Merge traffic collection with user kicking for efficient enforcement
|
||||
🧪 feat(api): Add user existence validation to `add-user` endpoint
|
||||
🖥️ feat(frontend): Improve user management UI and error feedback in `users.html`
|
||||
|
||||
### 🧠 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
|
||||
@ -227,7 +227,7 @@ def show_user_uri_json(usernames: list[str]):
|
||||
|
||||
|
||||
@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):
|
||||
try:
|
||||
cli_api.traffic_status(no_gui=no_gui)
|
||||
|
||||
@ -21,7 +21,7 @@ class Command(Enum):
|
||||
UPDATE_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'update.py')
|
||||
RESTART_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'restart.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')
|
||||
ADD_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'add_user.py')
|
||||
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')
|
||||
IP_ADD = os.path.join(SCRIPT_DIR, 'hysteria2', 'ip.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)
|
||||
UPDATE_GEO = os.path.join(SCRIPT_DIR, 'hysteria2', 'update_geo.py')
|
||||
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')
|
||||
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')
|
||||
UNINSTALL_WARP = os.path.join(SCRIPT_DIR, 'warp', 'uninstall.py')
|
||||
CONFIGURE_WARP = os.path.join(SCRIPT_DIR, 'warp', 'configure.py')
|
||||
@ -83,24 +83,32 @@ class ScriptNotFoundError(HysteriaError):
|
||||
# region Utils
|
||||
|
||||
|
||||
def run_cmd(command: list[str]) -> str | None:
|
||||
def run_cmd(command: list[str]) -> str:
|
||||
'''
|
||||
Runs a command and returns the output.
|
||||
Could raise subprocess.CalledProcessError
|
||||
Runs a command and returns its stdout if successful.
|
||||
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):
|
||||
print(' '.join(command))
|
||||
try:
|
||||
result = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=False)
|
||||
if result:
|
||||
result = result.decode().strip()
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
if DEBUG:
|
||||
raise CommandExecutionError(f'Command execution failed: {e}\nOutput: {e.output.decode()}')
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
print(f"Executing command: {' '.join(command)}")
|
||||
try:
|
||||
process = subprocess.run(command, capture_output=True, text=True, shell=False, check=False)
|
||||
|
||||
if process.returncode != 0:
|
||||
error_output = process.stderr.strip() if process.stderr.strip() else process.stdout.strip()
|
||||
if not error_output:
|
||||
error_output = f"Command exited with status {process.returncode} without specific error message."
|
||||
|
||||
detailed_error_message = f"Command '{' '.join(command)}' failed with exit code {process.returncode}: {error_output}"
|
||||
raise CommandExecutionError(detailed_error_message)
|
||||
|
||||
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:
|
||||
@ -176,7 +184,7 @@ def change_hysteria2_sni(sni: str):
|
||||
'''
|
||||
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():
|
||||
@ -192,7 +200,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:
|
||||
@ -211,12 +219,12 @@ def disable_hysteria2_obfs():
|
||||
|
||||
def enable_hysteria2_masquerade(domain: str):
|
||||
'''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():
|
||||
'''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]:
|
||||
@ -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):
|
||||
'''Fetches traffic status.'''
|
||||
if 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
|
||||
|
||||
|
||||
@ -431,7 +443,7 @@ def update_geo(country: str):
|
||||
|
||||
def install_tcp_brutal():
|
||||
'''Installs TCP Brutal.'''
|
||||
run_cmd(['bash', Command.INSTALL_TCP_BRUTAL.value])
|
||||
run_cmd(['python3', Command.INSTALL_TCP_BRUTAL.value])
|
||||
|
||||
|
||||
def install_warp():
|
||||
|
||||
212
core/scripts/hysteria2/change_sni.py
Normal file
212
core/scripts/hysteria2/change_sni.py
Normal 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))
|
||||
@ -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"
|
||||
@ -80,12 +80,12 @@ install_hysteria() {
|
||||
fi
|
||||
|
||||
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 "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 "*/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 */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 -
|
||||
|
||||
}
|
||||
|
||||
|
||||
178
core/scripts/hysteria2/kick.py
Normal file
178
core/scripts/hysteria2/kick.py
Normal 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()
|
||||
@ -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
|
||||
93
core/scripts/hysteria2/masquerade.py
Normal file
93
core/scripts/hysteria2/masquerade.py
Normal 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()
|
||||
@ -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
|
||||
164
core/scripts/hysteria2/restore.py
Normal file
164
core/scripts/hysteria2/restore.py
Normal 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())
|
||||
@ -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
|
||||
15
core/scripts/tcp-brutal/install.py
Normal file
15
core/scripts/tcp-brutal/install.py
Normal 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()
|
||||
@ -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."
|
||||
@ -1,3 +1,4 @@
|
||||
import json
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
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)}')
|
||||
|
||||
|
||||
@router.post('/', response_model=DetailResponse)
|
||||
@router.post('/', response_model=DetailResponse, status_code=201)
|
||||
async def add_user_api(body: AddUserInputBody):
|
||||
"""
|
||||
Add a new user to the system.
|
||||
|
||||
Args:
|
||||
body: An instance of AddUserInputBody containing the user's details.
|
||||
|
||||
Returns:
|
||||
A DetailResponse with a message indicating the user has been added.
|
||||
|
||||
Raises:
|
||||
HTTPException: if an error occurs while adding the user.
|
||||
"""
|
||||
try:
|
||||
cli_api.get_user(body.username)
|
||||
raise HTTPException(status_code=409,
|
||||
detail=f"User '{body.username}' already exists.")
|
||||
except cli_api.CommandExecutionError:
|
||||
pass
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(status_code=500,
|
||||
detail=f"{str(e)}")
|
||||
|
||||
try:
|
||||
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.')
|
||||
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:
|
||||
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)
|
||||
|
||||
@ -389,10 +389,11 @@
|
||||
// Add User Form Submit
|
||||
$("#addUserForm").on("submit", function (e) {
|
||||
e.preventDefault();
|
||||
// Additional check before submitting (in case JS is disabled briefly)
|
||||
if (!validateUsername($("#addUsername").val(), "addUsernameError")) {
|
||||
$("#addSubmitButton").prop("disabled", true);
|
||||
return;
|
||||
}
|
||||
$("#addSubmitButton").prop("disabled", true);
|
||||
|
||||
const formData = $(this).serializeArray();
|
||||
const jsonData = {};
|
||||
@ -406,31 +407,32 @@
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(jsonData),
|
||||
success: function (response) {
|
||||
if (response.detail) {
|
||||
Swal.fire({
|
||||
title: "Success!",
|
||||
text: response.detail,
|
||||
text: response.detail || "User added successfully!",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
}).then(() => {
|
||||
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({
|
||||
title: "Error!",
|
||||
text: "An error occurred while adding user",
|
||||
text: errorMessage,
|
||||
icon: "error",
|
||||
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);
|
||||
$("#searchInput").on("keyup", filterUsers);
|
||||
|
||||
|
||||
179
core/traffic.py
179
core/traffic.py
@ -1,15 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import fcntl
|
||||
import shutil
|
||||
import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from hysteria2_api import Hysteria2Client
|
||||
|
||||
# Define static variables for paths and URLs
|
||||
CONFIG_FILE = '/etc/hysteria/config.json'
|
||||
USERS_FILE = '/etc/hysteria/users.json'
|
||||
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):
|
||||
"""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'
|
||||
cyan = '\033[0;36m'
|
||||
NC = '\033[0m'
|
||||
@ -19,22 +53,24 @@ def traffic_status(no_gui=False):
|
||||
config = json.load(config_file)
|
||||
secret = config.get('trafficStats', {}).get('secret')
|
||||
except (json.JSONDecodeError, FileNotFoundError) as e:
|
||||
if not no_gui:
|
||||
print(f"Error: Failed to read secret from {CONFIG_FILE}. Details: {e}")
|
||||
return
|
||||
return None
|
||||
|
||||
if not secret:
|
||||
if not no_gui:
|
||||
print("Error: Secret not found in config.json")
|
||||
return
|
||||
return None
|
||||
|
||||
client = Hysteria2Client(base_url=API_BASE_URL, secret=secret)
|
||||
|
||||
try:
|
||||
traffic_stats = client.get_traffic_stats(clear=True)
|
||||
|
||||
online_status = client.get_online_clients()
|
||||
except Exception as e:
|
||||
if not no_gui:
|
||||
print(f"Error communicating with Hysteria2 API: {e}")
|
||||
return
|
||||
return None
|
||||
|
||||
users_data = {}
|
||||
if os.path.exists(USERS_FILE):
|
||||
@ -42,8 +78,9 @@ def traffic_status(no_gui=False):
|
||||
with open(USERS_FILE, 'r') as users_file:
|
||||
users_data = json.load(users_file)
|
||||
except json.JSONDecodeError:
|
||||
if not no_gui:
|
||||
print("Error: Failed to parse existing users data JSON file.")
|
||||
return
|
||||
return None
|
||||
|
||||
for user in users_data:
|
||||
users_data[user]["status"] = "Offline"
|
||||
@ -79,6 +116,7 @@ def traffic_status(no_gui=False):
|
||||
return users_data
|
||||
|
||||
def display_traffic_data(data, green, cyan, NC):
|
||||
"""Displays traffic data in a formatted table"""
|
||||
if not data:
|
||||
print("No traffic data to display.")
|
||||
return
|
||||
@ -100,6 +138,7 @@ def display_traffic_data(data, green, cyan, NC):
|
||||
print("-------------------------------------------------")
|
||||
|
||||
def format_bytes(bytes):
|
||||
"""Format bytes as human-readable string"""
|
||||
if bytes < 1024:
|
||||
return f"{bytes}B"
|
||||
elif bytes < 1048576:
|
||||
@ -111,5 +150,129 @@ def format_bytes(bytes):
|
||||
else:
|
||||
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__":
|
||||
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)
|
||||
201
install.sh
201
install.sh
@ -1,70 +1,197 @@
|
||||
#!/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() {
|
||||
local os_name os_version
|
||||
|
||||
log_info "Checking OS compatibility..."
|
||||
|
||||
if [ -f /etc/os-release ]; then
|
||||
os_name=$(grep '^ID=' /etc/os-release | cut -d= -f2)
|
||||
os_version=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
|
||||
else
|
||||
echo "Unsupported OS or unable to determine OS version."
|
||||
log_error "Unsupported OS or unable to determine OS version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This script must be run as root."
|
||||
exit 1
|
||||
fi
|
||||
install_packages() {
|
||||
local REQUIRED_PACKAGES=("jq" "qrencode" "curl" "pwgen" "python3" "python3-pip" "python3-venv" "git" "bc" "zip" "cron" "lsof")
|
||||
local MISSING_PACKAGES=()
|
||||
|
||||
check_os_version
|
||||
log_info "Checking required packages..."
|
||||
|
||||
REQUIRED_PACKAGES=("jq" "qrencode" "curl" "pwgen" "python3" "python3-pip" "python3-venv" "git" "bc" "zip" "cron" "lsof")
|
||||
MISSING_PACKAGES=()
|
||||
heavy_checkmark=$(printf "\xE2\x9C\x85")
|
||||
|
||||
for package in "${REQUIRED_PACKAGES[@]}"; do
|
||||
for package in "${REQUIRED_PACKAGES[@]}"; do
|
||||
if ! command -v "$package" &> /dev/null; then
|
||||
MISSING_PACKAGES+=("$package")
|
||||
else
|
||||
echo "Install $package $heavy_checkmark"
|
||||
log_success "Package $package is already installed"
|
||||
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
|
||||
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
|
||||
python3 -m venv hysteria2_venv
|
||||
source /etc/hysteria/hysteria2_venv/bin/activate
|
||||
pip install -r requirements.txt &> /dev/null && echo "Install Python requirements ✅"
|
||||
for package in "${MISSING_PACKAGES[@]}"; do
|
||||
log_info "Installing $package..."
|
||||
if apt install -y -qq "$package" &> /dev/null; then
|
||||
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() {
|
||||
log_info "Cloning Blitz repository..."
|
||||
|
||||
if [ -d "/etc/hysteria" ]; then
|
||||
log_warning "Directory /etc/hysteria already exists."
|
||||
read -p "Do you want to remove it and clone again? (y/n): " -n 1 -r
|
||||
echo
|
||||
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
|
||||
source ~/.bashrc
|
||||
fi
|
||||
sleep 5
|
||||
cd /etc/hysteria
|
||||
chmod +x menu.sh
|
||||
./menu.sh
|
||||
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
|
||||
|
||||
79
upgrade.sh
79
upgrade.sh
@ -16,9 +16,20 @@ FILES=(
|
||||
"/etc/hysteria/core/scripts/webpanel/Caddyfile"
|
||||
)
|
||||
|
||||
echo "Backing up and stopping all cron jobs"
|
||||
crontab -l > /tmp/crontab_backup
|
||||
crontab -r
|
||||
if crontab -l 2>/dev/null | grep -q "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py" || \
|
||||
crontab -l 2>/dev/null | grep -q "/etc/hysteria/core/scripts/hysteria2/kick.sh"; then
|
||||
|
||||
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"
|
||||
for FILE in "${FILES[@]}"; do
|
||||
@ -26,28 +37,6 @@ for FILE in "${FILES[@]}"; do
|
||||
cp "$FILE" "$TEMP_DIR/$FILE"
|
||||
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"
|
||||
rm -rf /etc/hysteria/
|
||||
|
||||
@ -63,24 +52,6 @@ for FILE in "${FILES[@]}"; do
|
||||
cp "$TEMP_DIR/$FILE" "$FILE"
|
||||
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"
|
||||
if [ ! -f "$CONFIG_ENV" ]; then
|
||||
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/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/kick.sh
|
||||
chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py
|
||||
|
||||
cd /etc/hysteria
|
||||
python3 -m venv hysteria2_venv
|
||||
@ -155,7 +126,6 @@ systemctl restart hysteria-caddy.service
|
||||
echo "Restarting other hysteria services"
|
||||
systemctl restart hysteria-server.service
|
||||
systemctl restart hysteria-telegram-bot.service
|
||||
# systemctl restart hysteria-singbox.service
|
||||
systemctl restart hysteria-normal-sub.service
|
||||
systemctl restart hysteria-webpanel.service
|
||||
|
||||
@ -167,9 +137,20 @@ else
|
||||
echo "Upgrade failed: hysteria-server.service is not active"
|
||||
fi
|
||||
|
||||
echo "Restoring cron jobs"
|
||||
crontab /tmp/crontab_backup
|
||||
rm /tmp/crontab_backup
|
||||
echo "Adding new Hysteria cronjobs..."
|
||||
if ! crontab -l 2>/dev/null | grep -q "python3 /etc/hysteria/core/cli.py traffic-status --no-gui"; then
|
||||
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
|
||||
./menu.sh
|
||||
|
||||
Reference in New Issue
Block a user