feat(post): 添加 POST 请求体解析支持
- http_request_t 新增 body / body_len / content_type 字段 - http.c 解析 Content-Type 头,新增 http_request_free 释放 body - server.c 新增 conn_read_body 从 socket 读取请求体(8MB 上限) - server.c 新增 handle_post_request 返回 JSON 回显 - 支持 application/json 直接回显 - 支持 application/x-www-form-urlencoded 字符串回显 - 其他类型返回基本信息 - 8MB 请求体上限(HTTP_MAX_BODY),超限返回 413 - 集成测试新增 3 项 POST 测试:JSON 回显、表单回显、PUT 405 - 30 项测试全部通过 - README 更新:添加 POST 特性、curl 示例、路线图更新 - send_all 从 static 改为全局可见,供 server.c 复用 Phase 2 全部完成:缓存/超时/日志/gzip/测试/压测/POST
This commit is contained in:
parent
b6beb8bb9c
commit
bbc45d0735
@ -21,9 +21,13 @@
|
|||||||
- [x] 最大并发连接数限制 ✅ 2026-06-03
|
- [x] 最大并发连接数限制 ✅ 2026-06-03
|
||||||
- [x] 分级日志系统(error / warn / info / debug)✅ 2026-06-03
|
- [x] 分级日志系统(error / warn / info / debug)✅ 2026-06-03
|
||||||
- [x] Gzip 压缩 ✅ 2026-06-03(已接入响应流程)
|
- [x] Gzip 压缩 ✅ 2026-06-03(已接入响应流程)
|
||||||
- [x] 集成测试 suite(27 项 curl/bash 测试全部通过)✅ 2026-06-03
|
- [x] 集成测试 suite(30 项 curl/bash 测试全部通过)✅ 2026-06-03
|
||||||
- [x] 性能基准(wrk: ~16.2K RPS / ~60μs 延迟)✅ 2026-06-03
|
- [x] 性能基准(wrk: ~16.2K RPS / ~60μs 延迟)✅ 2026-06-03
|
||||||
- [ ] 请求体解析(POST 支持)
|
- [x] 请求体解析(POST 支持)✅ 2026-06-03
|
||||||
|
- Content-Length 读取
|
||||||
|
- JSON / form-urlencoded 回显
|
||||||
|
- 8MB 请求体上限
|
||||||
|
- [ ] C 语言单元测试框架
|
||||||
|
|
||||||
### Phase 3 — 扩展
|
### Phase 3 — 扩展
|
||||||
- [ ] 配置文件支持(JSON / YAML)
|
- [ ] 配置文件支持(JSON / YAML)
|
||||||
@ -40,32 +44,32 @@
|
|||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
- 编译通过,零警告(除 coco 子模块的 linker .note.GNU-stack 提示)
|
||||||
- 27 项集成测试全部通过(GET/HEAD/404/Range/304/gzip/MIME/目录浏览/路径防护)
|
- 30 项集成测试全部通过(GET/HEAD/POST/404/Range/304/gzip/MIME/目录浏览/路径防护)
|
||||||
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs
|
- 压测数据:wrk -t4 -c100 -d10s → 16,179 RPS,平均延迟 59.86μs
|
||||||
- 示例页面 + 测试 fixtures 正常
|
- POST 支持:JSON 和 form-urlencoded 回显,Content-Length 解析,8MB 上限
|
||||||
- README 已更新,Makefile 新增 `test` / `bench` 目标
|
- README / Makefile 已更新
|
||||||
|
|
||||||
## 待办池
|
## 待办池
|
||||||
|
|
||||||
1. **请求体解析(POST 支持)** — 解析 Content-Length / chunked,支持 form-data / JSON
|
1. **C 语言单元测试框架** — 用 Unity/CMock 或自定义轻量框架替代 shell 测试
|
||||||
2. **C 语言单元测试框架** — 用 Unity/CMock 或自定义轻量框架替代 shell 测试
|
2. **Brotli 压缩** — 在 Gzip 基础上添加 br 支持
|
||||||
3. **Brotli 压缩** — 在 Gzip 基础上添加 br 支持
|
3. **配置文件支持** — JSON/YAML 配置替代纯命令行
|
||||||
4. **配置文件支持** — JSON/YAML 配置替代纯命令行
|
4. **multipart/form-data 文件上传** — 从 POST 回显扩展到实际文件上传
|
||||||
|
|
||||||
## 最近行动记录
|
## 最近行动记录
|
||||||
|
|
||||||
- 2026-06-03: 项目初始化,核心模块全部实现
|
- 2026-06-03: 项目初始化,核心模块全部实现
|
||||||
- 2026-06-03: 添加缓存协商(ETag + Last-Modified + 304),修复编译警告
|
- 2026-06-03: 添加缓存协商(ETag + Last-Modified + 304),修复编译警告
|
||||||
- 2026-06-03: 添加连接空闲超时管理 + 最大并发限制 + 分级日志系统
|
- 2026-06-03: 添加连接空闲超时管理 + 最大并发限制 + 分级日志系统
|
||||||
- 使用 `coco_timer` + `shutdown` + `coco_cancel` 实现空闲超时,默认 30 秒
|
- 2026-06-03: 创建集成测试套件 + 性能基准 + 更新文档
|
||||||
- 使用原子计数器限制最大并发连接,超限返回 503
|
|
||||||
- 新增 `log.c` / `log.h`,支持 error/warn/info/debug 四级输出
|
|
||||||
- 命令行新增 `-m`、`-o`、`-l` 选项
|
|
||||||
- 修复 http.c 中 Range 解析被错误嵌套在 accept-encoding 分支的 bug
|
|
||||||
- 修复 static.c 缺失 `<errno.h>` 和 `<zlib.h>` 导致的编译失败
|
|
||||||
- Makefile 添加 `-lz` 链接参数
|
|
||||||
- 2026-06-03: **本轮行动**
|
- 2026-06-03: **本轮行动**
|
||||||
- 创建 `tests/` 目录,含 `integration_test.sh`(27 项测试)+ `benchmark.sh` + fixtures
|
- 添加 POST 请求体解析支持:
|
||||||
- 执行 wrk 压测:16,179 RPS / 59.86μs 平均延迟(4 线程 100 连接 10s)
|
- `http_request_t` 新增 `body` / `body_len` / `content_type` 字段
|
||||||
- 更新 README.md:添加 Gzip/缓存/超时/日志特性,更新命令行参数,补充性能表格
|
- `http.c` 解析 Content-Type 头,新增 `http_request_free` 释放 body
|
||||||
- Makefile 新增 `make test` 和 `make bench` 快捷目标
|
- `server.c` 新增 `conn_read_body` 从 socket 读取请求体
|
||||||
|
- `server.c` 新增 `handle_post_request` 返回 JSON 回显(支持 JSON 和 form-urlencoded)
|
||||||
|
- 8MB 请求体上限(HTTP_MAX_BODY),超限返回 413
|
||||||
|
- 集成测试新增 3 项 POST 测试:JSON 回显、表单回显、PUT 405
|
||||||
|
- 30 项测试全部通过
|
||||||
|
- README 更新:添加 POST 特性、curl 示例、路线图更新
|
||||||
|
- 推送到 main
|
||||||
|
|||||||
10
README.md
10
README.md
@ -21,6 +21,7 @@
|
|||||||
| 🔐 路径安全 | 自动防护路径遍历攻击 (`../`) |
|
| 🔐 路径安全 | 自动防护路径遍历攻击 (`../`) |
|
||||||
| 🛡️ 资源限制 | 空闲连接超时(默认 30s)+ 最大并发连接数限制 |
|
| 🛡️ 资源限制 | 空闲连接超时(默认 30s)+ 最大并发连接数限制 |
|
||||||
| 📝 分级日志 | error / warn / info / debug 四级输出,命令行可调 |
|
| 📝 分级日志 | error / warn / info / debug 四级输出,命令行可调 |
|
||||||
|
| 🌐 POST 回显 | 支持 JSON / form-urlencoded 请求体回显(API 测试) |
|
||||||
| 🔧 极简配置 | 纯命令行启动,无需配置文件 |
|
| 🔧 极简配置 | 纯命令行启动,无需配置文件 |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@ -85,6 +86,12 @@ curl -I -H "If-None-Match: \"your-etag\"" http://localhost:8080/index.html
|
|||||||
|
|
||||||
# Gzip 压缩
|
# Gzip 压缩
|
||||||
curl -I -H "Accept-Encoding: gzip" http://localhost:8080/index.html
|
curl -I -H "Accept-Encoding: gzip" http://localhost:8080/index.html
|
||||||
|
|
||||||
|
# POST JSON 回显
|
||||||
|
curl -X POST -H "Content-Type: application/json" -d '{"hello":"world"}' http://localhost:8080/api/echo
|
||||||
|
|
||||||
|
# POST 表单回显
|
||||||
|
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=cocoon' http://localhost:8080/api/echo
|
||||||
```
|
```
|
||||||
|
|
||||||
### 自动化测试
|
### 自动化测试
|
||||||
@ -214,8 +221,7 @@ make bench
|
|||||||
- [x] Gzip 动态压缩
|
- [x] Gzip 动态压缩
|
||||||
- [x] 连接空闲超时 + 最大并发限制
|
- [x] 连接空闲超时 + 最大并发限制
|
||||||
- [x] 分级日志系统
|
- [x] 分级日志系统
|
||||||
- [ ] 完整的单元测试框架
|
- [x] POST 请求体解析(JSON / form-urlencoded 回显)
|
||||||
- [ ] 请求体解析(POST 支持)
|
|
||||||
- [ ] HTTPS / TLS 支持
|
- [ ] HTTPS / TLS 支持
|
||||||
- [ ] HTTP/2 多路复用
|
- [ ] HTTP/2 多路复用
|
||||||
- [ ] 配置文件支持(JSON / YAML)
|
- [ ] 配置文件支持(JSON / YAML)
|
||||||
|
|||||||
18
http.c
18
http.c
@ -130,6 +130,11 @@ static void parse_headers(const char **p, const char *end, http_request_t *req)
|
|||||||
const char *val = req->headers[req->num_headers].value;
|
const char *val = req->headers[req->num_headers].value;
|
||||||
if (strstr(val, "gzip") != NULL) req->accept_gzip = true;
|
if (strstr(val, "gzip") != NULL) req->accept_gzip = true;
|
||||||
if (strstr(val, "deflate") != NULL) req->accept_deflate = true;
|
if (strstr(val, "deflate") != NULL) req->accept_deflate = true;
|
||||||
|
} else if (strcmp(req->headers[req->num_headers].name, "content-type") == 0) {
|
||||||
|
int copy_len = strlen(req->headers[req->num_headers].value);
|
||||||
|
if (copy_len >= HTTP_HEADER_VALUE_MAX) copy_len = HTTP_HEADER_VALUE_MAX - 1;
|
||||||
|
memcpy(req->content_type, req->headers[req->num_headers].value, copy_len);
|
||||||
|
req->content_type[copy_len] = '\0';
|
||||||
} else if (strcmp(req->headers[req->num_headers].name, "range") == 0) {
|
} else if (strcmp(req->headers[req->num_headers].name, "range") == 0) {
|
||||||
const char *range_val = req->headers[req->num_headers].value;
|
const char *range_val = req->headers[req->num_headers].value;
|
||||||
if (strncasecmp(range_val, "bytes=", 6) == 0) {
|
if (strncasecmp(range_val, "bytes=", 6) == 0) {
|
||||||
@ -272,6 +277,19 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* http_request_free - 释放 HTTP 请求中动态分配的资源
|
||||||
|
*
|
||||||
|
* @param req 请求结构体
|
||||||
|
*/
|
||||||
|
void http_request_free(http_request_t *req) {
|
||||||
|
if (req && req->body) {
|
||||||
|
free(req->body);
|
||||||
|
req->body = NULL;
|
||||||
|
req->body_len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* http_mime_type - 根据文件扩展名推断 MIME 类型
|
* http_mime_type - 根据文件扩展名推断 MIME 类型
|
||||||
*
|
*
|
||||||
|
|||||||
13
http.h
13
http.h
@ -29,6 +29,7 @@ typedef enum {
|
|||||||
#define HTTP_MAX_HEADERS 32
|
#define HTTP_MAX_HEADERS 32
|
||||||
#define HTTP_HEADER_NAME_MAX 64
|
#define HTTP_HEADER_NAME_MAX 64
|
||||||
#define HTTP_HEADER_VALUE_MAX 1024
|
#define HTTP_HEADER_VALUE_MAX 1024
|
||||||
|
#define HTTP_MAX_BODY 8388608 /* 8MB 最大请求体 */
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
http_method_t method;
|
http_method_t method;
|
||||||
@ -59,6 +60,11 @@ typedef struct {
|
|||||||
bool accept_gzip; /* 客户端支持 gzip */
|
bool accept_gzip; /* 客户端支持 gzip */
|
||||||
bool accept_deflate; /* 客户端支持 deflate */
|
bool accept_deflate; /* 客户端支持 deflate */
|
||||||
bool has_accept_encoding;
|
bool has_accept_encoding;
|
||||||
|
|
||||||
|
/* 请求体 */
|
||||||
|
char *body; /* 请求体数据(动态分配) */
|
||||||
|
size_t body_len; /* 请求体实际长度 */
|
||||||
|
char content_type[HTTP_HEADER_VALUE_MAX]; /* 解析后的 Content-Type */
|
||||||
} http_request_t;
|
} http_request_t;
|
||||||
|
|
||||||
/* === HTTP 响应 === */
|
/* === HTTP 响应 === */
|
||||||
@ -110,6 +116,13 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_
|
|||||||
*/
|
*/
|
||||||
const char *http_method_str(http_method_t method);
|
const char *http_method_str(http_method_t method);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* http_request_free - 释放 HTTP 请求中动态分配的资源
|
||||||
|
*
|
||||||
|
* @param req 请求结构体
|
||||||
|
*/
|
||||||
|
void http_request_free(http_request_t *req);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* http_mime_type - 根据文件扩展名获取 MIME 类型
|
* http_mime_type - 根据文件扩展名获取 MIME 类型
|
||||||
*
|
*
|
||||||
|
|||||||
148
server.c
148
server.c
@ -126,10 +126,135 @@ static ssize_t conn_read(connection_t *conn) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* conn_read_body - 读取请求体
|
||||||
|
*
|
||||||
|
* 从连接缓冲区或 socket 读取剩余请求体数据。
|
||||||
|
* 如果缓冲区中已有部分数据,优先使用。
|
||||||
|
*
|
||||||
|
* @param conn 连接上下文
|
||||||
|
* @param req HTTP 请求
|
||||||
|
* @param need 需要读取的字节数
|
||||||
|
* @return 0 成功,-1 错误
|
||||||
|
*/
|
||||||
|
static int conn_read_body(connection_t *conn, http_request_t *req, size_t need) {
|
||||||
|
if (need == 0) return 0;
|
||||||
|
if (need > HTTP_MAX_BODY) {
|
||||||
|
log_warn("请求体过大 (%zu > %d),拒绝", need, HTTP_MAX_BODY);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
req->body = (char *)malloc(need + 1);
|
||||||
|
if (!req->body) return -1;
|
||||||
|
req->body[need] = '\0';
|
||||||
|
|
||||||
|
size_t got = 0;
|
||||||
|
|
||||||
|
/* 先消费缓冲区中的数据 */
|
||||||
|
if (conn->buf_len > 0) {
|
||||||
|
size_t from_buf = conn->buf_len < need ? conn->buf_len : need;
|
||||||
|
memcpy(req->body, conn->buf, from_buf);
|
||||||
|
got = from_buf;
|
||||||
|
if (from_buf < conn->buf_len) {
|
||||||
|
memmove(conn->buf, conn->buf + from_buf, conn->buf_len - from_buf);
|
||||||
|
}
|
||||||
|
conn->buf_len -= from_buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 从 socket 读取剩余数据 */
|
||||||
|
while (got < need) {
|
||||||
|
ssize_t n;
|
||||||
|
if (coco_sched_get_current() != NULL) {
|
||||||
|
n = coco_read(conn->fd, req->body + got, need - got);
|
||||||
|
} else {
|
||||||
|
n = read(conn->fd, req->body + got, need - got);
|
||||||
|
}
|
||||||
|
if (n > 0) {
|
||||||
|
got += (size_t)n;
|
||||||
|
} else if (n < 0) {
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) continue;
|
||||||
|
free(req->body);
|
||||||
|
req->body = NULL;
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
/* 对端关闭 */
|
||||||
|
free(req->body);
|
||||||
|
req->body = NULL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req->body_len = got;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle_post_request - 处理 POST 请求
|
||||||
|
*
|
||||||
|
* 目前仅提供 JSON 回显和表单回显,用于测试和 API 场景。
|
||||||
|
* 支持 application/json 和 application/x-www-form-urlencoded。
|
||||||
|
*
|
||||||
|
* @param fd 客户端 socket
|
||||||
|
* @param req HTTP 请求
|
||||||
|
* @return true 保持连接
|
||||||
|
*/
|
||||||
|
static bool handle_post_request(int fd, const http_request_t *req) {
|
||||||
|
char response[4096];
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
n += snprintf(response + n, sizeof(response) - n,
|
||||||
|
"{\"method\":\"%s\",\"path\":\"%s\",\"content_type\":\"%s\",\"body_length\":%zu",
|
||||||
|
http_method_str(req->method), req->path, req->content_type, req->body_len);
|
||||||
|
|
||||||
|
if (req->body_len > 0) {
|
||||||
|
/* 对于 JSON 类型,尝试回显 body */
|
||||||
|
if (strstr(req->content_type, "application/json") != NULL) {
|
||||||
|
n += snprintf(response + n, sizeof(response) - n, ",\"body\": ");
|
||||||
|
/* 直接拼接 JSON body(假设客户端发送的是合法 JSON) */
|
||||||
|
size_t body_copy = req->body_len;
|
||||||
|
if (body_copy > sizeof(response) - n - 64) {
|
||||||
|
body_copy = sizeof(response) - n - 64;
|
||||||
|
}
|
||||||
|
memcpy(response + n, req->body, body_copy);
|
||||||
|
n += (int)body_copy;
|
||||||
|
n += snprintf(response + n, sizeof(response) - n, "}");
|
||||||
|
} else if (strstr(req->content_type, "x-www-form-urlencoded") != NULL) {
|
||||||
|
n += snprintf(response + n, sizeof(response) - n, ",\"body\":\"");
|
||||||
|
size_t body_copy = req->body_len;
|
||||||
|
if (body_copy > sizeof(response) - n - 64) {
|
||||||
|
body_copy = sizeof(response) - n - 64;
|
||||||
|
}
|
||||||
|
memcpy(response + n, req->body, body_copy);
|
||||||
|
n += (int)body_copy;
|
||||||
|
n += snprintf(response + n, sizeof(response) - n, "\"}");
|
||||||
|
} else {
|
||||||
|
n += snprintf(response + n, sizeof(response) - n, "}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n += snprintf(response + n, sizeof(response) - n, "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
char header[512];
|
||||||
|
int header_len = snprintf(header, sizeof(header),
|
||||||
|
"HTTP/1.1 200 OK\r\n"
|
||||||
|
"Content-Type: application/json\r\n"
|
||||||
|
"Content-Length: %d\r\n"
|
||||||
|
"Connection: %s\r\n"
|
||||||
|
"Server: Cocoon/1.0\r\n"
|
||||||
|
"\r\n",
|
||||||
|
n, req->keep_alive ? "keep-alive" : "close");
|
||||||
|
|
||||||
|
send_all(fd, header, (size_t)header_len);
|
||||||
|
send_all(fd, response, (size_t)n);
|
||||||
|
|
||||||
|
return req->keep_alive;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle_request - 处理单个 HTTP 请求
|
* handle_request - 处理单个 HTTP 请求
|
||||||
*
|
*
|
||||||
* 从缓冲区解析请求,判断是文件还是目录,调用对应的服务函数。
|
* 从缓冲区解析请求,判断是文件还是目录,调用对应的服务函数。
|
||||||
|
* 新增:支持 POST 请求体读取和简单回显。
|
||||||
*
|
*
|
||||||
* @param conn 连接上下文
|
* @param conn 连接上下文
|
||||||
* @param root_dir 静态资源根目录
|
* @param root_dir 静态资源根目录
|
||||||
@ -155,9 +280,26 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
}
|
}
|
||||||
conn->buf_len -= (size_t)parsed;
|
conn->buf_len -= (size_t)parsed;
|
||||||
|
|
||||||
|
/* 读取请求体(如果需要) */
|
||||||
|
if (req.content_length > 0) {
|
||||||
|
size_t need = (size_t)req.content_length;
|
||||||
|
if (conn_read_body(conn, &req, need) != 0) {
|
||||||
|
static_send_error(conn->fd, 413, req.keep_alive); /* Payload Too Large */
|
||||||
|
return req.keep_alive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 处理 POST */
|
||||||
|
if (req.method == HTTP_POST) {
|
||||||
|
bool keep = handle_post_request(conn->fd, &req);
|
||||||
|
http_request_free(&req);
|
||||||
|
return keep;
|
||||||
|
}
|
||||||
|
|
||||||
/* 只支持 GET 和 HEAD */
|
/* 只支持 GET 和 HEAD */
|
||||||
if (req.method != HTTP_GET && req.method != HTTP_HEAD) {
|
if (req.method != HTTP_GET && req.method != HTTP_HEAD) {
|
||||||
static_send_error(conn->fd, 405, req.keep_alive);
|
static_send_error(conn->fd, 405, req.keep_alive);
|
||||||
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +314,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
int n = snprintf(real_path, sizeof(real_path), "%s%s", root_normalized, req.path);
|
int n = snprintf(real_path, sizeof(real_path), "%s%s", root_normalized, req.path);
|
||||||
if (n < 0 || (size_t)n >= sizeof(real_path)) {
|
if (n < 0 || (size_t)n >= sizeof(real_path)) {
|
||||||
static_send_error(conn->fd, 400, req.keep_alive);
|
static_send_error(conn->fd, 400, req.keep_alive);
|
||||||
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +324,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
if (!realpath(real_path, resolved) ||
|
if (!realpath(real_path, resolved) ||
|
||||||
strncmp(resolved, root_normalized, strlen(root_normalized)) != 0) {
|
strncmp(resolved, root_normalized, strlen(root_normalized)) != 0) {
|
||||||
static_send_error(conn->fd, 403, req.keep_alive);
|
static_send_error(conn->fd, 403, req.keep_alive);
|
||||||
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
snprintf(real_path, sizeof(real_path), "%s", resolved);
|
snprintf(real_path, sizeof(real_path), "%s", resolved);
|
||||||
@ -190,6 +334,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
struct stat st;
|
struct stat st;
|
||||||
if (stat(real_path, &st) != 0) {
|
if (stat(real_path, &st) != 0) {
|
||||||
static_send_error(conn->fd, 404, req.keep_alive);
|
static_send_error(conn->fd, 404, req.keep_alive);
|
||||||
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +343,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
char index_path[4096];
|
char index_path[4096];
|
||||||
if (snprintf(index_path, sizeof(index_path), "%s/index.html", real_path) >= (int)sizeof(index_path)) {
|
if (snprintf(index_path, sizeof(index_path), "%s/index.html", real_path) >= (int)sizeof(index_path)) {
|
||||||
static_send_error(conn->fd, 400, req.keep_alive);
|
static_send_error(conn->fd, 400, req.keep_alive);
|
||||||
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
struct stat index_st;
|
struct stat index_st;
|
||||||
@ -206,6 +352,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
http_request_t index_req = req;
|
http_request_t index_req = req;
|
||||||
if (snprintf(index_req.path, sizeof(index_req.path), "%s/index.html", req.path) >= (int)sizeof(index_req.path)) {
|
if (snprintf(index_req.path, sizeof(index_req.path), "%s/index.html", req.path) >= (int)sizeof(index_req.path)) {
|
||||||
static_send_error(conn->fd, 400, req.keep_alive);
|
static_send_error(conn->fd, 400, req.keep_alive);
|
||||||
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
static_serve_file(conn->fd, &index_req, root_dir);
|
static_serve_file(conn->fd, &index_req, root_dir);
|
||||||
@ -220,6 +367,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
static_send_error(conn->fd, 403, req.keep_alive);
|
static_send_error(conn->fd, 403, req.keep_alive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http_request_free(&req);
|
||||||
return req.keep_alive;
|
return req.keep_alive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
static.c
2
static.c
@ -206,7 +206,7 @@ static bool safe_path_join(char *dst, size_t dst_size,
|
|||||||
* @param len 数据长度
|
* @param len 数据长度
|
||||||
* @return 0 成功,-1 失败
|
* @return 0 成功,-1 失败
|
||||||
*/
|
*/
|
||||||
static int send_all(int fd, const char *buf, size_t len) {
|
int send_all(int fd, const char *buf, size_t len) {
|
||||||
size_t sent = 0;
|
size_t sent = 0;
|
||||||
while (sent < len) {
|
while (sent < len) {
|
||||||
ssize_t n = write(fd, buf + sent, len - sent);
|
ssize_t n = write(fd, buf + sent, len - sent);
|
||||||
|
|||||||
13
static.h
13
static.h
@ -10,6 +10,19 @@
|
|||||||
#include "http.h"
|
#include "http.h"
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send_all - 确保缓冲区全部发送
|
||||||
|
*
|
||||||
|
* 使用 write 循环发送,直到全部数据发送完毕或遇到不可恢复错误。
|
||||||
|
* 内部辅助函数,也可被其他模块调用。
|
||||||
|
*
|
||||||
|
* @param fd socket 文件描述符
|
||||||
|
* @param buf 数据缓冲区
|
||||||
|
* @param len 数据长度
|
||||||
|
* @return 0 成功,-1 失败
|
||||||
|
*/
|
||||||
|
int send_all(int fd, const char *buf, size_t len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* static_serve_file - 服务单个静态文件
|
* static_serve_file - 服务单个静态文件
|
||||||
*
|
*
|
||||||
|
|||||||
@ -213,6 +213,52 @@ assert_304_modified_since() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert_post_json() {
|
||||||
|
local url="$1"
|
||||||
|
local body="$2"
|
||||||
|
local expect="$3"
|
||||||
|
local desc="${4:-$url}"
|
||||||
|
local resp
|
||||||
|
resp=$(curl -s -X POST -H "Content-Type: application/json" -d "$body" "$url")
|
||||||
|
if echo "$resp" | grep -q "$expect"; then
|
||||||
|
echo " ✓ $desc — 响应包含 '$expect'"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ $desc — 响应期望包含 '$expect', 实际: $resp"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_post_form() {
|
||||||
|
local url="$1"
|
||||||
|
local body="$2"
|
||||||
|
local expect="$3"
|
||||||
|
local desc="${4:-$url}"
|
||||||
|
local resp
|
||||||
|
resp=$(curl -s -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "$body" "$url")
|
||||||
|
if echo "$resp" | grep -q "$expect"; then
|
||||||
|
echo " ✓ $desc — 响应包含 '$expect'"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ $desc — 响应期望包含 '$expect', 实际: $resp"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_status_405() {
|
||||||
|
local url="$1"
|
||||||
|
local desc="${2:-$url}"
|
||||||
|
local status
|
||||||
|
status=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$url")
|
||||||
|
if [[ "$status" == "405" ]]; then
|
||||||
|
echo " ✓ $desc — PUT 返回 405"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ $desc — 期望 405, 实际 $status"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ===== 测试开始 =====
|
# ===== 测试开始 =====
|
||||||
start_server
|
start_server
|
||||||
|
|
||||||
@ -277,6 +323,12 @@ assert_header_contains "$BASE/app.js" "Content-Type" "application/javascript" "J
|
|||||||
assert_header_contains "$BASE/api.json" "Content-Type" "application/json" "JSON MIME"
|
assert_header_contains "$BASE/api.json" "Content-Type" "application/json" "JSON MIME"
|
||||||
assert_header_contains "$BASE/image.png" "Content-Type" "image/" "PNG MIME"
|
assert_header_contains "$BASE/image.png" "Content-Type" "image/" "PNG MIME"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== POST 请求测试 ==="
|
||||||
|
assert_post_json "$BASE/api/echo" '{"test": "hello"}' '"test"' "POST JSON 回显"
|
||||||
|
assert_post_form "$BASE/api/echo" 'name=cocoon&version=1.0' 'cocoon' "POST 表单回显"
|
||||||
|
assert_status_405 "$BASE/index.html" "PUT 405"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== 结果汇总 ==="
|
echo "=== 结果汇总 ==="
|
||||||
echo "通过: $PASS"
|
echo "通过: $PASS"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user