feat(ncm): 网易云核心库 core/ (agent-203) — ncm_http(mbedtls TLS1.2+chunked+流式限幅)/ncm_crypto(eapi/weapi/linuxapi, RSA raw逐字节验证)/ncm_login(unikey→二维码→轮询状态机→cookie加密落盘)/ncm_api(歌单/详情/播放地址Hi-res逐档降级/歌词)/ncm_cache(LRU 默认10G)/ncm_store(deviceId持久化+机器密钥)
宿主+qemu-mipsel 全链路 PASS(unikey实拿/801轮询/eapi往返/RSA对拍/缓存LRU); -Wall -Wextra 自有代码0warning 待联调: 803真扫码、真机 hw:0,0 播放链路
This commit is contained in:
1 parent
85b4aea24f
commit
8012948b26
25 files changed
+3416
No files matched your search
@@ -0,0 +1,4 @@
|
||||
/* jsmn_impl.c — jsmn 唯一实现 TU。
|
||||
* jsmn.h 默认把实现(extern 函数)写进每个包含它的 TU,
|
||||
* 因此全工程只允许这一个 TU 不带 JSMN_HEADER 编译,其余 TU 用 -DJSMN_HEADER 取声明。 */
|
||||
#include "jsmn.h"
|
||||
@@ -0,0 +1,46 @@
|
||||
/* ncm.h — 网易云核心库伞头
|
||||
*
|
||||
* 汇总各模块,并提供 ncm_ctx_t 上下文(deviceId/会话 cookie/缓存/默认音质)。
|
||||
* 全部静态链接 mbedtls/jsmn/qrcodegen;纯 C,无 UI。
|
||||
*/
|
||||
#ifndef NCM_H
|
||||
#define NCM_H
|
||||
|
||||
#include "ncm_common.h"
|
||||
#include "ncm_crypto.h"
|
||||
#include "ncm_http.h"
|
||||
#include "ncm_json.h"
|
||||
#include "ncm_store.h"
|
||||
#include "ncm_req.h"
|
||||
#include "ncm_login.h"
|
||||
#include "ncm_api.h"
|
||||
#include "ncm_cache.h"
|
||||
|
||||
#define NCM_CTX_CA_PATH_MAX 256
|
||||
|
||||
typedef struct ncm_ctx {
|
||||
ncm_store_t store;
|
||||
ncm_cache_t cache;
|
||||
char device_id[NCM_STORE_ID_MAX]; /* 52hex 已持久化 */
|
||||
char cookie[NCM_COOKIE_MAX]; /* 会话 cookie,可为空 */
|
||||
int verify_ca; /* 1=校验CA(默认) */
|
||||
int timeout_ms; /* HTTP 超时 */
|
||||
char ca_path[NCM_CTX_CA_PATH_MAX]; /* 可选 CA bundle */
|
||||
int default_level; /* NCM_LEVEL_* 默认音质(Hi-res) */
|
||||
} ncm_ctx_t;
|
||||
|
||||
/* 默认缓存目录(<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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
Reference in new issue
Block a user