27
changelog
27
changelog
@ -1,21 +1,10 @@
|
||||
## [1.9.0] - 2025-05-02
|
||||
## [1.9.1] - 2025-05-03
|
||||
|
||||
### ✨ Changed
|
||||
🧠 **refactor:** Migrate core Hysteria2 functionality from Bash to Python
|
||||
- 🔁 Uninstallation
|
||||
- ⬆️ Server update
|
||||
- ➕ User addition
|
||||
- 🔁 User reset
|
||||
- 🔍 User info retrieval
|
||||
- ❌ User removal
|
||||
- 🔄 Port update
|
||||
- 🔁 Server restart
|
||||
|
||||
### 🐛 Fixed
|
||||
🛠️ **fix:** Resolved file permission issues by explicitly calling Python in subprocess
|
||||
🐛 **fix:** Removed improper usage of `asyncio.to_thread` in `remove_user.py`
|
||||
🌚 **fix (UI):** Fixed JSONEditor background in dark mode for better readability
|
||||
|
||||
### 🎨 UI / UX
|
||||
🎨 **feat:** Improved light/dark mode design with enhanced CSS
|
||||
🧩 **update:** Integrated custom CSS directly into `base.html`
|
||||
🧠 **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
|
||||
@ -365,16 +365,13 @@ def uninstall_warp():
|
||||
@click.option('--popular-sites', '-p', is_flag=True, help='Use WARP for popular sites like Google, OpenAI, etc')
|
||||
@click.option('--domestic-sites', '-d', is_flag=True, help='Use WARP for Iran domestic sites')
|
||||
@click.option('--block-adult-sites', '-x', is_flag=True, help='Block adult content (porn)')
|
||||
@click.option('--warp-option', '-w', type=click.Choice(['warp', 'warp plus'], case_sensitive=False), help='Specify whether to use WARP or WARP Plus')
|
||||
@click.option('--warp-key', '-k', help="WARP Plus key (required if warp-option is 'warp plus')")
|
||||
def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool, warp_option: str, warp_key: str):
|
||||
def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool):
|
||||
try:
|
||||
cli_api.configure_warp(all, popular_sites, domestic_sites, block_adult_sites, warp_option, warp_key)
|
||||
cli_api.configure_warp(all, popular_sites, domestic_sites, block_adult_sites)
|
||||
click.echo('WARP configured successfully.')
|
||||
except Exception as e:
|
||||
click.echo(f'{e}', err=True)
|
||||
|
||||
|
||||
@cli.command('warp-status')
|
||||
def warp_status():
|
||||
try:
|
||||
|
||||
@ -29,24 +29,24 @@ class Command(Enum):
|
||||
REMOVE_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'remove_user.py')
|
||||
SHOW_USER_URI = os.path.join(SCRIPT_DIR, 'hysteria2', 'show_user_uri.py')
|
||||
WRAPPER_URI = os.path.join(SCRIPT_DIR, 'hysteria2', 'wrapper_uri.py')
|
||||
IP_ADD = os.path.join(SCRIPT_DIR, 'hysteria2', 'ip.sh')
|
||||
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')
|
||||
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.sh')
|
||||
BACKUP_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'backup.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')
|
||||
INSTALL_TELEGRAMBOT = os.path.join(SCRIPT_DIR, 'telegrambot', 'runbot.sh')
|
||||
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_WARP = os.path.join(SCRIPT_DIR, 'warp', 'install.sh')
|
||||
UNINSTALL_WARP = os.path.join(SCRIPT_DIR, 'warp', 'uninstall.sh')
|
||||
CONFIGURE_WARP = os.path.join(SCRIPT_DIR, 'warp', 'configure.sh')
|
||||
STATUS_WARP = os.path.join(SCRIPT_DIR, 'warp', 'status.sh')
|
||||
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')
|
||||
STATUS_WARP = os.path.join(SCRIPT_DIR, 'warp', 'status.py')
|
||||
SERVICES_STATUS = os.path.join(SCRIPT_DIR, 'services_status.sh')
|
||||
VERSION = os.path.join(SCRIPT_DIR, 'hysteria2', 'version.py')
|
||||
LIMIT_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'limit.sh')
|
||||
@ -182,7 +182,7 @@ def change_hysteria2_sni(sni: str):
|
||||
def backup_hysteria2():
|
||||
'''Backups Hysteria configuration. Raises an exception on failure.'''
|
||||
try:
|
||||
run_cmd(['bash', Command.BACKUP_HYSTERIA2.value])
|
||||
run_cmd(['python3', Command.BACKUP_HYSTERIA2.value])
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception(f"Backup failed: {e}")
|
||||
except Exception as ex:
|
||||
@ -368,10 +368,12 @@ def traffic_status(no_gui=False, display_output=True):
|
||||
return data
|
||||
|
||||
|
||||
# Next Update:
|
||||
# TODO: it's better to return json
|
||||
# TODO: After json todo need fix Telegram Bot and WebPanel
|
||||
def server_info() -> str | None:
|
||||
'''Retrieves server information.'''
|
||||
return run_cmd(['bash', Command.SERVER_INFO.value])
|
||||
return run_cmd(['python3', Command.SERVER_INFO.value])
|
||||
|
||||
|
||||
def get_ip_address() -> tuple[str | None, str | None]:
|
||||
@ -387,7 +389,7 @@ def add_ip_address():
|
||||
'''
|
||||
Adds IP addresses from the environment to the .configs.env file.
|
||||
'''
|
||||
run_cmd(['bash', Command.IP_ADD.value, 'add'])
|
||||
run_cmd(['python3', Command.IP_ADD.value, 'add'])
|
||||
|
||||
|
||||
def edit_ip_address(ipv4: str, ipv6: str):
|
||||
@ -402,9 +404,9 @@ def edit_ip_address(ipv4: str, ipv6: str):
|
||||
if not ipv4 and not ipv6:
|
||||
raise InvalidInputError('Error: --edit requires at least one of --ipv4 or --ipv6.')
|
||||
if ipv4:
|
||||
run_cmd(['bash', Command.IP_ADD.value, 'edit', '-4', ipv4])
|
||||
run_cmd(['python3', Command.IP_ADD.value, 'edit', '-4', ipv4])
|
||||
if ipv6:
|
||||
run_cmd(['bash', Command.IP_ADD.value, 'edit', '-6', ipv6])
|
||||
run_cmd(['python3', Command.IP_ADD.value, 'edit', '-6', ipv6])
|
||||
|
||||
|
||||
def update_geo(country: str):
|
||||
@ -434,56 +436,48 @@ def install_tcp_brutal():
|
||||
|
||||
def install_warp():
|
||||
'''Installs WARP.'''
|
||||
run_cmd(['bash', Command.INSTALL_WARP.value])
|
||||
run_cmd(['python3', Command.INSTALL_WARP.value])
|
||||
|
||||
|
||||
def uninstall_warp():
|
||||
'''Uninstalls WARP.'''
|
||||
run_cmd(['bash', Command.UNINSTALL_WARP.value])
|
||||
run_cmd(['python3', Command.UNINSTALL_WARP.value])
|
||||
|
||||
|
||||
def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool, warp_option: str, warp_key: str):
|
||||
def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool):
|
||||
'''
|
||||
Configures WARP with various options.
|
||||
'''
|
||||
if warp_option == 'warp plus' and not warp_key:
|
||||
raise InvalidInputError('Error: WARP Plus key is required when \'warp plus\' is selected.')
|
||||
options = {
|
||||
'all': 'true' if all else 'false',
|
||||
'popular_sites': 'true' if popular_sites else 'false',
|
||||
'domestic_sites': 'true' if domestic_sites else 'false',
|
||||
'block_adult_sites': 'true' if block_adult_sites else 'false',
|
||||
'warp_option': warp_option or '',
|
||||
'warp_key': warp_key or ''
|
||||
}
|
||||
cmd_args = [
|
||||
'bash', Command.CONFIGURE_WARP.value,
|
||||
options['all'],
|
||||
options['popular_sites'],
|
||||
options['domestic_sites'],
|
||||
options['block_adult_sites'],
|
||||
options['warp_option']
|
||||
'python3', Command.CONFIGURE_WARP.value
|
||||
]
|
||||
if options['warp_key']:
|
||||
cmd_args.append(options['warp_key'])
|
||||
if all:
|
||||
cmd_args.append('--all')
|
||||
if popular_sites:
|
||||
cmd_args.append('--popular-sites')
|
||||
if domestic_sites:
|
||||
cmd_args.append('--domestic-sites')
|
||||
if block_adult_sites:
|
||||
cmd_args.append('--block-adult')
|
||||
|
||||
run_cmd(cmd_args)
|
||||
|
||||
|
||||
def warp_status() -> str | None:
|
||||
'''Checks the status of WARP.'''
|
||||
return run_cmd(['bash', Command.STATUS_WARP.value])
|
||||
return run_cmd(['python3', Command.STATUS_WARP.value])
|
||||
|
||||
|
||||
def start_telegram_bot(token: str, adminid: str):
|
||||
'''Starts the Telegram bot.'''
|
||||
if not token or not adminid:
|
||||
raise InvalidInputError('Error: Both --token and --adminid are required for the start action.')
|
||||
run_cmd(['bash', Command.INSTALL_TELEGRAMBOT.value, 'start', token, adminid])
|
||||
run_cmd(['python3', Command.INSTALL_TELEGRAMBOT.value, 'start', token, adminid])
|
||||
|
||||
|
||||
def stop_telegram_bot():
|
||||
'''Stops the Telegram bot.'''
|
||||
run_cmd(['bash', Command.INSTALL_TELEGRAMBOT.value, 'stop'])
|
||||
run_cmd(['python3', Command.INSTALL_TELEGRAMBOT.value, 'stop'])
|
||||
|
||||
|
||||
def start_singbox(domain: str, port: int):
|
||||
|
||||
27
core/scripts/hysteria2/backup.py
Normal file
27
core/scripts/hysteria2/backup.py
Normal file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
backup_dir = Path("/opt/hysbackup")
|
||||
backup_file = backup_dir / f"hysteria_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||||
|
||||
files_to_backup = [
|
||||
Path("/etc/hysteria/ca.key"),
|
||||
Path("/etc/hysteria/ca.crt"),
|
||||
Path("/etc/hysteria/users.json"),
|
||||
Path("/etc/hysteria/config.json"),
|
||||
Path("/etc/hysteria/.configs.env"),
|
||||
]
|
||||
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(backup_file, 'w') as zipf:
|
||||
for file_path in files_to_backup:
|
||||
if file_path.exists():
|
||||
zipf.write(file_path, arcname=file_path.name)
|
||||
print("Backup successfully created")
|
||||
except Exception as e:
|
||||
print("Backup failed!", str(e))
|
||||
@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BACKUP_DIR="/opt/hysbackup"
|
||||
BACKUP_FILE="$BACKUP_DIR/hysteria_backup_$(date +%Y%m%d_%H%M%S).zip"
|
||||
|
||||
if [ ! -d "$BACKUP_DIR" ]; then
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
fi
|
||||
|
||||
FILES_TO_BACKUP=(
|
||||
"/etc/hysteria/ca.key"
|
||||
"/etc/hysteria/ca.crt"
|
||||
"/etc/hysteria/users.json"
|
||||
"/etc/hysteria/config.json"
|
||||
"/etc/hysteria/.configs.env"
|
||||
)
|
||||
|
||||
zip -j "$BACKUP_FILE" "${FILES_TO_BACKUP[@]}" >/dev/null
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Backup successfully created"
|
||||
else
|
||||
echo "Backup failed!"
|
||||
fi
|
||||
102
core/scripts/hysteria2/ip.py
Normal file
102
core/scripts/hysteria2/ip.py
Normal file
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
core_scripts_dir = Path(__file__).resolve().parents[1]
|
||||
if str(core_scripts_dir) not in sys.path:
|
||||
sys.path.append(str(core_scripts_dir))
|
||||
|
||||
from paths import CONFIG_ENV
|
||||
|
||||
|
||||
def ensure_env_file_exists():
|
||||
if not CONFIG_ENV.exists():
|
||||
print("CONFIG_ENV not found. Creating a new one...")
|
||||
CONFIG_ENV.touch()
|
||||
|
||||
|
||||
def update_config(key: str, value: str):
|
||||
content = []
|
||||
if CONFIG_ENV.exists():
|
||||
with CONFIG_ENV.open("r") as f:
|
||||
content = [line for line in f if not line.startswith(f"{key}=")]
|
||||
content.append(f"{key}={value}\n")
|
||||
with CONFIG_ENV.open("w") as f:
|
||||
f.writelines(content)
|
||||
|
||||
|
||||
def get_interface_addresses():
|
||||
ipv4_address = ""
|
||||
ipv6_address = ""
|
||||
|
||||
interfaces = subprocess.check_output(["ip", "-o", "link", "show"]).decode()
|
||||
interfaces = [
|
||||
line.split(": ")[1]
|
||||
for line in interfaces.strip().splitlines()
|
||||
if not re.match(r"^(lo|wgcf|warp)$", line.split(": ")[1])
|
||||
]
|
||||
|
||||
for iface in interfaces:
|
||||
try:
|
||||
ipv4 = subprocess.check_output(["ip", "-o", "-4", "addr", "show", iface]).decode()
|
||||
for line in ipv4.strip().splitlines():
|
||||
addr = line.split()[3].split("/")[0]
|
||||
if not re.match(r"^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1]))", addr):
|
||||
ipv4_address = addr
|
||||
break
|
||||
ipv6 = subprocess.check_output(["ip", "-o", "-6", "addr", "show", iface]).decode()
|
||||
for line in ipv6.strip().splitlines():
|
||||
addr = line.split()[3].split("/")[0]
|
||||
if not re.match(r"^(::1|fe80:)", addr):
|
||||
ipv6_address = addr
|
||||
break
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
|
||||
return ipv4_address, ipv6_address
|
||||
|
||||
|
||||
def add_ips():
|
||||
ensure_env_file_exists()
|
||||
ipv4, ipv6 = get_interface_addresses()
|
||||
|
||||
update_config("IP4", ipv4 or "")
|
||||
update_config("IP6", ipv6 or "")
|
||||
|
||||
print(f"Updated IP4={ipv4 or 'Not Found'}")
|
||||
print(f"Updated IP6={ipv6 or 'Not Found'}")
|
||||
|
||||
|
||||
def edit_ip(option: str, new_ip: str):
|
||||
ensure_env_file_exists()
|
||||
if option == "-4":
|
||||
update_config("IP4", new_ip)
|
||||
print(f"IP4 has been updated to {new_ip}.")
|
||||
elif option == "-6":
|
||||
update_config("IP6", new_ip)
|
||||
print(f"IP6 has been updated to {new_ip}.")
|
||||
else:
|
||||
print("Invalid option. Use -4 for IPv4 or -6 for IPv6.")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: {add|edit -4|-6 <new_ip>}")
|
||||
sys.exit(1)
|
||||
|
||||
action = sys.argv[1]
|
||||
|
||||
if action == "add":
|
||||
add_ips()
|
||||
elif action == "edit" and len(sys.argv) == 4:
|
||||
edit_ip(sys.argv[2], sys.argv[3])
|
||||
else:
|
||||
print("Usage: {add|edit -4|-6 <new_ip>}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
source /etc/hysteria/core/scripts/path.sh
|
||||
|
||||
if [ ! -f "$CONFIG_ENV" ]; then
|
||||
echo "CONFIG_ENV not found. Creating a new one..."
|
||||
touch "$CONFIG_ENV"
|
||||
fi
|
||||
|
||||
update_config() {
|
||||
local key=$1
|
||||
local value=$2
|
||||
|
||||
sed -i "/^$key=/d" "$CONFIG_ENV" 2>/dev/null
|
||||
|
||||
echo "$key=$value" >> "$CONFIG_ENV"
|
||||
}
|
||||
|
||||
add_ips() {
|
||||
ipv4_address=""
|
||||
ipv6_address=""
|
||||
|
||||
interfaces=$(ip -o link show | awk -F': ' '{print $2}' | grep -vE '^(lo|wgcf|warp)$')
|
||||
|
||||
for interface in $interfaces; do
|
||||
if ip addr show "$interface" > /dev/null 2>&1; then
|
||||
ipv4=$(ip -o -4 addr show "$interface" | awk '{print $4}' | grep -vE '^(127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3[0-1]))' | head -n 1 | cut -d/ -f1)
|
||||
if [[ -z $ipv4_address && -n $ipv4 ]]; then
|
||||
ipv4_address=$ipv4
|
||||
fi
|
||||
|
||||
ipv6=$(ip -o -6 addr show "$interface" | awk '{print $4}' | grep -vE '^(::1|fe80:)' | head -n 1 | cut -d/ -f1)
|
||||
if [[ -z $ipv6_address && -n $ipv6 ]]; then
|
||||
ipv6_address=$ipv6
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
update_config "IP4" "${ipv4_address:-}"
|
||||
update_config "IP6" "${ipv6_address:-}"
|
||||
|
||||
echo "Updated IP4=${ipv4_address:-Not Found}"
|
||||
echo "Updated IP6=${ipv6_address:-Not Found}"
|
||||
}
|
||||
|
||||
edit_ip() {
|
||||
local type=$1
|
||||
local new_ip=$2
|
||||
|
||||
if [[ $type == "-4" ]]; then
|
||||
update_config "IP4" "$new_ip"
|
||||
echo "IP4 has been updated to $new_ip."
|
||||
elif [[ $type == "-6" ]]; then
|
||||
update_config "IP6" "$new_ip"
|
||||
echo "IP6 has been updated to $new_ip."
|
||||
else
|
||||
echo "Invalid option. Use -4 for IPv4 or -6 for IPv6."
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
add)
|
||||
add_ips
|
||||
;;
|
||||
edit)
|
||||
edit_ip "$2" "$3"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {add|edit -4|-6 <new_ip>}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
122
core/scripts/hysteria2/server_info.py
Normal file
122
core/scripts/hysteria2/server_info.py
Normal file
@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import json
|
||||
from hysteria2_api import Hysteria2Client
|
||||
import time
|
||||
from init_paths import *
|
||||
from paths import *
|
||||
|
||||
|
||||
def get_secret() -> str:
|
||||
if not CONFIG_FILE.exists():
|
||||
print("Error: config.json file not found!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with CONFIG_FILE.open() as f:
|
||||
data = json.load(f)
|
||||
|
||||
secret = data.get("trafficStats", {}).get("secret")
|
||||
if not secret:
|
||||
print("Error: secret not found in config.json!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return secret
|
||||
|
||||
|
||||
def convert_bytes(bytes_val: int) -> str:
|
||||
units = [("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)]
|
||||
for unit, factor in units:
|
||||
if bytes_val >= factor:
|
||||
return f"{bytes_val / factor:.2f} {unit}"
|
||||
return f"{bytes_val} B"
|
||||
|
||||
|
||||
def get_cpu_usage(interval: float = 0.1) -> float:
|
||||
def read_cpu_times():
|
||||
with open("/proc/stat") as f:
|
||||
line = f.readline()
|
||||
fields = list(map(int, line.strip().split()[1:]))
|
||||
idle, total = fields[3], sum(fields)
|
||||
return idle, total
|
||||
|
||||
idle1, total1 = read_cpu_times()
|
||||
time.sleep(interval)
|
||||
idle2, total2 = read_cpu_times()
|
||||
|
||||
idle_delta = idle2 - idle1
|
||||
total_delta = total2 - total1
|
||||
cpu_usage = 100.0 * (1 - idle_delta / total_delta) if total_delta else 0.0
|
||||
return round(cpu_usage, 1)
|
||||
|
||||
|
||||
|
||||
def get_memory_usage() -> tuple[int, int]:
|
||||
with open("/proc/meminfo") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
mem_total = int(next(line for line in lines if "MemTotal" in line).split()[1]) // 1024
|
||||
mem_available = int(next(line for line in lines if "MemAvailable" in line).split()[1]) // 1024
|
||||
mem_used = mem_total - mem_available
|
||||
|
||||
return mem_total, mem_used
|
||||
|
||||
|
||||
|
||||
def get_online_user_count(secret: str) -> int:
|
||||
try:
|
||||
client = Hysteria2Client(
|
||||
base_url=API_BASE_URL,
|
||||
secret=secret
|
||||
)
|
||||
online_users = client.get_online_clients()
|
||||
return sum(1 for user in online_users.values() if user.is_online)
|
||||
except Exception as e:
|
||||
print(f"Error getting online users: {e}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
def get_total_traffic() -> tuple[int, int]:
|
||||
if not USERS_FILE.exists():
|
||||
return 0, 0
|
||||
|
||||
try:
|
||||
with USERS_FILE.open() as f:
|
||||
users = json.load(f)
|
||||
|
||||
total_upload = 0
|
||||
total_download = 0
|
||||
|
||||
for user_data in users.values():
|
||||
total_upload += int(user_data.get("upload_bytes", 0) or 0)
|
||||
total_download += int(user_data.get("download_bytes", 0) or 0)
|
||||
|
||||
return total_upload, total_download
|
||||
except Exception as e:
|
||||
print(f"Error parsing traffic data: {e}", file=sys.stderr)
|
||||
return 0, 0
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
secret = get_secret()
|
||||
|
||||
cpu_usage = get_cpu_usage()
|
||||
mem_total, mem_used = get_memory_usage()
|
||||
online_users = get_online_user_count(secret)
|
||||
|
||||
print(f"📈 CPU Usage: {cpu_usage}")
|
||||
print(f"📋 Total RAM: {mem_total}MB")
|
||||
print(f"💻 Used RAM: {mem_used}MB")
|
||||
print(f"👥 Online Users: {online_users}")
|
||||
print()
|
||||
|
||||
total_upload, total_download = get_total_traffic()
|
||||
|
||||
print(f"🔼 Uploaded Traffic: {convert_bytes(total_upload)}")
|
||||
print(f"🔽 Downloaded Traffic: {convert_bytes(total_download)}")
|
||||
print(f"📊 Total Traffic: {convert_bytes(total_upload + total_download)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,63 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source /etc/hysteria/core/scripts/path.sh
|
||||
|
||||
get_secret() {
|
||||
[ ! -f "$CONFIG_FILE" ] && { echo "Error: config.json file not found!" >&2; exit 1; }
|
||||
|
||||
local secret=$(jq -r '.trafficStats.secret' "$CONFIG_FILE")
|
||||
|
||||
[ "$secret" = "null" ] || [ -z "$secret" ] && {
|
||||
echo "Error: secret not found in config.json!" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "$secret"
|
||||
}
|
||||
|
||||
convert_bytes() {
|
||||
local bytes=$1
|
||||
if (( bytes < 1048576 )); then
|
||||
printf "%.2f KB" "$(echo "scale=2; $bytes / 1024" | bc)"
|
||||
elif (( bytes < 1073741824 )); then
|
||||
printf "%.2f MB" "$(echo "scale=2; $bytes / 1048576" | bc)"
|
||||
elif (( bytes < 1099511627776 )); then
|
||||
printf "%.2f GB" "$(echo "scale=2; $bytes / 1073741824" | bc)"
|
||||
else
|
||||
printf "%.2f TB" "$(echo "scale=2; $bytes / 1099511627776" | bc)"
|
||||
fi
|
||||
}
|
||||
|
||||
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1"%"}')
|
||||
|
||||
mem_stats=$(free -m)
|
||||
mem_total=$(echo "$mem_stats" | awk '/Mem:/ {print $2}')
|
||||
mem_used=$(echo "$mem_stats" | awk '/Mem:/ {print $3}')
|
||||
|
||||
secret=$(get_secret)
|
||||
online_users=$(curl -s -H "Authorization: $secret" "$ONLINE_API_URL")
|
||||
online_user_count=$(echo "$online_users" | jq 'add // 0')
|
||||
|
||||
echo "📈 CPU Usage: $cpu_usage"
|
||||
echo "📋 Total RAM: ${mem_total}MB"
|
||||
echo "💻 Used RAM: ${mem_used}MB"
|
||||
echo "👥 Online Users: $online_user_count"
|
||||
echo
|
||||
|
||||
if [ -f "$USERS_FILE" ]; then
|
||||
read total_upload total_download <<< $(jq -r '
|
||||
reduce .[] as $user (
|
||||
{"up": 0, "down": 0};
|
||||
.up += (($user.upload_bytes | numbers) // 0) |
|
||||
.down += (($user.download_bytes | numbers) // 0)
|
||||
) | "\(.up) \(.down)"' "$USERS_FILE" 2>/dev/null || echo "0 0")
|
||||
|
||||
total_upload=${total_upload:-0}
|
||||
total_download=${total_download:-0}
|
||||
|
||||
echo "🔼 Uploaded Traffic: $(convert_bytes "$total_upload")"
|
||||
echo "🔽 Downloaded Traffic: $(convert_bytes "$total_download")"
|
||||
|
||||
total_traffic=$((total_upload + total_download))
|
||||
echo "📊 Total Traffic: $(convert_bytes "$total_traffic")"
|
||||
fi
|
||||
74
core/scripts/telegrambot/runbot.py
Normal file
74
core/scripts/telegrambot/runbot.py
Normal file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
core_scripts_dir = Path(__file__).resolve().parents[1]
|
||||
if str(core_scripts_dir) not in sys.path:
|
||||
sys.path.append(str(core_scripts_dir))
|
||||
|
||||
from paths import TELEGRAM_ENV
|
||||
|
||||
|
||||
|
||||
|
||||
def update_env_file(api_token, admin_user_ids):
|
||||
TELEGRAM_ENV.write_text(f"""API_TOKEN={api_token}
|
||||
ADMIN_USER_IDS=[{admin_user_ids}]
|
||||
""")
|
||||
|
||||
def create_service_file():
|
||||
Path("/etc/systemd/system/hysteria-telegram-bot.service").write_text("""[Unit]
|
||||
Description=Hysteria Telegram Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/hysteria2_venv/bin/python /etc/hysteria/core/scripts/telegrambot/tbot.py'
|
||||
WorkingDirectory=/etc/hysteria/core/scripts/telegrambot
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
""")
|
||||
|
||||
def start_service(api_token, admin_user_ids):
|
||||
if subprocess.run(["systemctl", "is-active", "--quiet", "hysteria-telegram-bot.service"]).returncode == 0:
|
||||
print("The hysteria-telegram-bot.service is already running.")
|
||||
return
|
||||
|
||||
update_env_file(api_token, admin_user_ids)
|
||||
create_service_file()
|
||||
|
||||
subprocess.run(["systemctl", "daemon-reload"])
|
||||
subprocess.run(["systemctl", "enable", "hysteria-telegram-bot.service"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
subprocess.run(["systemctl", "start", "hysteria-telegram-bot.service"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
if subprocess.run(["systemctl", "is-active", "--quiet", "hysteria-telegram-bot.service"]).returncode == 0:
|
||||
print("Hysteria bot setup completed. The service is now running.\n")
|
||||
else:
|
||||
print("Hysteria bot setup completed. The service failed to start.")
|
||||
|
||||
def stop_service():
|
||||
subprocess.run(["systemctl", "stop", "hysteria-telegram-bot.service"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
subprocess.run(["systemctl", "disable", "hysteria-telegram-bot.service"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
TELEGRAM_ENV.unlink(missing_ok=True)
|
||||
print("\nHysteria bot service stopped and disabled. .env file removed.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 runbot.py {start|stop} <API_TOKEN> <ADMIN_USER_IDS>")
|
||||
sys.exit(1)
|
||||
|
||||
action = sys.argv[1]
|
||||
|
||||
if action == "start":
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python3 runbot.py start <API_TOKEN> <ADMIN_USER_IDS>")
|
||||
sys.exit(1)
|
||||
start_service(sys.argv[2], sys.argv[3])
|
||||
elif action == "stop":
|
||||
stop_service()
|
||||
else:
|
||||
print("Usage: python3 runbot.py {start|stop} <API_TOKEN> <ADMIN_USER_IDS>")
|
||||
sys.exit(1)
|
||||
@ -1,78 +0,0 @@
|
||||
#!/bin/bash
|
||||
source /etc/hysteria/core/scripts/utils.sh
|
||||
define_colors
|
||||
|
||||
update_env_file() {
|
||||
local api_token=$1
|
||||
local admin_user_ids=$2
|
||||
|
||||
cat <<EOL > /etc/hysteria/core/scripts/telegrambot/.env
|
||||
API_TOKEN=$api_token
|
||||
ADMIN_USER_IDS=[$admin_user_ids]
|
||||
EOL
|
||||
}
|
||||
|
||||
create_service_file() {
|
||||
cat <<EOL > /etc/systemd/system/hysteria-telegram-bot.service
|
||||
[Unit]
|
||||
Description=Hysteria Telegram Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/hysteria2_venv/bin/python /etc/hysteria/core/scripts/telegrambot/tbot.py'
|
||||
WorkingDirectory=/etc/hysteria/core/scripts/telegrambot
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local api_token=$1
|
||||
local admin_user_ids=$2
|
||||
|
||||
if systemctl is-active --quiet hysteria-telegram-bot.service; then
|
||||
echo "The hysteria-telegram-bot.service is already running."
|
||||
return
|
||||
fi
|
||||
|
||||
update_env_file "$api_token" "$admin_user_ids"
|
||||
create_service_file
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable hysteria-telegram-bot.service > /dev/null 2>&1
|
||||
systemctl start hysteria-telegram-bot.service > /dev/null 2>&1
|
||||
|
||||
if systemctl is-active --quiet hysteria-telegram-bot.service; then
|
||||
echo -e "${green}Hysteria bot setup completed. The service is now running. ${NC}"
|
||||
echo -e "\n\n"
|
||||
else
|
||||
echo "Hysteria bot setup completed. The service failed to start."
|
||||
fi
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
systemctl stop hysteria-telegram-bot.service > /dev/null 2>&1
|
||||
systemctl disable hysteria-telegram-bot.service > /dev/null 2>&1
|
||||
|
||||
rm -f /etc/hysteria/core/scripts/telegrambot/.env
|
||||
echo -e "\n"
|
||||
|
||||
echo "Hysteria bot service stopped and disabled. .env file removed."
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start_service "$2" "$3"
|
||||
;;
|
||||
stop)
|
||||
stop_service
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop} <API_TOKEN> <ADMIN_USER_IDS>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
define_colors
|
||||
159
core/scripts/warp/configure.py
Normal file
159
core/scripts/warp/configure.py
Normal file
@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
core_scripts_dir = Path(__file__).resolve().parents[1]
|
||||
if str(core_scripts_dir) not in sys.path:
|
||||
sys.path.append(str(core_scripts_dir))
|
||||
|
||||
from paths import *
|
||||
|
||||
def warp_configure_handler(all_traffic=False, popular_sites=False, domestic_sites=False, block_adult_sites=False):
|
||||
"""
|
||||
Configure WARP routing rules based on provided parameters
|
||||
|
||||
Args:
|
||||
all_traffic (bool): Toggle WARP for all traffic
|
||||
popular_sites (bool): Toggle WARP for popular sites (Google, Netflix, etc.)
|
||||
domestic_sites (bool): Toggle between WARP and Reject for domestic sites
|
||||
block_adult_sites (bool): Toggle blocking of adult content
|
||||
"""
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
modified = False
|
||||
|
||||
if all_traffic:
|
||||
warp_all_active = any(rule == "warps(all)" for rule in config.get('acl', {}).get('inline', []))
|
||||
|
||||
if warp_all_active:
|
||||
config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule != "warps(all)"]
|
||||
print("Traffic configuration changed to Direct.")
|
||||
modified = True
|
||||
else:
|
||||
if 'acl' not in config:
|
||||
config['acl'] = {}
|
||||
if 'inline' not in config['acl']:
|
||||
config['acl']['inline'] = []
|
||||
config['acl']['inline'].append("warps(all)")
|
||||
print("Traffic configuration changed to WARP.")
|
||||
modified = True
|
||||
|
||||
if popular_sites:
|
||||
popular_rules = [
|
||||
"warps(geoip:google)",
|
||||
"warps(geosite:google)",
|
||||
"warps(geosite:netflix)",
|
||||
"warps(geosite:spotify)",
|
||||
"warps(geosite:openai)",
|
||||
"warps(geoip:openai)"
|
||||
]
|
||||
|
||||
rule_exists = any(rule in config.get('acl', {}).get('inline', []) for rule in popular_rules)
|
||||
|
||||
if rule_exists:
|
||||
config['acl']['inline'] = [rule for rule in config['acl']['inline']
|
||||
if rule not in popular_rules]
|
||||
print("WARP configuration for Google, OpenAI, etc. removed.")
|
||||
modified = True
|
||||
else:
|
||||
if 'acl' not in config:
|
||||
config['acl'] = {}
|
||||
if 'inline' not in config['acl']:
|
||||
config['acl']['inline'] = []
|
||||
config['acl']['inline'].extend(popular_rules)
|
||||
print("WARP configured for Google, OpenAI, etc.")
|
||||
modified = True
|
||||
|
||||
if domestic_sites:
|
||||
ir_site_rule = "warps(geosite:ir)"
|
||||
ir_ip_rule = "warps(geoip:ir)"
|
||||
reject_site_rule = "reject(geosite:ir)"
|
||||
reject_ip_rule = "reject(geoip:ir)"
|
||||
|
||||
using_warp = (ir_site_rule in config.get('acl', {}).get('inline', []) and
|
||||
ir_ip_rule in config.get('acl', {}).get('inline', []))
|
||||
using_reject = (reject_site_rule in config.get('acl', {}).get('inline', []) and
|
||||
reject_ip_rule in config.get('acl', {}).get('inline', []))
|
||||
|
||||
if 'acl' not in config:
|
||||
config['acl'] = {}
|
||||
if 'inline' not in config['acl']:
|
||||
config['acl']['inline'] = []
|
||||
|
||||
if using_warp:
|
||||
config['acl']['inline'] = [reject_site_rule if rule == ir_site_rule else
|
||||
reject_ip_rule if rule == ir_ip_rule else
|
||||
rule for rule in config['acl']['inline']]
|
||||
print("Configuration changed to Reject for geosite:ir and geoip:ir.")
|
||||
modified = True
|
||||
elif using_reject:
|
||||
config['acl']['inline'] = [ir_site_rule if rule == reject_site_rule else
|
||||
ir_ip_rule if rule == reject_ip_rule else
|
||||
rule for rule in config['acl']['inline']]
|
||||
print("Configuration changed to Use WARP for geosite:ir and geoip:ir.")
|
||||
modified = True
|
||||
else:
|
||||
config['acl']['inline'].extend([reject_site_rule, reject_ip_rule])
|
||||
print("Added Reject configuration for geosite:ir and geoip:ir.")
|
||||
modified = True
|
||||
|
||||
if block_adult_sites:
|
||||
nsfw_rule = "reject(geosite:nsfw)"
|
||||
|
||||
blocked = nsfw_rule in config.get('acl', {}).get('inline', [])
|
||||
|
||||
if blocked:
|
||||
config['acl']['inline'] = [rule for rule in config['acl']['inline']
|
||||
if rule != nsfw_rule]
|
||||
if 'resolver' not in config:
|
||||
config['resolver'] = {}
|
||||
if 'tls' not in config['resolver']:
|
||||
config['resolver']['tls'] = {}
|
||||
config['resolver']['tls']['addr'] = "1.1.1.1:853"
|
||||
print("Adult content blocking removed and resolver updated.")
|
||||
modified = True
|
||||
else:
|
||||
if 'acl' not in config:
|
||||
config['acl'] = {}
|
||||
if 'inline' not in config['acl']:
|
||||
config['acl']['inline'] = []
|
||||
config['acl']['inline'].append(nsfw_rule)
|
||||
if 'resolver' not in config:
|
||||
config['resolver'] = {}
|
||||
if 'tls' not in config['resolver']:
|
||||
config['resolver']['tls'] = {}
|
||||
config['resolver']['tls']['addr'] = "1.1.1.3:853"
|
||||
print("Adult content blocked and resolver updated.")
|
||||
modified = True
|
||||
|
||||
if modified:
|
||||
with open(CONFIG_FILE, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
try:
|
||||
subprocess.run(["python3", CLI_PATH, "restart-hysteria2"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Warning: Failed to restart hysteria2")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Configure WARP settings")
|
||||
parser.add_argument("--all", action="store_true", help="Toggle WARP for all traffic")
|
||||
parser.add_argument("--popular-sites", action="store_true", help="Toggle WARP for popular sites")
|
||||
parser.add_argument("--domestic-sites", action="store_true", help="Toggle between WARP and Reject for domestic sites")
|
||||
parser.add_argument("--block-adult", action="store_true", help="Toggle blocking of adult content")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
warp_configure_handler(
|
||||
all_traffic=args.all,
|
||||
popular_sites=args.popular_sites,
|
||||
domestic_sites=args.domestic_sites,
|
||||
block_adult_sites=args.block_adult
|
||||
)
|
||||
@ -1,78 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source /etc/hysteria/core/scripts/path.sh
|
||||
|
||||
warp_configure_handler() {
|
||||
local all=$1
|
||||
local popular_sites=$2
|
||||
local domestic_sites=$3
|
||||
local block_adult_sites=$4
|
||||
local warp_option=$5
|
||||
local warp_key=$6
|
||||
|
||||
if [ "$all" == "true" ]; then
|
||||
if [ "$(jq -r 'if .acl.inline | index("warps(all)") then "WARP active" else "Direct" end' "$CONFIG_FILE")" == "WARP active" ]; then
|
||||
jq 'del(.acl.inline[] | select(. == "warps(all)"))' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
echo "Traffic configuration changed to Direct."
|
||||
else
|
||||
jq '.acl.inline += ["warps(all)"]' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
echo "Traffic configuration changed to WARP."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$popular_sites" == "true" ]; then
|
||||
if [ "$(jq -r 'if (.acl.inline | index("warps(geoip:google)")) or (.acl.inline | index("warps(geosite:google)")) or (.acl.inline | index("warps(geosite:netflix)")) or (.acl.inline | index("warps(geosite:spotify)")) or (.acl.inline | index("warps(geosite:openai)")) or (.acl.inline | index("warps(geoip:openai)")) then "WARP active" else "Direct" end' "$CONFIG_FILE")" == "WARP active" ]; then
|
||||
jq 'del(.acl.inline[] | select(. == "warps(geoip:google)" or . == "warps(geosite:google)" or . == "warps(geosite:netflix)" or . == "warps(geosite:spotify)" or . == "warps(geosite:openai)" or . == "warps(geoip:openai)"))' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
echo "WARP configuration for Google, OpenAI, etc. removed."
|
||||
else
|
||||
jq '.acl.inline += ["warps(geoip:google)", "warps(geosite:google)", "warps(geosite:netflix)", "warps(geosite:spotify)", "warps(geosite:openai)", "warps(geoip:openai)"]' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
echo "WARP configured for Google, OpenAI, etc."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$domestic_sites" == "true" ]; then
|
||||
if [ "$(jq -r 'if (.acl.inline | index("warps(geosite:ir)")) and (.acl.inline | index("warps(geoip:ir)")) then "Use WARP" else "Reject" end' "$CONFIG_FILE")" == "Use WARP" ]; then
|
||||
jq '(.acl.inline[] | select(. == "warps(geosite:ir)")) = "reject(geosite:ir)" | (.acl.inline[] | select(. == "warps(geoip:ir)")) = "reject(geoip:ir)"' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
echo "Configuration changed to Reject for geosite:ir and geoip:ir."
|
||||
else
|
||||
jq '(.acl.inline[] | select(. == "reject(geosite:ir)")) = "warps(geosite:ir)" | (.acl.inline[] | select(. == "reject(geoip:ir)")) = "warps(geoip:ir)"' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
echo "Configuration changed to Use WARP for geosite:ir and geoip:ir."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$block_adult_sites" == "true" ]; then
|
||||
if [ "$(jq -r 'if .acl.inline | index("reject(geosite:nsfw)") then "Blocked" else "Not blocked" end' "$CONFIG_FILE")" == "Blocked" ]; then
|
||||
jq 'del(.acl.inline[] | select(. == "reject(geosite:nsfw)"))' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
jq '.resolver.tls.addr = "1.1.1.1:853"' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
echo "Adult content blocking removed and resolver updated."
|
||||
else
|
||||
jq '.acl.inline += ["reject(geosite:nsfw)"]' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
jq '.resolver.tls.addr = "1.1.1.3:853"' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE"
|
||||
echo "Adult content blocked and resolver updated."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$warp_option" == "warp plus" ]; then
|
||||
if [ -z "$warp_key" ]; then
|
||||
echo "Error: WARP Plus key is required. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
cd /etc/warp/ || { echo "Failed to change directory to /etc/warp/"; exit 1; }
|
||||
|
||||
WGCF_LICENSE_KEY="$warp_key" wgcf update
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to update WARP Plus configuration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
elif [ "$warp_option" == "warp" ]; then
|
||||
cd /etc/warp/ || { echo "Failed to change directory to /etc/warp/"; exit 1; }
|
||||
rm wgcf-account.toml && yes | wgcf register
|
||||
echo "WARP configured with a new account."
|
||||
fi
|
||||
|
||||
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
|
||||
}
|
||||
|
||||
warp_configure_handler "$1" "$2" "$3" "$4" "$5" "$6"
|
||||
77
core/scripts/warp/install.py
Normal file
77
core/scripts/warp/install.py
Normal file
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
core_scripts_dir = Path(__file__).resolve().parents[1]
|
||||
|
||||
if str(core_scripts_dir) not in sys.path:
|
||||
sys.path.append(str(core_scripts_dir))
|
||||
|
||||
from paths import *
|
||||
|
||||
WARP_DEVICE = "wgcf"
|
||||
|
||||
def is_service_active(service_name: str) -> bool:
|
||||
return subprocess.run(["systemctl", "is-active", "--quiet", service_name]).returncode == 0
|
||||
|
||||
|
||||
def install_warp():
|
||||
print("Installing WARP...")
|
||||
result = subprocess.run("bash <(curl -fsSL https://raw.githubusercontent.com/ReturnFI/Warp/main/warp.sh) wgx",
|
||||
shell=True, executable="/bin/bash")
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def add_warp_outbound_to_config():
|
||||
if not CONFIG_FILE.exists():
|
||||
print(f"Error: Config file {CONFIG_FILE} not found.")
|
||||
return
|
||||
|
||||
with open(CONFIG_FILE, "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
outbounds = config.get("outbounds", [])
|
||||
if any(outbound.get("name") == "warps" for outbound in outbounds):
|
||||
print("WARP outbound already exists in the configuration.")
|
||||
return
|
||||
|
||||
outbounds.append({
|
||||
"name": "warps",
|
||||
"type": "direct",
|
||||
"direct": {
|
||||
"mode": 4,
|
||||
"bindDevice": WARP_DEVICE
|
||||
}
|
||||
})
|
||||
config["outbounds"] = outbounds
|
||||
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
print("WARP outbound added to config.json.")
|
||||
|
||||
|
||||
def restart_hysteria():
|
||||
subprocess.run(["python3", str(CLI_PATH), "restart-hysteria2"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
print("Hysteria2 restarted with updated configuration.")
|
||||
|
||||
|
||||
def main():
|
||||
warp_service = f"wg-quick@{WARP_DEVICE}.service"
|
||||
|
||||
if is_service_active(warp_service):
|
||||
print("WARP is already active. Checking configuration...")
|
||||
add_warp_outbound_to_config()
|
||||
restart_hysteria()
|
||||
else:
|
||||
if install_warp() and is_service_active(warp_service):
|
||||
print("WARP installation successful.")
|
||||
add_warp_outbound_to_config()
|
||||
restart_hysteria()
|
||||
else:
|
||||
print("WARP installation failed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,41 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source /etc/hysteria/core/scripts/path.sh
|
||||
|
||||
if systemctl is-active --quiet wg-quick@wgcf.service; then
|
||||
echo "WARP is already active. Checking configuration..."
|
||||
|
||||
if [ -f "$CONFIG_FILE" ] && jq -e '.outbounds[] | select(.name == "warps")' "$CONFIG_FILE" > /dev/null 2>&1; then
|
||||
echo "WARP outbound already exists in the configuration. No changes needed."
|
||||
else
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
jq '.outbounds += [{"name": "warps", "type": "direct", "direct": {"mode": 4, "bindDevice": "wgcf"}}]' "$CONFIG_FILE" > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json "$CONFIG_FILE"
|
||||
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
|
||||
echo "WARP outbound added to config.json."
|
||||
else
|
||||
echo "Error: Config file $CONFIG_FILE not found."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Installing WARP..."
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/ReturnFI/Warp/main/warp.sh) wgx
|
||||
|
||||
if systemctl is-active --quiet wg-quick@wgcf.service; then
|
||||
echo "WARP installation successful."
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
if jq -e '.outbounds[] | select(.name == "warps")' "$CONFIG_FILE" > /dev/null 2>&1; then
|
||||
echo "WARP outbound already exists in the configuration."
|
||||
else
|
||||
jq '.outbounds += [{"name": "warps", "type": "direct", "direct": {"mode": 4, "bindDevice": "wgcf"}}]' "$CONFIG_FILE" > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json "$CONFIG_FILE"
|
||||
echo "WARP outbound added to config.json."
|
||||
fi
|
||||
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
|
||||
echo "Hysteria2 restarted with updated configuration."
|
||||
else
|
||||
echo "Error: Config file $CONFIG_FILE not found."
|
||||
fi
|
||||
else
|
||||
echo "WARP installation failed."
|
||||
fi
|
||||
fi
|
||||
74
core/scripts/warp/status.py
Normal file
74
core/scripts/warp/status.py
Normal file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
core_scripts_dir = Path(__file__).resolve().parents[1]
|
||||
if str(core_scripts_dir) not in sys.path:
|
||||
sys.path.append(str(core_scripts_dir))
|
||||
|
||||
from paths import *
|
||||
|
||||
colors = {
|
||||
"cyan": "\033[96m",
|
||||
"green": "\033[92m",
|
||||
"red": "\033[91m",
|
||||
"purple": "\033[95m",
|
||||
"end": "\033[0m"
|
||||
}
|
||||
|
||||
def echo_status(label, is_active):
|
||||
status = f"{colors['green']}Active{colors['end']}" if is_active else f"{colors['red']}Inactive{colors['end']}"
|
||||
print(f"{colors['cyan']}{label}:{colors['end']} {status}")
|
||||
|
||||
def check_warp_configuration():
|
||||
if not Path(CONFIG_FILE).exists():
|
||||
print(f"{colors['red']}Error: Config file not found at {CONFIG_FILE}{colors['end']}")
|
||||
return
|
||||
|
||||
with open(CONFIG_FILE, "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
acl_inline = config.get("acl", {}).get("inline", [])
|
||||
|
||||
def contains_warp(rule_prefixes):
|
||||
return any(rule.startswith(prefix) for rule in acl_inline for prefix in rule_prefixes)
|
||||
|
||||
print("--------------------------------")
|
||||
print(f"{colors['purple']}Current WARP Configuration:{colors['end']}")
|
||||
|
||||
echo_status(
|
||||
"All traffic",
|
||||
contains_warp(["warps(all)"])
|
||||
)
|
||||
|
||||
echo_status(
|
||||
"Popular sites (Google, Netflix, etc.)",
|
||||
contains_warp([
|
||||
"warps(geosite:google)",
|
||||
"warps(geoip:google)",
|
||||
"warps(geosite:netflix)",
|
||||
"warps(geosite:spotify)",
|
||||
"warps(geosite:openai)",
|
||||
"warps(geoip:openai)"
|
||||
])
|
||||
)
|
||||
|
||||
echo_status(
|
||||
"Domestic sites (geosite:ir, geoip:ir)",
|
||||
contains_warp([
|
||||
"warps(geosite:ir)",
|
||||
"warps(geoip:ir)"
|
||||
])
|
||||
)
|
||||
|
||||
echo_status(
|
||||
"Block adult content",
|
||||
"reject(geosite:nsfw)" in acl_inline
|
||||
)
|
||||
|
||||
print("--------------------------------")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_warp_configuration()
|
||||
@ -1,34 +0,0 @@
|
||||
source /etc/hysteria/core/scripts/utils.sh
|
||||
source /etc/hysteria/core/scripts/path.sh
|
||||
|
||||
check_warp_configuration() {
|
||||
echo "--------------------------------"
|
||||
echo -e "${LPurple}Current WARP Configuration: ${NC}"
|
||||
|
||||
if jq -e '.acl.inline[]? | select(test("warps\\(all\\)"))' "$CONFIG_FILE" > /dev/null; then
|
||||
echo -e "${cyan}All traffic:${NC} ${green}Active${NC}"
|
||||
else
|
||||
echo -e "${cyan}All traffic:${NC} ${red}Inactive${NC}"
|
||||
fi
|
||||
|
||||
if jq -e '.acl.inline[]? | select(test("warps\\(geosite:google\\)")) or select(test("warps\\(geoip:google\\)")) or select(test("warps\\(geosite:netflix\\)")) or select(test("warps\\(geosite:spotify\\)")) or select(test("warps\\(geosite:openai\\)")) or select(test("warps\\(geoip:openai\\)"))' "$CONFIG_FILE" > /dev/null; then
|
||||
echo -e "${cyan}Popular sites (Google, Netflix, etc.):${NC} ${green}Active${NC}"
|
||||
else
|
||||
echo -e "${cyan}Popular sites (Google, Netflix, etc.):${NC} ${red}Inactive${NC}"
|
||||
fi
|
||||
|
||||
if jq -e '.acl.inline[]? | select(test("warps\\(geosite:ir\\)")) or select(test("warps\\(geoip:ir\\)"))' "$CONFIG_FILE" > /dev/null; then
|
||||
echo -e "${cyan}Domestic sites (geosite:ir, geoip:ir):${NC} ${green}Active${NC}"
|
||||
else
|
||||
echo -e "${cyan}Domestic sites (geosite:ir, geoip:ir):${NC} ${red}Inactive${NC}"
|
||||
fi
|
||||
|
||||
if jq -e '.acl.inline[]? | select(test("reject\\(geosite:nsfw\\)"))' "$CONFIG_FILE" > /dev/null; then
|
||||
echo -e "${cyan}Block adult content:${NC} ${green}Active${NC}"
|
||||
else
|
||||
echo -e "${cyan}Block adult content:${NC} ${red}Inactive${NC}"
|
||||
fi
|
||||
echo "--------------------------------"
|
||||
}
|
||||
define_colors
|
||||
check_warp_configuration
|
||||
117
core/scripts/warp/uninstall.py
Normal file
117
core/scripts/warp/uninstall.py
Normal file
@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
core_scripts_dir = Path(__file__).resolve().parents[1]
|
||||
|
||||
if str(core_scripts_dir) not in sys.path:
|
||||
sys.path.append(str(core_scripts_dir))
|
||||
|
||||
from paths import CONFIG_FILE, CLI_PATH
|
||||
|
||||
TEMP_CONFIG = Path("/etc/hysteria/config_temp.json")
|
||||
|
||||
|
||||
def systemctl_active(service: str) -> bool:
|
||||
return subprocess.run(["systemctl", "is-active", "--quiet", service]).returncode == 0
|
||||
|
||||
|
||||
def run_shell(command: str):
|
||||
subprocess.run(command, shell=True, check=False)
|
||||
|
||||
|
||||
def load_config(path: Path):
|
||||
if path.exists():
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
print(f"❌ Config file not found: {path}")
|
||||
return None
|
||||
|
||||
|
||||
def save_config(config: dict, path: Path):
|
||||
with path.open("w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
shutil.move(str(path), str(CONFIG_FILE))
|
||||
|
||||
|
||||
def reset_acl_inline(config: dict):
|
||||
default = [
|
||||
"reject(geosite:ir)", "reject(geoip:ir)",
|
||||
"reject(geosite:category-ads-all)", "reject(geoip:private)",
|
||||
"reject(geosite:google@ads)"
|
||||
]
|
||||
updated = []
|
||||
for item in config.get("acl", {}).get("inline", []):
|
||||
if item in [
|
||||
"warps(all)", "warps(geoip:google)", "warps(geosite:google)",
|
||||
"warps(geosite:netflix)", "warps(geosite:spotify)",
|
||||
"warps(geosite:openai)", "warps(geoip:openai)"
|
||||
]:
|
||||
updated.append("direct")
|
||||
elif item == "warps(geosite:ir)":
|
||||
updated.append("reject(geosite:ir)")
|
||||
elif item == "warps(geoip:ir)":
|
||||
updated.append("reject(geoip:ir)")
|
||||
else:
|
||||
updated.append(item)
|
||||
|
||||
final_inline = default + [i for i in updated if i not in default and i != "direct"]
|
||||
config["acl"]["inline"] = final_inline
|
||||
return config
|
||||
|
||||
|
||||
def remove_warp_outbound(config: dict):
|
||||
config["outbounds"] = [
|
||||
o for o in config.get("outbounds", [])
|
||||
if not (
|
||||
o.get("name") == "warps" and
|
||||
o.get("type") == "direct" and
|
||||
o.get("direct", {}).get("mode") == 4 and
|
||||
o.get("direct", {}).get("bindDevice") == "wgcf"
|
||||
)
|
||||
]
|
||||
return config
|
||||
|
||||
|
||||
def remove_porn_blocking(config: dict):
|
||||
inline = config.get("acl", {}).get("inline", [])
|
||||
if "reject(geosite:category-porn)" in inline:
|
||||
config["acl"]["inline"] = [i for i in inline if i != "reject(geosite:category-porn)"]
|
||||
print("🔒 Adult content blocking removed.")
|
||||
return config
|
||||
|
||||
|
||||
def set_dns(config: dict):
|
||||
config.setdefault("resolver", {}).setdefault("tls", {})["addr"] = "1.1.1.1:853"
|
||||
print("🔧 DNS resolver changed to 1.1.1.1:853.")
|
||||
return config
|
||||
|
||||
|
||||
def restart_hysteria():
|
||||
subprocess.run(["python3", str(CLI_PATH), "restart-hysteria2"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def main():
|
||||
if systemctl_active("wg-quick@wgcf.service"):
|
||||
print("🧹 Uninstalling WARP...")
|
||||
run_shell('bash -c "bash <(curl -fsSL https://raw.githubusercontent.com/ReturnFI/Warp/main/warp.sh) dwg"')
|
||||
config = load_config(CONFIG_FILE)
|
||||
if config:
|
||||
config = reset_acl_inline(config)
|
||||
config = remove_warp_outbound(config)
|
||||
config = remove_porn_blocking(config)
|
||||
config = set_dns(config)
|
||||
save_config(config, TEMP_CONFIG)
|
||||
restart_hysteria()
|
||||
print("✅ WARP uninstalled and configuration reset.")
|
||||
else:
|
||||
print("ℹ️ WARP is not active. Skipping uninstallation.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,43 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
source /etc/hysteria/core/scripts/path.sh
|
||||
|
||||
if systemctl is-active --quiet wg-quick@wgcf.service; then
|
||||
echo "Uninstalling WARP..."
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/ReturnFI/Warp/main/warp.sh) dwg
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
default_config='["reject(geosite:ir)", "reject(geoip:ir)", "reject(geosite:category-ads-all)", "reject(geoip:private)", "reject(geosite:google@ads)"]'
|
||||
|
||||
jq --argjson default_config "$default_config" '
|
||||
.acl.inline |= map(
|
||||
if . == "warps(all)" or . == "warps(geoip:google)" or . == "warps(geosite:google)" or . == "warps(geosite:netflix)" or . == "warps(geosite:spotify)" or . == "warps(geosite:openai)" or . == "warps(geoip:openai)" then
|
||||
"direct"
|
||||
elif . == "warps(geosite:ir)" then
|
||||
"reject(geosite:ir)"
|
||||
elif . == "warps(geoip:ir)" then
|
||||
"reject(geoip:ir)"
|
||||
else
|
||||
.
|
||||
end
|
||||
) | .acl.inline |= ($default_config + (. - $default_config | map(select(. != "direct"))))
|
||||
' "$CONFIG_FILE" > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json "$CONFIG_FILE"
|
||||
|
||||
jq 'del(.outbounds[] | select(.name == "warps" and .type == "direct" and .direct.mode == 4 and .direct.bindDevice == "wgcf"))' "$CONFIG_FILE" > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json "$CONFIG_FILE"
|
||||
|
||||
if [ "$(jq -r 'if .acl.inline | index("reject(geosite:category-porn)") then "Blocked" else "Not blocked" end' "$CONFIG_FILE")" == "Blocked" ]; then
|
||||
jq 'del(.acl.inline[] | select(. == "reject(geosite:category-porn)"))' "$CONFIG_FILE" > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json "$CONFIG_FILE"
|
||||
echo "Adult content blocking removed."
|
||||
fi
|
||||
|
||||
jq '.resolver.tls.addr = "1.1.1.1:853"' "$CONFIG_FILE" > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json "$CONFIG_FILE"
|
||||
echo "DNS resolver address changed to 1.1.1.1:853."
|
||||
|
||||
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
|
||||
echo "WARP uninstalled and configurations reset to default."
|
||||
else
|
||||
echo "Error: Config file $CONFIG_FILE not found."
|
||||
fi
|
||||
else
|
||||
echo "WARP is not active. Skipping uninstallation."
|
||||
fi
|
||||
@ -61,7 +61,7 @@ async def configure(body: ConfigureInputBody):
|
||||
"""
|
||||
try:
|
||||
cli_api.configure_warp(body.all, body.popular_sites, body.domestic_sites,
|
||||
body.block_adult_sites, body.warp_option, body.warp_key)
|
||||
body.block_adult_sites)
|
||||
return DetailResponse(detail='WARP configured successfully.')
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
||||
|
||||
@ -7,8 +7,6 @@ class ConfigureInputBody(BaseModel):
|
||||
popular_sites: bool = False
|
||||
domestic_sites: bool = False
|
||||
block_adult_sites: bool = False
|
||||
warp_option: Literal['warp', 'warp plus', ''] = ''
|
||||
warp_key: str = ''
|
||||
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
|
||||
18
menu.sh
18
menu.sh
@ -389,10 +389,8 @@ warp_configure_handler() {
|
||||
echo "2. Use WARP for popular sites"
|
||||
echo "3. Use WARP for domestic sites"
|
||||
echo "4. Block adult content"
|
||||
echo "5. WARP (Plus) Profile"
|
||||
echo "6. WARP (Normal) Profile"
|
||||
echo "7. WARP Status Profile"
|
||||
echo "8. Change IP address"
|
||||
echo "5. WARP Status Profile"
|
||||
echo "6. Change IP address"
|
||||
echo "0. Cancel"
|
||||
|
||||
read -p "Select an option: " option
|
||||
@ -403,22 +401,12 @@ warp_configure_handler() {
|
||||
3) python3 $CLI_PATH configure-warp --domestic-sites ;;
|
||||
4) python3 $CLI_PATH configure-warp --block-adult-sites ;;
|
||||
5)
|
||||
echo "Please enter your WARP Plus key:"
|
||||
read -r warp_key
|
||||
if [ -z "$warp_key" ]; then
|
||||
echo "Error: WARP Plus key cannot be empty. Exiting."
|
||||
return
|
||||
fi
|
||||
python3 $CLI_PATH configure-warp --warp-option "warp plus" --warp-key "$warp_key"
|
||||
;;
|
||||
6) python3 $CLI_PATH configure-warp --warp-option "warp" ;;
|
||||
7)
|
||||
ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me)
|
||||
cd /etc/warp/ && wgcf status
|
||||
echo
|
||||
echo -e "${yellow}Warp IP :${NC} ${cyan}$ip ${NC}" ;;
|
||||
|
||||
8)
|
||||
6)
|
||||
old_ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me)
|
||||
echo "Current IP address: $old_ip"
|
||||
echo "Restarting $service_name..."
|
||||
|
||||
Reference in New Issue
Block a user