import json import sys import tempfile import unittest from contextlib import redirect_stdout from io import StringIO from pathlib import Path from unittest.mock import patch from tools import extract_app_login_fields from tools.extract_app_login_fields import extract_fields class ExtractAppLoginFieldsTests(unittest.TestCase): def test_cli_preserves_cached_weapon_kaw_when_new_log_has_none(self): """只抓到登录字段的新日志不能清空版本级 KAW 缓存。""" event = { "tag": "REQUEST_BUILD", "method": "POST", "url": ( "https://az2-api.ksapisrv.com/rest/nebula/user/login/mobileVerifyCode?" "did=ANDROID_APP_DID&sig=OLD" ), "body": "code=123456&mobile=LOGIN_ENC&passport_account_image=VIMG_LOGIN", } with tempfile.TemporaryDirectory() as td: root = Path(td) log = root / "probe_login.log" out = root / "app_login_fields_latest.json" log.write_text("@@LOGIN " + json.dumps(event), encoding="utf-8") out.write_text( json.dumps({"weapon_kaw": "CACHED_VERSION_KAW", "source_log": "older.log"}), encoding="utf-8", ) argv = [ "extract_app_login_fields.py", "--log", str(log), "--out", str(out), ] with patch.object(sys, "argv", argv), redirect_stdout(StringIO()): self.assertEqual(extract_app_login_fields.main(), 0) fields = json.loads(out.read_text(encoding="utf-8")) self.assertEqual(fields["weapon_kaw"], "CACHED_VERSION_KAW") def test_extract_fields_keeps_mobile_checker_fields(self): """登录日志里有 mobile/checker 时,提取 checker 专用密文和票据。""" checker_url = ( "https://az2-api.ksapisrv.com/rest/nebula/user/mobile/checker?" "did=ANDROID_APP_DID&sig=OLD&__NS_sig3=OLD3&__NS_xfalcon=OLDX" ) request_url = ( "https://az2-api.ksapisrv.com/rest/nebula/user/requestMobileCode?" "did=ANDROID_APP_DID&sig=OLD&__NS_sig3=OLD3&__NS_xfalcon=OLDX" ) login_url = ( "https://az2-api.ksapisrv.com/rest/nebula/user/login/mobileVerifyCode?" "did=ANDROID_APP_DID&sig=OLD&__NS_sig3=OLD3&__NS_xfalcon=OLDX" ) events = [ { "tag": "LOGINHELPER_B_PHONE", "in_value": "13800000000", "out_value": "CHECKER+ENC==", }, { "tag": "REQUEST_BUILD", "method": "POST", "url": checker_url, "body": ( "mobileCountryCode=+86&mobile=CHECKER+ENC==&cs=false&client_key=2ac2a76d" "&videoModelCrowdTag=&os=android&uQaTag=&passport_account_image=VIMG_CHECKER$AI_1111" ), }, { "tag": "REQUEST_BUILD", "method": "POST", "url": request_url, "body": ( "mobileCountryCode=+86&mobile=REQUEST+ENC==&type=27&useVoice=false" "&needCheck=false&prefetchPhoneNumber=&requestSource=1" "&passport_account_image=VIMG_REQUEST$AI_2222" ), }, { "tag": "REQUEST_BUILD", "method": "POST", "url": login_url, "body": ( "isDegraded=false&code=123456&mobileCountryCode=+86&deviceMode=OnePlus(PJZ110)" "&mobile=LOGIN+ENC==&prefetchPhoneNumber=&raw=1785044999746" "&publicKey=PUBLIC&secret=SECRET&type=27&deviceName=OnePlus(PJZ110)" "&passport_account_image=VIMG_LOGIN$AI_3333" ), }, ] with tempfile.TemporaryDirectory() as td: log = Path(td) / "probe.log" log.write_text( "\n".join("@@LOGIN " + json.dumps(event, ensure_ascii=False) for event in events), encoding="utf-8", ) fields = extract_fields(log) self.assertEqual(fields["checker_host"], "az2-api.ksapisrv.com") self.assertEqual(fields["checker_path"], "/rest/nebula/user/mobile/checker") self.assertEqual(fields["query_params"], {"did": "ANDROID_APP_DID"}) self.assertEqual(fields["source_mobile"], "13800000000") self.assertEqual(fields["checker_encrypted_mobile"], "CHECKER+ENC==") self.assertEqual(fields["request_encrypted_mobile"], "REQUEST+ENC==") self.assertEqual(fields["encrypted_mobile"], "LOGIN+ENC==") self.assertEqual(fields["checker_passport_account_image"], "VIMG_CHECKER$AI_1111") self.assertEqual(fields["request_passport_account_image"], "VIMG_REQUEST$AI_2222") self.assertEqual(fields["passport_account_image"], "VIMG_LOGIN$AI_3333") self.assertEqual( fields["checker_body_keys"], [ "mobileCountryCode", "mobile", "cs", "client_key", "videoModelCrowdTag", "os", "uQaTag", "passport_account_image", ], ) def test_extract_fields_from_all_layers_begin_chunk_log(self): """probe_nebula_all_layers 的 BEGIN/CHUNK 日志也能提取三段字段。""" checker_url = ( "https://az2-api.ksapisrv.com/rest/nebula/user/mobile/checker?" "did=ANDROID_APP_DID&sig=OLD&__NS_sig3=OLD3" ) request_url = ( "https://az2-api.ksapisrv.com/rest/nebula/user/requestMobileCode?" "did=ANDROID_APP_DID&sig=OLD&__NS_xfalcon=OLDX" ) login_url = ( "https://az2-api.ksapisrv.com/rest/nebula/user/login/mobileVerifyCode?" "did=ANDROID_APP_DID&sig=OLD" ) def begin(seq, tag, payload): return f"@@BEGIN\t{seq}\t{tag}\t" + json.dumps(payload, ensure_ascii=False) def chunk(seq, name, value): return f"@@CHUNK\t{seq}\t{name}\t0\t" + json.dumps(value, ensure_ascii=False) lines = [ begin(10, "OKHTTP_CHAIN_BEFORE_REQ", {"method": "POST", "url": checker_url, "tid": 7}), begin(11, "OKHTTP_CHAIN_BEFORE_REQ_BODY", {"tid": 7}), chunk(11, "utf8", "mobileCountryCode=%2B86&mobile=CHECKER%2BENC%3D%3D&passport_account_image=VIMG_CHECKER%24AI_1111"), begin(20, "OKHTTP_CHAIN_BEFORE_REQ", {"method": "POST", "url": request_url, "tid": 7}), begin(21, "OKHTTP_CHAIN_BEFORE_REQ_BODY", {"tid": 7}), chunk(21, "utf8", "mobileCountryCode=%2B86&mobile=REQUEST%2BENC%3D%3D&type=27&prefetchPhoneNumber=&passport_account_image=VIMG_REQUEST%24AI_2222"), begin(30, "OKHTTP_CHAIN_BEFORE_REQ", {"method": "POST", "url": login_url, "tid": 7}), begin(31, "OKHTTP_CHAIN_BEFORE_REQ_BODY", {"tid": 7}), chunk(31, "utf8", "code=123456&mobileCountryCode=%2B86&mobile=LOGIN%2BENC%3D%3D&prefetchPhoneNumber=&deviceName=OnePlus(PJZ110)&passport_account_image=VIMG_LOGIN%24AI_3333"), begin(32, "CRONET_ADD_HEADER", {"name": "kaw", "tid": 7}), chunk(32, "value", "KAW_VERSION_CONFIG"), ] with tempfile.TemporaryDirectory() as td: log = Path(td) / "probe_login_stable.log" log.write_text("\n".join(lines), encoding="utf-8") fields = extract_fields(log) self.assertEqual(fields["checker_host"], "az2-api.ksapisrv.com") self.assertEqual(fields["checker_path"], "/rest/nebula/user/mobile/checker") self.assertEqual(fields["query_params"], {"did": "ANDROID_APP_DID"}) self.assertEqual(fields["checker_encrypted_mobile"], "CHECKER+ENC==") self.assertEqual(fields["request_encrypted_mobile"], "REQUEST+ENC==") self.assertEqual(fields["encrypted_mobile"], "LOGIN+ENC==") self.assertEqual(fields["checker_passport_account_image"], "VIMG_CHECKER$AI_1111") self.assertEqual(fields["request_passport_account_image"], "VIMG_REQUEST$AI_2222") self.assertEqual(fields["passport_account_image"], "VIMG_LOGIN$AI_3333") self.assertEqual(fields["weapon_kaw"], "KAW_VERSION_CONFIG") def test_extract_fields_reads_mixed_utf8_utf16le_all_layers_log(self): """Start-Job/Tee 混合编码时仍能识别 BEGIN/CHUNK。""" url = "https://az2-api.ksapisrv.com/rest/nebula/user/login/mobileVerifyCode?did=ANDROID_APP_DID&sig=OLD" line1 = "@@BEGIN\t1\tOKHTTP_CHAIN_BEFORE_REQ\t" + json.dumps({"method": "POST", "url": url, "tid": 9}) line2 = "@@BEGIN\t2\tOKHTTP_CHAIN_BEFORE_REQ_BODY\t" + json.dumps({"tid": 9}) line3 = "@@CHUNK\t2\tutf8\t0\t" + json.dumps("mobileCountryCode=%2B86&mobile=LOGIN%2BENC%3D%3D&passport_account_image=VIMG_LOGIN%24AI_3333") with tempfile.TemporaryDirectory() as td: log = Path(td) / "probe_mixed.log" body = "\r\n".join([line1, line2, line3]) + "\r\n" log.write_bytes(b"\xef\xbb\xbfJOB_BEGIN pid=1\r\n" + body.encode("utf-16-le")) fields = extract_fields(log) self.assertEqual(fields["encrypted_mobile"], "LOGIN+ENC==") self.assertEqual(fields["passport_account_image"], "VIMG_LOGIN$AI_3333") if __name__ == "__main__": unittest.main()