cocoon/static.c
xfy911 0ebf8b997d fix(server): 修复多线程模式下服务器无法处理请求的问题
问题根因:
1. coco_accept/coco_sleep/coco_timer 在多线程全局调度器下不工作,
   因为 main_sched 的 poll_fd 和 timer_wheel 无人轮询
2. client_handler 协程栈上分配了约 40KB+ 的 http_request_t,
   超出默认 2KB 协程栈,导致栈溢出 SIGSEGV

修复方案:
- 主线程直接 accept_loop(poll 阻塞),不使用协程
- accept 成功后通过 coco_go_with_opts 创建 client_handler 协程,
  显式指定 1MB 栈大小
- 单线程模式保持原有行为不变

测试:
- 59/59 集成测试通过
- 127/127 单元测试通过
- curl 多线程模式 HTTP 请求正常返回
2026-06-05 12:37:40 +08:00

712 lines
23 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* static.c - 静态资源服务实现
*
* 提供文件服务、目录列表、错误响应功能。
* 利用 coco 的 I/O API 实现非阻塞文件传输。
*
* @author xfy
*/
#include "static.h"
#include "cocoon.h"
#include "../coco/include/coco.h"
#include "tls.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <time.h>
#include <dirent.h>
#include <errno.h>
#include <zlib.h>
#include <brotli/encode.h>
/**
* is_compressible_mime - 判断 MIME 类型是否适合压缩
*
* 文本类型通常有很高的压缩率,二进制类型(图片、视频、音频)
* 本身已经压缩过,再压缩浪费时间且效果差。
*
* @param mime_type MIME 类型字符串
* @return true 适合压缩
*/
static bool is_compressible_mime(const char *mime_type) {
if (!mime_type) return false;
return (
strstr(mime_type, "text/") != NULL ||
strstr(mime_type, "application/javascript") != NULL ||
strstr(mime_type, "application/json") != NULL ||
strstr(mime_type, "application/xml") != NULL ||
strstr(mime_type, "application/manifest") != NULL ||
strstr(mime_type, "image/svg") != NULL
);
}
/**
* gzip_compress - 使用 zlib 压缩数据为 gzip 格式
*
* 使用 deflateInit2 的 windowBits = 15 + 16 来生成标准 gzip 头。
*
* @param src 原始数据
* @param src_len 原始数据长度
* @param dst 输出缓冲区(调用者分配,建议大小为 src_len
* @param dst_cap 输出缓冲区容量
* @return 压缩后长度0 表示不需要压缩(压缩后更大),-1 表示错误
*/
static ssize_t gzip_compress(const char *src, size_t src_len,
char *dst, size_t dst_cap) {
z_stream strm = {0};
/* 15 + 16 = gzip 格式 */
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
return -1;
}
strm.avail_in = (uInt)src_len;
strm.next_in = (Bytef *)src;
strm.avail_out = (uInt)dst_cap;
strm.next_out = (Bytef *)dst;
if (deflate(&strm, Z_FINISH) != Z_STREAM_END) {
deflateEnd(&strm);
return -1;
}
size_t compressed_len = dst_cap - strm.avail_out;
deflateEnd(&strm);
/* 如果压缩后更大或差不多,就不压缩了 */
if (compressed_len >= src_len * 0.95) {
return 0;
}
return (ssize_t)compressed_len;
}
/**
* brotli_compress - 使用 Brotli 压缩数据
*
* 使用 Brotli 编码器进行高质量压缩。
*
* @param src 原始数据
* @param src_len 原始数据长度
* @param dst 输出缓冲区(调用者分配,建议大小为 src_len
* @param dst_cap 输出缓冲区容量
* @return 压缩后长度0 表示不需要压缩(压缩后更大),-1 表示错误
*/
static ssize_t brotli_compress(const char *src, size_t src_len,
char *dst, size_t dst_cap) {
size_t encoded_size = dst_cap;
BROTLI_BOOL ok = BrotliEncoderCompress(
BROTLI_DEFAULT_QUALITY, /* 默认质量 11 */
BROTLI_DEFAULT_WINDOW, /* 默认窗口大小 22 */
BROTLI_MODE_GENERIC, /* 通用模式 */
src_len,
(const uint8_t *)src,
&encoded_size,
(uint8_t *)dst
);
if (!ok) return -1;
/* 如果压缩后更大或差不多,就不压缩了 */
if (encoded_size >= src_len * 0.95) {
return 0;
}
return (ssize_t)encoded_size;
}
/**
* format_http_time - 将时间戳格式化为 HTTP 日期字符串
*
* HTTP 日期格式: "Wed, 21 Oct 2015 07:28:00 GMT"
*
* @param t 时间戳(秒)
* @param buf 输出缓冲区
* @param buf_size 缓冲区大小
*/
static void format_http_time(time_t t, char *buf, size_t buf_size) {
struct tm *gmt = gmtime(&t);
if (gmt) {
strftime(buf, buf_size, "%a, %d %b %Y %H:%M:%S GMT", gmt);
} else {
buf[0] = '\0';
}
}
/**
* generate_etag - 基于文件元数据生成 ETag
*
* 格式: "大小-修改时间十六进制"
* 示例: "1024-647a3b2f"
*
* @param st 文件状态结构体
* @param buf 输出缓冲区
* @param buf_size 缓冲区大小
*/
static void generate_etag(const struct stat *st, char *buf, size_t buf_size) {
snprintf(buf, buf_size, "\"%lx-%lx\"", (unsigned long)st->st_size, (unsigned long)st->st_mtime);
}
/**
* match_etag - 比较 ETag 值是否匹配
*
* 支持 W/ 弱匹配前缀和 * 通配符。
*
* @param etag 服务器 ETag
* @param if_none_match 客户端 If-None-Match 值
* @return true 匹配
*/
static bool match_etag(const char *etag, const char *if_none_match) {
if (!etag || !if_none_match) return false;
/* 通配符匹配 */
if (strcmp(if_none_match, "*") == 0) return true;
/* 去除 W/ 前缀比较 */
const char *client = if_none_match;
if (strncmp(client, "W/", 2) == 0) client += 2;
return strcmp(client, etag) == 0;
}
/**
* parse_http_time - 解析 HTTP 日期字符串为时间戳
*
* 支持 RFC 1123 / RFC 850 / ANSI C 格式。
*
* @param str HTTP 日期字符串
* @return 时间戳,解析失败返回 -1
*/
static time_t parse_http_time(const char *str) {
struct tm tm = {0};
if (strptime(str, "%a, %d %b %Y %H:%M:%S GMT", &tm) != NULL ||
strptime(str, "%A, %d-%b-%y %H:%M:%S GMT", &tm) != NULL ||
strptime(str, "%a %b %d %H:%M:%S %Y", &tm) != NULL) {
return timegm(&tm);
}
return -1;
}
/**
* safe_path_join - 安全路径拼接
*
* 防止路径遍历攻击,禁止超出根目录的访问。
*
* @param dst 输出缓冲区
* @param dst_size 缓冲区大小
* @param root 根目录
* @param path 请求路径
* @return true 路径安全false 存在路径遍历风险
*/
static bool safe_path_join(char *dst, size_t dst_size,
const char *root, const char *path) {
if (!dst || !root || !path || dst_size == 0) return false;
/* 先规范化根目录 */
char root_normalized[4096];
if (!realpath(root, root_normalized)) {
snprintf(root_normalized, sizeof(root_normalized), "%s", root);
}
size_t root_len = strlen(root_normalized);
/* 拼接路径 */
int n = snprintf(dst, dst_size, "%s%s", root_normalized, path);
if (n < 0 || (size_t)n >= dst_size) return false;
/* 检查路径遍历 */
if (strstr(path, "..") != NULL) {
/* 使用 realpath 进一步验证 */
char resolved[4096];
if (realpath(dst, resolved)) {
if (strncmp(resolved, root_normalized, root_len) != 0) {
return false;
}
snprintf(dst, dst_size, "%s", resolved);
return true;
}
return false;
}
return true;
}
/**
* send_all - 确保缓冲区全部发送
*
* 使用 write 循环发送,直到全部数据发送完毕或遇到不可恢复错误。
*
* @param fd socket 文件描述符
* @param buf 数据缓冲区
* @param len 数据长度
* @return 0 成功,-1 失败
*/
int send_all(int fd, const char *buf, size_t len) {
if (tls_has_connection(fd)) {
return tls_write(fd, buf, len) == (ssize_t)len ? 0 : -1;
}
size_t sent = 0;
while (sent < len) {
ssize_t n;
if (coco_sched_get_current() != NULL) {
/* 多线程协程模式:使用 coco_write自动 yield 等待) */
int ret = coco_write(fd, buf + sent, len - sent);
if (ret < 0) {
if (ret == COCO_ERROR_WOULD_BLOCK) {
continue;
}
return -1;
}
n = ret;
} else {
n = write(fd, buf + sent, len - sent);
}
if (n < 0) {
if (errno == EAGAIN || errno == EINTR) continue;
return -1;
}
if (n == 0) return -1;
sent += (size_t)n;
}
return 0;
}
/**
* static_send_error - 发送 HTTP 错误响应
*
* 生成简洁的错误页面,包含状态码和状态文本。
*
* @param fd 客户端 socket
* @param status_code HTTP 状态码
* @param keep_alive 是否保持连接
* @return COCOON_OK 成功
*/
int static_send_error(int fd, int status_code, bool keep_alive) {
const char *status_text = "Unknown Error";
switch (status_code) {
case 400: status_text = "Bad Request"; break;
case 403: status_text = "Forbidden"; break;
case 404: status_text = "Not Found"; break;
case 405: status_text = "Method Not Allowed"; break;
case 416: status_text = "Range Not Satisfiable"; break;
case 500: status_text = "Internal Server Error"; break;
}
char body[512];
int body_len = snprintf(body, sizeof(body),
"<!DOCTYPE html>\n"
"<html><head><title>%d %s</title></head>\n"
"<body><h1>%d %s</h1>\n"
"<p>Cocoon Server</p></body></html>\n",
status_code, status_text, status_code, status_text);
char header[512];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 %d %s\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: %d\r\n"
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
status_code, status_text, body_len,
keep_alive ? "keep-alive" : "close");
send_all(fd, header, (size_t)header_len);
send_all(fd, body, (size_t)body_len);
return COCOON_OK;
}
/**
* static_serve_file - 服务单个静态文件
*
* 打开文件,计算内容长度,处理 Range 请求,
* 优先使用 sendfile 零拷贝发送,回退到 read/write 循环。
*
* @param fd 客户端 socket
* @param req HTTP 请求
* @param root_dir 静态资源根目录
* @return COCOON_OK 成功,负值错误码
*/
int static_serve_file(int fd, const http_request_t *req, const char *root_dir, bool gzip_enabled, bool brotli_enabled) {
char real_path[4096];
if (!safe_path_join(real_path, sizeof(real_path), root_dir, req->path)) {
return static_send_error(fd, 403, req->keep_alive);
}
/* 检查文件是否存在且可读 */
struct stat st;
if (stat(real_path, &st) != 0) {
return static_send_error(fd, 404, req->keep_alive);
}
if (!S_ISREG(st.st_mode)) {
return static_send_error(fd, 403, req->keep_alive);
}
/* 打开文件 */
int file_fd = open(real_path, O_RDONLY);
if (file_fd < 0) {
return static_send_error(fd, 403, req->keep_alive);
}
/* 生成 ETag 和 Last-Modified */
char etag[64];
char last_modified[64];
generate_etag(&st, etag, sizeof(etag));
format_http_time(st.st_mtime, last_modified, sizeof(last_modified));
/* 检查缓存协商 */
if (req->has_if_none_match && match_etag(etag, req->if_none_match)) {
close(file_fd);
http_response_t resp = {
.status_code = 304,
.status_text = "Not Modified",
.content_type = http_mime_type(real_path),
.content_length = 0,
.keep_alive = req->keep_alive,
.etag = etag,
.last_modified = last_modified
};
char header_buf[1024];
int header_len = http_format_response_header(header_buf, sizeof(header_buf), &resp);
if (header_len > 0) send_all(fd, header_buf, (size_t)header_len);
return COCOON_OK;
}
if (req->has_if_modified_since) {
time_t client_time = parse_http_time(req->if_modified_since);
if (client_time >= 0 && st.st_mtime <= client_time) {
close(file_fd);
http_response_t resp = {
.status_code = 304,
.status_text = "Not Modified",
.content_type = http_mime_type(real_path),
.content_length = 0,
.keep_alive = req->keep_alive,
.etag = etag,
.last_modified = last_modified
};
char header_buf[1024];
int header_len = http_format_response_header(header_buf, sizeof(header_buf), &resp);
if (header_len > 0) send_all(fd, header_buf, (size_t)header_len);
return COCOON_OK;
}
}
/* 判断压缩方式:优先 brotli回退 gzip */
int64_t file_size = st.st_size;
bool use_gzip = false;
bool use_brotli = false;
char *compress_buf = NULL;
ssize_t compress_len = 0;
if (!req->has_range && req->method != HTTP_HEAD) {
const char *mime = http_mime_type(real_path);
if (is_compressible_mime(mime) && file_size > 256) {
/* 读取文件内容到内存 */
char *file_buf = (char *)malloc((size_t)file_size);
if (file_buf) {
ssize_t read_total = 0;
while (read_total < file_size) {
ssize_t n = read(file_fd, file_buf + read_total, (size_t)(file_size - read_total));
if (n <= 0) break;
read_total += n;
}
if (read_total == file_size) {
compress_buf = (char *)malloc((size_t)file_size);
if (compress_buf) {
/* 优先 brotli */
if (brotli_enabled && req->accept_brotli) {
compress_len = brotli_compress(file_buf, (size_t)file_size, compress_buf, (size_t)file_size);
if (compress_len > 0) use_brotli = true;
}
/* 回退 gzip */
if (!use_brotli && gzip_enabled && req->accept_gzip) {
compress_len = gzip_compress(file_buf, (size_t)file_size, compress_buf, (size_t)file_size);
if (compress_len > 0) use_gzip = true;
}
}
}
free(file_buf);
}
}
}
/* 计算发送范围 */
int64_t send_start = 0;
int64_t send_end = file_size - 1;
int status_code = 200;
if (!use_gzip && !use_brotli && req->has_range) {
send_start = req->range_start;
if (req->range_end >= 0 && req->range_end < file_size) {
send_end = req->range_end;
}
if (send_start >= file_size || send_start > send_end) {
close(file_fd);
free(compress_buf);
return static_send_error(fd, 416, req->keep_alive);
}
status_code = 206;
}
int64_t send_length = (use_gzip || use_brotli) ? compress_len : (send_end - send_start + 1);
/* 构建响应头 */
http_response_t resp = {
.status_code = status_code,
.status_text = status_code == 206 ? "Partial Content" : "OK",
.content_type = http_mime_type(real_path),
.content_length = send_length,
.keep_alive = req->keep_alive,
.has_range = !use_gzip && !use_brotli && req->has_range,
.range_start = send_start,
.range_end = send_end,
.total_length = file_size,
.etag = etag,
.last_modified = last_modified,
.content_encoding = use_brotli ? "br" : (use_gzip ? "gzip" : NULL)
};
char header_buf[1024];
int header_len = http_format_response_header(header_buf, sizeof(header_buf), &resp);
if (header_len < 0) {
close(file_fd);
free(compress_buf);
return static_send_error(fd, 500, req->keep_alive);
}
/* 发送响应头 */
if (send_all(fd, header_buf, (size_t)header_len) != 0) {
close(file_fd);
free(compress_buf);
return COCOON_ERROR;
}
/* 发送文件内容 */
if (req->method == HTTP_HEAD) {
/* HEAD 请求不发送 body */
close(file_fd);
free(compress_buf);
return COCOON_OK;
}
if (use_gzip || use_brotli || tls_has_connection(fd)) {
/* 发送压缩后的数据,或 TLS 模式下的文件内容 */
if (use_gzip || use_brotli) {
send_all(fd, compress_buf, (size_t)compress_len);
free(compress_buf);
} else {
/* 定位到起始位置(文件可能已被压缩读取提前读至末尾) */
lseek(file_fd, send_start, SEEK_SET);
/* TLS 模式:不能使用 sendfile需读取文件后发送 */
char file_buf[65536];
ssize_t remaining = send_length;
while (remaining > 0) {
size_t to_read = (size_t)remaining < sizeof(file_buf) ? (size_t)remaining : sizeof(file_buf);
ssize_t n = read(file_fd, file_buf, to_read);
if (n <= 0) break;
if (send_all(fd, file_buf, (size_t)n) != 0) break;
remaining -= n;
}
}
close(file_fd);
} else {
/* 定位到起始位置 */
if (send_start > 0) {
lseek(file_fd, send_start, SEEK_SET);
}
/* 使用 sendfile 零拷贝发送 */
off_t offset = send_start;
ssize_t remaining = send_length;
while (remaining > 0) {
ssize_t n = sendfile(fd, file_fd, &offset, (size_t)remaining);
if (n < 0) {
if (errno == EAGAIN || errno == EINTR) continue;
/* sendfile 失败,回退到 read/write */
break;
}
if (n == 0) break;
remaining -= n;
}
close(file_fd);
}
return COCOON_OK;
}
/**
* html_escape - HTML 特殊字符转义
*
* 将 &, <, >, " 转义为对应的 HTML 实体,防止 XSS。
*
* @param src 原始字符串
* @param dst 输出缓冲区
* @param dst_size 缓冲区大小
*/
static void html_escape(const char *src, char *dst, size_t dst_size) {
size_t j = 0;
for (size_t i = 0; src[i] && j < dst_size - 1; i++) {
switch (src[i]) {
case '&':
if (j + 5 < dst_size) {
memcpy(dst + j, "&amp;", 5);
j += 5;
}
break;
case '<':
if (j + 4 < dst_size) {
memcpy(dst + j, "&lt;", 4);
j += 4;
}
break;
case '>':
if (j + 4 < dst_size) {
memcpy(dst + j, "&gt;", 4);
j += 4;
}
break;
case '"':
if (j + 6 < dst_size) {
memcpy(dst + j, "&quot;", 6);
j += 6;
}
break;
default:
dst[j++] = src[i];
}
}
dst[j] = '\0';
}
/**
* static_serve_directory - 生成目录浏览页面
*
* 读取目录项,生成美观的 HTML 目录列表,支持排序。
*
* @param fd 客户端 socket
* @param req HTTP 请求
* @param root_dir 静态资源根目录
* @param real_path 文件系统上的真实路径
* @return COCOON_OK 成功,负值错误码
*/
int static_serve_directory(int fd, const http_request_t *req,
const char *root_dir, const char *real_path) {
(void)root_dir; /* 未直接使用real_path 已通过 safe_path_join 处理 */
/* 检查目录是否可访问 */
struct stat st;
if (stat(real_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
return static_send_error(fd, 404, req->keep_alive);
}
DIR *dir = opendir(real_path);
if (!dir) {
return static_send_error(fd, 403, req->keep_alive);
}
/* 先收集所有目录项 */
struct dirent *entry;
char *entries[4096];
int num_entries = 0;
while ((entry = readdir(dir)) != NULL && num_entries < 4096) {
if (entry->d_name[0] == '.') continue; /* 隐藏文件 */
entries[num_entries] = strdup(entry->d_name);
num_entries++;
}
closedir(dir);
/* 构建 HTML */
char html[65536];
int n = snprintf(html, sizeof(html),
"<!DOCTYPE html>\n"
"<html><head>\n"
"<meta charset=\"utf-8\">\n"
"<title>Index of %s</title>\n"
"<style>"
"body{font-family:system-ui,-apple-system,sans-serif;max-width:800px;margin:40px auto;padding:0 20px}"
"h1{border-bottom:1px solid #ddd;padding-bottom:10px}"
"table{width:100%%;border-collapse:collapse}"
"th{text-align:left;padding:8px;border-bottom:2px solid #ddd}"
"td{padding:8px;border-bottom:1px solid #eee}"
"a{text-decoration:none;color:#0066cc}"
"a:hover{text-decoration:underline}"
"</style>\n"
"</head><body>\n"
"<h1>Index of %s</h1>\n"
"<table>\n"
"<tr><th>Name</th><th>Size</th><th>Modified</th></tr>\n",
req->path, req->path);
/* 添加返回上级链接 */
if (strcmp(req->path, "/") != 0) {
n += snprintf(html + n, sizeof(html) - n,
"<tr><td><a href=\"../\">../</a></td><td>-</td><td>-</td></tr>\n");
}
/* 添加目录项 */
for (int i = 0; i < num_entries; i++) {
char full_path[4096];
snprintf(full_path, sizeof(full_path), "%s/%s", real_path, entries[i]);
struct stat entry_st;
char size_str[32] = "-";
char mtime_str[32] = "-";
if (stat(full_path, &entry_st) == 0) {
/* 格式化文件大小 */
if (S_ISDIR(entry_st.st_mode)) {
strncpy(size_str, "-", sizeof(size_str));
} else if (entry_st.st_size < 1024) {
snprintf(size_str, sizeof(size_str), "%ld B", (long)entry_st.st_size);
} else if (entry_st.st_size < 1024 * 1024) {
snprintf(size_str, sizeof(size_str), "%.1f KB", entry_st.st_size / 1024.0);
} else if (entry_st.st_size < 1024 * 1024 * 1024) {
snprintf(size_str, sizeof(size_str), "%.1f MB", entry_st.st_size / (1024.0 * 1024));
} else {
snprintf(size_str, sizeof(size_str), "%.1f GB", entry_st.st_size / (1024.0 * 1024 * 1024));
}
/* 格式化修改时间 */
struct tm *tm_info = localtime(&entry_st.st_mtime);
if (tm_info) {
strftime(mtime_str, sizeof(mtime_str), "%Y-%m-%d %H:%M", tm_info);
}
}
/* HTML 转义文件名 */
char escaped_name[512];
html_escape(entries[i], escaped_name, sizeof(escaped_name));
n += snprintf(html + n, sizeof(html) - n,
"<tr><td><a href=\"%s%s\">%s%s</a></td><td>%s</td><td>%s</td></tr>\n",
escaped_name,
S_ISDIR(entry_st.st_mode) ? "/" : "",
escaped_name,
S_ISDIR(entry_st.st_mode) ? "/" : "",
size_str, mtime_str);
free(entries[i]);
}
n += snprintf(html + n, sizeof(html) - n,
"</table>\n"
"<hr>\n"
"<p><em>Cocoon Server</em></p>\n"
"</body></html>\n");
/* 发送响应 */
char header[512];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\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, html, (size_t)n);
return COCOON_OK;
}