From ecb08dbd44680f7de4c55790c62f65aacee72fe0 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:41:34 +0330 Subject: [PATCH] perf(core): optimize bulk user URI generation in wrapper_uri.py --- core/cli.py | 2 +- core/scripts/hysteria2/wrapper_uri.py | 193 +++++++++++++++++++------- 2 files changed, 141 insertions(+), 54 deletions(-) diff --git a/core/cli.py b/core/cli.py index 223ba0e..e3bef53 100644 --- a/core/cli.py +++ b/core/cli.py @@ -160,7 +160,7 @@ def bulk_user_add(traffic_gb: float, expiration_days: int, count: int, prefix: s @click.option('--new-expiration-days', '-ne', required=False, help='Expiration days for the new user', type=int) @click.option('--renew-password', '-rp', is_flag=True, help='Renew password for the user') @click.option('--renew-creation-date', '-rc', is_flag=True, help='Renew creation date for the user') -@click.option('--blocked/--unblocked', 'blocked', default=None, help='Block or unblock the user.') +@click.option('--blocked/--unblocked', 'blocked', '-b', default=None, help='Block or unblock the user.') @click.option('--unlimited-ip/--limited-ip', 'unlimited_ip', default=None, help='Set user to be exempt from or subject to IP limits.') def edit_user(username: str, new_username: str, new_traffic_limit: int, new_expiration_days: int, renew_password: bool, renew_creation_date: bool, blocked: bool | None, unlimited_ip: bool | None): try: diff --git a/core/scripts/hysteria2/wrapper_uri.py b/core/scripts/hysteria2/wrapper_uri.py index 86ae88b..44bfb4f 100644 --- a/core/scripts/hysteria2/wrapper_uri.py +++ b/core/scripts/hysteria2/wrapper_uri.py @@ -1,65 +1,152 @@ -import subprocess -import concurrent.futures -import re -import json +#!/usr/bin/env python3 + +import os import sys +import json +import argparse +from functools import lru_cache +from typing import Dict, List, Any from init_paths import * from paths import * -DEFAULT_ARGS = ["-a", "-n", "-s"] - -def run_show_uri(username): +@lru_cache(maxsize=None) +def load_json_file(file_path: str) -> Any: + if not os.path.exists(file_path): + return None try: - cmd = ["python3", CLI_PATH, "show-user-uri", "-u", username] + DEFAULT_ARGS - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - output = result.stdout - if "Invalid username" in output: - return {"username": username, "error": "User not found"} - return parse_output(username, output) - except subprocess.CalledProcessError as e: - return {"username": username, "error": e.stderr.strip()} + with open(file_path, 'r') as f: + content = f.read() + return json.loads(content) if content else None + except (json.JSONDecodeError, IOError): + return None -def parse_output(username, output): - ipv4 = None - ipv6 = None - normal_sub = None - nodes = [] +@lru_cache(maxsize=None) +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: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + env_vars[key] = value.strip() + return env_vars - ipv4_match = re.search(r"IPv4:\s*(hy2://[^\s]+)", output) - ipv6_match = re.search(r"IPv6:\s*(hy2://[^\s]+)", output) - normal_sub_match = re.search(r"Normal-SUB Sublink:\s*(https?://[^\s]+)", output) - - if ipv4_match: - ipv4 = ipv4_match.group(1) - if ipv6_match: - ipv6 = ipv6_match.group(1) - if normal_sub_match: - normal_sub = normal_sub_match.group(1) - - node_matches = re.findall(r"Node: (.+?) \(IPv[46]\):\s*(hy2://[^\s]+)", output) - for name, uri in node_matches: - nodes.append({"name": name.strip(), "uri": uri}) - - - return { - "username": username, - "ipv4": ipv4, - "ipv6": ipv6, - "nodes": nodes, - "normal_sub": normal_sub +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: + 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()]) + return f"{uri_base}?{query_string}#{fragment_tag}" -def batch_show_uri(usernames, max_workers=20): - with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: - results = list(executor.map(run_show_uri, usernames)) - return results - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: python3 show_uri_json.py user1 user2 ...") +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) sys.exit(1) - usernames = sys.argv[1:] - output_list = batch_show_uri(usernames) - print(json.dumps(output_list, indent=2)) \ No newline at end of file + 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) + 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') + + results = [] + + for username in target_usernames: + user_data = all_users.get(username) + if not user_data or "password" not in user_data: + results.append({"username": username, "error": "User not found or password not set"}) + continue + + auth_password = user_data["password"] + 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" + ) + + if ip6 and ip6 != "None": + user_output["ipv6"] = generate_uri( + username, auth_password, ip6, port, obfs_password, sha256, sni, 6, insecure, 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 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}" + + results.append(user_output) + + return results + +def main(): + parser = argparse.ArgumentParser( + description="Efficiently generate Hysteria2 URIs for multiple users.", + formatter_class=argparse.RawTextHelpFormatter + ) + 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: + target_usernames = list(all_users.keys()) + else: + print("Error: Could not load users.json to process all users.", file=sys.stderr) + sys.exit(1) + + if not target_usernames: + parser.print_help() + sys.exit(1) + + output_list = process_users(target_usernames) + print(json.dumps(output_list, indent=2)) + +if __name__ == "__main__": + main() \ No newline at end of file