test_refresh_extend.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. """Experiment: does getByRefresh extend the token's validity?
  2. Method:
  3. 1. Decode current JWT payload (iat/exp/refresh_exp) BEFORE the call
  4. 2. Call POST user-api.996hezi.com/v1/UserInfo/getByRefresh with the token
  5. 3. Dump the FULL raw response (every key — last time we only looked at nick/id)
  6. 4. Decode JWT AFTER — compare; look for ANY new token string anywhere
  7. in the response (data.token / data.userinfo.token / set-cookie header etc.)
  8. """
  9. import asyncio
  10. import base64
  11. import json
  12. import sys
  13. from datetime import datetime, timezone
  14. from pathlib import Path
  15. _HERE = Path(__file__).resolve().parent
  16. sys.path.insert(0, str(_HERE.parent / "src"))
  17. from cq2_sdk.env_loader import load_env
  18. load_env(_HERE.parent / ".env")
  19. from cq2_sdk.login import Box996Login, ManualGeetestSolver
  20. from cq2_sdk.store import UserStore, connection
  21. def decode_jwt(token: str) -> dict:
  22. parts = token.split(".")
  23. pad = parts[1] + "=" * (-len(parts[1]) % 4)
  24. return json.loads(base64.urlsafe_b64decode(pad))
  25. def show_life(payload: dict, label: str) -> None:
  26. def ts(k):
  27. v = payload.get(k)
  28. return f"{datetime.fromtimestamp(v, tz=timezone.utc).isoformat()[:16]}" if v else "-"
  29. print(f" [{label}] iat={ts('iat')} exp={ts('exp')} refresh_exp={ts('refresh_exp')}")
  30. def hunt_tokens(obj, path="") -> list:
  31. """Recursively find any long eyJ... strings anywhere in the response."""
  32. found = []
  33. if isinstance(obj, dict):
  34. for k, v in obj.items():
  35. found += hunt_tokens(v, f"{path}.{k}")
  36. elif isinstance(obj, list):
  37. for i, v in enumerate(obj):
  38. found += hunt_tokens(v, f"{path}[{i}]")
  39. elif isinstance(obj, str) and obj.startswith("eyJ") and len(obj) > 100:
  40. found.append((path, obj))
  41. return found
  42. async def main() -> int:
  43. with connection() as conn:
  44. u = UserStore(conn).get_by_username("longwu988")
  45. old_token = u.token
  46. print("=== 1) JWT BEFORE ===")
  47. p_before = decode_jwt(old_token)
  48. show_life(p_before, "before")
  49. print("\n=== 2) call getByRefresh ===")
  50. box = Box996Login(geetest=ManualGeetestSolver(), device_id="00:15:5d:0a:04:00")
  51. box.token = old_token
  52. r = await box._request(
  53. "POST", "https://user-api.996hezi.com", "/v1/UserInfo/getByRefresh"
  54. )
  55. print(f" code={r.get('code')} msg={r.get('msg', '')!r}")
  56. print("\n=== 3) FULL raw response (every key) ===")
  57. print(json.dumps(r, ensure_ascii=False, indent=2)[:3000])
  58. print("\n=== 4) token hunt: any NEW eyJ... anywhere in response? ===")
  59. hits = hunt_tokens(r)
  60. if not hits:
  61. print(" (none found)")
  62. new_token = None
  63. for path, tok in hits:
  64. is_new = tok != old_token
  65. print(f" {path}: len={len(tok)} NEW={is_new}")
  66. if is_new and ".eyJ" in tok:
  67. new_token = tok
  68. try:
  69. show_life(decode_jwt(tok), "candidate-new")
  70. except Exception as e:
  71. print(f" (decode failed: {e})")
  72. print("\n=== verdict ===")
  73. if new_token:
  74. p_after = decode_jwt(new_token)
  75. show_life(p_after, "after")
  76. gained = p_after.get("exp", 0) - p_before.get("exp", 0)
  77. print(f" NEW TOKEN issued! exp extended by {gained/86400:.1f} days")
  78. with connection() as conn:
  79. UserStore(conn).update_token(user_id=u.id, token=new_token)
  80. print(" → saved to MySQL")
  81. else:
  82. exp_left = (p_before.get("exp", 0) - 1789700000) / 86400 # vs ~now
  83. print(" getByRefresh does NOT issue a new token.")
  84. print(f" current token still valid until exp (above); plan: re-login via A2 before expiry.")
  85. return 0
  86. if __name__ == "__main__":
  87. sys.exit(asyncio.run(main()))