Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/session_gate_sim.c
T
AskaEth 2ca386973b test(ncm): M9 分类/登录门宿主单测(83 断言)+ 传输层归一 4 例 + REPORT §41
- bench/session_gate_sim.c(新):真实 core + ncm_http_request 桩,覆盖
  A 错误分类矩阵(-4/-5/-6/-17/裸 mbedtls vs -13/其它)、B HTTP 状态矩阵
  (401/403 vs 5xx/429 vs 3xx/4xx)、C 登录门 + 启动决策(无网→离线、无会话→扫码)、
  D 真实 ncm_api 状态/业务码映射(含 -460→SVC、301/302→AUTH)、
  E 真实 ncm_login_status_check/refresh 映射、F 会话密钥稳定性(换 MAC 仍可解、
  旧 MAC 密文可解且自动迁移)。83 PASS / 0 FAIL,0 真实请求。
- bench/http_truncate_sim.c:追加 4 例传输层归一(握手 fatal alert→NET、握手超时→TIMEOUT、
  读 fatal alert→NET、CA 失败→TLS),原 8 例截断守卫零回归(12 PASS)。
- build_ncm_sims.sh:window/search/plpage/apicache/cookie_refresh 链入 ncm_session.o
  (appstate/api/login 新依赖),新增 session_gate_sim 构建与运行。
- test_ncm_core.c:采用 M8 分支的 $TMPDIR 适配(Termux /tmp 不可写;合并时以 M8 为准)。
- 相关 sim 头注释同步链接命令(ncm_apicache.o/ncm_session.o)。
- REPORT §41:判定点调用链与行号、根因证据、实现、验证矩阵与诚实出入
  (mipsel/真机未跑;app.c 宿主不可编译的既有原因;403/-460 取舍)。

验证:test_ncm_core_host 139 PASS/0 FAIL;session_gate_sim 83 PASS;
http_truncate_sim 12 PASS;window/search/apicache/cookie_refresh sim 通过;
核心 12 TU + 应用 13 TU(-Wall -Wextra)0 warning。
2026-11-05 11:45:44 +08:00

423 lines
22 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)。
* 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);
}
}
/* ============================================================
* 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_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;
}