- 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。
423 lines
22 KiB
C
423 lines
22 KiB
C
/* 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;
|
||
}
|