feat: working hysteria2 via standalone server with enriched links
- add _build_hysteria_link: rebuild bare panel hy2 links into hysteria2://sub_id:auth@host:port?obfs=salamander&obfs-password=..\&sni=.. using per-server hy2 block in servers.conf (gitignored) - add _sync_hysteria_users hook: push panel hy2 clients to the standalone server config on create/delete (cron backstop every 2 min)
This commit is contained in:
@@ -29,6 +29,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
import uvicorn
|
||||
import httpx
|
||||
import re
|
||||
import urllib.parse
|
||||
from py3xui import Api
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
@@ -509,6 +510,39 @@ def _link_protocol(link: str) -> str:
|
||||
def _get_inbound_protocol(inbound: dict) -> str:
|
||||
return inbound.get("protocol", "vless")
|
||||
|
||||
def _build_hysteria_link(panel_link: str, sub_id: str, srv: dict) -> Optional[str]:
|
||||
"""Rebuild panel hysteria2://AUTH@host:port link into a working client link.
|
||||
|
||||
Standalone hysteria2 server uses userpass auth (username=sub_id) plus
|
||||
salamander obfs, so the bare panel link is extended to:
|
||||
hysteria2://sub_id:auth@host:port?obfs=salamander&obfs-password=..&sni=..
|
||||
Server-side params come from the optional "hy2" block in servers.conf:
|
||||
{"sni": "example.com", "obfs_password": "..."}.
|
||||
Returns the original link when no hy2 block is configured, None on parse error.
|
||||
"""
|
||||
hy2 = srv.get("hy2") or {}
|
||||
if not hy2.get("obfs_password"):
|
||||
return panel_link
|
||||
try:
|
||||
parts = urllib.parse.urlparse(panel_link.split("#")[0])
|
||||
auth = urllib.parse.unquote(parts.username or "")
|
||||
if not auth:
|
||||
return None
|
||||
host = parts.hostname or ""
|
||||
port = parts.port or 443
|
||||
if not host:
|
||||
return None
|
||||
sni = hy2.get("sni") or host
|
||||
query = urllib.parse.urlencode({
|
||||
"obfs": "salamander",
|
||||
"obfs-password": hy2["obfs_password"],
|
||||
"sni": sni,
|
||||
})
|
||||
userinfo = f"{urllib.parse.quote(sub_id, safe='')}:{urllib.parse.quote(auth, safe='')}"
|
||||
return f"hysteria2://{userinfo}@{host}:{port}?{query}"
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def fetch_vless_links(url: str) -> List[str]:
|
||||
try:
|
||||
async with httpx.AsyncClient(verify=False, timeout=10.0, follow_redirects=True) as client:
|
||||
@@ -938,6 +972,8 @@ async def get_subscription(request: Request, subscription_id: str, format: str =
|
||||
continue
|
||||
|
||||
link = matched_link
|
||||
if ib_protocol == "hysteria":
|
||||
link = _build_hysteria_link(link, subscription_id, srv) or link
|
||||
clean_link = link.split('#')[0]
|
||||
if clean_link in seen_links:
|
||||
continue
|
||||
@@ -1403,6 +1439,15 @@ def _hysteria_api_call(api_host: str, api_user: str, api_pass: str, endpoint: st
|
||||
except Exception as e:
|
||||
return {"success": False, "msg": str(e)[:100]}
|
||||
|
||||
def _sync_hysteria_users():
|
||||
"""Push panel hysteria clients into standalone hysteria2 server config (best-effort)."""
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.run(["/usr/bin/python3", "/opt/hysteria/sync_users.py"],
|
||||
capture_output=True, timeout=60)
|
||||
except Exception as e:
|
||||
logger.warning(f"hysteria users sync failed: {e}")
|
||||
|
||||
def _hysteria_get_inbound(api_host: str, api_user: str, api_pass: str, inbound_id: int) -> dict:
|
||||
result = _hysteria_api_call(api_host, api_user, api_pass, "panel/api/inbounds/list")
|
||||
for ib in result.get("obj", []):
|
||||
@@ -1467,6 +1512,7 @@ def create_3xui_client(username: str, sub_id: str, inbound: dict, traffic_gb: in
|
||||
result = _hysteria_update_inbound(api_host, api_user, api_pass, inbound_id, json.dumps(settings), ib_data)
|
||||
if result.get("success"):
|
||||
logger.info(f"Client created successfully on {inbound_name} (hysteria)")
|
||||
_sync_hysteria_users()
|
||||
return {"success": True, "email": email}
|
||||
else:
|
||||
error_msg = result.get("msg", "unknown error")
|
||||
@@ -1546,6 +1592,7 @@ def delete_3xui_client(username: str, sub_id: str, inbound: dict) -> dict:
|
||||
result = _hysteria_update_inbound(api_host, api_user, api_pass, inbound_id, json.dumps(settings), ib_data)
|
||||
if result.get("success"):
|
||||
logger.info(f"Deleted client from {inbound_name} (hysteria)")
|
||||
_sync_hysteria_users()
|
||||
return {"success": True}
|
||||
return {"success": False, "error": result.get("msg", "unknown")[:100]}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user