feat(cache): 实现 HTTP 缓存协商(ETag + Last-Modified + 304)

- static: 添加 format_http_time / generate_etag / match_etag / parse_http_time 辅助函数
- static: 静态文件服务生成 ETag(文件大小+修改时间)和 Last-Modified
- static: 实现 If-None-Match 和 If-Modified-Since 缓存协商,命中时返回 304 Not Modified
- http: 请求结构体添加 if_none_match / if_modified_since 字段
- http: 响应头格式化支持 ETag 和 Last-Modified 输出
- http: 重构响应头构建,先输出固定头再追加缓存头

测试验证:curl 带 If-None-Match / If-Modified-Since 均返回 304
This commit is contained in:
xfy911 2026-06-03 17:38:28 +08:00
parent 413288d837
commit f5563381b8
2 changed files with 134 additions and 13 deletions

31
http.c
View File

@ -227,30 +227,37 @@ int http_format_response_header(char *buf, size_t buf_size, const http_response_
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Accept-Ranges: bytes\r\n"
"Content-Range: bytes %ld-%ld/%ld\r\n"
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
"Content-Range: bytes %ld-%ld/%ld\r\n",
resp->status_code, resp->status_text,
resp->content_type ? resp->content_type : "application/octet-stream",
(long)resp->content_length,
(long)resp->range_start, (long)resp->range_end, (long)resp->total_length,
resp->keep_alive ? "keep-alive" : "close");
(long)resp->range_start, (long)resp->range_end, (long)resp->total_length);
} else {
n += snprintf(buf + n, buf_size - n,
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Accept-Ranges: bytes\r\n"
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
"Accept-Ranges: bytes\r\n",
resp->status_code, resp->status_text,
resp->content_type ? resp->content_type : "application/octet-stream",
(long)resp->content_length,
resp->keep_alive ? "keep-alive" : "close");
(long)resp->content_length);
}
/* 缓存头 */
if (resp->etag && resp->etag[0]) {
n += snprintf(buf + n, buf_size - n, "ETag: %s\r\n", resp->etag);
}
if (resp->last_modified && resp->last_modified[0]) {
n += snprintf(buf + n, buf_size - n, "Last-Modified: %s\r\n", resp->last_modified);
}
/* 连接头 + 空行 */
n += snprintf(buf + n, buf_size - n,
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
resp->keep_alive ? "keep-alive" : "close");
if ((size_t)n >= buf_size) return -1;
return n;
}

116
static.c
View File

@ -22,6 +22,75 @@
#include <dirent.h>
#include <errno.h>
/**
* 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 -
*
@ -166,6 +235,49 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir) {
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;
}
}
/* 计算发送范围 */
int64_t file_size = st.st_size;
int64_t send_start = 0;
@ -196,7 +308,9 @@ int static_serve_file(int fd, const http_request_t *req, const char *root_dir) {
.has_range = req->has_range,
.range_start = send_start,
.range_end = send_end,
.total_length = file_size
.total_length = file_size,
.etag = etag,
.last_modified = last_modified
};
char header_buf[1024];