feat(ncm): 网易云核心库 core/ (agent-203) — ncm_http(mbedtls TLS1.2+chunked+流式限幅)/ncm_crypto(eapi/weapi/linuxapi, RSA raw逐字节验证)/ncm_login(unikey→二维码→轮询状态机→cookie加密落盘)/ncm_api(歌单/详情/播放地址Hi-res逐档降级/歌词)/ncm_cache(LRU 默认10G)/ncm_store(deviceId持久化+机器密钥)

宿主+qemu-mipsel 全链路 PASS(unikey实拿/801轮询/eapi往返/RSA对拍/缓存LRU); -Wall -Wextra 自有代码0warning
待联调: 803真扫码、真机 hw:0,0 播放链路
This commit is contained in:
MeowOS committed 2026-08-31 06:41:23 +08:00
1 parent 85b4aea24f
commit 8012948b26
25 files changed
+3416

No files matched your search

+94
View File
@@ -122,3 +122,97 @@ CA bundle 内置方案、音质档位映射实测。这些属设备/账号侧联
- **推荐 TLS/加密一体用 mbedtls**(单库一次解决 TLS + AES/MD5/RSA raw/Base64 + X509,规避 RNSS-raw 手写风险);BearSSL 为省 flash 备选。 - **推荐 TLS/加密一体用 mbedtls**(单库一次解决 TLS + AES/MD5/RSA raw/Base64 + X509,规避 RNSS-raw 手写风险);BearSSL 为省 flash 备选。
- **v1 体积增量 ≈ 438 KB(库)/ 只做 mp3**,内存估计数百 KB 工作集,14MB 可行;主要优化点是 mbedtls 裁剪与封面限幅。 - **v1 体积增量 ≈ 438 KB(库)/ 只做 mp3**,内存估计数百 KB 工作集,14MB 可行;主要优化点是 mbedtls 裁剪与封面限幅。
- **最关键风险(RSA raw)已验证**;其余风险集中在风控(deviceId/UA 稳定持久化)与无版权降级策略。 - **最关键风险(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/<pkg>/data/session.enc`,密钥 = `sha256(MAC + 盐)`;无 MAC(qemu/无网卡)回退到设备私有 `.mkey` 派生。**仅为防裸读模糊,非真安全**(头文件已注明)。
- **缓存**:目录默认 `MeowData/os.meow.ncm/cache/`;上限默认 10GB(1~100 可配),读设置键 `ncm.cache_limit_gb`(`<data_dir>/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":"<uuid>"}`(宿主 `dc5bd090-…`,qemu-mipsel `ef9a5db2-…`);二维码 URL `https://music.163.com/login?codekey=<uuid>`。
- **轮询(未扫码)**:`{"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,未在本次接线)。
+62
View File
@@ -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"
+4
View File
@@ -0,0 +1,4 @@
/* jsmn_impl.c — jsmn 唯一实现 TU。
* jsmn.h 默认把实现(extern 函数)写进每个包含它的 TU,
* 因此全工程只允许这一个 TU 不带 JSMN_HEADER 编译,其余 TU 用 -DJSMN_HEADER 取声明。 */
#include "jsmn.h"
+46
View File
@@ -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;
/* 默认缓存目录(<data_dir>/../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 */
+265
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
+82
View File
@@ -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 <stddef.h>
#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 */
+202
View File
@@ -0,0 +1,202 @@
/* ncm_cache.c — 缓存实现 */
#include "ncm_cache.h"
#include "ncm_common.h"
#include "ncm_store.h"
#include <dirent.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <utime.h>
#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;
}
+62
View File
@@ -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 设置,读 <data_dir>/<key>.cfg)。
* LRU:以文件 mtime 作为最后访问时间(lookup/commit 时刷新);超限时按 mtime 升序删最旧。
* 命中缓存即可离线播放(ncm_cache_lookup 返回已缓存文件路径)。
*/
#ifndef NCM_CACHE_H
#define NCM_CACHE_H
#include <stddef.h>
#include <stdint.h>
#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 */
+193
View File
@@ -0,0 +1,193 @@
/* ncm_common.c — 公共工具层实现 */
#include "ncm_common.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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;
}
+76
View File
@@ -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 <stddef.h>
#include <stdint.h>
#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 */
+140
View File
@@ -0,0 +1,140 @@
/* ncm_crypto.c — 加密层实现 */
#include "ncm_crypto.h"
#include "ncm_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
+49
View File
@@ -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 <stddef.h>
#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 */
+57
View File
@@ -0,0 +1,57 @@
/* ncm_ctx.c — 上下文实现 */
#include "ncm.h"
#include <stdio.h>
#include <string.h>
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);
}
+527
View File
@@ -0,0 +1,527 @@
/* ncm_http.c — mbedtls HTTPS 客户端封装实现 */
#include "ncm_http.h"
#include "ncm_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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;
}
+74
View File
@@ -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 <stddef.h>
#include <stdint.h>
#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 */
+189
View File
@@ -0,0 +1,189 @@
/* ncm_json.c — jsmn 封装实现 */
#include "ncm_json.h"
#include "ncm_common.h"
#include <stdlib.h>
#include <string.h>
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;
}
+58
View File
@@ -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 <stddef.h>
#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 */
+197
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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);
}
+58
View File
@@ -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 <stddef.h>
#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 */
+298
View File
@@ -0,0 +1,298 @@
/* ncm_req.c — 加密请求构建实现 */
#include "ncm_req.h"
#include "ncm_common.h"
#include "ncm_crypto.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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>"(尾部)。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;
}
+50
View File
@@ -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 <stddef.h>
#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 */
+329
View File
@@ -0,0 +1,329 @@
/* ncm_store.c — 持久化存储层实现 */
#include "ncm_store.h"
#include "ncm_common.h"
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#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字节):<data_dir>/.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;
}
+62
View File
@@ -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 + 魔数头)
* - 原子写文件、读取、配置键读取(<dir>/<key>.cfg,对齐 SDK meow_config)
*/
#ifndef NCM_STORE_H
#define NCM_STORE_H
#include <stddef.h>
#include <stdint.h>
#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_<pid>)。
* 返回 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 加密落盘/读回:<data_dir>/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);
/* 读配置整型键:<dir>/<key>.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 */
+242
View File
@@ -0,0 +1,242 @@
/* test_ncm_core.c — 网易云核心库宿主实测
*
* 覆盖:加密(eapi 往返/weapi 形状/linuxapi)、AES/hex/base64、
* 缓存 LRU、设备键/cookie 加解密、二维码矩阵、以及
* 网络接口(unikey 实拿 / 轮询 801 / 无凭据错误处理)。
* 网络不可用时网络项按 SKIP 处理,不判失败;内部逻辑错才 FAIL。
* 用法:test_ncm_core [data_dir](缺省 /tmp/ncm_test)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ncm.h"
#include "mbedtls/aes.h"
static int g_fail = 0;
#define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0)
#define CHECKRC(expr, fmt, ...) do { \
int _r = (expr); \
if (_r == NCM_OK) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt " (rc=%d)\n", ##__VA_ARGS__, _r); g_fail = 1; } \
} while (0)
/* ---------- 辅助: AES-128-ECB 解密回明文(验证 eapi 往返) ---------- */
static int aes_ecb_decrypt_hex(const char *hex_upper, const unsigned char *key, char *out, size_t out_sz) {
int nbytes = strlen(hex_upper) / 2;
unsigned char *ct = malloc((size_t)nbytes);
unsigned char *pt = malloc((size_t)nbytes);
unsigned char *kv = malloc(16);
if (!ct || !pt || !kv) return -1;
kv[0]=0;
memcpy(kv, key, 16);
for (int i = 0; i < nbytes; i++) {
int hi, lo;
char a = hex_upper[i*2], b = hex_upper[i*2+1];
hi = (a <= '9') ? a-'0' : (a <= 'F' ? a-'A'+10 : a-'a'+10);
lo = (b <= '9') ? b-'0' : (b <= 'F' ? b-'A'+10 : b-'a'+10);
ct[i] = (unsigned char)((hi<<4)|lo);
}
mbedtls_aes_context aes; mbedtls_aes_init(&aes);
mbedtls_aes_setkey_dec(&aes, kv, 128);
for (int off = 0; off < nbytes; off += 16)
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, ct+off, pt+off);
mbedtls_aes_free(&aes);
/* 去 PKCS7 */
int pad = pt[nbytes-1];
if (pad < 1 || pad > 16 || pad > nbytes) { free(ct);free(pt);free(kv); return -1; }
int plen = nbytes - pad;
if ((size_t)plen >= out_sz) { free(ct);free(pt);free(kv); return -1; }
memcpy(out, pt, (size_t)plen); out[plen] = 0;
free(ct); free(pt); free(kv);
return plen;
}
static void test_crypto(void) {
printf("== crypto ==\n");
/* eapi 往返 */
char params[2048];
CHECKRC(ncm_crypto_eapi("/api/login/qrcode/unikey", "{\"type\":3}", params, sizeof params),
"eapi params produced");
char plain[2048];
int pl = aes_ecb_decrypt_hex(params, (const unsigned char *)NCM_EAPI_KEY, plain, sizeof plain);
CHECK(pl > 0, "eapi params decrypts (%d bytes)", pl);
if (pl > 0) {
CHECK(strstr(plain, "/api/login/qrcode/unikey") != NULL, "eapi url present in plain");
CHECK(strstr(plain, "{\"type\":3}") != NULL, "eapi json present in plain");
}
/* weapi 形状 */
char p2[1024], es[300];
CHECKRC(ncm_crypto_weapi("{\"id\":123}", p2, sizeof p2, es, sizeof es), "weapi produced");
CHECK(strlen(es) == 256, "weapi encSecKey is 256 hex (got %zu)", strlen(es));
CHECK(p2[0] != 0, "weapi params non-empty");
/* linuxapi */
char lp[2048];
CHECKRC(ncm_crypto_linuxapi("{\"a\":1}", lp, sizeof lp), "linuxapi produced");
}
static void test_store_cookie(void) {
printf("== store/device/cookie ==\n");
ncm_store_t st;
ncm_store_init(&st, NULL); /* env MEOW_DATA_DIR 或 /tmp */
char id1[NCM_STORE_ID_MAX], id2[NCM_STORE_ID_MAX];
ncm_store_get_device_id(&st, id1);
ncm_store_get_device_id(&st, id2);
CHECK(strcmp(id1, id2) == 0, "deviceId stable across calls");
CHECK(strlen(id1) == 52, "deviceId length 52 (got %zu)", strlen(id1));
unsigned char kb[8192];
size_t klen;
const char *cs = "MUSIC_U=abcdef123456; __csrf=xyz; os=pc";
CHECKRC(ncm_store_cookie_encrypt(cs, kb, &klen, sizeof kb), "cookie encrypt");
char cs2[4096];
CHECKRC(ncm_store_cookie_decrypt(kb, klen, cs2, sizeof cs2), "cookie decrypt");
CHECK(strcmp(cs, cs2) == 0, "cookie roundtrip matches");
/* 机器密钥可派生且 16 字节 */
unsigned char key[16];
CHECK(ncm_store_machine_key("t", key) == 0, "machine key derivable");
}
static void test_cache(void) {
printf("== cache ==\n");
ncm_cache_t c;
char dir[] = "/tmp/ncm_cache_test";
ncm_cache_init(&c, dir);
/* 清空目录 */
{
char cmd[128];
snprintf(cmd, sizeof cmd, "rm -rf %s; mkdir -p %s", dir, dir);
if (system(cmd) != 0) { /* best-effort */ }
}
/* miss */
char path[NCM_CACHE_DIR_MAX + 32];
int mrc = ncm_cache_lookup(&c, "1", "mp3", path, sizeof path);
CHECK(mrc == NCM_ERR_NOENT, "lookup miss returns NOENT (rc=%d)", mrc);
/* write + commit */
ncm_cache_writer_t *w;
CHECKRC(ncm_cache_open(&c, "100", "mp3", &w), "cache open");
CHECKRC(ncm_cache_write(w, (const unsigned char *)"AUDIODATA", 9), "cache write");
CHECKRC(ncm_cache_commit(w), "cache commit");
CHECKRC(ncm_cache_lookup(&c, "100", "mp3", path, sizeof path), "lookup hit");
CHECK(strstr(path, "song_100.mp3") != NULL, "hit path name");
CHECK(ncm_cache_total_bytes(&c) >= 9, "total_bytes >= 9");
/* abort leaves nothing */
ncm_cache_writer_t *w2;
ncm_cache_open(&c, "101", "mp3", &w2);
ncm_cache_write(w2, (const unsigned char *)"x", 1);
ncm_cache_abort(w2);
int arc = ncm_cache_lookup(&c, "101", "mp3", path, sizeof path);
CHECK(arc == NCM_ERR_NOENT, "aborted not cached (rc=%d)", arc);
/* 强制小上限测 LRU 淘汰:设 limit_bytes 极小,写入第二首歌触发淘汰 */
c.limit_bytes = 5; /* 仅 5 字节,任何条目超限 */
ncm_cache_writer_t *w3;
ncm_cache_open(&c, "102", "mp3", &w3);
ncm_cache_write(w3, (const unsigned char *)"abcdefgh", 8);
ncm_cache_commit(w3);
ncm_cache_evict(&c);
CHECK(ncm_cache_total_bytes(&c) <= 5, "evict kept total <= limit (%llu bytes)",
(unsigned long long)ncm_cache_total_bytes(&c));
/* 恢复默认上限 */
ncm_cache_set_limit_gb(&c, 10);
CHECK(ncm_cache_limit_gb(&c) == 10, "limit_gb reset to 10");
}
static void test_qr(void) {
printf("== qrcodegen ==\n");
unsigned char mods[177 * 177];
int size = 0;
CHECKRC(ncm_login_qr_matrix("https://music.163.com/login?codekey=test-uuid-1234", mods, 177, &size),
"qr matrix");
CHECK(size >= 21 && size <= 177, "qr size sane (got %d)", size);
int dark = 0;
for (int i = 0; i < size*size; i++) if (mods[i]) dark++;
CHECK(dark > 0, "qr has dark modules (%d)", dark);
}
static void test_crypto_vectors(void) {
/* 交叉:固定密文来源已在 rsa_raw_test 验证;这里仅核对 eapi 对已知 JSON 的确定性。 */
printf("== known eapi (deterministic) ==\n");
char a[2048], b[2048];
ncm_crypto_eapi("/api/x", "{\"k\":1}", a, sizeof a);
ncm_crypto_eapi("/api/x", "{\"k\":1}", b, sizeof b);
CHECK(strcmp(a, b) == 0, "eapi deterministic for same input");
}
static void test_network(void) {
printf("== network (live) ==\n");
int net_ok = 0;
char device_id[NCM_STORE_ID_MAX];
ncm_store_t st; ncm_store_init(&st, NULL);
ncm_store_get_device_id(&st, device_id);
/* unikey 实拿 */
char unikey[64] = "";
int rc = ncm_login_unikey(device_id, unikey, sizeof unikey);
if (rc == NCM_OK && unikey[0]) {
net_ok = 1;
printf(" PASS unikey = %s\n", unikey);
char qurl[256];
ncm_login_qr_url(unikey, qurl, sizeof qurl);
CHECK(strstr(qurl, "codekey=") != NULL, "qr url built : %s", qurl);
/* 轮询(未扫码,期望 801 等待扫码,或无效 key 800 过期) */
char cookiebuf[1024];
ncm_login_poll_t pr;
rc = ncm_login_poll(device_id, unikey, NULL, &pr, cookiebuf, sizeof cookiebuf);
if (rc == NCM_OK) {
printf(" PASS poll code=%d state=%d msg=%s\n", pr.code, pr.state, pr.message);
if (pr.state == NCM_LOGIN_PENDING)
printf(" PASS state=等待扫码(801)\n");
else
printf(" NOTE state=%d (code=%d), 非 801(可能已过期/已扫码)\n", pr.state, pr.code);
} else {
printf(" SKIP poll rc=%d (网络/被拒)\n", rc);
}
} else {
printf(" SKIP unikey rc=%d (网络不可达?) %s\n", rc, rc != NCM_OK ? "网络/证书" : "");
}
/* 无凭据/无效凭据的错误处理:不应崩溃,且应给出可判定的失败 */
printf("-- no-credential error handling --\n");
/* 伪造 MUSIC_U → guest → profile null → NCM_ERR_AUTH */
int r2 = ncm_login_status_check(device_id, "MUSIC_U=deadbeef0000", NULL, 0, NULL, 0);
printf(" NOTE login_status(bogus cookie) rc=%d\n", r2);
CHECK(r2 != NCM_OK, "login_status(bogus) not OK (rc=%d)", r2);
/* 无 cookie 也应优雅返回(可能 200 公开数据,或错误码)——只验证不崩溃且计数合法 */
ncm_playlist_t pls[8]; int pn = 0;
int r3 = ncm_api_user_playlist(device_id, "", "0", 30, 0, pls, 8, &pn);
printf(" NOTE user_playlist(uid=0, no cookie) rc=%d count=%d%s\n",
r3, pn, pn > 0 ? " (公开数据)" : "");
CHECK(r3 == NCM_OK || r3 < 0, "user_playlist no-cred graceful (rc=%d)", r3);
CHECK(pn >= 0, "user_playlist count sane (%d)", pn);
/* 无效 id → url 为 null,应无人可播 */
ncm_play_url_t pu;
int r4 = ncm_api_song_url(device_id, "", "0", NCM_LEVEL_HIGHER, &pu);
printf(" NOTE song_url(id=0) rc=%d url='%s'\n", r4, pu.url);
CHECK(r4 != NCM_OK || !pu.url[0], "song_url(bogus id) no url (rc=%d)", r4);
if (!net_ok) printf(" SKIP network live checks (host unreachable or TLS CA missing)\n");
}
int main(int argc, char **argv) {
const char *data_dir = (argc > 1) ? argv[1] : "/tmp/ncm_test";
printf("ncm_core self-test, data_dir=%s\n", data_dir);
ncm_ctx_t ctx;
ncm_ctx_init(&ctx, data_dir);
printf("deviceId=%s\n", ctx.device_id);
printf("cache_dir=%s limit_gb=%d\n", ctx.cache.dir, ctx.cache.limit_gb);
test_crypto();
test_crypto_vectors();
test_store_cookie();
test_cache();
test_qr();
test_network();
printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS ==");
return g_fail ? 1 : 0;
}
Binary file not shown.