修复微信QQ支付参数分流
This commit is contained in:
@@ -3,11 +3,17 @@ import { JSDOM, ResourceLoader } from 'jsdom';
|
||||
import { patchEnvironment } from './scripts/jsdom-patch-env.mjs';
|
||||
|
||||
const ROOT = '/Users/yml/codes/douyu_login_py';
|
||||
const out = ROOT + '/data/yyb-worker-dev/49684f85543d4fd1/jsdom-order';
|
||||
const taskId = process.argv[2] || '142265ef3d874ce6';
|
||||
const out = `${ROOT}/data/yyb-worker-dev/${taskId}/jsdom-order`;
|
||||
const goodsUrl = JSON.parse(fs.readFileSync(`${out}/device-fp.json`, 'utf8')).goods_url;
|
||||
const pageInfoPath = process.argv[3] || `${out}/web-page-info-response.json`;
|
||||
const pageInfo = fs.existsSync(pageInfoPath)
|
||||
? fs.readFileSync(pageInfoPath, 'utf8')
|
||||
: '{"ret":0,"msg":"","info":{}}';
|
||||
|
||||
const captured = [];
|
||||
const requests = [];
|
||||
const pageErrors = [];
|
||||
const dom = new JSDOM(fs.readFileSync(`${out}/goods.html`, 'utf8'), {
|
||||
url: goodsUrl,
|
||||
referrer: 'https://z.iwan.yyb.qq.com/',
|
||||
@@ -26,11 +32,15 @@ const dom = new JSDOM(fs.readFileSync(`${out}/goods.html`, 'utf8'), {
|
||||
requests.push({ name, url: this.__u.slice(0, 120), bodyLen: String(body || '').length });
|
||||
if (name === 'web_save') captured.push(String(body || ''));
|
||||
this.readyState = 4; this.status = 200;
|
||||
this.responseText = '{"ret":0,"msg":"","info":{}}';
|
||||
this.responseText = name === 'web_page_info' ? pageInfo : '{"ret":0,"msg":"","info":{}}';
|
||||
this.response = this.responseText;
|
||||
if (typeof this.onreadystatechange === 'function') this.onreadystatechange();
|
||||
if (typeof this.onload === 'function') this.onload();
|
||||
this.dispatchEvent(new window.Event('readystatechange'));
|
||||
this.dispatchEvent(new window.Event('load'));
|
||||
}
|
||||
};
|
||||
window.addEventListener('error', event => pageErrors.push(String(event.error || event.message || 'unknown error')));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,9 +48,24 @@ await new Promise(r => setTimeout(r, 15000));
|
||||
console.log('=== 页面发出的 XHR ===');
|
||||
for (const r of requests) console.log(' ', r.name, '|', r.url, '| bodyLen:', r.bodyLen);
|
||||
console.log('=== web_save body 捕获:', captured.length ? 'YES len=' + captured[0].length : 'NO');
|
||||
console.log('=== 页面按钮 ===');
|
||||
console.log('=== 页面可见控件 ===');
|
||||
const btns = dom.window.document.querySelectorAll('[class*="btn"],[class*="confirm"],[class*="submit"],button');
|
||||
for (const b of btns) {
|
||||
const t = (b.textContent || '').trim().replace(/\s+/g, ' ');
|
||||
if (t && t.length < 40 && b.offsetParent !== null) console.log(' btn:', t.slice(0, 30));
|
||||
if (t && t.length < 40) console.log(' control:', b.tagName, b.className, '|', t.slice(0, 30));
|
||||
}
|
||||
console.log('=== Avalon 控制器 ===');
|
||||
const models = dom.window.avalon?.vmodels || {};
|
||||
for (const [id, model] of Object.entries(models)) {
|
||||
const keys = Object.keys(model).filter(key => !key.startsWith('$')).slice(0, 30);
|
||||
console.log(' model:', id, '|', keys.join(','));
|
||||
}
|
||||
console.log('=== 订单状态 ===');
|
||||
console.log(JSON.stringify({
|
||||
channelListLength: String(models.order?.channelList || '').length,
|
||||
channelData: models.order?.channelData,
|
||||
goodsName: models.order?.goodsName,
|
||||
amount: models.order?.amount,
|
||||
pageErrors: pageErrors.slice(0, 10),
|
||||
}, null, 2));
|
||||
dom.window.close();
|
||||
|
||||
@@ -13,7 +13,7 @@ def describe_payment_failure(provider: str, phase: str, response: object) -> str
|
||||
"QQ 支付请求被风控/限流暂时拦截(1099-1007-0),未生成微信付款码;"
|
||||
"请停止连续重试,等待 30-60 分钟后再使用新订单尝试。"
|
||||
)
|
||||
return "支付请求被暂时拦截(1099-1007-0),未生成微信付款码;请稍后使用新订单重试。"
|
||||
return "微信支付请求被服务端拒绝(1099-1007-0),未生成微信付款码;请查看本次请求证据。"
|
||||
|
||||
if "10007" in text:
|
||||
if is_qq:
|
||||
|
||||
@@ -221,9 +221,16 @@ def build_page_info_body(order: dict[str, str], cookies: dict[str, str], anti_to
|
||||
return urllib.parse.urlencode(fields)
|
||||
|
||||
|
||||
def make_encrypt_rand(params: dict[str, str], fk_extend: str, ts: str) -> str:
|
||||
"""生成与当前订单明文长度匹配的 _rand,确保 goods VM 不截断尾块。"""
|
||||
def make_encrypt_rand(params: dict[str, str], fk_extend: str, ts: str,
|
||||
is_qq_login: bool) -> str:
|
||||
"""按登录渠道生成页面已验证形态的 _rand,不能仅按长度替换控制字节。"""
|
||||
prefix = "".join(random.choices(string.ascii_letters + string.digits, k=8))
|
||||
# 微信历史成功请求固定使用 8 个随机字符加 \x01。_rand 位于加密明文中,
|
||||
# 末控制字节是协议内容,不能为了对齐擅自替换为 QQ 使用的 \x03。
|
||||
if not is_qq_login:
|
||||
return prefix + "\x01"
|
||||
|
||||
# QQ 当前订单的明文长度会随 OAuth 字段变化;补齐后才能避免 VM 丢弃尾块。
|
||||
base_length = len(build_plaintext(params, fk_extend, ts, "").encode("latin-1"))
|
||||
padding_length = (-(base_length + len(prefix))) % 16
|
||||
return prefix + ("\x03" * padding_length)
|
||||
@@ -445,20 +452,22 @@ def main() -> int:
|
||||
print(f"[jsdom-pay] fp-behv 失败: ret={fp_json.get('ret')} {fp_json.get('msg', '')}")
|
||||
return 1
|
||||
|
||||
print("[jsdom-pay] 拉取 web_page_info...")
|
||||
page_info = request_bytes(PAGE_INFO_URL, cookies, build_page_info_body(
|
||||
order, cookies, anti_token, args.zone_id, payment_pf,
|
||||
).encode())
|
||||
(out_dir / "web-page-info-response.json").write_text(page_info, encoding="utf-8")
|
||||
try:
|
||||
page_info_json = json.loads(page_info)
|
||||
except json.JSONDecodeError:
|
||||
page_info_json = {}
|
||||
if page_info_json.get("ret") != 0:
|
||||
print(f"[jsdom-pay] web_page_info 失败: ret={page_info_json.get('ret')} "
|
||||
f"{page_info_json.get('msg', '')}")
|
||||
print(f"[jsdom-pay] 证据目录: {out_dir}")
|
||||
return 1
|
||||
# QQ 成功 HAR 包含该前置;微信历史成功链路没有,不能将 QQ 状态机混入微信请求。
|
||||
if midas_login_params(cookies)["qq_appid"]:
|
||||
print("[jsdom-pay] 拉取 QQ web_page_info...")
|
||||
page_info = request_bytes(PAGE_INFO_URL, cookies, build_page_info_body(
|
||||
order, cookies, anti_token, args.zone_id, payment_pf,
|
||||
).encode())
|
||||
(out_dir / "web-page-info-response.json").write_text(page_info, encoding="utf-8")
|
||||
try:
|
||||
page_info_json = json.loads(page_info)
|
||||
except json.JSONDecodeError:
|
||||
page_info_json = {}
|
||||
if page_info_json.get("ret") != 0:
|
||||
print(f"[jsdom-pay] QQ web_page_info 失败: ret={page_info_json.get('ret')} "
|
||||
f"{page_info_json.get('msg', '')}")
|
||||
print(f"[jsdom-pay] 证据目录: {out_dir}")
|
||||
return 1
|
||||
|
||||
web_args = load_template_args()
|
||||
# key 派生已破解(F-2052,2026-08-12):key16 可随机生成,key1 由反解器
|
||||
@@ -489,7 +498,9 @@ def main() -> int:
|
||||
params.update(midas_login_params(cookies))
|
||||
now_seconds = str(int(time.time()))
|
||||
fk_extend = "tdrc_session%3D" + fp["session_id"]
|
||||
random_suffix = make_encrypt_rand(params, fk_extend, now_seconds)
|
||||
random_suffix = make_encrypt_rand(
|
||||
params, fk_extend, now_seconds, bool(midas_login_params(cookies)["qq_appid"]),
|
||||
)
|
||||
plaintext_length = len(build_plaintext(params, fk_extend, now_seconds, random_suffix).encode("latin-1"))
|
||||
encrypt_msg = generate_encrypt_msg_offline(
|
||||
params, fk_extend, now_seconds, random_suffix,
|
||||
|
||||
@@ -74,22 +74,32 @@ class TestJsdomPay(unittest.TestCase):
|
||||
self.assertIn("未生成微信付款码", message)
|
||||
self.assertIn("停止连续重试", message)
|
||||
|
||||
def test_wechat_rejection_is_not_labeled_as_risk_control(self):
|
||||
message = describe_payment_failure("WX", "payment", "ret=1099 err_code=1099-1007-0")
|
||||
self.assertIn("服务端拒绝", message)
|
||||
self.assertNotIn("风控", message)
|
||||
|
||||
def test_game_role_error_is_classified_separately(self):
|
||||
message = describe_payment_failure("QC", "order", "inner=10014 gamerole err")
|
||||
self.assertIn("角色或区服校验失败", message)
|
||||
self.assertIn("重新选择角色和区服", message)
|
||||
|
||||
def test_encrypt_rand_aligns_plaintext_to_aes_block(self):
|
||||
def test_qq_encrypt_rand_aligns_plaintext_to_aes_block(self):
|
||||
params = {
|
||||
"token_id": "t", "openid": "o", "openkey": "k", "session_id": "openid",
|
||||
"session_type": "kp_accesstoken", "zoneid": "1", "pay_method": "wechat",
|
||||
"buy_quantity": "1", "from_h5": "1", "webversion": "minipayv2",
|
||||
}
|
||||
rand_value = _MODULE.make_encrypt_rand(params, "tdrc_session%3Dpay-test", "1775990000")
|
||||
rand_value = _MODULE.make_encrypt_rand(params, "tdrc_session%3Dpay-test", "1775990000", True)
|
||||
plaintext = _MODULE.build_plaintext(params, "tdrc_session%3Dpay-test", "1775990000", rand_value)
|
||||
self.assertEqual(len(plaintext.encode("latin-1")) % 16, 0)
|
||||
self.assertGreaterEqual(len(rand_value), 8)
|
||||
|
||||
def test_wechat_encrypt_rand_preserves_historical_control_byte(self):
|
||||
rand_value = _MODULE.make_encrypt_rand({}, "tdrc_session%3Dpay-test", "1775990000", False)
|
||||
self.assertEqual(len(rand_value), 9)
|
||||
self.assertEqual(rand_value[-1], "\x01")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user