问题根因: 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 请求正常返回
712 lines
23 KiB
C
712 lines
23 KiB
C
|
||
/**
|
||
* 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, "&", 5);
|
||
j += 5;
|
||
}
|
||
break;
|
||
case '<':
|
||
if (j + 4 < dst_size) {
|
||
memcpy(dst + j, "<", 4);
|
||
j += 4;
|
||
}
|
||
break;
|
||
case '>':
|
||
if (j + 4 < dst_size) {
|
||
memcpy(dst + j, ">", 4);
|
||
j += 4;
|
||
}
|
||
break;
|
||
case '"':
|
||
if (j + 6 < dst_size) {
|
||
memcpy(dst + j, """, 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;
|
||
}
|