diff --git a/README-fa.md b/README-fa.md index ca0bcfd..b9fee48 100644 --- a/README-fa.md +++ b/README-fa.md @@ -43,6 +43,18 @@ --- +## 💎 حامی مالی +
+ +[![Petrosky Hosting](https://img.shields.io/badge/Recommended_Host-Petrosky-blue?logo=server&logoColor=white)](https://client.petrosky.io/aff.php?aff=344) + +[**هاستینگی برای تمام مسیر شما!**](https://client.petrosky.io/aff.php?aff=344) 👉 + +*سرورهای با کیفیت بهینه‌سازی شده برای Hysteria2 و اپلیکیشن‌های پروکسی* +
+ + + ## 📋 راهنمای شروع سریع ### نصب با یک کلیک diff --git a/README.md b/README.md index 83ed369..82e50f4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,17 @@ A powerful and user-friendly management panel for Hysteria2 proxy server. Featur - ♻️ Hysteria2 Core Management (Restart, Update, Uninstall) - ✏️ IP Address Management (IPv4 and IPv6) +## 💎 Sponsorship +
+ +[![Petrosky Hosting](https://img.shields.io/badge/Recommended_Host-Petrosky-blue?logo=server&logoColor=white)](https://client.petrosky.io/aff.php?aff=344) + +👉 [**A hosting for your entire journey!**](https://client.petrosky.io/aff.php?aff=344) + +*Quality servers optimized for Hysteria2 and proxy applications* + +
## 📋 Quick Start Guide diff --git a/changelog b/changelog index 6404a17..4145713 100644 --- a/changelog +++ b/changelog @@ -1,30 +1,23 @@ -# [1.9.3] - 2025-05-16 +# [1.10.0] - 2025-05-18 -## ✨ Changed +## ✨ New Features -### 🔧 System Improvements +### ⚙️ NormalSub Configuration Enhancements -* 🕒 **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: - * Traffic updates every 1 minute - * Backups every 6 hours with isolated lock management -* 📝 **feat:** Add detailed logging for easier troubleshooting and monitoring +* 🛠️ **feat:** Add NormalSub subpath editing via Settings UI + - New 'Configure' tab in the Settings panel (visible only if NormalSub is active) + - Real-time client-side validation and live subpath editing -### 🛠️ Script Enhancements +* 🔌 **feat:** Add API endpoints + - `GET /api/v1/config/normalsub/subpath`: Fetch current subpath + - `PUT /api/v1/config/normalsub/edit_subpath`: Update the subpath securely -* 📦 **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 -* 👤 **feat:** Add a default user after installation -* 🔁 **fix:** Automatically restart `normal-sub` service after changing its path +* 🖥️ **feat:** Add CLI command support + - `edit_subpath` option added to CLI and `normal-sub` command + - Automatically restarts the service after applying changes -### 🤖 Telegram Bot Improvements +* 🔧 **feat:** Add backend CLI + shell logic to update `.env` subpath for NormalSub -* ➕ **feat:** Show Normal-SUB link and QR code after adding user -* 🔁 If Normal-SUB is not available, fallback to Hysteria2 direct URI and QR -* 🧪 Improved input validation for username, traffic, and expiration +## 📄 Documentation + +* 📚 **docs:** Add sponsorship section with referral link to README diff --git a/core/cli.py b/core/cli.py index 8c684f6..6672e5b 100644 --- a/core/cli.py +++ b/core/cli.py @@ -421,10 +421,13 @@ def singbox(action: str, domain: str, port: int): @cli.command('normal-sub') -@click.option('--action', '-a', required=True, help='Action to perform: start or stop', type=click.Choice(['start', 'stop'], case_sensitive=False)) -@click.option('--domain', '-d', required=False, help='Domain name for SSL', type=str) -@click.option('--port', '-p', required=False, help='Port number for NormalSub service', type=int) -def normalsub(action: str, domain: str, port: int): +@click.option('--action', '-a', required=True, + type=click.Choice(['start', 'stop', 'edit_subpath'], case_sensitive=False), + help='Action to perform: start, stop, or edit_subpath') +@click.option('--domain', '-d', required=False, help='Domain name for SSL (for start action)', type=str) +@click.option('--port', '-p', required=False, help='Port number for NormalSub service (for start action)', type=int) +@click.option('--subpath', '-sp', required=False, help='New subpath (alphanumeric, for edit_subpath action)', type=str) +def normalsub(action: str, domain: str, port: int, subpath: str): try: if action == 'start': if not domain or not port: @@ -434,6 +437,11 @@ def normalsub(action: str, domain: str, port: int): elif action == 'stop': cli_api.stop_normalsub() click.echo(f'NormalSub stopped successfully.') + elif action == 'edit_subpath': + if not subpath: + raise click.UsageError('Error: --subpath is required for the edit_subpath action.') + cli_api.edit_normalsub_subpath(subpath) + click.echo(f'NormalSub subpath updated to {subpath} successfully.') except Exception as e: click.echo(f'{e}', err=True) diff --git a/core/cli_api.py b/core/cli_api.py index 4bbc44e..ad7a8e2 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -13,6 +13,8 @@ SCRIPT_DIR = '/etc/hysteria/core/scripts' CONFIG_FILE = '/etc/hysteria/config.json' CONFIG_ENV_FILE = '/etc/hysteria/.configs.env' WEBPANEL_ENV_FILE = '/etc/hysteria/core/scripts/webpanel/.env' +NORMALSUB_ENV_FILE = '/etc/hysteria/core/scripts/normalsub/.env' + class Command(Enum): '''Contains path to command's script''' @@ -510,6 +512,26 @@ def start_normalsub(domain: str, port: int): raise InvalidInputError('Error: Both --domain and --port are required for the start action.') run_cmd(['bash', Command.INSTALL_NORMALSUB.value, 'start', domain, str(port)]) +def edit_normalsub_subpath(new_subpath: str): + '''Edits the subpath for NormalSub service.''' + if not new_subpath: + raise InvalidInputError('Error: New subpath cannot be empty.') + if not new_subpath.isalnum(): + raise InvalidInputError('Error: New subpath must contain only alphanumeric characters (a-z, A-Z, 0-9).') + + run_cmd(['bash', Command.INSTALL_NORMALSUB.value, 'edit_subpath', new_subpath]) + +def get_normalsub_subpath() -> str | None: + '''Retrieves the current SUBPATH for the NormalSub service from its .env file.''' + try: + if not os.path.exists(NORMALSUB_ENV_FILE): + return None + + env_vars = dotenv_values(NORMALSUB_ENV_FILE) + return env_vars.get('SUBPATH') + except Exception as e: + print(f"Error reading NormalSub .env file: {e}") + return None def stop_normalsub(): '''Stops NormalSub.''' diff --git a/core/scripts/normalsub/normalsub.sh b/core/scripts/normalsub/normalsub.sh index a02b6ae..ceae80f 100644 --- a/core/scripts/normalsub/normalsub.sh +++ b/core/scripts/normalsub/normalsub.sh @@ -70,7 +70,6 @@ start_service() { systemctl daemon-reload systemctl enable hysteria-normal-sub.service > /dev/null 2>&1 systemctl start hysteria-normal-sub.service > /dev/null 2>&1 - # systemctl restart caddy.service > /dev/null 2>&1 # We stopped caddy service just after its installation systemctl daemon-reload > /dev/null 2>&1 if systemctl is-active --quiet hysteria-normal-sub.service; then @@ -85,12 +84,12 @@ stop_service() { source /etc/hysteria/core/scripts/normalsub/.env fi - # if [ -n "$HYSTERIA_DOMAIN" ]; then - # echo -e "${yellow}Deleting SSL certificate for domain: $HYSTERIA_DOMAIN...${NC}" - # certbot delete --cert-name "$HYSTERIA_DOMAIN" --non-interactive > /dev/null 2>&1 - # else - # echo -e "${red}HYSTERIA_DOMAIN not found in .env. Skipping certificate deletion.${NC}" - # fi + if [ -n "$HYSTERIA_DOMAIN" ]; then + echo -e "${yellow}Deleting SSL certificate for domain: $HYSTERIA_DOMAIN...${NC}" + certbot delete --cert-name "$HYSTERIA_DOMAIN" --non-interactive > /dev/null 2>&1 + else + echo -e "${red}HYSTERIA_DOMAIN not found in .env. Skipping certificate deletion.${NC}" + fi systemctl stop hysteria-normal-sub.service > /dev/null 2>&1 systemctl disable hysteria-normal-sub.service > /dev/null 2>&1 @@ -101,6 +100,38 @@ stop_service() { echo -e "${yellow}normalsub service stopped and disabled. .env file removed.${NC}" } +edit_subpath() { + local new_path="$1" + local env_file="/etc/hysteria/core/scripts/normalsub/.env" + + if [[ ! "$new_path" =~ ^[a-zA-Z0-9]+$ ]]; then + echo -e "${red}Error: New subpath must contain only alphanumeric characters (a-z, A-Z, 0-9) and cannot be empty.${NC}" + exit 1 + fi + + if [ ! -f "$env_file" ]; then + echo -e "${red}Error: .env file ($env_file) not found. Please run the start command first.${NC}" + exit 1 + fi + + if grep -q "^SUBPATH=" "$env_file"; then + sed -i "s|^SUBPATH=.*|SUBPATH=$new_path|" "$env_file" + else + echo "SUBPATH=$new_path" >> "$env_file" + fi + echo -e "${green}SUBPATH updated to $new_path in $env_file.${NC}" + + echo -e "${yellow}Restarting hysteria-normal-sub service...${NC}" + systemctl daemon-reload + systemctl restart hysteria-normal-sub.service + + if systemctl is-active --quiet hysteria-normal-sub.service; then + echo -e "${green}hysteria-normal-sub service restarted successfully.${NC}" + else + echo -e "${red}Error: hysteria-normal-sub service failed to restart. Please check logs.${NC}" + fi +} + case "$1" in start) if [ -z "$2" ] || [ -z "$3" ]; then @@ -112,10 +143,15 @@ case "$1" in stop) stop_service ;; + edit_subpath) + if [ -z "$2" ]; then + echo -e "${red}Usage: $0 edit_subpath ${NC}" + exit 1 + fi + edit_subpath "$2" + ;; *) - echo -e "${red}Usage: $0 {start|stop} ${NC}" + echo -e "${red}Usage: $0 {start | stop | edit_subpath } ${NC}" exit 1 ;; -esac - -define_colors +esac \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/config/normalsub.py b/core/scripts/webpanel/routers/api/v1/config/normalsub.py index 93e933f..5f26569 100644 --- a/core/scripts/webpanel/routers/api/v1/config/normalsub.py +++ b/core/scripts/webpanel/routers/api/v1/config/normalsub.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, HTTPException from ..schema.response import DetailResponse -from ..schema.config.normalsub import StartInputBody +from ..schema.config.normalsub import StartInputBody, EditSubPathInputBody, GetSubPathResponse import cli_api router = APIRouter() @@ -51,4 +51,38 @@ async def normal_sub_stop_api(): except Exception as e: raise HTTPException(status_code=400, detail=f'Error: {str(e)}') -# TODO: Maybe would be nice to have a status endpoint + +@router.put('/edit_subpath', response_model=DetailResponse, summary='Edit NormalSub Subpath') +async def normal_sub_edit_subpath_api(body: EditSubPathInputBody): + """ + Edits the subpath for the NormalSub service. + + Args: + body (EditSubPathInputBody): The request body containing the new subpath. + + Returns: + DetailResponse: A response object containing a success message indicating + that the NormalSub subpath has been updated successfully. + + Raises: + HTTPException: If there is an error editing the NormalSub subpath, an + HTTPException with status code 400 and error details will be raised. + """ + try: + cli_api.edit_normalsub_subpath(body.subpath) + return DetailResponse(detail=f'Normalsub subpath updated to {body.subpath} successfully.') + except cli_api.InvalidInputError as e: + raise HTTPException(status_code=422, detail=f'Validation Error: {str(e)}') + except Exception as e: + raise HTTPException(status_code=400, detail=f'Error: {str(e)}') + +@router.get('/subpath', response_model=GetSubPathResponse, summary='Get Current NormalSub Subpath') +async def normal_sub_get_subpath_api(): + """ + Retrieves the current subpath for the NormalSub service. + """ + try: + current_subpath = cli_api.get_normalsub_subpath() + return GetSubPathResponse(subpath=current_subpath) + except Exception as e: + raise HTTPException(status_code=500, detail=f'Error retrieving subpath: {str(e)}') \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/schema/config/normalsub.py b/core/scripts/webpanel/routers/api/v1/schema/config/normalsub.py index 10933a9..7f2af1f 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/config/normalsub.py +++ b/core/scripts/webpanel/routers/api/v1/schema/config/normalsub.py @@ -1,9 +1,12 @@ -from pydantic import BaseModel - -# The StartInputBody is the same as in /hysteria/core/scripts/webpanel/routers/api/v1/schema/config/singbox.py but for /normalsub endpoint -# I'm defining it separately because highly likely it'll be different - +from pydantic import BaseModel, Field +from typing import Optional class StartInputBody(BaseModel): domain: str port: int + +class EditSubPathInputBody(BaseModel): + subpath: str = Field(..., min_length=1, pattern=r"^[a-zA-Z0-9]+$", description="The new subpath, must be alphanumeric.") + +class GetSubPathResponse(BaseModel): + subpath: Optional[str] = Field(None, description="The current NormalSub subpath, or null if not set/found.") \ No newline at end of file diff --git a/core/scripts/webpanel/templates/settings.html b/core/scripts/webpanel/templates/settings.html index 88097bd..5f0d733 100644 --- a/core/scripts/webpanel/templates/settings.html +++ b/core/scripts/webpanel/templates/settings.html @@ -56,7 +56,7 @@ aria-controls='ip-limit' aria-selected='false'> IP Limit -