feat(static): 实现静态资源服务模块

- static_serve_file: 文件服务(sendfile 零拷贝 + Range 支持)
- static_serve_directory: 目录浏览(HTML 列表 + 文件大小/时间)
- static_send_error: HTTP 错误响应(400/403/404/416/500)
- safe_path_join: 路径遍历防护
This commit is contained in:
xfy911 2026-06-03 16:45:48 +08:00
parent 0ab3291035
commit d7b9c93e78
2 changed files with 466 additions and 0 deletions

416
static.c Normal file
View File

@ -0,0 +1,416 @@
/**
* static.c -
*
*
* coco I/O API
*
* @author xfy
*/
#include "static.h"
#include "cocoon.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 <dirent.h>
#include <errno.h>
/**
* 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)) {
strncpy(root_normalized, root, sizeof(root_normalized) - 1);
root_normalized[sizeof(root_normalized) - 1] = '\0';
}
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;
}
strncpy(dst, resolved, dst_size - 1);
dst[dst_size - 1] = '\0';
return true;
}
return false;
}
return true;
}
/**
* send_all -
*
* 使 write
*
* @param fd socket
* @param buf
* @param len
* @return 0 -1
*/
static int send_all(int fd, const char *buf, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t 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) {
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);
}
/* 计算发送范围 */
int64_t file_size = st.st_size;
int64_t send_start = 0;
int64_t send_end = file_size - 1;
int status_code = 200;
if (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);
return static_send_error(fd, 416, req->keep_alive);
}
status_code = 206;
}
int64_t send_length = 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 = req->has_range,
.range_start = send_start,
.range_end = send_end,
.total_length = file_size
};
char header_buf[1024];
int header_len = http_format_response_header(header_buf, sizeof(header_buf), &resp);
if (header_len < 0) {
close(file_fd);
return static_send_error(fd, 500, req->keep_alive);
}
/* 发送响应头 */
if (send_all(fd, header_buf, (size_t)header_len) != 0) {
close(file_fd);
return COCOON_ERROR;
}
/* 发送文件内容 */
if (req->method == HTTP_HEAD) {
/* HEAD 请求不发送 body */
close(file_fd);
return COCOON_OK;
}
/* 定位到起始位置 */
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) {
/* 检查目录是否可访问 */
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;
}

50
static.h Normal file
View File

@ -0,0 +1,50 @@
/**
* static.h -
*
* @author xfy
*/
#ifndef COCOON_STATIC_H
#define COCOON_STATIC_H
#include "http.h"
#include <stdbool.h>
/**
* static_serve_file -
*
* Range
* sendfile
*
* @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);
/**
* static_serve_directory - HTML
*
* index.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);
/**
* static_send_error - HTTP
*
* @param fd socket
* @param status_code HTTP 404403
* @param keep_alive
* @return COCOON_OK
*/
int static_send_error(int fd, int status_code, bool keep_alive);
#endif /* COCOON_STATIC_H */