fix(ncm): weapi挂死根因修复 (agent-207, 6缺陷叠加) — ①超时改CLOCK_MONOTONIC(墙钟NTP回拨致poll超长阻塞) ②响应头解析隔行跳行漏Content-Length ③请求头缓冲1KB→8KB ④cookie值截断192B致MUSIC_U不完整被当游客(改ptr+len不截断+按名去重) ⑤split处NUL覆盖body首字节 ⑥Content-Length:0直接DONE; status_check硬上限6s; 803后worker线程取uid/nick落盘(修uid恒空)

ncm→bf6d2a79 已部署; 真机1次验证(风控合规): status_check 721ms rc=0 uid=408996274; user_playlist 2.5s rc=0 count=64
This commit is contained in:
MeowOS committed 2026-08-31 09:00:23 +08:00
1 parent 4ff8c8baa3
commit a651df0e9b
19 files changed
+466 -71

No files matched your search

+10
View File
@@ -42,12 +42,22 @@ int ncm_parse_int(const char *s, long *out) {
return 0;
}
/* 墙钟毫秒(CLOCK_REALTIME)。用于 request_id/_ntes_nnid/落盘时间戳等“取当前墙钟时间”的场合。
* 勿用于超时/截止计算:REALTIME 会随 NTP 校时跳变,导致 deadline-now 算错(详见 ncm_http.c 注释)。 */
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;
}
/* 单调毫秒(CLOCK_MONOTONIC)。仅用于超时/截止/间隔计时——永不随 NTP/校时跳变,
* 保证 wait_io 的 (deadline - now) 不被校时扰动成巨大值。 */
int64_t ncm_monotonic_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &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) {
+2 -1
View File
@@ -47,7 +47,8 @@ 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); /* 毫秒时间戳 */
int64_t ncm_now_ms(void); /* 墙钟毫秒(CLOCK_REALTIME),供 request_id/落盘时间戳 */
int64_t ncm_monotonic_ms(void); /* 单调毫秒(CLOCK_MONOTONIC),仅用于超时/截止/间隔计时 */
void ncm_random_bytes(unsigned char *out, size_t n); /* /dev/urandom,失败则回退时钟扰动 */
/* ---------- hex ---------- */
+47 -32
View File
@@ -84,9 +84,13 @@ static int load_ca(mbedtls_x509_crt *cacert, const char *ca_path) {
return -1;
}
/* ---------- 等待 IO(deadline 约束) ---------- */
/* ---------- 等待 IO(deadline 约束) ----------
* 超时/截止一律用 ncm_monotonic_ms()(CLOCK_MONOTONIC)。若用 ncm_now_ms()(CLOCK_REALTIME),
* 设备 NTP 校时把墙钟拨回/拨快时,(deadline - now) 会被扰动成远超 15s 的值甚至溢出成巨大
* uint32_t,于是 mbedtls_net_poll 得到超长时间,线程长期阻塞在 poll(表现为 wchan=poll_schedule_timeout
* 且 >10 分钟不超时)。单调钟不受校时影响,保证每次 poll 至多等剩余 deadline。 */
static int wait_io(mbedtls_net_context *net, uint32_t rw, int64_t deadline_ms) {
int64_t now = ncm_now_ms();
int64_t now = ncm_monotonic_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);
@@ -136,34 +140,37 @@ static int parse_headers(ncm_http_resp_t *resp) {
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;
/* 状态行: "HTTP/1.1 200 OK" —— 取协议版本后的状态码(显式用 strtol 忽略 reason 文本)。 */
const char *eol = strstr(h, "\r\n");
if (!eol) return -1;
char line[128];
size_t line_len = (size_t)(eol - h);
if (line_len >= sizeof line) line_len = sizeof line - 1;
memcpy(line, h, line_len);
line[line_len] = 0;
{
const char *sp = strchr(line, ' ');
const char *sp = strchr(line, ' '); /* 第一个空格即版本与状态码的分隔 */
if (sp) {
long st = parse_long_field(sp + 1);
sp++;
while (*sp == ' ' || *sp == '\t') sp++;
long st = strtol(sp, NULL, 10); /* "200 OK" -> 200,忽略 reason */
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);
/* 头行遍历:逐行处理,不跳行(旧实现 strstr+&p+=2 会每隔一行跳一行,漏掉 Content-Length 等)。 */
const char *p = eol + 2;
while (*p) {
if (*p == '\r' || *p == '\n') break; /* 空行 = 头结束 */
const char *le = strstr(p, "\r\n");
if (!le) break;
size_t l = (size_t)(le - p);
char hdr[256];
if (l >= sizeof hdr) { p = eol + 2; continue; }
if (l >= sizeof hdr) { p = le + 2; continue; }
memcpy(hdr, p, l);
hdr[l] = 0;
char *colon = strchr(hdr, ':');
if (!colon) { p = eol + 2; continue; }
if (!colon) { p = le + 2; continue; }
*colon = 0;
char *name = hdr;
char *val = colon + 1;
@@ -181,7 +188,7 @@ static int parse_headers(ncm_http_resp_t *resp) {
resp->has_content_type = 1;
ncm_strlcpy(resp->content_type, val, sizeof resp->content_type);
}
p = eol + 2;
p = le + 2;
}
return 0;
}
@@ -206,7 +213,9 @@ typedef struct {
static void body_st_init(body_st *b, const ncm_http_resp_t *hdr) {
memset(b, 0, sizeof *b);
if (hdr->chunked) {
if (hdr->content_length == 0) {
b->st = BS_DONE; /* Content-Length: 0 → 无 body,避免等对端关闭 */
} else if (hdr->chunked) {
b->st = BS_CHUNK_SZ;
} else if (hdr->content_length != (size_t)-1) {
b->st = BS_LEN;
@@ -320,7 +329,8 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
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;
int64_t deadline = ncm_monotonic_ms() + (int64_t)tmo; /* 截止时刻用单调钟,避免 NTP 校时跳变 */
mbedtls_net_context net;
mbedtls_ssl_context ssl;
@@ -366,8 +376,10 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
} else goto done;
}
/* 构造请求(先写头、再写体,避免大 body 撑爆单一缓冲) */
char headbuf[1024];
/* 构造请求(先写头、再写体,避免大 body 撑爆单一缓冲)。
* 头缓冲 ≥ extra_hdrs(≤NCM_REQ_HDRS_MAX=4096)+~200,(M2 联调) weapi 带真实会话 cookie 时
* extra_hdrs 可达 ~1.1KB,旧 1024 会溢出返回 NCM_ERR_FULL(请求未发出),故用 8192。 */
char headbuf[8192];
int hl;
const char *m = req->method ? req->method : "POST";
int has_body = (req->body && req->body_len > 0);
@@ -432,19 +444,22 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
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 */
/* split+4 即 body 起始;注意其与 resp->headers+consumed 是同一地址,
故先送 body 再在 consumed 处 NUL 终结头部,否则会覆盖 body 首字节。 */
size_t consumed = (size_t)(split + 4 - resp->headers); /* body 起始偏移 */
size_t old_hlen = resp->headers_len;
resp->headers_len = consumed;
hdr_done = 1;
/* 需要把 headers 缓冲里 split+4 之后的字节(若有) 也送到 body */
size_t hdr_after = resp->headers_len - consumed;
/* headers 缓冲里 split+4 之后的字节(本读取已复制进 headers 的部分)送入 body */
size_t hdr_after = old_hlen - consumed;
if (hdr_after > 0) {
if (body_feed(&bst, (const unsigned char *)(split + 4), hdr_after, sink) < 0) { ret=NCM_ERR_JSON; goto done; }
}
/* tmp 中超出被复制到 headers 的部分也属 body */
if (space < n) {
if (body_feed(&bst, tmp + space, n - space, sink) < 0) { ret=NCM_ERR_JSON; goto done; }
}
resp->headers[consumed] = 0; /* body 已复制走,安全 NUL 终结头部 */
} else {
if (resp->headers_len >= NCM_HTTP_HDR_MAX - 1) { ret = NCM_ERR_FULL; goto done; }
}
+3 -2
View File
@@ -154,9 +154,10 @@ int ncm_login_status_check(const char *device_id, const char *cookie,
req.body_len = strlen(body);
req.extra_hdrs = hdrs;
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
req.timeout_ms = 15000;
/* 硬超时上限:即使服务端无响应也不久等(歌单请求会独立验证 cookie,status_check 只是取 uid/昵称的辅助) */
req.timeout_ms = 6000;
char respbuf[4096];
char respbuf[16384];
ncm_http_resp_t resp;
rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp);
if (rc != NCM_OK) return rc;
+44 -31
View File
@@ -17,24 +17,33 @@
#define APPVER "3.1.17.204416"
#define CHANNEL "netease"
/* ---------- cookie map ---------- */
typedef struct { char name[48]; char val[192]; } ck_t;
/* ---------- cookie map ----------
* 值用 (ptr,len) 指向源串/字符串字面量,避免把 ~1KB 的 MUSIC_U 截断到固定缓冲
* (旧实现 val[192] 会把 834 字节的 MUSIC_U 截到 191,导致服务端识别为游客 session)。
* 所有 ptr 在 ncm_req_build 调用期间有效;ck_to_string 同步使用,不跨调用保留。 */
typedef struct { char name[48]; const char *val; size_t vlen; } 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->p[m->n].val = val ? val : "";
m->p[m->n].vlen = val ? strlen(val) : 0;
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) {
static void ck_set(ckmap_t *m, const char *name, const char *val, size_t vlen) {
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);
if (i >= 0) { m->p[i].val = val; m->p[i].vlen = vlen; }
else {
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);
m->p[m->n].val = val; m->p[m->n].vlen = vlen;
m->n++;
}
}
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);
@@ -42,19 +51,19 @@ static void ck_set_default(ckmap_t *m, const char *name, const char *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++) {
size_t name_len = strlen(m->p[i].name);
size_t vlen = m->p[i].vlen;
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 (o + sep + name_len + 1 + vlen + 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;
memcpy(out + o, m->p[i].name, name_len); o += name_len;
out[o++] = '=';
size_t vl = strlen(m->p[i].val);
memcpy(out + o, m->p[i].val, vl); o += vl;
memcpy(out + o, m->p[i].val, vlen); o += vlen;
}
out[o] = 0;
}
/* 解析 cookie 字符串(; 分隔) 进 map */
/* 解析 cookie 字符串(; 分隔) 进 map:值以 (ptr,len) 存(指向 cookie 源串,不截断),按名去重(后值覆盖)。 */
static void ck_parse(ckmap_t *m, const char *cookie) {
if (!cookie) return;
const char *p = cookie;
@@ -65,13 +74,14 @@ static void ck_parse(ckmap_t *m, const char *cookie) {
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];
const char *vstart = eq + 1;
size_t val_len = semi ? (size_t)(semi - vstart) : strlen(vstart);
char name[48];
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);
/* 去尾空格 */
while (val_len && (vstart[val_len-1]==' '||vstart[val_len-1]=='\t')) val_len--;
ck_set(m, name, vstart, val_len);
}
p = semi ? semi + 1 : p + strlen(p);
}
@@ -83,7 +93,10 @@ int ncm_req_cookie_get(const char *cookie, const char *name, char *out, size_t o
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);
size_t cp = m.p[i].vlen;
if (cp >= out_sz) cp = out_sz - 1;
memcpy(out, m.p[i].val, cp);
out[cp] = 0;
return NCM_OK;
}
@@ -189,23 +202,23 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
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 + 默认伪装字段补齐 */
/* 会话 cookie + 默认伪装字段补齐。
* 生成值 buffer(nuid/nuid_hex/nnid/wnmcid/nmtid)都提到本块作用域,
* 确保 ck 存的是其值指针在 ck_to_string 调用前仍然有效(ck 只存 ptr,不复制)。 */
char nuid[96], nuid_hex[33], nnid[96], wnmcid[40], nmtid[9];
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);
ncm_random_bytes((unsigned char *)nuid, 16);
/* _ntes_nnid = nuid,ts ; _ntes_nuid = nuid */
ncm_hex_encode((const unsigned char *)nuid, 16, nuid_hex, 0);
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 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);
ncm_hex_encode(r, 16, wnmcid, 0);
}
ck_set_default(&m, "WNMCID", wnmcid);
ck_set_default(&m, "WEVNSM", "1.0.0");
@@ -216,10 +229,10 @@ int ncm_req_build(const char *device_id, const char *cookie, const char *path,
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);
ncm_hex_encode(r, 4, nmtid, 1); /* 大写 */
ck_set_default(&m, "NMTID", nmtid);
}
char cookie_full[2048];
char cookie_full[4096];
ck_to_string(&m, cookie_full, sizeof cookie_full);
int n = snprintf(hdrs, hdrs_sz,