28
changelog
28
changelog
@ -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
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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):
|
|
||||||
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:
|
if DEBUG:
|
||||||
raise CommandExecutionError(f'Command execution failed: {e}\nOutput: {e.output.decode()}')
|
print(f"Executing command: {' '.join(command)}")
|
||||||
else:
|
try:
|
||||||
return None
|
process = subprocess.run(command, capture_output=True, text=True, shell=False, check=False)
|
||||||
return None
|
|
||||||
|
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:
|
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)
|
||||||
|
traffic.kick_expired_users()
|
||||||
|
else:
|
||||||
data = traffic.traffic_status(no_gui=True if not display_output else no_gui)
|
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():
|
||||||
|
|||||||
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
|
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 -
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 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)
|
||||||
|
|||||||
@ -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,
|
text: response.detail || "User added successfully!",
|
||||||
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);
|
||||||
|
|
||||||
|
|||||||
179
core/traffic.py
179
core/traffic.py
@ -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:
|
||||||
|
if not no_gui:
|
||||||
print(f"Error: Failed to read secret from {CONFIG_FILE}. Details: {e}")
|
print(f"Error: Failed to read secret from {CONFIG_FILE}. Details: {e}")
|
||||||
return
|
return None
|
||||||
|
|
||||||
if not secret:
|
if not secret:
|
||||||
|
if not no_gui:
|
||||||
print("Error: Secret not found in config.json")
|
print("Error: Secret not found in config.json")
|
||||||
return
|
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:
|
||||||
|
if not no_gui:
|
||||||
print(f"Error communicating with Hysteria2 API: {e}")
|
print(f"Error communicating with Hysteria2 API: {e}")
|
||||||
return
|
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:
|
||||||
|
if not no_gui:
|
||||||
print("Error: Failed to parse existing users data JSON file.")
|
print("Error: Failed to parse existing users data JSON file.")
|
||||||
return
|
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)
|
||||||
181
install.sh
181
install.sh
@ -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")
|
|
||||||
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
|
if ! command -v "$package" &> /dev/null; then
|
||||||
MISSING_PACKAGES+=("$package")
|
MISSING_PACKAGES+=("$package")
|
||||||
else
|
else
|
||||||
echo "Install $package $heavy_checkmark"
|
log_success "Package $package is already installed"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ ${#MISSING_PACKAGES[@]} -ne 0 ]; then
|
if [ ${#MISSING_PACKAGES[@]} -ne 0 ]; then
|
||||||
echo "The following packages are missing and will be installed: ${MISSING_PACKAGES[@]}"
|
log_info "Installing missing packages: ${MISSING_PACKAGES[*]}"
|
||||||
apt update -qq && apt upgrade -y -qq
|
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..."; }
|
||||||
|
|
||||||
for package in "${MISSING_PACKAGES[@]}"; do
|
for package in "${MISSING_PACKAGES[@]}"; do
|
||||||
apt install -y -qq "$package" &> /dev/null && echo "Install $package $heavy_checkmark"
|
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
|
done
|
||||||
else
|
else
|
||||||
echo "All required packages are already installed."
|
log_success "All required packages are already installed."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
fi
|
||||||
|
|
||||||
git clone https://github.com/ReturnFI/Blitz /etc/hysteria
|
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
|
||||||
|
}
|
||||||
|
|
||||||
cd /etc/hysteria
|
setup_python_env() {
|
||||||
python3 -m venv hysteria2_venv
|
log_info "Setting up Python virtual environment..."
|
||||||
source /etc/hysteria/hysteria2_venv/bin/activate
|
|
||||||
pip install -r requirements.txt &> /dev/null && echo "Install Python requirements ✅"
|
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
|
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
|
echo "alias hys2='source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/menu.sh'" >> ~/.bashrc
|
||||||
source ~/.bashrc
|
log_success "Added 'hys2' alias to .bashrc"
|
||||||
|
else
|
||||||
|
log_info "Alias 'hys2' already exists in .bashrc"
|
||||||
fi
|
fi
|
||||||
sleep 5
|
}
|
||||||
cd /etc/hysteria
|
|
||||||
chmod +x menu.sh
|
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
|
./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"
|
"/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
|
||||||
|
|||||||
Reference in New Issue
Block a user