feat(ncm): MUSIC_U 临期主动续期 — 启动查龄超阈值后台静默刷新/eapi 会话锚点/cookie 合并+冷却

用户定案:失效前用当前 cookie 换新登录态(用户无感);仅刷新失败才退扫码重登。

- core/ncm_req.c|h:eapi Cookie 头补齐会话锚点 MUSIC_U/MUSIC_A(对齐 request.rs,
  补 §36.5 遗留——登录刷新/song_url 会员档依赖);新增 ncm_req_cookie_merge
  (patch 覆盖 base 同名 cookie,MUSIC_A/__remember_me 等不丢)。
- core/ncm_login.c|h:ncm_login_refresh(eapi /api/login/token/refresh 空体,
  code200+set-cookie→merge;code!=200→NCM_ERR_AUTH 退扫码;网络/TLS→负码退避)。
- app/logic/account.c:account.json 增 last_refresh_ts + 拆分 cvip/svip;
  account_cookie_maybe_refresh(年龄阈值 12h 可配 ncm.cookie_refresh_h,单飞/boot,
  失败冷却 30min 跨 boot 持久化,AUTH 置回登录页标志)+ mark_fresh/need_relogin。
- app/ui/home.c:home_worker 先续期再拉歌单;app/ui/login.c:803 落盘后 mark_fresh;
  app/app.c:UI tick 消费回登录标志 → logout+切登录页。
- 新增测试:test_ncm_core 的 cookie_merge + eapi_musicu_hdr;bench/cookie_refresh_sim
  (端点语义,0 网络);bench/account_cookie_sim(临期/未临期/退避/失效/冷却,0 网络)。

验证:host+qemu 核心测试 PASS;build_ncm_sims.sh 全家桶 PASS;build_ncm.sh 0 warning。
真机只读探针 2 次(设备 10.78.193.50,不写回):refresh 走 eapi 域
interface.music.163.com 在当前网络被 TLS reset(alert 80)→ rc=-5 瞬断退避,
session.enc/deviceId md5 前后一致(登录态零损伤)。新 ncm md5 c960244b。
This commit is contained in:
AskaEth committed 2026-09-02 14:30:04 +08:00
1 parent de18cef02c
commit cd7329d141
17 files changed
+673 -13

No files matched your search

+44
View File
@@ -187,6 +187,50 @@ int ncm_login_status_check(const char *device_id, const char *cookie,
return NCM_OK;
}
int ncm_login_refresh(const char *device_id, const char *cookie,
char *out_cookie, size_t out_cookie_sz) {
if (!device_id || !cookie) return NCM_ERR_ARG;
char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX];
int rc = ncm_req_build(device_id, cookie, "/api/login/token/refresh", "{}",
NCM_CRYPT_EAPI,
body, sizeof body, hdrs, sizeof hdrs, url, sizeof url);
if (rc != NCM_OK) return rc;
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 = 10000;
char respbuf[16384];
ncm_http_resp_t resp;
rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp);
if (rc != NCM_OK) return rc; /* 网络/TLS/超时 → 调用方退避 */
if (resp.status != 200) return NCM_ERR_HTTP; /* 异常 HTTP → 调用方退避 */
/* 提取响应 set-cookie(会话轮换:MUSIC_U/__csrf),patch 进现存 cookie */
char captured[4096] = "";
ncm_http_parse_set_cookie(resp.headers, resp.headers_len, captured, sizeof captured);
ncm_json_t j;
jsmntok_t toks[512];
int parent[512];
rc = ncm_json_parse(&j, resp.body, resp.body_len, toks, 512, parent);
if (rc < 0) return NCM_ERR_JSON;
long code = -1;
if (ncm_json_path_int(&j, "code", &code) != NCM_OK) return NCM_ERR_JSON;
if (code != 200) return NCM_ERR_AUTH; /* 301=未登录 / -460=风控 / 其它 → 会话失效,重登 */
if (out_cookie)
ncm_req_cookie_merge(cookie, captured, out_cookie, out_cookie_sz);
return NCM_OK;
}
int ncm_login_save_cookie(const ncm_store_t *store, const char *cookie) {
return ncm_store_save_session(store, cookie);
}
+7
View File
@@ -47,6 +47,13 @@ int ncm_login_poll(const char *device_id, const char *unikey, const char *cur_co
int ncm_login_status_check(const char *device_id, const char *cookie,
char *uid, size_t usz, char *nickname, size_t nsz);
/* 登录续期(eapi /api/login/token/refresh,空体):带当前 cookie 换取新登录态。
* 成功(code==200)把响应 set-cookie 的新 MUSIC_U/__csrf patch 进 cookie 写 out_cookie 并返
* NCM_OK;code!=200(301/-460/会话失效)返 NCM_ERR_AUTH(调用方应清登录态进扫码);网络/TLS/
* 超时/HTTP 异常返负错误码(调用方退避,保持旧 cookie)。 */
int ncm_login_refresh(const char *device_id, const char *cookie,
char *out_cookie, size_t out_cookie_sz);
/* 会话 cookie 加密落盘/读回(机器绑定密钥,仅防裸读)。 */
int ncm_login_save_cookie(const ncm_store_t *store, const char *cookie);
int ncm_login_load_cookie(const ncm_store_t *store, char *cookie, size_t sz);
+40 -5
View File
@@ -100,6 +100,17 @@ int ncm_req_cookie_get(const char *cookie, const char *name, char *out, size_t o
return NCM_OK;
}
/* 合并 cookie:先 parse base(字段指针指向 base 源串),再 parse patch——同名字段被 patch 值
* 覆盖(ck_set 替换值指针),新字段追加;ck_to_string 在调用期间同步使用,指针不外泄。 */
int ncm_req_cookie_merge(const char *base, const char *patch, char *out, size_t out_sz) {
if (!out || out_sz == 0) return NCM_ERR_ARG;
ckmap_t m; memset(&m, 0, sizeof m);
ck_parse(&m, base);
ck_parse(&m, patch); /* patch 覆盖 base(后值覆盖先值) */
ck_to_string(&m, out, out_sz);
return NCM_OK;
}
/* ---------- json 注入 ---------- */
/* 把 json{...} 内部插入 ",<extra>"(尾部)。extra 形如 "\"csrf_token\":\"x\""(无前导逗号) */
static int json_inject(const char *json, const char *extra, char *out, size_t out_sz) {
@@ -155,6 +166,20 @@ static void hm_to_cookie(const hmap_tab *h, char *out, size_t out_sz) {
out[o < out_sz ? o : out_sz - 1] = 0;
}
/* eapi 会话 cookie 锚点:把 cookie 中某个字段(MUSIC_U / MUSIC_A)urlencode 后追加到
* Cookie 头。MUSIC_U 值可达 ~800B 不进 hmap(val[256] 放不下),故单独附加。有界附加:
* 放不下则静默停止(不截断破坏 Cookie 头)。 */
static void eapi_append_cookie(const char *cookie, const char *name, char *hdr, size_t hdr_sz) {
char val[3072] = "";
if (ncm_req_cookie_get(cookie, name, val, sizeof val) != NCM_OK) return;
char ev[4096];
if (ncm_url_encode(val, ev, sizeof ev) < 0) return;
size_t o = strlen(hdr);
if (o >= hdr_sz) return;
int n = snprintf(hdr + o, hdr_sz - o, "%s%s=%s", (o > 0) ? "; " : "", name, ev);
if (n < 0 || (size_t)n >= hdr_sz - o) hdr[o] = 0; /* 放不下:还原,丢弃本次附加 */
}
/* head 注入(eapi):把 __csrf/deviceId/os/... 等填到 header map 与 cookie。
* 同时产出 json.header 片段。 */
static void eapi_fill_header(const char *device_id, const char *csrf,
@@ -283,14 +308,24 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
char params[4096];
if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO;
char cookie_hdr[2048];
char cookie_hdr[3072];
hm_to_cookie(&h, cookie_hdr, sizeof cookie_hdr);
/* 游客 cookie 伪装(NCMAPI_NOTES §4):无 MUSIC_U 时补 MUSIC_A=anonymous_token
* 到 eapi Cookie 头(weapi 分支同样注入);login 路径不注入。仅检测存在性
* (MUSIC_U 值可能很长,无需拷出)。 */
/* 会话 cookie 锚点(对齐 request.rs):eapi Cookie 头需带 MUSIC_U(登录态识别——
* 登录刷新/song_url 会员档等必须)与 MUSIC_A(游客)。MUSIC_U 值长不进 hmap,
* 单独 urlencode 追加。仅当 cookie 确有该字段才追加(游客/未登录不注入)。 */
{
char musicu[8];
if (ncm_req_cookie_get(cookie, "MUSIC_U", musicu, sizeof musicu) == NCM_OK)
eapi_append_cookie(cookie, "MUSIC_U", cookie_hdr, sizeof cookie_hdr);
if (ncm_req_cookie_get(cookie, "MUSIC_A", musicu, sizeof musicu) == NCM_OK)
eapi_append_cookie(cookie, "MUSIC_A", cookie_hdr, sizeof cookie_hdr);
}
/* 游客 cookie 伪装(NCMAPI_NOTES §4):无 MUSIC_U/MUSIC_A 时补 MUSIC_A=anonymous_token
* 到 eapi Cookie 头;login 路径不注入(登录端点自身处理匿名)。 */
if (!strstr(path, "login")) {
char musicu[8];
if (ncm_req_cookie_get(cookie, "MUSIC_U", musicu, sizeof musicu) != NCM_OK)
if (ncm_req_cookie_get(cookie, "MUSIC_U", musicu, sizeof musicu) != NCM_OK &&
ncm_req_cookie_get(cookie, "MUSIC_A", musicu, sizeof musicu) != NCM_OK)
ncm_strlcat(cookie_hdr, "; MUSIC_A=anonymous_token", sizeof cookie_hdr);
}
+5
View File
@@ -44,6 +44,11 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
/* 从 cookie 串取某个字段值(如 __csrf)。找到返回 0。*/
int ncm_req_cookie_get(const char *cookie, const char *name, char *out, size_t out_sz);
/* 合并 cookie:patch 中每个 name=value 覆盖 base 同名字段(无则追加),其余 base 字段保留。
* 用于登录续期:把响应的新 MUSIC_U/__csrf patch 进现有会话 cookie(base 的 MUSIC_A/__remember_me
* 等其它 cookie 不丢失),输出为按名去重、"; " 连接的完整 cookie 串。返回 NCM_OK。 */
int ncm_req_cookie_merge(const char *base, const char *patch, char *out, size_t out_sz);
#ifdef __cplusplus
}
#endif