Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/session_gate_sim.c
T
AskaEth 31879f3302 fix(ncm): 缓存命中改单请求出参,移除全局命中计数(评审 P2-2/P2-1)
问题(P2-2):离线判定用 `ncm_apicache_hits()` 前后差值判「本次是否缓存命中」——
①其它 worker 的命中会污染差值 → 误判「已回在线」;②盘缓存分支的 g_hits++ 在 g_mu 锁外,
跨线程自增/64 位读存在竞争与撕裂可能。

修法(出参方案):
- `api_post_cached(..., int *from_cache)`:命中置 1(未走网络),miss/失败为 0;
  四个调用点显式传参。
- 新增 `ncm_api_user_playlist_ex(..., int *out_from_cache)`;out_n 与原接口一致**必填**
  (NULL→NCM_ERR_ARG,修入口容忍 NULL、成功路径却无条件解引用的契约不一致,评审 P2-1);
  out_from_cache 可 NULL。旧 `ncm_api_user_playlist` 为薄包装(调用方零改动)。
- `appstate_load_playlists_ex` 透传该标志;home_worker 用本次请求的 from_cache 判定是否
  回在线;歌单页只在失败时同步离线态(成功可能是缓存命中,不据此清离线)。
- 删除 `ncm_apicache_hits()`/g_hits(无使用者,同时消除数据竞争);盘缓存命中分支两行
  合并为一行(行为不变,重构)。
- 测试:session_gate_sim 新增 D2 五例(miss→0 且真网络 +1 次 / 命中→1 且 0 请求 /
  失败→0 / 兼容入口不崩 / out_n 必填契约);playlist_race_sim、plpage_sim 补 `_ex` 桩。

验证:session_gate_sim 88 PASS/0 FAIL;test_ncm_core_host 139 PASS/0 FAIL;
http_truncate_sim 13 PASS/0 FAIL;核心 13 TU(-Wall -Wextra)0 warning。
2026-11-05 11:59:33 +08:00

472 lines
24 KiB
C
Raw 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/session_gate_sim.c — M9 离线会话保持 / 登录门决策 宿主单测(0 真实网络)
*
* 覆盖(真实 core 代码 + 网络桩,0 请求):
* A. 错误分类矩阵 ncm_err_classify:-4/-5/-6/-17 网络类;-13 鉴权;JSON/IO/其它 = other;
* 历史泄漏的 mbedtls 裸负码按网络类兜底。
* B. HTTP 状态分类 ncm_http_status_class / ncm_http_status_err:
* 200/204 → ok;401/403 → auth;408/425/429/5xx → transient(SVC);3xx/其它 4xx → error(HTTP)。
* C. 登录门 ncm_gate_decide / ncm_session_boot_decide:在线/离线/鉴权失败/启动无网/无会话。
* D. 真实 ncm_api(ncm_http_request 桩)HTTP 状态与业务码映射:
* 401/403 与业务 301/302 → AUTH(退登录);5xx/429、业务 -460 → SVC(保登录态/离线);
* HTML 体 → JSON(other);其它业务码 → HTTP(other)。
* D2. `ncm_api_user_playlist_ex` 的 from_cache 出参:miss→0(真网络 +1 次)、命中→1(0 请求)、
* 失败→0;兼容入口不崩;out_n 必填契约(NULL→NCM_ERR_ARG)(离线判定不得用全局计数,
* 评审 P2-2/P2-1)。
* E. 真实 ncm_login_status_check / ncm_login_refresh 同上(AUTH 只在服务端明确否定时)。
* F. 会话加密密钥稳定性(M9 根因回归):session.enc 保存后改变网卡 MAC(NCM_MAC 桩)仍能解出
* cookie;旧版 MAC 派生密文可解且自动迁移到 .mkey 密钥。
*
* 编译(在 ncm_app 下;先 bash build_core.sh 生成宿主核心对象):
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/jsmn -Ivendor/mbedtls/include \
* -c bench/session_gate_sim.c -o out/chk/sg.o
* gcc -O2 -o session_gate_sim out/chk/sg.o \
* out/core-host/ncm_api.o out/core-host/ncm_login.o out/core-host/ncm_req.o \
* out/core-host/ncm_crypto.o out/core-host/ncm_json.o out/core-host/ncm_common.o \
* out/core-host/ncm_store.o out/core-host/ncm_apicache.o out/core-host/ncm_session.o \
* out/core-host/jsmn_impl.o out/core-host/v_qrcodegen.o \
* -Lout/mbedtls/host -lmbedtls -lmbedx509 -lmbedcrypto -lpthread -lm
* TMPDIR 可用(Termux /tmp 不可写时用 $TMPDIR 或 HOME/.cache)。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/stat.h>
#include "ncm.h"
#include "ncm_http.h"
#include "mbedtls/aes.h"
static int g_fail = 0;
#define CHECK(cond, ...) do { \
int _ok = (cond) ? 1 : 0; \
printf(" %s ", _ok ? "PASS" : "FAIL"); printf(__VA_ARGS__); printf("\n"); \
if (!_ok) g_fail++; \
} while (0)
/* ============================================================
* ncm_http_request 桩:按 g_stub_* 脚本返回(不触碰网络/文件)
* ============================================================ */
static int g_stub_rc; /* 非 0:直接返回该传输层错误码 */
static int g_stub_status; /* HTTP 状态 */
static const char *g_stub_body; /* 响应体(缺省 "") */
static const char *g_stub_cookie; /* Set-Cookie 头(可 NULL) */
static int g_http_calls;
int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_http_resp_t *resp)
{
(void)req;
g_http_calls++;
memset(resp, 0, sizeof *resp);
if (g_stub_rc) return g_stub_rc;
const char *body = g_stub_body ? g_stub_body : "";
size_t n = strlen(body);
if (n >= buf_sz) n = buf_sz - 1;
memcpy(buf, body, n);
resp->body = buf;
resp->body_len = n;
resp->status = g_stub_status;
if (g_stub_cookie) {
/* 真实响应每个 cookie 一条 Set-Cookie 头;测试串用 "; " 分隔多 cookie。 */
size_t o = 0;
const char *p = g_stub_cookie;
while (*p && o < sizeof resp->headers) {
const char *sep = strstr(p, "; ");
size_t n = sep ? (size_t)(sep - p) : strlen(p);
int hn = snprintf(resp->headers + o, sizeof resp->headers - o,
"Set-Cookie: %.*s\r\n", (int)n, p);
if (hn <= 0) break;
o += (size_t)hn;
if (!sep) break;
p = sep + 2;
}
resp->headers_len = o;
}
return NCM_OK;
}
static void stub(int status, const char *body, const char *cookie_set)
{
g_stub_rc = NCM_OK; g_stub_status = status; g_stub_body = body; g_stub_cookie = cookie_set;
}
static void stub_err(int rc)
{
g_stub_rc = rc; g_stub_status = 0; g_stub_body = NULL; g_stub_cookie = NULL;
}
/* ncm_http_parse_set_cookie:本 sim 不链 ncm_http.o(ncm_http_request 已桩化),
* 这里放一份与 core/ncm_http.c 相同的实现,供 ncm_login_refresh 提取 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;
}
/* ============================================================
* A. 错误分类
* ============================================================ */
static void test_err_class(void)
{
printf("== A. 错误分类(-5/-4/-6/超时/DNS/连接拒/5xx vs 401/业务 301/302)==\n");
struct { int rc; ncm_fail_class_t want; const char *why; } m[] = {
{ NCM_OK, NCM_FAIL_NONE, "NCM_OK" },
{ NCM_ERR_NET, NCM_FAIL_NETWORK, "-4 网络错误(DNS/连接拒绝/连接中断)" },
{ NCM_ERR_TLS, NCM_FAIL_NETWORK, "-5 TLS 错误(历史真机 eapi 域 reset 即此码)" },
{ NCM_ERR_TIMEOUT, NCM_FAIL_NETWORK, "-6 超时" },
{ NCM_ERR_SVC, NCM_FAIL_NETWORK, "-17 服务端 5xx/429/-460 暂不可用" },
{ NCM_ERR_AUTH, NCM_FAIL_AUTH, "-13 服务端明确未登录/鉴权失败" },
{ NCM_ERR_JSON, NCM_FAIL_OTHER, "-9 JSON(数据问题,不登出)" },
{ NCM_ERR_HTTP, NCM_FAIL_OTHER, "-7 HTTP 非 2xx(其它)" },
{ NCM_ERR_IO, NCM_FAIL_OTHER, "-3 文件/截断 IO" },
{ NCM_ERR_NOTFOUND, NCM_FAIL_OTHER, "-10 缺字段" },
{ -0x7280, NCM_FAIL_NETWORK, "mbedtls 裸负码(历史泄漏)→ 网络类兜底" },
};
for (size_t i = 0; i < sizeof m / sizeof m[0]; i++) {
ncm_fail_class_t got = ncm_err_classify(m[i].rc);
CHECK(got == m[i].want, "classify(%d)=%s 期望 %s [%s]", m[i].rc,
ncm_fail_class_name(got), ncm_fail_class_name(m[i].want), m[i].why);
}
CHECK(ncm_err_is_network(NCM_ERR_TLS) == 1 && ncm_err_is_network(NCM_ERR_AUTH) == 0,
"ncm_err_is_network: TLS=1 / AUTH=0");
}
/* ============================================================
* B. HTTP 状态 → 分类/错误码
* ============================================================ */
static void test_http_class(void)
{
printf("== B. HTTP 状态分类(401/403 vs 5xx vs 3xx/4xx)==\n");
struct { int st; ncm_http_cls_t cls; int err; } m[] = {
{ 200, NCM_HTTP_CLS_OK, NCM_OK },
{ 204, NCM_HTTP_CLS_OK, NCM_OK },
{ 401, NCM_HTTP_CLS_AUTH, NCM_ERR_AUTH },
{ 403, NCM_HTTP_CLS_AUTH, NCM_ERR_AUTH },
{ 408, NCM_HTTP_CLS_TRANSIENT, NCM_ERR_SVC },
{ 425, NCM_HTTP_CLS_TRANSIENT, NCM_ERR_SVC },
{ 429, NCM_HTTP_CLS_TRANSIENT, NCM_ERR_SVC },
{ 500, NCM_HTTP_CLS_TRANSIENT, NCM_ERR_SVC },
{ 502, NCM_HTTP_CLS_TRANSIENT, NCM_ERR_SVC },
{ 503, NCM_HTTP_CLS_TRANSIENT, NCM_ERR_SVC },
{ 301, NCM_HTTP_CLS_ERROR, NCM_ERR_HTTP },
{ 302, NCM_HTTP_CLS_ERROR, NCM_ERR_HTTP },
{ 400, NCM_HTTP_CLS_ERROR, NCM_ERR_HTTP },
{ 404, NCM_HTTP_CLS_ERROR, NCM_ERR_HTTP },
};
for (size_t i = 0; i < sizeof m / sizeof m[0]; i++) {
ncm_http_cls_t c = ncm_http_status_class(m[i].st);
int e = ncm_http_status_err(m[i].st);
CHECK(c == m[i].cls && e == m[i].err, "status %d -> cls=%d err=%d(期望 %d/%d)",
m[i].st, (int)c, e, (int)m[i].cls, m[i].err);
}
}
/* ============================================================
* C. 登录门决策
* ============================================================ */
static void test_gate(void)
{
printf("== C. 登录门决策(离线/在线/鉴权失败/启动无网)==\n");
struct { int logged_in; int rc; ncm_gate_t want; const char *why; } m[] = {
{ 1, NCM_OK, NCM_GATE_ONLINE, "请求成功 → 在线" },
{ 1, NCM_ERR_NET, NCM_GATE_OFFLINE, "断网/不可达 → 离线(保持账号态)" },
{ 1, NCM_ERR_TLS, NCM_GATE_OFFLINE, "TLS 失败 → 离线" },
{ 1, NCM_ERR_TIMEOUT, NCM_GATE_OFFLINE, "超时 → 离线" },
{ 1, NCM_ERR_SVC, NCM_GATE_OFFLINE, "服务端 5xx/-460 → 离线(可重试,不登出)" },
{ 1, NCM_ERR_AUTH, NCM_GATE_RELOGIN, "业务 301/HTTP 401 → 退登录" },
{ 1, NCM_ERR_JSON, NCM_GATE_HOLD, "解析失败 → 保持现状(不登出)" },
{ 1, NCM_ERR_NOTFOUND, NCM_GATE_HOLD,"缺字段 → 保持现状" },
{ 0, NCM_OK, NCM_GATE_RELOGIN, "本地无会话 → 扫码" },
{ 0, NCM_ERR_NET, NCM_GATE_RELOGIN, "本地无会话(无网也一样扫码)" },
};
for (size_t i = 0; i < sizeof m / sizeof m[0]; i++) {
ncm_gate_t got = ncm_gate_decide(m[i].logged_in, m[i].rc);
CHECK(got == m[i].want, "gate(logged_in=%d, rc=%d)=%s 期望 %s [%s]",
m[i].logged_in, m[i].rc, ncm_gate_name(got), ncm_gate_name(m[i].want), m[i].why);
}
printf("-- 启动决策(有本地会话就先信它;无网也进 home/缓存)--\n");
struct { int has_cookie; int probe_rc; ncm_gate_t want; const char *why; } b[] = {
{ 1, NCM_OK, NCM_GATE_ONLINE, "有会话 + 探测成功 → 在线" },
{ 1, NCM_ERR_NET, NCM_GATE_OFFLINE, "启动时就没网 → 离线(绝不进扫码页)" },
{ 1, NCM_ERR_TLS, NCM_GATE_OFFLINE, "启动时 TLS 失败 → 离线" },
{ 1, NCM_ERR_AUTH, NCM_GATE_RELOGIN, "服务端明确否定 → 扫码" },
{ 1, NCM_ERR_JSON, NCM_GATE_ONLINE, "不确定的失败 → 不赶人,先进 App" },
{ 0, NCM_OK, NCM_GATE_RELOGIN, "无本地会话 → 扫码" },
};
for (size_t i = 0; i < sizeof b / sizeof b[0]; i++) {
ncm_gate_t got = ncm_session_boot_decide(b[i].has_cookie, b[i].probe_rc);
CHECK(got == b[i].want, "boot(cookie=%d, probe=%d)=%s 期望 %s [%s]",
b[i].has_cookie, b[i].probe_rc, ncm_gate_name(got), ncm_gate_name(b[i].want),
b[i].why);
}
}
/* ============================================================
* D. 真实 ncm_api:HTTP 状态 / 业务码 → 错误码
* ============================================================ */
static void test_api_map(void)
{
printf("== D. ncm_api(user_playlist)状态/业务码映射 ==\n");
ncm_apicache_init(NULL);
const char *json_ok = "{\"code\":200,\"playlist\":[{\"id\":1,\"name\":\"a\",\"trackCount\":2}]}";
struct { int status; const char *body; int stub_err; int want; const char *why; } m[] = {
{ 200, json_ok, 0, NCM_OK, "200+code200 → 成功" },
{ 401, "<html>401</html>", 0, NCM_ERR_AUTH, "HTTP 401 → AUTH(退登录)" },
{ 403, "{}", 0, NCM_ERR_AUTH, "HTTP 403 → AUTH" },
{ 500, "<html>oops</html>", 0, NCM_ERR_SVC, "HTTP 500(HTML)→ SVC(保留登录态)" },
{ 503, "{\"code\":200}", 0, NCM_ERR_SVC, "HTTP 503 → SVC" },
{ 429, "", 0, NCM_ERR_SVC, "HTTP 429 → SVC" },
{ 404, "{}", 0, NCM_ERR_HTTP, "HTTP 404 → HTTP(不登出)" },
{ 200, "{\"code\":301}", 0, NCM_ERR_AUTH, "业务 301 → AUTH" },
{ 200, "{\"code\":302}", 0, NCM_ERR_AUTH, "业务 302 → AUTH" },
{ 200, "{\"code\":-460}", 0, NCM_ERR_SVC, "业务 -460 风控 → SVC(M9:旧版误判 AUTH)" },
{ 200, "<html>not json</html>", 0, NCM_ERR_JSON, "200+非 JSON → JSON/NOTFOUND(other,不登出)" },
{ 200, "{\"code\":-447}", 0, NCM_ERR_HTTP, "其它业务码 → HTTP(other)" },
{ 0, NULL, NCM_ERR_NET, NCM_ERR_NET, "-4 传输层不可达 → NET(离线)" },
{ 0, NULL, NCM_ERR_TLS, NCM_ERR_TLS, "-5 TLS → TLS(离线)" },
{ 0, NULL, NCM_ERR_TIMEOUT, NCM_ERR_TIMEOUT, "-6 超时 → TIMEOUT(离线)" },
};
for (size_t i = 0; i < sizeof m / sizeof m[0]; i++) {
ncm_apicache_clear();
if (m[i].stub_err) stub_err(m[i].stub_err);
else stub(m[i].status, m[i].body, NULL);
ncm_playlist_t pls[4];
int n = -1;
int rc = ncm_api_user_playlist("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n);
/* 非 JSON 体:「解析失败」具体码可能是 JSON(-9) 或 NOTFOUND(-10)(jsmn 把裸词当原语),
* 关键语义是**不属于鉴权/网络类**(不登出、不改离线态)。 */
int ok = (rc == m[i].want) ||
(m[i].want == NCM_ERR_JSON && ncm_err_classify(rc) == NCM_FAIL_OTHER);
CHECK(ok, "[%s] rc=%d(期望 %d)", m[i].why, rc, m[i].want);
}
}
/* ============================================================
* D2. user_playlist_ex:本次结果是否来自接口缓存(评审 P2-2)
* ============================================================ */
static void test_api_from_cache(void)
{
printf("== D2. ncm_api_user_playlist_ex 缓存命中出参(离线判定用)==\n");
ncm_apicache_init(NULL);
ncm_apicache_clear();
const char *body = "{\"code\":200,\"playlist\":[{\"id\":1,\"name\":\"a\",\"trackCount\":1}]}";
ncm_playlist_t pls[4];
int n = 0, fc = -1, rc;
stub(200, body, NULL);
int calls0 = g_http_calls;
rc = ncm_api_user_playlist_ex("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n, &fc);
CHECK(rc == NCM_OK && fc == 0 && g_http_calls == calls0 + 1 && n == 1,
"首次(缓存 miss):rc=0 n=%d from_cache=%d,真网络 +1 次(calls %d→%d)",
n, fc, calls0, g_http_calls);
fc = -1;
calls0 = g_http_calls;
rc = ncm_api_user_playlist_ex("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n, &fc);
CHECK(rc == NCM_OK && fc == 1 && g_http_calls == calls0 && n == 1,
"二次同参(缓存命中):rc=0 from_cache=%d,未发网络请求(calls=%d)",
fc, g_http_calls);
/* 失败不得把 from_cache 留成 1(App 会据此判离线,方向必须正确) */
ncm_apicache_clear();
stub_err(NCM_ERR_NET);
fc = -1;
rc = ncm_api_user_playlist_ex("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n, &fc);
CHECK(rc == NCM_ERR_NET && fc == 0, "网络失败:rc=%d from_cache=%d(期望 -4/0)", rc, fc);
/* 兼容入口仍可用(out_from_cache=NULL 不崩) */
ncm_apicache_clear();
stub(200, body, NULL);
rc = ncm_api_user_playlist("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n);
CHECK(rc == NCM_OK && n == 1, "兼容入口 ncm_api_user_playlist 正常(rc=%d n=%d)", rc, n);
/* 契约:out_n 必填(与原接口一致);out_from_cache 可 NULL(评审 P2-1) */
rc = ncm_api_user_playlist_ex("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, NULL, &fc);
CHECK(rc == NCM_ERR_ARG, "out_n=NULL → NCM_ERR_ARG(契约与原接口一致)rc=%d", rc);
ncm_apicache_clear();
}
/* ============================================================
* E. 真实 ncm_login:cookie 校验 / 续期
* ============================================================ */
static void test_login_map(void)
{
printf("== E. ncm_login_status_check(启动/uid 校验)==\n");
char uid[32], nick[128];
struct { int status; const char *body; int stub_err; int want; const char *why; } m[] = {
{ 200, "{\"code\":200,\"profile\":{\"userId\":\"42\",\"nickname\":\"n\"},"
"\"account\":{\"id\":42}}", 0, NCM_OK, "已登录 → OK" },
{ 200, "{\"code\":200,\"profile\":null,\"account\":null}", 0, NCM_ERR_AUTH,
"游客(profile=null)→ AUTH" },
{ 401, "{}", 0, NCM_ERR_AUTH, "HTTP 401 → AUTH" },
{ 500, "<html>bad gateway</html>", 0, NCM_ERR_SVC, "HTTP 500 → SVC(断网/服务异常不清会话)" },
{ 200, "{\"code\":-460}", 0, NCM_ERR_SVC, "业务 -460 风控 → SVC" },
{ 200, "{}", 0, NCM_ERR_JSON, "缺 code → JSON(other)" },
{ 0, NULL, NCM_ERR_NET, NCM_ERR_NET, "网络不可达 → NET" },
{ 0, NULL, NCM_ERR_TLS, NCM_ERR_TLS, "TLS 失败 → TLS" },
};
for (size_t i = 0; i < sizeof m / sizeof m[0]; i++) {
if (m[i].stub_err) stub_err(m[i].stub_err);
else stub(m[i].status, m[i].body, NULL);
int rc = ncm_login_status_check("dev", "MUSIC_U=x", uid, sizeof uid, nick, sizeof nick);
CHECK(rc == m[i].want, "[%s] rc=%d(期望 %d)", m[i].why, rc, m[i].want);
}
printf("== E2. ncm_login_refresh(MUSIC_U 临期续期)==\n");
char out[NCM_COOKIE_MAX];
struct { int status; const char *body; const char *setc; int stub_err; int want; const char *why; } r[] = {
{ 200, "{\"code\":200}", "MUSIC_U=NEW; __csrf=c2", 0, NCM_OK,
"200+code200 → OK(换新 cookie)" },
{ 200, "{\"code\":301}", NULL, 0, NCM_ERR_AUTH, "业务 301 → AUTH(会话确失效)" },
{ 200, "{\"code\":302}", NULL, 0, NCM_ERR_AUTH, "业务 302 → AUTH" },
{ 200, "{\"code\":-460}", NULL, 0, NCM_ERR_SVC, "业务 -460 风控 → SVC(退避,不清登录态)" },
{ 200, "{\"code\":400}", NULL, 0, NCM_ERR_HTTP, "其它业务码 → HTTP(退避,不登出)" },
{ 502, "<html>bad gateway</html>", NULL, 0, NCM_ERR_SVC, "HTTP 502 → SVC(退避)" },
{ 0, NULL, NULL, NCM_ERR_TLS, NCM_ERR_TLS, "TLS 失败 → TLS(退避)" },
{ 0, NULL, NULL, NCM_ERR_NET, NCM_ERR_NET, "网络不可达 → NET(退避)" },
};
for (size_t i = 0; i < sizeof r / sizeof r[0]; i++) {
if (r[i].stub_err) stub_err(r[i].stub_err);
else stub(r[i].status, r[i].body, r[i].setc);
int rc = ncm_login_refresh("dev", "MUSIC_U=OLD; __csrf=c1", out, sizeof out);
CHECK(rc == r[i].want, "[%s] rc=%d(期望 %d)", r[i].why, rc, r[i].want);
if (r[i].want == NCM_OK)
CHECK(strstr(out, "MUSIC_U=NEW") != NULL && strstr(out, "__csrf=c2") != NULL,
"续期成功:新 MUSIC_U/__csrf 已合并");
}
}
/* ============================================================
* F. 会话加密密钥稳定性(旧版依赖网卡 MAC = 断网/换网后被踢扫码的根因)
* ============================================================ */
static void tmpdir_of(const char *name, char *out, size_t sz)
{
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = getenv("HOME");
if (!base || !base[0]) base = ".";
snprintf(out, sz, "%s/ncm_m9_%s_%d", base, name, (int)getpid());
char cmd[512];
snprintf(cmd, sizeof cmd, "rm -rf '%s'; mkdir -p '%s'", out, out);
(void)system(cmd);
}
/* 旧版(MAC 派生密钥)密文构造:magic "NCM1" + iv + AES-128-CBC(PKCS7)。
* 仅单测用,复刻 ncm_store 的旧格式以便验证「升级后可解 + 自动迁移」。 */
static int legacy_encrypt(const char *plain, unsigned char *out, size_t *out_len)
{
unsigned char key[16];
if (ncm_store_machine_key("os.meow.ncm.session.v1", key) != 0) return -1;
size_t n = strlen(plain);
int pad = (int)(16 - (n % 16));
size_t total = n + (size_t)pad;
unsigned char iv[16], iv0[16];
for (int i = 0; i < 16; i++) iv[i] = (unsigned char)(0x10 + i);
memcpy(iv0, iv, 16);
unsigned char *pt = (unsigned char *)malloc(total);
unsigned char *ct = (unsigned char *)malloc(total);
if (!pt || !ct) { free(pt); free(ct); return -1; }
memcpy(pt, plain, n);
memset(pt + n, pad, (size_t)pad);
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, key, 128);
mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, total, iv0, pt, ct);
mbedtls_aes_free(&aes);
memcpy(out, "NCM1", 4);
memcpy(out + 4, iv, 16);
memcpy(out + 20, ct, total);
*out_len = 20 + total;
free(pt); free(ct);
return 0;
}
static void test_session_key_stability(void)
{
printf("== F. 会话加密密钥稳定性(断网/网卡变化不再丢登录态)==\n");
const char *cookie = "MUSIC_U=ABC123; __csrf=xyz";
char dir[256];
tmpdir_of("store", dir, sizeof dir);
setenv("NCM_MAC", "aa:bb:cc:dd:ee:01", 1);
ncm_store_t st;
CHECK(ncm_store_init(&st, dir) == 0, "store init");
CHECK(ncm_store_save_session(&st, cookie) == NCM_OK, "session 落盘(.mkey 派生密钥)");
/* 网卡变化(断网/换网卡/uplink 顺序变化):MAC 从 01 变 02 */
setenv("NCM_MAC", "aa:bb:cc:dd:ee:02", 1);
char back[NCM_COOKIE_MAX];
int rc = ncm_store_load_session(&st, back, sizeof back);
CHECK(rc == NCM_OK && strcmp(back, cookie) == 0,
"MAC 变化后 session 仍可解(旧版此处失败→cookie 丢失→退扫码)");
/* 旧版 MAC 派生密文:仍可解,且 load_session 自动迁移为新密钥 */
char dir2[256];
tmpdir_of("legacy", dir2, sizeof dir2);
ncm_store_t st2;
ncm_store_init(&st2, dir2);
unsigned char enc[8192];
size_t elen = 0;
setenv("NCM_MAC", "aa:bb:cc:dd:ee:03", 1);
CHECK(legacy_encrypt(cookie, enc, &elen) == 0, "构造旧版(MAC 派生)密文");
char path[600];
snprintf(path, sizeof path, "%s/session.enc", dir2);
CHECK(ncm_store_write_file(path, enc, elen) == NCM_OK, "写入旧版 session.enc");
setenv("NCM_MAC", "aa:bb:cc:dd:ee:04", 1); /* 故意换 MAC:旧解密本应失败 */
char back2[NCM_COOKIE_MAX];
rc = ncm_store_load_session(&st2, back2, sizeof back2);
/* 旧格式是用 03 的 MAC 加的密钥;此刻 NCM_MAC=04 → 旧密钥也解不出,这是预期:
* 迁移只在「当前 MAC 与旧文件一致」时发生(真机升级时 MAC 未变)。验证迁移路径: */
CHECK(rc != NCM_OK, "旧密文 + 已换 MAC → 解不出(预期,证明旧方案确实依赖网卡)");
snprintf(path, sizeof path, "%s/session.enc", dir2);
setenv("NCM_MAC", "aa:bb:cc:dd:ee:03", 1); /* 回到写入时的 MAC(=真机升级现场) */
CHECK(ncm_store_write_file(path, enc, elen) == NCM_OK, "重写旧版 session.enc");
rc = ncm_store_load_session(&st2, back2, sizeof back2);
CHECK(rc == NCM_OK && strcmp(back2, cookie) == 0, "旧密文在升级现场可解(兼容)");
setenv("NCM_MAC", "aa:bb:cc:dd:ee:05", 1); /* 迁移后:换 MAC 仍可解 */
char back3[NCM_COOKIE_MAX];
rc = ncm_store_load_session(&st2, back3, sizeof back3);
CHECK(rc == NCM_OK && strcmp(back3, cookie) == 0, "已自动迁移为 .mkey 密钥(换 MAC 仍可解)");
char cmd[512];
snprintf(cmd, sizeof cmd, "rm -rf '%s' '%s'", dir, dir2);
(void)system(cmd);
unsetenv("NCM_MAC");
}
int main(void)
{
printf("===== M9 离线会话保持 / 登录门 宿主单测 =====\n");
test_err_class();
test_http_class();
test_gate();
test_api_map();
test_api_from_cache();
test_login_map();
test_session_key_stability();
printf("\n== SESSION GATE SIM: %s (http stubs called %d times) ==\n",
g_fail ? "FAIL" : "ALL PASS", g_http_calls);
return g_fail ? 1 : 0;
}