Show WireGuard QR, full .conf and ZIP download after 3x-ui client create
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+126
-11
@@ -2,14 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
from typing import Any
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
import httpx
|
||||
|
||||
from app.services.crypto import public_key_from_private, render_xui_wireguard_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SUPPORTED_CLIENT_PROTOCOLS = ("vless", "wireguard")
|
||||
@@ -26,6 +29,100 @@ def _random_sub_id(length: int = 16) -> str:
|
||||
return "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
def _as_dict(value: Any) -> dict:
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, str) and value.strip():
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_client_allowed_ips(details: Any, email: str, inbound: dict) -> list[str]:
|
||||
allowed: list[str] = []
|
||||
if isinstance(details, dict):
|
||||
allowed = details.get("allowedIPs") or details.get("allowed_ips") or []
|
||||
if not allowed:
|
||||
for key in ("client", "obj"):
|
||||
nested = details.get(key)
|
||||
if isinstance(nested, dict) and nested.get("allowedIPs"):
|
||||
allowed = nested["allowedIPs"]
|
||||
break
|
||||
if allowed:
|
||||
return [str(x) for x in allowed if x]
|
||||
|
||||
settings = _as_dict(inbound.get("settings"))
|
||||
for c in settings.get("clients") or []:
|
||||
if not isinstance(c, dict):
|
||||
continue
|
||||
if str(c.get("email") or "") == email:
|
||||
return [str(x) for x in (c.get("allowedIPs") or []) if x]
|
||||
return []
|
||||
|
||||
|
||||
def build_wireguard_client_bundle(
|
||||
*,
|
||||
panel_base_url: str,
|
||||
inbound: dict,
|
||||
private_key: str,
|
||||
public_key: str,
|
||||
email: str,
|
||||
allowed_ips: list[str],
|
||||
remark: str = "",
|
||||
) -> dict[str, Any]:
|
||||
settings = _as_dict(inbound.get("settings"))
|
||||
secret_key = str(settings.get("secretKey") or "").strip()
|
||||
if not secret_key:
|
||||
raise XuiApiError("У inbound WireGuard нет secretKey — не удалось собрать конфиг")
|
||||
server_public_key = public_key_from_private(secret_key)
|
||||
dns = str(settings.get("dns") or "").strip() or "1.1.1.1, 1.0.0.1"
|
||||
mtu = int(settings.get("mtu") or 1420)
|
||||
port = int(inbound.get("port") or 0)
|
||||
host = urlparse(panel_base_url).hostname or ""
|
||||
listen = str(inbound.get("listen") or "").strip()
|
||||
if listen and listen not in {"0.0.0.0", "::", ""}:
|
||||
host = listen
|
||||
if not host or not port:
|
||||
raise XuiApiError("Не удалось определить Endpoint (host:port) для WireGuard")
|
||||
endpoint = f"{host}:{port}"
|
||||
|
||||
address = ""
|
||||
for item in allowed_ips:
|
||||
s = str(item).strip()
|
||||
if s and not s.startswith("0.0.0.0") and s not in {"::/0", "::"}:
|
||||
address = s if "/" in s else f"{s}/32"
|
||||
break
|
||||
if not address and allowed_ips:
|
||||
address = str(allowed_ips[0])
|
||||
if not address:
|
||||
raise XuiApiError("3x-ui не выделил Address (allowedIPs) для клиента")
|
||||
|
||||
peer_remark = remark or str(inbound.get("remark") or email)
|
||||
config_text = render_xui_wireguard_config(
|
||||
private_key=private_key,
|
||||
address=address,
|
||||
dns=dns,
|
||||
mtu=mtu,
|
||||
server_public_key=server_public_key,
|
||||
endpoint=endpoint,
|
||||
remark=peer_remark,
|
||||
)
|
||||
return {
|
||||
"address": address,
|
||||
"dns": dns,
|
||||
"mtu": mtu,
|
||||
"endpoint": endpoint,
|
||||
"server_public_key": server_public_key,
|
||||
"config_text": config_text,
|
||||
"allowedIPs": allowed_ips,
|
||||
"privateKey": private_key,
|
||||
"publicKey": public_key,
|
||||
}
|
||||
|
||||
|
||||
class XuiClient:
|
||||
"""
|
||||
Client for MHSanaei/3x-ui REST API (v3.5.0).
|
||||
@@ -313,6 +410,7 @@ class XuiClient:
|
||||
expiry_time: int = 0,
|
||||
limit_ip: int = 0,
|
||||
comment: str = "",
|
||||
endpoint_host: str | None = None,
|
||||
) -> dict:
|
||||
inbound = await self.get_inbound(inbound_id)
|
||||
proto = str(inbound.get("protocol") or "").lower()
|
||||
@@ -339,16 +437,27 @@ class XuiClient:
|
||||
}
|
||||
await self.add_client_raw(client, [inbound_id])
|
||||
details = await self.get_client(email)
|
||||
allowed_ips = []
|
||||
if isinstance(details, dict):
|
||||
allowed_ips = details.get("allowedIPs") or details.get("allowed_ips") or []
|
||||
if not allowed_ips:
|
||||
# Client may be nested under inbounds / clients list
|
||||
for key in ("client", "obj"):
|
||||
nested = details.get(key)
|
||||
if isinstance(nested, dict) and nested.get("allowedIPs"):
|
||||
allowed_ips = nested["allowedIPs"]
|
||||
break
|
||||
# Re-fetch inbound so allocated allowedIPs are present in settings.clients
|
||||
inbound = await self.get_inbound(inbound_id)
|
||||
allowed_ips = _extract_client_allowed_ips(details, email, inbound)
|
||||
|
||||
base = endpoint_host or self.base_url
|
||||
# If endpoint_host is a bare hostname, wrap as URL for urlparse
|
||||
if endpoint_host and "://" not in endpoint_host:
|
||||
panel_url = f"https://{endpoint_host}"
|
||||
else:
|
||||
panel_url = base if "://" in str(base) else f"https://{base}"
|
||||
|
||||
remark = str(inbound.get("remark") or email)
|
||||
bundle = build_wireguard_client_bundle(
|
||||
panel_base_url=panel_url,
|
||||
inbound=inbound,
|
||||
private_key=keys["privateKey"],
|
||||
public_key=keys["publicKey"],
|
||||
email=email,
|
||||
allowed_ips=allowed_ips,
|
||||
remark=remark,
|
||||
)
|
||||
return {
|
||||
"protocol": "wireguard",
|
||||
"email": email,
|
||||
@@ -356,6 +465,12 @@ class XuiClient:
|
||||
"privateKey": keys["privateKey"],
|
||||
"publicKey": keys["publicKey"],
|
||||
"allowedIPs": allowed_ips,
|
||||
"address": bundle["address"],
|
||||
"dns": bundle["dns"],
|
||||
"mtu": bundle["mtu"],
|
||||
"endpoint": bundle["endpoint"],
|
||||
"server_public_key": bundle["server_public_key"],
|
||||
"config_text": bundle["config_text"],
|
||||
"expiryTime": int(expiry_time),
|
||||
"details": details,
|
||||
"inbound": {
|
||||
|
||||
Reference in New Issue
Block a user