diff --git a/changelog b/changelog index ae2988a..0b75cf7 100644 --- a/changelog +++ b/changelog @@ -1,20 +1,25 @@ -# [1.9.2] - 2025-05-10 +# [1.9.3] - 2025-05-16 ## โœจ Changed -### โœจ Features -๐Ÿ”„ feat: Merge traffic collection with user kicking for efficient enforcement -๐Ÿงช feat(api): Add user existence validation to `add-user` endpoint -๐Ÿ–ฅ๏ธ feat(frontend): Improve user management UI and error feedback in `users.html` +### ๐Ÿ”ง System Improvements -### ๐Ÿง  Refactors -๐Ÿ“ฆ refactor: Migrate Hysteria restore functionality to Python -๐ŸŒ refactor: Implement SNI changer in Python -๐Ÿ› ๏ธ refactor: Port TCP Brutal installation to Python -๐ŸŽญ refactor: Rewrite masquerade management in Python3 -โš™๏ธ refactor(cli): Enhance error propagation and handling in CLI scripts +* ๐Ÿ•’ **feat:** Replace unreliable cron jobs with a systemd-based `HysteriaScheduler` service +* ๐Ÿ” **feat:** Add file locking to prevent concurrent access issues with `users.json` +* โฑ๏ธ **feat:** Schedule: -### ๐Ÿ› ๏ธ System Enhancements -๐Ÿงฉ update: Improve upgrade script with better cronjob management -๐Ÿ› ๏ธ enhance: Setup script with robust error handling and cleaner UX -๐Ÿ“ฆ update: Drop Debian 11 support to focus on supported OS versions \ No newline at end of file + * Traffic updates every 1 minute + * Backups every 6 hours with isolated lock management +* ๐Ÿ“ **feat:** Add detailed logging for easier troubleshooting and monitoring +* ๐Ÿ‘ค **feat:** Automatically add a default user after fresh installation + +### ๐Ÿ› ๏ธ Script Enhancements + +* ๐Ÿ“ฆ **refactor:** Create shared scheduler install function (used in both `install.sh` & `upgrade.sh`) +* โš™๏ธ **enhance:** Improve `upgrade.sh`: + + * Add service checks + * Backup handling + * Color UI + * Robust error handling +* ๐Ÿงผ **fix:** Improve uninstall script to clean up `HysteriaScheduler` service completely diff --git a/core/scripts/hysteria2/install.sh b/core/scripts/hysteria2/install.sh index 761ed4d..bea3751 100644 --- a/core/scripts/hysteria2/install.sh +++ b/core/scripts/hysteria2/install.sh @@ -2,6 +2,7 @@ source /etc/hysteria/core/scripts/path.sh source /etc/hysteria/core/scripts/utils.sh +source /etc/hysteria/core/scripts/scheduler.sh define_colors install_hysteria() { @@ -82,11 +83,9 @@ install_hysteria() { chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py - (crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status --no-gui'") | crontab - - # (crontab -l ; echo "0 3 */3 * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py restart-hysteria2' >/dev/null 2>&1") | crontab - - (crontab -l ; echo "0 */6 * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria' >/dev/null 2>&1") | crontab - - # (crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/scripts/hysteria2/kick.py' >/dev/null 2>&1") | crontab - - + if ! check_scheduler_service; then + setup_hysteria_scheduler + fi } if systemctl is-active --quiet hysteria-server.service; then @@ -100,6 +99,7 @@ else if systemctl is-active --quiet hysteria-server.service; then echo "Installation and configuration complete." + python3 $CLI_PATH add-user --username default --traffic-limit 30 --expiration-days 30 else echo -e "${red}Error:${NC} Hysteria2 service is not active. Please check the logs for more details." fi diff --git a/core/scripts/hysteria2/uninstall.py b/core/scripts/hysteria2/uninstall.py index 81f1e18..5ea2767 100644 --- a/core/scripts/hysteria2/uninstall.py +++ b/core/scripts/hysteria2/uninstall.py @@ -12,6 +12,7 @@ SERVICES = [ "hysteria-normal-sub.service", "hysteria-singbox.service", "hysteria-ip-limit.service", + "hysteria-scheduler.service", ] def run_command(command, error_message): diff --git a/core/scripts/scheduler.py b/core/scripts/scheduler.py new file mode 100644 index 0000000..a733a1c --- /dev/null +++ b/core/scripts/scheduler.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +import os +import sys +import time +import schedule +import logging +import subprocess +import fcntl +import datetime +from pathlib import Path +from paths import * + +logging.basicConfig( + level=logging.WARNING, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler("/var/log/hysteria_scheduler.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger("HysteriaScheduler") + +# Constants +BASE_DIR = Path("/etc/hysteria") +VENV_ACTIVATE = BASE_DIR / "hysteria2_venv/bin/activate" +# CLI_PATH = BASE_DIR / "core/cli.py" +LOCK_FILE = "/tmp/hysteria_scheduler.lock" + +def acquire_lock(): + try: + lock_fd = open(LOCK_FILE, 'w') + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + return lock_fd + except IOError: + logger.warning("Another process is already running and has the lock") + return None + +def release_lock(lock_fd): + if lock_fd: + fcntl.flock(lock_fd, fcntl.LOCK_UN) + lock_fd.close() + +def run_command(command, log_success=False): + + activate_cmd = f"source {VENV_ACTIVATE}" + full_cmd = f"{activate_cmd} && {command}" + + try: + result = subprocess.run( + full_cmd, + shell=True, + executable="/bin/bash", + capture_output=True, + text=True + ) + + if result.returncode != 0: + logger.error(f"Command failed: {full_cmd}") + logger.error(f"Error: {result.stderr}") + elif log_success: + logger.info(f"Command executed successfully: {full_cmd}") + + return result.returncode == 0 + except Exception as e: + logger.exception(f"Exception running command: {full_cmd}") + return False + +def check_traffic_status(): + lock_fd = acquire_lock() + if not lock_fd: + return + + try: + success = run_command(f"python3 {CLI_PATH} traffic-status --no-gui", log_success=False) + if not success: + pass + finally: + release_lock(lock_fd) + +def backup_hysteria(): + lock_fd = acquire_lock() + if not lock_fd: + logger.warning("Skipping backup due to lock") + return + + try: + run_command(f"python3 {CLI_PATH} backup-hysteria", log_success=True) + finally: + release_lock(lock_fd) + +def main(): + logger.info("Starting Hysteria Scheduler") + + schedule.every(1).minutes.do(check_traffic_status) + schedule.every(6).hours.do(backup_hysteria) + + backup_hysteria() + + while True: + try: + schedule.run_pending() + time.sleep(1) + except KeyboardInterrupt: + logger.info("Shutting down scheduler") + break + except Exception as e: + logger.exception("Error in main loop") + time.sleep(60) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/core/scripts/scheduler.sh b/core/scripts/scheduler.sh new file mode 100644 index 0000000..95dbc8a --- /dev/null +++ b/core/scripts/scheduler.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +setup_hysteria_scheduler() { + + chmod +x /etc/hysteria/core/scripts/scheduler.py + + cat > /etc/systemd/system/hysteria-scheduler.service << 'EOF' +[Unit] +Description=Hysteria2 Scheduler Service +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/etc/hysteria +ExecStart=/etc/hysteria/hysteria2_venv/bin/python3 /etc/hysteria/core/scripts/scheduler.py +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=hysteria-scheduler + +[Install] +WantedBy=multi-user.target +EOF + + systemctl daemon-reload + systemctl enable hysteria-scheduler.service + systemctl start hysteria-scheduler.service + # wait 2 + (crontab -l | grep -v "hysteria2_venv.*traffic-status" | grep -v "hysteria2_venv.*backup-hysteria") | crontab - + + # return 0 +} + +check_scheduler_service() { + if systemctl is-active --quiet hysteria-scheduler.service; then + return 0 + else + return 1 + fi +} diff --git a/core/scripts/services_status.sh b/core/scripts/services_status.sh index f9b0679..a5cdbee 100644 --- a/core/scripts/services_status.sh +++ b/core/scripts/services_status.sh @@ -2,6 +2,7 @@ declare -a services=( "hysteria-server.service" + "hysteria-scheduler.service" "hysteria-webpanel.service" "hysteria-caddy.service" "hysteria-telegram-bot.service" diff --git a/requirements.txt b/requirements.txt index 54ba0ed..411400c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ typing_extensions==4.13.2 urllib3==2.4.0 yarl==1.20.0 hysteria2-api==0.1.2 +schedule==1.2.2 # webpanel annotated-types==0.7.0 diff --git a/upgrade.sh b/upgrade.sh index 908838b..b96893a 100644 --- a/upgrade.sh +++ b/upgrade.sh @@ -1,156 +1,144 @@ #!/bin/bash -cd /root/ -TEMP_DIR=$(mktemp -d) +set -euo pipefail +trap 'echo -e "\nโŒ An error occurred. Aborting."; exit 1' ERR +# ========== Variables ========== +HYSTERIA_INSTALL_DIR="/etc/hysteria" +HYSTERIA_VENV_DIR="$HYSTERIA_INSTALL_DIR/hysteria2_venv" +REPO_URL="https://github.com/ReturnFI/Blitz" +REPO_BRANCH="main" +GEOSITE_URL="https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geosite.dat" +GEOIP_URL="https://raw.githubusercontent.com/Chocolate4U/Iran-v2ray-rules/release/geoip.dat" + +# ========== Color Setup ========== +GREEN=$(tput setaf 2) +RED=$(tput setaf 1) +YELLOW=$(tput setaf 3) +BLUE=$(tput setaf 4) +RESET=$(tput sgr0) + +info() { echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] - ${RESET} $1"; } +success() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [OK] - ${RESET} $1"; } +warn() { echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] - ${RESET} $1"; } +error() { echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] - ${RESET} $1"; } + +# ========== Backup Files ========== +cd /root +TEMP_DIR=$(mktemp -d) FILES=( - "/etc/hysteria/ca.key" - "/etc/hysteria/ca.crt" - "/etc/hysteria/users.json" - "/etc/hysteria/config.json" - "/etc/hysteria/.configs.env" - "/etc/hysteria/core/scripts/telegrambot/.env" - "/etc/hysteria/core/scripts/singbox/.env" - "/etc/hysteria/core/scripts/normalsub/.env" - "/etc/hysteria/core/scripts/webpanel/.env" - "/etc/hysteria/core/scripts/webpanel/Caddyfile" + "$HYSTERIA_INSTALL_DIR/ca.key" + "$HYSTERIA_INSTALL_DIR/ca.crt" + "$HYSTERIA_INSTALL_DIR/users.json" + "$HYSTERIA_INSTALL_DIR/config.json" + "$HYSTERIA_INSTALL_DIR/.configs.env" + "$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot/.env" + "$HYSTERIA_INSTALL_DIR/core/scripts/singbox/.env" + "$HYSTERIA_INSTALL_DIR/core/scripts/normalsub/.env" + "$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/.env" + "$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/Caddyfile" ) -if crontab -l 2>/dev/null | grep -q "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py" || \ - crontab -l 2>/dev/null | grep -q "/etc/hysteria/core/scripts/hysteria2/kick.sh"; then - - echo "Removing existing Hysteria cronjobs..." - crontab -l | grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status" | \ - grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py restart-hysteria2" | \ - grep -v "source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria" | \ - grep -v "/etc/hysteria/core/scripts/hysteria2/kick.sh" | \ - crontab - - echo "Old Hysteria cronjobs removed successfully." -else - echo "No existing Hysteria cronjobs found. Skipping removal." -fi - - -echo "Backing up files to $TEMP_DIR" +info "Backing up configuration files to: $TEMP_DIR" for FILE in "${FILES[@]}"; do - mkdir -p "$TEMP_DIR/$(dirname "$FILE")" - cp "$FILE" "$TEMP_DIR/$FILE" + if [[ -f "$FILE" ]]; then + mkdir -p "$TEMP_DIR/$(dirname "$FILE")" + cp -p "$FILE" "$TEMP_DIR/$FILE" + success "Backed up: $FILE" + else + warn "File not found: $FILE" + fi done -echo "Removing /etc/hysteria directory" -rm -rf /etc/hysteria/ +# ========== Replace Installation ========== +info "Removing old hysteria directory..." +rm -rf "$HYSTERIA_INSTALL_DIR" -echo "Cloning Blitz repository" -git clone https://github.com/ReturnFI/Blitz /etc/hysteria +info "Cloning Blitz repository (branch: $REPO_BRANCH)..." +git clone -q -b "$REPO_BRANCH" "$REPO_URL" "$HYSTERIA_INSTALL_DIR" -echo "Downloading geosite.dat and geoip.dat" -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 +# ========== Download Geo Data ========== +info "Downloading geosite.dat and geoip.dat..." +wget -q -O "$HYSTERIA_INSTALL_DIR/geosite.dat" "$GEOSITE_URL" +wget -q -O "$HYSTERIA_INSTALL_DIR/geoip.dat" "$GEOIP_URL" +success "Geo data downloaded." -echo "Restoring backup files" +# ========== Restore Backup ========== +info "Restoring configuration files..." for FILE in "${FILES[@]}"; do - cp "$TEMP_DIR/$FILE" "$FILE" + BACKUP="$TEMP_DIR/$FILE" + if [[ -f "$BACKUP" ]]; then + cp -p "$BACKUP" "$FILE" + success "Restored: $FILE" + else + warn "Missing backup file: $BACKUP" + fi done -CONFIG_ENV="/etc/hysteria/.configs.env" -if [ ! -f "$CONFIG_ENV" ]; then - echo ".configs.env not found, creating it with default values." - echo "SNI=bts.com" > "$CONFIG_ENV" -fi +# ========== Permissions ========== +info "Setting ownership and permissions..." +chown hysteria:hysteria "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt" +chmod 640 "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt" -export $(grep -v '^#' "$CONFIG_ENV" | xargs 2>/dev/null) +chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/singbox" +chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot" -if [[ -z "$IP4" ]]; then - echo "IP4 not found, fetching from ip.gs..." - IP4=$(curl -s -4 ip.gs || echo "") - echo "IP4=${IP4:-}" >> "$CONFIG_ENV" -fi +chmod +x "$HYSTERIA_INSTALL_DIR/core/scripts/hysteria2/user.sh" +chmod +x "$HYSTERIA_INSTALL_DIR/core/scripts/hysteria2/kick.py" -if [[ -z "$IP6" ]]; then - echo "IP6 not found, fetching from ip.gs..." - IP6=$(curl -s -6 ip.gs || echo "") - echo "IP6=${IP6:-}" >> "$CONFIG_ENV" -fi +# ========== Virtual Environment ========== +info "Setting up virtual environment and installing dependencies..." +cd "$HYSTERIA_INSTALL_DIR" +python3 -m venv "$HYSTERIA_VENV_DIR" +source "$HYSTERIA_VENV_DIR/bin/activate" +pip install --upgrade pip >/dev/null +pip install -r requirements.txt >/dev/null +success "Python environment ready." -NORMALSUB_ENV="/etc/hysteria/core/scripts/normalsub/.env" - -if [[ -f "$NORMALSUB_ENV" ]]; then - echo "Checking if SUBPATH exists in $NORMALSUB_ENV..." - - if ! grep -q '^SUBPATH=' "$NORMALSUB_ENV"; then - echo "SUBPATH not found, generating a new one..." - SUBPATH=$(pwgen -s 32 1) - echo -e "\nSUBPATH=$SUBPATH" >> "$NORMALSUB_ENV" +# ========== Scheduler ========== +info "Ensuring scheduler is set..." +if source "$HYSTERIA_INSTALL_DIR/core/scripts/scheduler.sh"; then + if ! check_scheduler_service; then + if setup_hysteria_scheduler; then + success "Scheduler service configured." + else + warn "Scheduler setup failed, but continuing upgrade..." + fi else - echo "SUBPATH already exists, no changes made." + success "Scheduler already set." fi else - echo "$NORMALSUB_ENV not found. Skipping SUBPATH check." + warn "Failed to source scheduler.sh, continuing without scheduler setup..." fi -CONFIG_FILE="/etc/hysteria/config.json" -if [ -f "$CONFIG_FILE" ]; then - echo "Checking and converting pinSHA256 format in config.json" - - if grep -q "pinSHA256.*=" "$CONFIG_FILE"; then - echo "Converting pinSHA256 from base64 to hex format" - - HEX_FINGERPRINT=$(openssl x509 -noout -fingerprint -sha256 -inform pem -in /etc/hysteria/ca.crt | sed 's/.*=//;s///g') - - sed -i "s|\"pinSHA256\": \"sha256/.*\"|\"pinSHA256\": \"$HEX_FINGERPRINT\"|" "$CONFIG_FILE" - - echo "pinSHA256 converted to hex format: $HEX_FINGERPRINT" +# ========== Restart Services ========== +SERVICES=( + hysteria-caddy.service + hysteria-server.service + hysteria-scheduler.service + hysteria-telegram-bot.service + hysteria-normal-sub.service + hysteria-webpanel.service + hysteria-ip-limit.service +) + +info "Restarting available services..." +for SERVICE in "${SERVICES[@]}"; do + if systemctl status "$SERVICE" &>/dev/null; then + systemctl restart "$SERVICE" && success "$SERVICE restarted." || warn "$SERVICE failed to restart." else - echo "pinSHA256 appears to already be in hex format or not present, no conversion needed" + warn "$SERVICE not found or not installed. Skipping..." fi -fi +done -echo "Setting ownership and permissions" -chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt -chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt -chown -R hysteria:hysteria /etc/hysteria/core/scripts/singbox -chown -R hysteria:hysteria /etc/hysteria/core/scripts/telegrambot - -echo "Setting execute permissions for user.sh and kick.py" -chmod +x /etc/hysteria/core/scripts/hysteria2/user.sh -chmod +x /etc/hysteria/core/scripts/hysteria2/kick.py - -cd /etc/hysteria -python3 -m venv hysteria2_venv -source /etc/hysteria/hysteria2_venv/bin/activate -pip install -r requirements.txt - -echo "Restarting hysteria-caddy service" -systemctl restart hysteria-caddy.service - -echo "Restarting other hysteria services" -systemctl restart hysteria-server.service -systemctl restart hysteria-telegram-bot.service -systemctl restart hysteria-normal-sub.service -systemctl restart hysteria-webpanel.service - - -echo "Checking hysteria-server.service status" +# ========== Final Check ========== if systemctl is-active --quiet hysteria-server.service; then - echo "Upgrade completed successfully" + success "๐ŸŽ‰ Upgrade completed successfully!" else - echo "Upgrade failed: hysteria-server.service is not active" -fi - -echo "Adding new Hysteria cronjobs..." -if ! crontab -l 2>/dev/null | grep -q "python3 /etc/hysteria/core/cli.py traffic-status --no-gui"; then - echo "Adding traffic-status cronjob..." - (crontab -l ; echo "*/1 * * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py traffic-status --no-gui'") | crontab - -else - echo "Traffic-status cronjob already exists. Skipping." -fi - -if ! crontab -l 2>/dev/null | grep -q "python3 /etc/hysteria/core/cli.py backup-hysteria"; then - echo "Adding backup-hysteria cronjob..." - (crontab -l ; echo "0 */6 * * * /bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && python3 /etc/hysteria/core/cli.py backup-hysteria' >/dev/null 2>&1") | crontab - -else - echo "Backup-hysteria cronjob already exists. Skipping." + warn "โš ๏ธ hysteria-server.service is not active. Check logs if needed." fi +# ========== Launch Menu ========== +sleep 10 chmod +x menu.sh ./menu.sh