feat(ncm): 网易云核心库 core/ (agent-203) — ncm_http(mbedtls TLS1.2+chunked+流式限幅)/ncm_crypto(eapi/weapi/linuxapi, RSA raw逐字节验证)/ncm_login(unikey→二维码→轮询状态机→cookie加密落盘)/ncm_api(歌单/详情/播放地址Hi-res逐档降级/歌词)/ncm_cache(LRU 默认10G)/ncm_store(deviceId持久化+机器密钥)
宿主+qemu-mipsel 全链路 PASS(unikey实拿/801轮询/eapi往返/RSA对拍/缓存LRU); -Wall -Wextra 自有代码0warning 待联调: 803真扫码、真机 hw:0,0 播放链路
This commit is contained in:
1 parent
85b4aea24f
commit
8012948b26
25 files changed
+3416
No files matched your search
@@ -0,0 +1,242 @@
|
||||
/* test_ncm_core.c — 网易云核心库宿主实测
|
||||
*
|
||||
* 覆盖:加密(eapi 往返/weapi 形状/linuxapi)、AES/hex/base64、
|
||||
* 缓存 LRU、设备键/cookie 加解密、二维码矩阵、以及
|
||||
* 网络接口(unikey 实拿 / 轮询 801 / 无凭据错误处理)。
|
||||
* 网络不可用时网络项按 SKIP 处理,不判失败;内部逻辑错才 FAIL。
|
||||
* 用法:test_ncm_core [data_dir](缺省 /tmp/ncm_test)
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ncm.h"
|
||||
#include "mbedtls/aes.h"
|
||||
|
||||
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)
|
||||
|
||||
#define CHECKRC(expr, fmt, ...) do { \
|
||||
int _r = (expr); \
|
||||
if (_r == NCM_OK) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||||
else { printf(" FAIL " fmt " (rc=%d)\n", ##__VA_ARGS__, _r); g_fail = 1; } \
|
||||
} while (0)
|
||||
|
||||
/* ---------- 辅助: AES-128-ECB 解密回明文(验证 eapi 往返) ---------- */
|
||||
static int aes_ecb_decrypt_hex(const char *hex_upper, const unsigned char *key, char *out, size_t out_sz) {
|
||||
int nbytes = strlen(hex_upper) / 2;
|
||||
unsigned char *ct = malloc((size_t)nbytes);
|
||||
unsigned char *pt = malloc((size_t)nbytes);
|
||||
unsigned char *kv = malloc(16);
|
||||
if (!ct || !pt || !kv) return -1;
|
||||
kv[0]=0;
|
||||
memcpy(kv, key, 16);
|
||||
for (int i = 0; i < nbytes; i++) {
|
||||
int hi, lo;
|
||||
char a = hex_upper[i*2], b = hex_upper[i*2+1];
|
||||
hi = (a <= '9') ? a-'0' : (a <= 'F' ? a-'A'+10 : a-'a'+10);
|
||||
lo = (b <= '9') ? b-'0' : (b <= 'F' ? b-'A'+10 : b-'a'+10);
|
||||
ct[i] = (unsigned char)((hi<<4)|lo);
|
||||
}
|
||||
mbedtls_aes_context aes; mbedtls_aes_init(&aes);
|
||||
mbedtls_aes_setkey_dec(&aes, kv, 128);
|
||||
for (int off = 0; off < nbytes; off += 16)
|
||||
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, ct+off, pt+off);
|
||||
mbedtls_aes_free(&aes);
|
||||
/* 去 PKCS7 */
|
||||
int pad = pt[nbytes-1];
|
||||
if (pad < 1 || pad > 16 || pad > nbytes) { free(ct);free(pt);free(kv); return -1; }
|
||||
int plen = nbytes - pad;
|
||||
if ((size_t)plen >= out_sz) { free(ct);free(pt);free(kv); return -1; }
|
||||
memcpy(out, pt, (size_t)plen); out[plen] = 0;
|
||||
free(ct); free(pt); free(kv);
|
||||
return plen;
|
||||
}
|
||||
|
||||
static void test_crypto(void) {
|
||||
printf("== crypto ==\n");
|
||||
/* eapi 往返 */
|
||||
char params[2048];
|
||||
CHECKRC(ncm_crypto_eapi("/api/login/qrcode/unikey", "{\"type\":3}", params, sizeof params),
|
||||
"eapi params produced");
|
||||
char plain[2048];
|
||||
int pl = aes_ecb_decrypt_hex(params, (const unsigned char *)NCM_EAPI_KEY, plain, sizeof plain);
|
||||
CHECK(pl > 0, "eapi params decrypts (%d bytes)", pl);
|
||||
if (pl > 0) {
|
||||
CHECK(strstr(plain, "/api/login/qrcode/unikey") != NULL, "eapi url present in plain");
|
||||
CHECK(strstr(plain, "{\"type\":3}") != NULL, "eapi json present in plain");
|
||||
}
|
||||
/* weapi 形状 */
|
||||
char p2[1024], es[300];
|
||||
CHECKRC(ncm_crypto_weapi("{\"id\":123}", p2, sizeof p2, es, sizeof es), "weapi produced");
|
||||
CHECK(strlen(es) == 256, "weapi encSecKey is 256 hex (got %zu)", strlen(es));
|
||||
CHECK(p2[0] != 0, "weapi params non-empty");
|
||||
/* linuxapi */
|
||||
char lp[2048];
|
||||
CHECKRC(ncm_crypto_linuxapi("{\"a\":1}", lp, sizeof lp), "linuxapi produced");
|
||||
}
|
||||
|
||||
static void test_store_cookie(void) {
|
||||
printf("== store/device/cookie ==\n");
|
||||
ncm_store_t st;
|
||||
ncm_store_init(&st, NULL); /* env MEOW_DATA_DIR 或 /tmp */
|
||||
char id1[NCM_STORE_ID_MAX], id2[NCM_STORE_ID_MAX];
|
||||
ncm_store_get_device_id(&st, id1);
|
||||
ncm_store_get_device_id(&st, id2);
|
||||
CHECK(strcmp(id1, id2) == 0, "deviceId stable across calls");
|
||||
CHECK(strlen(id1) == 52, "deviceId length 52 (got %zu)", strlen(id1));
|
||||
unsigned char kb[8192];
|
||||
size_t klen;
|
||||
const char *cs = "MUSIC_U=abcdef123456; __csrf=xyz; os=pc";
|
||||
CHECKRC(ncm_store_cookie_encrypt(cs, kb, &klen, sizeof kb), "cookie encrypt");
|
||||
char cs2[4096];
|
||||
CHECKRC(ncm_store_cookie_decrypt(kb, klen, cs2, sizeof cs2), "cookie decrypt");
|
||||
CHECK(strcmp(cs, cs2) == 0, "cookie roundtrip matches");
|
||||
/* 机器密钥可派生且 16 字节 */
|
||||
unsigned char key[16];
|
||||
CHECK(ncm_store_machine_key("t", key) == 0, "machine key derivable");
|
||||
}
|
||||
|
||||
static void test_cache(void) {
|
||||
printf("== cache ==\n");
|
||||
ncm_cache_t c;
|
||||
char dir[] = "/tmp/ncm_cache_test";
|
||||
ncm_cache_init(&c, dir);
|
||||
/* 清空目录 */
|
||||
{
|
||||
char cmd[128];
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s; mkdir -p %s", dir, dir);
|
||||
if (system(cmd) != 0) { /* best-effort */ }
|
||||
}
|
||||
/* miss */
|
||||
char path[NCM_CACHE_DIR_MAX + 32];
|
||||
int mrc = ncm_cache_lookup(&c, "1", "mp3", path, sizeof path);
|
||||
CHECK(mrc == NCM_ERR_NOENT, "lookup miss returns NOENT (rc=%d)", mrc);
|
||||
/* write + commit */
|
||||
ncm_cache_writer_t *w;
|
||||
CHECKRC(ncm_cache_open(&c, "100", "mp3", &w), "cache open");
|
||||
CHECKRC(ncm_cache_write(w, (const unsigned char *)"AUDIODATA", 9), "cache write");
|
||||
CHECKRC(ncm_cache_commit(w), "cache commit");
|
||||
CHECKRC(ncm_cache_lookup(&c, "100", "mp3", path, sizeof path), "lookup hit");
|
||||
CHECK(strstr(path, "song_100.mp3") != NULL, "hit path name");
|
||||
CHECK(ncm_cache_total_bytes(&c) >= 9, "total_bytes >= 9");
|
||||
/* abort leaves nothing */
|
||||
ncm_cache_writer_t *w2;
|
||||
ncm_cache_open(&c, "101", "mp3", &w2);
|
||||
ncm_cache_write(w2, (const unsigned char *)"x", 1);
|
||||
ncm_cache_abort(w2);
|
||||
int arc = ncm_cache_lookup(&c, "101", "mp3", path, sizeof path);
|
||||
CHECK(arc == NCM_ERR_NOENT, "aborted not cached (rc=%d)", arc);
|
||||
/* 强制小上限测 LRU 淘汰:设 limit_bytes 极小,写入第二首歌触发淘汰 */
|
||||
c.limit_bytes = 5; /* 仅 5 字节,任何条目超限 */
|
||||
ncm_cache_writer_t *w3;
|
||||
ncm_cache_open(&c, "102", "mp3", &w3);
|
||||
ncm_cache_write(w3, (const unsigned char *)"abcdefgh", 8);
|
||||
ncm_cache_commit(w3);
|
||||
ncm_cache_evict(&c);
|
||||
CHECK(ncm_cache_total_bytes(&c) <= 5, "evict kept total <= limit (%llu bytes)",
|
||||
(unsigned long long)ncm_cache_total_bytes(&c));
|
||||
/* 恢复默认上限 */
|
||||
ncm_cache_set_limit_gb(&c, 10);
|
||||
CHECK(ncm_cache_limit_gb(&c) == 10, "limit_gb reset to 10");
|
||||
}
|
||||
|
||||
static void test_qr(void) {
|
||||
printf("== qrcodegen ==\n");
|
||||
unsigned char mods[177 * 177];
|
||||
int size = 0;
|
||||
CHECKRC(ncm_login_qr_matrix("https://music.163.com/login?codekey=test-uuid-1234", mods, 177, &size),
|
||||
"qr matrix");
|
||||
CHECK(size >= 21 && size <= 177, "qr size sane (got %d)", size);
|
||||
int dark = 0;
|
||||
for (int i = 0; i < size*size; i++) if (mods[i]) dark++;
|
||||
CHECK(dark > 0, "qr has dark modules (%d)", dark);
|
||||
}
|
||||
|
||||
static void test_crypto_vectors(void) {
|
||||
/* 交叉:固定密文来源已在 rsa_raw_test 验证;这里仅核对 eapi 对已知 JSON 的确定性。 */
|
||||
printf("== known eapi (deterministic) ==\n");
|
||||
char a[2048], b[2048];
|
||||
ncm_crypto_eapi("/api/x", "{\"k\":1}", a, sizeof a);
|
||||
ncm_crypto_eapi("/api/x", "{\"k\":1}", b, sizeof b);
|
||||
CHECK(strcmp(a, b) == 0, "eapi deterministic for same input");
|
||||
}
|
||||
|
||||
static void test_network(void) {
|
||||
printf("== network (live) ==\n");
|
||||
int net_ok = 0;
|
||||
char device_id[NCM_STORE_ID_MAX];
|
||||
ncm_store_t st; ncm_store_init(&st, NULL);
|
||||
ncm_store_get_device_id(&st, device_id);
|
||||
|
||||
/* unikey 实拿 */
|
||||
char unikey[64] = "";
|
||||
int rc = ncm_login_unikey(device_id, unikey, sizeof unikey);
|
||||
if (rc == NCM_OK && unikey[0]) {
|
||||
net_ok = 1;
|
||||
printf(" PASS unikey = %s\n", unikey);
|
||||
char qurl[256];
|
||||
ncm_login_qr_url(unikey, qurl, sizeof qurl);
|
||||
CHECK(strstr(qurl, "codekey=") != NULL, "qr url built : %s", qurl);
|
||||
/* 轮询(未扫码,期望 801 等待扫码,或无效 key 800 过期) */
|
||||
char cookiebuf[1024];
|
||||
ncm_login_poll_t pr;
|
||||
rc = ncm_login_poll(device_id, unikey, NULL, &pr, cookiebuf, sizeof cookiebuf);
|
||||
if (rc == NCM_OK) {
|
||||
printf(" PASS poll code=%d state=%d msg=%s\n", pr.code, pr.state, pr.message);
|
||||
if (pr.state == NCM_LOGIN_PENDING)
|
||||
printf(" PASS state=等待扫码(801)\n");
|
||||
else
|
||||
printf(" NOTE state=%d (code=%d), 非 801(可能已过期/已扫码)\n", pr.state, pr.code);
|
||||
} else {
|
||||
printf(" SKIP poll rc=%d (网络/被拒)\n", rc);
|
||||
}
|
||||
} else {
|
||||
printf(" SKIP unikey rc=%d (网络不可达?) %s\n", rc, rc != NCM_OK ? "网络/证书" : "");
|
||||
}
|
||||
|
||||
/* 无凭据/无效凭据的错误处理:不应崩溃,且应给出可判定的失败 */
|
||||
printf("-- no-credential error handling --\n");
|
||||
/* 伪造 MUSIC_U → guest → profile null → NCM_ERR_AUTH */
|
||||
int r2 = ncm_login_status_check(device_id, "MUSIC_U=deadbeef0000", NULL, 0, NULL, 0);
|
||||
printf(" NOTE login_status(bogus cookie) rc=%d\n", r2);
|
||||
CHECK(r2 != NCM_OK, "login_status(bogus) not OK (rc=%d)", r2);
|
||||
|
||||
/* 无 cookie 也应优雅返回(可能 200 公开数据,或错误码)——只验证不崩溃且计数合法 */
|
||||
ncm_playlist_t pls[8]; int pn = 0;
|
||||
int r3 = ncm_api_user_playlist(device_id, "", "0", 30, 0, pls, 8, &pn);
|
||||
printf(" NOTE user_playlist(uid=0, no cookie) rc=%d count=%d%s\n",
|
||||
r3, pn, pn > 0 ? " (公开数据)" : "");
|
||||
CHECK(r3 == NCM_OK || r3 < 0, "user_playlist no-cred graceful (rc=%d)", r3);
|
||||
CHECK(pn >= 0, "user_playlist count sane (%d)", pn);
|
||||
|
||||
/* 无效 id → url 为 null,应无人可播 */
|
||||
ncm_play_url_t pu;
|
||||
int r4 = ncm_api_song_url(device_id, "", "0", NCM_LEVEL_HIGHER, &pu);
|
||||
printf(" NOTE song_url(id=0) rc=%d url='%s'\n", r4, pu.url);
|
||||
CHECK(r4 != NCM_OK || !pu.url[0], "song_url(bogus id) no url (rc=%d)", r4);
|
||||
|
||||
if (!net_ok) printf(" SKIP network live checks (host unreachable or TLS CA missing)\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *data_dir = (argc > 1) ? argv[1] : "/tmp/ncm_test";
|
||||
printf("ncm_core self-test, data_dir=%s\n", data_dir);
|
||||
ncm_ctx_t ctx;
|
||||
ncm_ctx_init(&ctx, data_dir);
|
||||
printf("deviceId=%s\n", ctx.device_id);
|
||||
printf("cache_dir=%s limit_gb=%d\n", ctx.cache.dir, ctx.cache.limit_gb);
|
||||
|
||||
test_crypto();
|
||||
test_crypto_vectors();
|
||||
test_store_cookie();
|
||||
test_cache();
|
||||
test_qr();
|
||||
test_network();
|
||||
|
||||
printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS ==");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
Reference in new issue
Block a user