feat(nl): token-based API support for 3x-ui 3.8.5 (api_token)
- add api_token field handling for NL (new panel uses Bearer token, not cookie) - modern clients API: POST /panel/api/clients/add and bulkDel via token - fetch sub_ids via token, support find/traffic via token - fixes create/delete for NL where py3xui login fails due to CSRF
This commit is contained in:
+140
@@ -471,8 +471,24 @@ def get_traffic_stats(sub_id: str) -> dict:
|
||||
api_host = inbound.get("api_host")
|
||||
api_user = inbound.get("api_user")
|
||||
api_pass = inbound.get("api_pass")
|
||||
api_token = inbound.get("api_token")
|
||||
inbound_id = inbound.get("id")
|
||||
|
||||
if api_token:
|
||||
# token-based traffic: try modern API, fallback to 0
|
||||
try:
|
||||
import httpx as _httpx
|
||||
with _httpx.Client(verify=False, timeout=10.0) as c:
|
||||
r = c.get(f"{api_host.rstrip('/')}/panel/api/clients/list", headers=_token_headers(api_token))
|
||||
data = r.json()
|
||||
for cl in data.get("obj", []):
|
||||
if (cl.get("subId") or cl.get("sub_id")) == sub_id and inbound_id in (cl.get("inboundIds") or []):
|
||||
srv_up += cl.get("up", 0) or 0
|
||||
srv_down += cl.get("down", 0) or 0
|
||||
except:
|
||||
pass
|
||||
continue
|
||||
|
||||
if not all([api_host, api_user, api_pass, inbound_id]):
|
||||
continue
|
||||
|
||||
@@ -579,8 +595,29 @@ def find_user_on_servers(sub_id: str) -> Optional[str]:
|
||||
api_host = inbound.get("api_host")
|
||||
api_user = inbound.get("api_user")
|
||||
api_pass = inbound.get("api_pass")
|
||||
api_token = inbound.get("api_token")
|
||||
inbound_id = inbound.get("id")
|
||||
|
||||
if api_token:
|
||||
try:
|
||||
import httpx as _httpx
|
||||
with _httpx.Client(verify=False, timeout=10.0) as c:
|
||||
r = c.get(f"{api_host.rstrip('/')}/panel/api/clients/list", headers=_token_headers(api_token))
|
||||
data = r.json()
|
||||
for cl in data.get("obj", []):
|
||||
sid = cl.get("subId") or cl.get("sub_id") or ""
|
||||
if sid == sub_id and inbound_id in (cl.get("inboundIds") or []):
|
||||
email = cl.get("email") or ""
|
||||
if '@' in email:
|
||||
username = email.split('@')[0]
|
||||
if '_' in username:
|
||||
username = username.split('_')[0]
|
||||
logger.info(f"Migrated user: {username} (found on {srv['name']}/{inbound['name']} via token)")
|
||||
return username
|
||||
except Exception as e:
|
||||
logger.warning(f"Skipping unavailable server {srv['name']}/{inbound['name']} (token): {e}")
|
||||
continue
|
||||
|
||||
if not all([api_host, api_user, api_pass, inbound_id]):
|
||||
continue
|
||||
|
||||
@@ -1471,12 +1508,98 @@ def _hysteria_update_inbound(api_host: str, api_user: str, api_pass: str, inboun
|
||||
"tag": raw_obj.get("tag", "")
|
||||
})
|
||||
|
||||
def _token_headers(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
def _fetch_via_token(api_host: str, api_token: str, inbound_id: int) -> set:
|
||||
try:
|
||||
import httpx as _httpx
|
||||
url = f"{api_host.rstrip('/')}/panel/api/clients/list"
|
||||
with _httpx.Client(verify=False, timeout=15.0) as c:
|
||||
r = c.get(url, headers=_token_headers(api_token))
|
||||
data = r.json()
|
||||
if not data.get("success"):
|
||||
return set()
|
||||
ids = set()
|
||||
for cl in data.get("obj", []):
|
||||
if inbound_id in (cl.get("inboundIds") or []):
|
||||
sid = cl.get("subId") or cl.get("sub_id") or ""
|
||||
if sid:
|
||||
ids.add(sid)
|
||||
return ids
|
||||
except Exception as e:
|
||||
logger.warning(f"token fetch failed {api_host}: {e}")
|
||||
return set()
|
||||
|
||||
def _create_via_token(api_host: str, api_token: str, inbound: dict, username: str, sub_id: str, traffic_gb: int) -> dict:
|
||||
try:
|
||||
import httpx as _httpx
|
||||
inbound_id = inbound.get("id")
|
||||
inbound_name = inbound.get("name", "default")
|
||||
url = f"{api_host.rstrip('/')}/panel/api/clients/add"
|
||||
with _httpx.Client(verify=False, timeout=15.0) as c:
|
||||
r = c.get(f"{api_host.rstrip('/')}/panel/api/clients/list", headers=_token_headers(api_token))
|
||||
data = r.json()
|
||||
for cl in data.get("obj", []):
|
||||
if cl.get("subId") == sub_id and inbound_id in (cl.get("inboundIds") or []):
|
||||
return {"success": True, "email": cl.get("email", "")}
|
||||
client_uuid = str(uuid.uuid4())
|
||||
email = f"{username}_{inbound_name}@vless.local"
|
||||
payload = {
|
||||
"client": {
|
||||
"email": email,
|
||||
"id": client_uuid,
|
||||
"subId": sub_id,
|
||||
"enable": True,
|
||||
"limitIp": 0,
|
||||
"totalGB": traffic_gb * 1073741824 if traffic_gb > 0 else 0,
|
||||
"expiryTime": 0,
|
||||
"tgId": 0
|
||||
},
|
||||
"inboundIds": [inbound_id]
|
||||
}
|
||||
r2 = c.post(url, json=payload, headers=_token_headers(api_token))
|
||||
j = r2.json()
|
||||
if j.get("success"):
|
||||
return {"success": True, "email": email}
|
||||
return {"success": False, "error": j.get("msg", str(j))[:150]}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)[:150]}
|
||||
|
||||
def _delete_via_token(api_host: str, api_token: str, sub_id: str) -> dict:
|
||||
try:
|
||||
import httpx as _httpx
|
||||
with _httpx.Client(verify=False, timeout=15.0) as c:
|
||||
# find email by subId
|
||||
r = c.get(f"{api_host.rstrip('/')}/panel/api/clients/list", headers=_token_headers(api_token))
|
||||
data = r.json()
|
||||
emails = []
|
||||
for cl in data.get("obj", []):
|
||||
if (cl.get("subId") or cl.get("sub_id")) == sub_id:
|
||||
emails.append(cl.get("email"))
|
||||
if not emails:
|
||||
return {"success": True, "error": "not_found"}
|
||||
url = f"{api_host.rstrip('/')}/panel/api/clients/bulkDel"
|
||||
r2 = c.post(url, json={"emails": emails}, headers=_token_headers(api_token))
|
||||
j = r2.json()
|
||||
if j.get("success"):
|
||||
return {"success": True}
|
||||
return {"success": False, "error": j.get("msg", str(j))[:100]}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)[:100]}
|
||||
|
||||
def create_3xui_client(username: str, sub_id: str, inbound: dict, traffic_gb: int = 0) -> dict:
|
||||
api_host = inbound.get("api_host")
|
||||
api_user = inbound.get("api_user")
|
||||
api_pass = inbound.get("api_pass")
|
||||
api_token = inbound.get("api_token")
|
||||
inbound_id = inbound.get("id")
|
||||
|
||||
if api_token:
|
||||
if not all([api_host, api_token, inbound_id]):
|
||||
return {"success": False, "error": "missing_credentials"}
|
||||
return _create_via_token(api_host, api_token, inbound, username, sub_id, traffic_gb)
|
||||
|
||||
if not all([api_host, api_user, api_pass, inbound_id]):
|
||||
return {"success": False, "error": "missing_credentials"}
|
||||
|
||||
@@ -1570,8 +1693,14 @@ def delete_3xui_client(username: str, sub_id: str, inbound: dict) -> dict:
|
||||
api_host = inbound.get("api_host")
|
||||
api_user = inbound.get("api_user")
|
||||
api_pass = inbound.get("api_pass")
|
||||
api_token = inbound.get("api_token")
|
||||
inbound_id = inbound.get("id")
|
||||
|
||||
if api_token:
|
||||
if not all([api_host, api_token, inbound_id]):
|
||||
return {"success": False, "error": "missing_credentials"}
|
||||
return _delete_via_token(api_host, api_token, sub_id)
|
||||
|
||||
if not all([api_host, api_user, api_pass, inbound_id]):
|
||||
return {"success": False, "error": "missing_credentials"}
|
||||
|
||||
@@ -1622,8 +1751,19 @@ def fetch_server_sub_ids(srv: dict) -> dict:
|
||||
api_host = inbound.get("api_host")
|
||||
api_user = inbound.get("api_user")
|
||||
api_pass = inbound.get("api_pass")
|
||||
api_token = inbound.get("api_token")
|
||||
inbound_id = inbound.get("id")
|
||||
ib_name = inbound.get("name", "?")
|
||||
if api_token:
|
||||
if not all([api_host, api_token, inbound_id]):
|
||||
continue
|
||||
try:
|
||||
fetched = _fetch_via_token(api_host, api_token, inbound_id)
|
||||
result[inbound_id] = fetched
|
||||
logger.info(f"fetch_server_sub_ids {srv_name}/{ib_name}: got {len(fetched)} sub_ids (token)")
|
||||
except Exception as e:
|
||||
logger.warning(f"fetch_server_sub_ids {srv_name}/{ib_name} token error: {e}")
|
||||
continue
|
||||
if not all([api_host, api_user, api_pass, inbound_id]):
|
||||
continue
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user