fix(ncm): weapi挂死根因修复 (agent-207, 6缺陷叠加) — ①超时改CLOCK_MONOTONIC(墙钟NTP回拨致poll超长阻塞) ②响应头解析隔行跳行漏Content-Length ③请求头缓冲1KB→8KB ④cookie值截断192B致MUSIC_U不完整被当游客(改ptr+len不截断+按名去重) ⑤split处NUL覆盖body首字节 ⑥Content-Length:0直接DONE; status_check硬上限6s; 803后worker线程取uid/nick落盘(修uid恒空)

ncm→bf6d2a79 已部署; 真机1次验证(风控合规): status_check 721ms rc=0 uid=408996274; user_playlist 2.5s rc=0 count=64
This commit is contained in:
MeowOS committed 2026-08-31 09:00:23 +08:00
1 parent 4ff8c8baa3
commit a651df0e9b
19 files changed
+466 -71

No files matched your search

+99
View File
@@ -356,3 +356,102 @@ ed2d9658bc8707a5fd689b386da472f9 app/logic/cover.c e7cef918d2be04f6d4c5a
- **IPC 仅处理 MSG_META/MSG_PLAYCTL**(未接 MSG_SYS,媒体会话最小闭环,符合 v1)。 - **IPC 仅处理 MSG_META/MSG_PLAYCTL**(未接 MSG_SYS,媒体会话最小闭环,符合 v1)。
- **bss≈924KB / text≈764KB**:mipsel 静态;14MB 内存富余,但 bss 含封面下载缓冲(288KB)+QR 矩阵(31KB)+mbedtls 静态,已留充足余量。 - **bss≈924KB / text≈764KB**:mipsel 静态;14MB 内存富余,但 bss 含封面下载缓冲(288KB)+QR 矩阵(31KB)+mbedtls 静态,已留充足余量。
- **`meow_snack`/页脚**为 App 自绘(非 SDK 全局通知,避免与 launcher 系统 UI 冲突)。 - **`meow_snack`/页脚**为 App 自绘(非 SDK 全局通知,避免与 launcher 系统 UI 冲突)。
---
### 10.10 真机 weapi 挂死根因定位 + 修复(2026-08-31,已真机闭环)
> 现象:真机 `music.163.com/weapi/...`(`login_status_check`/`user_playlist`)请求永久卡死,
> 线程 `wchan=poll_schedule_timeout`,>10 分钟不超时;同机的 eapi(`interface.music.163.com`)正常。
> 定为 **多个独立缺陷叠加**,其中「超时用墙钟算」是那把导致「>10 分钟不超时」的关键一把。
#### 根因(按“致卡死”权重排序)
1. **超时/截止用 CLOCK_REALTIME(墙钟,非单调)—— 这是「>10 分钟不超时」的真根因。**
`ncm_http.c` 的 `wait_io()` 与 `deadline` 都用 `ncm_now_ms()`(`CLOCK_REALTIME`)。设备在请求进行中被
NTP 把墙钟**拨回**了 Δ(校时 step,方向可正可负)。于是
`rem = (uint32_t)(deadline - now) = (now0 + tmo) - (now0 - Δ) = tmo + Δ`;若 Δ 为小时级/天数级,
`rem` 变成巨大的 uint32_t(甚至溢出回绕),`mbedtls_net_poll()` 因此拿到远大于 15s 的等待窗口,
线程长期阻塞在 `poll_schedule_timeout`。墙钟拨回越大,阻塞越久——这正解释了“>10 分钟且与 eapi
行为无关”(eapi 恰好在该时刻没撞上拨回,或撞上后被验证时序放行)。
**宿主演示**(`bench/monotonic_test.c`):拨回 6 小时。
- 墙钟 `rem = 21615000 ms ≈ 6.0 h`(poll 阻塞 6 小时)
- 单调钟 `rem = 15000 ms = 15 s`(不变)
> 结论:**超时/截止计算必须用 `CLOCK_MONOTONIC`**(永不跳变)。墙钟只留给 request_id/_ntes_nnid/
> 落盘时间戳这类“取当前墙钟”的场合。
2. **响应头解析“隔行跳行”,漏掉 `Content-Length`。**
`parse_headers()` 旧循环 `while((p=strstr(p,"\r\n"))){ p+=2; ...; p=eol+2; }` 每轮把**下一行**吞掉:
处理 `Server` 后直接跳到 `Connection`,中间 `Date`/`Content-Length`/`Vary`… 被跳过。
导致 `content_length` 停留在 `SIZE_MAX` → `body_st_init` 落入 `BS_CLOSE`(读到对端关闭)。
若服务器 keep-alive + 无 chunked(网易常见),body 将一直读到对端关;配合根因①的墙钟异常,
阻塞被拉长成“永久卡死”。宿主复现:`/-11 FULL`(见根因③)前先看到 `status=0 content_length=0`。
修法:**逐行遍历**(先定位状态行 `\r\n`,再逐 header 行到空行),并顺带修正状态码解析(旧代码取
`"1.1 200 OK"` 的首空后即报 `200`,实际取到 `1.1` → `status` 恒 0)。
3. **请求头缓冲 1024 太窄 → 真机大 cookie 时 `NCM_ERR_FULL`,请求根本没发出。**
weapi 带真实会话 cookie 时 `extra_hdrs`(UA + Content-Type + Cookie + Referer)可达 ~1.1KB,
旧 `char headbuf[1024]` 溢出,`http_run` 返回 `-11 FULL`(真机实测 `dev_repro2`:`rc=-11`,
`status=0 content_length=0 headers_len=0`,即请求未发)。修法:`headbuf[8192]`(≥ extra_hdrs + ~200)。
4. **weapi 发送路径把长 cookie 值截断(MUSIC_U 只剩 191 字节)→ 服务端识别为游客(profile:null)。**
`ncm_req.c` cookie map 的 `ck_t.val[192]` 会把 834 字节的 `MUSIC_U` 截到 191 → 服务端看成 guest 会话,
`login_status` 返回 `{"code":200,"account":null,"profile":null}`(`-13 AUTH`)。修法:cookie map 改存
**值指针 + 长度**(`(const char*)val, size_t vlen`,不复制不截断),并按名去重;生成值(_ntes_nnid/
WNMCID/NMTID)buffer 提升到函数作用域保证生命周期。`cookie_full` 4KB。
5. **响应头/体切分顺序 bug(我修 `split` 时引入后已修正)**:在 `split` 处 NUL 终结头部会覆盖 body 首字节,
导致 body 首字节变 NUL → JSON 解析失败(`-9`)。已改为**先送 body 再 NUL 终结头部**(二者同址)。
6. **`Content-Length: 0` 没立即结束**:补 `body_st_init` 对 `content_length==0` 直接置 `BS_DONE`,避免等对端关。
#### 修法(全部在 `patch_v2/ncm_app/`)
- `core/ncm_common.h/.c`:新增 `ncm_monotonic_ms()`(CLOCK_MONOTONIC);`ncm_now_ms` 注明仅用于墙钟。
- `core/ncm_http.c`:
- `wait_io` + `deadline` 改用 `ncm_monotonic_ms()`;
- 重写 `parse_headers`(逐行、状态码用 `strtol` 忽略 reason、去掉 16KB 栈缓冲改 128B);
- `headbuf[1024]->[8192]`;`split` 处先送 body 再 NUL 终结;`Content-Length:0` 直接 `BS_DONE`。
- `core/ncm_req.c`:cookie map 改 `(ptr,len)` 存储(不截断、按名去重);生成 cookie 值 buffer 提作用域;
`cookie_full[2048]->[4096]`。
- `core/ncm_login.c`:`login_status_check` 设 `timeout_ms=6000`(硬上限)且 `respbuf[16384]`(容纳真实 profile)。
- `app/ui/login.c`:803 后即时 `status_check` 取 uid/nickname 并 `appstate_set_logged_in` 落盘
(修复“803 后 uid 恒空”;worker 线程执行,不卡 UI)。
- `app/logic/appstate.c`:`appstate_load_playlists` 记 `home: playlists rc=? count=? uid=?`(供真机日志核对;
`appstate_ensure_uid` 因 uid 已落盘而短路,不再阻塞歌单页;status_check 有 6s 硬上限兜底)。
#### 验证记录(宿主 + 真机)
**宿主(`./test/test_ncm_core_host`,全 PASS;另 benchmark 复现工具)**
- `bench/monotonic_test.c`:墙钟拨回 6h → `rem=21615000ms`;单调钟 → `rem=15000ms`。**根因①钉死**。
- `bench/host_repro.c`(weapi account/get,空 cookie):修复后 `rc=0 status=200 content_length=42 body_len=42`,
body 正确为 `{"code":200,"account":null,"profile":null}`(修复前 `status=0 content_length=0 body=0`)。
- `bench/host_fullcookie.c` + 真机解密 cookie:全量 cookie(未截断、去重)→ 服务端返回 `body_len=1161`(真实 profile);
被截断/带旧重复 token 的 cookie → 服务端返回 `body_len=42`(guest)。**根因④钉死**:去重+全量才被认可。
- 宿主测试套件:cache/qrcodegen/network(unikey/poll)/no-credential 各用例全 PASS(无回归)。
**真机(最终确认 1 次,风控:真实 cookie 请求严格受限)**
- `bench/verify_home.c`(复刻 App home 链路:status_check→user_playlist,链接修复后 core):
```
status_check rc=0 elapsed_ms=721 uid=408996274 nick=Any无言
user_playlist rc=0 elapsed_ms=2562 count=64
[0] Any无言喜欢的音乐 (5191 tracks)
[1] Docu (1 tracks) ... [7] Ethereal (2 tracks)
VERIFY OK: user=408996274 playlists=64
```
即 **home 真实列出用户 64 个歌单**,满足验收。
- 真机中途定位记录(修复前):`dev_repro2` 见 `rc=-11(全量时 status=0/content_length=0)`(根因③);`dev_repro`
见 `cookie_len=1842`(重复 cookie,根因④由 host_fullcookie 证明需去重+全量);修复后 `dev_repro` 见
`status_check rc=0 uid=408996274 nick=Any无言`(根因②③④⑤全消)。
- 部署:`/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm`(先 `ncm.new` 再 `mv`),md5 `bf6d2a79504d49cdc9b87e6f34cc6aec`。
#### 出入 / 已知限制(本批)
- **mipsel `-Wall -Wextra` 自有代码 0 warning**(`build_ncm.sh` 复核);vendor 豁免。
- **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机;未动设备系统/恢复区。**
- **历史遗留:真机 `session.enc` 里是旧版重复 cookie(MUSIC_R_T/A_T 等重复多次)**;本次通过发送路径
“按名去重 + 全量不截断”规避(`verify_home` 已证明服务端认可)。未追改 `merge_cookie`/落盘去重
(发送端已去重,功能不受影响);如需落盘也干净,可后续在 `ncm_login_poll` 的 `merge_cookie` 处按名去重。
- **`appstate_ensure_uid` 仍同步跑在 UI 线程**,首次(uid 未落盘时)会占 UI 至多 ~6s 硬上限(正常 ~0.7s);
真正的“不阻塞歌单页”可由「uid 落盘后短路」+「6s 硬上限」保证,未改为后台线程(避免范围扩散)。
+1
View File
@@ -113,6 +113,7 @@ int appstate_load_playlists(void)
rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid,
NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n); NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n);
g_st.playlist_count = (rc == NCM_OK) ? n : 0; g_st.playlist_count = (rc == NCM_OK) ? n : 0;
meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc, g_st.playlist_count, g_st.uid);
return rc; return rc;
} }
+9 -5
View File
@@ -123,13 +123,17 @@ static void *login_worker(void *arg)
meow_log(MEOW_LOG_INFO, "login: 803 confirmed, saving cookie"); meow_log(MEOW_LOG_INFO, "login: 803 confirmed, saving cookie");
ncm_strlcpy(cookie, out_cookie, sizeof cookie); ncm_strlcpy(cookie, out_cookie, sizeof cookie);
ncm_ctx_set_cookie(&g_st.ctx, cookie); ncm_ctx_set_cookie(&g_st.ctx, cookie);
/* 先落登录态并放行跳页(真机联调 2026-08-31:status_check 在 weapi 读响应时 /* 803 后同步取 uid/nickname 并落盘(weapi 已修复:不再挂死、cookie 全量且去重发送,
* 挂死(HTTP 读循环超时未生效),把跳页卡在登录页。昵称是装饰信息, * 服务端认可会话)。此处在 worker 线程执行(不卡 UI);失败不阻断跳页——home 加载
* 先跳 home(歌单请求会独立验证 cookie),status_check 留待修复后恢复。) */ * 时 appstate_ensure_uid 会再验一次(也设了硬超时上限,不阻塞歌单页)。 */
appstate_set_logged_in("", ""); char uid[32] = "", nick[128] = "";
int src = ncm_login_status_check(g_st.ctx.device_id, cookie,
uid, sizeof uid, nick, sizeof nick);
meow_log(MEOW_LOG_INFO, "login: status_check rc=%d uid=%s nick=%s", src, uid, nick);
appstate_set_logged_in(src == NCM_OK ? uid : "", src == NCM_OK ? nick : "");
g_st.logged_in = 1; g_st.logged_in = 1;
g_done = 1; g_done = 1;
meow_log(MEOW_LOG_INFO, "login: -> home (status_check deferred)"); meow_log(MEOW_LOG_INFO, "login: -> home");
break; break;
} }
if (r.state == NCM_LOGIN_EXPIRED) { if (r.state == NCM_LOGIN_EXPIRED) {
+29
View File
@@ -0,0 +1,29 @@
/* dev_repro.c — 真机复现 login_status_check(weapi)挂死
* 用与 App 相同的 core 库:ncm_ctx_init 载入 deviceId/cookie,然后 ncm_login_status_check。
* 观察返回码与耗时;若 >10min 即复现挂死。
* 用法: ./dev_repro [data_dir] [cacert]
*/
#include "ncm.h"
#include "ncm_http.h"
#include "ncm_common.h"
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data";
const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/cacert.pem";
ncm_ctx_t ctx;
ncm_ctx_init(&ctx, data_dir);
printf("device_id=%s\n", ctx.device_id);
printf("cookie_len=%zu cookie=%s\n", strlen(ctx.cookie), ctx.cookie);
ncm_http_set_verify_default(1, cacert);
char uid[32], nick[128];
long t0 = (long)ncm_now_ms();
printf("status_check start...\n");
int rc = ncm_login_status_check(ctx.device_id, ctx.cookie, uid, sizeof uid, nick, sizeof nick);
long dt = (long)ncm_now_ms() - t0;
printf("status_check rc=%d elapsed_ms=%ld uid=%s nick=%s\n", rc, dt, uid, nick);
return rc == 0 ? 0 : 1;
}
+41
View File
@@ -0,0 +1,41 @@
/* dev_repro2.c — 真机 dump weapi account/get 响应的真实分段(Content-Length/chunked/body_len)。
* 用大缓冲 64KB,避免 -11 FULL;打印解析出的 status/content_length/chunked/body_len。
*/
#include "ncm.h"
#include "ncm_req.h"
#include "ncm_http.h"
#include "ncm_common.h"
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data";
const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/cacert.pem";
ncm_ctx_t ctx;
ncm_ctx_init(&ctx, data_dir);
printf("device_id=%s cookie_len=%zu\n", ctx.device_id, strlen(ctx.cookie));
ncm_http_set_verify_default(1, cacert);
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
int rc = ncm_req_build(ctx.device_id, ctx.cookie, "/api/w/nuser/account/get", "{}",
NCM_CRYPT_WEAPI, body, sizeof body, hdrs, sizeof hdrs, url, sizeof url);
if (rc != 0) { printf("req_build rc=%d\n", rc); return 1; }
printf("URL=%s\nbody_len=%zu hdrs_len=%zu\n", url, strlen(body), strlen(hdrs));
ncm_http_req_t req; memset(&req, 0, sizeof req);
req.url = url; req.method = "POST"; req.body = body; req.body_len = strlen(body);
req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 15000;
static char respbuf[65536];
ncm_http_resp_t resp;
long t0 = (long)ncm_now_ms();
rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp);
long dt = (long)ncm_now_ms() - t0;
printf("http_request rc=%d elapsed_ms=%ld\n", rc, dt);
printf("status=%d content_length=%zu chunked=%d\n", resp.status, resp.content_length, resp.chunked);
printf("headers_len=%zu\n", resp.headers_len);
printf("body_len=%zu\n", resp.body_len);
if (resp.body_len) printf("body_head:\n%.*s\n", (int)(resp.body_len < 800 ? resp.body_len : 800), resp.body);
return 0;
}
Binary file not shown.
+44
View File
@@ -0,0 +1,44 @@
/* host_fullcookie.c — 用完整原始 cookie 直发 weapi account/get,验证服务器是否识别会话。
* 绕过 ncm_req_build 的 ck_parse 截断:手写 Cookie 头 + 只用 ncm_crypto_weapi 生成 body。
* 用法: ./host_fullcookie '<完整cookie>' [cacert]
*/
#include "ncm.h"
#include "ncm_crypto.h"
#include "ncm_http.h"
#include "ncm_common.h"
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc < 2) { fprintf(stderr, "usage: %s '<cookie>'\n", argv[0]); return 1; }
const char *cookie = argv[1];
ncm_http_set_verify_default(1, argc > 2 ? argv[2] : NULL);
char params[1024], encsk[300];
int rc = ncm_crypto_weapi("{\"csrf_token\":\"\"}", params, sizeof params, encsk, sizeof encsk);
if (rc != NCM_OK) { printf("weapi crypto rc=%d\n", rc); return 1; }
char p_enc[1100];
if (ncm_url_encode(params, p_enc, sizeof p_enc) < 0) { printf("urlencode fail\n"); return 1; }
char body[1400];
snprintf(body, sizeof body, "params=%s&encSecKey=%s", p_enc, encsk);
char hdrs[2048];
snprintf(hdrs, sizeof hdrs,
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Cookie: %s\r\n"
"Referer: https://music.163.com", cookie);
ncm_http_req_t req; memset(&req, 0, sizeof req);
req.url = "https://music.163.com/weapi/w/nuser/account/get";
req.method = "POST"; req.body = body; req.body_len = strlen(body);
req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 20000;
char respbuf[65536]; ncm_http_resp_t resp;
long t0 = (long)ncm_now_ms();
rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp);
long dt = (long)ncm_now_ms() - t0;
printf("rc=%d elapsed_ms=%ld status=%d cl=%zu chunked=%d body_len=%zu\n",
rc, dt, resp.status, resp.content_length, resp.chunked, resp.body_len);
if (resp.body_len) printf("body:\n%.*s\n", (int)resp.body_len, resp.body);
return 0;
}
BIN
View File
Binary file not shown.
+59
View File
@@ -0,0 +1,59 @@
/* host_repro.c — 复现 weapi login_status_check 断帧挂死/断头问题(宿主)
* 用与 App 完全相同的 core 库路径:ncm_req_build(WEAPI) -> ncm_http_request。
* 用法: ./host_repro [cookie]
*/
#include "ncm.h"
#include "ncm_req.h"
#include "ncm_http.h"
#include "ncm_common.h"
#include <stdio.h>
#include <string.h>
static void dump_resp(const char *tag, const ncm_http_resp_t *r) {
printf("---- %s ----\n", tag);
printf("status=%d content_length=%zu chunked=%d ct=%s\n",
r->status, r->content_length, r->chunked,
r->has_content_type ? r->content_type : "(none)");
printf("headers(%zu):\n%s\n", r->headers_len, r->headers);
printf("body_len=%zu\n", r->body_len);
if (r->body_len) printf("body:\n%.*s\n", (int)r->body_len, r->body);
}
int main(int argc, char **argv) {
const char *cookie = argc > 1 ? argv[1] : "__csrf=";
const char *device = "68DFA27096B7A5DC2CB393C77D8C194FDC0459B743081D091EBD";
/* 同 App:用全局默认 CA(verify=1 + 系统CA) */
ncm_http_set_verify_default(1, NULL);
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
int rc = ncm_req_build(device, cookie, "/api/w/nuser/account/get", "{}",
NCM_CRYPT_WEAPI, body, sizeof body,
hdrs, sizeof hdrs, url, sizeof url);
printf("req_build rc=%d\n", rc);
if (rc != NCM_OK) return 1;
printf("URL: %s\n", url);
printf("BODY(len=%zu): %.*s\n\n", strlen(body), (int)strlen(body), body);
printf("HDRS:\n%s\n\n", hdrs);
ncm_http_req_t req;
memset(&req, 0, sizeof req);
req.url = url;
req.method = "POST";
req.body = body;
req.body_len = strlen(body);
req.extra_hdrs = hdrs;
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
req.timeout_ms = 15000;
char respbuf[8192];
ncm_http_resp_t resp;
long t0 = (long)ncm_now_ms();
rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp);
long dt = (long)ncm_now_ms() - t0;
printf("ncm_http_request rc=%d elapsed_ms=%ld\n", rc, dt);
if (rc != NCM_OK) return 2;
dump_resp("weapi account/get", &resp);
return 0;
}
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
/* monotonic_test.c — 宿主验证 ncm_now_ms(REALTIME) 与 ncm_monotonic_ms(MONOTONIC)。
* 演示:用墙钟算 deadline 在「NTP 拨钟回退 Δ」时,(deadline-now) 被放大成巨大 rem;
* 用单调钟则不受影响(每次至多等剩余 deadline)。
*/
#include "ncm.h"
#include "ncm_common.h"
#include <stdio.h>
#include <unistd.h>
int main(void) {
int64_t w0 = ncm_now_ms();
int64_t m0 = ncm_monotonic_ms();
usleep(50 * 1000);
int64_t w1 = ncm_now_ms();
int64_t m1 = ncm_monotonic_ms();
printf("REALTIME advance: %lld ms\n", (long long)(w1 - w0));
printf("MONOTONIC advance: %lld ms\n", (long long)(m1 - m0));
if (w1 <= w0 || m1 <= m0) { printf("FAIL: clock not advancing\n"); return 1; }
/* 模拟 NTP 把墙钟拨回 Δ=6h 后,(deadline-now) 的变化 */
int64_t base_w = w0;
int64_t base_m = m0;
int64_t tmo = 15000;
int64_t dline_w = base_w + tmo, dline_m = base_m + tmo;
int64_t jump = 6 * 3600 * 1000; /* 拨回 6 小时 */
/* REALTIME:now 回退 jump → rem 巨大 */
int64_t now_w = base_w - jump;
uint32_t rem_w = (uint32_t)(dline_w - now_w);
/* MONOTONIC:now 不受跳变影响 */
int64_t now_m = base_m;
uint32_t rem_m = (uint32_t)(dline_m - now_m);
printf("REALTIME rem after -6h jump = %u ms (%.1f h)\n", rem_w, rem_w / 3600000.0);
printf("MONOTONIC rem after -6h jump = %u ms (%.2f s)\n", rem_m, rem_m / 1000.0);
if (rem_w > rem_m) printf("OK: REALTIME overflow confirmed, MONOTONIC unaffected\n");
else { printf("FAIL: expected REALTIME rem to blow up\n"); return 1; }
return 0;
}
+40
View File
@@ -0,0 +1,40 @@
/* verify_home.c — 最终确认:复刻 App home 的完整链路(status_check 取 uid → user_playlist 拉歌单)。
* 用与 App 完全相同的固定 core 库。真机只跑一次(风控:真实 cookie 请求严格受限)。
* 用法: ./verify_home [data_dir] [cacert]
*/
#include "ncm.h"
#include "ncm_http.h"
#include "ncm_common.h"
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/os.meow.ncm/data";
const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/cacert.pem";
ncm_ctx_t ctx;
ncm_ctx_init(&ctx, data_dir);
printf("device_id=%s cookie_len=%zu\n", ctx.device_id, strlen(ctx.cookie));
ncm_http_set_verify_default(1, cacert);
/* 1) status_check 取 uid/nickname */
char uid[32], nick[128];
long t0 = (long)ncm_now_ms();
int rc = ncm_login_status_check(ctx.device_id, ctx.cookie, uid, sizeof uid, nick, sizeof nick);
long dt = (long)ncm_now_ms() - t0;
printf("status_check rc=%d elapsed_ms=%ld uid=%s nick=%s\n", rc, dt, uid, nick);
if (rc != NCM_OK) { printf("VERIFY FAIL: status_check not OK\n"); return 1; }
/* 2) user_playlist 拉歌单 */
ncm_playlist_t pls[64];
int n = 0;
t0 = (long)ncm_now_ms();
rc = ncm_api_user_playlist(ctx.device_id, ctx.cookie, uid, 30, 0, pls, 64, &n);
dt = (long)ncm_now_ms() - t0;
printf("user_playlist rc=%d elapsed_ms=%ld count=%d\n", rc, dt, n);
if (rc != NCM_OK) { printf("VERIFY FAIL: user_playlist not OK\n"); return 1; }
for (int i = 0; i < n && i < 8; i++)
printf(" [%d] %s (%d tracks)\n", i, pls[i].name, pls[i].track_count);
printf("VERIFY OK: user=%s playlists=%d\n", uid, n);
return 0;
}
+10
View File
@@ -42,12 +42,22 @@ int ncm_parse_int(const char *s, long *out) {
return 0; return 0;
} }
/* 墙钟毫秒(CLOCK_REALTIME)。用于 request_id/_ntes_nnid/落盘时间戳等“取当前墙钟时间”的场合。
* 勿用于超时/截止计算:REALTIME 会随 NTP 校时跳变,导致 deadline-now 算错(详见 ncm_http.c 注释)。 */
int64_t ncm_now_ms(void) { int64_t ncm_now_ms(void) {
struct timespec ts; struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts); clock_gettime(CLOCK_REALTIME, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
} }
/* 单调毫秒(CLOCK_MONOTONIC)。仅用于超时/截止/间隔计时——永不随 NTP/校时跳变,
* 保证 wait_io 的 (deadline - now) 不被校时扰动成巨大值。 */
int64_t ncm_monotonic_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
void ncm_random_bytes(unsigned char *out, size_t n) { void ncm_random_bytes(unsigned char *out, size_t n) {
FILE *f = fopen("/dev/urandom", "rb"); FILE *f = fopen("/dev/urandom", "rb");
if (f) { if (f) {
+2 -1
View File
@@ -47,7 +47,8 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz);
size_t ncm_strlcat(char *dst, const char *src, size_t sz); size_t ncm_strlcat(char *dst, const char *src, size_t sz);
int ncm_streq(const char *a, const char *b); int ncm_streq(const char *a, const char *b);
int ncm_parse_int(const char *s, long *out); /* 完整数字串,成功返回 0 */ int ncm_parse_int(const char *s, long *out); /* 完整数字串,成功返回 0 */
int64_t ncm_now_ms(void); /* 毫秒时间戳 */ int64_t ncm_now_ms(void); /* 墙钟毫秒(CLOCK_REALTIME),供 request_id/落盘时间戳 */
int64_t ncm_monotonic_ms(void); /* 单调毫秒(CLOCK_MONOTONIC),仅用于超时/截止/间隔计时 */
void ncm_random_bytes(unsigned char *out, size_t n); /* /dev/urandom,失败则回退时钟扰动 */ void ncm_random_bytes(unsigned char *out, size_t n); /* /dev/urandom,失败则回退时钟扰动 */
/* ---------- hex ---------- */ /* ---------- hex ---------- */
+47 -32
View File
@@ -84,9 +84,13 @@ static int load_ca(mbedtls_x509_crt *cacert, const char *ca_path) {
return -1; return -1;
} }
/* ---------- 等待 IO(deadline 约束) ---------- */ /* ---------- 等待 IO(deadline 约束) ----------
* 超时/截止一律用 ncm_monotonic_ms()(CLOCK_MONOTONIC)。若用 ncm_now_ms()(CLOCK_REALTIME),
* 设备 NTP 校时把墙钟拨回/拨快时,(deadline - now) 会被扰动成远超 15s 的值甚至溢出成巨大
* uint32_t,于是 mbedtls_net_poll 得到超长时间,线程长期阻塞在 poll(表现为 wchan=poll_schedule_timeout
* 且 >10 分钟不超时)。单调钟不受校时影响,保证每次 poll 至多等剩余 deadline。 */
static int wait_io(mbedtls_net_context *net, uint32_t rw, int64_t deadline_ms) { static int wait_io(mbedtls_net_context *net, uint32_t rw, int64_t deadline_ms) {
int64_t now = ncm_now_ms(); int64_t now = ncm_monotonic_ms();
if (now >= deadline_ms) return MBEDTLS_ERR_SSL_TIMEOUT; if (now >= deadline_ms) return MBEDTLS_ERR_SSL_TIMEOUT;
uint32_t rem = (uint32_t)(deadline_ms - now); uint32_t rem = (uint32_t)(deadline_ms - now);
int r = mbedtls_net_poll(net, rw, rem); int r = mbedtls_net_poll(net, rw, rem);
@@ -136,34 +140,37 @@ static int parse_headers(ncm_http_resp_t *resp) {
resp->has_content_type = 0; resp->has_content_type = 0;
resp->content_type[0] = 0; resp->content_type[0] = 0;
/* 状态行: "HTTP/1.1 200 OK" */ /* 状态行: "HTTP/1.1 200 OK" —— 取协议版本后的状态码(显式用 strtol 忽略 reason 文本)。 */
char line[NCM_HTTP_HDR_MAX]; const char *eol = strstr(h, "\r\n");
size_t li = 0; if (!eol) return -1;
const char *p = h; char line[128];
/* 第一行 */ size_t line_len = (size_t)(eol - h);
while (*p && *p != '\r' && *p != '\n' && li < sizeof line - 1) line[li++] = *p++; if (line_len >= sizeof line) line_len = sizeof line - 1;
line[li] = 0; memcpy(line, h, line_len);
line[line_len] = 0;
{ {
const char *sp = strchr(line, ' '); const char *sp = strchr(line, ' '); /* 第一个空格即版本与状态码的分隔 */
if (sp) { if (sp) {
long st = parse_long_field(sp + 1); sp++;
while (*sp == ' ' || *sp == '\t') sp++;
long st = strtol(sp, NULL, 10); /* "200 OK" -> 200,忽略 reason */
if (st >= 100 && st <= 599) resp->status = (int)st; if (st >= 100 && st <= 599) resp->status = (int)st;
} }
} }
/* 头行遍历 */
p = h; /* 头行遍历:逐行处理,不跳行(旧实现 strstr+&p+=2 会每隔一行跳一行,漏掉 Content-Length 等)。 */
while ((p = strstr(p, "\r\n")) != NULL) { const char *p = eol + 2;
p += 2; while (*p) {
if (*p == '\r' || *p == '\n' || *p == 0) break; if (*p == '\r' || *p == '\n') break; /* 空行 = 头结束 */
const char *eol = strstr(p, "\r\n"); const char *le = strstr(p, "\r\n");
if (!eol) break; if (!le) break;
size_t l = (size_t)(eol - p); size_t l = (size_t)(le - p);
char hdr[256]; char hdr[256];
if (l >= sizeof hdr) { p = eol + 2; continue; } if (l >= sizeof hdr) { p = le + 2; continue; }
memcpy(hdr, p, l); memcpy(hdr, p, l);
hdr[l] = 0; hdr[l] = 0;
char *colon = strchr(hdr, ':'); char *colon = strchr(hdr, ':');
if (!colon) { p = eol + 2; continue; } if (!colon) { p = le + 2; continue; }
*colon = 0; *colon = 0;
char *name = hdr; char *name = hdr;
char *val = colon + 1; char *val = colon + 1;
@@ -181,7 +188,7 @@ static int parse_headers(ncm_http_resp_t *resp) {
resp->has_content_type = 1; resp->has_content_type = 1;
ncm_strlcpy(resp->content_type, val, sizeof resp->content_type); ncm_strlcpy(resp->content_type, val, sizeof resp->content_type);
} }
p = eol + 2; p = le + 2;
} }
return 0; return 0;
} }
@@ -206,7 +213,9 @@ typedef struct {
static void body_st_init(body_st *b, const ncm_http_resp_t *hdr) { static void body_st_init(body_st *b, const ncm_http_resp_t *hdr) {
memset(b, 0, sizeof *b); memset(b, 0, sizeof *b);
if (hdr->chunked) { if (hdr->content_length == 0) {
b->st = BS_DONE; /* Content-Length: 0 → 无 body,避免等对端关闭 */
} else if (hdr->chunked) {
b->st = BS_CHUNK_SZ; b->st = BS_CHUNK_SZ;
} else if (hdr->content_length != (size_t)-1) { } else if (hdr->content_length != (size_t)-1) {
b->st = BS_LEN; b->st = BS_LEN;
@@ -320,7 +329,8 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
if (strcmp(url.scheme, "https") != 0 && strcmp(url.scheme, "http") != 0) return NCM_ERR_ARG; if (strcmp(url.scheme, "https") != 0 && strcmp(url.scheme, "http") != 0) return NCM_ERR_ARG;
uint32_t tmo = req->timeout_ms ? req->timeout_ms : 15000; uint32_t tmo = req->timeout_ms ? req->timeout_ms : 15000;
int64_t deadline = ncm_now_ms() + (int64_t)tmo; int64_t deadline = ncm_monotonic_ms() + (int64_t)tmo; /* 截止时刻用单调钟,避免 NTP 校时跳变 */
mbedtls_net_context net; mbedtls_net_context net;
mbedtls_ssl_context ssl; mbedtls_ssl_context ssl;
@@ -366,8 +376,10 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
} else goto done; } else goto done;
} }
/* 构造请求(先写头、再写体,避免大 body 撑爆单一缓冲) */ /* 构造请求(先写头、再写体,避免大 body 撑爆单一缓冲)。
char headbuf[1024]; * 头缓冲 ≥ extra_hdrs(≤NCM_REQ_HDRS_MAX=4096)+~200,(M2 联调) weapi 带真实会话 cookie 时
* extra_hdrs 可达 ~1.1KB,旧 1024 会溢出返回 NCM_ERR_FULL(请求未发出),故用 8192。 */
char headbuf[8192];
int hl; int hl;
const char *m = req->method ? req->method : "POST"; const char *m = req->method ? req->method : "POST";
int has_body = (req->body && req->body_len > 0); int has_body = (req->body && req->body_len > 0);
@@ -432,19 +444,22 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
if (split) { if (split) {
parse_headers(resp); parse_headers(resp);
body_st_init(&bst, resp); body_st_init(&bst, resp);
size_t consumed = (size_t)(split + 4 - resp->headers); /* split+4 即 body 起始;注意其与 resp->headers+consumed 是同一地址,
/* 当前 tmp 中,已进 headers 的字节数 = space;tmp 剩余部分属于 body */ 故先送 body 再在 consumed 处 NUL 终结头部,否则会覆盖 body 首字节。 */
/* headers 里 split+4 之后的字节实际上没被复制进 headers(因为 space<=..? ) size_t consumed = (size_t)(split + 4 - resp->headers); /* body 起始偏移 */
这里简化:把 tmp[space..n) 送入 body */ size_t old_hlen = resp->headers_len;
resp->headers_len = consumed;
hdr_done = 1; hdr_done = 1;
/* 需要把 headers 缓冲里 split+4 之后的字节(若有) 也送到 body */ /* headers 缓冲里 split+4 之后的字节(本读取已复制进 headers 的部分)送入 body */
size_t hdr_after = resp->headers_len - consumed; size_t hdr_after = old_hlen - consumed;
if (hdr_after > 0) { if (hdr_after > 0) {
if (body_feed(&bst, (const unsigned char *)(split + 4), hdr_after, sink) < 0) { ret=NCM_ERR_JSON; goto done; } if (body_feed(&bst, (const unsigned char *)(split + 4), hdr_after, sink) < 0) { ret=NCM_ERR_JSON; goto done; }
} }
/* tmp 中超出被复制到 headers 的部分也属 body */
if (space < n) { if (space < n) {
if (body_feed(&bst, tmp + space, n - space, sink) < 0) { ret=NCM_ERR_JSON; goto done; } if (body_feed(&bst, tmp + space, n - space, sink) < 0) { ret=NCM_ERR_JSON; goto done; }
} }
resp->headers[consumed] = 0; /* body 已复制走,安全 NUL 终结头部 */
} else { } else {
if (resp->headers_len >= NCM_HTTP_HDR_MAX - 1) { ret = NCM_ERR_FULL; goto done; } if (resp->headers_len >= NCM_HTTP_HDR_MAX - 1) { ret = NCM_ERR_FULL; goto done; }
} }
+3 -2
View File
@@ -154,9 +154,10 @@ int ncm_login_status_check(const char *device_id, const char *cookie,
req.body_len = strlen(body); req.body_len = strlen(body);
req.extra_hdrs = hdrs; req.extra_hdrs = hdrs;
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
req.timeout_ms = 15000; /* 硬超时上限:即使服务端无响应也不久等(歌单请求会独立验证 cookie,status_check 只是取 uid/昵称的辅助) */
req.timeout_ms = 6000;
char respbuf[4096]; char respbuf[16384];
ncm_http_resp_t resp; ncm_http_resp_t resp;
rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp);
if (rc != NCM_OK) return rc; if (rc != NCM_OK) return rc;
+44 -31
View File
@@ -17,24 +17,33 @@
#define APPVER "3.1.17.204416" #define APPVER "3.1.17.204416"
#define CHANNEL "netease" #define CHANNEL "netease"
/* ---------- cookie map ---------- */ /* ---------- cookie map ----------
typedef struct { char name[48]; char val[192]; } ck_t; * 值用 (ptr,len) 指向源串/字符串字面量,避免把 ~1KB 的 MUSIC_U 截断到固定缓冲
* (旧实现 val[192] 会把 834 字节的 MUSIC_U 截到 191,导致服务端识别为游客 session)。
* 所有 ptr 在 ncm_req_build 调用期间有效;ck_to_string 同步使用,不跨调用保留。 */
typedef struct { char name[48]; const char *val; size_t vlen; } ck_t;
typedef struct { ck_t p[48]; int n; } ckmap_t; typedef struct { ck_t p[48]; int n; } ckmap_t;
static void ck_add_ev(ckmap_t *m, const char *name, const char *val) { static void ck_add_ev(ckmap_t *m, const char *name, const char *val) {
if (m->n >= (int)(sizeof m->p / sizeof m->p[0])) return; if (m->n >= (int)(sizeof m->p / sizeof m->p[0])) return;
ncm_strlcpy(m->p[m->n].name, name, sizeof m->p[m->n].name); ncm_strlcpy(m->p[m->n].name, name, sizeof m->p[m->n].name);
ncm_strlcpy(m->p[m->n].val, val ? val : "", sizeof m->p[m->n].val); m->p[m->n].val = val ? val : "";
m->p[m->n].vlen = val ? strlen(val) : 0;
m->n++; m->n++;
} }
static int ck_find(const ckmap_t *m, const char *name) { static int ck_find(const ckmap_t *m, const char *name) {
for (int i = 0; i < m->n; i++) if (ncm_streq(m->p[i].name, name)) return i; for (int i = 0; i < m->n; i++) if (ncm_streq(m->p[i].name, name)) return i;
return -1; return -1;
} }
static void ck_set(ckmap_t *m, const char *name, const char *val) { static void ck_set(ckmap_t *m, const char *name, const char *val, size_t vlen) {
int i = ck_find(m, name); int i = ck_find(m, name);
if (i >= 0) ncm_strlcpy(m->p[i].val, val ? val : "", sizeof m->p[i].val); if (i >= 0) { m->p[i].val = val; m->p[i].vlen = vlen; }
else ck_add_ev(m, name, val); else {
if (m->n >= (int)(sizeof m->p / sizeof m->p[0])) return;
ncm_strlcpy(m->p[m->n].name, name, sizeof m->p[m->n].name);
m->p[m->n].val = val; m->p[m->n].vlen = vlen;
m->n++;
}
} }
static void ck_set_default(ckmap_t *m, const char *name, const char *val) { static void ck_set_default(ckmap_t *m, const char *name, const char *val) {
if (ck_find(m, name) < 0) ck_add_ev(m, name, val); if (ck_find(m, name) < 0) ck_add_ev(m, name, val);
@@ -42,19 +51,19 @@ static void ck_set_default(ckmap_t *m, const char *name, const char *val) {
static void ck_to_string(const ckmap_t *m, char *out, size_t out_sz) { static void ck_to_string(const ckmap_t *m, char *out, size_t out_sz) {
size_t o = 0; size_t o = 0;
for (int i = 0; i < m->n; i++) { for (int i = 0; i < m->n; i++) {
size_t name_len = strlen(m->p[i].name);
size_t vlen = m->p[i].vlen;
int sep = (o > 0) ? 2 : 0; /* "; " */ int sep = (o > 0) ? 2 : 0; /* "; " */
if (o + sep + strlen(m->p[i].name) + 1 + strlen(m->p[i].val) + 1 > out_sz) break; if (o + sep + name_len + 1 + vlen + 1 > out_sz) break;
if (sep) { out[o++] = ';'; out[o++] = ' '; } if (sep) { out[o++] = ';'; out[o++] = ' '; }
size_t nl = strlen(m->p[i].name); memcpy(out + o, m->p[i].name, name_len); o += name_len;
memcpy(out + o, m->p[i].name, nl); o += nl;
out[o++] = '='; out[o++] = '=';
size_t vl = strlen(m->p[i].val); memcpy(out + o, m->p[i].val, vlen); o += vlen;
memcpy(out + o, m->p[i].val, vl); o += vl;
} }
out[o] = 0; out[o] = 0;
} }
/* 解析 cookie 字符串(; 分隔) 进 map */ /* 解析 cookie 字符串(; 分隔) 进 map:值以 (ptr,len) 存(指向 cookie 源串,不截断),按名去重(后值覆盖)。 */
static void ck_parse(ckmap_t *m, const char *cookie) { static void ck_parse(ckmap_t *m, const char *cookie) {
if (!cookie) return; if (!cookie) return;
const char *p = cookie; const char *p = cookie;
@@ -65,13 +74,14 @@ static void ck_parse(ckmap_t *m, const char *cookie) {
const char *semi = strchr(p, ';'); const char *semi = strchr(p, ';');
size_t name_len = eq ? (size_t)(eq - p) : (semi ? (size_t)(semi - p) : strlen(p)); size_t name_len = eq ? (size_t)(eq - p) : (semi ? (size_t)(semi - p) : strlen(p));
if (eq) { if (eq) {
size_t val_len = semi ? (size_t)(semi - eq - 1) : strlen(eq + 1); const char *vstart = eq + 1;
char name[48], val[192]; size_t val_len = semi ? (size_t)(semi - vstart) : strlen(vstart);
char name[48];
if (name_len >= sizeof name) name_len = sizeof name - 1; if (name_len >= sizeof name) name_len = sizeof name - 1;
memcpy(name, p, name_len); name[name_len] = 0; memcpy(name, p, name_len); name[name_len] = 0;
if (val_len >= sizeof val) val_len = sizeof val - 1; /* 去尾空格 */
memcpy(val, eq + 1, val_len); val[val_len] = 0; while (val_len && (vstart[val_len-1]==' '||vstart[val_len-1]=='\t')) val_len--;
ck_set(m, name, val); ck_set(m, name, vstart, val_len);
} }
p = semi ? semi + 1 : p + strlen(p); p = semi ? semi + 1 : p + strlen(p);
} }
@@ -83,7 +93,10 @@ int ncm_req_cookie_get(const char *cookie, const char *name, char *out, size_t o
ck_parse(&m, cookie); ck_parse(&m, cookie);
int i = ck_find(&m, name); int i = ck_find(&m, name);
if (i < 0) return NCM_ERR_NOTFOUND; if (i < 0) return NCM_ERR_NOTFOUND;
ncm_strlcpy(out, m.p[i].val, out_sz); size_t cp = m.p[i].vlen;
if (cp >= out_sz) cp = out_sz - 1;
memcpy(out, m.p[i].val, cp);
out[cp] = 0;
return NCM_OK; return NCM_OK;
} }
@@ -189,23 +202,23 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
if (ncm_url_encode(params, p_enc, sizeof p_enc) < 0) return NCM_ERR_FULL; if (ncm_url_encode(params, p_enc, sizeof p_enc) < 0) return NCM_ERR_FULL;
char referer[256]; char referer[256];
snprintf(referer, sizeof referer, "https://music.163.com"); snprintf(referer, sizeof referer, "https://music.163.com");
/* 会话 cookie + 默认伪装字段补齐 */ /* 会话 cookie + 默认伪装字段补齐。
* 生成值 buffer(nuid/nuid_hex/nnid/wnmcid/nmtid)都提到本块作用域,
* 确保 ck 存的是其值指针在 ck_to_string 调用前仍然有效(ck 只存 ptr,不复制)。 */
char nuid[96], nuid_hex[33], nnid[96], wnmcid[40], nmtid[9];
ckmap_t m; memset(&m, 0, sizeof m); ckmap_t m; memset(&m, 0, sizeof m);
ck_parse(&m, cookie); ck_parse(&m, cookie);
ck_add_ev(&m, "__remember_me", "true"); ck_add_ev(&m, "__remember_me", "true");
ck_add_ev(&m, "ntes_kaola_ad", "1"); ck_add_ev(&m, "ntes_kaola_ad", "1");
char nuid[96]; ncm_random_bytes((unsigned char *)nuid, 16); ncm_random_bytes((unsigned char *)nuid, 16);
/* _ntes_nnid = nuid,ts ; _ntes_nuid = nuid */ /* _ntes_nnid = nuid,ts ; _ntes_nuid = nuid */
ncm_hex_encode((const unsigned char *)nuid, 16, nuid_hex, 0);
snprintf(nnid, sizeof nnid, "%s,%lld", nuid_hex, (long long)ncm_now_ms());
ck_add_ev(&m, "_ntes_nnid", nnid);
ck_add_ev(&m, "_ntes_nuid", nuid_hex);
{ {
char nuid_hex[33]; ncm_hex_encode((const unsigned char *)nuid, 16, nuid_hex, 0);
char nnid[96];
snprintf(nnid, sizeof nnid, "%s,%lld", nuid_hex, (long long)ncm_now_ms());
ck_add_ev(&m, "_ntes_nnid", nnid);
ck_add_ev(&m, "_ntes_nuid", nuid_hex);
}
char wnmcid[40]; {
unsigned char r[16]; ncm_random_bytes(r, 16); unsigned char r[16]; ncm_random_bytes(r, 16);
char h[33]; ncm_hex_encode(r, 16, h, 0); snprintf(wnmcid, sizeof wnmcid, "%s", h); ncm_hex_encode(r, 16, wnmcid, 0);
} }
ck_set_default(&m, "WNMCID", wnmcid); ck_set_default(&m, "WNMCID", wnmcid);
ck_set_default(&m, "WEVNSM", "1.0.0"); ck_set_default(&m, "WEVNSM", "1.0.0");
@@ -216,10 +229,10 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
ck_set_default(&m, "appver", APPVER); ck_set_default(&m, "appver", APPVER);
if (!strstr(path, "login")) { if (!strstr(path, "login")) {
unsigned char r[4]; ncm_random_bytes(r, 4); unsigned char r[4]; ncm_random_bytes(r, 4);
char h[9]; ncm_hex_encode(r, 4, h, 1); ncm_hex_encode(r, 4, nmtid, 1); /* 大写 */
ck_set_default(&m, "NMTID", h); ck_set_default(&m, "NMTID", nmtid);
} }
char cookie_full[2048]; char cookie_full[4096];
ck_to_string(&m, cookie_full, sizeof cookie_full); ck_to_string(&m, cookie_full, sizeof cookie_full);
int n = snprintf(hdrs, hdrs_sz, int n = snprintf(hdrs, hdrs_sz,
Binary file not shown.
Binary file not shown.