From 18d3a1029b452ed5300c723c5f686f5e74b75af4 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:22:04 +0330 Subject: [PATCH] perf(api): optimize bulk user URI fetching in webpanel Refactored the web panel's user link generation to resolve a major performance bottleneck. Previously, fetching links for N users would trigger N separate script executions, causing significant delays from process startup overhead. - Introduced a new bulk API endpoint (`/api/v1/users/uri/bulk`) that accepts a list of usernames and calls the backend script only once. - Updated the frontend JavaScript in `users.html` to use this new endpoint, replacing N parallel API calls with a single one. - Cleaned up the `wrapper_uri.py` script for better readability and maintainability. --- core/scripts/hysteria2/wrapper_uri.py | 90 +- .../webpanel/routers/api/v1/schema/user.py | 2 + core/scripts/webpanel/routers/api/v1/user.py | 35 +- core/scripts/webpanel/templates/users.html | 798 ++++-------------- 4 files changed, 227 insertions(+), 698 deletions(-) diff --git a/core/scripts/hysteria2/wrapper_uri.py b/core/scripts/hysteria2/wrapper_uri.py index 44bfb4f..57a0d4a 100644 --- a/core/scripts/hysteria2/wrapper_uri.py +++ b/core/scripts/hysteria2/wrapper_uri.py @@ -15,7 +15,7 @@ def load_json_file(file_path: str) -> Any: if not os.path.exists(file_path): return None try: - with open(file_path, 'r') as f: + with open(file_path, 'r', encoding='utf-8') as f: content = f.read() return json.loads(content) if content else None except (json.JSONDecodeError, IOError): @@ -25,7 +25,7 @@ def load_json_file(file_path: str) -> Any: def load_env_file(env_file: str) -> Dict[str, str]: env_vars = {} if os.path.exists(env_file): - with open(env_file, 'r') as f: + with open(env_file, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if line and not line.startswith('#') and '=' in line: @@ -34,51 +34,44 @@ def load_env_file(env_file: str) -> Dict[str, str]: return env_vars def generate_uri(username: str, auth_password: str, ip: str, port: str, - obfs_password: str, sha256: str, sni: str, ip_version: int, - insecure: bool, fragment_tag: str) -> str: + uri_params: Dict[str, str], ip_version: int, fragment_tag: str) -> str: ip_part = f"[{ip}]" if ip_version == 6 and ':' in ip else ip uri_base = f"hy2://{username}:{auth_password}@{ip_part}:{port}" - - params = { - "insecure": "1" if insecure else "0", - "sni": sni - } - if obfs_password: - params["obfs"] = "salamander" - params["obfs-password"] = obfs_password - if sha256: - params["pinSHA256"] = sha256 - - query_string = "&".join([f"{k}={v}" for k, v in params.items()]) + query_string = "&".join([f"{k}={v}" for k, v in uri_params.items()]) return f"{uri_base}?{query_string}#{fragment_tag}" def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: config = load_json_file(CONFIG_FILE) all_users = load_json_file(USERS_FILE) - nodes = load_json_file(NODES_JSON_PATH) or [] if not config or not all_users: - print("Error: Could not load hysteria2 configuration or user files.", file=sys.stderr) + print("Error: Could not load Hysteria2 configuration or user files.", file=sys.stderr) sys.exit(1) + nodes = load_json_file(NODES_JSON_PATH) or [] port = config.get("listen", "").split(":")[-1] tls_config = config.get("tls", {}) - sha256 = tls_config.get("pinSHA256", "") - insecure = tls_config.get("insecure", True) - obfs_password = config.get("obfs", {}).get("salamander", {}).get("password", "") - hy2_env = load_env_file(CONFIG_ENV) + ns_env = load_env_file(NORMALSUB_ENV) + + base_uri_params = { + "insecure": "1" if tls_config.get("insecure", True) else "0", + "sni": hy2_env.get('SNI', '') + } + obfs_password = config.get("obfs", {}).get("salamander", {}).get("password") + if obfs_password: + base_uri_params["obfs"] = "salamander" + base_uri_params["obfs-password"] = obfs_password + + sha256 = tls_config.get("pinSHA256") + if sha256: + base_uri_params["pinSHA256"] = sha256 + ip4 = hy2_env.get('IP4') ip6 = hy2_env.get('IP6') - sni = hy2_env.get('SNI', '') - - ns_env = load_env_file(NORMALSUB_ENV) - ns_domain = ns_env.get('HYSTERIA_DOMAIN') - ns_port = ns_env.get('HYSTERIA_PORT') - ns_subpath = ns_env.get('SUBPATH') + ns_domain, ns_port, ns_subpath = ns_env.get('HYSTERIA_DOMAIN'), ns_env.get('HYSTERIA_PORT'), ns_env.get('SUBPATH') results = [] - for username in target_usernames: user_data = all_users.get(username) if not user_data or "password" not in user_data: @@ -86,34 +79,20 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: continue auth_password = user_data["password"] - 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": - user_output["ipv4"] = generate_uri( - username, auth_password, ip4, port, obfs_password, sha256, sni, 4, insecure, f"{username}-IPv4" - ) - + user_output["ipv4"] = generate_uri(username, auth_password, ip4, port, base_uri_params, 4, f"{username}-IPv4") if ip6 and ip6 != "None": - user_output["ipv6"] = generate_uri( - username, auth_password, ip6, port, obfs_password, sha256, sni, 6, insecure, f"{username}-IPv6" - ) + user_output["ipv6"] = generate_uri(username, auth_password, ip6, port, base_uri_params, 6, f"{username}-IPv6") for node in nodes: - node_name, node_ip = node.get("name"), node.get("ip") - if not (node_name and node_ip): - continue - ip_v = 6 if ':' in node_ip else 4 - tag = f"{username}-{node_name}" - uri = generate_uri( - username, auth_password, node_ip, port, obfs_password, sha256, sni, ip_v, insecure, tag - ) - user_output["nodes"].append({"name": node_name, "uri": uri}) + if node_name := node.get("name"): + if node_ip := node.get("ip"): + ip_v = 6 if ':' in node_ip else 4 + tag = f"{username}-{node_name}" + uri = generate_uri(username, auth_password, node_ip, port, base_uri_params, ip_v, tag) + 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}/sub/normal/{auth_password}#{username}" @@ -123,16 +102,13 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: return results def main(): - parser = argparse.ArgumentParser( - description="Efficiently generate Hysteria2 URIs for multiple users.", - formatter_class=argparse.RawTextHelpFormatter - ) + parser = argparse.ArgumentParser(description="Efficiently generate Hysteria2 URIs for multiple users.") parser.add_argument('usernames', nargs='*', help="A list of usernames to process.") parser.add_argument('--all', action='store_true', help="Process all users from users.json.") args = parser.parse_args() - target_usernames = args.usernames + if args.all: all_users = load_json_file(USERS_FILE) if all_users: diff --git a/core/scripts/webpanel/routers/api/v1/schema/user.py b/core/scripts/webpanel/routers/api/v1/schema/user.py index 4f69289..64dc768 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/user.py +++ b/core/scripts/webpanel/routers/api/v1/schema/user.py @@ -18,6 +18,8 @@ class UserInfoResponse(BaseModel): class UserListResponse(RootModel): root: dict[str, UserInfoResponse] +class UsernamesRequest(BaseModel): + usernames: List[str] class AddUserInputBody(BaseModel): username: str diff --git a/core/scripts/webpanel/routers/api/v1/user.py b/core/scripts/webpanel/routers/api/v1/user.py index 6bdd238..4d57a31 100644 --- a/core/scripts/webpanel/routers/api/v1/user.py +++ b/core/scripts/webpanel/routers/api/v1/user.py @@ -1,7 +1,15 @@ import json +from typing import List from fastapi import APIRouter, HTTPException - -from .schema.user import UserListResponse, UserInfoResponse, AddUserInputBody, EditUserInputBody, UserUriResponse, AddBulkUsersInputBody +from .schema.user import ( + UserListResponse, + UserInfoResponse, + AddUserInputBody, + EditUserInputBody, + UserUriResponse, + AddBulkUsersInputBody, + UsernamesRequest +) from .schema.response import DetailResponse import cli_api @@ -79,6 +87,29 @@ async def add_bulk_users_api(body: AddBulkUsersInputBody): raise HTTPException(status_code=500, detail=f"An unexpected error occurred while adding bulk users: {str(e)}") +@router.post('/uri/bulk', response_model=List[UserUriResponse]) +async def show_multiple_user_uris_api(request: UsernamesRequest): + """ + Get URI information for multiple users in a single request for efficiency. + """ + if not request.usernames: + return [] + + try: + uri_data_list = cli_api.show_user_uri_json(request.usernames) + if not uri_data_list: + raise HTTPException(status_code=404, detail='No URI data found for the provided users.') + + valid_responses = [data for data in uri_data_list if not data.get('error')] + + return valid_responses + except cli_api.ScriptNotFoundError as e: + raise HTTPException(status_code=500, detail=f'Server script error: {str(e)}') + except cli_api.CommandExecutionError as e: + raise HTTPException(status_code=400, detail=f'Error executing script: {str(e)}') + except Exception as e: + raise HTTPException(status_code=400, detail=f'Unexpected error: {str(e)}') + @router.get('/{username}', response_model=UserInfoResponse) async def get_user_api(username: str): diff --git a/core/scripts/webpanel/templates/users.html b/core/scripts/webpanel/templates/users.html index 34933ef..873e1c8 100644 --- a/core/scripts/webpanel/templates/users.html +++ b/core/scripts/webpanel/templates/users.html @@ -26,8 +26,8 @@
-
@@ -66,8 +66,8 @@
- {% if users|length == 0 %} -
@@ -203,7 +196,7 @@ @@ -237,7 +230,7 @@ @@ -249,8 +242,7 @@ -