diff --git a/core/cli_api.py b/core/cli_api.py index c415d4d..1d61c66 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -21,7 +21,7 @@ class Command(Enum): UPDATE_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'update.py') RESTART_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'restart.py') CHANGE_PORT_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'change_port.py') - CHANGE_SNI_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'change_sni.sh') + CHANGE_SNI_HYSTERIA2 = os.path.join(SCRIPT_DIR, 'hysteria2', 'change_sni.py') GET_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'get_user.py') ADD_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'add_user.py') EDIT_USER = os.path.join(SCRIPT_DIR, 'hysteria2', 'edit_user.sh') @@ -176,7 +176,7 @@ def change_hysteria2_sni(sni: str): ''' Changes the SNI for Hysteria2. ''' - run_cmd(['bash', Command.CHANGE_SNI_HYSTERIA2.value, sni]) + run_cmd(['python3', Command.CHANGE_SNI_HYSTERIA2.value, sni]) def backup_hysteria2(): diff --git a/core/scripts/hysteria2/change_sni.py b/core/scripts/hysteria2/change_sni.py new file mode 100644 index 0000000..5b929a4 --- /dev/null +++ b/core/scripts/hysteria2/change_sni.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import time +import subprocess +import socket +from pathlib import Path +from init_paths import * +from paths import * + +def run_command(command, capture_output=True, shell=True): + """Run a shell command and return its output""" + result = subprocess.run( + command, + shell=shell, + capture_output=capture_output, + text=True + ) + if capture_output: + return result.stdout.strip() + return None + +def get_ip_from_domain(domain): + """Get the first IPv4 address from a domain using dig""" + try: + output = run_command(f"dig +short {domain} A | head -n 1") + if output and is_valid_ipv4(output): + return output + except: + pass + return None + +def is_valid_ipv4(ip): + """Check if a string is a valid IPv4 address""" + try: + socket.inet_pton(socket.AF_INET, ip) + return True + except (socket.error, ValueError): + return False + +def get_server_ip(): + """Get the server's public IP address""" + return run_command("curl -s -4 ifconfig.me") + +def update_sni(sni): + if not sni: + print("Invalid SNI. Please provide a valid SNI.") + print(f"Example: {sys.argv[0]} yourdomain.com") + return 1 + + if os.path.isfile(CONFIG_ENV): + env_vars = {} + with open(CONFIG_ENV, 'r') as f: + for line in f: + if '=' in line: + name, value = line.strip().split('=', 1) + env_vars[name] = value + else: + print(f"Error: Config file {CONFIG_ENV} not found.") + return 1 + + server_ip = None + if 'IP4' in env_vars: + ip4 = env_vars['IP4'] + if is_valid_ipv4(ip4): + server_ip = ip4 + print(f"Using server IP from config: {server_ip}") + else: + domain_ip = get_ip_from_domain(ip4) + if domain_ip: + server_ip = domain_ip + print(f"Resolved domain {ip4} to IP: {server_ip}") + else: + server_ip = get_server_ip() + print(f"Could not resolve domain {ip4}. Using auto-detected server IP: {server_ip}") + else: + server_ip = get_server_ip() + print(f"Using auto-detected server IP: {server_ip}") + + print(f"Checking if {sni} points to this server ({server_ip})...") + domain_ip = get_ip_from_domain(sni) + + use_certbot = False + if not domain_ip: + print(f"Warning: Could not resolve {sni} to an IPv4 address.") + elif domain_ip == server_ip: + print(f"Success: {sni} correctly points to this server ({server_ip}).") + use_certbot = True + else: + print(f"Notice: {sni} points to {domain_ip}, not to this server ({server_ip}).") + + os.chdir('/etc/hysteria/') + + if use_certbot: + print(f"Using certbot to obtain a valid certificate for {sni}...") + + certbot_output = run_command(f"certbot certificates") + if sni in certbot_output: + print(f"Certificate for {sni} already exists. Renewing...") + run_command(f"certbot renew --cert-name {sni}", capture_output=False) + else: + print(f"Requesting new certificate for {sni}...") + run_command(f"certbot certonly --standalone -d {sni} --non-interactive --agree-tos --email admin@{sni}", + capture_output=False) + + run_command(f"cp /etc/letsencrypt/live/{sni}/fullchain.pem /etc/hysteria/ca.crt", capture_output=False) + run_command(f"cp /etc/letsencrypt/live/{sni}/privkey.pem /etc/hysteria/ca.key", capture_output=False) + + print("Certificates successfully installed from Let's Encrypt.") + + if os.path.isfile(CONFIG_FILE): + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + + config['tls']['insecure'] = False + + with open(CONFIG_FILE, 'w') as f: + json.dump(config, f, indent=2) + + print(f"TLS insecure flag set to false in {CONFIG_FILE}") + else: + print(f"Using self-signed certificate with openssl for {sni}...") + + if os.path.exists("ca.key"): + os.remove("ca.key") + if os.path.exists("ca.crt"): + os.remove("ca.crt") + + print(f"Generating CA key and certificate for SNI: {sni} ...") + run_command("openssl ecparam -genkey -name prime256v1 -out ca.key > /dev/null 2>&1", capture_output=False) + run_command(f"openssl req -new -x509 -days 36500 -key ca.key -out ca.crt -subj '/CN={sni}' > /dev/null 2>&1", + capture_output=False) + print(f"Self-signed certificate generated for {sni}") + + if os.path.isfile(CONFIG_FILE): + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + + config['tls']['insecure'] = True + + with open(CONFIG_FILE, 'w') as f: + json.dump(config, f, indent=2) + + print(f"TLS insecure flag set to true in {CONFIG_FILE}") + + run_command("chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt", capture_output=False) + run_command("chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt", capture_output=False) + + sha256 = run_command( + "openssl x509 -noout -fingerprint -sha256 -inform pem -in ca.crt | sed 's/.*=//;s///g'" + ) + print(f"SHA-256 fingerprint generated: {sha256}") + + if os.path.isfile(CONFIG_FILE): + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + + config['tls']['pinSHA256'] = sha256 + + with open(CONFIG_FILE, 'w') as f: + json.dump(config, f, indent=2) + + print(f"SHA-256 updated successfully in {CONFIG_FILE}") + else: + print(f"Error: Config file {CONFIG_FILE} not found.") + return 1 + + sni_found = False + if os.path.isfile(CONFIG_ENV): + with open(CONFIG_ENV, 'r') as f: + lines = f.readlines() + + with open(CONFIG_ENV, 'w') as f: + for line in lines: + if line.startswith('SNI='): + f.write(f'SNI={sni}\n') + sni_found = True + else: + f.write(line) + + if not sni_found: + f.write(f'SNI={sni}\n') + print(f"Added new SNI entry to {CONFIG_ENV}") + else: + print(f"SNI updated successfully in {CONFIG_ENV}") + else: + with open(CONFIG_ENV, 'w') as f: + f.write(f'SNI={sni}\n') + print(f"Created {CONFIG_ENV} with new SNI.") + + run_command(f"python3 {CLI_PATH} restart-hysteria2 > /dev/null 2>&1", capture_output=False) + print(f"Hysteria2 restarted successfully with new SNI: {sni}.") + + if use_certbot: + print(f"✅ Valid Let's Encrypt certificate installed for {sni}") + print(" TLS insecure mode is now DISABLED") + else: + print(f"⚠️ Self-signed certificate installed for {sni}") + print(" TLS insecure mode is now ENABLED") + print(" (This certificate won't be trusted by browsers)") + + return 0 + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + sni = sys.argv[1] + sys.exit(update_sni(sni)) \ No newline at end of file diff --git a/core/scripts/hysteria2/change_sni.sh b/core/scripts/hysteria2/change_sni.sh deleted file mode 100644 index 6f835d1..0000000 --- a/core/scripts/hysteria2/change_sni.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/bin/bash - -source /etc/hysteria/core/scripts/path.sh - -sni="$1" - -if [ -f "$CONFIG_ENV" ]; then - source "$CONFIG_ENV" -else - echo "Error: Config file $CONFIG_ENV not found." - exit 1 -fi - -update_sni() { - local sni=$1 - local server_ip - - if [ -z "$sni" ]; then - echo "Invalid SNI. Please provide a valid SNI." - echo "Example: $0 yourdomain.com" - return 1 - fi - - if [ -n "$IP4" ]; then - if [[ $IP4 =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - server_ip="$IP4" - echo "Using server IP from config: $server_ip" - else - domain_ip=$(dig +short "$IP4" A | head -n 1) - if [ -n "$domain_ip" ]; then - server_ip="$domain_ip" - echo "Resolved domain $IP4 to IP: $server_ip" - else - server_ip=$(curl -s -4 ifconfig.me) - echo "Could not resolve domain $IP4. Using auto-detected server IP: $server_ip" - fi - fi - else - server_ip=$(curl -s -4 ifconfig.me) - echo "Using auto-detected server IP: $server_ip" - fi - - echo "Checking if $sni points to this server ($server_ip)..." - domain_ip=$(dig +short "$sni" A | head -n 1) - - if [ -z "$domain_ip" ]; then - echo "Warning: Could not resolve $sni to an IPv4 address." - use_certbot=false - elif [ "$domain_ip" = "$server_ip" ]; then - echo "Success: $sni correctly points to this server ($server_ip)." - use_certbot=true - else - echo "Notice: $sni points to $domain_ip, not to this server ($server_ip)." - use_certbot=false - fi - - cd /etc/hysteria/ || exit - - if [ "$use_certbot" = true ]; then - echo "Using certbot to obtain a valid certificate for $sni..." - - if certbot certificates | grep -q "$sni"; then - echo "Certificate for $sni already exists. Renewing..." - certbot renew --cert-name "$sni" - else - echo "Requesting new certificate for $sni..." - certbot certonly --standalone -d "$sni" --non-interactive --agree-tos --email admin@"$sni" - fi - - cp /etc/letsencrypt/live/"$sni"/fullchain.pem /etc/hysteria/ca.crt - cp /etc/letsencrypt/live/"$sni"/privkey.pem /etc/hysteria/ca.key - - echo "Certificates successfully installed from Let's Encrypt." - - if [ -f "$CONFIG_FILE" ]; then - jq '.tls.insecure = false' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE" - echo "TLS insecure flag set to false in $CONFIG_FILE" - fi - else - echo "Using self-signed certificate with openssl for $sni..." - rm -f ca.key ca.crt - - echo "Generating CA key and certificate for SNI: $sni ..." - openssl ecparam -genkey -name prime256v1 -out ca.key >/dev/null 2>&1 - openssl req -new -x509 -days 36500 -key ca.key -out ca.crt -subj "/CN=$sni" >/dev/null 2>&1 - echo "Self-signed certificate generated for $sni" - - if [ -f "$CONFIG_FILE" ]; then - jq '.tls.insecure = true' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE" - echo "TLS insecure flag set to true in $CONFIG_FILE" - fi - fi - - chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt - chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt - - sha256=$(openssl x509 -noout -fingerprint -sha256 -inform pem -in ca.crt | sed 's/.*=//;s///g') - echo "SHA-256 fingerprint generated: $sha256" - - if [ -f "$CONFIG_FILE" ]; then - jq --arg sha256 "$sha256" '.tls.pinSHA256 = $sha256' "$CONFIG_FILE" > "${CONFIG_FILE}.temp" && mv "${CONFIG_FILE}.temp" "$CONFIG_FILE" - echo "SHA-256 updated successfully in $CONFIG_FILE" - else - echo "Error: Config file $CONFIG_FILE not found." - return 1 - fi - - if [ -f "$CONFIG_ENV" ]; then - if grep -q "^SNI=" "$CONFIG_ENV"; then - sed -i "s/^SNI=.*$/SNI=$sni/" "$CONFIG_ENV" - echo "SNI updated successfully in $CONFIG_ENV" - else - echo "SNI=$sni" >> "$CONFIG_ENV" - echo "Added new SNI entry to $CONFIG_ENV" - fi - else - echo "SNI=$sni" > "$CONFIG_ENV" - echo "Created $CONFIG_ENV with new SNI." - fi - - python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1 - echo "Hysteria2 restarted successfully with new SNI: $sni." - - if [ "$use_certbot" = true ]; then - echo "✅ Valid Let's Encrypt certificate installed for $sni" - echo " TLS insecure mode is now DISABLED" - else - echo "⚠️ Self-signed certificate installed for $sni" - echo " TLS insecure mode is now ENABLED" - echo " (This certificate won't be trusted by browsers)" - fi -} - -update_sni "$sni" \ No newline at end of file