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:
1 parent
4ff8c8baa3
commit
a651df0e9b
19 files changed
+466
-71
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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 ---------- */
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user