Files
AskaEth cd7329d141 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。
2026-09-02 14:30:04 +08:00

135 lines
6.2 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* bench/cookie_refresh_sim.c — MUSIC_U 登录续期端点语义验证(§NEW,0 真实 NCM 请求)。
*
* 用真实 core(ncm_login.c / ncm_req.c / ncm_crypto.c / ncm_json.c / ncm_common.c),
* 仅把网络层换成计数桩(canned eapi set-cookie 响应 / 网络错误),验证 ncm_login_refresh:
* ① 成功(code200 + Set-Cookie MUSIC_U/__csrf)→ NCM_OK,新 MUSIC_U patch 进 cookie、
* 旧 cookie 其它字段(MUSIC_A/__remember_me)保留(= 新 cookie 替换落盘的合并源);
* ② 会话失效(code301)→ NCM_ERR_AUTH(调用方据此退扫码重登);
* ③ 网络/TLS 瞬断 → 负错误码(调用方退避,保持旧 cookie);
* ④ code200 但无 set-cookie(服务端未轮换)→ NCM_OK,cookie 原样(不损坏)。
* 风控纪律:0 次真实 NCM 请求;桩按 mode 切换。
* 编译(宿主),在 ncm_app 目录:见 build_ncm_sims.sh 新增 cookie_refresh_sim 段。
* 运行:./cookie_refresh_sim
*/
#include "ncm.h"
#include "ncm_http.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static int g_mode; /* 0=成功+set-cookie; 1=code301; 2=网络错误; 3=code200 无 set-cookie */
static int g_http_calls; /* ncm_http_request 桩计数 = 真实网络调用数 */
static const char *HDR_OK =
"Content-Type: application/json\r\n"
"Set-Cookie: MUSIC_U=NEWTOKEN123; Path=/; Max-Age=7776000; HttpOnly\r\n"
"Set-Cookie: __csrf=newcsrf99; Domain=.music.163.com\r\n";
static const char *BODY_OK = "{\"code\":200,\"message\":\"ok\"}";
static const char *BODY_AUTH = "{\"code\":301,\"message\":\"需要登录\"}";
int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_http_resp_t *resp) {
(void)req; (void)buf_sz;
g_http_calls++;
memset(resp, 0, sizeof *resp);
if (g_mode == 2) return NCM_ERR_NET; /* 网络瞬断 */
const char *body = (g_mode == 1) ? BODY_AUTH : BODY_OK;
size_t bn = strlen(body);
memcpy(buf, body, bn);
resp->status = 200;
resp->body = buf;
resp->body_len = bn;
resp->content_length = bn;
if (g_mode == 0) { /* 带 Set-Cookie 轮换 */
size_t hn = strlen(HDR_OK);
memcpy(resp->headers, HDR_OK, hn);
resp->headers_len = hn;
}
return NCM_OK;
}
/* 复刻 ncm_http.c 的 set-cookie 提取(解析所有 Set-Cookie 名值,去属性,"; " 连接) */
int ncm_http_parse_set_cookie(const char *hdrs, size_t len, char *out, size_t out_sz) {
if (!hdrs || !out || out_sz == 0) return -1;
out[0] = 0;
size_t o = 0;
int count = 0;
const char *p = hdrs;
const char *end = hdrs + len;
while ((p = strstr(p, "Set-Cookie:")) != NULL && p < end) {
const char *val = p + strlen("Set-Cookie:");
while (*val == ' ' || *val == '\t') val++;
const char *semi = strchr(val, ';');
size_t nlen = semi ? (size_t)(semi - val) : strlen(val);
while (nlen && (val[nlen-1]==' '||val[nlen-1]=='\t')) nlen--;
if (nlen == 0) { p = val; continue; }
if (o > 0) { if (o + 2 >= out_sz) break; out[o++] = ';'; out[o++] = ' '; }
if (o + nlen >= out_sz) break;
memcpy(out + o, val, nlen);
o += nlen;
count++;
p = val;
}
out[o] = 0;
return count;
}
void ncm_http_set_verify_default(int verify, const char *ca_path) { (void)verify; (void)ca_path; }
static int g_fail = 0;
#define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0)
static const char *g_device = "AA11BB22CC33DD44EE55FF6600112233445566778899AABB00";
int main(void) {
/* 基础会话 cookie(MUSIC_U 在中间,__remember_me/MUSIC_A 在后——验证不丢失、不被重排破坏) */
static char cookie[NCM_COOKIE_MAX];
snprintf(cookie, sizeof cookie,
"__csrf=old_csrf; MUSIC_U=OLD_TOKEN_abc_123; MUSIC_A=anon_a; __remember_me=true");
char out[NCM_COOKIE_MAX];
char v[64];
puts("== cookie_refresh_sim: ncm_login_refresh endpoint semantics ==");
/* ① 成功:code200 + Set-Cookie 轮换 → 新 MUSIC_U 覆盖旧值,其余字段保留 */
g_mode = 0; g_http_calls = 0;
int rc = ncm_login_refresh(g_device, cookie, out, sizeof out);
CHECK(rc == NCM_OK, "success rc=0 (got %d)", rc);
CHECK(g_http_calls == 1, "exactly 1 http call (got %d)", g_http_calls);
CHECK(ncm_req_cookie_get(out, "MUSIC_U", v, sizeof v) == NCM_OK &&
strcmp(v, "NEWTOKEN123") == 0, "MUSIC_U replaced with NEWTOKEN123 (got %s)", v);
CHECK(ncm_req_cookie_get(out, "__csrf", v, sizeof v) == NCM_OK &&
strcmp(v, "newcsrf99") == 0, "__csrf replaced (got %s)", v);
CHECK(ncm_req_cookie_get(out, "MUSIC_A", v, sizeof v) == NCM_OK &&
strcmp(v, "anon_a") == 0, "MUSIC_A preserved (got %s)", v);
CHECK(ncm_req_cookie_get(out, "__remember_me", v, sizeof v) == NCM_OK &&
strcmp(v, "true") == 0, "__remember_me preserved (got %s)", v);
/* ② 会话失效:code301 → NCM_ERR_AUTH(退扫码重登) */
g_mode = 1; g_http_calls = 0;
rc = ncm_login_refresh(g_device, cookie, out, sizeof out);
CHECK(rc == NCM_ERR_AUTH, "auth-fail rc=%d (期望 %d)", rc, NCM_ERR_AUTH);
/* ③ 网络瞬断 → 负错误码(退避,保持旧 cookie 由调用方保证) */
g_mode = 2; g_http_calls = 0;
rc = ncm_login_refresh(g_device, cookie, out, sizeof out);
CHECK(rc == NCM_ERR_NET, "net-fail rc=%d (期望 %d)", rc, NCM_ERR_NET);
/* ④ code200 但无 set-cookie(未轮换)→ NCM_OK,cookie 原样(无害) */
g_mode = 3; g_http_calls = 0;
memset(out, 0, sizeof out);
rc = ncm_login_refresh(g_device, cookie, out, sizeof out);
CHECK(rc == NCM_OK, "code200-no-cookie rc=0 (got %d)", rc);
CHECK(strcmp(out, cookie) == 0, "no-cookie: cookie unchanged");
/* ⑤ 无 cookie(未登录)→ 参数允许空 body 字段但仍应优雅(eapi 头不注入 MUSIC_U) */
g_mode = 3; g_http_calls = 0;
rc = ncm_login_refresh(g_device, "", out, sizeof out);
CHECK(rc == NCM_OK, "empty-cookie still builds http ok (got %d)", rc);
printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS ==");
return g_fail ? 1 : 0;
}