Merge branch 'ReturnFI:main' into ordev3

This commit is contained in:
Seyed Mahdi
2026-01-03 22:21:31 +03:30
committed by GitHub
15 changed files with 268 additions and 88 deletions

3
.gitignore vendored
View File

@ -160,4 +160,5 @@ cython_debug/
#.idea/ #.idea/
hysteria2_venv/ hysteria2_venv/
.vscode/ # .vscode/
# !.vscode/settings.json

172
.vscode/settings.json vendored Normal file
View File

@ -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"
}

View File

@ -1 +1 @@
2.5.0 2.5.1

View File

@ -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 ## ✨ New Features
### 🧠 **Core & Service Management** * 🤖 **Telegram Bot**: Added settings menu
* 🌐 **Web Panel URL**:
* 🔍 Added **Hysteria2 core version detection** and validation * Added service status check before retrieving web panel URL
* 📊 Display **core version** directly in the web panel * New handlers to retrieve and display web panel URL
* 🔁 Added **Restart Hysteria2** button in the web UI with proper fetch handling * 🖥️ **CLI**: Added option to display *only* the web panel URL
* 🔌 New API endpoint to **restart Hysteria2 service** programmatically * 🔀 **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 * 🛡 Prevent enabling **Masquerade** when **OBFS** is active
* 🧩 Added **geo rule detection & auto-update** for ACL configurations * ⚙️ Conditionally generate **sing-box OBFS** configuration(SingBox)
* 🧹 Removed redundant `geoip` / `geosite` rejection rules
* 🚫 Removed speedtest-related rejection from ACL rules
### 🛡 **Security & Password Handling** ### Refactors & Improvements
* 🔐 Replaced `pwgen` with **Python `secrets` module** for secure password generation * 🧭 Introduced settings submenu structure in Telegram bot
* 🔑 Improved password generation for: * 🔗 Removed username from URI fragments
* 💾 Streamlined upgrade backup process and removed geo data download
* add-user ### 🔧 Dependencies
* bulk users
* internal password utilities
* 🧪 Removed implicit password auto-generation where not required
### 🧰 **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** ### ❤️ Credits
* ⚙️ 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
Special thanks to **@MiliAxe** and **@SeyedHashtag** for their valuable contributions 🙌

View File

@ -670,9 +670,13 @@ def stop_webpanel_decoy():
click.echo(f'{e}', err=True) click.echo(f'{e}', err=True)
@cli.command('get-webpanel-url') @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: try:
url = cli_api.get_webpanel_url() url = cli_api.get_webpanel_url()
if url_only:
click.echo(url)
else:
click.echo(f'Hysteria web panel is now running. The service is accessible on: {url}') click.echo(f'Hysteria web panel is now running. The service is accessible on: {url}')
except Exception as e: except Exception as e:
click.echo(f'{e}', err=True) click.echo(f'{e}', err=True)

View File

@ -160,13 +160,13 @@ def show_uri(args: argparse.Namespace) -> None:
if args.all or args.ip_version == 4: if args.all or args.ip_version == 4:
if ip4 and ip4 != "None": if ip4 and ip4 != "None":
uri = generate_uri(args.username, auth_password, ip4, local_port, 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) display_uri_and_qr(uri, "IPv4", args, terminal_width)
if args.all or args.ip_version == 6: if args.all or args.ip_version == 6:
if ip6 and ip6 != "None": if ip6 and ip6 != "None":
uri = generate_uri(args.username, auth_password, ip6, local_port, 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) display_uri_and_qr(uri, "IPv6", args, terminal_width)
for node in nodes: for node in nodes:
@ -194,14 +194,14 @@ def show_uri(args: argparse.Namespace) -> None:
sni=node_sni, sni=node_sni,
ip_version=ip_v, ip_version=ip_v,
insecure=node_insecure, 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) 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"): if args.singbox and is_service_active("hysteria-singbox.service"):
domain, port = get_singbox_domain_and_port() domain, port = get_singbox_domain_and_port()
if 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"): if args.normalsub and is_service_active("hysteria-normal-sub.service"):
domain, port, subpath = get_normalsub_domain_and_port() domain, port, subpath = get_normalsub_domain_and_port()

View File

@ -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} user_output = {"username": username, "ipv4": None, "ipv6": None, "nodes": [], "normal_sub": None}
if ip4 and ip4 != "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": 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: for node in nodes:
node_name = node.get("name") node_name = node.get("name")
@ -98,7 +98,7 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]:
continue continue
ip_v = 6 if ':' in node_ip else 4 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_port = str(node.get("port", default_port))
node_sni = node.get("sni", default_sni) 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}) user_output["nodes"].append({"name": node_name, "uri": uri})
if ns_domain and ns_port and ns_subpath: 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) results.append(user_output)

View File

@ -308,15 +308,11 @@ class SingboxConfigGenerator:
print(f"Error during Singbox config generation from URI: {e}, URI: {uri}") print(f"Error during Singbox config generation from URI: {e}, URI: {uri}")
return None return None
return { outbound_config = {
"type": "hysteria2", "type": "hysteria2",
"tag": unquote(parsed_url.fragment), "tag": unquote(parsed_url.fragment),
"server": server, "server": server,
"server_port": server_port, "server_port": server_port,
"obfs": {
"type": "salamander",
"password": obfs_password
},
"password": final_password, "password": final_password,
"tls": { "tls": {
"enabled": True, "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]]: def combine_configs(self, all_uris: List[str], username: str, fragment: str) -> Optional[Dict[str, Any]]:
if not all_uris: if not all_uris:
return None return None

View File

@ -70,11 +70,19 @@
{ {
"outbounds": [ "outbounds": [
"auto", "auto",
"direct" "direct",
"select"
], ],
"tag": "proxy", "tag": "proxy",
"type": "selector" "type": "selector"
}, },
{
"type": "selector",
"tag": "select",
"outbounds": [],
"interrupt_exist_connections": false
},
{ {
"interval": "10m", "interval": "10m",
"outbounds": [], "outbounds": [],

View File

@ -8,3 +8,5 @@ from .search import *
from .serverinfo import * from .serverinfo import *
from .cpu import * from .cpu import *
from .check_version import * from .check_version import *
from .weburl import *
from .settings import *

View File

@ -4,5 +4,11 @@ def create_main_markup():
markup = types.ReplyKeyboardMarkup(resize_keyboard=True) markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
markup.row(' Add User', '🔍 Show User') markup.row(' Add User', '🔍 Show User')
markup.row('🗑️ Delete User', '🖥️ Server Info') 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 return markup

View File

@ -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())

View File

@ -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)

View File

@ -161,6 +161,7 @@
<option value="25">25</option> <option value="25">25</option>
<option value="50" selected>50</option> <option value="50" selected>50</option>
<option value="100">100</option> <option value="100">100</option>
<option value="1000">1000</option>
</select> </select>
</form> </form>
</div> </div>

View File

@ -6,7 +6,7 @@ aiofiles==25.1.0
click==8.3.1 click==8.3.1
# Utilities # Utilities
psutil==7.1.3 psutil==7.2.1
python-dotenv==1.2.1 python-dotenv==1.2.1
schedule==1.2.2 schedule==1.2.2
requests==2.32.5 requests==2.32.5
@ -16,7 +16,7 @@ typing_extensions==4.15.0
pyTelegramBotAPI==4.29.1 pyTelegramBotAPI==4.29.1
qrcode==8.2 qrcode==8.2
pypng==0.20220715.0 pypng==0.20220715.0
pillow==12.0.0 pillow==12.1.0
# Cache / misc # Cache / misc
propcache==0.4.1 propcache==0.4.1
@ -28,7 +28,7 @@ pymongo==4.15.5
hysteria2-api==0.1.3 hysteria2-api==0.1.3
# Web panel (FastAPI stack) # Web panel (FastAPI stack)
fastapi==0.124.4 fastapi==0.128.0
Jinja2==3.1.6 Jinja2==3.1.6
python-multipart==0.0.21 python-multipart==0.0.21
hypercorn==0.18.0 hypercorn==0.18.0