diff --git a/.gitignore b/.gitignore index 358b61c..3043593 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,5 @@ cython_debug/ #.idea/ hysteria2_venv/ -.vscode/ +# .vscode/ +# !.vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b19fff9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,172 @@ +{ + "workbench.colorTheme": "Default Dark+", + "workbench.colorCustomizations": { + "editor.background": "#252b3d", + "editor.foreground": "#dde1eb", + "activityBar.background": "#2f3648", + "activityBar.foreground": "#FF6B35", + "activityBar.activeBorder": "#FF6B35", + "activityBar.inactiveForeground": "#a8afc0", + "sideBar.background": "#2f3648", + "sideBar.foreground": "#dde1eb", + "sideBar.border": "#3a4158", + "sideBarTitle.foreground": "#FFA500", + "statusBar.background": "#2f3648", + "statusBar.foreground": "#58C4DC", + "statusBar.noFolderBackground": "#2f3648", + "statusBar.debuggingBackground": "#FF6347", + "statusBar.debuggingForeground": "#ffffff", + "titleBar.activeBackground": "#252b3d", + "titleBar.activeForeground": "#dde1eb", + "titleBar.inactiveBackground": "#252b3d", + "titleBar.inactiveForeground": "#a8afc0", + "titleBar.border": "#3a4158", + "editorGroupHeader.tabsBackground": "#252b3d", + "editorGroupHeader.tabsBorder": "#3a4158", + "tab.activeBackground": "#2f3648", + "tab.activeForeground": "#FFA500", + "tab.inactiveBackground": "#252b3d", + "tab.inactiveForeground": "#a8afc0", + "tab.border": "#3a4158", + "tab.activeBorder": "#FF6B35", + "tab.activeBorderTop": "#FF6B35", + "editorLineNumber.foreground": "#8891a4", + "editorLineNumber.activeForeground": "#58C4DC", + "editorCursor.foreground": "#FFA500", + "editor.selectionBackground": "#4a6589", + "editor.selectionHighlightBackground": "#4a658960", + "editor.lineHighlightBackground": "#2f364860", + "editorBracketMatch.background": "#4a658960", + "editorBracketMatch.border": "#FFA500", + "editorWidget.background": "#2f3648", + "editorSuggestWidget.background": "#2f3648", + "editorSuggestWidget.selectedBackground": "#4a6589", + "editorSuggestWidget.highlightForeground": "#FFA500", + "input.background": "#252b3d", + "input.border": "#3a4158", + "input.foreground": "#dde1eb", + "inputOption.activeBorder": "#58C4DC", + "dropdown.background": "#2f3648", + "dropdown.border": "#3a4158", + "button.background": "#3FB950", + "button.foreground": "#ffffff", + "button.hoverBackground": "#4ec55e", + "list.activeSelectionBackground": "#4a6589", + "list.activeSelectionForeground": "#ffffff", + "list.inactiveSelectionBackground": "#4a658960", + "list.hoverBackground": "#4a658950", + "list.focusBackground": "#4a6589", + "terminal.foreground": "#dde1eb", + "terminal.ansiGreen": "#3FB950", + "terminal.ansiYellow": "#D29922", + "terminal.ansiBlue": "#58A6FF", + "terminal.ansiMagenta": "#BC8CFF", + "terminal.ansiCyan": "#39C5CF", + "terminal.ansiRed": "#FF6347", + "gitDecoration.modifiedResourceForeground": "#D29922", + "gitDecoration.addedResourceForeground": "#3FB950", + "gitDecoration.deletedResourceForeground": "#FF6347", + "gitDecoration.untrackedResourceForeground": "#39C5CF", + "badge.background": "#FF6B35", + "badge.foreground": "#ffffff", + "notifications.background": "#2f3648", + "notifications.foreground": "#dde1eb", + "notifications.border": "#3a4158", + "notificationLink.foreground": "#58A6FF", + "panel.background": "#252b3d", + "panel.border": "#3a4158", + "panelTitle.activeForeground": "#FFA500", + "panelTitle.inactiveForeground": "#a8afc0", + "panelTitle.activeBorder": "#FF6B35" + }, + "editor.tokenColorCustomizations": { + "textMateRules": [ + { + "scope": "comment", + "settings": { + "foreground": "#8891a4", + "fontStyle": "italic" + } + }, + { + "scope": ["keyword", "storage.type", "storage.modifier"], + "settings": { + "foreground": "#FF6347" + } + }, + { + "scope": ["string", "string.quoted"], + "settings": { + "foreground": "#3FB950" + } + }, + { + "scope": ["constant.numeric", "constant.language"], + "settings": { + "foreground": "#BC8CFF" + } + }, + { + "scope": ["variable", "support.variable"], + "settings": { + "foreground": "#58A6FF" + } + }, + { + "scope": ["entity.name.function", "support.function"], + "settings": { + "foreground": "#D29922" + } + }, + { + "scope": ["entity.name.type", "entity.name.class", "support.class"], + "settings": { + "foreground": "#39C5CF" + } + }, + { + "scope": ["entity.name.tag"], + "settings": { + "foreground": "#3FB950" + } + }, + { + "scope": ["entity.other.attribute-name"], + "settings": { + "foreground": "#39C5CF" + } + }, + { + "scope": ["support.type.property-name"], + "settings": { + "foreground": "#58A6FF" + } + }, + { + "scope": ["meta.import", "meta.export"], + "settings": { + "foreground": "#BC8CFF" + } + }, + { + "scope": ["punctuation.definition.tag"], + "settings": { + "foreground": "#a8afc0" + } + } + ] + }, + "editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace", + "editor.fontSize": 13.5, + "editor.lineHeight": 1.6, + "editor.fontLigatures": true, + "editor.fontWeight": "400", + "editor.letterSpacing": 0.5, + "editor.cursorBlinking": "expand", + "editor.cursorSmoothCaretAnimation": "on", + "editor.smoothScrolling": true, + "workbench.list.smoothScrolling": true, + "editor.minimap.enabled": false, + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": "active" +} \ No newline at end of file diff --git a/VERSION b/VERSION index fad066f..4fd0fe3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.5.0 \ No newline at end of file +2.5.1 \ No newline at end of file diff --git a/changelog b/changelog index 5440a8a..14d1760 100644 --- a/changelog +++ b/changelog @@ -1,79 +1,36 @@ -# πŸ“¦ ** Release 2.5.0 β€” ACL & WARP fixes, safer password generation, service controls πŸš€** +# πŸ”– ** release(2.5.1): Telegram UX improvements, web panel access & outbound enhancements πŸš€ ** -*Released: 2025-12-19* +*Released: 2026-01-02* ## ✨ New Features -### 🧠 **Core & Service Management** +* πŸ€– **Telegram Bot**: Added settings menu +* 🌐 **Web Panel URL**: -* πŸ” Added **Hysteria2 core version detection** and validation -* πŸ“Š Display **core version** directly in the web panel -* πŸ” Added **Restart Hysteria2** button in the web UI with proper fetch handling -* πŸ”Œ New API endpoint to **restart Hysteria2 service** programmatically + * Added service status check before retrieving web panel URL + * New handlers to retrieve and display web panel URL +* πŸ–₯️ **CLI**: Added option to display *only* the web panel URL +* πŸ”€ **Outbounds**: Introduced new `select` outbound type(SingBox) +* πŸ‘₯ **Users**: Added option to display up to **1000 users** in selection dropdown +* ⏳ **Expiration**: Added validation for expiration days and improved UI -### 🌍 **Geo & ACL Rule Management** +### πŸ› Fixes -* πŸ—ΊοΈ Enhanced **Geo file update process** with intelligent ACL rule handling -* 🧩 Added **geo rule detection & auto-update** for ACL configurations -* 🧹 Removed redundant `geoip` / `geosite` rejection rules -* 🚫 Removed speedtest-related rejection from ACL rules +* πŸ›‘οΈ Prevent enabling **Masquerade** when **OBFS** is active +* βš™οΈ Conditionally generate **sing-box OBFS** configuration(SingBox) -### πŸ›‘οΈ **Security & Password Handling** +### ♻️ Refactors & Improvements -* πŸ” Replaced `pwgen` with **Python `secrets` module** for secure password generation -* πŸ”‘ Improved password generation for: +* 🧭 Introduced settings submenu structure in Telegram bot +* πŸ”— Removed username from URI fragments +* πŸ’Ύ Streamlined upgrade backup process and removed geo data download - * add-user - * bulk users - * internal password utilities -* πŸ§ͺ Removed implicit password auto-generation where not required +### πŸ”§ Dependencies -### 🧰 **CLI Improvements** +* ⬆️ **FastAPI** bumped to `0.128.0` +* ⬆️ **psutil** bumped to `7.2.1` +* ⬆️ **pillow** bumped to `12.1.0` -* πŸ“€ Added `run_cmd_and_stream` for **real-time command output streaming** -* βš™οΈ Improved error messages during **Hysteria2 installation** -* πŸ” Updated install scripts to return meaningful output messages -* πŸ”’ Replaced `pwgen` with **openssl** for: - - * obfs password generation - * normalsub path generation - -### 🌐 **Network & IP Detection** - -* 🌍 Added **ip.sb fallback** for public IP detection to improve reliability - ---- - -## πŸ› Fixes & Improvements - -### πŸ‘€ **User & Username Handling** - -* βœ… Allowed **underscores (`_`) in usernames** -* 🧼 Improved validation regex and error handling -* πŸ”§ Updated `remove-user` command syntax -* 🧠 Improved user handler robustness - -### 🎭 **Masquerade & OBFS** - -* 🚫 Prevented **OBFS usage when masquerade is enabled** to avoid conflicts - -### 🧹 **Scheduler & System Stability** - -* πŸ”‡ Suppressed noisy `systemctl` output during scheduler setup -* 🧯 Improved rule-checking logic for WARP configuration status - -### 🧼 **Uninstall & Cleanup** - -* ♻️ Improved WARP uninstall flow: - - * Better config loading & error handling - * Automatic **ACL rule reset** after uninstall - ---- - -## πŸ“¦ Dependency Updates - -* ⬆️ **FastAPI** β†’ 0.124.4 -* ⬆️ **Certbot** β†’ 5.2.2 -* ⬆️ **python-multipart** β†’ 0.0.21 +### ❀️ Credits +Special thanks to **@MiliAxe** and **@SeyedHashtag** for their valuable contributions πŸ™Œ diff --git a/core/cli.py b/core/cli.py index 8518039..5c39552 100644 --- a/core/cli.py +++ b/core/cli.py @@ -670,10 +670,14 @@ def stop_webpanel_decoy(): click.echo(f'{e}', err=True) @cli.command('get-webpanel-url') -def get_web_panel_url(): +@click.option('--url-only', is_flag=True, help='Only display the URL without additional text') +def get_web_panel_url(url_only: bool): try: url = cli_api.get_webpanel_url() - click.echo(f'Hysteria web panel is now running. The service is accessible on: {url}') + if url_only: + click.echo(url) + else: + click.echo(f'Hysteria web panel is now running. The service is accessible on: {url}') except Exception as e: click.echo(f'{e}', err=True) diff --git a/core/scripts/hysteria2/show_user_uri.py b/core/scripts/hysteria2/show_user_uri.py index 5981369..f361bf6 100644 --- a/core/scripts/hysteria2/show_user_uri.py +++ b/core/scripts/hysteria2/show_user_uri.py @@ -160,13 +160,13 @@ def show_uri(args: argparse.Namespace) -> None: if args.all or args.ip_version == 4: if ip4 and ip4 != "None": uri = generate_uri(args.username, auth_password, ip4, local_port, - local_obfs_password, local_sha256, local_sni, 4, local_insecure, f"{args.username}-IPv4") + local_obfs_password, local_sha256, local_sni, 4, local_insecure, "IPv4") display_uri_and_qr(uri, "IPv4", args, terminal_width) if args.all or args.ip_version == 6: if ip6 and ip6 != "None": uri = generate_uri(args.username, auth_password, ip6, local_port, - local_obfs_password, local_sha256, local_sni, 6, local_insecure, f"{args.username}-IPv6") + local_obfs_password, local_sha256, local_sni, 6, local_insecure, "IPv6") display_uri_and_qr(uri, "IPv6", args, terminal_width) for node in nodes: @@ -194,14 +194,14 @@ def show_uri(args: argparse.Namespace) -> None: sni=node_sni, ip_version=ip_v, insecure=node_insecure, - fragment_tag=f"{args.username}-{node_name}" + fragment_tag=node_name ) display_uri_and_qr(uri, f"Node: {node_name} (IPv{ip_v})", args, terminal_width) if args.singbox and is_service_active("hysteria-singbox.service"): domain, port = get_singbox_domain_and_port() if domain and port: - print(f"\nSingbox Sublink:\nhttps://{domain}:{port}/sub/singbox/{args.username}/{args.ip_version}#{args.username}\n") + print(f"\nSingbox Sublink:\nhttps://{domain}:{port}/sub/singbox/{args.username}/{args.ip_version}#Hysteria2\n") if args.normalsub and is_service_active("hysteria-normal-sub.service"): domain, port, subpath = get_normalsub_domain_and_port() diff --git a/core/scripts/hysteria2/wrapper_uri.py b/core/scripts/hysteria2/wrapper_uri.py index d0246d3..0c5c65a 100644 --- a/core/scripts/hysteria2/wrapper_uri.py +++ b/core/scripts/hysteria2/wrapper_uri.py @@ -87,9 +87,9 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: user_output = {"username": username, "ipv4": None, "ipv6": None, "nodes": [], "normal_sub": None} if ip4 and ip4 != "None": - user_output["ipv4"] = generate_uri(username, auth_password, ip4, default_port, base_uri_params, 4, f"{username}-IPv4") + user_output["ipv4"] = generate_uri(username, auth_password, ip4, default_port, base_uri_params, 4, "IPv4") if ip6 and ip6 != "None": - user_output["ipv6"] = generate_uri(username, auth_password, ip6, default_port, base_uri_params, 6, f"{username}-IPv6") + user_output["ipv6"] = generate_uri(username, auth_password, ip6, default_port, base_uri_params, 6, "IPv6") for node in nodes: node_name = node.get("name") @@ -98,7 +98,7 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: continue ip_v = 6 if ':' in node_ip else 4 - tag = f"{username}-{node_name}" + tag = node_name node_port = str(node.get("port", default_port)) node_sni = node.get("sni", default_sni) @@ -117,7 +117,7 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: user_output["nodes"].append({"name": node_name, "uri": uri}) if ns_domain and ns_port and ns_subpath: - user_output["normal_sub"] = f"https://{ns_domain}:{ns_port}/{ns_subpath}/{auth_password}#{username}" + user_output["normal_sub"] = f"https://{ns_domain}:{ns_port}/{ns_subpath}/{auth_password}#Hysteria2" results.append(user_output) diff --git a/core/scripts/normalsub/normalsub.py b/core/scripts/normalsub/normalsub.py index af4906b..aa963e5 100644 --- a/core/scripts/normalsub/normalsub.py +++ b/core/scripts/normalsub/normalsub.py @@ -308,15 +308,11 @@ class SingboxConfigGenerator: print(f"Error during Singbox config generation from URI: {e}, URI: {uri}") return None - return { + outbound_config = { "type": "hysteria2", "tag": unquote(parsed_url.fragment), "server": server, "server_port": server_port, - "obfs": { - "type": "salamander", - "password": obfs_password - }, "password": final_password, "tls": { "enabled": True, @@ -325,6 +321,14 @@ class SingboxConfigGenerator: } } + if obfs_password: + outbound_config["obfs"] = { + "type": "salamander", + "password": obfs_password + } + + return outbound_config + def combine_configs(self, all_uris: List[str], username: str, fragment: str) -> Optional[Dict[str, Any]]: if not all_uris: return None diff --git a/core/scripts/normalsub/singbox.json b/core/scripts/normalsub/singbox.json index cbcebd1..da9789f 100644 --- a/core/scripts/normalsub/singbox.json +++ b/core/scripts/normalsub/singbox.json @@ -70,11 +70,19 @@ { "outbounds": [ "auto", - "direct" + "direct", + "select" ], "tag": "proxy", "type": "selector" }, + { + "type": "selector", + "tag": "select", + + "outbounds": [], + "interrupt_exist_connections": false + }, { "interval": "10m", "outbounds": [], diff --git a/core/scripts/telegrambot/utils/__init__.py b/core/scripts/telegrambot/utils/__init__.py index 49f6c6c..4ec52d0 100644 --- a/core/scripts/telegrambot/utils/__init__.py +++ b/core/scripts/telegrambot/utils/__init__.py @@ -8,3 +8,5 @@ from .search import * from .serverinfo import * from .cpu import * from .check_version import * +from .weburl import * +from .settings import * \ No newline at end of file diff --git a/core/scripts/telegrambot/utils/common.py b/core/scripts/telegrambot/utils/common.py index edf9c3c..963b885 100644 --- a/core/scripts/telegrambot/utils/common.py +++ b/core/scripts/telegrambot/utils/common.py @@ -4,5 +4,11 @@ def create_main_markup(): markup = types.ReplyKeyboardMarkup(resize_keyboard=True) markup.row('βž• Add User', 'πŸ” Show User') markup.row('πŸ—‘οΈ Delete User', 'πŸ–₯️ Server Info') - markup.row('πŸ’Ύ Backup Server') + markup.row('πŸ’Ύ Backup Server', 'βš™οΈ Settings') return markup + +def create_settings_markup(): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + markup.row('πŸ”— Get Webpanel URL') + markup.row('⬅️ Back') + return markup \ No newline at end of file diff --git a/core/scripts/telegrambot/utils/settings.py b/core/scripts/telegrambot/utils/settings.py new file mode 100644 index 0000000..0976044 --- /dev/null +++ b/core/scripts/telegrambot/utils/settings.py @@ -0,0 +1,10 @@ +from utils.command import * +from utils.common import create_main_markup, create_settings_markup + +@bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'βš™οΈ Settings') +def settings_menu_handler(message): + bot.send_message(message.chat.id, "βš™οΈ Settings Menu:", reply_markup=create_settings_markup()) + +@bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == '⬅️ Back') +def back_to_main_menu_handler(message): + bot.send_message(message.chat.id, "⬅️ Returning to Main Menu...", reply_markup=create_main_markup()) \ No newline at end of file diff --git a/core/scripts/telegrambot/utils/weburl.py b/core/scripts/telegrambot/utils/weburl.py new file mode 100644 index 0000000..33df1bb --- /dev/null +++ b/core/scripts/telegrambot/utils/weburl.py @@ -0,0 +1,15 @@ +from utils.command import * + +@bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'πŸ”— Get Webpanel URL') +def get_webpanel_url_handler(message): + status_command = f"python3 {CLI_PATH} get-webpanel-services-status" + status_result = run_cli_command(status_command) + + if "hysteria-webpanel.service: Inactive" in status_result: + bot.reply_to(message, "⚠️ The Webpanel service is currently inactive.") + return + + command = f"python3 {CLI_PATH} get-webpanel-url --url-only" + result = run_cli_command(command) + bot.send_chat_action(message.chat.id, 'typing') + bot.reply_to(message, "🌐 Webpanel URL:\n" + result) \ No newline at end of file diff --git a/core/scripts/webpanel/templates/users.html b/core/scripts/webpanel/templates/users.html index c0c3da6..4699e19 100644 --- a/core/scripts/webpanel/templates/users.html +++ b/core/scripts/webpanel/templates/users.html @@ -161,6 +161,7 @@ + diff --git a/requirements.txt b/requirements.txt index 8433a81..184a85a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ aiofiles==25.1.0 click==8.3.1 # Utilities -psutil==7.1.3 +psutil==7.2.1 python-dotenv==1.2.1 schedule==1.2.2 requests==2.32.5 @@ -16,7 +16,7 @@ typing_extensions==4.15.0 pyTelegramBotAPI==4.29.1 qrcode==8.2 pypng==0.20220715.0 -pillow==12.0.0 +pillow==12.1.0 # Cache / misc propcache==0.4.1 @@ -28,7 +28,7 @@ pymongo==4.15.5 hysteria2-api==0.1.3 # Web panel (FastAPI stack) -fastapi==0.124.4 +fastapi==0.128.0 Jinja2==3.1.6 python-multipart==0.0.21 hypercorn==0.18.0