/* http_truncate_sim.c — 截断守卫宿主测试(0 真实网络) * * 背景(agent-355 挖出):core/ncm_http.c 的 http_run() 在 body 读取循环里, * 对端于 Content-Length 未收满时提前关闭(mbedtls_ssl_read 返回 0 / * MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY),loop 后仅判 overflow,否则一律 NCM_OK, * 于是残缺 body 被调用方当完整成功 → ncm_cache_commit 把半截 .part/.tmp 转正。 * * 本 sim mock 整个 mbedtls 传输层(仅符号桩,不链真实 libmbedtls),脚本化喂给 * ncm_http.c 一个可分块的对端响应流,验证 http_run(经公开入口 * ncm_http_download_status)对以下语义的判定: * 定长(Content-Length) 未收满即关闭 → 截断 → 非 NCM_OK(NCM_ERR_IO) * 定长(Content-Length) 收满 → BS_DONE → NCM_OK * Content-Length: 0(空 body) → BS_DONE → NCM_OK * 无 Content-Length、靠对端关闭结束(BS_CLOSE) → 合法 → NCM_OK * chunked 完整(0 分块末端) → BS_DONE → NCM_OK * chunked 中途关闭 → 截断 → 非 NCM_OK(NCM_ERR_IO) * * 编译(在 ncm_app 下): * gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \ * -c bench/http_truncate_sim.c -o /tmp/t.o * gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \ * -ffunction-sections -c core/ncm_http.c -o /tmp/t_http.o * gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -Ivendor/mbedtls/include \ * -ffunction-sections -c core/ncm_common.c -o /tmp/t_common.o * gcc -Wl,--gc-sections -o http_truncate_sim /tmp/t.o /tmp/t_http.o /tmp/t_common.o */ #include #include #include #include #include "ncm_http.h" #include "ncm_common.h" #include "mbedtls/net_sockets.h" #include "mbedtls/ssl.h" #include "mbedtls/x509_crt.h" /* ============================================================ * 传输层桩:脚本化"对端读流"。 * 每次 ncm_http_download_status 调用前用 tr_script(...) 装填脚本, * 脚本用尽后 mbedtls_ssl_read 返回 g_close_rc(0=有序关闭;或 * MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY 模拟 TLS close_notify)。 * ============================================================ */ typedef struct { const unsigned char *buf; size_t len; } tr_chunk_t; static const tr_chunk_t *g_script; static int g_script_n; static int g_script_i; static int g_close_rc; /* 脚本耗尽后的 ssl_read 返回值 */ static void tr_script(const tr_chunk_t *chunks, int n, int close_rc) { g_script = chunks; g_script_n = n; g_script_i = 0; g_close_rc = close_rc; } int mbedtls_ssl_read(mbedtls_ssl_context *ssl, unsigned char *buf, size_t len) { (void)ssl; if (g_script_i < g_script_n) { const tr_chunk_t *c = &g_script[g_script_i]; size_t n = c->len; if (n > len) n = len; /* 脚本块恒 <= 4096(tmp),一般整块发出 */ memcpy(buf, c->buf, n); g_script_i++; return (int)n; } return g_close_rc; } int mbedtls_ssl_write(mbedtls_ssl_context *ssl, const unsigned char *buf, size_t len) { (void)ssl; (void)buf; return (int)len; /* 请求头/体全部写成功 */ } /* 其余 mbedtls 桩:连接/握手/配置全部成功,不触碰任何真实网络。 */ void mbedtls_net_init(mbedtls_net_context *ctx) { (void)ctx; } int mbedtls_net_connect(mbedtls_net_context *ctx, const char *host, const char *port, int proto) { (void)ctx; (void)host; (void)port; (void)proto; return 0; } int mbedtls_net_set_nonblock(mbedtls_net_context *ctx) { (void)ctx; return 0; } int mbedtls_net_poll(mbedtls_net_context *ctx, uint32_t rw, uint32_t timeout) { (void)ctx; (void)rw; (void)timeout; return 1; /* 就绪 */ } void mbedtls_net_free(mbedtls_net_context *ctx) { (void)ctx; } int mbedtls_net_recv(void *ctx, unsigned char *buf, size_t len) { (void)ctx; (void)buf; (void)len; return MBEDTLS_ERR_SSL_WANT_READ; /* 实际读由 mbedtls_ssl_read 桩接管 */ } int mbedtls_net_send(void *ctx, const unsigned char *buf, size_t len) { (void)ctx; (void)buf; return (int)len; } void mbedtls_ssl_init(mbedtls_ssl_context *ssl) { (void)ssl; } void mbedtls_ssl_config_init(mbedtls_ssl_config *conf) { (void)conf; } int mbedtls_ssl_config_defaults(mbedtls_ssl_config *conf, int endpoint, int transport, int preset) { (void)conf; (void)endpoint; (void)transport; (void)preset; return 0; } void mbedtls_ssl_conf_authmode(mbedtls_ssl_config *conf, int authmode) { (void)conf; (void)authmode; } void mbedtls_ssl_conf_ca_chain(mbedtls_ssl_config *conf, mbedtls_x509_crt *ca_chain, mbedtls_x509_crl *ca_crl) { (void)conf; (void)ca_chain; (void)ca_crl; } void mbedtls_ssl_conf_rng(mbedtls_ssl_config *conf, int (*f_rng)(void *, unsigned char *, size_t), void *p_rng) { (void)conf; (void)f_rng; (void)p_rng; } int mbedtls_ssl_setup(mbedtls_ssl_context *ssl, const mbedtls_ssl_config *conf) { (void)ssl; (void)conf; return 0; } int mbedtls_ssl_set_hostname(mbedtls_ssl_context *ssl, const char *hostname) { (void)ssl; (void)hostname; return 0; } void mbedtls_ssl_set_bio(mbedtls_ssl_context *ssl, void *p_bio, mbedtls_ssl_send_t *f_send, mbedtls_ssl_recv_t *f_recv, mbedtls_ssl_recv_timeout_t *f_recv_timeout) { (void)ssl; (void)p_bio; (void)f_send; (void)f_recv; (void)f_recv_timeout; } int mbedtls_ssl_handshake(mbedtls_ssl_context *ssl) { (void)ssl; return 0; } void mbedtls_ssl_free(mbedtls_ssl_context *ssl) { (void)ssl; } void mbedtls_ssl_config_free(mbedtls_ssl_config *conf) { (void)conf; } void mbedtls_x509_crt_init(mbedtls_x509_crt *crt) { (void)crt; } int mbedtls_x509_crt_parse_file(mbedtls_x509_crt *crt, const char *path) { (void)crt; (void)path; return 0; /* CA 加载成功(validate=0 时不走此径,桩兜底) */ } void mbedtls_x509_crt_free(mbedtls_x509_crt *crt) { (void)crt; } /* ============================================================ * 下载回调 sink:累加接收到的 body 字节。 * ============================================================ */ typedef struct { unsigned char data[1 << 16]; size_t len; } sink_cb_t; static int on_body(void *ctx, const unsigned char *data, size_t len) { sink_cb_t *s = (sink_cb_t *)ctx; if (s->len + len > sizeof s->data) return -1; /* 上限(不触发) */ memcpy(s->data + s->len, data, len); s->len += len; return 0; } /* 运行一个用例:装填脚本 → 发起 GET 下载 → 记录 rc / body_len / content_length */ typedef struct { int rc; size_t body_len; size_t content_length; } case_result_t; static case_result_t run_case(const tr_chunk_t *script, int script_n, int close_rc) { ncm_http_req_t req; memset(&req, 0, sizeof req); req.url = "https://ex.test/song.mp3"; req.method = "GET"; req.verify_ca = 0; /* 跳过 CA 校验,聚焦 body 终态判定 */ req.timeout_ms = 3000; sink_cb_t sink; memset(&sink, 0, sizeof sink); ncm_http_resp_t resp; memset(&resp, 0, sizeof resp); tr_script(script, script_n, close_rc); int rc = ncm_http_download_status(&req, on_body, &sink, 0, &resp, NULL); case_result_t r; r.rc = rc; r.body_len = sink.len; r.content_length = resp.content_length; return r; } static int g_fail = 0; static void check(const char *name, int ok, const char *detail) { if (!ok) g_fail++; printf(" %s %s\n", ok ? "PASS" : "FAIL", name); if (detail) printf(" %s\n", detail); } int main(void) { char dbg[160]; /* ---- 场景 1:定长(CL=100) 对端提前关闭(只发 50 字节)→ 截断 ---- */ { static const unsigned char hdrs[] = "HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n"; static unsigned char chunk[512]; size_t hlen = sizeof hdrs - 1; memcpy(chunk, hdrs, hlen); for (size_t i = 0; i < 50; i++) chunk[hlen + i] = 'A'; tr_chunk_t script[1]; script[0].buf = chunk; script[0].len = hlen + 50; case_result_t r = run_case(script, 1, 0 /* 对端关闭 */); snprintf(dbg, sizeof dbg, "rc=%d body=%zu content_length=%zu", r.rc, r.body_len, r.content_length); check("truncate_fixed(CL=100, 50B, close) -> NCM_ERR_IO", (r.rc != NCM_OK) && (r.rc == NCM_ERR_IO) && (r.body_len == 50), dbg); } /* ---- 场景 2:定长(CL=100) 完整收满 → BS_DONE → NCM_OK ---- */ { static const unsigned char hdrs[] = "HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n"; static unsigned char chunk[512]; size_t hlen = sizeof hdrs - 1; memcpy(chunk, hdrs, hlen); for (size_t i = 0; i < 100; i++) chunk[hlen + i] = 'B'; tr_chunk_t script[1]; script[0].buf = chunk; script[0].len = hlen + 100; case_result_t r = run_case(script, 1, 0); snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len); check("normal_fixed(CL=100, 100B) -> NCM_OK", (r.rc == NCM_OK) && (r.body_len == 100), dbg); } /* ---- 场景 3:定长(CL=100) 两次读到收满(多块)→ NCM_OK ---- */ { static const unsigned char hdrs[] = "HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n"; static unsigned char part1[512], part2[64]; size_t hlen = sizeof hdrs - 1; memcpy(part1, hdrs, hlen); for (size_t i = 0; i < 60; i++) part1[hlen + i] = 'C'; for (size_t i = 0; i < 40; i++) part2[i] = 'D'; tr_chunk_t script[2]; script[0].buf = part1; script[0].len = hlen + 60; script[1].buf = part2; script[1].len = 40; case_result_t r = run_case(script, 2, 0); snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len); check("normal_fixed_multi(CL=100, 60+40B) -> NCM_OK", (r.rc == NCM_OK) && (r.body_len == 100), dbg); } /* ---- 场景 4:Content-Length: 0(空 body)→ BS_DONE → NCM_OK ---- */ { static const unsigned char chunk[] = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n"; static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } }; case_result_t r = run_case(script, 1, 0); snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len); check("cl_zero(CL=0) -> NCM_OK empty", (r.rc == NCM_OK) && (r.body_len == 0), dbg); } /* ---- 场景 5:无 Content-Length、靠对端关闭结束(BS_CLOSE) → 合法 → NCM_OK ---- */ { static const unsigned char hdrs[] = "HTTP/1.1 200 OK\r\n\r\n"; /* 无 CL、无 chunked */ static unsigned char chunk[512]; size_t hlen = sizeof hdrs - 1; memcpy(chunk, hdrs, hlen); for (size_t i = 0; i < 100; i++) chunk[hlen + i] = 'E'; tr_chunk_t script[1]; script[0].buf = chunk; script[0].len = hlen + 100; case_result_t r = run_case(script, 1, 0); snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len); check("close_delimited(no CL, 100B, close) -> NCM_OK", (r.rc == NCM_OK) && (r.body_len == 100), dbg); } /* ---- 场景 6:定长(CL=100) 提前关闭,且用 TLS close_notify 模拟 → 截断 ---- */ { static const unsigned char hdrs[] = "HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n"; static unsigned char chunk[512]; size_t hlen = sizeof hdrs - 1; memcpy(chunk, hdrs, hlen); for (size_t i = 0; i < 50; i++) chunk[hlen + i] = 'F'; tr_chunk_t script[1]; script[0].buf = chunk; script[0].len = hlen + 50; case_result_t r = run_case(script, 1, MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY); snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len); check("truncate_close_notify(CL=100, 50B) -> NCM_ERR_IO", (r.rc != NCM_OK) && (r.rc == NCM_ERR_IO) && (r.body_len == 50), dbg); } /* ---- 场景 7:chunked 完整(0 分块末端)→ BS_DONE → NCM_OK ---- */ { static const unsigned char chunk[] = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" "A\r\n0123456789\r\n" /* 16 进制 0xA = 10 字节 */ "0\r\n\r\n"; static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } }; case_result_t r = run_case(script, 1, 0); snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len); check("chunked_full(10B + 0-chunk) -> NCM_OK", (r.rc == NCM_OK) && (r.body_len == 10), dbg); } /* ---- 场景 8:chunked 中途关闭(数据未收完)→ 截断 → 非 NCM_OK ---- */ { static const unsigned char chunk[] = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" "5\r\nhel"; /* 宣称 5 字节,只发了 3,未到 0 分块 */ static const tr_chunk_t script[] = { { chunk, sizeof chunk - 1 } }; case_result_t r = run_case(script, 1, 0); snprintf(dbg, sizeof dbg, "rc=%d body=%zu", r.rc, r.body_len); check("chunked_truncated(3/5B, close) -> NCM_ERR_IO", (r.rc != NCM_OK) && (r.rc == NCM_ERR_IO), dbg); } printf("\n== HTTP TRUNCATE SIM: %s ==\n", g_fail ? "FAIL" : "ALL PASS"); return g_fail ? 1 : 0; }