diff --git a/config.json b/config.json index fd9d9d4..81afeba 100644 --- a/config.json +++ b/config.json @@ -14,7 +14,7 @@ }, "auth": { "type": "command", - "command": "/etc/hysteria/users/user.sh" + "command": "/etc/hysteria/core/scripts/hysteria2/user.sh" }, "quic": { "initStreamReceiveWindow": 8388608, @@ -80,4 +80,4 @@ "listen": "127.0.0.1:25413", "secret": "$UUID" } -} +} \ No newline at end of file diff --git a/core/cli.py b/core/cli.py new file mode 100644 index 0000000..8b0666f --- /dev/null +++ b/core/cli.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 + +from datetime import datetime +import os +import io +import click +import subprocess +from enum import Enum + +import traffic +import validator + + +SCRIPT_DIR = '/etc/hysteria/core/scripts' +DEBUG = True + + +class Command(Enum): + '''Constais path to command's script''' + INSTALL_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'install.sh') + UNINSTALL_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'uninstall.sh') + UPDATE_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'update.sh') + RESTART_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'restart.sh') + CHANGE_PORT_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'change_port.sh') + GET_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'get_user.sh') + ADD_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'add_user.sh') + EDIT_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'edit_user.sh') + REMOVE_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'remove_user.sh') + SHOW_USER_URI = os.path.join(SCRIPT_DIR, 'hysteria2', 'show_user_uri.sh') + TRAFFIC_STATUS = 'traffic.py' # won't be call directly (it's a python module) + LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.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') + + +# region utils +def run_cmd(command: list[str]): + ''' + Runs a command and returns the output. + Could raise subprocess.CalledProcessError + ''' + if DEBUG and Command.GET_USER.value not in command and Command.LIST_USERS.value not in command: + print(' '.join(command)) + result = subprocess.check_output(command, shell=False) + if DEBUG: + print(result.decode().strip()) + + +def generate_password() -> str: + ''' + Generates a random password using pwgen for user. + Could raise subprocess.CalledProcessError + ''' + return subprocess.check_output(['pwgen', '-s', '32', '1'], shell=False).decode().strip() + +# endregion + + +@click.group() +def cli(): + pass + +# region hysteria2 menu options + + +@cli.command('install-hysteria2') +@click.option('--port', '-p', required=True, help='New port for Hysteria2', type=int, callback=validator.validate_port) +def install_hysteria2(port: int): + run_cmd(['bash', Command.INSTALL_HYSTERIA2.value, str(port)]) + + +@cli.command('uninstall-hysteria2') +def uninstall_hysteria2(): + run_cmd(['bash', Command.UNINSTALL_HYSTERIA2.value]) + + +@cli.command('update-hysteria2') +def update_hysteria2(): + run_cmd(['bash', Command.UPDATE_HYSTERIA2.value]) + + +@cli.command('restart-hysteria2') +def restart_hysteria2(): + run_cmd(['bash', Command.RESTART_HYSTERIA2.value]) + + +@cli.command('change-hysteria2-port') +@click.option('--port', '-p', required=True, help='New port for Hysteria2', type=int, callback=validator.validate_port) +def change_hysteria2_port(port: int): + run_cmd(['bash', Command.CHANGE_PORT_HYSTERIA2.value, str(port)]) + + +@cli.command('get-user') +@click.option('--username', '-u', required=True, help='Username for the user to get', type=str) +def get_user(username: str): + run_cmd(['bash', Command.GET_USER.value, username]) + + +@cli.command('add-user') +@click.option('--username', '-u', required=True, help='Username for the new user', type=str) +@click.option('--traffic-limit', '-t', required=True, help='Traffic limit for the new user in GB', type=int) +@click.option('--expiration-days', '-e', required=True, help='Expiration days for the new user', type=int) +@click.option('--password', '-p', required=False, help='Password for the user', type=str) +@click.option('--creation-date', '-c', required=False, help='Creation date for the user', type=str) +def add_user(username: str, traffic_limit: int, expiration_days: int, password: str, creation_date: str): + if not password: + try: + password = generate_password() + except subprocess.CalledProcessError as e: + print(f'Error: failed to generate password\n{e}') + exit(1) + if not creation_date: + creation_date = datetime.now().strftime('%Y-%m-%d') + + run_cmd(['bash', Command.ADD_USER.value, username, str(traffic_limit), str(expiration_days), password, creation_date]) + + +@cli.command('edit-user') +@click.option('--username', '-u', required=True, help='Username for the user to edit', type=str) +@click.option('--new-username', '-nu', required=False, help='New username for the user', type=str) +@click.option('--new-traffic-limit', '-nt', required=False, help='Traffic limit for the new user in GB', type=int) +@click.option('--new-expiration-days', '-ne', required=False, help='Expiration days for the new user', type=int) +@click.option('--renew-password', '-rp', is_flag=True, help='Renew password for the user') +@click.option('--renew-creation-date', '-rc', is_flag=True, help='Renew creation date for the user') +@click.option('--blocked', '-b', is_flag=True, help='Block the user') +def edit_user(username: str, new_username: str, new_traffic_limit: int, new_expiration_days: int, renew_password: bool, renew_creation_date: bool, blocked: bool): + if not username: + print('Error: username is required') + exit(1) + + if not any([new_username, new_traffic_limit, new_expiration_days, renew_password, renew_creation_date, blocked is not None]): + print('Error: at least one option is required') + exit(1) + + if new_traffic_limit is not None and new_traffic_limit <= 0: + print('Error: traffic limit must be greater than 0') + exit(1) + + if new_expiration_days is not None and new_expiration_days <= 0: + print('Error: expiration days must be greater than 0') + exit(1) + + # Handle renewing password and creation date + if renew_password: + try: + password = generate_password() + except subprocess.CalledProcessError as e: + print(f'Error: failed to generate password\n{e}') + exit(1) + else: + password = "" + + if renew_creation_date: + creation_date = datetime.now().strftime('%Y-%m-%d') + else: + creation_date = "" + + # Prepare arguments for the command + command_args = [ + 'bash', + Command.EDIT_USER.value, + username, + new_username or '', + str(new_traffic_limit) if new_traffic_limit is not None else '', + str(new_expiration_days) if new_expiration_days is not None else '', + password, + creation_date, + str(blocked).lower() if blocked is not None else 'false' + ] + + run_cmd(command_args) + + +@ cli.command('remove-user') +@ click.option('--username', '-u', required=True, help='Username for the user to remove', type=str) +def remove_user(username: str): + run_cmd(['bash', Command.REMOVE_USER.value, username]) + + +@ cli.command('show-user-uri') +@ click.option('--username', '-u', required=True, help='Username for the user to show the URI', type=str) +def show_user_uri(username: str): + run_cmd(['bash', Command.SHOW_USER_URI.value, username]) + + +@ cli.command('traffic-status') +def traffic_status(): + traffic.traffic_status() + + +@ cli.command('list-users') +def list_users(): + run_cmd(['bash', Command.LIST_USERS.value]) + +# endregion + +# region advanced menu + + +@ cli.command('install-tcp-brutal') +def install_tcp_brutal(): + run_cmd(['bash', Command.INSTALL_TCP_BRUTAL.value]) + + +@ cli.command('install-warp') +def install_warp(): + run_cmd(['bash', Command.INSTALL_WARP.value]) + + +@ cli.command('uninstall-warp') +def uninstall_warp(): + run_cmd(['bash', Command.UNINSTALL_WARP.value]) + + +@ cli.command('configure-warp') +@ click.option('--all', '-a', is_flag=True, help='Use WARP for all connections') +@ 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)') +def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool): + run_cmd(['bash', Command.CONFIGURE_WARP.value, str(all).lower(), str(popular_sites).lower(), str(domestic_sites).lower(), str(block_adult_sites).lower()]) + +# endregion + + +if __name__ == '__main__': + cli() diff --git a/core/scripts/hysteria2/add_user.sh b/core/scripts/hysteria2/add_user.sh new file mode 100644 index 0000000..90b3844 --- /dev/null +++ b/core/scripts/hysteria2/add_user.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Source the path.sh script to load the necessary variables +source /etc/hysteria/core/scripts/path.sh +source /etc/hysteria/core/scripts/utils.sh +define_colors + +# Function to add a new user to the configuration +add_user() { + if [ $# -ne 3 ] && [ $# -ne 5 ]; then + echo "Usage: $0 [password] [creation_date]" + exit 1 + fi + + username=$1 + traffic_gb=$2 + expiration_days=$3 + password=$4 + creation_date=$5 + + if [ -z "$password" ]; then + password=$(pwgen -s 32 1) + fi + if [ -z "$creation_date" ]; then + creation_date=$(date +%Y-%m-%d) + else + # Validate the date format (YYYY-MM-DD) + if ! [[ "$creation_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "Invalid date format. Expected YYYY-MM-DD." + exit 1 + fi + # Check if the date is valid + if ! date -d "$creation_date" >/dev/null 2>&1; then + echo "Invalid date. Please provide a valid date in YYYY-MM-DD format." + exit 1 + fi + fi + + # Validate the username + if ! [[ "$username" =~ ^[a-z0-9]+$ ]]; then + echo -e "${red}Error:${NC} Username can only contain lowercase letters and numbers." + exit 1 + fi + + # Convert GB to bytes (1 GB = 1073741824 bytes) + traffic=$(echo "$traffic_gb * 1073741824" | bc) + + if [ ! -f "$USERS_FILE" ]; then + echo "{}" > "$USERS_FILE" + fi + + jq --arg username "$username" --arg password "$password" --argjson traffic "$traffic" --argjson expiration_days "$expiration_days" --arg creation_date "$creation_date" \ + '.[$username] = {password: $password, max_download_bytes: $traffic, expiration_days: $expiration_days, account_creation_date: $creation_date, blocked: false}' \ + "$USERS_FILE" > "${USERS_FILE}.temp" && mv "${USERS_FILE}.temp" "$USERS_FILE" + + python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 + + echo -e "${green}User $username added successfully.${NC}" +} + +# Call the function with the provided arguments +add_user "$1" "$2" "$3" "$4" "$5" diff --git a/core/scripts/hysteria2/change_port.sh b/core/scripts/hysteria2/change_port.sh new file mode 100644 index 0000000..c5a8d30 --- /dev/null +++ b/core/scripts/hysteria2/change_port.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Source the path.sh script to load the CONFIG_FILE variable +source /etc/hysteria/core/scripts/path.sh + +# Function to update port number in configuration +update_port() { + local port=$1 + + # Validate the port number + if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + echo "Invalid port number. Please enter a number between 1 and 65535." + return 1 + fi + + # Check if the config file exists and update the port number + if [ -f "$CONFIG_FILE" ]; then + jq --arg port "$port" '.listen = ":" + $port' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE" + python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 + echo "Port changed successfully to $port." + else + echo "Error: Config file $CONFIG_FILE not found." + return 1 + fi +} + +update_port "$1" diff --git a/core/scripts/hysteria2/edit_user.sh b/core/scripts/hysteria2/edit_user.sh new file mode 100644 index 0000000..3fc4c4d --- /dev/null +++ b/core/scripts/hysteria2/edit_user.sh @@ -0,0 +1,177 @@ +#!/bin/bash + +source /etc/hysteria/core/scripts/utils.sh +source /etc/hysteria/core/scripts/path.sh + +# Function to validate all user input fields +validate_inputs() { + local new_username=$1 + local new_password=$2 + local new_traffic_limit=$3 + local new_expiration_days=$4 + local new_creation_date=$5 + local new_blocked=$6 + + # Validate username + if [ -n "$new_username" ]; then + if ! [[ "$new_username" =~ ^[a-z0-9]+$ ]]; then + echo -e "${red}Error:${NC} Username can only contain lowercase letters and numbers." + exit 1 + fi + fi + + # Validate traffic limit + if [[ ! "$new_traffic_limit" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then + if ! [[ "$new_traffic_limit" =~ ^[0-9]+$ ]]; then + echo -e "${red}Error:${NC} Traffic limit must be a valid integer." + exit 1 + fi + fi + + # Validate expiration days + if [ -n "$new_expiration_days" ]; then + if ! [[ "$new_expiration_days" =~ ^[0-9]+$ ]]; then + echo -e "${red}Error:${NC} Expiration days must be a valid integer." + exit 1 + fi + fi + + # Validate date format + if [ -n "$new_creation_date" ]; then + if ! [[ "$new_creation_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "Invalid date format. Expected YYYY-MM-DD." + exit 1 + elif ! date -d "$new_creation_date" >/dev/null 2>&1; then + echo "Invalid date. Please provide a valid date in YYYY-MM-DD format." + exit 1 + fi + fi + + # Validate blocked status + if [ -n "$new_blocked" ]; then + if [ "$new_blocked" != "true" ] && [ "$new_blocked" != "false" ]; then + echo -e "${red}Error:${NC} Blocked status must be 'true' or 'false'." + exit 1 + fi + fi +} + +# Function to get user info +get_user_info() { + local username=$1 + python3 $CLI_PATH get-user -u "$username" +} + +# Function to update user info in JSON +update_user_info() { + local old_username=$1 + local new_username=$2 + local new_password=$3 + local new_max_download_bytes=$4 + local new_expiration_days=$5 + local new_account_creation_date=$6 + local new_blocked=$7 + + if [ ! -f "$USERS_FILE" ]; then + echo "Error: File '$USERS_FILE' not found." + return 1 + fi + + + echo "checking if user exists" + # Check if the old username exists + user_exists=$(jq -e --arg username "$old_username" '.[$username]' "$USERS_FILE") + if [ $? -ne 0 ]; then + echo "Error: User '$old_username' not found." + return 1 + fi + echo "user exists done" + + echo "change key" + # If new_username is provided and different from old_username, rename the key + if [ -n "$new_username" ] && [ "$old_username" != "$new_username" ]; then + jq --arg old_username "$old_username" \ + --arg new_username "$new_username" \ + 'if .[$new_username] then error("User already exists with new username") else . end | + .[$new_username] = .[$old_username] | del(.[$old_username])' \ + "$USERS_FILE" > tmp.$$.json && mv tmp.$$.json "$USERS_FILE" + + if [ $? -ne 0 ]; then + echo "Error: Failed to rename user '$old_username' to '$new_username'." + return 1 + fi + fi + echo "change key done" + + echo "update user fields" + # print all new values + echo "Old username" "$old_username" + echo "New username: $new_username" + echo "New password: $new_password" + echo "New traffic limit: $new_max_download_bytes" + echo "New expiration days: $new_expiration_days" + echo "New creation date: $new_account_creation_date" + echo "New blocked status: $new_blocked" + + jq --arg username "$new_username" \ + --arg password "$new_password" \ + --argjson max_download_bytes "$new_max_download_bytes" \ + --argjson expiration_days "$new_expiration_days" \ + --arg account_creation_date "$new_account_creation_date" \ + --argjson blocked "$new_blocked" \ + '.[$username] |= (.password = $password | .max_download_bytes = $max_download_bytes | .expiration_days = $expiration_days | .account_creation_date = $account_creation_date | .blocked = $blocked)' \ + "$USERS_FILE" > tmp.$$.json && mv tmp.$$.json "$USERS_FILE" + + echo "update user fields done" + if [ $? -ne 0 ]; then + echo "Error: Failed to update user '$old_username'." + return 1 + fi + + echo "User '$old_username' updated successfully." +} + + +# Main function to edit user +edit_user() { + local username=$1 + local new_username=$2 + local new_traffic_limit=$3 + local new_expiration_days=$4 + local new_password=$5 + local new_creation_date=$6 + local new_blocked=$7 + + # Get user info + user_info=$(get_user_info "$username") + if [ $? -ne 0 ] || [ -z "$user_info" ]; then + echo -e "${red}Error:${NC} User '$username' not found." + exit 1 + fi + + # Extract user info + local password=$(echo "$user_info" | jq -r '.password') + local traffic_limit=$(echo "$user_info" | jq -r '.max_download_bytes') + local expiration_days=$(echo "$user_info" | jq -r '.expiration_days') + local creation_date=$(echo "$user_info" | jq -r '.account_creation_date') + local blocked=$(echo "$user_info" | jq -r '.blocked') + + # Validate all inputs + validate_inputs "$new_username" "$new_password" "$new_traffic_limit" "$new_expiration_days" "$new_creation_date" "$new_blocked" + + # Set new values with validation + new_username=${new_username:-$username} + new_password=${new_password:-$password} + new_traffic_limit=${new_traffic_limit:-$traffic_limit} + new_traffic_limit=$(echo "$new_traffic_limit * 1073741824" | bc) + new_expiration_days=${new_expiration_days:-$expiration_days} + new_creation_date=${new_creation_date:-$creation_date} + new_blocked=${new_blocked:-$blocked} + + + # Update user info in JSON file + update_user_info "$username" "$new_username" "$new_password" "$new_traffic_limit" "$new_expiration_days" "$new_creation_date" "$new_blocked" +} + +# Run the script +edit_user "$1" "$2" "$3" "$4" "$5" "$6" "$7" diff --git a/core/scripts/hysteria2/get_user.sh b/core/scripts/hysteria2/get_user.sh new file mode 100644 index 0000000..d67b4c1 --- /dev/null +++ b/core/scripts/hysteria2/get_user.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +source /etc/hysteria/core/scripts/path.sh + + +# Check if a username is provided +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +USERNAME=$1 + +# Check if users.json file exists +if [ ! -f "$USERS_FILE" ]; then + echo "users.json file not found!" + exit 1 +fi + +# Extract user info using jq +USER_INFO=$(jq -r --arg username "$USERNAME" '.[$username] // empty' $USERS_FILE) + +# Check if user info is found +if [ -z "$USER_INFO" ]; then + echo "User '$USERNAME' not found." + exit 1 +fi + +# Print user info +echo "$USER_INFO" | jq . + +exit 0 diff --git a/core/scripts/hysteria2/install.sh b/core/scripts/hysteria2/install.sh new file mode 100644 index 0000000..df8ee60 --- /dev/null +++ b/core/scripts/hysteria2/install.sh @@ -0,0 +1,140 @@ +#!/bin/bash + +# Source the path.sh script to load the necessary variables +source /etc/hysteria/core/scripts/path.sh +source /etc/hysteria/core/scripts/utils.sh +define_colors + +install_hysteria() { + local port=$1 + + # Step 1: Install Hysteria2 + echo "Installing Hysteria2..." + bash <(curl -fsSL https://get.hy2.sh/) >/dev/null 2>&1 + + # Step 2: Create hysteria directory and navigate into it + mkdir -p /etc/hysteria && cd /etc/hysteria/ + + # Step 3: Generate CA key and certificate and download geo data + echo "Generating CA key and certificate..." + 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=bts.com" >/dev/null 2>&1 + echo "Downloading geo data..." + wget -O /etc/hysteria/geosite.dat https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geosite.dat >/dev/null 2>&1 + wget -O /etc/hysteria/geoip.dat https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geoip.dat >/dev/null 2>&1 + + # Step 4: Extract the SHA-256 fingerprint + fingerprint=$(openssl x509 -noout -fingerprint -sha256 -inform pem -in ca.crt | sed 's/.*=//;s/://g') + + # Step 5: Generate the base64 encoded SHA-256 fingerprint + echo "Generating base64 encoded SHA-256 fingerprint..." + cat < generate.py +import base64 +import binascii + +# Hexadecimal string +hex_string = "$fingerprint" + +# Convert hex to binary +binary_data = binascii.unhexlify(hex_string) + +# Encode binary data to base64 +base64_encoded = base64.b64encode(binary_data).decode('utf-8') + +# Print the result prefixed with 'sha256/' +print('sha256/' + base64_encoded) +EOF + + # Execute the Python script and capture the output + sha256=$(python3 generate.py) + + # Step 7: Ask for the port number and validate input + if [[ $port =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )); then + # Check if the port is in use + if ss -tuln | grep -q ":$port\b"; then + echo -e "${red}Port $port is already in use. Please choose another port.${NC}" + exit 1 + fi + else + echo "Invalid port number. Please enter a number between 1 and 65535." + exit 1 + fi + + # Step 8: Generate required passwords and UUID + echo "Generating passwords and UUID..." + obfspassword=$(pwgen -s 32 1) + UUID=$(uuidgen) + + # Step 9: Adjust file permissions for Hysteria service + chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt + chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt + + # Create hysteria user without login permissions + if ! id -u hysteria &> /dev/null; then + useradd -r -s /usr/sbin/nologin hysteria + fi + + # Get the default network interface + networkdef=$(ip route | grep "^default" | awk '{print $5}') + + # Step 10: Customize the config.json file + echo "Customizing config.json..." + jq --arg port "$port" \ + --arg sha256 "$sha256" \ + --arg obfspassword "$obfspassword" \ + --arg UUID "$UUID" \ + --arg networkdef "$networkdef" \ + '.listen = ":\($port)" | + .tls.cert = "/etc/hysteria/ca.crt" | + .tls.key = "/etc/hysteria/ca.key" | + .tls.pinSHA256 = $sha256 | + .obfs.salamander.password = $obfspassword | + .trafficStats.secret = $UUID | + .outbounds[0].direct.bindDevice = $networkdef' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE" + + # Step 11: Modify the systemd service file to use config.json + echo "Updating hysteria-server.service to use config.json..." + sed -i 's|(config.yaml)||' /etc/systemd/system/hysteria-server.service + sed -i "s|/etc/hysteria/config.yaml|$CONFIG_FILE|" /etc/systemd/system/hysteria-server.service + rm /etc/hysteria/config.yaml + sleep 1 + + # Step 12: Start and enable the Hysteria service + echo "Starting and enabling Hysteria service..." + systemctl daemon-reload >/dev/null 2>&1 + systemctl start hysteria-server.service >/dev/null 2>&1 + systemctl enable hysteria-server.service >/dev/null 2>&1 + systemctl restart hysteria-server.service >/dev/null 2>&1 + + # Step 13: Check if the hysteria-server.service is active + if systemctl is-active --quiet hysteria-server.service; then + echo "${cyan}Hysteria2${green} has been successfully installed." + else + echo "${red}Error:${NC} hysteria-server.service is not active." + exit 1 + fi + + # Step 15: Give right permissions to scripts + chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh + chmod +x /etc/hysteria/core/scripts/hysteria2/kick.sh + + # Add the scripts to the crontab + (crontab -l ; echo "*/1 * * * * python3 /etc/hysteria/core/cli.py traffic-status >/dev/null 2>&1") | crontab - + (crontab -l ; echo "*/1 * * * * /etc/hysteria/core/scripts/hysteria2/kick.sh >/dev/null 2>&1") | crontab - +} + +if systemctl is-active --quiet hysteria-server.service; then + echo -e "${red}Error:${NC} Hysteria2 is already installed and running." + echo + echo "If you need to update the core, please use the 'Update Core' option." +else + echo "Installing and configuring Hysteria2..." + install_hysteria "$1" + echo -e "\n" + + if systemctl is-active --quiet hysteria-server.service; then + echo "Installation and configuration complete." + else + echo -e "${red}Error:${NC} Hysteria2 service is not active. Please check the logs for more details." + fi +fi diff --git a/kick.sh b/core/scripts/hysteria2/kick.sh similarity index 89% rename from kick.sh rename to core/scripts/hysteria2/kick.sh index da6c872..be6f3df 100644 --- a/kick.sh +++ b/core/scripts/hysteria2/kick.sh @@ -1,8 +1,6 @@ #!/bin/bash -USERS_FILE="/etc/hysteria/users/users.json" -TRAFFIC_FILE="/etc/hysteria/traffic_data.json" -CONFIG_FILE="/etc/hysteria/config.json" +source /etc/hysteria/core/scripts/path.sh kick_user() { local username=$1 diff --git a/core/scripts/hysteria2/list_users.sh b/core/scripts/hysteria2/list_users.sh new file mode 100644 index 0000000..ca76d18 --- /dev/null +++ b/core/scripts/hysteria2/list_users.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +source /etc/hysteria/core/scripts/path.sh + + +cat "$USERS_FILE" \ No newline at end of file diff --git a/core/scripts/hysteria2/remove_user.sh b/core/scripts/hysteria2/remove_user.sh new file mode 100644 index 0000000..1759032 --- /dev/null +++ b/core/scripts/hysteria2/remove_user.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Source the path.sh script to load the necessary variables +source /etc/hysteria/core/scripts/path.sh +source /etc/hysteria/core/scripts/utils.sh +define_colors + +# Function to remove a user from the configuration +remove_user() { + if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 + fi + + username=$1 + + if [ -f "$USERS_FILE" ]; then + # Check if the username exists in the users.json file + if jq -e "has(\"$username\")" "$USERS_FILE" > /dev/null; then + jq --arg username "$username" 'del(.[$username])' "$USERS_FILE" > "${USERS_FILE}.temp" && mv "${USERS_FILE}.temp" "$USERS_FILE" + + if [ -f "$TRAFFIC_FILE" ]; then + jq --arg username "$username" 'del(.[$username])' "$TRAFFIC_FILE" > "${TRAFFIC_FILE}.temp" && mv "${TRAFFIC_FILE}.temp" "$TRAFFIC_FILE" + fi + + python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 + echo "User $username removed successfully." + else + echo -e "${red}Error:${NC} User $username not found." + fi + else + echo -e "${red}Error:${NC} Config file $USERS_FILE not found." + fi +} + +# Call the function with the provided username argument +remove_user "$1" diff --git a/core/scripts/hysteria2/restart.sh b/core/scripts/hysteria2/restart.sh new file mode 100644 index 0000000..fa4be6c --- /dev/null +++ b/core/scripts/hysteria2/restart.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +python3 /etc/hysteria/core/cli.py traffic-status > /dev/null 2>&1 +systemctl restart hysteria-server.service \ No newline at end of file diff --git a/core/scripts/hysteria2/show_user_uri.sh b/core/scripts/hysteria2/show_user_uri.sh new file mode 100644 index 0000000..e6b4e0c --- /dev/null +++ b/core/scripts/hysteria2/show_user_uri.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Source the path.sh script to load the configuration variables +source /etc/hysteria/core/scripts/path.sh + +# Function to show URI if Hysteria2 is installed and active +show_uri() { + if [ -f "$USERS_FILE" ]; then + if systemctl is-active --quiet hysteria-server.service; then + # Check if the username is provided as an argument + if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 + fi + + username=$1 + + # Validate the username + if jq -e "has(\"$username\")" "$USERS_FILE" > /dev/null; then + # Get the selected user's details + authpassword=$(jq -r ".\"$username\".password" "$USERS_FILE") + port=$(jq -r '.listen' "$CONFIG_FILE" | cut -d':' -f2) + sha256=$(jq -r '.tls.pinSHA256' "$CONFIG_FILE") + obfspassword=$(jq -r '.obfs.salamander.password' "$CONFIG_FILE") + + # Get IP addresses + IP=$(curl -s -4 ip.gs) + IP6=$(curl -s -6 ip.gs) + + # Construct URI + URI="hy2://$username%3A$authpassword@$IP:$port?obfs=salamander&obfs-password=$obfspassword&pinSHA256=$sha256&insecure=1&sni=bts.com#$username-IPv4" + URI6="hy2://$username%3A$authpassword@[$IP6]:$port?obfs=salamander&obfs-password=$obfspassword&pinSHA256=$sha256&insecure=1&sni=bts.com#$username-IPv6" + + # Generate QR codes + qr1=$(echo -n "$URI" | qrencode -t UTF8 -s 3 -m 2) + qr2=$(echo -n "$URI6" | qrencode -t UTF8 -s 3 -m 2) + + # Display QR codes and URIs + cols=$(tput cols) + echo -e "\nIPv4:\n" + echo "$qr1" | while IFS= read -r line; do + printf "%*s\n" $(( (${#line} + cols) / 2)) "$line" + done + + echo -e "\nIPv6:\n" + echo "$qr2" | while IFS= read -r line; do + printf "%*s\n" $(( (${#line} + cols) / 2)) "$line" + done + + echo + echo "IPv4: $URI" + echo + echo "IPv6: $URI6" + echo + else + echo "Invalid username. Please try again." + fi + else + echo -e "\033[0;31mError:\033[0m Hysteria2 is not active." + fi + else + echo -e "\033[0;31mError:\033[0m Config file $USERS_FILE not found." + fi +} + +# Call the function with the provided username argument +show_uri "$1" diff --git a/core/scripts/hysteria2/uninstall.sh b/core/scripts/hysteria2/uninstall.sh new file mode 100644 index 0000000..f5617b6 --- /dev/null +++ b/core/scripts/hysteria2/uninstall.sh @@ -0,0 +1,28 @@ +source /etc/hysteria/core/scripts/path.sh + +echo "Uninstalling Hysteria2..." +#sleep 1 +echo "Running uninstallation script..." +bash <(curl -fsSL https://get.hy2.sh/) --remove >/dev/null 2>&1 +#sleep 1 +echo "Removing WARP" +python3 $CLI_PATH uninstall-warp +echo "Removing Hysteria folder..." +rm -rf /etc/hysteria >/dev/null 2>&1 +#sleep 1 +echo "Deleting hysteria user..." +userdel -r hysteria >/dev/null 2>&1 +#sleep 1 +echo "Removing systemd service files..." +rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server.service >/dev/null 2>&1 +rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server@*.service >/dev/null 2>&1 +#sleep 1 +echo "Reloading systemd daemon..." +systemctl daemon-reload >/dev/null 2>&1 +#sleep 1 +echo "Removing cron jobs..." +(crontab -l | grep -v "python3 /etc/hysteria/core/cli.py traffic-status" | crontab -) >/dev/null 2>&1 +(crontab -l | grep -v "/etc/hysteria/core/scripts/hysteria2/kick.sh" | crontab -) >/dev/null 2>&1 +#sleep 1 +echo "Hysteria2 uninstalled!" +echo "" \ No newline at end of file diff --git a/core/scripts/hysteria2/update.sh b/core/scripts/hysteria2/update.sh new file mode 100644 index 0000000..ec371bc --- /dev/null +++ b/core/scripts/hysteria2/update.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Source the path.sh script to load the CONFIG_FILE variable +source /etc/hysteria/core/scripts/path.sh + +echo "Starting the update process for Hysteria2..." +echo "Backing up the current configuration..." +cp "$CONFIG_FILE" /etc/hysteria/config_backup.json +if [ $? -ne 0 ]; then + echo "Error: Failed to back up configuration. Aborting update." + exit 1 +fi + +echo "Downloading and installing the latest version of Hysteria2..." +bash <(curl -fsSL https://get.hy2.sh/) >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "Error: Failed to download or install the latest version. Restoring backup configuration." + mv /etc/hysteria/config_backup.json "$CONFIG_FILE" + python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 + exit 1 +fi + +echo "Restoring configuration from backup..." +mv /etc/hysteria/config_backup.json "$CONFIG_FILE" +if [ $? -ne 0 ]; then + echo "Error: Failed to restore configuration from backup." + exit 1 +fi + +echo "Modifying systemd service to use config.json..." +sed -i "s|/etc/hysteria/config.yaml|$CONFIG_FILE|" /etc/systemd/system/hysteria-server.service +if [ $? -ne 0 ]; then + echo "Error: Failed to modify systemd service." + exit 1 +fi + +rm /etc/hysteria/config.yaml +systemctl daemon-reload >/dev/null 2>&1 +python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "Error: Failed to restart Hysteria2 service." + exit 1 +fi + +echo "Hysteria2 has been successfully updated." +echo "" +exit 0 diff --git a/user.sh b/core/scripts/hysteria2/user.sh similarity index 77% rename from user.sh rename to core/scripts/hysteria2/user.sh index 4e6f115..f5e5ad0 100644 --- a/user.sh +++ b/core/scripts/hysteria2/user.sh @@ -4,26 +4,30 @@ ADDR="$1" AUTH="$2" TX="$3" -USERS_FILE="/etc/hysteria/users/users.json" -TRAFFIC_FILE="/etc/hysteria/traffic_data.json" -CONFIG_FILE="/etc/hysteria/config.json" +# Source the path.sh script to load variables +source /etc/hysteria/core/scripts/path.sh +# Extract username and password from AUTH IFS=':' read -r USERNAME PASSWORD <<< "$AUTH" +# Retrieve stored user data STORED_PASSWORD=$(jq -r --arg user "$USERNAME" '.[$user].password' "$USERS_FILE") MAX_DOWNLOAD_BYTES=$(jq -r --arg user "$USERNAME" '.[$user].max_download_bytes' "$USERS_FILE") EXPIRATION_DAYS=$(jq -r --arg user "$USERNAME" '.[$user].expiration_days' "$USERS_FILE") ACCOUNT_CREATION_DATE=$(jq -r --arg user "$USERNAME" '.[$user].account_creation_date' "$USERS_FILE") BLOCKED=$(jq -r --arg user "$USERNAME" '.[$user].blocked' "$USERS_FILE") +# Check if the user is blocked if [ "$BLOCKED" == "true" ]; then exit 1 fi +# Check if the provided password matches the stored password if [ "$STORED_PASSWORD" != "$PASSWORD" ]; then exit 1 fi +# Check if the user's account has expired CURRENT_DATE=$(date +%s) EXPIRATION_DATE=$(date -d "$ACCOUNT_CREATION_DATE + $EXPIRATION_DAYS days" +%s) @@ -32,6 +36,7 @@ if [ "$CURRENT_DATE" -ge "$EXPIRATION_DATE" ]; then exit 1 fi +# Check if the user's download limit has been exceeded CURRENT_DOWNLOAD_BYTES=$(jq -r --arg user "$USERNAME" '.[$user].download_bytes' "$TRAFFIC_FILE") if [ "$CURRENT_DOWNLOAD_BYTES" -ge "$MAX_DOWNLOAD_BYTES" ]; then @@ -43,5 +48,6 @@ if [ "$CURRENT_DOWNLOAD_BYTES" -ge "$MAX_DOWNLOAD_BYTES" ]; then exit 1 fi +# If all checks pass, print the username and exit successfully echo "$USERNAME" -exit 0 \ No newline at end of file +exit 0 diff --git a/core/scripts/path.sh b/core/scripts/path.sh new file mode 100644 index 0000000..d44a4b6 --- /dev/null +++ b/core/scripts/path.sh @@ -0,0 +1,4 @@ +CLI_PATH="/etc/hysteria/core/cli.py" +USERS_FILE="/etc/hysteria/users.json" +TRAFFIC_FILE="/etc/hysteria/traffic_data.json" +CONFIG_FILE="/etc/hysteria/config.json" \ No newline at end of file diff --git a/core/scripts/tcp-brutal/install.sh b/core/scripts/tcp-brutal/install.sh new file mode 100644 index 0000000..356fd70 --- /dev/null +++ b/core/scripts/tcp-brutal/install.sh @@ -0,0 +1,6 @@ +#!/bin/bash +echo "Installing TCP Brutal..." +bash <(curl -fsSL https://tcp.hy2.sh/) +sleep 3 +clear +echo "TCP Brutal installation complete." \ No newline at end of file diff --git a/core/scripts/utils.sh b/core/scripts/utils.sh new file mode 100644 index 0000000..3e261d8 --- /dev/null +++ b/core/scripts/utils.sh @@ -0,0 +1,22 @@ + +# Function to define colors +define_colors() { + green='\033[0;32m' + cyan='\033[0;36m' + red='\033[0;31m' + yellow='\033[0;33m' + LPurple='\033[1;35m' + NC='\033[0m' # No Color +} + +# Function to get system information +get_system_info() { + OS=$(lsb_release -d | awk -F'\t' '{print $2}') + ARCH=$(uname -m) + # Fetching detailed IP information in JSON format + IP_API_DATA=$(curl -s https://ipapi.co/json/ -4) + ISP=$(echo "$IP_API_DATA" | jq -r '.org') + IP=$(echo "$IP_API_DATA" | jq -r '.ip') + CPU=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}') + RAM=$(free -m | awk 'NR==2{printf "%.2f%%", $3*100/$2 }') +} \ No newline at end of file diff --git a/core/scripts/warp/configure.sh b/core/scripts/warp/configure.sh new file mode 100644 index 0000000..0f3a5eb --- /dev/null +++ b/core/scripts/warp/configure.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Source the path.sh script to load the CONFIG_FILE and CLI_PATH variables +source /etc/hysteria/core/scripts/path.sh + +check_warp_service() { + if ! systemctl is-active --quiet wg-quick@wgcf.service; then + echo "WARP is not active. Please install WARP before configuring." + exit 1 + fi +} + +check_config_file() { + if [ ! -f "$CONFIG_FILE" ]; then + echo "Error: Config file $CONFIG_FILE not found." + exit 1 + fi +} + +get_status() { + warp_all_status=$(jq -r 'if .acl.inline | index("warps(all)") then "WARP active" else "Direct" end' "$CONFIG_FILE") + google_openai_status=$(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") + iran_status=$(jq -r 'if (.acl.inline | index("warps(geosite:ir)")) and (.acl.inline | index("warps(geoip:ir)")) then "Use WARP" else "Reject" end' "$CONFIG_FILE") + adult_content_status=$(jq -r 'if .acl.inline | index("reject(geosite:category-porn)") then "Blocked" else "Not blocked" end' "$CONFIG_FILE") +} + +display_menu() { + echo "===== Configuration Menu =====" + echo "1. Use WARP for all traffic ($warp_all_status)" + echo "2. Use WARP for Google, OpenAI, etc. ($google_openai_status)" + echo "3. Use WARP for geosite:ir and geoip:ir ($iran_status)" + echo "4. Block adult content ($adult_content_status)" + echo "5. Back to Advance Menu" + echo "===================================" +} + +update_config() { + local jq_command=$1 + jq "$jq_command" "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE" +} + +configure_warp_all() { + if [ "$warp_all_status" == "WARP active" ]; then + update_config 'del(.acl.inline[] | select(. == "warps(all)"))' + echo "Traffic configuration changed to Direct." + else + update_config '.acl.inline += ["warps(all)"]' + echo "Traffic configuration changed to WARP." + fi +} + +configure_google_openai() { + if [ "$google_openai_status" == "WARP active" ]; then + update_config '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)"))' + echo "WARP configuration for Google, OpenAI, etc. removed." + else + update_config '.acl.inline += ["warps(geoip:google)", "warps(geosite:google)", "warps(geosite:netflix)", "warps(geosite:spotify)", "warps(geosite:openai)", "warps(geoip:openai)"]' + echo "WARP configured for Google, OpenAI, etc." + fi +} + +configure_iran() { + if [ "$iran_status" == "Use WARP" ]; then + update_config '(.acl.inline[] | select(. == "warps(geosite:ir)")) = "reject(geosite:ir)" | (.acl.inline[] | select(. == "warps(geoip:ir)")) = "reject(geoip:ir)"' + echo "Configuration changed to Reject for geosite:ir and geoip:ir." + else + update_config '(.acl.inline[] | select(. == "reject(geosite:ir)")) = "warps(geosite:ir)" | (.acl.inline[] | select(. == "reject(geoip:ir)")) = "warps(geoip:ir)"' + echo "Configuration changed to Use WARP for geosite:ir and geoip:ir." + fi +} + +configure_adult_content() { + if [ "$adult_content_status" == "Blocked" ]; then + update_config 'del(.acl.inline[] | select(. == "reject(geosite:category-porn)"))' + update_config '.resolver.tls.addr = "1.1.1.1:853"' + echo "Adult content blocking removed and resolver updated." + else + update_config '.acl.inline += ["reject(geosite:category-porn)"]' + update_config '.resolver.tls.addr = "1.1.1.3:853"' + echo "Adult content blocked and resolver updated." + fi +} + +handle_choice() { + case $choice in + 1) configure_warp_all ;; + 2) configure_google_openai ;; + 3) configure_iran ;; + 4) configure_adult_content ;; + 5) exit 0 ;; + *) echo "Invalid option. Please try again." ;; + esac + python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 +} + +main() { + check_warp_service + check_config_file + get_status + display_menu + read -p "Enter your choice: " choice + handle_choice +} + +main diff --git a/core/scripts/warp/install.sh b/core/scripts/warp/install.sh new file mode 100644 index 0000000..1e66ad3 --- /dev/null +++ b/core/scripts/warp/install.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Source the path.sh script to load the CONFIG_FILE variable +source /etc/hysteria/core/scripts/path.sh + +# Check if wg-quick@wgcf.service is active +if systemctl is-active --quiet wg-quick@wgcf.service; then + echo "WARP is already active. Skipping installation and configuration update." +else + echo "Installing WARP..." + bash <(curl -fsSL git.io/warp.sh) wgx + + # Check if the config file exists + if [ -f "$CONFIG_FILE" ]; then + # Add the outbound configuration to the config.json file + 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" + # Restart the hysteria-server service + python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 + echo "WARP installed and outbound added to config.json." + else + echo "Error: Config file $CONFIG_FILE not found." + fi +fi diff --git a/core/scripts/warp/uninstall.sh b/core/scripts/warp/uninstall.sh new file mode 100644 index 0000000..c51aedf --- /dev/null +++ b/core/scripts/warp/uninstall.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Source the path.sh script to load the CONFIG_FILE and CLI_PATH variables +source /etc/hysteria/core/scripts/path.sh + +# Check if WARP is active +if systemctl is-active --quiet wg-quick@wgcf.service; then + echo "Uninstalling WARP..." + bash <(curl -fsSL git.io/warp.sh) dwg + + # Check if the config file exists + 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" + + 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 diff --git a/traffic.py b/core/traffic.py similarity index 83% rename from traffic.py rename to core/traffic.py index d874694..a623ba5 100644 --- a/traffic.py +++ b/core/traffic.py @@ -3,15 +3,21 @@ import subprocess import json import os +# Define static variables for paths and URLs +CONFIG_FILE = '/etc/hysteria/config.json' +TRAFFIC_FILE = '/etc/hysteria/traffic_data.json' +TRAFFIC_API_URL = 'http://127.0.0.1:25413/traffic?clear=1' +ONLINE_API_URL = 'http://127.0.0.1:25413/online' + def traffic_status(): green = '\033[0;32m' cyan = '\033[0;36m' NC = '\033[0m' try: - secret = subprocess.check_output(['jq', '-r', '.trafficStats.secret', '/etc/hysteria/config.json']).decode().strip() + secret = subprocess.check_output(['jq', '-r', '.trafficStats.secret', CONFIG_FILE]).decode().strip() except subprocess.CalledProcessError as e: - print(f"Error: Failed to read secret from config.json. Details: {e}") + print(f"Error: Failed to read secret from {CONFIG_FILE}. Details: {e}") return if not secret: @@ -19,7 +25,7 @@ def traffic_status(): return try: - response = subprocess.check_output(['curl', '-s', '-H', f'Authorization: {secret}', 'http://127.0.0.1:25413/traffic?clear=1']).decode().strip() + response = subprocess.check_output(['curl', '-s', '-H', f'Authorization: {secret}', TRAFFIC_API_URL]).decode().strip() except subprocess.CalledProcessError as e: print(f"Error: Failed to fetch traffic data. Details: {e}") return @@ -29,7 +35,7 @@ def traffic_status(): return try: - online_response = subprocess.check_output(['curl', '-s', '-H', f'Authorization: {secret}', 'http://127.0.0.1:25413/online']).decode().strip() + online_response = subprocess.check_output(['curl', '-s', '-H', f'Authorization: {secret}', ONLINE_API_URL]).decode().strip() except subprocess.CalledProcessError as e: print(f"Error: Failed to fetch online status data. Details: {e}") return @@ -55,9 +61,9 @@ def traffic_status(): } existing_data = {} - if os.path.exists('/etc/hysteria/traffic_data.json'): + if os.path.exists(TRAFFIC_FILE): try: - with open('/etc/hysteria/traffic_data.json', 'r') as json_file: + with open(TRAFFIC_FILE, 'r') as json_file: existing_data = json.load(json_file) except json.JSONDecodeError: print("Error: Failed to parse existing traffic data JSON file.") @@ -71,7 +77,7 @@ def traffic_status(): else: existing_data[user] = data - with open('/etc/hysteria/traffic_data.json', 'w') as json_file: + with open(TRAFFIC_FILE, 'w') as json_file: json.dump(existing_data, json_file, indent=4) display_traffic_data(existing_data, green, cyan, NC) @@ -108,5 +114,3 @@ def format_bytes(bytes): return f"{bytes / 1073741824:.2f}GB" else: return f"{bytes / 1099511627776:.2f}TB" - -traffic_status() diff --git a/core/validator.py b/core/validator.py new file mode 100644 index 0000000..04c09a2 --- /dev/null +++ b/core/validator.py @@ -0,0 +1,10 @@ +import os +import click + +def validate_port(ctx,param,value:int) -> int: + if value < 1 or value > 65535: + raise click.BadParameter('Port must be between 1 and 65535') + # check if port is in use + if os.system(f'lsof -i:{value}') == 0: + raise click.BadParameter(f'Port {value} is in use') + return value \ No newline at end of file diff --git a/install.sh b/install.sh index 6b5f289..e213f87 100644 --- a/install.sh +++ b/install.sh @@ -1,126 +1,28 @@ -#!/bin/bash -define_colors() { - green='\033[0;32m' - cyan='\033[0;36m' - red='\033[0;31m' - yellow='\033[0;33m' - LPurple='\033[1;35m' - NC='\033[0m' -} -# Step 1: Install Hysteria2 -echo "Installing Hysteria2..." -bash <(curl -fsSL https://get.hy2.sh/) >/dev/null 2>&1 - -# Step 2: Create hysteria directory and navigate into it -mkdir -p /etc/hysteria && cd /etc/hysteria/ - -# Step 3: Generate CA key and certificate and download geo data -echo "Generating CA key and certificate..." -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=bts.com" >/dev/null 2>&1 -echo "Downloading geo data..." -wget -O /etc/hysteria/geosite.dat https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geosite.dat >/dev/null 2>&1 -wget -O /etc/hysteria/geoip.dat https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geoip.dat >/dev/null 2>&1 - -# Step 4: Extract the SHA-256 fingerprint -fingerprint=$(openssl x509 -noout -fingerprint -sha256 -inform pem -in ca.crt | sed 's/.*=//;s/://g') - -# Step 5: Generate the base64 encoded SHA-256 fingerprint -echo "Generating base64 encoded SHA-256 fingerprint..." -echo "import re, base64, binascii - -hex_string = \"$fingerprint\" -binary_data = binascii.unhexlify(hex_string) -base64_encoded = base64.b64encode(binary_data).decode('utf-8') - -print(\"sha256/\" + base64_encoded)" > generate.py - -sha256=$(python3 generate.py) - -# Step 6: Download the config.json file -echo "Downloading config.json..." -wget https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/config.json -O /etc/hysteria/config.json >/dev/null 2>&1 -echo - -# Step 7: Ask for the port number and validate input -while true; do - read -p "Enter the port number you want to use (1-65535): " port - if [[ $port =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )); then - # Check if the port is in use - if ss -tuln | grep -q ":$port\b"; then - clear - echo -e "\e[91mPort $port is already in use. Please choose another port.\e[0m" - echo - else - break - fi - else - echo "Invalid port number. Please enter a number between 1 and 65535." - fi -done - -# Step 8: Generate required passwords and UUID -echo "Generating passwords and UUID..." -obfspassword=$(pwgen -s 32 1) -UUID=$(uuidgen) - -# Step 9: Adjust file permissions for Hysteria service -chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt -chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt - -# Create hysteria user without login permissions -if ! id -u hysteria &> /dev/null; then - useradd -r -s /usr/sbin/nologin hysteria +# Ensure necessary packages are installed +# Check if the script is being run by the root user +if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root." + exit 1 fi -# Get the default network interface -networkdef=$(ip route | grep "^default" | awk '{print $5}') - -# Step 10: Customize the config.json file -echo "Customizing config.json..." -jq --arg port "$port" \ - --arg sha256 "$sha256" \ - --arg obfspassword "$obfspassword" \ - --arg UUID "$UUID" \ - --arg networkdef "$networkdef" \ - '.listen = ":\($port)" | - .tls.cert = "/etc/hysteria/ca.crt" | - .tls.key = "/etc/hysteria/ca.key" | - .tls.pinSHA256 = $sha256 | - .obfs.salamander.password = $obfspassword | - .trafficStats.secret = $UUID | - .outbounds[0].direct.bindDevice = $networkdef' /etc/hysteria/config.json > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json /etc/hysteria/config.json - -# Step 11: Modify the systemd service file to use config.json -echo "Updating hysteria-server.service to use config.json..." -sed -i 's|(config.yaml)||' /etc/systemd/system/hysteria-server.service -sed -i 's|/etc/hysteria/config.yaml|/etc/hysteria/config.json|' /etc/systemd/system/hysteria-server.service -rm /etc/hysteria/config.yaml -sleep 1 - -# Step 12: Start and enable the Hysteria service -echo "Starting and enabling Hysteria service..." -systemctl daemon-reload >/dev/null 2>&1 -systemctl start hysteria-server.service >/dev/null 2>&1 -systemctl enable hysteria-server.service >/dev/null 2>&1 -systemctl restart hysteria-server.service >/dev/null 2>&1 - -# Step 13: Check if the hysteria-server.service is active -if systemctl is-active --quiet hysteria-server.service; then - echo "${cyan}Hysteria2${green} has been successfully install." -else - echo "${red}Error:${NC} hysteria-server.service is not active." +clear +if ! command -v jq &> /dev/null || ! command -v git &> /dev/null || ! command -v qrencode &> /dev/null || ! command -v curl &> /dev/null; then + echo "${yellow}Necessary packages are not installed. Please wait while they are being installed..." + sleep 3 + echo + apt update && apt upgrade -y && apt install jq qrencode curl pwgen uuid-runtime python3 python3-pip git -y fi -# Step 15: wget Traffic/user/kick script -wget https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/traffic.py -O /etc/hysteria/traffic.py >/dev/null 2>&1 -mkdir -p /etc/hysteria/users -wget https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/user.sh -O /etc/hysteria/users/user.sh >/dev/null 2>&1 -wget https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/kick.sh -O /etc/hysteria/users/kick.sh >/dev/null 2>&1 -wget https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/modify.py -O /etc/hysteria/users/modify.py >/dev/null 2>&1 -chmod +x /etc/hysteria/users/user.sh -chmod +x /etc/hysteria/users/kick.sh -# Add the commands to the crontab -(crontab -l ; echo "*/1 * * * * python3 /etc/hysteria/traffic.py >/dev/null 2>&1") | crontab - -(crontab -l ; echo "*/1 * * * * /etc/hysteria/users/kick.sh >/dev/null 2>&1") | crontab - \ No newline at end of file +# TODO: change the url later +git clone https://github.com/Iam54r1n4/Hysteria2 /etc/hysteria + +# Add alias 'hys2' for Hysteria2 +if ! grep -q "alias hys2='/etc/hysteria/menu.sh'" ~/.bashrc; then + echo "alias hys2='/etc/hysteria/menu.sh'" >> ~/.bashrc + source ~/.bashrc +fi + +cd /etc/hysteria +chmod +x menu.sh +./menu.sh diff --git a/menu.sh b/menu.sh index 387ffcb..6bee8bf 100644 --- a/menu.sh +++ b/menu.sh @@ -1,109 +1,10 @@ #!/bin/bash -# Function to define colors -define_colors() { - green='\033[0;32m' - cyan='\033[0;36m' - red='\033[0;31m' - yellow='\033[0;33m' - LPurple='\033[1;35m' - NC='\033[0m' # No Color -} +source /etc/hysteria/core/scripts/utils.sh +source /etc/hysteria/core/scripts/path.sh -# Ensure necessary packages are installed -clear -if ! command -v jq &> /dev/null || ! command -v qrencode &> /dev/null || ! command -v curl &> /dev/null; then - echo "${yellow}Necessary packages are not installed. Please wait while they are being installed..." - sleep 3 - echo - apt update && apt upgrade -y && apt install jq qrencode curl pwgen uuid-runtime python3 python3-pip -y -fi - -# Add alias 'hys2' for Hysteria2 -if ! grep -q "alias hys2='bash <(curl https://raw.githubusercontent.com/H-Return/Hysteria2/main/menu.sh)'" ~/.bashrc; then - echo "alias hys2='bash <(curl https://raw.githubusercontent.com/H-Return/Hysteria2/main/menu.sh)'" >> ~/.bashrc - source ~/.bashrc -fi - -# Function to get system information -get_system_info() { - OS=$(lsb_release -d | awk -F'\t' '{print $2}') - ARCH=$(uname -m) - # Fetching detailed IP information in JSON format - IP_API_DATA=$(curl -s https://ipapi.co/json/ -4) - ISP=$(echo "$IP_API_DATA" | jq -r '.org') - IP=$(echo "$IP_API_DATA" | jq -r '.ip') - CPU=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}') - RAM=$(free -m | awk 'NR==2{printf "%.2f%%", $3*100/$2 }') -} - -# Function to install and configure Hysteria2 -install_and_configure() { - if systemctl is-active --quiet hysteria-server.service; then - echo -e "${red}Error:${NC} Hysteria2 is already installed and running." - echo - echo "If you need to update the core, please use the 'Update Core' option." - else - echo "Installing and configuring Hysteria2..." - bash <(curl -s https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/install.sh) - echo -e "\n" - - if systemctl is-active --quiet hysteria-server.service; then - echo "Installation and configuration complete." - else - echo -e "${red}Error:${NC} Hysteria2 service is not active. Please check the logs for more details." - fi - fi -} - -# Function to update Hysteria2 -update_core() { - echo "Starting the update process for Hysteria2..." - echo "Backing up the current configuration..." - cp /etc/hysteria/config.json /etc/hysteria/config_backup.json - if [ $? -ne 0 ]; then - echo "${red}Error:${NC} Failed to back up configuration. Aborting update." - return 1 - fi - - echo "Downloading and installing the latest version of Hysteria2..." - bash <(curl -fsSL https://get.hy2.sh/) >/dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "${red}Error:${NC} Failed to download or install the latest version. Restoring backup configuration." - mv /etc/hysteria/config_backup.json /etc/hysteria/config.json - restart_hysteria_service >/dev/null 2>&1 - return 1 - fi - - echo "Restoring configuration from backup..." - mv /etc/hysteria/config_backup.json /etc/hysteria/config.json - if [ $? -ne 0 ]; then - echo "${red}Error:${NC} Failed to restore configuration from backup." - return 1 - fi - - echo "Modifying systemd service to use config.json..." - sed -i 's|/etc/hysteria/config.yaml|/etc/hysteria/config.json|' /etc/systemd/system/hysteria-server.service - if [ $? -ne 0 ]; then - echo "${red}Error:${NC} Failed to modify systemd service." - return 1 - fi - - rm /etc/hysteria/config.yaml - systemctl daemon-reload >/dev/null 2>&1 - restart_hysteria_service >/dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "${red}Error:${NC} Failed to restart Hysteria2 service." - return 1 - fi - - echo "Hysteria2 has been successfully updated." - echo "" - return 0 -} - -# Function to change port -change_port() { +# OPTION HANDLERS (ONLY NEEDED ONE) +hysteria2_install_handler() { while true; do read -p "Enter the new port number you want to use: " port if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then @@ -112,385 +13,200 @@ change_port() { break fi done - - if [ -f "/etc/hysteria/config.json" ]; then - jq --arg port "$port" '.listen = ":" + $port' /etc/hysteria/config.json > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json /etc/hysteria/config.json - restart_hysteria_service >/dev/null 2>&1 - echo "Port changed successfully to $port." - else - echo "${red}Error:${NC} Config file /etc/hysteria/config.json not found." - fi + python3 $CLI_PATH install-hysteria2 --port "$port" } -# Function to show URI if Hysteria2 is installed and active -show_uri() { - if [ -f "/etc/hysteria/users/users.json" ]; then - if systemctl is-active --quiet hysteria-server.service; then - # Get the list of configured usernames - usernames=$(jq -r 'keys_unsorted[]' /etc/hysteria/users/users.json) - - # Prompt the user to select a username - PS3="Select a username: " - select username in $usernames; do - if [ -n "$username" ]; then - # Get the selected user's details - authpassword=$(jq -r ".\"$username\".password" /etc/hysteria/users/users.json) - port=$(jq -r '.listen' /etc/hysteria/config.json | cut -d':' -f2) - sha256=$(jq -r '.tls.pinSHA256' /etc/hysteria/config.json) - obfspassword=$(jq -r '.obfs.salamander.password' /etc/hysteria/config.json) - - # Get IP addresses - IP=$(curl -s -4 ip.gs) - IP6=$(curl -s -6 ip.gs) - - # Construct URI - URI="hy2://$username%3A$authpassword@$IP:$port?obfs=salamander&obfs-password=$obfspassword&pinSHA256=$sha256&insecure=1&sni=bts.com#$username-IPv4" - URI6="hy2://$username%3A$authpassword@[$IP6]:$port?obfs=salamander&obfs-password=$obfspassword&pinSHA256=$sha256&insecure=1&sni=bts.com#$username-IPv6" - - # Generate QR codes - qr1=$(echo -n "$URI" | qrencode -t UTF8 -s 3 -m 2) - qr2=$(echo -n "$URI6" | qrencode -t UTF8 -s 3 -m 2) - - # Display QR codes and URIs - cols=$(tput cols) - echo -e "\nIPv4:\n" - echo "$qr1" | while IFS= read -r line; do - printf "%*s\n" $(( (${#line} + cols) / 2)) "$line" - done - - echo -e "\nIPv6:\n" - echo "$qr2" | while IFS= read -r line; do - printf "%*s\n" $(( (${#line} + cols) / 2)) "$line" - done - - echo - echo "IPv4: $URI" - echo - echo "IPv6: $URI6" - echo - break - else - echo "Invalid selection. Please try again." - fi - done - else - echo -e "\033[0;31mError:\033[0m Hysteria2 is not active." - fi - else - echo -e "\033[0;31mError:\033[0m Config file /etc/hysteria/users/users.json not found." - fi -} - -# Function to check traffic status for each user -traffic_status() { - if [ -f "/etc/hysteria/traffic.py" ]; then - python3 /etc/hysteria/traffic.py >/dev/null 2>&1 - else - echo "Error: /etc/hysteria/traffic.py not found." - return 1 - fi - - if [ ! -f "/etc/hysteria/traffic_data.json" ]; then - echo "Error: /etc/hysteria/traffic_data.json not found." - return 1 - fi - - data=$(cat /etc/hysteria/traffic_data.json) - echo "Traffic Data:" - echo "---------------------------------------------------------------------------" - echo -e "User Upload (TX) Download (RX) Status" - echo "---------------------------------------------------------------------------" - - echo "$data" | jq -r 'to_entries[] | [.key, .value.upload_bytes, .value.download_bytes, .value.status] | @tsv' | while IFS=$'\t' read -r user upload_bytes download_bytes status; do - if [ $(echo "$upload_bytes < 1073741824" | bc -l) -eq 1 ]; then - upload=$(echo "scale=2; $upload_bytes / 1024 / 1024" | bc) - upload_unit="MB" - else - upload=$(echo "scale=2; $upload_bytes / 1024 / 1024 / 1024" | bc) - upload_unit="GB" - fi - - if [ $(echo "$download_bytes < 1073741824" | bc -l) -eq 1 ]; then - download=$(echo "scale=2; $download_bytes / 1024 / 1024" | bc) - download_unit="MB" - else - download=$(echo "scale=2; $download_bytes / 1024 / 1024 / 1024" | bc) - download_unit="GB" - fi - - printf "${yellow}%-15s ${cyan}%-15s ${green}%-15s ${NC}%-10s\n" "$user" "$(printf "%.2f%s" "$upload" "$upload_unit")" "$(printf "%.2f%s" "$download" "$download_unit")" "$status" - echo "---------------------------------------------------------------------------" - done -} - - -# Function to restart Hysteria2 service -restart_hysteria_service() { - python3 /etc/hysteria/traffic.py >/dev/null 2>&1 - systemctl restart hysteria-server.service -} - -# Function to modify users -modify_users() { - modify_script="/etc/hysteria/users/modify.py" - github_raw_url="https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/modify.py" - - [ -f "$modify_script" ] || wget "$github_raw_url" -O "$modify_script" >/dev/null 2>&1 - - python3 "$modify_script" -} - -# Function to uninstall Hysteria2 -uninstall_hysteria() { - echo "Uninstalling Hysteria2..." - sleep 1 - echo "Running uninstallation script..." - bash <(curl -fsSL https://get.hy2.sh/) --remove >/dev/null 2>&1 - sleep 1 - echo "Removing Hysteria folder..." - rm -rf /etc/hysteria >/dev/null 2>&1 - sleep 1 - echo "Deleting hysteria user..." - userdel -r hysteria >/dev/null 2>&1 - sleep 1 - echo "Removing systemd service files..." - rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server.service >/dev/null 2>&1 - rm -f /etc/systemd/system/multi-user.target.wants/hysteria-server@*.service >/dev/null 2>&1 - sleep 1 - echo "Reloading systemd daemon..." - systemctl daemon-reload >/dev/null 2>&1 - sleep 1 - echo "Removing cron jobs..." - (crontab -l | grep -v "python3 /etc/hysteria/traffic.py" | crontab -) >/dev/null 2>&1 - (crontab -l | grep -v "/etc/hysteria/users/kick.sh" | crontab -) >/dev/null 2>&1 - sleep 1 - echo "Hysteria2 uninstalled!" - echo "" -} - -# Function to install TCP Brutal -install_tcp_brutal() { - echo "Installing TCP Brutal..." - bash <(curl -fsSL https://tcp.hy2.sh/) - sleep 3 - clear - echo "TCP Brutal installation complete." -} - -# Function to install WARP and update config.json -install_warp() { - # Check if wg-quick@wgcf.service is active - if systemctl is-active --quiet wg-quick@wgcf.service; then - echo "WARP is already active. Skipping installation and configuration update." - else - echo "Installing WARP..." - bash <(curl -fsSL git.io/warp.sh) wgx - - # Check if the config file exists - if [ -f "/etc/hysteria/config.json" ]; then - # Add the outbound configuration to the config.json file - jq '.outbounds += [{"name": "warps", "type": "direct", "direct": {"mode": 4, "bindDevice": "wgcf"}}]' /etc/hysteria/config.json > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json /etc/hysteria/config.json - # Restart the hysteria-server service - restart_hysteria_service >/dev/null 2>&1 - echo "WARP installed and outbound added to config.json." - else - echo "${red}Error:${NC} Config file /etc/hysteria/config.json not found." - fi - fi -} - -# Function to uninstall WARP and update config.json -uninstall_warp() { - if systemctl is-active --quiet wg-quick@wgcf.service; then - echo "Uninstalling WARP..." - bash <(curl -fsSL git.io/warp.sh) dwg - - if [ -f "/etc/hysteria/config.json" ]; 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")))) - ' /etc/hysteria/config.json > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json /etc/hysteria/config.json - jq 'del(.outbounds[] | select(.name == "warps" and .type == "direct" and .direct.mode == 4 and .direct.bindDevice == "wgcf"))' /etc/hysteria/config.json > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json /etc/hysteria/config.json - - restart_hysteria_service >/dev/null 2>&1 - echo "WARP uninstalled and configurations reset to default." - else - echo "${red}Error:${NC} Config file /etc/hysteria/config.json not found." - fi - else - echo "WARP is not active. Skipping uninstallation." - fi -} - -# Function to configure WARP -configure_warp() { - # Check if wg-quick@wgcf.service is active - if ! systemctl is-active --quiet wg-quick@wgcf.service; then - echo "WARP is not active. Please install WARP before configuring." - return - fi - - CONFIG_FILE="/etc/hysteria/config.json" - - if [ -f "$CONFIG_FILE" ]; then - # Check the current status of WARP configurations - warp_all_status=$(jq -r 'if .acl.inline | index("warps(all)") then "WARP active" else "Direct" end' "$CONFIG_FILE") - google_openai_status=$(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") - iran_status=$(jq -r 'if (.acl.inline | index("warps(geosite:ir)")) and (.acl.inline | index("warps(geoip:ir)")) then "Use WARP" else "Reject" end' "$CONFIG_FILE") - adult_content_status=$(jq -r 'if .acl.inline | index("reject(geosite:category-porn)") then "Blocked" else "Not blocked" end' "$CONFIG_FILE") - - echo "===== Configuration Menu =====" - echo "1. Use WARP for all traffic ($warp_all_status)" - echo "2. Use WARP for Google, OpenAI, etc. ($google_openai_status)" - echo "3. Use WARP for geosite:ir and geoip:ir ($iran_status)" - echo "4. Block adult content ($adult_content_status)" - echo "5. Back to Advance Menu" - echo "===================================" - - read -p "Enter your choice: " choice - case $choice in - 1) - if [ "$warp_all_status" == "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 - ;; - 2) - if [ "$google_openai_status" == "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 - ;; - 3) - if [ "$iran_status" == "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 - ;; - 4) - if [ "$adult_content_status" == "Blocked" ]; then - jq 'del(.acl.inline[] | select(. == "reject(geosite:category-porn)"))' "$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:category-porn)"]' "$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 - ;; - 5) - return - ;; - *) - echo "Invalid option. Please try again." - ;; - esac - restart_hysteria_service >/dev/null 2>&1 - else - echo "${red}Error:${NC} Config file $CONFIG_FILE not found." - fi -} -# Function to add a new user to the configuration -add_user() { +hysteria2_add_user_handler() { while true; do read -p "Enter the username: " username if [[ "$username" =~ ^[a-z0-9]+$ ]]; then break else - echo -e "\033[0;31mError:\033[0m Username can only contain lowercase letters and numbers." + echo -e "${red}Error:${NC} Username can only contain lowercase letters and numbers." fi done - read -p "Enter the traffic limit (in GB): " traffic_gb - # Convert GB to bytes (1 GB = 1073741824 bytes) - traffic=$((traffic_gb * 1073741824)) + read -p "Enter the traffic limit (in GB): " traffic_limit_GB read -p "Enter the expiration days: " expiration_days password=$(pwgen -s 32 1) creation_date=$(date +%Y-%m-%d) - if [ ! -f "/etc/hysteria/users/users.json" ]; then - echo "{}" > /etc/hysteria/users/users.json - fi - - jq --arg username "$username" --arg password "$password" --argjson traffic "$traffic" --argjson expiration_days "$expiration_days" --arg creation_date "$creation_date" \ - '.[$username] = {password: $password, max_download_bytes: $traffic, expiration_days: $expiration_days, account_creation_date: $creation_date, blocked: false}' \ - /etc/hysteria/users/users.json > /etc/hysteria/users/users_temp.json && mv /etc/hysteria/users/users_temp.json /etc/hysteria/users/users.json - - restart_hysteria_service >/dev/null 2>&1 - - echo -e "\033[0;32mUser $username added successfully.\033[0m" + python3 $CLI_PATH add-user --username "$username" --traffic-limit "$traffic_limit_GB" --expiration-days "$expiration_days" --password "$password" --creation-date "$creation_date" } +hysteria2_edit_user() { + # Function to prompt for user input with validation + prompt_for_input() { + local prompt_message="$1" + local validation_regex="$2" + local default_value="$3" + local input_variable_name="$4" -# Function to remove a user from the configuration -remove_user() { - if [ -f "/etc/hysteria/users/users.json" ]; then - # Extract current users from the users.json file - users=$(jq -r 'keys[]' /etc/hysteria/users/users.json) - - if [ -z "$users" ]; then - echo "No users found." - return - fi - - # Display current users with numbering - echo "Current users:" - echo "-----------------" - i=1 - for user in $users; do - echo "$i. $user" - ((i++)) + while true; do + read -p "$prompt_message" input + if [[ -z "$input" ]]; then + input="$default_value" + fi + if [[ "$input" =~ $validation_regex ]]; then + eval "$input_variable_name='$input'" + break + else + echo -e "${red}Error:${NC} Invalid input. Please try again." + fi done - echo "-----------------" + } - read -p "Enter the number of the user to remove: " selected_number + # Prompt for username + prompt_for_input "Enter the username you want to edit: " '^[a-z0-9]+$' '' username - if ! [[ "$selected_number" =~ ^[0-9]+$ ]]; then - echo "${red}Error:${NC} Invalid input. Please enter a number." - return + # Check if user exists + if ! python3 $CLI_PATH get-user --username "$username" > /dev/null 2>&1; then + echo -e "${red}Error:${NC} User '$username' not found." + return 1 + fi + + # Prompt for new username + prompt_for_input "Enter the new username (leave empty to keep the current username): " '^[a-z0-9]*$' '' new_username + + # Prompt for new traffic limit + prompt_for_input "Enter the new traffic limit (in GB) (leave empty to keep the current limit): " '^[0-9]*$' '' new_traffic_limit_GB + + # Prompt for new expiration days + prompt_for_input "Enter the new expiration days (leave empty to keep the current expiration days): " '^[0-9]*$' '' new_expiration_days + + # Prompt for renewing password + while true; do + read -p "Do you want to generate a new password? (y/n): " renew_password + case "$renew_password" in + y|Y) renew_password=true; break ;; + n|N) renew_password=false; break ;; + *) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;; + esac + done + + # Prompt for renewing creation date + while true; do + read -p "Do you want to generate a new creation date? (y/n): " renew_creation_date + case "$renew_creation_date" in + y|Y) renew_creation_date=true; break ;; + n|N) renew_creation_date=false; break ;; + *) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;; + esac + done + + # Prompt for blocking user + while true; do + read -p "Do you want to block the user? (y/n): " block_user + case "$block_user" in + y|Y) blocked=true; break ;; + n|N) blocked=false; break ;; + *) echo -e "${red}Error:${NC} Please answer 'y' or 'n'." ;; + esac + done + + # Call the edit-user script with appropriate flags + python3 $CLI_PATH edit-user \ + --username "$username" \ + ${new_username:+--new-username "$new_username"} \ + ${new_traffic_limit_GB:+--new-traffic-limit "$new_traffic_limit_GB"} \ + ${new_expiration_days:+--new-expiration-days "$new_expiration_days"} \ + ${renew_password:+--renew-password} \ + ${renew_creation_date:+--renew-creation-date} \ + ${blocked:+--blocked} +} + +hysteria2_remove_user_handler() { + while true; do + read -p "Enter the username: " username + + if [[ "$username" =~ ^[a-z0-9]+$ ]]; then + break + else + echo -e "${red}Error:${NC} Username can only contain lowercase letters and numbers." fi + done + python3 $CLI_PATH remove-user --username "$username" +} - if [ "$selected_number" -lt 1 ] || [ "$selected_number" -gt "$i" ]; then - echo "${red}Error:${NC} Invalid selection. Please enter a number within the range." - return +hysteria2_get_user_handler() { + while true; do + read -p "Enter the username: " username + if [[ "$username" =~ ^[a-z0-9]+$ ]]; then + break + else + echo -e "${red}Error:${NC} Username can only contain lowercase letters and numbers." fi + done - selected_user=$(echo "$users" | sed -n "${selected_number}p") - - jq --arg selected_user "$selected_user" 'del(.[$selected_user])' /etc/hysteria/users/users.json > /etc/hysteria/users/users_temp.json && mv /etc/hysteria/users/users_temp.json /etc/hysteria/users/users.json - - if [ -f "/etc/hysteria/traffic_data.json" ]; then - jq --arg selected_user "$selected_user" 'del(.[$selected_user])' /etc/hysteria/traffic_data.json > /etc/hysteria/traffic_data_temp.json && mv /etc/hysteria/traffic_data_temp.json /etc/hysteria/traffic_data.json - fi - - restart_hysteria_service >/dev/null 2>&1 - echo "User $selected_user removed successfully." - else - echo "${red}Error:${NC} Config file /etc/hysteria/traffic_data.json not found." + # Run the command and suppress error output + if ! python3 "$CLI_PATH" get-user --username "$username" > /dev/null 2>&1; then + echo -e "${red}Error:${NC} User '$username' not found." + return 1 fi } + +hysteria2_list_users_handler() { + users_json=$(python3 $CLI_PATH list-users 2>/dev/null) + if [ $? -ne 0 ] || [ -z "$users_json" ]; then + echo -e "${red}Error:${NC} Failed to list users." + return 1 + fi + + # Extract keys (usernames) from JSON + users_keys=$(echo "$users_json" | jq -r 'keys[]') + + if [ -z "$users_keys" ]; then + echo -e "${red}Error:${NC} No users found." + return 1 + fi + + # Print headers + printf "%-20s %-20s %-15s %-20s %-30s %-10s\n" "Username" "Traffic Limit (GB)" "Expiration (Days)" "Creation Date" "Password" "Blocked" + + # Print user details + for key in $users_keys; do + echo "$users_json" | jq -r --arg key "$key" ' + "\($key) \(.[$key].max_download_bytes / 1073741824) \(.[$key].expiration_days) \(.[$key].account_creation_date) \(.[$key].password) \(.[$key].blocked)"' | \ + while IFS= read -r line; do + IFS=' ' read -r username traffic_limit expiration_date creation_date password blocked <<< "$line" + printf "%-20s %-20s %-15s %-20s %-30s %-10s\n" "$username" "$traffic_limit" "$expiration_date" "$creation_date" "$password" "$blocked" + done + done +} + + +hysteria2_show_user_uri_handler() { + while true; do + read -p "Enter the username: " username + + if [[ "$username" =~ ^[a-z0-9]+$ ]]; then + break + else + echo -e "${red}Error:${NC} Username can only contain lowercase letters and numbers." + fi + done + python3 $CLI_PATH show-user-uri --username "$username" +} + +hysteria2_change_port_handler() { + while true; do + read -p "Enter the new port number you want to use: " port + if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then + echo "Invalid port number. Please enter a number between 1 and 65535." + else + break + fi + done + python3 $CLI_PATH change-hysteria2-port --port "$port" +} + +warp_configure_handler() { + # Placeholder function, add implementation here if needed + echo "empty" +} + # Function to display the main menu display_main_menu() { clear @@ -518,7 +234,6 @@ main_menu() { clear local choice while true; do - define_colors get_system_info display_main_menu read -r choice @@ -544,10 +259,12 @@ display_hysteria2_menu() { echo -e "${green}[1] ${NC}↝ Install and Configure Hysteria2" echo -e "${cyan}[2] ${NC}↝ Add User" - echo -e "${cyan}[3] ${NC}↝ Modify User" - echo -e "${cyan}[4] ${NC}↝ Show URI" - echo -e "${cyan}[5] ${NC}↝ Check Traffic Status" - echo -e "${cyan}[6] ${NC}↝ Remove User" + echo -e "${cyan}[3] ${NC}↝ Edit User" + echo -e "${cyan}[4] ${NC}↝ Remove User" + echo -e "${cyan}[5] ${NC}↝ Get User" + echo -e "${cyan}[6] ${NC}↝ List Users (WIP)" + echo -e "${cyan}[7] ${NC}↝ Check Traffic Status" + echo -e "${cyan}[8] ${NC}↝ Show User URI" echo -e "${red}[0] ${NC}↝ Back to Main Menu" @@ -561,41 +278,18 @@ hysteria2_menu() { clear local choice while true; do - define_colors get_system_info display_hysteria2_menu read -r choice case $choice in - 1) install_and_configure ;; - 2) add_user ;; - 3) modify_users ;; - 4) show_uri ;; - 5) traffic_status ;; - 6) remove_user ;; - 0) return ;; - *) echo "Invalid option. Please try again." ;; - esac - echo - read -rp "Press Enter to continue..." - done -} - -# Function to handle Advance menu options -advance_menu() { - clear - local choice - while true; do - define_colors - display_advance_menu - read -r choice - case $choice in - 1) install_tcp_brutal ;; - 2) install_warp ;; - 3) configure_warp ;; - 4) uninstall_warp ;; - 5) change_port ;; - 6) update_core ;; - 7) uninstall_hysteria ;; + 1) hysteria2_install_handler ;; + 2) hysteria2_add_user_handler ;; + 3) hysteria2_edit_user ;; + 4) hysteria2_remove_user_handler ;; + 5) hysteria2_get_user_handler ;; + 6) hysteria2_list_users_handler ;; + 7) python3 $CLI_PATH traffic-status ;; + 8) hysteria2_show_user_uri_handler ;; 0) return ;; *) echo "Invalid option. Please try again." ;; esac @@ -622,10 +316,34 @@ display_advance_menu() { echo -ne "${yellow}➜ Enter your option: ${NC}" } +# Function to handle Advance menu options +advance_menu() { + clear + local choice + while true; do + display_advance_menu + read -r choice + case $choice in + 1) python3 $CLI_PATH install-tcp-brutal ;; + 2) python3 $CLI_PATH install-warp ;; + 3) warp_configure_handler ;; + 4) python3 $CLI_PATH uninstall-warp ;; + 5) hysteria2_change_port_handler ;; + 6) python3 $CLI_PATH update-hysteria2 ;; + 7) python3 $CLI_PATH uninstall-hysteria2 ;; + 0) return ;; + *) echo "Invalid option. Please try again." ;; + esac + echo + read -rp "Press Enter to continue..." + done +} + # Main function to run the script main() { main_menu } +define_colors # Run the main function main