feat: add Nox-API upstream support
This commit is contained in:
@@ -90,6 +90,11 @@ def _curate_candidates(
|
||||
"""Scan extracted data for likely credentials with confidence scoring."""
|
||||
candidates: list[dict[str, Any]] = []
|
||||
|
||||
if not new_api_user:
|
||||
new_api_user = _find_user_header(auth_headers)
|
||||
if not new_api_user:
|
||||
new_api_user = _find_new_api_user(local_storage, session_storage)
|
||||
|
||||
# 0. 完整 Cookie Bundle(最高优先级)
|
||||
# 按页面 origin 收集所有相关 cookie,包含 cf_clearance 等 Cloudflare cookie
|
||||
cookie_string, cookie_names = _build_cookie_bundle(cookies, page_url)
|
||||
@@ -100,6 +105,7 @@ def _curate_candidates(
|
||||
}
|
||||
if new_api_user:
|
||||
bundle_extra["new_api_user"] = new_api_user
|
||||
bundle_extra["user_id"] = new_api_user
|
||||
_add(
|
||||
candidates, "cookie_bundle",
|
||||
f"bundle:{page_url[:60]}",
|
||||
@@ -113,18 +119,21 @@ def _curate_candidates(
|
||||
# 1. CDP-captured network headers (high confidence)
|
||||
seen = set()
|
||||
for h in auth_headers:
|
||||
htype = h.get("type", "authorization")
|
||||
if htype == "user_id":
|
||||
continue
|
||||
dedup_key = h["value"]
|
||||
if dedup_key in seen:
|
||||
continue
|
||||
seen.add(dedup_key)
|
||||
htype = h.get("type", "authorization")
|
||||
preview = _preview(h["value"])
|
||||
if htype == "api_key":
|
||||
_add(candidates, "api_key", f"network:{h['url'][:60]}", h["value"], preview,
|
||||
f"X-API-Key — {h['url'][:40]}", 95)
|
||||
else:
|
||||
_add(candidates, "bearer_token", f"network:{h['url'][:60]}", h["value"], preview,
|
||||
f"Authorization — {h['url'][:40]}", 95)
|
||||
f"Authorization — {h['url'][:40]}", 95,
|
||||
extra={"new_api_user": new_api_user, "user_id": new_api_user} if new_api_user else None)
|
||||
|
||||
# 2. localStorage/sessionStorage items
|
||||
for store_name, store in [("localStorage", local_storage), ("sessionStorage", session_storage)]:
|
||||
@@ -155,9 +164,6 @@ def _curate_candidates(
|
||||
_add(candidates, "bearer_token", f"{store_name}.{key}", val, _preview(val),
|
||||
f"{store_name}.{key} (API Key)", 90)
|
||||
|
||||
if not new_api_user:
|
||||
new_api_user = _find_new_api_user(local_storage, session_storage)
|
||||
|
||||
# 3. 单个 Session cookie(保留,供独立 fallback / bearer 降级使用)
|
||||
for c in cookies:
|
||||
cname = c["name"].lower()
|
||||
@@ -168,6 +174,7 @@ def _curate_candidates(
|
||||
extra = {"cookie_name": c["name"], "cookie_value": c["value"]}
|
||||
if cname == "session" and new_api_user:
|
||||
extra["new_api_user"] = new_api_user
|
||||
extra["user_id"] = new_api_user
|
||||
_add(candidates, "cookie", f"cookie:{c['name']}", cookie_val, preview,
|
||||
f"Cookie {c['name']} ({c['domain']})", confidence,
|
||||
extra=extra)
|
||||
@@ -182,6 +189,15 @@ def _curate_candidates(
|
||||
return candidates
|
||||
|
||||
|
||||
def _find_user_header(headers: list[dict[str, str]]) -> str:
|
||||
for header in headers:
|
||||
if header.get("type") == "user_id":
|
||||
value = str(header.get("value") or "").strip()
|
||||
if value:
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
def _find_storage_value(*stores: dict[str, str], key: str) -> str:
|
||||
for store in stores:
|
||||
value = store.get(key)
|
||||
|
||||
@@ -80,6 +80,7 @@ def _normalize_headers(value: Any) -> list[dict[str, str]]:
|
||||
"type": str(item.get("type") or "authorization"),
|
||||
"value": header_value,
|
||||
"url": str(item.get("url") or ""),
|
||||
"header": str(item.get("header") or ""),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@@ -137,6 +137,8 @@ def _unwrap_list(value: Any) -> Optional[list[dict[str, Any]]]:
|
||||
|
||||
if isinstance(value, list):
|
||||
return _normalize(value)
|
||||
if isinstance(value, dict) and not _is_success_response(value):
|
||||
raise UpstreamError(_response_message(value, "upstream API returned success=false"))
|
||||
if isinstance(value, dict):
|
||||
for key in ("data", "items", "groups", "available_groups", "availableGroups"):
|
||||
nested = value.get(key)
|
||||
@@ -145,8 +147,11 @@ def _unwrap_list(value: Any) -> Optional[list[dict[str, Any]]]:
|
||||
elif isinstance(nested, dict):
|
||||
# Handle /api/user/self/groups where data is a dict of group_name -> { desc, ratio }
|
||||
out = []
|
||||
for k in nested.keys():
|
||||
out.append({"id": k, "name": k})
|
||||
for k, item in nested.items():
|
||||
row = {"id": k, "name": k}
|
||||
if isinstance(item, dict):
|
||||
row.update(item)
|
||||
out.append(row)
|
||||
return out
|
||||
return None
|
||||
|
||||
@@ -316,9 +321,16 @@ class UpstreamClient:
|
||||
bool(self.auth_config.get("new_api_user"))
|
||||
or login_path == "/api/user/login"
|
||||
or self.auth_type == "cookie"
|
||||
or self.auth_type == "nox_token"
|
||||
)
|
||||
)
|
||||
|
||||
def _user_header_value(self) -> str:
|
||||
return _clean_auth_header_value(
|
||||
self.auth_config.get("user_id", "") or self.auth_config.get("new_api_user", ""),
|
||||
"User header",
|
||||
)
|
||||
|
||||
def _headers(self, auth: bool = True) -> dict[str, str]:
|
||||
headers: dict[str, str] = {
|
||||
"Accept": "application/json",
|
||||
@@ -330,6 +342,13 @@ class UpstreamClient:
|
||||
token = _clean_auth_header_value(self.auth_config.get("token", ""), "Bearer token")
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
elif self.auth_type == "nox_token":
|
||||
token = _clean_auth_header_value(self.auth_config.get("token", ""), "Nox access token")
|
||||
user_id = self._user_header_value()
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
if user_id:
|
||||
headers["Nox-Api-User"] = user_id
|
||||
elif self.auth_type == "api_key":
|
||||
key = _clean_auth_header_value(self.auth_config.get("key", ""), "API key")
|
||||
header = self.auth_config.get("header", "Authorization")
|
||||
@@ -339,20 +358,24 @@ class UpstreamClient:
|
||||
cookie_str = _clean_auth_header_value(self.auth_config.get("cookie_string", ""), "Cookie")
|
||||
if cookie_str:
|
||||
headers["Cookie"] = cookie_str
|
||||
new_api_user = _clean_auth_header_value(self.auth_config.get("new_api_user", ""), "New-Api-User")
|
||||
if new_api_user:
|
||||
headers["New-Api-User"] = new_api_user
|
||||
user_id = self._user_header_value()
|
||||
if user_id:
|
||||
headers["New-Api-User"] = user_id
|
||||
headers["Nox-Api-User"] = user_id
|
||||
elif self.auth_type == "login_password" and self._token:
|
||||
token = _clean_auth_header_value(self._token, "Login token")
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
if self.auth_type == "login_password" and self._new_api_user:
|
||||
headers["New-Api-User"] = self._new_api_user
|
||||
headers["Nox-Api-User"] = self._new_api_user
|
||||
return headers
|
||||
|
||||
def _request(self, method: str, path: str, body: Any = None, auth: bool = True) -> Any:
|
||||
if auth and self.auth_type == "cookie" and "user/self" in path and not self.auth_config.get("new_api_user"):
|
||||
raise UpstreamError("New-API user endpoint requires New-Api-User; re-extract the session cookie after login and save the upstream")
|
||||
if auth and self.auth_type == "nox_token" and "user/self" in path and not self._user_header_value():
|
||||
raise UpstreamError("Nox-API endpoint requires Nox-Api-User; please fill user id and retry")
|
||||
url = self._url(path)
|
||||
if body is not None:
|
||||
resp = self._client.request(
|
||||
|
||||
Reference in New Issue
Block a user