1
This commit is contained in:
parent
b067c21b08
commit
5a94ac454e
@ -1,5 +1,2 @@
|
|||||||
KS_ACCOUNT=大号#kpn=NEBULA; kpf=ANDROID_PHONE; userId=...; did=...; kuaishou.api_st=...; token=...; client_key=2ac2a76d#client_salt
|
ksck=大号#kpn=NEBULA; kpf=ANDROID_PHONE; userId=...; did=...; kuaishou.api_st=...; token=...; client_key=2ac2a76d#client_salt
|
||||||
KS_AD_COUNT=1
|
|
||||||
KS_TREASURE_AD_COUNT=1
|
|
||||||
KS_DELAY_SECONDS=1
|
|
||||||
KS_TIMEOUT=30
|
|
||||||
|
|||||||
53
main.py
53
main.py
@ -427,6 +427,22 @@ def replace_pair_values(pairs: list[tuple[str, str]], updates: dict[str, str]) -
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def new_api_sig3_state() -> Kwsg10418State | None:
|
||||||
|
"""一次 run 的一次冷启动:时间驱动 session_seed + 进程起始 counter。
|
||||||
|
|
||||||
|
真机每次冷启动 srand(time) 生成新 seed;counter 从进程已发生的
|
||||||
|
请求数附近起步(0x5F 为 sms_login 实测值),之后随请求递增。
|
||||||
|
设 ``KS_API_SIG3_FRESH_STATE=0`` 时返回 None,由调用方回退旧的
|
||||||
|
``from_digest(HAR_SEED)`` 行为,用于 A/B 对比验证服务端是否按签名降级。
|
||||||
|
"""
|
||||||
|
from core.sig3 import kwsg_10418_session_seed_from_time
|
||||||
|
|
||||||
|
if os.environ.get("KS_API_SIG3_FRESH_STATE", "1").strip().lower() in {"0", "false", "no"}:
|
||||||
|
return None # 由调用方回退 from_digest(HAR_SEED)
|
||||||
|
seed = kwsg_10418_session_seed_from_time(int(time.time()))
|
||||||
|
return Kwsg10418State(session_seed=seed, counter=0x5F)
|
||||||
|
|
||||||
|
|
||||||
def signed_api_url(
|
def signed_api_url(
|
||||||
path: str,
|
path: str,
|
||||||
params: dict[str, str],
|
params: dict[str, str],
|
||||||
@ -809,6 +825,7 @@ class KsNebulaClient:
|
|||||||
self._h5_kww_warned = False
|
self._h5_kww_warned = False
|
||||||
self._api_http_session: Any | None = None
|
self._api_http_session: Any | None = None
|
||||||
self._api_weapon_header_provider = WeaponProofProvider(API_HEADERS["kaw"])
|
self._api_weapon_header_provider = WeaponProofProvider(API_HEADERS["kaw"])
|
||||||
|
self.api_sig3_state: Kwsg10418State | None = None # 进程级共享 10418 state,run_all_once 里初始化
|
||||||
self.sign_in_event_tracking = SIGN_IN_EVENT_TRACKING
|
self.sign_in_event_tracking = SIGN_IN_EVENT_TRACKING
|
||||||
self.treasure_open_event_tracking = _compact_json(TREASURE_OPEN_EVENT_TRACKING)
|
self.treasure_open_event_tracking = _compact_json(TREASURE_OPEN_EVENT_TRACKING)
|
||||||
if self.memory_device:
|
if self.memory_device:
|
||||||
@ -1332,10 +1349,31 @@ class KsNebulaClient:
|
|||||||
)
|
)
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
def _resolve_api_sig3_state(
|
||||||
|
self,
|
||||||
|
state: Kwsg10418State | None,
|
||||||
|
seed: str,
|
||||||
|
) -> Kwsg10418State:
|
||||||
|
"""解析 10418 state:显式参数 > 进程级共享 > 新 seed > HAR 回退。
|
||||||
|
|
||||||
|
真机一次冷启动 = 一个共享 state,counter 随请求递增。这里在
|
||||||
|
``run_all_once`` 里初始化一次 ``self.api_sig3_state`` 后,所有
|
||||||
|
fetch/report/sign 共用它;显式 ``state`` 优先(测试用);新 seed
|
||||||
|
禁用(``KS_API_SIG3_FRESH_STATE=0``)时回退旧的 ``from_digest``。
|
||||||
|
"""
|
||||||
|
if state is not None:
|
||||||
|
return state
|
||||||
|
if self.api_sig3_state is None:
|
||||||
|
self.api_sig3_state = new_api_sig3_state()
|
||||||
|
if self.api_sig3_state is not None:
|
||||||
|
return self.api_sig3_state
|
||||||
|
return Kwsg10418State.from_digest(API_SIG3_SEED[seed])
|
||||||
|
|
||||||
def sign_in_api(self) -> ResponseRecord:
|
def sign_in_api(self) -> ResponseRecord:
|
||||||
path = "/rest/n/encourage/unionTask/signIn/report"
|
path = "/rest/n/encourage/unionTask/signIn/report"
|
||||||
body_pairs = [("eventTrackingLogInfo", "")] + self._api_body_common()
|
body_pairs = [("eventTrackingLogInfo", "")] + self._api_body_common()
|
||||||
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED["sign_in_report"])
|
state = self._resolve_api_sig3_state(None, "sign_in_report")
|
||||||
|
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED["sign_in_report"], state=state)
|
||||||
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
|
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
|
||||||
headers = dict(API_HEADERS)
|
headers = dict(API_HEADERS)
|
||||||
headers["Cookie"] = self._api_cookie_header()
|
headers["Cookie"] = self._api_cookie_header()
|
||||||
@ -1375,7 +1413,8 @@ class KsNebulaClient:
|
|||||||
def open_treasure_box_api(self) -> ResponseRecord:
|
def open_treasure_box_api(self) -> ResponseRecord:
|
||||||
path = "/rest/n/encourage/unionTask/treasureBox/report"
|
path = "/rest/n/encourage/unionTask/treasureBox/report"
|
||||||
body_pairs = [("layoutPage", "NEBULA_ACTIVITY"), ("eventTrackingLogInfo", "")] + self._api_body_common()
|
body_pairs = [("layoutPage", "NEBULA_ACTIVITY"), ("eventTrackingLogInfo", "")] + self._api_body_common()
|
||||||
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED["treasure_report"])
|
state = self._resolve_api_sig3_state(None, "treasure_report")
|
||||||
|
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED["treasure_report"], state=state)
|
||||||
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
|
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
|
||||||
headers = dict(API_HEADERS)
|
headers = dict(API_HEADERS)
|
||||||
headers["Cookie"] = self._api_cookie_header()
|
headers["Cookie"] = self._api_cookie_header()
|
||||||
@ -1398,7 +1437,7 @@ class KsNebulaClient:
|
|||||||
network_ip=os.environ.get("KS_AD_NETWORK_IP", "172.31.230.147"),
|
network_ip=os.environ.get("KS_AD_NETWORK_IP", "172.31.230.147"),
|
||||||
ipdx_ip=os.environ.get("KS_AD_IPDX_IP", "106.178.190.155"),
|
ipdx_ip=os.environ.get("KS_AD_IPDX_IP", "106.178.190.155"),
|
||||||
)
|
)
|
||||||
state = Kwsg10418State.from_digest(API_SIG3_SEED[f"{kind}_fetch"])
|
state = self._resolve_api_sig3_state(None, f"{kind}_fetch")
|
||||||
t1, t2 = enc_tables()
|
t1, t2 = enc_tables()
|
||||||
reward_body = build_reward_body(str_e, state, t1=t1, t2=t2)
|
reward_body = build_reward_body(str_e, state, t1=t1, t2=t2)
|
||||||
body_pairs = [("encData", reward_body.enc_data), ("sign", reward_body.sign)] + self._api_body_common()
|
body_pairs = [("encData", reward_body.enc_data), ("sign", reward_body.sign)] + self._api_body_common()
|
||||||
@ -1427,7 +1466,8 @@ class KsNebulaClient:
|
|||||||
first_pair = ("bizStr", biz_str) if mode == "bizstr" else ("encData", build_report_enc_data(biz_str.encode("utf-8")))
|
first_pair = ("bizStr", biz_str) if mode == "bizstr" else ("encData", build_report_enc_data(biz_str.encode("utf-8")))
|
||||||
body_pairs = [first_pair] + self._api_body_common()
|
body_pairs = [first_pair] + self._api_body_common()
|
||||||
path = "/rest/r/ad/task/report"
|
path = "/rest/r/ad/task/report"
|
||||||
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED[f"{kind}_report"])
|
state = self._resolve_api_sig3_state(None, f"{kind}_report")
|
||||||
|
url = signed_api_url(path, self._api_params(), body_pairs, self.client_salt, API_SIG3_SEED[f"{kind}_report"], state=state)
|
||||||
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
|
body = urllib.parse.urlencode(body_pairs).encode("utf-8")
|
||||||
headers = dict(API_HEADERS)
|
headers = dict(API_HEADERS)
|
||||||
headers["Cookie"] = self._api_cookie_header()
|
headers["Cookie"] = self._api_cookie_header()
|
||||||
@ -1492,6 +1532,11 @@ class KsNebulaClient:
|
|||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
record_path = out_dir / f"request_replay_{time.strftime('%Y%m%d_%H%M%S')}.jsonl"
|
record_path = out_dir / f"request_replay_{time.strftime('%Y%m%d_%H%M%S')}.jsonl"
|
||||||
|
|
||||||
|
# 一次 run = 一次冷启动 = 一个进程级共享 10418 state,
|
||||||
|
# counter 随整条广告/任务链路单调递增(真机行为)。
|
||||||
|
if self.api_sig3_state is None:
|
||||||
|
self.api_sig3_state = new_api_sig3_state()
|
||||||
|
|
||||||
def run_ad_action(kind: str) -> list[ResponseRecord]:
|
def run_ad_action(kind: str) -> list[ResponseRecord]:
|
||||||
if skip_ad_reward_leq:
|
if skip_ad_reward_leq:
|
||||||
return self.run_ad_sequence(kind, ad_wait, skip_reward_leq=skip_ad_reward_leq)
|
return self.run_ad_sequence(kind, ad_wait, skip_reward_leq=skip_ad_reward_leq)
|
||||||
|
|||||||
74
tests/test_api_sig3_state.py
Normal file
74
tests/test_api_sig3_state.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""main.py 广告/任务链路进程级 10418 state 的行为锁定。
|
||||||
|
|
||||||
|
真机一次冷启动 = 一个 Kwsg10418State(时间驱动 session_seed + counter
|
||||||
|
随请求单调递增)。这里锁定 main.py 的 ``new_api_sig3_state`` /
|
||||||
|
``_resolve_api_sig3_state`` 复刻该行为,防止未来回退成"每次 fetch 新建
|
||||||
|
state、counter 恒定 + 旧 HAR seed 重放"的可检测指纹。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from core.sig3 import Kwsg10418State, kwsg_10418_digest24_unmix
|
||||||
|
from main import API_SIG3_SEED, KsNebulaClient, new_api_sig3_state
|
||||||
|
|
||||||
|
|
||||||
|
class NewApiSig3StateTests(unittest.TestCase):
|
||||||
|
def test_signing_increments_counter_with_same_seed(self):
|
||||||
|
# 连签两次:counter 0x60→0x61,session_seed 不变(同一次冷启动)
|
||||||
|
with patch.dict(os.environ, {"KS_API_SIG3_FRESH_STATE": "1"}, clear=False):
|
||||||
|
state = new_api_sig3_state()
|
||||||
|
self.assertIsNotNone(state)
|
||||||
|
d1 = kwsg_10418_digest24_unmix(state.sig3_hex(b"req1"))
|
||||||
|
d2 = kwsg_10418_digest24_unmix(state.sig3_hex(b"req2"))
|
||||||
|
self.assertEqual(d1["session_seed"], d2["session_seed"])
|
||||||
|
self.assertEqual([d1["counter"], d2["counter"]], [0x60, 0x61])
|
||||||
|
|
||||||
|
def test_counter_starts_at_sms_login_value(self):
|
||||||
|
with patch.dict(os.environ, {"KS_API_SIG3_FRESH_STATE": "1"}, clear=False):
|
||||||
|
state = new_api_sig3_state()
|
||||||
|
self.assertEqual(state.counter, 0x5F) # 同 tools/sms_login_cli.py:_new_sig3_state
|
||||||
|
|
||||||
|
def test_disabled_returns_none(self):
|
||||||
|
# KS_API_SIG3_FRESH_STATE=0 → None,由调用方回退旧 from_digest(HAR_SEED)
|
||||||
|
with patch.dict(os.environ, {"KS_API_SIG3_FRESH_STATE": "0"}, clear=False):
|
||||||
|
self.assertIsNone(new_api_sig3_state())
|
||||||
|
|
||||||
|
|
||||||
|
class ResolveApiSig3StateTests(unittest.TestCase):
|
||||||
|
def _bare_client(self):
|
||||||
|
# 裸实例避免走重量级 __init__;_resolve 只读写 self.api_sig3_state
|
||||||
|
client = object.__new__(KsNebulaClient)
|
||||||
|
client.api_sig3_state = None
|
||||||
|
return client
|
||||||
|
|
||||||
|
def test_shares_process_state_across_calls(self):
|
||||||
|
client = self._bare_client()
|
||||||
|
s1 = KsNebulaClient._resolve_api_sig3_state(client, None, "normal_fetch")
|
||||||
|
s2 = KsNebulaClient._resolve_api_sig3_state(client, None, "normal_fetch")
|
||||||
|
self.assertIs(s1, s2) # 同一实例 → counter 持续递增而非恒定重放
|
||||||
|
d1 = kwsg_10418_digest24_unmix(s1.sig3_hex(b"req1"))
|
||||||
|
d2 = kwsg_10418_digest24_unmix(s1.sig3_hex(b"req2"))
|
||||||
|
self.assertEqual(d1["session_seed"], d2["session_seed"])
|
||||||
|
self.assertEqual(d2["counter"], d1["counter"] + 1)
|
||||||
|
|
||||||
|
def test_explicit_state_wins_and_does_not_pollute_shared(self):
|
||||||
|
client = self._bare_client()
|
||||||
|
explicit = Kwsg10418State(session_seed=0x12345678, counter=0x10)
|
||||||
|
resolved = KsNebulaClient._resolve_api_sig3_state(client, explicit, "normal_fetch")
|
||||||
|
self.assertIs(resolved, explicit)
|
||||||
|
self.assertIsNone(client.api_sig3_state) # 显式优先时不写回共享 state
|
||||||
|
|
||||||
|
def test_falls_back_to_har_seed_when_disabled(self):
|
||||||
|
client = self._bare_client()
|
||||||
|
with patch.dict(os.environ, {"KS_API_SIG3_FRESH_STATE": "0"}, clear=False):
|
||||||
|
resolved = KsNebulaClient._resolve_api_sig3_state(client, None, "normal_fetch")
|
||||||
|
har = Kwsg10418State.from_digest(API_SIG3_SEED["normal_fetch"])
|
||||||
|
self.assertEqual(resolved.session_seed, har.session_seed)
|
||||||
|
self.assertEqual(resolved.counter, har.counter)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue
Block a user