diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index 3987c36..0188244 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -122,3 +122,97 @@ CA bundle 内置方案、音质档位映射实测。这些属设备/账号侧联 - **推荐 TLS/加密一体用 mbedtls**(单库一次解决 TLS + AES/MD5/RSA raw/Base64 + X509,规避 RNSS-raw 手写风险);BearSSL 为省 flash 备选。 - **v1 体积增量 ≈ 438 KB(库)/ 只做 mp3**,内存估计数百 KB 工作集,14MB 可行;主要优化点是 mbedtls 裁剪与封面限幅。 - **最关键风险(RSA raw)已验证**;其余风险集中在风控(deviceId/UA 稳定持久化)与无版权降级策略。 + +--- + +## 9. 核心库批次(core/,纯 C 静态库,无 UI) + +> 日期:2026-08-31。在 §1-8 预研(候选库/工具链已定型)基础上落地核心库:http/crypto/login/api/cache 五模块 +> + 公共/存储/JSON/请求构建支撑模块 + ncm_ctx 上下文。仅改动 `patch_v2/ncm_app/`。未 commit/push/刷机/动设备。 + +### 9.1 模块与公开 API(头文件签名) + +| 模块 | 文件 | 主要公开函数 | +|---|---|---| +| 公共工具 | `core/ncm_common.h/.c` | `ncm_hex_encode/decode` `ncm_b64_encode/decode` `ncm_md5_hex` `ncm_sha256_hex` `ncm_url_encode/decode` `ncm_random_bytes` `ncm_now_ms` 等 | +| JSON | `core/ncm_json.h/.c` | `ncm_json_parse` `ncm_json_obj_get` `ncm_json_arr_get/len` `ncm_json_path[_str/_int/_bool]` `ncm_json_get_str_at/get_int_at` | +| HTTP | `core/ncm_http.h/.c` | `ncm_http_request`(一次性 POST/GET,限幅)`ncm_http_download`(流式回调 + max_bytes)`ncm_http_parse_set_cookie` `ncm_http_set_verify_default` | +| 存储 | `core/ncm_store.h/.c` | `ncm_store_init` `ncm_store_get/set_device_id` `ncm_store_machine_key` `ncm_store_cookie_encrypt/decrypt` `ncm_store_save/load/clear_session` `ncm_store_write/read_file` `ncm_store_read_cfg_int` | +| 加密 | `core/ncm_crypto.h/.c` | `ncm_crypto_eapi` `ncm_crypto_weapi` `ncm_crypto_linuxapi` `ncm_crypto_rsa_raw_selfcheck` | +| 请求构建 | `core/ncm_req.h/.c` | `ncm_req_build`(eapi/weapi/linux,注入 header/csrf_token/UA/Cookie)`ncm_req_cookie_get` | +| 登录 | `core/ncm_login.h/.c` | `ncm_login_qr_url` `ncm_login_unikey` `ncm_login_qr_matrix`(qrcodegen)`ncm_login_poll` `ncm_login_status_check` `ncm_login_save/load/clear_cookie` | +| 歌单/播放 | `core/ncm_api.h/.c` | `ncm_api_user_playlist` `ncm_api_playlist_detail` `ncm_api_song_detail` `ncm_api_song_url` `ncm_api_song_url_auto` `ncm_api_lyric` `ncm_api_level_str` | +| 缓存 | `core/ncm_cache.h/.c` | `ncm_cache_init` `ncm_cache_set_limit_gb/limit_gb` `ncm_cache_load_setting` `ncm_cache_lookup` `ncm_cache_open/write/commit/abort` `ncm_cache_total_bytes` `ncm_cache_evict` `ncm_cache_path` | +| 上下文 | `core/ncm.h` + `ncm_ctx.c` | `ncm_ctx_t`(deviceId/cookie/CA/默认音质/cache)`ncm_ctx_init` `ncm_ctx_set/load/clear_cookie` `ncm_ctx_default_cache_dir` | + +关键常量:`NCM_EAPI_KEY`/`NCM_WEAPI_PRESET`/`NCM_WEAPI_IV`/`NCM_LINUXAPI_KEY`/`NCM_RSA_DER_B64`(`ncm_crypto.h`); +UA 按加密类型分流(eapi=iPhone `NeteaseMusic 9.0.90/5038`,weapi=Chrome/Edg 124),请求头注入对齐 CNMPlayer `request.rs`(deviceId/UA 稳定)。 + +### 9.2 关键实现取舍 +- **CA 方案**(`ncm_http.h` 注释):`verify_ca=1`(默认)加载系统 CA bundle(或 `ca_path`),失败即拒绝(防 MITM);`verify_ca=0` 跳过校验(仅调试/受限网)。全局默认由 `ncm_http_set_verify_default` 设定,`ncm_ctx_init` 以其 `verify_ca/ca_path` 设置。**设备侧需内置 CA 根**(VENDOR_NOTES §备注),否则接口 eapi/weapi 会被拒。 +- **RSA raw**:用 `mbedtls_rsa_public()` 无 padding 纯模幂,16 字节 secret 右对齐 128 字节(前导 0),输出 256 小写 hex——已与 Python `pow(m,e,n)` 逐字节一致(见 9.4)。 +- **weapi**:双层 AES-128-CBC(`params` base64,含 `+/=` 需 urlencode)+ RSA(raw) 加密随机 16 位 base62 密钥(RSA 前**反转**)。 +- **会话cookie**:加密落盘 `MeowData//data/session.enc`,密钥 = `sha256(MAC + 盐)`;无 MAC(qemu/无网卡)回退到设备私有 `.mkey` 派生。**仅为防裸读模糊,非真安全**(头文件已注明)。 +- **缓存**:目录默认 `MeowData/os.meow.ncm/cache/`;上限默认 10GB(1~100 可配),读设置键 `ncm.cache_limit_gb`(`/ncm.cache_limit_gb.cfg`,对齐 SDK meow_config);LRU 按文件 mtime,`ncm_cache_lookup/commit` 刷新;命中即离线可播。 +- **音质**:`ncm_api_song_url_auto` 从 desired(默认 Hi-res)往下降级 `hires→lossless→exhigh→higher→standard`,只返回可解码 mp3(url 非空且 type=mp3);无该档对当前歌曲降级。 + +### 9.3 构建 +`./build_core.sh` → `out/core-host/`、`core-mipsel/`(自有代码对象 + vendor qrcodegen + jsmn 唯一实现 TU),链接 `out/mbedtls/{host,mipsel}/*.a`,产出: +- `test/test_ncm_core_host`(宿主,可跑) +- `out/test_ncm_core_mipsel`(mipsel 静态)。`size`:`text 462662 / data 212 / bss 27688`(含测试 printf 基建 + mbedtls + qrcodegen;核心库本身链接更小)。 +- 自有代码 `-Wall -Wextra` **0 warning**;vendor 警告豁免。mipsel 用 `.tools/zig-mipsel-cc` 静态交叉。 + +### 9.4 验证记录(宿主 + qemu-mipsel 双向实测,响应已脱敏/截断) +- **unikey 实拿(eapi)**:`{"code":200,"unikey":""}`(宿主 `dc5bd090-…`,qemu-mipsel `ef9a5db2-…`);二维码 URL `https://music.163.com/login?codekey=`。 +- **轮询(未扫码)**:`{"code":801,"message":"等待扫码"}` → `state=等待扫码(801)`,两平台一致。 +- **login_status(伪造 cookie)**:返回 weapi 可解析 JSON(code 200 但 profile null)→ 库判 `NCM_ERR_AUTH(-13)`,证明 weapi 请求被服务端接受。 +- **RSA raw 交叉**:`ncm_crypto_rsa_raw_selfcheck` 输出 `55b33e7e…b2b83`,与 Python `pow(m,65537,n)`(§3 已交叉验证值)**逐字节一致**。weapi params 为合法 base64(RSA 封装的 16 字节随机密钥无法无私钥解密,其 AES-CBC 为标准用法)。 +- **eapi 往返**:`ncm_crypto_eapi` 产物可用 AES-ECB 解回含 `url` + `json` 的明文(`/api/login/qrcode/unikey` + `{"type":3}`)。 +- **cookie 加解密**:38 字节串 `MUSIC_U=…; __csrf=xyz; os=pc` 加密→解密完全一致(含一次 mbedtls `crypt_cbc` 就地改写 IV 的坑,已修)。 +- **缓存**:写入/命中/abort 不留档/强制小上限触发 LRU 淘汰(总字节降至 ≤ 上限)。 +- **二维码**:41×41 内矩阵生成,暗模块 >0。 +- **无凭据错误处理**:`user_playlist(uid=0,无cookie)` 优雅返回(公开数据或错误码,不崩溃);`song_url(id=0)` 返回无 url。 + +### 9.5 md5(自有源码) +``` +778847a8419325af2b95ad0ef7749781 core/ncm_api.h c07d246c9b64e4d3b9078b9e33d19fe4 core/ncm_cache.h +6e878566f6a4c4377c98e87e9967fdcc core/ncm_common.h 9056253bfb635d8224e7454c6b1796a2 core/ncm_crypto.h +157807f07a96f43249f6aeb9707d053f core/ncm.h 477dd9f5eab61e2940cb432d1b215b3a core/ncm_http.h +398e62a0b641f13bc80c02d5e42a0401 core/ncm_json.h 05fb336d3b65b041095bd8fb3c64bc3c core/ncm_login.h +96f423ef1a36bda7b5cb3961fbe22028 core/ncm_req.h 02c7ff38dd856455f32a4cc7bbc5edd5 core/ncm_store.h +f8f8e96464d3c4ebcf1898eb4212c55a core/jsmn_impl.c a00e9b9b1adf7c6cf924eb9ecfc90a67 core/ncm_common.c +e75c29217ef5dd3f0e23c740b79210c1 core/ncm_crypto.c 9ebfa83e31a530b4dc8fd7877c2f5fb9 core/ncm_ctx.c +319df449ecd7727f328e01adb518ba57 core/ncm_http.c d69ff302ec111a74f621571818da4622 core/ncm_json.c +a53e55f9cab828fd59b694330ec7371e core/ncm_login.c d81eda09640ff748b0a1c14607447d1b core/ncm_req.c +1ceda66d30d44a3d8850df2e876c0195 core/ncm_store.c 7cdb84fcf87384eb5125e5aae01eac84 test/test_ncm_core.c +94a4dd07fba98a6e7b70021aaeb8cb19 build_core.sh +``` +产物:`test/test_ncm_core_host` md5 `0e41d3d413819768d65d2e9ca9e082ed`;`out/test_ncm_core_mipsel` md5 `e05f1937e5fc49a2590b830c28ee6372`。 + +### 9.6 出入清单(只改 `patch_v2/ncm_app/`) +``` +新增: + core/ncm.h ncm_ctx.c // 伞头 + 上下文 + core/ncm_common.h/.c // hex/base64/md5/sha256/urlencode/errcode + core/ncm_json.h/.c // jsmn 路径导航 + core/ncm_crypto.h/.c // eapi/weapi/linuxapi + RSA raw + core/ncm_http.h/.c // mbedtls HTTPS POST/GET + CA取舍 + 超时 + chunked + core/ncm_store.h/.c // deviceId/MAC机器密钥/cookie加解密/配置读取 + core/ncm_req.h/.c // 加密请求构建(md5/UA/cookie 注入) + core/ncm_login.h/.c // unikey/二维码矩阵/轮询状态机/cookie捕获/login_status + core/ncm_api.h/.c // 歌单/详情/歌曲/播放地址/歌词 + core/ncm_cache.h/.c // LRU 缓存 + 离线可播 + core/jsmn_impl.c // jsmn 唯一实现 TU(防多 TU 重复定义) + test/test_ncm_core.c // 宿主 + mipsel 综合实测 + build_core.sh // 宿主 + mipsel 编译链接 +修改: + REPORT.md // 追加本批记录 +(未动 vendor/,未动其余目录) +``` + +### 9.7 未做/诚实出入 +- **803 真登录**未实测(需真实扫码):code 803 分支的 set-cookie 捕获/合并逻辑按 document 实现,但未真跑;联调时建议真扫码验证一次 `MUSIC_U` 内容与 `login_status` 校验链路。 +- **weapi params 无法无 RSA 私钥解密**核验其内部 AES 双层,但 RSA 封路径已逐字节交叉,AES-CBC 为标准用法(可用 CNMPlayer 同源实现比对)。 +- **播放地址/缓存仅验证到“请求-解析-无版权返回 null”**;未做真实音频下载落盘(缓存 open/write/commit 已由单元测试覆盖;真实 mp3 下载留给带 UI 的播放层用 `ncm_http_download`+`ncm_cache_*` 串联)。 +- **栈用量**:单次 http 调用涉及 mbedtls ssl/净栈,峰值栈估几十 KB(与预研 demo 同级);App 线程栈需 ≥64KB,联调时以真机线程栈为准。 +- **JSON 响应限幅**:歌单/歌曲详情走 1MB~2MB 堆缓冲 + token 计数后精确分配,不算常驻;封面/大 JSON 不整体驻留,封面下载另有 `max_bytes` 限幅(建议 ~256KB,未在本次接线)。 diff --git a/patch_v2/ncm_app/build_core.sh b/patch_v2/ncm_app/build_core.sh new file mode 100755 index 0000000..dd23063 --- /dev/null +++ b/patch_v2/ncm_app/build_core.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# build_core.sh — 编译/链接 ncm_app 核心库与宿主测试(宿主 + mipsel 静态)。 +# 依赖:先运行 build_mbedtls.sh(产出 out/mbedtls/{host,mipsel}/*.a)。 +. ./libs_common.sh + +INC_CORE="-Icore" +INC_VENDOR="-I$VENDOR/mbedtls/include -I$VENDOR/jsmn -I$VENDOR/qrcodegen/c" +CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \ + core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \ + core/ncm_cache.c core/ncm_ctx.c" +VENDOR_C="vendor/qrcodegen/c/qrcodegen.c" # qrcodegen 单独编译 +JSMN_IMPL="core/jsmn_impl.c" # jsmn 唯一实现 TU + +STRICT="-Wall -Wextra" +HOST_LIBS="-L$OUT/mbedtls/host -lmbedtls -lmbedx509 -lmbedcrypto" +MIPS_LIBS="$OUT/mbedtls/mipsel/libmbedtls.a $OUT/mbedtls/mipsel/libmbedx509.a $OUT/mbedtls/mipsel/libmbedcrypto.a" + +echo "==> host 核心库对象" +mkdir -p out/core-host out/core-mipsel +ok=1 +for f in $CORE; do + b=$(basename "$f" .c) + if ! $CC_HOST -O2 -c $STRICT -DJSMN_HEADER -Icore $INC_VENDOR "$f" -o "out/core-host/$b.o"; then ok=0; break; fi +done +[ $ok -ne 1 ] && { echo "host core build FAILED"; exit 1; } + +echo "==> host vendor/jsmn 对象(豁免警告)" +$CC_HOST -O2 -c -I$VENDOR/jsmn $JSMN_IMPL -o out/core-host/jsmn_impl.o 2>/dev/null || { echo "jsmn_impl FAILED"; exit 1; } +for f in $VENDOR_C; do + b=$(basename "$f" .c) + $CC_HOST -O2 -c -I$VENDOR "$f" -o "out/core-host/v_$b.o" 2>/dev/null || { echo "vendor $f FAILED"; exit 1; } +done + +echo "==> host 测试可执行" +$CC_HOST -O2 $STRICT -DJSMN_HEADER -Icore $INC_VENDOR test/test_ncm_core.c \ + out/core-host/*.o $HOST_LIBS -o test/test_ncm_core_host -lpthread -lm || { echo "host test link FAILED"; exit 1; } +echo " 运行: ./test/test_ncm_core_host" + +echo +echo "==> mipsel 核心库对象(交叉)" +ok=1 +for f in $CORE; do + b=$(basename "$f" .c) + if ! $CC_MIPS -O2 -static -c $STRICT -DJSMN_HEADER -Icore $INC_VENDOR "$f" -o "out/core-mipsel/$b.o"; then ok=0; break; fi +done +[ $ok -ne 1 ] && { echo "mipsel core build FAILED"; exit 1; } + +echo "==> mipsel vendor/jsmn 对象(豁免警告)" +$CC_MIPS -O2 -static -c -I$VENDOR/jsmn $JSMN_IMPL -o out/core-mipsel/jsmn_impl.o 2>/dev/null || { echo "jsmn_impl FAILED"; exit 1; } +for f in $VENDOR_C; do + b=$(basename "$f" .c) + $CC_MIPS -O2 -static -c -I$VENDOR "$f" -o "out/core-mipsel/v_$b.o" 2>/dev/null || { echo "vendor $f FAILED"; exit 1; } +done + +echo "==> mipsel 测试可执行(静态)" +$CC_MIPS -O2 -static -Wl,--gc-sections $STRICT -DJSMN_HEADER -Icore $INC_VENDOR test/test_ncm_core.c \ + out/core-mipsel/*.o $MIPS_LIBS -o out/test_ncm_core_mipsel -lpthread -lm \ + || { echo "mipsel test link FAILED"; exit 1; } +size out/test_ncm_core_mipsel +echo " 运行: timeout 120 qemu-mipsel ./out/test_ncm_core_mipsel" +echo +echo "build_core OK" diff --git a/patch_v2/ncm_app/core/jsmn_impl.c b/patch_v2/ncm_app/core/jsmn_impl.c new file mode 100644 index 0000000..5bdad47 --- /dev/null +++ b/patch_v2/ncm_app/core/jsmn_impl.c @@ -0,0 +1,4 @@ +/* jsmn_impl.c — jsmn 唯一实现 TU。 + * jsmn.h 默认把实现(extern 函数)写进每个包含它的 TU, + * 因此全工程只允许这一个 TU 不带 JSMN_HEADER 编译,其余 TU 用 -DJSMN_HEADER 取声明。 */ +#include "jsmn.h" diff --git a/patch_v2/ncm_app/core/ncm.h b/patch_v2/ncm_app/core/ncm.h new file mode 100644 index 0000000..9985650 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm.h @@ -0,0 +1,46 @@ +/* ncm.h — 网易云核心库伞头 + * + * 汇总各模块,并提供 ncm_ctx_t 上下文(deviceId/会话 cookie/缓存/默认音质)。 + * 全部静态链接 mbedtls/jsmn/qrcodegen;纯 C,无 UI。 + */ +#ifndef NCM_H +#define NCM_H + +#include "ncm_common.h" +#include "ncm_crypto.h" +#include "ncm_http.h" +#include "ncm_json.h" +#include "ncm_store.h" +#include "ncm_req.h" +#include "ncm_login.h" +#include "ncm_api.h" +#include "ncm_cache.h" + +#define NCM_CTX_CA_PATH_MAX 256 + +typedef struct ncm_ctx { + ncm_store_t store; + ncm_cache_t cache; + char device_id[NCM_STORE_ID_MAX]; /* 52hex 已持久化 */ + char cookie[NCM_COOKIE_MAX]; /* 会话 cookie,可为空 */ + int verify_ca; /* 1=校验CA(默认) */ + int timeout_ms; /* HTTP 超时 */ + char ca_path[NCM_CTX_CA_PATH_MAX]; /* 可选 CA bundle */ + int default_level; /* NCM_LEVEL_* 默认音质(Hi-res) */ +} ncm_ctx_t; + +/* 默认缓存目录(/../cache,即 MeowData/os.meow.ncm/cache)。 */ +int ncm_ctx_default_cache_dir(const char *data_dir, char *out, size_t out_sz); + +/* 初始化上下文:store(数据目录) + deviceId 载入/生成 + cookie 载入 + cache 目录 + 缓存上限配置。 */ +void ncm_ctx_init(ncm_ctx_t *ctx, const char *data_dir); + +/* 会话 cookie 便捷存取(经 ncm_ctx 的 store)。 */ +int ncm_ctx_set_cookie(ncm_ctx_t *ctx, const char *cookie); /* 更新内存 + 加密落盘 */ +int ncm_ctx_load_cookie(ncm_ctx_t *ctx); /* 从盘载入到 ctx->cookie */ +int ncm_ctx_clear_cookie(ncm_ctx_t *ctx); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_H */ diff --git a/patch_v2/ncm_app/core/ncm_api.c b/patch_v2/ncm_app/core/ncm_api.c new file mode 100644 index 0000000..6877e29 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_api.c @@ -0,0 +1,265 @@ +/* ncm_api.c — 歌单/播放数据接口实现(响应/token 走堆,避免大缓冲驻栈) */ +#include "ncm_api.h" +#include "ncm_common.h" +#include "ncm_http.h" +#include "ncm_json.h" +#include "ncm_req.h" + +#include +#include +#include + +const char *ncm_api_level_str(int level) { + switch (level) { + case NCM_LEVEL_HIRES: return "hires"; + case NCM_LEVEL_LOSSLESS: return "lossless"; + case NCM_LEVEL_EXHIGH: return "exhigh"; + case NCM_LEVEL_HIGHER: return "higher"; + default: return "standard"; + } +} + +/* 堆上建立一次接口调用 + 解析上下文;用完必须 api_free */ +typedef struct api_json { + char *rbuf; /* 响应体缓冲(含 body) */ + jsmntok_t *toks; + int *parent; + int ntoks; + ncm_json_t j; + ncm_http_resp_t resp; +} api_json_t; + +static void api_free(api_json_t *c); + +static int api_post(const char *device_id, const char *cookie, const char *path, + const char *json, int kind, size_t resp_cap, api_json_t *c) { + char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX]; + int rc = ncm_req_build(device_id, cookie, path, json, kind, + body, sizeof body, hdrs, sizeof hdrs, url, sizeof url); + if (rc != NCM_OK) return rc; + + ncm_http_req_t req; + memset(&req, 0, sizeof req); + req.url = url; req.method = "POST"; req.body = body; req.body_len = strlen(body); + req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 15000; + + c->rbuf = (char *)malloc(resp_cap); + if (!c->rbuf) return NCM_ERR_OOM; + rc = ncm_http_request(&req, c->rbuf, resp_cap, &c->resp); + if (rc != NCM_OK) { free(c->rbuf); c->rbuf = NULL; return rc; } + + jsmn_parser p; + jsmn_init(&p); + int n = jsmn_parse(&p, c->resp.body, c->resp.body_len, NULL, 0); + if (n <= 0) { free(c->rbuf); c->rbuf = NULL; return NCM_ERR_JSON; } + c->toks = (jsmntok_t *)malloc(sizeof(jsmntok_t) * (size_t)n); + c->parent = (int *)malloc(sizeof(int) * (size_t)n); + if (!c->toks || !c->parent) { + free(c->rbuf); free(c->toks); free(c->parent); + c->rbuf = NULL; c->toks = NULL; c->parent = NULL; + return NCM_ERR_OOM; + } + rc = ncm_json_parse(&c->j, c->resp.body, c->resp.body_len, c->toks, n, c->parent); + if (rc < 0) { api_free(c); return NCM_ERR_JSON; } + c->ntoks = n; + + long code; + if (ncm_json_path_int(&c->j, "code", &code) == NCM_OK && code != 200) { + int ec = NCM_ERR_HTTP; + if (code == 301 || code == -460 || code == 2000 || code == 201) ec = NCM_ERR_AUTH; + api_free(c); + return ec; + } + return NCM_OK; +} + +static void api_free(api_json_t *c) { + free(c->rbuf); c->rbuf = NULL; + free(c->toks); c->toks = NULL; + free(c->parent); c->parent = NULL; +} + +static int obj_str(const ncm_json_t *j, int tok, const char *key, char *out, size_t out_sz) { + if (!out || out_sz == 0) return 0; + int vt = ncm_json_obj_get(j, tok, key); + if (vt < 0) { out[0] = 0; return 0; } + if (!ncm_json_get_str_at(j, vt, out, out_sz)) out[0] = 0; + return 0; +} +static long obj_int(const ncm_json_t *j, int tok, const char *key) { + int vt = ncm_json_obj_get(j, tok, key); + int rc; + return ncm_json_get_int_at(j, vt, &rc); +} + +int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid, + int limit, int offset, ncm_playlist_t *out, int max, int *out_n) { + if (!out_n) return NCM_ERR_ARG; + *out_n = 0; + if (!uid) return NCM_ERR_ARG; + char json[256]; + snprintf(json, sizeof json, "{\"uid\":\"%s\",\"limit\":%d,\"offset\":%d,\"includeVideo\":true}", + uid, limit > 0 ? limit : 30, offset > 0 ? offset : 0); + + api_json_t c; memset(&c, 0, sizeof c); + int rc = api_post(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI, + NCM_RESP_MAX, &c); + if (rc != NCM_OK) return rc; + + int arr = ncm_json_path(&c.j, "playlist"); + if (arr < 0 || ncm_json_type(&c.j, arr) != JSMN_ARRAY) { api_free(&c); return NCM_ERR_NOTFOUND; } + int n = ncm_json_arr_len(&c.j, arr); + if (n > max) n = max; + memset(out, 0, sizeof(*out) * (size_t)(n > 0 ? n : 0)); + for (int i = 0; i < n; i++) { + int e = ncm_json_arr_get(&c.j, arr, i); + if (e < 0) break; + obj_str(&c.j, e, "id", out[i].id, sizeof out[i].id); + obj_str(&c.j, e, "name", out[i].name, sizeof out[i].name); + obj_str(&c.j, e, "coverImgUrl", out[i].cover, sizeof out[i].cover); + out[i].track_count = (int)obj_int(&c.j, e, "trackCount"); + } + *out_n = n; + api_free(&c); + return NCM_OK; +} + +int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id, + char *name, size_t name_sz, char *cover, size_t cover_sz, + long *ids, int id_cap, int *out_id_n) { + if (!out_id_n) return NCM_ERR_ARG; + *out_id_n = 0; + char json[256]; + snprintf(json, sizeof json, "{\"id\":\"%s\",\"n\":100000,\"s\":8}", playlist_id); + + api_json_t c; memset(&c, 0, sizeof c); + int rc = api_post(device_id, cookie, "/api/v6/playlist/detail", json, NCM_CRYPT_EAPI, + NCM_RESP_MAX, &c); + if (rc != NCM_OK) return rc; + + int pl = ncm_json_path(&c.j, "playlist"); + if (pl < 0 || ncm_json_type(&c.j, pl) != JSMN_OBJECT) { api_free(&c); return NCM_ERR_NOTFOUND; } + if (name) obj_str(&c.j, pl, "name", name, name_sz); + if (cover) obj_str(&c.j, pl, "coverImgUrl", cover, cover_sz); + + int tid = ncm_json_obj_get(&c.j, pl, "trackIds"); + if (tid < 0 || ncm_json_type(&c.j, tid) != JSMN_ARRAY) { + tid = ncm_json_obj_get(&c.j, pl, "tracks"); + if (tid < 0) { api_free(&c); return NCM_OK; } + } + int n = ncm_json_arr_len(&c.j, tid); + int take = n < id_cap ? n : id_cap; + for (int i = 0; i < take; i++) { + int e = ncm_json_arr_get(&c.j, tid, i); + int vt = ncm_json_obj_get(&c.j, e, "id"); + int rc2; + ids[i] = ncm_json_get_int_at(&c.j, vt, &rc2); + } + *out_id_n = take; + api_free(&c); + return NCM_OK; +} + +int ncm_api_song_detail(const char *device_id, const char *cookie, const long *ids, int n, + ncm_song_t *out, int max, int *out_n) { + if (!out_n) return NCM_ERR_ARG; + *out_n = 0; + char clist[4096] = "[", tmp[64]; + for (int i = 0; i < n && strlen(clist) < sizeof clist - 80; i++) { + snprintf(tmp, sizeof tmp, "%s{\"id\":%ld}", i ? "," : "", ids[i]); + ncm_strlcat(clist, tmp, sizeof clist); + } + ncm_strlcat(clist, "]", sizeof clist); + char json[4500]; + snprintf(json, sizeof json, "{\"c\":\"%s\"}", clist); + + api_json_t c; memset(&c, 0, sizeof c); + int rc = api_post(device_id, cookie, "/api/v3/song/detail", json, NCM_CRYPT_EAPI, + 2 * 1024 * 1024, &c); + if (rc != NCM_OK) return rc; + + int arr = ncm_json_path(&c.j, "songs"); + if (arr < 0 || ncm_json_type(&c.j, arr) != JSMN_ARRAY) { api_free(&c); return NCM_ERR_NOTFOUND; } + int cnt = ncm_json_arr_len(&c.j, arr); + if (cnt > max) cnt = max; + memset(out, 0, sizeof(*out) * (size_t)(cnt > 0 ? cnt : 0)); + for (int i = 0; i < cnt; i++) { + int e = ncm_json_arr_get(&c.j, arr, i); + if (e < 0) break; + obj_str(&c.j, e, "id", out[i].id, sizeof out[i].id); + obj_str(&c.j, e, "name", out[i].name, sizeof out[i].name); + out[i].duration_ms = obj_int(&c.j, e, "dt"); + out[i].fee = (int)obj_int(&c.j, e, "fee"); + int ar = ncm_json_obj_get(&c.j, e, "ar"); + if (ar >= 0 && ncm_json_type(&c.j, ar) == JSMN_ARRAY) { + int a0 = ncm_json_arr_get(&c.j, ar, 0); + if (a0 >= 0) obj_str(&c.j, a0, "name", out[i].art_name, sizeof out[i].art_name); + } + int al = ncm_json_obj_get(&c.j, e, "al"); + if (al >= 0) obj_str(&c.j, al, "name", out[i].album_name, sizeof out[i].album_name); + } + *out_n = cnt; + api_free(&c); + return NCM_OK; +} + +int ncm_api_song_url(const char *device_id, const char *cookie, const char *id, + int level, ncm_play_url_t *out) { + if (!out) return NCM_ERR_ARG; + memset(out, 0, sizeof *out); + char json[256]; + snprintf(json, sizeof json, + "{\"ids\":\"[%s]\",\"level\":\"%s\",\"encodeType\":\"flac\"}", + id, ncm_api_level_str(level)); + + api_json_t c; memset(&c, 0, sizeof c); + int rc = api_post(device_id, cookie, "/api/song/enhance/player/url/v1", json, NCM_CRYPT_EAPI, + NCM_RESP_MAX, &c); + if (rc != NCM_OK) return rc; + + int d0 = ncm_json_path(&c.j, "data[0]"); + if (d0 < 0) { api_free(&c); return NCM_ERR_NOTFOUND; } + obj_str(&c.j, d0, "url", out->url, sizeof out->url); + obj_str(&c.j, d0, "type", out->type, sizeof out->type); + obj_str(&c.j, d0, "level", out->level, sizeof out->level); + out->br = (int)obj_int(&c.j, d0, "br"); + out->size = obj_int(&c.j, d0, "size"); + api_free(&c); + return NCM_OK; +} + +int ncm_api_song_url_auto(const char *device_id, const char *cookie, const char *id, + int desired, ncm_play_url_t *out) { + int start = desired; + if (start < NCM_LEVEL_STANDARD) start = NCM_LEVEL_HIRES; + if (start > NCM_LEVEL_HIRES) start = NCM_LEVEL_HIRES; + /* 从 desired 往下降级:hires->lossless->exhigh->higher->standard */ + for (int lv = start; lv >= NCM_LEVEL_STANDARD; lv--) { + ncm_play_url_t p; + int rc = ncm_api_song_url(device_id, cookie, id, lv, &p); + if (rc != NCM_OK) continue; + /* 只接受有 url 且 type 为 mp3(v1 不解 flac) */ + if (p.url[0] && (ncm_streq(p.type, "mp3") || p.type[0] == 0)) { + *out = p; + return NCM_OK; + } + /* url 为 null 或 flac:继续降级 */ + } + return NCM_ERR_NOTFOUND; +} + +int ncm_api_lyric(const char *device_id, const char *cookie, const char *id, + char *lrc, size_t lrc_sz, char *tlrc, size_t tlrc_sz) { + char json[256]; + snprintf(json, sizeof json, "{\"id\":\"%s\",\"tv\":-1,\"lv\":-1,\"rv\":-1,\"kv\":-1,\"_nmclfl\":1}", + id); + + api_json_t c; memset(&c, 0, sizeof c); + int rc = api_post(device_id, cookie, "/api/song/lyric", json, NCM_CRYPT_EAPI, + NCM_RESP_MAX, &c); + if (rc != NCM_OK) return rc; + if (lrc && lrc_sz) { lrc[0] = 0; ncm_json_path_str(&c.j, "lrc.lyric", lrc, lrc_sz); } + if (tlrc && tlrc_sz) { tlrc[0] = 0; ncm_json_path_str(&c.j, "tlyric.lyric", tlrc, tlrc_sz); } + api_free(&c); + return NCM_OK; +} diff --git a/patch_v2/ncm_app/core/ncm_api.h b/patch_v2/ncm_app/core/ncm_api.h new file mode 100644 index 0000000..e6d9802 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_api.h @@ -0,0 +1,82 @@ +/* ncm_api.h — 歌单 / 播放数据接口 + * + * 覆盖 NCM_API_NOTES §2.1 的 5 个数据端点: + * user_playlist(weapi) / playlist_detail(eapi,v6) / song_detail(eapi,v3) + * song_url_v1(eapi) / lyric(eapi) + * 解析用 jsmn 路径导航,响应体限幅(NCM_RESP_MAX)。封面/大 JSON 不整体驻留。 + */ +#ifndef NCM_API_H +#define NCM_API_H + +#include "ncm_store.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 音质档位(song_url_v1 level)。v1 可解码 mp3:standard/higher/exhigh。 */ +enum { + NCM_LEVEL_STANDARD = 0, + NCM_LEVEL_HIGHER, + NCM_LEVEL_EXHIGH, + NCM_LEVEL_LOSSLESS, + NCM_LEVEL_HIRES, +}; +const char *ncm_api_level_str(int level); + +typedef struct ncm_playlist { + char id[32]; + char name[256]; + char cover[512]; + int track_count; +} ncm_playlist_t; + +typedef struct ncm_song { + char id[32]; + char name[256]; + char art_name[256]; /* 首个歌手名 */ + char album_name[256]; + long duration_ms; + int fee; +} ncm_song_t; + +typedef struct ncm_play_url { + char url[1024]; + char type[16]; + char level[16]; + int br; + long size; +} ncm_play_url_t; + +/* 用户歌单列表(weapi /user/playlist)。uid 必填;limit/offset 翻页。 + * 返回 NCM_OK;songs/out_n 为列表。 */ +int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid, + int limit, int offset, ncm_playlist_t *out, int max, int *out_n); + +/* 歌单详情(eapi /v6/playlist/detail):取名字/封面 + trackIds 数组(填 ids, cap)。 */ +int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id, + char *name, size_t name_sz, char *cover, size_t cover_sz, + long *ids, int id_cap, int *out_id_n); + +/* 批量歌曲详情(eapi /v3/song/detail)。ids 传入曲目 id 数组。 */ +int ncm_api_song_detail(const char *device_id, const char *cookie, const long *ids, int n, + ncm_song_t *out, int max, int *out_n); + +/* 播放地址(eapi /song/enhance/player/url/v1)。level 指定档位;url 可能为 null(无版权/VIP)。 */ +int ncm_api_song_url(const char *device_id, const char *cookie, const char *id, + int level, ncm_play_url_t *out); + +/* 播放地址自动降级:从 desired 档开始,遇 url 为 null 或非 mp3(本机不可解码)则逐步降档, + * 只返回 mp3。desired 默认 hires。 */ +int ncm_api_song_url_auto(const char *device_id, const char *cookie, const char *id, + int desired, ncm_play_url_t *out); + +/* 歌词(eapi /song/lyric):lrc 原文 + 翻译。 */ +int ncm_api_lyric(const char *device_id, const char *cookie, const char *id, + char *lrc, size_t lrc_sz, char *tlrc, size_t tlrc_sz); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_API_H */ diff --git a/patch_v2/ncm_app/core/ncm_cache.c b/patch_v2/ncm_app/core/ncm_cache.c new file mode 100644 index 0000000..441baaa --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_cache.c @@ -0,0 +1,202 @@ +/* ncm_cache.c — 缓存实现 */ +#include "ncm_cache.h" +#include "ncm_common.h" +#include "ncm_store.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_LIMIT_GB 10 +#define MIN_LIMIT_GB 1 +#define MAX_LIMIT_GB 100 +#define MAX_ENTRIES 4096 + +typedef struct entry { + char name[256]; + uint64_t size; + int64_t mtime; +} entry_t; + +static int gb_to_bytes(int gb) { + if (gb < MIN_LIMIT_GB) gb = MIN_LIMIT_GB; + if (gb > MAX_LIMIT_GB) gb = MAX_LIMIT_GB; + return (int)((uint64_t)gb * 1024ULL * 1024ULL * 1024ULL); +} + +int ncm_cache_init(ncm_cache_t *c, const char *dir) { + if (!c) return NCM_ERR_ARG; + memset(c, 0, sizeof *c); + if (dir && *dir) ncm_strlcpy(c->dir, dir, sizeof c->dir); + else ncm_strlcpy(c->dir, "cache", sizeof c->dir); + size_t n = strlen(c->dir); + while (n > 1 && c->dir[n-1]=='/') c->dir[--n] = 0; + ncm_store_mkdir_p(c->dir); + c->limit_gb = DEFAULT_LIMIT_GB; + c->limit_bytes = (uint64_t)gb_to_bytes(DEFAULT_LIMIT_GB); + return NCM_OK; +} + +void ncm_cache_set_limit_gb(ncm_cache_t *c, int gb) { + if (!c) return; + c->limit_gb = gb < MIN_LIMIT_GB ? MIN_LIMIT_GB : (gb > MAX_LIMIT_GB ? MAX_LIMIT_GB : gb); + c->limit_bytes = (uint64_t)gb_to_bytes(c->limit_gb); +} + +int ncm_cache_limit_gb(const ncm_cache_t *c) { + return c ? c->limit_gb : DEFAULT_LIMIT_GB; +} + +void ncm_cache_load_setting(ncm_cache_t *c, const char *data_dir) { + if (!c || !data_dir) return; + int gb = ncm_store_read_cfg_int(data_dir, "ncm.cache_limit_gb", DEFAULT_LIMIT_GB); + ncm_cache_set_limit_gb(c, gb); +} + +const char *ncm_cache_dir(const ncm_cache_t *c) { + return c ? c->dir : ""; +} + +int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, + char *out, size_t out_sz) { + if (!c || !id || !out) return NCM_ERR_ARG; + int n = snprintf(out, out_sz, "%s/song_%s.%s", c->dir, id, ext ? ext : "bin"); + return (n >= 0 && (size_t)n < out_sz) ? NCM_OK : NCM_ERR_FULL; +} + +int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, + char *out, size_t out_sz) { + char path[NCM_CACHE_DIR_MAX + 32]; + if (ncm_cache_path(c, id, ext, path, sizeof path) != 0) return NCM_ERR_FULL; + struct stat st; + if (stat(path, &st) != 0) return NCM_ERR_NOENT; + if (!S_ISREG(st.st_mode)) return NCM_ERR_NOENT; + utime(path, NULL); /* 刷新最后访问(按 mtime LRU) */ + ncm_strlcpy(out, path, out_sz); + return NCM_OK; +} + +struct ncm_cache_writer { + char final_path[NCM_CACHE_DIR_MAX + 64]; + char tmp_path[NCM_CACHE_DIR_MAX + 80]; + FILE *fp; + uint64_t nwritten; + int failed; +}; + +int ncm_cache_open(const ncm_cache_t *c, const char *id, const char *ext, + ncm_cache_writer_t **w_out) { + if (!c || !id || !w_out) return NCM_ERR_ARG; + ncm_cache_writer_t *w = (ncm_cache_writer_t *)calloc(1, sizeof *w); + if (!w) return NCM_ERR_OOM; + if (ncm_cache_path(c, id, ext, w->final_path, sizeof w->final_path) != 0) { + free(w); return NCM_ERR_FULL; + } + snprintf(w->tmp_path, sizeof w->tmp_path, "%s.tmp", w->final_path); + w->fp = fopen(w->tmp_path, "wb"); + if (!w->fp) { free(w); return NCM_ERR_IO; } + *w_out = w; + return NCM_OK; +} + +int ncm_cache_write(ncm_cache_writer_t *w, const unsigned char *data, size_t len) { + if (!w || !w->fp || w->failed) return NCM_ERR_ARG; + if (fwrite(data, 1, len, w->fp) != len) { w->failed = 1; return NCM_ERR_IO; } + w->nwritten += len; + return NCM_OK; +} + +static int cmp_mtime_asc(const void *a, const void *b) { + const entry_t *x = (const entry_t *)a, *y = (const entry_t *)b; + if (x->mtime < y->mtime) return -1; + if (x->mtime > y->mtime) return 1; + return 0; +} + +int ncm_cache_evict(const ncm_cache_t *c) { + if (!c) return NCM_ERR_ARG; + DIR *d = opendir(c->dir); + if (!d) return NCM_ERR_IO; + entry_t *ents = (entry_t *)malloc(sizeof(entry_t) * MAX_ENTRIES); + if (!ents) { closedir(d); return NCM_ERR_OOM; } + int n = 0; + uint64_t total = 0; + struct dirent *e; + while ((e = readdir(d)) && n < MAX_ENTRIES) { + if (strncmp(e->d_name, "song_", 5) != 0) continue; + char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; + snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name); + struct stat st; + if (stat(p, &st) != 0 || !S_ISREG(st.st_mode)) continue; + ncm_strlcpy(ents[n].name, e->d_name, sizeof ents[n].name); + ents[n].size = (uint64_t)st.st_size; + ents[n].mtime = (int64_t)st.st_mtim.tv_sec; + total += ents[n].size; + n++; + } + closedir(d); + + if (total > c->limit_bytes && n > 0) { + qsort(ents, (size_t)n, sizeof(entry_t), cmp_mtime_asc); + /* 从最旧删起,直到 <= 上限 */ + for (int i = 0; i < n; i++) { + if (total <= c->limit_bytes) break; + char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; + snprintf(p, sizeof p, "%s/%s", c->dir, ents[i].name); + if (unlink(p) == 0 && ents[i].size < total) total -= ents[i].size; + } + } + free(ents); + return NCM_OK; +} + +int ncm_cache_commit(ncm_cache_writer_t *w) { + if (!w) return NCM_ERR_ARG; + int rc = NCM_OK; + if (w->failed || !w->fp) rc = NCM_ERR_IO; + else if (fflush(w->fp) != 0) rc = NCM_ERR_IO; + else if (fsync(fileno(w->fp)) != 0) rc = NCM_ERR_IO; + else if (fclose(w->fp) != 0) rc = NCM_ERR_IO; + else { + w->fp = NULL; + if (rename(w->tmp_path, w->final_path) != 0) { unlink(w->tmp_path); rc = NCM_ERR_IO; } + } + if (w->fp) { fclose(w->fp); w->fp = NULL; } + if (rc == NCM_OK) { + /* 刷新 mtime(= 最近访问)*/ + utime(w->final_path, NULL); + } + free(w); + return rc; +} + +int ncm_cache_abort(ncm_cache_writer_t *w) { + if (!w) return NCM_ERR_ARG; + if (w->fp) { fclose(w->fp); w->fp = NULL; } + unlink(w->tmp_path); + free(w); + return NCM_OK; +} + +uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) { + if (!c) return 0; + DIR *d = opendir(c->dir); + if (!d) return 0; + uint64_t total = 0; + struct dirent *e; + while ((e = readdir(d))) { + if (strncmp(e->d_name, "song_", 5) != 0) continue; + char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8]; + snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name); + struct stat st; + if (stat(p, &st) == 0 && S_ISREG(st.st_mode)) total += (uint64_t)st.st_size; + } + closedir(d); + return total; +} diff --git a/patch_v2/ncm_app/core/ncm_cache.h b/patch_v2/ncm_app/core/ncm_cache.h new file mode 100644 index 0000000..7f4691e --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_cache.h @@ -0,0 +1,62 @@ +/* ncm_cache.h — 听过的歌自动缓存(LRU 淘汰到上限) + * + * 目录:MeowData/os.meow.ncm/cache/(可被 ncm_cache_set_base_dir 覆盖)。 + * 上限:默认 10GB,可配 1~100GB,配置键 ncm.cache_limit_gb(MeowOS 设置,读 /.cfg)。 + * LRU:以文件 mtime 作为最后访问时间(lookup/commit 时刷新);超限时按 mtime 升序删最旧。 + * 命中缓存即可离线播放(ncm_cache_lookup 返回已缓存文件路径)。 + */ +#ifndef NCM_CACHE_H +#define NCM_CACHE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define NCM_CACHE_DIR_MAX 512 + +typedef struct ncm_cache { + char dir[NCM_CACHE_DIR_MAX]; + uint64_t limit_bytes; /* 上限字节 */ + int limit_gb; +} ncm_cache_t; + +/* 初始化:设置目录 + 默认 10GB 上限,mkdir -p。 */ +int ncm_cache_init(ncm_cache_t *c, const char *dir); + +/* 设置/获取上限(1~100GB,越界钳制)。 */ +void ncm_cache_set_limit_gb(ncm_cache_t *c, int gb); +int ncm_cache_limit_gb(const ncm_cache_t *c); + +/* 从数据目录读 ncm.cache_limit_gb 配置(对齐 SDK meow_config_int)并应用。 */ +void ncm_cache_load_setting(ncm_cache_t *c, const char *data_dir); + +/* 缓存目录 / 单文件路径 */ +const char *ncm_cache_dir(const ncm_cache_t *c); +int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, + char *out, size_t out_sz); + +/* 查找已缓存文件。命中:写 out 路径并刷新 mtime,返回 NCM_OK。未命中:NCM_ERR_NOENT。 */ +int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, + char *out, size_t out_sz); + +/* 流式写入句柄:open 建 temp;write 累积;commit 落盘+刷新LRU+触发淘汰;abort 丢弃。 */ +typedef struct ncm_cache_writer ncm_cache_writer_t; +int ncm_cache_open(const ncm_cache_t *c, const char *id, const char *ext, + ncm_cache_writer_t **w); +int ncm_cache_write(ncm_cache_writer_t *w, const unsigned char *data, size_t len); +int ncm_cache_commit(ncm_cache_writer_t *w); /* win; 返回 0=成功 */ +int ncm_cache_abort(ncm_cache_writer_t *w); + +/* 当前缓存总字节(扫描目录) */ +uint64_t ncm_cache_total_bytes(const ncm_cache_t *c); + +/* 立即执行 LRU 淘汰(超上限则删最旧,直到 <= 上限)。 */ +int ncm_cache_evict(const ncm_cache_t *c); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_CACHE_H */ diff --git a/patch_v2/ncm_app/core/ncm_common.c b/patch_v2/ncm_app/core/ncm_common.c new file mode 100644 index 0000000..6594023 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_common.c @@ -0,0 +1,193 @@ +/* ncm_common.c — 公共工具层实现 */ +#include "ncm_common.h" + +#include +#include +#include +#include +#include + +#include "mbedtls/base64.h" +#include "mbedtls/md.h" + +size_t ncm_strlcpy(char *dst, const char *src, size_t sz) { + size_t sl = strlen(src); + if (sz == 0) return sl; + size_t cp = (sl < sz - 1) ? sl : sz - 1; + memcpy(dst, src, cp); + dst[cp] = 0; + return sl; +} + +size_t ncm_strlcat(char *dst, const char *src, size_t sz) { + size_t dl = strlen(dst); + size_t sl = strlen(src); + if (sz == 0 || dl >= sz) return dl + sl; + size_t cp = (sl < sz - dl - 1) ? sl : sz - dl - 1; + memcpy(dst + dl, src, cp); + dst[dl + cp] = 0; + return dl + sl; +} + +int ncm_streq(const char *a, const char *b) { + return strcmp(a, b) == 0; +} + +int ncm_parse_int(const char *s, long *out) { + if (!s || !*s) return -1; + char *end = NULL; + long v = strtol(s, &end, 10); + if (end == s || *end != '\0') return -1; + *out = v; + return 0; +} + +int64_t ncm_now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +void ncm_random_bytes(unsigned char *out, size_t n) { + FILE *f = fopen("/dev/urandom", "rb"); + if (f) { + size_t got = fread(out, 1, n, f); + fclose(f); + if (got == n) return; + } + /* 回退:时钟 + 地址扰动(仅用于 deviceId 初次生成,非密码学用途) */ + static uint64_t seed = 0; + if (seed == 0) + seed = (uint64_t)ncm_now_ms() ^ (uint64_t)(uintptr_t)out; + for (size_t i = 0; i < n; i++) { + seed = seed * 6364136223846793005ULL + 1442695040888963407ULL; + out[i] = (unsigned char)(seed >> 33); + } +} + +void ncm_hex_encode(const unsigned char *in, size_t n, char *out, int upper) { + static const char dL[] = "0123456789abcdef"; + static const char dU[] = "0123456789ABCDEF"; + const char *d = upper ? dU : dL; + for (size_t i = 0; i < n; i++) { + out[i * 2] = d[in[i] >> 4]; + out[i * 2 + 1] = d[in[i] & 15]; + } + out[n * 2] = 0; +} + +int ncm_hex_decode(const char *hex, unsigned char *out, size_t out_sz) { + size_t n = strlen(hex); + if (n % 2) return -1; + size_t bytes = n / 2; + if (bytes > out_sz) return -1; + for (size_t i = 0; i < bytes; i++) { + int hi, lo; + char c = hex[i * 2]; + if (c >= '0' && c <= '9') hi = c - '0'; + else if (c >= 'a' && c <= 'f') hi = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') hi = c - 'A' + 10; + else return -1; + c = hex[i * 2 + 1]; + if (c >= '0' && c <= '9') lo = c - '0'; + else if (c >= 'a' && c <= 'f') lo = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') lo = c - 'A' + 10; + else return -1; + out[i] = (unsigned char)((hi << 4) | lo); + } + return (int)bytes; +} + +int ncm_b64_encode(const unsigned char *in, size_t inlen, char *out, size_t out_sz) { + size_t olen = 0; + int rc = mbedtls_base64_encode((unsigned char *)out, out_sz, &olen, in, inlen); + if (rc != 0) return -1; + out[olen < out_sz ? olen : out_sz - 1] = 0; + return (int)olen; +} + +int ncm_b64_decode(const char *in, size_t inlen, unsigned char *out, size_t out_sz) { + size_t olen = 0; + int rc = mbedtls_base64_decode(out, out_sz, &olen, (const unsigned char *)in, inlen); + if (rc != 0) return -1; + return (int)olen; +} + +void ncm_md5_hex(const void *data, size_t len, char out_hex[33]) { + unsigned char d[16]; + const mbedtls_md_info_t *inf = mbedtls_md_info_from_type(MBEDTLS_MD_MD5); + if (inf) mbedtls_md(inf, (const unsigned char *)data, len, d); + else memset(d, 0, 16); + ncm_hex_encode(d, 16, out_hex, 0); +} + +int ncm_sha256(const void *data, size_t len, unsigned char out[32]) { + const mbedtls_md_info_t *inf = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + if (!inf) return -1; + return mbedtls_md(inf, (const unsigned char *)data, len, out); +} + +void ncm_sha256_hex(const void *data, size_t len, char out_hex[65]) { + unsigned char d[32]; + ncm_sha256(data, len, d); + ncm_hex_encode(d, 32, out_hex, 0); +} + +int ncm_url_encode_span(const char *src, size_t slen, char *dst, size_t dst_sz) { + size_t o = 0; + for (size_t i = 0; i < slen; i++) { + unsigned char c = (unsigned char)src[i]; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') { + if (o + 1 >= dst_sz) return -NCM_ERR_FULL; + dst[o++] = (char)c; + } else if (c == ' ') { + if (o + 1 >= dst_sz) return -NCM_ERR_FULL; + dst[o++] = '+'; + } else { + if (o + 3 >= dst_sz) return -NCM_ERR_FULL; + static const char d[] = "0123456789ABCDEF"; + dst[o++] = '%'; + dst[o++] = d[c >> 4]; + dst[o++] = d[c & 15]; + } + } + dst[o] = 0; + return (int)o; +} + +int ncm_url_encode(const char *src, char *dst, size_t dst_sz) { + return ncm_url_encode_span(src, strlen(src), dst, dst_sz); +} + +int ncm_url_decode(const char *src, char *dst, size_t dst_sz) { + size_t o = 0; + size_t slen = strlen(src); + for (size_t i = 0; i < slen; i++) { + char c = src[i]; + if (c == '%') { + if (i + 2 >= slen) return -1; + int hi, lo; + char a = src[i + 1], b = src[i + 2]; + if (a >= '0' && a <= '9') hi = a - '0'; + else if (a >= 'a' && a <= 'f') hi = a - 'a' + 10; + else if (a >= 'A' && a <= 'F') hi = a - 'A' + 10; + else return -1; + if (b >= '0' && b <= '9') lo = b - '0'; + else if (b >= 'a' && b <= 'f') lo = b - 'a' + 10; + else if (b >= 'A' && b <= 'F') lo = b - 'A' + 10; + else return -1; + if (o + 1 >= dst_sz) return -NCM_ERR_FULL; + dst[o++] = (char)((hi << 4) | lo); + i += 2; + } else if (c == '+') { + if (o + 1 >= dst_sz) return -NCM_ERR_FULL; + dst[o++] = ' '; + } else { + if (o + 1 >= dst_sz) return -NCM_ERR_FULL; + dst[o++] = c; + } + } + dst[o] = 0; + return (int)o; +} diff --git a/patch_v2/ncm_app/core/ncm_common.h b/patch_v2/ncm_app/core/ncm_common.h new file mode 100644 index 0000000..e25a3b3 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_common.h @@ -0,0 +1,76 @@ +/* ncm_common.h — 网易云 App 核心库公共工具层(纯 C,无 UI) + * + * 提供:错误码、定长/动态缓冲、hex、base64、md5、sha256、urlencode 等 + * 无关业务的通用原语。所有实现集中在本目录,不依赖 UI/SDK。 + */ +#ifndef NCM_COMMON_H +#define NCM_COMMON_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---------- 错误码 ---------- */ +enum { + NCM_OK = 0, /* 成功 */ + NCM_ERR_ARG = -1, /* 参数错误 */ + NCM_ERR_OOM = -2, /* 内存不足 */ + NCM_ERR_IO = -3, /* 文件/IO 错误 */ + NCM_ERR_NET = -4, /* 网络错误 */ + NCM_ERR_TLS = -5, /* TLS 错误 */ + NCM_ERR_TIMEOUT = -6, /* 超时 */ + NCM_ERR_HTTP = -7, /* HTTP 状态非 2xx */ + NCM_ERR_CRYPTO = -8, /* 加解密错误 */ + NCM_ERR_JSON = -9, /* JSON 解析错误 */ + NCM_ERR_NOTFOUND = -10, /* 未找到(缺字段/null) */ + NCM_ERR_FULL = -11, /* 目标缓冲满 */ + NCM_ERR_NOENT = -12, /* 文件不存在 */ + NCM_ERR_AUTH = -13, /* 未登录/鉴权失败 */ + NCM_ERR_LIMIT = -14, /* 超出上限级 */ +}; + +/* ---------- 常用尺寸 ---------- */ +#define NCM_DEVICE_ID_LEN 52 /* 52 位小写十六进制(实际上传大写,见 crypto) */ +#define NCM_DEVICE_ID_SZ (NCM_DEVICE_ID_LEN + 1) +#define NCM_COOKIE_MAX 4096 /* 会话 cookie 串最大长度 */ +#define NCM_URL_MAX 2048 /* 单个 URL/路径最大长度 */ +#define NCM_HEADER_MAX 1024 /* 单次请求 Cookie 头最大长度 */ +#define NCM_JSON_MAX 8192 /* 小 JSON 明文(请求体)最大长度 */ +#define NCM_RESP_MAX (1024 * 1024) /* 单次响应体默认上限 (1MB) */ +#define NCM_SMALLBUF 256 + +/* ---------- 小工具 ---------- */ +size_t ncm_strlcpy(char *dst, const char *src, size_t sz); +size_t ncm_strlcat(char *dst, const char *src, size_t sz); +int ncm_streq(const char *a, const char *b); +int ncm_parse_int(const char *s, long *out); /* 完整数字串,成功返回 0 */ +int64_t ncm_now_ms(void); /* 毫秒时间戳 */ +void ncm_random_bytes(unsigned char *out, size_t n); /* /dev/urandom,失败则回退时钟扰动 */ + +/* ---------- hex ---------- */ +void ncm_hex_encode(const unsigned char *in, size_t n, char *out, int upper); /* out >= 2n+1 */ +int ncm_hex_decode(const char *hex, unsigned char *out, size_t out_sz); /* 返回字节数, 失败 -1 */ + +/* ---------- base64 (mbedtls 封装; 均为栈外缓冲调用) ---------- */ +int ncm_b64_encode(const unsigned char *in, size_t inlen, char *out, size_t out_sz); /* 成功返回写入长度,失败 -1 */ +int ncm_b64_decode(const char *in, size_t inlen, unsigned char *out, size_t out_sz); /* 成功返回字节数, 失败 -1 */ + +/* ---------- MD5 / SHA256 (mbedtls 封装) ---------- */ +void ncm_md5_hex(const void *data, size_t len, char out_hex[33]); /* 小写 hex */ +int ncm_sha256(const void *data, size_t len, unsigned char out[32]);/* 原始 32 字节, 0=成功 */ +void ncm_sha256_hex(const void *data, size_t len, char out_hex[65]);/* 小写 hex */ + +/* ---------- urlencode (application/x-www-form-urlencoded: 空格->'+') ---------- */ +/* 成功返回写入长度(不含NUL),溢出返回 -NCM_ERR_FULL */ +int ncm_url_encode(const char *src, char *dst, size_t dst_sz); +int ncm_url_encode_span(const char *src, size_t slen, char *dst, size_t dst_sz); +/* 解码 (也接受 '+' -> 空格)。返回解码字节数,失败 -1 */ +int ncm_url_decode(const char *src, char *dst, size_t dst_sz); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_COMMON_H */ diff --git a/patch_v2/ncm_app/core/ncm_crypto.c b/patch_v2/ncm_app/core/ncm_crypto.c new file mode 100644 index 0000000..abf7d5d --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_crypto.c @@ -0,0 +1,140 @@ +/* ncm_crypto.c — 加密层实现 */ +#include "ncm_crypto.h" +#include "ncm_common.h" + +#include +#include +#include + +#include "mbedtls/aes.h" +#include "mbedtls/base64.h" +#include "mbedtls/pk.h" +#include "mbedtls/rsa.h" + +#define SEP "-36cd479b6b5-" + +/* ---------- AES 原语 ---------- */ +/* AES-128-ECB 加密(PKCS7)-> 大写 hex */ +static int aes_ecb_hex(const unsigned char *in, size_t n, const unsigned char *key, + char *out, size_t out_sz) { + int pad = 16 - (int)(n % 16); + size_t total = n + (size_t)pad; + if (out_sz < total * 2 + 1) return -1; + unsigned char *buf = (unsigned char *)malloc(total); + unsigned char *enc = (unsigned char *)malloc(total); + if (!buf || !enc) { free(buf); free(enc); return -1; } + memcpy(buf, in, n); + for (int i = 0; i < pad; i++) buf[n + i] = (unsigned char)pad; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_enc(&aes, key, 128); + for (size_t off = 0; off < total; off += 16) + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, &buf[off], &enc[off]); + mbedtls_aes_free(&aes); + ncm_hex_encode(enc, total, out, 1); /* 大写 */ + free(buf); + free(enc); + return 0; +} + +/* AES-128-CBC 加密(PKCS7)-> base64 */ +static int aes_cbc_b64(const unsigned char *in, size_t n, + const unsigned char *key, const unsigned char *iv, + char *out, size_t out_sz) { + int pad = 16 - (int)(n % 16); + size_t total = n + (size_t)pad; + unsigned char *buf = (unsigned char *)malloc(total); + unsigned char *enc = (unsigned char *)malloc(total); + if (!buf || !enc) { free(buf); free(enc); return -1; } + memcpy(buf, in, n); + for (int i = 0; i < pad; i++) buf[n + i] = (unsigned char)pad; + unsigned char iv2[16]; + memcpy(iv2, iv, 16); + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_enc(&aes, key, 128); + mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, total, iv2, buf, enc); + mbedtls_aes_free(&aes); + int rc = ncm_b64_encode(enc, total, out, out_sz); + free(buf); + free(enc); + return rc < 0 ? -1 : 0; +} + +/* ---------- RSA raw ---------- */ +static int rsa_encrypt_raw_hex(const unsigned char secret[16], char *out, size_t out_sz) { + unsigned char der[512]; + int dl = ncm_b64_decode(NCM_RSA_DER_B64, strlen(NCM_RSA_DER_B64), der, sizeof der); + if (dl <= 0) return -1; + size_t der_len = (size_t)dl; + mbedtls_pk_context pk; + mbedtls_pk_init(&pk); + if (mbedtls_pk_parse_public_key(&pk, der, der_len) != 0) { + mbedtls_pk_free(&pk); + return -1; + } + mbedtls_rsa_context *rsa = mbedtls_pk_rsa(pk); + int keylen = mbedtls_rsa_get_len(rsa); + if (keylen <= 0 || keylen > 256) { mbedtls_pk_free(&pk); return -1; } + unsigned char input[256], output[256]; + memset(input, 0, sizeof input); + /* 右对齐到 keylen */ + memcpy(input + keylen - 16, secret, 16); /* 前导字节为 0 => input < N */ + if (mbedtls_rsa_public(rsa, input, output) != 0) { mbedtls_pk_free(&pk); return -1; } + if (out_sz < (size_t)keylen * 2 + 1) { mbedtls_pk_free(&pk); return -1; } + ncm_hex_encode(output, (size_t)keylen, out, 0); /* 小写 */ + mbedtls_pk_free(&pk); + return 0; +} + +int ncm_crypto_rsa_raw_selfcheck(const unsigned char secret[16], char *out, size_t out_sz) { + return rsa_encrypt_raw_hex(secret, out, out_sz); +} + +int ncm_crypto_eapi(const char *url, const char *json, char *out, size_t out_sz) { + if (!url || !json || !out) return NCM_ERR_ARG; + char msg[4096]; + int ml = snprintf(msg, sizeof msg, "nobody%suse%smd5forencrypt", url, json); + if (ml < 0 || (size_t)ml >= sizeof msg) return NCM_ERR_FULL; + char digest[33]; + ncm_md5_hex(msg, (size_t)ml, digest); + char plain[4096]; + int pl = snprintf(plain, sizeof plain, "%s%s%s%s%s", url, SEP, json, SEP, digest); + if (pl < 0 || (size_t)pl >= sizeof plain) return NCM_ERR_FULL; + return aes_ecb_hex((const unsigned char *)plain, (size_t)pl, + (const unsigned char *)NCM_EAPI_KEY, out, out_sz) == 0 ? NCM_OK : NCM_ERR_CRYPTO; +} + +int ncm_crypto_weapi(const char *json, char *params, size_t psz, + char *enc_sec_key, size_t esz) { + if (!json || !params || !enc_sec_key) return NCM_ERR_ARG; + /* 生成 16 位 base62 随机密钥 */ + unsigned char secret[16]; + unsigned char rnd[16]; + ncm_random_bytes(rnd, sizeof rnd); + for (int i = 0; i < 16; i++) + secret[i] = (unsigned char)NCM_WEAPI_BASE62[rnd[i] % 62]; + + /* 第一层 AES-CBC(preset) */ + char first[1024]; + if (aes_cbc_b64((const unsigned char *)json, strlen(json), + (const unsigned char *)NCM_WEAPI_PRESET, + (const unsigned char *)NCM_WEAPI_IV, first, sizeof first) != 0) + return NCM_ERR_CRYPTO; + /* 第二层 AES-CBC(secret) */ + if (aes_cbc_b64((const unsigned char *)first, strlen(first), + secret, (const unsigned char *)NCM_WEAPI_IV, params, psz) != 0) + return NCM_ERR_CRYPTO; + /* RSA: 反转 secret 后 raw 加密 */ + unsigned char rev[16]; + for (int i = 0; i < 16; i++) rev[i] = secret[15 - i]; + if (rsa_encrypt_raw_hex(rev, enc_sec_key, esz) != 0) return NCM_ERR_CRYPTO; + return NCM_OK; +} + +int ncm_crypto_linuxapi(const char *json, char *out, size_t out_sz) { + if (!json || !out) return NCM_ERR_ARG; + return aes_ecb_hex((const unsigned char *)json, strlen(json), + (const unsigned char *)NCM_LINUXAPI_KEY, out, out_sz) == 0 + ? NCM_OK : NCM_ERR_CRYPTO; +} diff --git a/patch_v2/ncm_app/core/ncm_crypto.h b/patch_v2/ncm_app/core/ncm_crypto.h new file mode 100644 index 0000000..f9ee88a --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_crypto.h @@ -0,0 +1,49 @@ +/* ncm_crypto.h — 网易云请求加密层 + * + * 提供 eapi / weapi / linuxapi 三种加密(对齐 CNMPlayer ncm-api-rs crypto.rs): + * eapi: digest=md5("nobody"+url+"use"+json+"md5forencrypt") + * data=url+"-36cd479b6b5-"+json+"-36cd479b6b5-"+digest + * params=HEX_UPPER(AES128ECB(data, EAPI_KEY)) (eapi key: e82ckenh8dichen8) + * weapi: 双层 AES-128-CBC + RSA raw (无 padding 纯模幂) + * secret=16 随机 base62; first=AES_CBC_b64(json, PRESET, IV) + * params=AES_CBC_b64(first, secret, IV) + * encSecKey=HEX_LOWER(RSA_raw(REVERSE(secret), 1024 公钥)) + * linuxapi: params=HEX_UPPER(AES128ECB(json, LINUXAPI_KEY)) + * + * 由调用方负责对输出值做 urlencode(weapi 的 params 是 base64,含 '+' '/' '=' 需转义)。 + */ +#ifndef NCM_CRYPTO_H +#define NCM_CRYPTO_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define NCM_EAPI_KEY "e82ckenh8dichen8" +#define NCM_WEAPI_PRESET "0CoJUm6Qyw8W8jud" +#define NCM_WEAPI_IV "0102030405060708" +#define NCM_LINUXAPI_KEY "rFgB&h#%2?^eDg:Q" +#define NCM_WEAPI_BASE62 \ + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +#define NCM_RSA_DER_B64 \ + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB" + +/* eapi:json 明文 -> params 值(大写 hex)。out_sz 需 >= 2*len(plain)+1。返回 NCM_OK */ +int ncm_crypto_eapi(const char *url, const char *json, char *out, size_t out_sz); + +/* weapi:json 明文 -> params(base64) 与 encSecKey(小写 hex)。返回 NCM_OK */ +int ncm_crypto_weapi(const char *json, char *params, size_t psz, + char *enc_sec_key, size_t esz); + +/* linuxapi:json 明文 -> params 值(大写 hex)。返回 NCM_OK */ +int ncm_crypto_linuxapi(const char *json, char *out, size_t out_sz); + +/* 低频自测:固定明文跑一遍 RSA raw,写 256 hex 到 out。返回 NCM_OK */ +int ncm_crypto_rsa_raw_selfcheck(const unsigned char secret[16], char *out, size_t out_sz); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_CRYPTO_H */ diff --git a/patch_v2/ncm_app/core/ncm_ctx.c b/patch_v2/ncm_app/core/ncm_ctx.c new file mode 100644 index 0000000..97577de --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_ctx.c @@ -0,0 +1,57 @@ +/* ncm_ctx.c — 上下文实现 */ +#include "ncm.h" +#include +#include + +int ncm_ctx_default_cache_dir(const char *data_dir, char *out, size_t out_sz) { + if (!data_dir || !out) return NCM_ERR_ARG; + size_t dlen = strlen(data_dir); + /* 去掉尾部斜杠 */ + while (dlen > 1 && data_dir[dlen-1]=='/') dlen--; + if (dlen >= 5 && strcmp(data_dir + dlen - 5, "/data") == 0) { + int n = snprintf(out, out_sz, "%.*s/cache", (int)(dlen - 5), data_dir); + return (n >= 0 && (size_t)n < out_sz) ? NCM_OK : NCM_ERR_FULL; + } + int n = snprintf(out, out_sz, "%.*s/cache", (int)dlen, data_dir); + return (n >= 0 && (size_t)n < out_sz) ? NCM_OK : NCM_ERR_FULL; +} + +void ncm_ctx_init(ncm_ctx_t *ctx, const char *data_dir) { + if (!ctx) return; + memset(ctx, 0, sizeof *ctx); + ncm_store_init(&ctx->store, data_dir); + ncm_store_get_device_id(&ctx->store, ctx->device_id); + ncm_ctx_load_cookie(ctx); + + char cdir[NCM_CACHE_DIR_MAX]; + if (ncm_ctx_default_cache_dir(ctx->store.data_dir, cdir, sizeof cdir) != 0) + ncm_strlcpy(cdir, "cache", sizeof cdir); + ncm_cache_init(&ctx->cache, cdir); + ncm_cache_load_setting(&ctx->cache, ctx->store.data_dir); + + ctx->verify_ca = 1; + ctx->timeout_ms = 15000; + ctx->default_level = NCM_LEVEL_HIRES; + ncm_http_set_verify_default(ctx->verify_ca, ctx->ca_path[0] ? ctx->ca_path : NULL); +} + +int ncm_ctx_set_cookie(ncm_ctx_t *ctx, const char *cookie) { + if (!ctx) return NCM_ERR_ARG; + ncm_strlcpy(ctx->cookie, cookie ? cookie : "", sizeof ctx->cookie); + return ncm_login_save_cookie(&ctx->store, ctx->cookie); +} + +int ncm_ctx_load_cookie(ncm_ctx_t *ctx) { + if (!ctx) return NCM_ERR_ARG; + char buf[NCM_COOKIE_MAX]; + int rc = ncm_login_load_cookie(&ctx->store, buf, sizeof buf); + if (rc == NCM_OK) ncm_strlcpy(ctx->cookie, buf, sizeof ctx->cookie); + else ctx->cookie[0] = 0; + return rc; +} + +int ncm_ctx_clear_cookie(ncm_ctx_t *ctx) { + if (!ctx) return NCM_ERR_ARG; + ctx->cookie[0] = 0; + return ncm_login_clear_cookie(&ctx->store); +} diff --git a/patch_v2/ncm_app/core/ncm_http.c b/patch_v2/ncm_app/core/ncm_http.c new file mode 100644 index 0000000..450eec6 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_http.c @@ -0,0 +1,527 @@ +/* ncm_http.c — mbedtls HTTPS 客户端封装实现 */ +#include "ncm_http.h" +#include "ncm_common.h" + +#include +#include +#include +#include + +#include "mbedtls/net_sockets.h" +#include "mbedtls/ssl.h" +#include "mbedtls/x509_crt.h" +#include "mbedtls/error.h" + +/* ---------- 自定义 RNG ---------- */ +static FILE *s_rng_fp = NULL; +static int my_rng(void *arg, unsigned char *out, size_t len) { + (void)arg; + if (!s_rng_fp) s_rng_fp = fopen("/dev/urandom", "rb"); + if (!s_rng_fp) return -1; + return fread(out, 1, len, s_rng_fp) == len ? 0 : -1; +} + +/* ---------- 全局 CA 默认 ---------- */ +static int g_verify_ca = 1; +static char g_ca_path[NCM_CA_PATH_MAX] = ""; +void ncm_http_set_verify_default(int verify, const char *ca_path) { + g_verify_ca = verify ? 1 : 0; + if (ca_path) ncm_strlcpy(g_ca_path, ca_path, sizeof g_ca_path); + else g_ca_path[0] = 0; +} + +/* ---------- URL 解析 ---------- */ +typedef struct url_parts { + char scheme[8]; + char host[256]; + int port; + char path[NCM_URL_MAX]; +} url_parts_t; + +static int parse_url(const char *url, url_parts_t *u) { + if (!url) return -1; + const char *p = strstr(url, "://"); + if (!p) return -1; + size_t schemelen = (size_t)(p - url); + if (schemelen >= sizeof u->scheme) return -1; + memcpy(u->scheme, url, schemelen); + u->scheme[schemelen] = 0; + const char *rest = p + 3; + const char *slash = strchr(rest, '/'); + const char *hostend = slash ? slash : rest + strlen(rest); + size_t hostlen = (size_t)(hostend - rest); + if (hostlen >= sizeof u->host) return -1; + memcpy(u->host, rest, hostlen); + u->host[hostlen] = 0; + + u->port = 443; + char *colon = strrchr(u->host, ':'); + if (colon) { + long port; + if (ncm_parse_int(colon + 1, &port) != 0 || port <= 0 || port > 65535) return -1; + u->port = (int)port; + *colon = 0; + } + const char *path = slash ? slash : "/"; + if (strlen(path) >= sizeof u->path) return -1; + ncm_strlcpy(u->path, path, sizeof u->path); + return 0; +} + +/* ---------- CA 加载 ---------- */ +static int load_ca(mbedtls_x509_crt *cacert, const char *ca_path) { + const char *defaults[] = { + "/etc/ssl/certs/ca-certificates.crt", + "/etc/ssl/certs/ca-bundle.crt", + "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/cert.pem", + NULL + }; + if (ca_path && *ca_path) + return mbedtls_x509_crt_parse_file(cacert, ca_path) == 0 ? 0 : -1; + for (int i = 0; defaults[i]; i++) + if (mbedtls_x509_crt_parse_file(cacert, defaults[i]) == 0) return 0; + return -1; +} + +/* ---------- 等待 IO(deadline 约束) ---------- */ +static int wait_io(mbedtls_net_context *net, uint32_t rw, int64_t deadline_ms) { + int64_t now = ncm_now_ms(); + if (now >= deadline_ms) return MBEDTLS_ERR_SSL_TIMEOUT; + uint32_t rem = (uint32_t)(deadline_ms - now); + int r = mbedtls_net_poll(net, rw, rem); + if (r < 0) return r; + if (r == 0) return MBEDTLS_ERR_SSL_TIMEOUT; + return 0; +} + +/* ---------- 响应头解析 ---------- */ +static int ci_eq(const char *a, const char *b) { + while (*a && *b) { + unsigned char ca = (unsigned char)*a, cb = (unsigned char)*b; + if (ca >= 'A' && ca <= 'Z') ca = (unsigned char)(ca - 'A' + 'a'); + if (cb >= 'A' && cb <= 'Z') cb = (unsigned char)(cb - 'A' + 'a'); + if (ca != cb) return 0; + a++; b++; + } + return *a == 0 && *b == 0; +} +static int ci_contains(const char *hay, const char *needle) { + size_t nlen = strlen(needle); + if (!nlen) return 1; + for (; *hay; hay++) { + size_t i = 0; + while (i < nlen && hay[i]) { + unsigned char h = (unsigned char)hay[i], n = (unsigned char)needle[i]; + if (h >= 'A' && h <= 'Z') h = (unsigned char)(h - 'A' + 'a'); + if (n >= 'A' && n <= 'Z') n = (unsigned char)(n - 'A' + 'a'); + if (h != n) break; + i++; + } + if (i == nlen) return 1; + } + return 0; +} + +static long parse_long_field(const char *value) { + long v; + return ncm_parse_int(value, &v) == 0 ? v : -1; +} + +static int parse_headers(ncm_http_resp_t *resp) { + const char *h = resp->headers; + resp->status = 0; + resp->content_length = (size_t)-1; + resp->chunked = 0; + resp->has_content_type = 0; + resp->content_type[0] = 0; + + /* 状态行: "HTTP/1.1 200 OK" */ + char line[NCM_HTTP_HDR_MAX]; + size_t li = 0; + const char *p = h; + /* 第一行 */ + while (*p && *p != '\r' && *p != '\n' && li < sizeof line - 1) line[li++] = *p++; + line[li] = 0; + { + const char *sp = strchr(line, ' '); + if (sp) { + long st = parse_long_field(sp + 1); + if (st >= 100 && st <= 599) resp->status = (int)st; + } + } + /* 头行遍历 */ + p = h; + while ((p = strstr(p, "\r\n")) != NULL) { + p += 2; + if (*p == '\r' || *p == '\n' || *p == 0) break; + const char *eol = strstr(p, "\r\n"); + if (!eol) break; + size_t l = (size_t)(eol - p); + char hdr[256]; + if (l >= sizeof hdr) { p = eol + 2; continue; } + memcpy(hdr, p, l); + hdr[l] = 0; + char *colon = strchr(hdr, ':'); + if (!colon) { p = eol + 2; continue; } + *colon = 0; + char *name = hdr; + char *val = colon + 1; + while (*val == ' ' || *val == '\t') val++; + /* name 去尾空格 */ + char *nend = name + strlen(name); + while (nend > name && (nend[-1]==' '||nend[-1]=='\t')) *--nend = 0; + + if (ci_eq(name, "Content-Length")) { + long v = parse_long_field(val); + if (v >= 0) resp->content_length = (size_t)v; + } else if (ci_eq(name, "Transfer-Encoding")) { + if (ci_contains(val, "chunked")) resp->chunked = 1; + } else if (ci_eq(name, "Content-Type")) { + resp->has_content_type = 1; + ncm_strlcpy(resp->content_type, val, sizeof resp->content_type); + } + p = eol + 2; + } + return 0; +} + +/* ---------- 响应体分块状态机 ---------- */ +enum { + BS_LEN, /* 定长 body,剩余 remaining */ + BS_CLOSE, /* 读到对端关闭 */ + BS_CHUNK_SZ, /* 解析 chunk 尺寸行 */ + BS_CHUNK_DATA,/* 读 chunk 数据 remaining */ + BS_CHUNK_CRLF,/* 吃 chunk 终止 CRLF */ + BS_DONE +}; +typedef struct { + int st; + size_t remaining; + size_t total; + int exceeded; + char chunk_buf[16]; + int chunk_len; +} body_st; + +static void body_st_init(body_st *b, const ncm_http_resp_t *hdr) { + memset(b, 0, sizeof *b); + if (hdr->chunked) { + b->st = BS_CHUNK_SZ; + } else if (hdr->content_length != (size_t)-1) { + b->st = BS_LEN; + b->remaining = hdr->content_length; + } else { + b->st = BS_CLOSE; + } +} + +static int parse_chunk_size(body_st *b) { + long sz; + char *end = NULL; + b->chunk_buf[b->chunk_len] = 0; + /* 忽略扩展: 取 ';' 之前 */ + char *semi = strchr(b->chunk_buf, ';'); + if (semi) *semi = 0; + char *sb = b->chunk_buf; + while (*sb == ' ' || *sb == '\t') sb++; + if (!*sb) return -1; + sz = strtol(sb, &end, 16); + if (end == sb || *end != 0) return -1; + if (sz == 0) { b->st = BS_DONE; return 0; } + b->st = BS_CHUNK_DATA; + b->remaining = (size_t)sz; + return 0; +} + +/* 处理一段输入,产出解码后的 body 字节到 sink。 + * sink: (buf,cap,&len) 累加;或 (cb,ctx) 回调。返回 0=继续,NZ=中止。 */ +typedef struct sink { + unsigned char *buf; + size_t cap; + size_t len; + size_t max_bytes; /* 0=不设上限(受 cap 约束) */ + ncm_http_write_cb cb; + void *cb_ctx; + int overflow; +} sink_t; + +static void sink_write(sink_t *s, const unsigned char *data, size_t len) { + if (s->max_bytes && s->len + len > s->max_bytes) { s->overflow = 1; return; } + if (s->cb) { + if (s->cb(s->cb_ctx, data, len) != 0) { s->overflow = 1; } + return; + } + if (s->len + len > s->cap) { s->overflow = 1; return; } + memcpy(s->buf + s->len, data, len); + s->len += len; +} + +static int body_feed(body_st *b, const unsigned char *data, size_t len, sink_t *s) { + size_t i = 0; + while (i < len) { + switch (b->st) { + case BS_LEN: { + size_t take = len - i; + if (take > b->remaining) take = b->remaining; + if (take) { sink_write(s, data + i, take); b->remaining -= take; b->total += take; i += take; } + if (b->remaining == 0) b->st = BS_DONE; + if (take == 0) i++; /* 防御性推进 */ + break; + } + case BS_CLOSE: { + sink_write(s, data + i, len - i); + b->total += len - i; + i = len; + break; + } + case BS_CHUNK_SZ: { + if (i < len && (data[i] == '\r')) { i++; continue; } + if (i < len && data[i] == '\n') { + if (parse_chunk_size(b) != 0) return -1; + i++; + continue; + } + if (b->chunk_len < (int)sizeof b->chunk_buf - 1) { + b->chunk_buf[b->chunk_len++] = (char)data[i]; + } + i++; + break; + } + case BS_CHUNK_DATA: { + size_t take = len - i; + if (take > b->remaining) take = b->remaining; + if (take) { sink_write(s, data + i, take); b->remaining -= take; b->total += take; i += take; } + if (b->remaining == 0) b->st = BS_CHUNK_CRLF; + if (take == 0) i++; + break; + } + case BS_CHUNK_CRLF: { + while (i < len && (data[i]=='\r'||data[i]=='\n')) i++; + b->chunk_len = 0; + b->st = BS_CHUNK_SZ; + break; + } + case BS_DONE: + return (int)i; + default: + return -1; + } + if (s->overflow) return (int)i; + } + return (int)i; +} + +/* ---------- 核心请求 ---------- */ +static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *resp) { + int ret; + url_parts_t url; + if (parse_url(req->url, &url) != 0) return NCM_ERR_ARG; + if (strcmp(url.scheme, "https") != 0 && strcmp(url.scheme, "http") != 0) return NCM_ERR_ARG; + + uint32_t tmo = req->timeout_ms ? req->timeout_ms : 15000; + int64_t deadline = ncm_now_ms() + (int64_t)tmo; + + mbedtls_net_context net; + mbedtls_ssl_context ssl; + mbedtls_ssl_config conf; + mbedtls_x509_crt cacert; + mbedtls_net_init(&net); + mbedtls_ssl_init(&ssl); + mbedtls_ssl_config_init(&conf); + mbedtls_x509_crt_init(&cacert); + int validate = req->verify_ca < 0 ? g_verify_ca : req->verify_ca; + const char *ca_path = (req->ca_path && *req->ca_path) ? req->ca_path + : (g_ca_path[0] ? g_ca_path : NULL); + + if (validate && load_ca(&cacert, ca_path) != 0) { + ret = NCM_ERR_TLS; /* 无 CA 则拒绝,不静默放行 */ + goto done; + } + + char portstr[8]; + snprintf(portstr, sizeof portstr, "%d", url.port); + if ((ret = mbedtls_net_connect(&net, url.host, portstr, MBEDTLS_NET_PROTO_TCP)) != 0) { + ret = NCM_ERR_NET; goto done; + } + mbedtls_net_set_nonblock(&net); + + if ((ret = mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT)) != 0) goto done; + mbedtls_ssl_conf_authmode(&conf, validate ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_NONE); + mbedtls_ssl_conf_ca_chain(&conf, &cacert, NULL); + mbedtls_ssl_conf_rng(&conf, my_rng, NULL); + if ((ret = mbedtls_ssl_setup(&ssl, &conf)) != 0) goto done; + if ((ret = mbedtls_ssl_set_hostname(&ssl, url.host)) != 0) goto done; + mbedtls_ssl_set_bio(&ssl, &net, mbedtls_net_send, mbedtls_net_recv, NULL); + + /* 握手(带 deadline) */ + for (;;) { + ret = mbedtls_ssl_handshake(&ssl); + if (ret == 0) break; + if (ret == MBEDTLS_ERR_SSL_WANT_READ) { + if ((ret = wait_io(&net, MBEDTLS_NET_POLL_READ, deadline)) != 0) goto done; + } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + if ((ret = wait_io(&net, MBEDTLS_NET_POLL_WRITE, deadline)) != 0) goto done; + } else goto done; + } + + /* 构造请求(先写头、再写体,避免大 body 撑爆单一缓冲) */ + char headbuf[1024]; + int hl; + const char *m = req->method ? req->method : "POST"; + int has_body = (req->body && req->body_len > 0); + const char *eh = req->extra_hdrs ? req->extra_hdrs : ""; + const char *eh_sep = (eh[0]) ? "\r\n" : ""; + if (has_body) { + hl = snprintf(headbuf, sizeof headbuf, + "%s %s HTTP/1.1\r\nHost: %s\r\n" + "%s%s" + "Content-Length: %zu\r\nConnection: close\r\n\r\n", + m, url.path, url.host, eh, eh_sep, req->body_len); + } else { + hl = snprintf(headbuf, sizeof headbuf, + "%s %s HTTP/1.1\r\nHost: %s\r\n" + "%s%s" + "Connection: close\r\n\r\n", + m, url.path, url.host, eh, eh_sep); + } + if (hl < 0 || hl >= (int)sizeof headbuf) { ret = NCM_ERR_FULL; goto done; } + + /* 发送头 */ + size_t off = 0; + while (off < (size_t)hl) { + ret = mbedtls_ssl_write(&ssl, (const unsigned char *)headbuf + off, (size_t)hl - off); + if (ret > 0) { off += (size_t)ret; } + else if (ret == MBEDTLS_ERR_SSL_WANT_READ) { if ((ret=wait_io(&net, MBEDTLS_NET_POLL_READ, deadline))!=0) goto done; } + else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { if ((ret=wait_io(&net, MBEDTLS_NET_POLL_WRITE, deadline))!=0) goto done; } + else goto done; + } + if (has_body) { + off = 0; + while (off < req->body_len) { + ret = mbedtls_ssl_write(&ssl, (const unsigned char *)req->body + off, req->body_len - off); + if (ret > 0) { off += (size_t)ret; } + else if (ret == MBEDTLS_ERR_SSL_WANT_READ) { if ((ret=wait_io(&net, MBEDTLS_NET_POLL_READ, deadline))!=0) goto done; } + else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { if ((ret=wait_io(&net, MBEDTLS_NET_POLL_WRITE, deadline))!=0) goto done; } + else goto done; + } + } + + /* 读响应头 */ + resp->headers_len = 0; + resp->headers[0] = 0; + int hdr_done = 0; + unsigned char tmp[4096]; + body_st bst; + memset(&bst, 0, sizeof bst); + + for (;;) { + ret = mbedtls_ssl_read(&ssl, tmp, sizeof tmp); + if (ret > 0) { + size_t n = (size_t)ret; + if (!hdr_done) { + /* 幂等累积到 headers */ + size_t space = NCM_HTTP_HDR_MAX - 1 - resp->headers_len; + if (space > n) space = n; + memcpy(resp->headers + resp->headers_len, tmp, space); + resp->headers_len += space; + resp->headers[resp->headers_len] = 0; + /* 找 \r\n\r\n */ + char *split = strstr(resp->headers, "\r\n\r\n"); + if (split) { + parse_headers(resp); + body_st_init(&bst, resp); + size_t consumed = (size_t)(split + 4 - resp->headers); + /* 当前 tmp 中,已进 headers 的字节数 = space;tmp 剩余部分属于 body */ + /* headers 里 split+4 之后的字节实际上没被复制进 headers(因为 space<=..? ) + 这里简化:把 tmp[space..n) 送入 body */ + hdr_done = 1; + /* 需要把 headers 缓冲里 split+4 之后的字节(若有) 也送到 body */ + size_t hdr_after = resp->headers_len - consumed; + if (hdr_after > 0) { + if (body_feed(&bst, (const unsigned char *)(split + 4), hdr_after, sink) < 0) { ret=NCM_ERR_JSON; goto done; } + } + if (space < n) { + if (body_feed(&bst, tmp + space, n - space, sink) < 0) { ret=NCM_ERR_JSON; goto done; } + } + } else { + if (resp->headers_len >= NCM_HTTP_HDR_MAX - 1) { ret = NCM_ERR_FULL; goto done; } + } + } else { + if (body_feed(&bst, tmp, n, sink) < 0) { ret = NCM_ERR_JSON; goto done; } + } + if (sink->overflow || bst.st == BS_DONE) break; + } else if (ret == MBEDTLS_ERR_SSL_WANT_READ) { + if ((ret = wait_io(&net, MBEDTLS_NET_POLL_READ, deadline)) != 0) { if (ret==MBEDTLS_ERR_SSL_TIMEOUT) ret=NCM_ERR_TIMEOUT; goto done; } + } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + if ((ret = wait_io(&net, MBEDTLS_NET_POLL_WRITE, deadline)) != 0) goto done; + } else if (ret == 0 || ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { + break; /* 对端关闭 */ + } else { + if (ret == MBEDTLS_ERR_SSL_TIMEOUT) ret = NCM_ERR_TIMEOUT; else ret = NCM_ERR_NET; + goto done; + } + } + if (sink->overflow) { ret = NCM_ERR_FULL; goto done; } + ret = NCM_OK; +done: + mbedtls_net_free(&net); + mbedtls_ssl_free(&ssl); + mbedtls_ssl_config_free(&conf); + mbedtls_x509_crt_free(&cacert); + if (s_rng_fp) { fclose(s_rng_fp); s_rng_fp = NULL; } + return ret; +} + +int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_http_resp_t *resp) { + if (!req || !resp) return NCM_ERR_ARG; + memset(resp, 0, sizeof *resp); + sink_t s; + memset(&s, 0, sizeof s); + s.buf = (unsigned char *)buf; + s.cap = buf_sz; + int rc = http_run(req, &s, resp); + resp->body = buf; + resp->body_len = s.len; + return rc; +} + +int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx, + size_t max_bytes, ncm_http_resp_t *resp) { + if (!req || !cb || !resp) return NCM_ERR_ARG; + memset(resp, 0, sizeof *resp); + sink_t s; + memset(&s, 0, sizeof s); + s.cb = cb; + s.cb_ctx = ctx; + s.max_bytes = max_bytes; + int rc = http_run(req, &s, resp); + return rc; +} + +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); + /* 名字可能含空格? 直接取 val[0..nlen) 去尾空格 */ + 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; +} diff --git a/patch_v2/ncm_app/core/ncm_http.h b/patch_v2/ncm_app/core/ncm_http.h new file mode 100644 index 0000000..ab0988e --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_http.h @@ -0,0 +1,74 @@ +/* ncm_http.h — mbedtls HTTPS 客户端封装 + * + * 职责:TLS1.2 下 POST/GET;带超时;支持 Content-Length 与 Transfer-Encoding: chunked。 + * CA 方案取舍: + * verify_ca=1(默认):加载系统 CA bundle(或 ca_path 指定)校验服务器证书,失败拒绝。 + * 防中间人;但依赖设备内置 CA 根(VENDOR 侧需内置 ca-certificates)。 + * verify_ca=0:跳过证书校验(MBEDTLS_SSL_VERIFY_NONE)。仅用于无 CA 的调试/受限网络, + * 会暴露 MITM 风险,正式发布应=1。API 层默认为 1。 + * + * 本层只补 Host/Content-Length/Connection: close 三个必需头;UA/Cookie/Content-Type 等 + * 由调用方经 extra_hdrs 传入(完整 CRLF 行,无结尾空行)。 + */ +#ifndef NCM_HTTP_H +#define NCM_HTTP_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define NCM_HTTP_HDR_MAX 4096 +#define NCM_CA_PATH_MAX 256 + +/* req->verify_ca 三态:1=校验,0=跳过,-1=用全局默认(ncm_http_set_verify_default) */ +#define NCM_HTTP_VERIFY_DEFAULT (-1) + +/* 设置全局默认 CA 行为(供 api/login 层无需逐参传递)。verify=1 校验、0 跳过; + * ca_path 拷贝到内部,可为 NULL(用系统默认)。 */ +void ncm_http_set_verify_default(int verify, const char *ca_path); + +typedef struct ncm_http_req { + const char *url; /* 完整 URL(http/https,仅 https 加密) */ + const char *method; /* "GET"/"POST" */ + const char *body; /* 表单体,可为 NULL */ + size_t body_len; /* 0 表示 strlen(body) */ + const char *extra_hdrs; /* 附加头(完整 CRLF 行),可为 NULL */ + uint32_t timeout_ms; /* 整体超时(ms);0=默认 15000 */ + int verify_ca; /* 1=校验CA,0=跳过(仅调试) */ + const char *ca_path; /* 指定 CA bundle 路径;NULL=系统默认 */ +} ncm_http_req_t; + +typedef struct ncm_http_resp { + int status; /* HTTP 状态码(解析自响应行) */ + char headers[NCM_HTTP_HDR_MAX]; /* 响应头(自持,NUL 终止) */ + size_t headers_len; + const char *body; /* 指向请求方 body 缓冲(ncm_http_request 模式) */ + size_t body_len; /* 已落入 body 缓冲的字节数 */ + size_t content_length; /* Content-Length 解析值;SIZE_MAX=未知 */ + int chunked; + int has_content_type; + char content_type[128]; +} ncm_http_resp_t; + +/* 一次性请求:响应(头+体)读入调用方缓冲。buf 容量 buf_sz(含 body)。 + * resp->body 指向 buf 内 body 起始。返回 NCM_OK 或负错误码。 */ +int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, + ncm_http_resp_t *resp); + +/* 流式下载(用于音频/封面缓存):响应体逐块交给回调,回调返回非 0 中止。 + * max_bytes 上限,超出即中止并返回 NCM_ERR_LIMIT。 */ +typedef int (*ncm_http_write_cb)(void *ctx, const unsigned char *data, size_t len); +int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx, + size_t max_bytes, ncm_http_resp_t *resp); + +/* 从响应头提取所有 Set-Cookie 的 name=value(去掉 Domain/Path/Expires 等属性), + * 以 "; " 连接写入 out(用于登录捕获 MUSIC_U 等)。成功返回 cookie 条数,失败 -1。 */ +int ncm_http_parse_set_cookie(const char *hdrs, size_t len, char *out, size_t out_sz); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_HTTP_H */ diff --git a/patch_v2/ncm_app/core/ncm_json.c b/patch_v2/ncm_app/core/ncm_json.c new file mode 100644 index 0000000..bf98335 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_json.c @@ -0,0 +1,189 @@ +/* ncm_json.c — jsmn 封装实现 */ +#include "ncm_json.h" +#include "ncm_common.h" + +#include +#include + +int ncm_json_parse(ncm_json_t *j, const char *text, size_t len, + jsmntok_t *toks, int tokcap, int *parent) { + jsmn_parser p; + jsmn_init(&p); + int n = jsmn_parse(&p, text, len, NULL, 0); + if (n <= 0) return NCM_ERR_JSON; + if (n > tokcap) return NCM_ERR_FULL; + + jsmn_init(&p); + if (jsmn_parse(&p, text, len, toks, n) < 0) return NCM_ERR_JSON; + + j->text = text; + j->len = len; + j->toks = toks; + j->ntoks = n; + j->parent = parent; + + /* 构建 parent:预序 DFS,用“最近包含”的开放 token 作父。 + * stack 为临时分配(随解析生命周期,建成后释放)。 */ + int *stack = (int *)malloc(sizeof(int) * (size_t)n); + if (!stack) return NCM_ERR_OOM; + int top = -1; + for (int i = 0; i < n; i++) { + while (top >= 0) { + int s = stack[top]; + int s_start = toks[s].start, s_end = toks[s].end; + int i_start = toks[i].start, i_end = toks[i].end; + /* i 是否完全落在 s 之内 */ + int inside = (i_start >= s_start && i_end <= s_end); + if (inside) break; + top--; + } + parent[i] = (top >= 0) ? stack[top] : -1; + stack[++top] = i; + } + free(stack); + + /* 合理性: 每个非根 token 必须有父,父索引应小于自己 */ + for (int i = 1; i < n; i++) { + if (parent[i] >= i) return NCM_ERR_JSON; + } + return n; +} + +int ncm_json_type(const ncm_json_t *j, int tok) { + if (tok < 0 || tok >= j->ntoks) return JSMN_UNDEFINED; + return j->toks[tok].type; +} + +static int is_object(const ncm_json_t *j, int tok) { + return ncm_json_type(j, tok) == JSMN_OBJECT; +} +static int is_array(const ncm_json_t *j, int tok) { + return ncm_json_type(j, tok) == JSMN_ARRAY; +} + +int ncm_json_obj_get(const ncm_json_t *j, int tok, const char *key) { + if (!is_object(j, tok)) return -1; + size_t kl = strlen(key); + for (int t = 0; t < j->ntoks; t++) { + if (j->parent[t] == tok && j->toks[t].type == JSMN_STRING) { + size_t len = (size_t)(j->toks[t].end - j->toks[t].start); + if (len == kl && memcmp(j->text + j->toks[t].start, key, kl) == 0) { + if (t + 1 < j->ntoks && j->parent[t + 1] == tok) + return t + 1; + return -1; + } + } + } + return -1; +} + +int ncm_json_arr_get(const ncm_json_t *j, int tok, int idx) { + if (!is_array(j, tok)) return -1; + int k = 0; + for (int t = 0; t < j->ntoks; t++) { + if (j->parent[t] == tok) { + if (k == idx) return t; + k++; + } + } + return -1; +} + +int ncm_json_arr_len(const ncm_json_t *j, int tok) { + if (!is_array(j, tok)) return -1; + int k = 0; + for (int t = 0; t < j->ntoks; t++) + if (j->parent[t] == tok) k++; + return k; +} + +const char *ncm_json_get_str_at(const ncm_json_t *j, int tok, char *out, size_t out_sz) { + if (tok < 0 || tok >= j->ntoks) return NULL; + int type = j->toks[tok].type; + if (type != JSMN_STRING && type != JSMN_PRIMITIVE) return NULL; + int start = j->toks[tok].start; + int end = j->toks[tok].end; + /* JSON 字面量 null 视为“无值”,避免把 "null" 当字符串 */ + if (type == JSMN_PRIMITIVE && (size_t)(end - start) == 4 && + memcmp(j->text + start, "null", 4) == 0) + return NULL; + if (end < start) return NULL; + size_t len = (size_t)(end - start); + if (len >= out_sz) return NULL; + memcpy(out, j->text + start, len); + out[len] = 0; + return out; +} + +long ncm_json_get_int_at(const ncm_json_t *j, int tok, int *rc) { + if (rc) *rc = NCM_OK; + if (tok < 0 || tok >= j->ntoks) { if (rc) *rc = NCM_ERR_NOTFOUND; return 0; } + char s[32]; + if (!ncm_json_get_str_at(j, tok, s, sizeof s)) { if (rc) *rc = NCM_ERR_NOTFOUND; return 0; } + long v = 0; + if (ncm_parse_int(s, &v) != 0) { if (rc) *rc = NCM_ERR_JSON; return 0; } + return v; +} + +/* --- 路径导航 --- */ +static int obj_get_path(const ncm_json_t *j, int cur, const char *seg_start, int klen) { + char key[256]; + if (klen <= 0 || klen >= (int)sizeof key) return -1; + memcpy(key, seg_start, (size_t)klen); + key[klen] = 0; + return ncm_json_obj_get(j, cur, key); +} + +int ncm_json_path(const ncm_json_t *j, const char *path) { + if (!path || !*path) return 0; + int cur = 0; /* 根 token */ + const char *p = path; + while (*p) { + if (*p == '.') { p++; continue; } + if (*p == '[') { + const char *close = strchr(p, ']'); + if (!close) return -1; + long idx; + size_t l = (size_t)(close - p - 1); + if (l == 0 || l >= 16) return -1; + char tmp[16]; + memcpy(tmp, p + 1, l); + tmp[l] = 0; + if (ncm_parse_int(tmp, &idx) != 0 || idx < 0) return -1; + cur = ncm_json_arr_get(j, cur, (int)idx); + if (cur < 0) return -1; + p = close + 1; + } else { + const char *seg_start = p; + while (*p && *p != '.' && *p != '[') p++; + cur = obj_get_path(j, cur, seg_start, (int)(p - seg_start)); + if (cur < 0) return -1; + } + } + return cur; +} + +const char *ncm_json_path_str(const ncm_json_t *j, const char *path, char *out, size_t out_sz) { + int tok = ncm_json_path(j, path); + if (tok < 0) return NULL; + return ncm_json_get_str_at(j, tok, out, out_sz); +} + +int ncm_json_path_int(const ncm_json_t *j, const char *path, long *out) { + int tok = ncm_json_path(j, path); + int rc = NCM_ERR_NOTFOUND; + long v = ncm_json_get_int_at(j, tok, &rc); + if (rc != NCM_OK) return rc; + if (out) *out = v; + return NCM_OK; +} + +int ncm_json_path_bool(const ncm_json_t *j, const char *path) { + int tok = ncm_json_path(j, path); + if (tok < 0) return -1; + char s[8]; + if (!ncm_json_get_str_at(j, tok, s, sizeof s)) return -1; + if (ncm_streq(s, "true")) return 1; + if (ncm_streq(s, "false")) return 0; + return -1; +} diff --git a/patch_v2/ncm_app/core/ncm_json.h b/patch_v2/ncm_app/core/ncm_json.h new file mode 100644 index 0000000..dc5fbf5 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_json.h @@ -0,0 +1,58 @@ +/* ncm_json.h — jsmn 封装:无 DOM、路径导航取值 + * + * jsmn 只做分词(无 malloc/DOM)。本封装在其之上构建 parent 数组, + * 提供按点路径(如 "data[0].url")取值的小接口,用于解析响应 JSON。 + * 调用方提供 token 数组 + parent 数组(预分配),避免库内 malloc 大块。 + */ +#ifndef NCM_JSON_H +#define NCM_JSON_H + +#include +#include "jsmn.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ncm_json { + const char *text; /* JSON 字符串(非拥有) */ + size_t len; + jsmntok_t *toks; /* token 数组(调用方提供) */ + int ntoks; /* 解析出的 token 数 */ + int *parent; /* parent 索引数组(调用方提供,长度 >= ntoks) */ +} ncm_json_t; + +/* 解析 text,写入 toks/parent。tokcap 为 toks 容量。 + * 返回 token 数(>=0),失败返回负错误码(NCM_ERR_*)。 + * 完成后 j->parent 已构建(parent[k] = k 的父 token 索引,根为 -1)。 */ +int ncm_json_parse(ncm_json_t *j, const char *text, size_t len, + jsmntok_t *toks, int tokcap, int *parent); + +/* 返回 token 类型(JSMN_OBJECT/ARRAY/STRING/PRIMITIVE/UNDEFINED) */ +int ncm_json_type(const ncm_json_t *j, int tok); + +/* 对象取键值的 token 索引;非对象/未找到返回 -1 */ +int ncm_json_obj_get(const ncm_json_t *j, int tok, const char *key); +/* 数组取下标的 token 索引;越界返回 -1 */ +int ncm_json_arr_get(const ncm_json_t *j, int tok, int idx); +/* 数组元素个数;非数组返回 -1 */ +int ncm_json_arr_len(const ncm_json_t *j, int tok); + +/* 取 token 处字符串值(去引号);写入 out,返回 out 或 NULL */ +const char *ncm_json_get_str_at(const ncm_json_t *j, int tok, char *out, size_t out_sz); +/* 取 token 处整数;primitiv/text 转 long,失败返回 0(用 rc 判错) */ +long ncm_json_get_int_at(const ncm_json_t *j, int tok, int *rc); + +/* 路径导航:支持 "data[0].url" / "playlist.trackIds" / "code" 等点+下标路径。 + * 返回目标 token 索引,失败 -1。 */ +int ncm_json_path(const ncm_json_t *j, const char *path); + +/* 便捷取值:路径 -> 字符串/整数/布尔 */ +const char *ncm_json_path_str(const ncm_json_t *j, const char *path, char *out, size_t out_sz); +int ncm_json_path_int(const ncm_json_t *j, const char *path, long *out); +int ncm_json_path_bool(const ncm_json_t *j, const char *path); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_JSON_H */ diff --git a/patch_v2/ncm_app/core/ncm_login.c b/patch_v2/ncm_app/core/ncm_login.c new file mode 100644 index 0000000..26ccda0 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_login.c @@ -0,0 +1,197 @@ +/* ncm_login.c — 扫码登录实现 */ +#include "ncm_login.h" +#include "ncm_common.h" +#include "ncm_http.h" +#include "ncm_json.h" +#include "ncm_req.h" + +#include +#include +#include + +#include "qrcodegen.h" + +#define QR_URL_FMT "https://music.163.com/login?codekey=%s" + +int ncm_login_qr_url(const char *unikey, char *url, size_t usz) { + if (!unikey || !url) return NCM_ERR_ARG; + int n = snprintf(url, usz, QR_URL_FMT, unikey); + return (n >= 0 && (size_t)n < usz) ? NCM_OK : NCM_ERR_FULL; +} + +int ncm_login_unikey(const char *device_id, char *unikey, size_t usz) { + char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX]; + int rc = ncm_req_build(device_id, NULL, "/api/login/qrcode/unikey", + "{\"type\":3}", NCM_CRYPT_EAPI, + body, sizeof body, hdrs, sizeof hdrs, url, sizeof url); + if (rc != NCM_OK) return rc; + ncm_http_req_t req; + memset(&req, 0, sizeof req); + req.url = url; + req.method = "POST"; + req.body = body; + req.body_len = strlen(body); + req.extra_hdrs = hdrs; + req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; + req.timeout_ms = 15000; + + char respbuf[4096]; + ncm_http_resp_t resp; + rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); + if (rc != NCM_OK) return rc; + + ncm_json_t j; + jsmntok_t toks[512]; + int parent[512]; + rc = ncm_json_parse(&j, resp.body, resp.body_len, toks, 512, parent); + if (rc < 0) return NCM_ERR_JSON; + + char u[64] = ""; + const char *v = ncm_json_path_str(&j, "unikey", u, sizeof u); + if (!v) v = ncm_json_path_str(&j, "data.unikey", u, sizeof u); + if (!v) v = ncm_json_path_str(&j, "data.uniKey", u, sizeof u); + if (!v) return NCM_ERR_NOTFOUND; + ncm_strlcpy(unikey, u, usz); + return NCM_OK; +} + +int ncm_login_qr_matrix(const char *url, unsigned char *mods, int max_size, int *out_size) { + if (!url || !mods || !out_size) return NCM_ERR_ARG; + uint8_t temp[qrcodegen_BUFFER_LEN_MAX], qr[qrcodegen_BUFFER_LEN_MAX]; + if (!qrcodegen_encodeText(url, temp, qr, qrcodegen_Ecc_MEDIUM, 1, 40, + qrcodegen_Mask_AUTO, true)) + return NCM_ERR_CRYPTO; + int size = qrcodegen_getSize(qr); + if (size > max_size) return NCM_ERR_FULL; + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + mods[y * size + x] = qrcodegen_getModule(qr, x, y) ? 1 : 0; + *out_size = size; + return NCM_OK; +} + +/* 合并 set-cookie 到会话:给 out 以 captured,若 captured 空则保留 cur_cookie 中已有的对 */ +static void merge_cookie(const char *cur, const char *captured, char *out, size_t out_sz) { + if (!captured || !*captured) { + ncm_strlcpy(out, cur ? cur : "", out_sz); + return; + } + ncm_strlcpy(out, captured, out_sz); +} + +int ncm_login_poll(const char *device_id, const char *unikey, const char *cur_cookie, + ncm_login_poll_t *r, char *out_cookie, size_t out_cookie_sz) { + if (!device_id || !unikey || !r) return NCM_ERR_ARG; + memset(r, 0, sizeof *r); + r->state = NCM_LOGIN_ERROR; + r->code = -1; + + char json[256]; + snprintf(json, sizeof json, "{\"key\":\"%s\",\"type\":3}", unikey); + + char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX]; + int rc = ncm_req_build(device_id, cur_cookie, "/api/login/qrcode/client/login", + json, NCM_CRYPT_EAPI, + body, sizeof body, hdrs, sizeof hdrs, url, sizeof url); + if (rc != NCM_OK) return rc; + + ncm_http_req_t req; + memset(&req, 0, sizeof req); + req.url = url; + req.method = "POST"; + req.body = body; + req.body_len = strlen(body); + req.extra_hdrs = hdrs; + req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; + req.timeout_ms = 15000; + + char respbuf[4096]; + ncm_http_resp_t resp; + rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); + if (rc != NCM_OK) return rc; + + /* 提取 set-cookie */ + char captured[1024] = ""; + ncm_http_parse_set_cookie(resp.headers, resp.headers_len, captured, sizeof captured); + if (captured[0]) r->cookie_was_set = 1; + if (out_cookie) merge_cookie(cur_cookie, captured, out_cookie, out_cookie_sz); + + ncm_json_t j; + jsmntok_t toks[512]; + int parent[512]; + rc = ncm_json_parse(&j, resp.body, resp.body_len, toks, 512, parent); + if (rc < 0) return NCM_ERR_JSON; + + long code = 0; + int crc = ncm_json_path_int(&j, "code", &code); + if (crc != NCM_OK) code = -1; + r->code = (int)code; + ncm_json_path_str(&j, "message", r->message, sizeof r->message); + + switch (r->code) { + case 800: r->state = NCM_LOGIN_EXPIRED; break; + case 801: r->state = NCM_LOGIN_PENDING; break; + case 802: r->state = NCM_LOGIN_SCANNED; break; + case 803: + case 200: r->state = NCM_LOGIN_OK; break; + default: r->state = NCM_LOGIN_ERROR; break; + } + return NCM_OK; +} + +int ncm_login_status_check(const char *device_id, const char *cookie, + char *uid, size_t usz, char *nickname, size_t nsz) { + char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX]; + int rc = ncm_req_build(device_id, cookie, "/api/w/nuser/account/get", "{}", NCM_CRYPT_WEAPI, + body, sizeof body, hdrs, sizeof hdrs, url, sizeof url); + if (rc != NCM_OK) return rc; + + ncm_http_req_t req; + memset(&req, 0, sizeof req); + req.url = url; + req.method = "POST"; + req.body = body; + req.body_len = strlen(body); + req.extra_hdrs = hdrs; + req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; + req.timeout_ms = 15000; + + char respbuf[4096]; + ncm_http_resp_t resp; + rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); + if (rc != NCM_OK) return rc; + + ncm_json_t j; + jsmntok_t toks[512]; + int parent[1024]; + rc = ncm_json_parse(&j, resp.body, resp.body_len, toks, 1024, parent); + if (rc < 0) return NCM_ERR_JSON; + + long code; + if (ncm_json_path_int(&j, "code", &code) != NCM_OK) return NCM_ERR_JSON; + if (code != 200) return NCM_ERR_AUTH; + /* 未登录(guest)时 code 可能仍为 200 但 profile 为 null —— 必须 profile 是对象才算已登录 */ + int ptok = ncm_json_path(&j, "profile"); + if (ptok < 0 || ncm_json_type(&j, ptok) != JSMN_OBJECT) return NCM_ERR_AUTH; + + if (uid) { + uid[0] = 0; + if (!ncm_json_path_str(&j, "account.id", uid, usz)) + ncm_json_path_str(&j, "profile.userId", uid, usz); + if (uid[0] == 0) + ncm_json_path_str(&j, "profile.uid", uid, usz); + } + if (nickname) + ncm_json_path_str(&j, "profile.nickname", nickname, nsz); + return NCM_OK; +} + +int ncm_login_save_cookie(const ncm_store_t *store, const char *cookie) { + return ncm_store_save_session(store, cookie); +} +int ncm_login_load_cookie(const ncm_store_t *store, char *cookie, size_t sz) { + return ncm_store_load_session(store, cookie, sz); +} +int ncm_login_clear_cookie(const ncm_store_t *store) { + return ncm_store_clear_session(store); +} diff --git a/patch_v2/ncm_app/core/ncm_login.h b/patch_v2/ncm_app/core/ncm_login.h new file mode 100644 index 0000000..5c64544 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_login.h @@ -0,0 +1,58 @@ +/* ncm_login.h — 扫码登录 + * + * 流程:unikey(eapi) → 本地构造二维码 → 轮询(801/802/803) → 捕获 set-cookie(MUSIC_U) + * → 加密落盘 → login_status(weapi) 校验。 + * 二维码位图用 qrcodegen 生成 1 bit/模块 矩阵,交给 UI 层渲染。 + */ +#ifndef NCM_LOGIN_H +#define NCM_LOGIN_H + +#include "ncm_store.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 登录状态(映射服务端 code) */ +enum { + NCM_LOGIN_PENDING = 0, /* 801 等待扫码 */ + NCM_LOGIN_SCANNED = 1, /* 802 已扫码待确认 */ + NCM_LOGIN_OK = 2, /* 803/200 成功 */ + NCM_LOGIN_EXPIRED = 3, /* 800 过期 */ + NCM_LOGIN_ERROR = 4, /* 其它异常 */ +}; + +/* 二维码 URL(本地构造) */ +int ncm_login_qr_url(const char *unikey, char *url, size_t usz); + +/* 拿 unikey(请求 /eapi/login/qrcode/unikey) */ +int ncm_login_unikey(const char *device_id, char *unikey, size_t usz); + +/* 二维码位图矩阵:mods 为 size*size 字节(1=暗)。max_size 为 mods 容量对应边长。 */ +int ncm_login_qr_matrix(const char *url, unsigned char *mods, int max_size, int *out_size); + +typedef struct ncm_login_poll { + int state; /* NCM_LOGIN_* */ + int code; /* 服务端 code(800/801/802/803/200) */ + int cookie_was_set; /* 本次响应是否带 set-cookie */ + char message[64]; /* 服务端 message */ +} ncm_login_poll_t; + +/* 轮询扫码状态。cur_cookie 为当前会话 cookie(可为空);成功时捕获 set-cookie 合并写入 out_cookie。 */ +int ncm_login_poll(const char *device_id, const char *unikey, const char *cur_cookie, + ncm_login_poll_t *r, char *out_cookie, size_t out_cookie_sz); + +/* login_status(weapi):校验 cookie 是否仍有效。有效写 uid/nickname 并返回 NCM_OK;失效返回 NCM_ERR_AUTH。 */ +int ncm_login_status_check(const char *device_id, const char *cookie, + char *uid, size_t usz, char *nickname, size_t nsz); + +/* 会话 cookie 加密落盘/读回(机器绑定密钥,仅防裸读)。 */ +int ncm_login_save_cookie(const ncm_store_t *store, const char *cookie); +int ncm_login_load_cookie(const ncm_store_t *store, char *cookie, size_t sz); +int ncm_login_clear_cookie(const ncm_store_t *store); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_LOGIN_H */ diff --git a/patch_v2/ncm_app/core/ncm_req.c b/patch_v2/ncm_app/core/ncm_req.c new file mode 100644 index 0000000..35efb70 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_req.c @@ -0,0 +1,298 @@ +/* ncm_req.c — 加密请求构建实现 */ +#include "ncm_req.h" +#include "ncm_common.h" +#include "ncm_crypto.h" + +#include +#include +#include +#include + +#define UA_EAPI "NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)" +#define UA_WEAPI "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0" +#define API_DOMAIN "https://interface.music.163.com" +#define DOMAIN "https://music.163.com" + +#define OSVER "Microsoft-Windows-10-Professional-build-19045-64bit" +#define APPVER "3.1.17.204416" +#define CHANNEL "netease" + +/* ---------- cookie map ---------- */ +typedef struct { char name[48]; char val[192]; } ck_t; +typedef struct { ck_t p[48]; int n; } ckmap_t; + +static void ck_add_ev(ckmap_t *m, const char *name, const char *val) { + if (m->n >= (int)(sizeof m->p / sizeof m->p[0])) return; + ncm_strlcpy(m->p[m->n].name, name, sizeof m->p[m->n].name); + ncm_strlcpy(m->p[m->n].val, val ? val : "", sizeof m->p[m->n].val); + m->n++; +} +static int ck_find(const ckmap_t *m, const char *name) { + for (int i = 0; i < m->n; i++) if (ncm_streq(m->p[i].name, name)) return i; + return -1; +} +static void ck_set(ckmap_t *m, const char *name, const char *val) { + int i = ck_find(m, name); + if (i >= 0) ncm_strlcpy(m->p[i].val, val ? val : "", sizeof m->p[i].val); + else ck_add_ev(m, name, val); +} +static void ck_set_default(ckmap_t *m, const char *name, const char *val) { + if (ck_find(m, name) < 0) ck_add_ev(m, name, val); +} +static void ck_to_string(const ckmap_t *m, char *out, size_t out_sz) { + size_t o = 0; + for (int i = 0; i < m->n; i++) { + int sep = (o > 0) ? 2 : 0; /* "; " */ + if (o + sep + strlen(m->p[i].name) + 1 + strlen(m->p[i].val) + 1 > out_sz) break; + if (sep) { out[o++] = ';'; out[o++] = ' '; } + size_t nl = strlen(m->p[i].name); + memcpy(out + o, m->p[i].name, nl); o += nl; + out[o++] = '='; + size_t vl = strlen(m->p[i].val); + memcpy(out + o, m->p[i].val, vl); o += vl; + } + out[o] = 0; +} + +/* 解析 cookie 字符串(; 分隔) 进 map */ +static void ck_parse(ckmap_t *m, const char *cookie) { + if (!cookie) return; + const char *p = cookie; + while (*p) { + while (*p == ' ' || *p == ';') p++; + if (!*p) break; + const char *eq = strchr(p, '='); + const char *semi = strchr(p, ';'); + size_t name_len = eq ? (size_t)(eq - p) : (semi ? (size_t)(semi - p) : strlen(p)); + if (eq) { + size_t val_len = semi ? (size_t)(semi - eq - 1) : strlen(eq + 1); + char name[48], val[192]; + if (name_len >= sizeof name) name_len = sizeof name - 1; + memcpy(name, p, name_len); name[name_len] = 0; + if (val_len >= sizeof val) val_len = sizeof val - 1; + memcpy(val, eq + 1, val_len); val[val_len] = 0; + ck_set(m, name, val); + } + p = semi ? semi + 1 : p + strlen(p); + } +} + +int ncm_req_cookie_get(const char *cookie, const char *name, char *out, size_t out_sz) { + if (!cookie || !name || !out) return NCM_ERR_ARG; + ckmap_t m; memset(&m, 0, sizeof m); + ck_parse(&m, cookie); + int i = ck_find(&m, name); + if (i < 0) return NCM_ERR_NOTFOUND; + ncm_strlcpy(out, m.p[i].val, out_sz); + return NCM_OK; +} + +/* ---------- json 注入 ---------- */ +/* 把 json{...} 内部插入 ","(尾部)。extra 形如 "\"csrf_token\":\"x\""(无前导逗号) */ +static int json_inject(const char *json, const char *extra, char *out, size_t out_sz) { + size_t jl = strlen(json); + if (jl < 2 || json[0] != '{' || json[jl - 1] != '}') return -1; + const char *inner = json + 1; + size_t inner_len = jl - 2; + int first = 1; + /* 检查内部是否为空 */ + for (size_t i = 0; i < inner_len; i++) { + char c = inner[i]; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { first = 0; break; } + } + const char *sep = first ? "" : ","; + int n = snprintf(out, out_sz, "{%s%s%s}", inner, sep, extra); + return (n >= 0 && (size_t)n < out_sz) ? 0 : -1; +} + +/* ---------- eapi header map ---------- */ +typedef struct hmap { + char key[24]; + char val[256]; +} hmap_t; +typedef struct { hmap_t e[16]; int n; } hmap_tab; + +static void hm_add(hmap_tab *h, const char *k, const char *v) { + if (h->n >= (int)(sizeof h->e / sizeof h->e[0])) return; + ncm_strlcpy(h->e[h->n].key, k, sizeof h->e[h->n].key); + ncm_strlcpy(h->e[h->n].val, v, sizeof h->e[h->n].val); + h->n++; +} +static void hm_to_json(const hmap_tab *h, char *out, size_t out_sz) { + size_t o = 0; + o += (size_t)snprintf(out + o, out_sz - o, "{"); + for (int i = 0; i < h->n; i++) { + int n = snprintf(out + o, out_sz - o, "%s\"%s\":\"%s\"", i ? "," : "", + h->e[i].key, h->e[i].val); + if (n < 0 || (size_t)n >= out_sz - o) break; + o += (size_t)n; + } + snprintf(out + o, out_sz - o, "}"); +} +static void hm_to_cookie(const hmap_tab *h, char *out, size_t out_sz) { + size_t o = 0; + for (int i = 0; i < h->n; i++) { + char ev[512], ec[512]; + if (ncm_url_encode(h->e[i].val, ev, sizeof ev) < 0 || + ncm_url_encode(h->e[i].key, ec, sizeof ec) < 0) break; + int n = snprintf(out + o, out_sz - o, "%s%s=%s", i ? "; " : "", ec, ev); + if (n < 0 || (size_t)n >= out_sz - o) break; + o += (size_t)n; + } + out[o < out_sz ? o : out_sz - 1] = 0; +} + +/* head 注入(eapi):把 __csrf/deviceId/os/... 等填到 header map 与 cookie。 + * 同时产出 json.header 片段。 */ +static void eapi_fill_header(const char *device_id, const char *csrf, + hmap_tab *h, char *buildver, size_t bvsz, + char *request_id, size_t ri_sz) { + (void)bvsz; + char ts[32], ms[48]; + snprintf(ts, sizeof ts, "%lld", (long long)time(NULL)); + snprintf(ms, sizeof ms, "%lld", (long long)(ncm_now_ms())); + memcpy(buildver, ts, 10); buildver[10] = 0; + snprintf(request_id, ri_sz, "%s_0000", ms); + + hm_add(h, "osver", OSVER); + hm_add(h, "deviceId", device_id); + hm_add(h, "os", "pc"); + hm_add(h, "appver", APPVER); + hm_add(h, "versioncode", "140"); + hm_add(h, "mobilename", ""); + hm_add(h, "buildver", buildver); + hm_add(h, "resolution", "1920x1080"); + hm_add(h, "__csrf", csrf ? csrf : ""); + hm_add(h, "channel", CHANNEL); + hm_add(h, "requestId", request_id); +} + +int ncm_req_build(const char *device_id, const char *cookie, const char *path, + const char *json, int kind, + char *body, size_t body_sz, + char *hdrs, size_t hdrs_sz, + char *url, size_t url_sz) { + if (!device_id || !path || !json || !body || !hdrs || !url) return NCM_ERR_ARG; + + if (kind == NCM_CRYPT_WEAPI) { + /* csrf_token */ + char csrf[128] = ""; + ncm_req_cookie_get(cookie, "__csrf", csrf, sizeof csrf); + char full[2048], extra[256]; + snprintf(extra, sizeof extra, "\"csrf_token\":\"%s\"", csrf); + if (json_inject(json, extra, full, sizeof full) != 0) return NCM_ERR_ARG; + + char params[1024], encsk[300]; + if (ncm_crypto_weapi(full, params, sizeof params, encsk, sizeof encsk) != 0) + return NCM_ERR_CRYPTO; + char p_enc[1024]; + if (ncm_url_encode(params, p_enc, sizeof p_enc) < 0) return NCM_ERR_FULL; + char referer[256]; + snprintf(referer, sizeof referer, "https://music.163.com"); + /* 会话 cookie + 默认伪装字段补齐 */ + ckmap_t m; memset(&m, 0, sizeof m); + ck_parse(&m, cookie); + ck_add_ev(&m, "__remember_me", "true"); + ck_add_ev(&m, "ntes_kaola_ad", "1"); + char nuid[96]; ncm_random_bytes((unsigned char *)nuid, 16); + /* _ntes_nnid = nuid,ts ; _ntes_nuid = nuid */ + { + char nuid_hex[33]; ncm_hex_encode((const unsigned char *)nuid, 16, nuid_hex, 0); + char nnid[96]; + snprintf(nnid, sizeof nnid, "%s,%lld", nuid_hex, (long long)ncm_now_ms()); + ck_add_ev(&m, "_ntes_nnid", nnid); + ck_add_ev(&m, "_ntes_nuid", nuid_hex); + } + char wnmcid[40]; { + unsigned char r[16]; ncm_random_bytes(r, 16); + char h[33]; ncm_hex_encode(r, 16, h, 0); snprintf(wnmcid, sizeof wnmcid, "%s", h); + } + ck_set_default(&m, "WNMCID", wnmcid); + ck_set_default(&m, "WEVNSM", "1.0.0"); + ck_set_default(&m, "osver", OSVER); + ck_set_default(&m, "deviceId", device_id); + ck_set_default(&m, "os", "pc"); + ck_set_default(&m, "channel", CHANNEL); + ck_set_default(&m, "appver", APPVER); + if (!strstr(path, "login")) { + unsigned char r[4]; ncm_random_bytes(r, 4); + char h[9]; ncm_hex_encode(r, 4, h, 1); + ck_set_default(&m, "NMTID", h); + } + char cookie_full[2048]; + ck_to_string(&m, cookie_full, sizeof cookie_full); + + int n = snprintf(hdrs, hdrs_sz, + "User-Agent: %s\r\nContent-Type: application/x-www-form-urlencoded\r\n" + "Cookie: %s\r\nReferer: %s", UA_WEAPI, cookie_full, referer); + if (n < 0 || (size_t)n >= hdrs_sz) return NCM_ERR_FULL; + + /* body */ + n = snprintf(body, body_sz, "params=%s&encSecKey=%s", p_enc, encsk); + if (n < 0 || (size_t)n >= body_sz) return NCM_ERR_FULL; + + /* url */ + const char *suffix = path + 5; /* 去 "/api/" */ + n = snprintf(url, url_sz, "%s/weapi/%s", DOMAIN, suffix); + if (n < 0 || (size_t)n >= url_sz) return NCM_ERR_FULL; + return NCM_OK; + } + + if (kind == NCM_CRYPT_EAPI) { + char csrf[128] = ""; + ncm_req_cookie_get(cookie, "__csrf", csrf, sizeof csrf); + hmap_tab h; memset(&h, 0, sizeof h); + char buildver[16], rid[64]; + eapi_fill_header(device_id, csrf, &h, buildver, sizeof buildver, rid, sizeof rid); + char header_json[1024]; + hm_to_json(&h, header_json, sizeof header_json); + + /* 注入 header 与 e_r 到加密 JSON */ + char full2[3072]; + { + size_t jl = strlen(json); + if (jl >= 2 && json[0]=='{' && json[jl-1]=='}') { + int fn = snprintf(full2, sizeof full2, "{%s,\"header\":%s,\"e_r\":false}", + json + 1, header_json); + if (fn < 0 || (size_t)fn >= sizeof full2) return NCM_ERR_FULL; + } else { + return NCM_ERR_ARG; + } + } + + char params[2048]; + if (ncm_crypto_eapi(path, full2, params, sizeof params) != 0) return NCM_ERR_CRYPTO; + + char cookie_hdr[2048]; + hm_to_cookie(&h, cookie_hdr, sizeof cookie_hdr); + + int n = snprintf(hdrs, hdrs_sz, + "User-Agent: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nCookie: %s", + UA_EAPI, cookie_hdr); + if (n < 0 || (size_t)n >= hdrs_sz) return NCM_ERR_FULL; + + /* 此处注意:我们已把 header 插入到加密 JSON 内,同时 Cookie 头也带同一组键值(对齐 request.rs) */ + n = snprintf(body, body_sz, "params=%s", params); + if (n < 0 || (size_t)n >= body_sz) return NCM_ERR_FULL; + + const char *suffix = path + 5; + n = snprintf(url, url_sz, "%s/eapi/%s", API_DOMAIN, suffix); + if (n < 0 || (size_t)n >= url_sz) return NCM_ERR_FULL; + return NCM_OK; + } + + if (kind == NCM_CRYPT_LINUX) { + char params[1024]; + if (ncm_crypto_linuxapi(json, params, sizeof params) != 0) return NCM_ERR_CRYPTO; + int n = snprintf(hdrs, hdrs_sz, + "User-Agent: %s\r\nContent-Type: application/x-www-form-urlencoded", UA_WEAPI); + if (n < 0 || (size_t)n >= hdrs_sz) return NCM_ERR_FULL; + n = snprintf(body, body_sz, "eparams=%s", params); + if (n < 0 || (size_t)n >= body_sz) return NCM_ERR_FULL; + n = snprintf(url, url_sz, "%s/api/linux/forward", API_DOMAIN); + if (n < 0 || (size_t)n >= url_sz) return NCM_ERR_FULL; + return NCM_OK; + } + + return NCM_ERR_ARG; +} diff --git a/patch_v2/ncm_app/core/ncm_req.h b/patch_v2/ncm_app/core/ncm_req.h new file mode 100644 index 0000000..b990784 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_req.h @@ -0,0 +1,50 @@ +/* ncm_req.h — 加密请求构建 + * + * 统一把「核心参数 JSON」+ deviceId/cookie 组装成一条可直接 POST 的请求: + * - eapi: 注入 header/e_r,MD5+AES-ECB 得 params;Cookie 头=urlencode(header map) + * - weapi: 注入 csrf_token,双层 AES-CBC+RSA raw 得 params/encSecKey;Referer+Chrome UA + * 头注入对齐 CNMPlayer request.rs(deviceId/UA 稳定是风控关键)。 + */ +#ifndef NCM_REQ_H +#define NCM_REQ_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 加密类型 */ +enum { + NCM_CRYPT_EAPI = 0, + NCM_CRYPT_WEAPI = 1, + NCM_CRYPT_LINUX = 2, + NCM_CRYPT_API = 3 /* 明文 not supported here */ +}; + +#define NCM_REQ_URL_MAX 2048 +#define NCM_REQ_BODY_MAX 8192 +#define NCM_REQ_HDRS_MAX 4096 + +/* 构建一条 POST 请求。参数: + * device_id 52hex deviceId(已持久化) + * cookie 会话 cookie(可为空;weapi 会补齐默认伪装字段;eapi 只用其 __csrf) + * path API 路径(以 "/api/" 开头,如 /api/login/qrcode/unikey) + * json 核心参数 JSON(扁平对象,如 {"type":3}) + * kind NCM_CRYPT_* + * 输出:body(表单体)、hdrs(extra 头,仅 UA/Content-Type/Cookie/Referer),url(完整 URL) + * 返回 NCM_OK 或负错误码。 + */ +int ncm_req_build(const char *device_id, const char *cookie, const char *path, + const char *json, int kind, + char *body, size_t body_sz, + char *hdrs, size_t hdrs_sz, + char *url, size_t url_sz); + +/* 从 cookie 串取某个字段值(如 __csrf)。找到返回 0。*/ +int ncm_req_cookie_get(const char *cookie, const char *name, char *out, size_t out_sz); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_REQ_H */ diff --git a/patch_v2/ncm_app/core/ncm_store.c b/patch_v2/ncm_app/core/ncm_store.c new file mode 100644 index 0000000..2dd9dc1 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_store.c @@ -0,0 +1,329 @@ +/* ncm_store.c — 持久化存储层实现 */ +#include "ncm_store.h" +#include "ncm_common.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mbedtls/aes.h" + +#define SESSION_FILE "session.enc" +#define SESSION_MAGIC "NCM1" +#define SESSION_MAGIC_LEN 4 +#define SESSION_SALT "os.meow.ncm.session.v1" +#define DEVICE_FILE "deviceId" +#define MKEY_FILE ".mkey" + +/* 数据目录快照(ncm_store_init 设置)。用于无 MAC 时派生机器绑定密钥。单实例约定。 */ +static char g_store_dir[NCM_STORE_DIR_MAX] = ""; + +int ncm_store_mkdir_p(const char *path) { + if (!path || !*path) return -1; + char tmp[NCM_STORE_DIR_MAX]; + size_t len = strlen(path); + if (len >= sizeof tmp) return -1; + memcpy(tmp, path, len + 1); + if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; + for (size_t i = 1; i <= strlen(tmp); i++) { + if (tmp[i] == '/' || tmp[i] == '\0') { + char ch = tmp[i]; + tmp[i] = '\0'; + if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1; + tmp[i] = ch; + } + } + return 0; +} + +int ncm_store_init(ncm_store_t *st, const char *dir) { + if (!st) return NCM_ERR_ARG; + memset(st, 0, sizeof *st); + const char *d = dir; + if (!d || !*d) { + d = getenv("MEOW_DATA_DIR"); + if (!d || !*d) { + snprintf(st->data_dir, sizeof st->data_dir, "/tmp/meowcs_%d", (int)getpid()); + d = st->data_dir; /* 回退后指向自身 */ + } + } + if (strlen(d) >= sizeof st->data_dir) return NCM_ERR_ARG; + ncm_strlcpy(st->data_dir, d, sizeof st->data_dir); + /* 去尾斜杠 */ + size_t n = strlen(st->data_dir); + while (n > 1 && st->data_dir[n - 1] == '/') st->data_dir[--n] = '\0'; + ncm_store_mkdir_p(st->data_dir); + ncm_strlcpy(g_store_dir, st->data_dir, sizeof g_store_dir); + return NCM_OK; +} + +const char *ncm_store_data_dir(const ncm_store_t *st) { + return st->data_dir; +} + +int ncm_store_get_device_id(ncm_store_t *st, char out[NCM_STORE_ID_MAX]) { + if (!st || !out) return NCM_ERR_ARG; + char path[NCM_STORE_DIR_MAX + 16]; + snprintf(path, sizeof path, "%s/%s", st->data_dir, DEVICE_FILE); + char buf[NCM_STORE_ID_MAX]; + int n = ncm_store_read_file(path, buf, sizeof buf); + if (n >= NCM_DEVICE_ID_LEN) { + /* 找到已持久化的 52hex(容错小写→大写) */ + char up[NCM_STORE_ID_MAX]; + for (int i = 0; i < NCM_DEVICE_ID_LEN; i++) up[i] = (char)toupper((unsigned char)buf[i]); + up[NCM_DEVICE_ID_LEN] = 0; + ncm_strlcpy(out, up, NCM_STORE_ID_MAX); + return NCM_OK; + } + /* 生成并持久化 */ + unsigned char r[26]; + ncm_random_bytes(r, sizeof r); + char id[NCM_STORE_ID_MAX]; + ncm_hex_encode(r, sizeof r, id, 1); + ncm_strlcpy(out, id, NCM_STORE_ID_MAX); + ncm_store_write_file(path, id, strlen(id)); + return NCM_OK; +} + +int ncm_store_set_device_id(ncm_store_t *st, const char id[NCM_STORE_ID_MAX]) { + if (!st || !id || strlen(id) != NCM_DEVICE_ID_LEN) return NCM_ERR_ARG; + char path[NCM_STORE_DIR_MAX + 16]; + snprintf(path, sizeof path, "%s/%s", st->data_dir, DEVICE_FILE); + return ncm_store_write_file(path, id, strlen(id)); +} + +/* 首要网卡 MAC 小写 "aa:bb:cc:dd:ee:ff";NCM_MAC 优先;失败返回非 0。 */ +static int read_mac(char out[32]) { + out[0] = 0; + const char *env = getenv("NCM_MAC"); + if (env && strlen(env) == 17) { + ncm_strlcpy(out, env, 18); + for (char *p = out; *p; p++) *p = (char)tolower((unsigned char)*p); + return 0; + } + DIR *d = opendir("/sys/class/net"); + if (!d) return -1; + int rc = -1; + struct dirent *e; + while ((e = readdir(d))) { + if (e->d_name[0] == '.') continue; + if (strcmp(e->d_name, "lo") == 0) continue; + char p[320], addr[64] = {0}; + snprintf(p, sizeof p, "/sys/class/net/%s/address", e->d_name); + FILE *f = fopen(p, "r"); + if (!f) continue; + if (fgets(addr, sizeof addr, f)) { + fclose(f); + size_t l = strlen(addr); + while (l && (addr[l-1]=='\n' || addr[l-1]=='\r')) addr[--l] = 0; + if (l == 17 && strcmp(addr, "00:00:00:00:00:00") != 0) { + for (char *q = addr; *q; q++) *q = (char)tolower((unsigned char)*q); + ncm_strlcpy(out, addr, 18); + rc = 0; + break; + } + } + } + closedir(d); + return rc; +} + +/* 读取或生成设备私有密钥(32字节):/.mkey。成功返回 0。 */ +static int get_or_create_mkey(unsigned char out[32]) { + char path[NCM_STORE_DIR_MAX + 16]; + snprintf(path, sizeof path, "%s/%s", g_store_dir, MKEY_FILE); + char raw[80]; + int n = ncm_store_read_file(path, raw, sizeof raw); + if (n >= 32) { + memcpy(out, raw, 32); + return 0; + } + ncm_random_bytes(out, 32); + return ncm_store_write_file(path, out, 32); +} + +int ncm_store_machine_key(const char *salt, unsigned char key[16]) { + char mac[32]; + unsigned char h[32]; + if (read_mac(mac) == 0) { + char buf[64]; + snprintf(buf, sizeof buf, "%s%s", mac, salt ? salt : ""); + if (ncm_sha256(buf, strlen(buf), h) != 0) return -1; + } else { + /* 无 MAC(如 qemu/无网卡环境):回退到设备私有 .mkey,绑定本机数据目录。 + * 仍是模糊防裸读,绝非真安全(注释见头文件)。 */ + unsigned char mkey[32]; + if (get_or_create_mkey(mkey) != 0) return -1; + char mkey_hex[65]; + ncm_hex_encode(mkey, 32, mkey_hex, 0); + char buf[96]; + snprintf(buf, sizeof buf, "%s%s", mkey_hex, salt ? salt : ""); + if (ncm_sha256(buf, strlen(buf), h) != 0) return -1; + } + memcpy(key, h, 16); + return 0; +} + +static int pkcs7_pad_len(size_t n) { + return (int)(16 - (n % 16)); /* 1..16 */ +} + +int ncm_store_cookie_encrypt(const char *plain, + unsigned char *out, size_t *out_len, size_t cap) { + if (!plain || !out || !out_len) return NCM_ERR_ARG; + unsigned char key[16]; + if (ncm_store_machine_key(SESSION_SALT, key) != 0) return NCM_ERR_CRYPTO; + + size_t n = strlen(plain); + int pad = pkcs7_pad_len(n); + size_t total = n + (size_t)pad; + if (cap < SESSION_MAGIC_LEN + 16 + total) return NCM_ERR_FULL; + + unsigned char iv[16], iv_file[16]; + ncm_random_bytes(iv, sizeof iv); + memcpy(iv_file, iv, 16); /* crypt_cbc 会就地改写 iv,必须先存原始值 */ + + unsigned char *ct = (unsigned char *)malloc(total); + if (!ct) return NCM_ERR_OOM; + + /* AES-CBC 加密 */ + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_enc(&aes, key, 128); + unsigned char *in = (unsigned char *)malloc(total); + if (!in) { free(ct); return NCM_ERR_OOM; } + memcpy(in, plain, n); + for (int i = 0; i < pad; i++) in[n + i] = (unsigned char)pad; + mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, total, iv, in, ct); + mbedtls_aes_free(&aes); + free(in); + + memcpy(out, SESSION_MAGIC, SESSION_MAGIC_LEN); + memcpy(out + SESSION_MAGIC_LEN, iv_file, 16); + memcpy(out + SESSION_MAGIC_LEN + 16, ct, total); + *out_len = SESSION_MAGIC_LEN + 16 + total; + free(ct); + return NCM_OK; +} + +int ncm_store_cookie_decrypt(const unsigned char *in, size_t in_len, + char *out, size_t out_sz) { + if (!in || !out || !out_sz) return NCM_ERR_ARG; + if (in_len < SESSION_MAGIC_LEN + 16 || memcmp(in, SESSION_MAGIC, SESSION_MAGIC_LEN) != 0) + return NCM_ERR_CRYPTO; + unsigned char key[16]; + if (ncm_store_machine_key(SESSION_SALT, key) != 0) return NCM_ERR_CRYPTO; + const unsigned char *iv = in + SESSION_MAGIC_LEN; + size_t clen = in_len - SESSION_MAGIC_LEN - 16; + if (clen == 0 || clen % 16) return NCM_ERR_CRYPTO; + + unsigned char *pt = (unsigned char *)malloc(clen); + if (!pt) return NCM_ERR_OOM; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_dec(&aes, key, 128); + unsigned char iv2[16]; + memcpy(iv2, iv, 16); + mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, clen, iv2, in + SESSION_MAGIC_LEN + 16, pt); + mbedtls_aes_free(&aes); + + /* 去 PKCS7 */ + unsigned char pad = pt[clen - 1]; + if (pad < 1 || pad > 16 || (size_t)pad > clen) { free(pt); return NCM_ERR_CRYPTO; } + for (size_t i = 0; i < pad; i++) + if (pt[clen - 1 - i] != pad) { free(pt); return NCM_ERR_CRYPTO; } + size_t plen = clen - pad; + if (plen >= out_sz) { free(pt); return NCM_ERR_FULL; } + memcpy(out, pt, plen); + out[plen] = 0; + free(pt); + return NCM_OK; +} + +int ncm_store_save_session(const ncm_store_t *st, const char *cookie) { + if (!st || !cookie) return NCM_ERR_ARG; + unsigned char enc[8192]; + size_t elen = 0; + int rc = ncm_store_cookie_encrypt(cookie, enc, &elen, sizeof enc); + if (rc != NCM_OK) return rc; + char path[NCM_STORE_DIR_MAX + 16]; + snprintf(path, sizeof path, "%s/%s", st->data_dir, SESSION_FILE); + return ncm_store_write_file(path, enc, elen); +} + +int ncm_store_load_session(const ncm_store_t *st, char *cookie, size_t sz) { + if (!st || !cookie) return NCM_ERR_ARG; + char path[NCM_STORE_DIR_MAX + 16]; + snprintf(path, sizeof path, "%s/%s", st->data_dir, SESSION_FILE); + char raw[8192]; + int n = ncm_store_read_file(path, raw, sizeof raw); + if (n <= 0) return NCM_ERR_NOENT; + return ncm_store_cookie_decrypt((const unsigned char *)raw, (size_t)n, cookie, sz); +} + +int ncm_store_clear_session(const ncm_store_t *st) { + if (!st) return NCM_ERR_ARG; + char path[NCM_STORE_DIR_MAX + 16]; + snprintf(path, sizeof path, "%s/%s", st->data_dir, SESSION_FILE); + unlink(path); + return NCM_OK; +} + +int ncm_store_write_file(const char *path, const void *data, size_t len) { + if (!path || !data) return NCM_ERR_ARG; + char tmp[NCM_STORE_DIR_MAX + 32]; + snprintf(tmp, sizeof tmp, "%s.tmp", path); + int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) return NCM_ERR_IO; + size_t off = 0; + while (off < len) { + ssize_t w = write(fd, (const char *)data + off, len - off); + if (w <= 0) { close(fd); unlink(tmp); return NCM_ERR_IO; } + off += (size_t)w; + } + if (fsync(fd) != 0) { close(fd); unlink(tmp); return NCM_ERR_IO; } + close(fd); + if (rename(tmp, path) != 0) { unlink(tmp); return NCM_ERR_IO; } + return NCM_OK; +} + +int ncm_store_read_file(const char *path, char *buf, size_t sz) { + if (!path || !buf || sz == 0) return NCM_ERR_ARG; + int fd = open(path, O_RDONLY); + if (fd < 0) return NCM_ERR_NOENT; + ssize_t n = read(fd, buf, sz - 1); + close(fd); + if (n < 0) return NCM_ERR_IO; + buf[n] = 0; + return (int)n; +} + +int ncm_store_read_cfg_int(const char *dir, const char *key, int def) { + if (!dir || !key) return def; + /* key 清洗(对齐 SDK:允许 [A-Za-z0-9._-],其余换 '_') */ + char k[128], out[32]; + size_t len = 0; + for (const unsigned char *p = (const unsigned char *)key; *p && len < sizeof k - 1; p++) { + unsigned char c = *p; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-') + k[len++] = (char)c; + else + k[len++] = '_'; + } + if (len == 0) return def; + k[len] = 0; + char path[NCM_STORE_DIR_MAX + 160]; + snprintf(path, sizeof path, "%s/%s.cfg", dir, k); + int n = ncm_store_read_file(path, out, sizeof out); + if (n <= 0) return def; + long v; + if (ncm_parse_int(out, &v) != 0) return def; + return (int)v; +} diff --git a/patch_v2/ncm_app/core/ncm_store.h b/patch_v2/ncm_app/core/ncm_store.h new file mode 100644 index 0000000..b99de93 --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_store.h @@ -0,0 +1,62 @@ +/* ncm_store.h — 持久化存储层 + * + * 职责(纯 C,不依赖 UI/SDK): + * - 应用数据目录推断(env MEOW_DATA_DIR 优先,否则 /tmp 回退) + * - deviceId(52hex) 首生成 + 持久化(风控要求稳定) + * - 机器绑定密钥派生:sha256(MAC + 盐) → AES-128-CBC key(防裸读,非真安全) + * - 登录 cookie 加密落盘/读回(AES-128-CBC + 随机 IV + 魔数头) + * - 原子写文件、读取、配置键读取(/.cfg,对齐 SDK meow_config) + */ +#ifndef NCM_STORE_H +#define NCM_STORE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define NCM_STORE_DIR_MAX 512 +#define NCM_STORE_ID_MAX 65 /* 52 hex + NUL */ + +typedef struct ncm_store { + char data_dir[NCM_STORE_DIR_MAX]; /* 无尾斜杠 */ +} ncm_store_t; + +/* 初始化数据目录。dir 为 NULL 时用 env MEOW_DATA_DIR(无则 /tmp/meowcs_)。 + * 返回 0=成功。 */ +int ncm_store_init(ncm_store_t *st, const char *dir); +const char *ncm_store_data_dir(const ncm_store_t *st); + +/* deviceId:读持久化;缺失则生成 26 随机字节 → 52hex 大写并落盘。 */ +int ncm_store_get_device_id(ncm_store_t *st, char out[NCM_STORE_ID_MAX]); +int ncm_store_set_device_id(ncm_store_t *st, const char id[NCM_STORE_ID_MAX]); + +/* 机器绑定密钥:sha256(mac + salt) 取前 16 字节作为 AES-128 key。 + * mac 取首选网卡 MAC 小写;NCM_MAC 环境变量可覆盖(测试用)。失败返回非 0。 */ +int ncm_store_machine_key(const char *salt, unsigned char key[16]); + +/* cookie 加解密(AES-128-CBC,PKCS7)。注意:这是防裸读模糊,非真安全。 */ +int ncm_store_cookie_encrypt(const char *plain, + unsigned char *out, size_t *out_len, size_t cap); +int ncm_store_cookie_decrypt(const unsigned char *in, size_t in_len, + char *out, size_t out_sz); + +/* session 加密落盘/读回:/session.enc */ +int ncm_store_save_session(const ncm_store_t *st, const char *cookie); +int ncm_store_load_session(const ncm_store_t *st, char *cookie, size_t sz); +int ncm_store_clear_session(const ncm_store_t *st); + +/* 通用原子写 / 读 / 建目录 */ +int ncm_store_write_file(const char *path, const void *data, size_t len); +int ncm_store_read_file(const char *path, char *buf, size_t sz); +int ncm_store_mkdir_p(const char *path); + +/* 读配置整型键:/.cfg(与 SDK meow_config_int 同约定)。未设/非法返回 def。 */ +int ncm_store_read_cfg_int(const char *dir, const char *key, int def); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_STORE_H */ diff --git a/patch_v2/ncm_app/test/test_ncm_core.c b/patch_v2/ncm_app/test/test_ncm_core.c new file mode 100644 index 0000000..b7c46a3 --- /dev/null +++ b/patch_v2/ncm_app/test/test_ncm_core.c @@ -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 +#include +#include + +#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; +} diff --git a/patch_v2/ncm_app/test/test_ncm_core_host b/patch_v2/ncm_app/test/test_ncm_core_host new file mode 100755 index 0000000..ec123ba Binary files /dev/null and b/patch_v2/ncm_app/test/test_ncm_core_host differ