- on_data_chunk_recv_callback: 将 DATA 帧数据追加到 stream->request.body - on_frame_recv_callback: 根据请求方法选择处理方式(POST → http2_serve_post) - http2_serve_post: 处理 HTTP/2 POST 请求 - 支持 multipart/form-data 文件上传 - 支持 JSON 和 form-urlencoded 回显 - 构建 JSON 响应并通过 nghttp2 发送 - 包含 multipart.h 和 platform.h 到 http2.c - 142 个单元测试 + 77 项集成测试全部通过,编译零警告
1433 lines
49 KiB
C
1433 lines
49 KiB
C
/**
|
||
* http2.c - HTTP/2 支持实现
|
||
*
|
||
* 使用 nghttp2 库实现 HTTP/2 协议支持。
|
||
* 与 cocoon 的协程 I/O 和 TLS 层集成。
|
||
*
|
||
* @author xfy
|
||
*/
|
||
|
||
#include "http2.h"
|
||
#include "static.h"
|
||
#include "log.h"
|
||
#include "tls.h"
|
||
#include "multipart.h"
|
||
#include "platform.h"
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <errno.h>
|
||
#include <fcntl.h>
|
||
#include <sys/stat.h>
|
||
#include <time.h>
|
||
#include <dirent.h>
|
||
#include <zlib.h>
|
||
#include <brotli/encode.h>
|
||
|
||
/* 最大 HTTP/2 会话数 */
|
||
#define MAX_HTTP2_SESSIONS 1024
|
||
|
||
/* 压缩辅助函数(与 static.c 独立,保持模块边界) */
|
||
/**
|
||
* is_compressible_mime - 判断 MIME 类型是否可压缩
|
||
*
|
||
* 检查给定 MIME 类型是否为文本类或可压缩格式。
|
||
* 用于决定是否对响应内容应用 gzip/brotli 压缩。
|
||
*
|
||
* @param mime_type MIME 类型字符串
|
||
* @return true 可压缩,false 不可压缩
|
||
*/
|
||
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 - gzip 压缩数据
|
||
*
|
||
* 使用 zlib 对输入数据进行 gzip 压缩。
|
||
* 如果压缩后大小未明显减小(< 95%),返回 0 表示不压缩更优。
|
||
*
|
||
* @param src 原始数据
|
||
* @param src_len 原始数据长度
|
||
* @param dst 输出缓冲区
|
||
* @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};
|
||
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 编码器对输入数据进行压缩。
|
||
* 通常比 gzip 压缩率更高,CPU 消耗也更大。
|
||
*
|
||
* @param src 原始数据
|
||
* @param src_len 原始数据长度
|
||
* @param dst 输出缓冲区
|
||
* @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,
|
||
BROTLI_DEFAULT_WINDOW,
|
||
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;
|
||
}
|
||
|
||
static http2_session_t *g_sessions[MAX_HTTP2_SESSIONS];
|
||
static int g_session_count = 0;
|
||
|
||
/* 静态文件服务辅助函数 */
|
||
static void format_http_time(time_t t, char *buf, size_t buf_size);
|
||
static void generate_etag(const struct stat *st, char *buf, size_t buf_size);
|
||
static time_t parse_http_time(const char *str);
|
||
static bool match_etag(const char *etag, const char *if_none_match);
|
||
|
||
/* 内部函数声明 */
|
||
static ssize_t send_callback(nghttp2_session *session __attribute__((unused)), const uint8_t *data,
|
||
size_t length, int flags __attribute__((unused)), void *user_data);
|
||
static int on_begin_headers_callback(nghttp2_session *session,
|
||
const nghttp2_frame *frame,
|
||
void *user_data);
|
||
static int on_header_callback(nghttp2_session *session,
|
||
const nghttp2_frame *frame, const uint8_t *name,
|
||
size_t namelen, const uint8_t *value,
|
||
size_t valuelen, uint8_t flags,
|
||
void *user_data);
|
||
static int on_frame_recv_callback(nghttp2_session *session,
|
||
const nghttp2_frame *frame, void *user_data);
|
||
static int on_stream_close_callback(nghttp2_session *session,
|
||
int32_t stream_id, uint32_t error_code,
|
||
void *user_data);
|
||
static int on_data_chunk_recv_callback(nghttp2_session *session,
|
||
uint8_t flags, int32_t stream_id,
|
||
const uint8_t *data,
|
||
size_t len, void *user_data);
|
||
static ssize_t http2_data_source_read_callback(nghttp2_session *session, int32_t stream_id,
|
||
uint8_t *buf, size_t length, uint32_t *data_flags,
|
||
nghttp2_data_source *source, void *user_data);
|
||
static bool http2_serve_directory(http2_session_t *h2, http2_stream_data_t *stream,
|
||
const char *real_path, const char *request_path);
|
||
static void http2_serve_static(http2_session_t *h2, http2_stream_data_t *stream);
|
||
static void http2_serve_post(http2_session_t *h2, http2_stream_data_t *stream);
|
||
|
||
/* ===================== 会话管理 ===================== */
|
||
|
||
/**
|
||
* http2_init - 初始化 HTTP/2 模块
|
||
*
|
||
* 清空会话数组,准备接受新连接。
|
||
*
|
||
* @return 0 成功
|
||
*/
|
||
int http2_init(void) {
|
||
memset(g_sessions, 0, sizeof(g_sessions));
|
||
g_session_count = 0;
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* http2_cleanup - 清理所有 HTTP/2 会话
|
||
*
|
||
* 遍历会话数组,销毁所有存在的会话。
|
||
* 通常在服务器关闭时调用。
|
||
*/
|
||
void http2_cleanup(void) {
|
||
for (int i = 0; i < MAX_HTTP2_SESSIONS; i++) {
|
||
if (g_sessions[i]) {
|
||
http2_session_destroy(g_sessions[i]);
|
||
g_sessions[i] = NULL;
|
||
}
|
||
}
|
||
g_session_count = 0;
|
||
}
|
||
|
||
/**
|
||
* http2_session_create - 创建 HTTP/2 会话
|
||
*
|
||
* 为指定 fd 创建 nghttp2 会话,注册回调函数,发送 SETTINGS 帧。
|
||
* 如果该 fd 已存在会话,先销毁旧会话。
|
||
*
|
||
* @param fd 客户端 socket
|
||
* @param tls_mode 是否为 TLS 模式
|
||
* @return 新会话指针,NULL 失败
|
||
*/
|
||
http2_session_t *http2_session_create(int fd, bool tls_mode) {
|
||
if (fd < 0 || fd >= MAX_HTTP2_SESSIONS) {
|
||
return NULL;
|
||
}
|
||
if (g_sessions[fd] != NULL) {
|
||
/* 已存在,先销毁 */
|
||
http2_session_destroy(g_sessions[fd]);
|
||
}
|
||
|
||
http2_session_t *h2 = calloc(1, sizeof(http2_session_t));
|
||
if (!h2) {
|
||
return NULL;
|
||
}
|
||
|
||
h2->fd = fd;
|
||
h2->tls_mode = tls_mode;
|
||
|
||
/* 创建 nghttp2 会话 */
|
||
nghttp2_session_callbacks *callbacks = NULL;
|
||
if (nghttp2_session_callbacks_new(&callbacks) != 0) {
|
||
free(h2);
|
||
return NULL;
|
||
}
|
||
|
||
nghttp2_session_callbacks_set_send_callback(callbacks, send_callback);
|
||
nghttp2_session_callbacks_set_on_begin_headers_callback(
|
||
callbacks, on_begin_headers_callback);
|
||
nghttp2_session_callbacks_set_on_header_callback(callbacks,
|
||
on_header_callback);
|
||
nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks,
|
||
on_frame_recv_callback);
|
||
nghttp2_session_callbacks_set_on_stream_close_callback(
|
||
callbacks, on_stream_close_callback);
|
||
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
|
||
callbacks, on_data_chunk_recv_callback);
|
||
|
||
if (nghttp2_session_server_new(&h2->session, callbacks, h2) != 0) {
|
||
nghttp2_session_callbacks_del(callbacks);
|
||
free(h2);
|
||
return NULL;
|
||
}
|
||
|
||
nghttp2_session_callbacks_del(callbacks);
|
||
|
||
/* 发送服务器连接前言(SETTINGS 帧) */
|
||
nghttp2_settings_entry iv[] = {
|
||
{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100},
|
||
{NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, (1 << 16) - 1}};
|
||
if (nghttp2_submit_settings(h2->session, NGHTTP2_FLAG_NONE, iv,
|
||
sizeof(iv) / sizeof(iv[0])) != 0) {
|
||
nghttp2_session_del(h2->session);
|
||
free(h2);
|
||
return NULL;
|
||
}
|
||
|
||
g_sessions[fd] = h2;
|
||
g_session_count++;
|
||
|
||
return h2;
|
||
}
|
||
|
||
/**
|
||
* http2_session_destroy - 销毁 HTTP/2 会话
|
||
*
|
||
* 从全局数组移除,释放 nghttp2 会话,关闭所有关联流。
|
||
*
|
||
* @param h2 会话指针
|
||
*/
|
||
void http2_session_destroy(http2_session_t *h2) {
|
||
if (!h2) return;
|
||
|
||
if (h2->fd >= 0 && h2->fd < MAX_HTTP2_SESSIONS) {
|
||
g_sessions[h2->fd] = NULL;
|
||
}
|
||
g_session_count--;
|
||
|
||
if (h2->session) {
|
||
nghttp2_session_del(h2->session);
|
||
}
|
||
|
||
/* 清理所有流 */
|
||
http2_stream_data_t *stream = h2->streams;
|
||
while (stream) {
|
||
http2_stream_data_t *next = stream->next;
|
||
if (stream->file_fd >= 0) {
|
||
close(stream->file_fd);
|
||
}
|
||
free(stream->response_body);
|
||
http_request_free(&stream->request);
|
||
free(stream);
|
||
stream = next;
|
||
}
|
||
|
||
free(h2);
|
||
}
|
||
|
||
/**
|
||
* http2_session_is_http2 - 检查 fd 是否已升级为 HTTP/2
|
||
*
|
||
* @param fd 客户端 socket
|
||
* @return true 是 HTTP/2 连接
|
||
*/
|
||
bool http2_session_is_http2(int fd) {
|
||
if (fd < 0 || fd >= MAX_HTTP2_SESSIONS) return false;
|
||
return g_sessions[fd] != NULL;
|
||
}
|
||
|
||
/**
|
||
* http2_session_get - 获取 fd 对应的 HTTP/2 会话
|
||
*
|
||
* @param fd 客户端 socket
|
||
* @return 会话指针,NULL 不存在
|
||
*/
|
||
http2_session_t *http2_session_get(int fd) {
|
||
if (fd < 0 || fd >= MAX_HTTP2_SESSIONS) return NULL;
|
||
return g_sessions[fd];
|
||
}
|
||
|
||
/**
|
||
* http2_session_set_context - 设置会话的服务上下文
|
||
*
|
||
* 传入 root_dir 和压缩配置,供后续静态文件服务使用。
|
||
*
|
||
* @param h2 会话指针
|
||
* @param root_dir 静态资源根目录
|
||
* @param gzip_enabled 是否启用 gzip
|
||
* @param brotli_enabled 是否启用 brotli
|
||
*/
|
||
void http2_session_set_context(http2_session_t *h2, const char *root_dir, bool gzip_enabled, bool brotli_enabled) {
|
||
if (!h2) return;
|
||
h2->root_dir = root_dir;
|
||
h2->gzip_enabled = gzip_enabled;
|
||
h2->brotli_enabled = brotli_enabled;
|
||
}
|
||
|
||
/**
|
||
* http2_session_upgrade - 将 HTTP/1.1 连接升级为 HTTP/2
|
||
*
|
||
* 通过 nghttp2_session_upgrade2 注册 stream 1,处理已解析的 HTTP/1.1 请求。
|
||
* 用于 h2c 升级(Upgrade: h2c)场景。
|
||
*
|
||
* @param h2 会话指针
|
||
* @param req HTTP/1.1 请求(已解析)
|
||
* @return 0 成功,-1 失败
|
||
*/
|
||
int http2_session_upgrade(http2_session_t *h2, const http_request_t *req) {
|
||
if (!h2 || !h2->session || !req) return -1;
|
||
|
||
/* 创建流数据(stream_id=1) */
|
||
http2_stream_data_t *stream = calloc(1, sizeof(http2_stream_data_t));
|
||
if (!stream) return -1;
|
||
|
||
stream->stream_id = 1;
|
||
stream->file_fd = -1;
|
||
stream->request = *req;
|
||
stream->request.body = NULL; /* 升级请求通常无 body,避免双重释放 */
|
||
stream->request_complete = true;
|
||
|
||
/* 注册升级流到 nghttp2 */
|
||
int is_head = (req->method == HTTP_HEAD) ? 1 : 0;
|
||
if (nghttp2_session_upgrade2(h2->session, NULL, 0, is_head, stream) != 0) {
|
||
free(stream);
|
||
return -1;
|
||
}
|
||
|
||
/* 关联到会话 */
|
||
stream->next = h2->streams;
|
||
h2->streams = stream;
|
||
nghttp2_session_set_stream_user_data(h2->session, 1, stream);
|
||
|
||
/* 提交静态文件响应 */
|
||
http2_serve_static(h2, stream);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* ===================== 数据收发 ===================== */
|
||
|
||
/**
|
||
* http2_recv - 接收 HTTP/2 帧数据
|
||
*
|
||
* 将数据喂给 nghttp2 解析器,并发送任何挂起的输出帧。
|
||
*
|
||
* @param h2 会话指针
|
||
* @param buf 输入数据
|
||
* @param len 数据长度
|
||
* @return 0 成功,-1 失败
|
||
*/
|
||
int http2_recv(http2_session_t *h2, const uint8_t *buf, size_t len) {
|
||
if (!h2 || !h2->session) return -1;
|
||
|
||
ssize_t readlen = nghttp2_session_mem_recv(h2->session, buf, len);
|
||
if (readlen < 0) {
|
||
log_error("HTTP/2 接收错误: %s", nghttp2_strerror((int)readlen));
|
||
return -1;
|
||
}
|
||
|
||
/* 发送任何挂起的输出帧 */
|
||
return http2_send_pending(h2);
|
||
}
|
||
|
||
/**
|
||
* http2_send_pending - 发送挂起的 HTTP/2 输出帧
|
||
*
|
||
* 调用 nghttp2_session_send 处理所有待发送的帧。
|
||
*
|
||
* @param h2 会话指针
|
||
* @return 0 成功,-1 失败
|
||
*/
|
||
int http2_send_pending(http2_session_t *h2) {
|
||
if (!h2 || !h2->session) return -1;
|
||
|
||
int rv = nghttp2_session_send(h2->session);
|
||
if (rv != 0) {
|
||
log_error("HTTP/2 发送错误: %s", nghttp2_strerror(rv));
|
||
return -1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* http2_want_read - 检查会话是否还需要读取数据
|
||
*
|
||
* @param h2 会话指针
|
||
* @return true 需要读取
|
||
*/
|
||
bool http2_want_read(http2_session_t *h2) {
|
||
if (!h2 || !h2->session) return false;
|
||
return nghttp2_session_want_read(h2->session) != 0;
|
||
}
|
||
|
||
/**
|
||
* http2_want_write - 检查会话是否还需要写入数据
|
||
*
|
||
* @param h2 会话指针
|
||
* @return true 需要写入
|
||
*/
|
||
bool http2_want_write(http2_session_t *h2) {
|
||
if (!h2 || !h2->session) return false;
|
||
return nghttp2_session_want_write(h2->session) != 0;
|
||
}
|
||
|
||
/* ===================== nghttp2 回调 ===================== */
|
||
|
||
/**
|
||
* send_callback - nghttp2 发送回调
|
||
*
|
||
* 将 nghttp2 生成的帧数据通过 TLS 或原始 socket 发送。
|
||
* 这是 nghttp2 与 cocoon I/O 层之间的桥接。
|
||
*
|
||
* @param session nghttp2 会话(未使用)
|
||
* @param data 待发送数据
|
||
* @param length 数据长度
|
||
* @param flags 标志(未使用)
|
||
* @param user_data 用户数据(http2_session_t)
|
||
* @return 发送字节数,或 NGHTTP2_ERR_CALLBACK_FAILURE
|
||
*/
|
||
static ssize_t send_callback(nghttp2_session *session __attribute__((unused)),
|
||
const uint8_t *data, size_t length,
|
||
int flags __attribute__((unused)),
|
||
void *user_data) {
|
||
http2_session_t *h2 = (http2_session_t *)user_data;
|
||
|
||
/* 使用 TLS 或原始 socket 发送 */
|
||
ssize_t sent;
|
||
if (h2->tls_mode && tls_has_connection(h2->fd)) {
|
||
sent = tls_write(h2->fd, (const char *)data, length);
|
||
} else {
|
||
if (send_all(h2->fd, (const char *)data, length) != 0) {
|
||
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
||
}
|
||
sent = (ssize_t)length;
|
||
}
|
||
|
||
if (sent < 0) {
|
||
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
||
}
|
||
return (int)sent;
|
||
}
|
||
|
||
/**
|
||
* on_begin_headers_callback - 新流开始时创建流数据
|
||
*
|
||
* 当 nghttp2 开始接收 HEADERS 帧时创建 http2_stream_data_t,
|
||
* 插入会话链表并关联到 nghttp2 流。
|
||
*
|
||
* @param session nghttp2 会话
|
||
* @param frame 当前帧
|
||
* @param user_data 会话指针
|
||
* @return 0 成功,NGHTTP2_ERR_CALLBACK_FAILURE 失败
|
||
*/
|
||
static int on_begin_headers_callback(nghttp2_session *session __attribute__((unused)),
|
||
const nghttp2_frame *frame,
|
||
void *user_data) {
|
||
http2_session_t *h2 = (http2_session_t *)user_data;
|
||
|
||
if (frame->hd.type != NGHTTP2_HEADERS ||
|
||
frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
|
||
return 0;
|
||
}
|
||
|
||
/* 创建新流数据 */
|
||
http2_stream_data_t *stream = calloc(1, sizeof(http2_stream_data_t));
|
||
if (!stream) {
|
||
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
||
}
|
||
|
||
stream->stream_id = frame->hd.stream_id;
|
||
stream->file_fd = -1;
|
||
|
||
/* 插入链表头部 */
|
||
stream->next = h2->streams;
|
||
h2->streams = stream;
|
||
|
||
/* 关联到 nghttp2 流 */
|
||
nghttp2_session_set_stream_user_data(session, frame->hd.stream_id, stream);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* on_header_callback - 解析请求头字段
|
||
*
|
||
* 处理 HTTP/2 伪头(:path, :method)和普通头字段。
|
||
* 提取缓存相关头(If-None-Match, If-Modified-Since)。
|
||
*
|
||
* @param session nghttp2 会话
|
||
* @param frame 当前帧
|
||
* @param name 头名
|
||
* @param namelen 头名长度
|
||
* @param value 头值
|
||
* @param valuelen 头值长度
|
||
* @param flags 标志(未使用)
|
||
* @param user_data 未使用
|
||
* @return 0 成功
|
||
*/
|
||
static int on_header_callback(nghttp2_session *session,
|
||
const nghttp2_frame *frame, const uint8_t *name,
|
||
size_t namelen, const uint8_t *value,
|
||
size_t valuelen, uint8_t flags __attribute__((unused)),
|
||
void *user_data __attribute__((unused))) {
|
||
if (frame->hd.type != NGHTTP2_HEADERS ||
|
||
frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
|
||
return 0;
|
||
}
|
||
|
||
http2_stream_data_t *stream =
|
||
nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
|
||
if (!stream) {
|
||
return 0;
|
||
}
|
||
|
||
/* 收集伪头和普通头 */
|
||
if (namelen == 5 && memcmp(":path", name, 5) == 0) {
|
||
/* 解析路径(可能包含 query string) */
|
||
size_t path_len = valuelen;
|
||
for (size_t i = 0; i < valuelen; i++) {
|
||
if (value[i] == '?') {
|
||
path_len = i;
|
||
break;
|
||
}
|
||
}
|
||
if (path_len > 0) {
|
||
size_t copy_len = path_len < sizeof(stream->request.path) - 1
|
||
? path_len
|
||
: sizeof(stream->request.path) - 1;
|
||
memcpy(stream->request.path, value, copy_len);
|
||
stream->request.path[copy_len] = '\0';
|
||
} else {
|
||
stream->request.path[0] = '/';
|
||
stream->request.path[1] = '\0';
|
||
}
|
||
} else if (namelen == 7 && memcmp(":method", name, 7) == 0) {
|
||
if (valuelen == 3 && memcmp("GET", value, 3) == 0) {
|
||
stream->request.method = HTTP_GET;
|
||
} else if (valuelen == 4 && memcmp("HEAD", value, 4) == 0) {
|
||
stream->request.method = HTTP_HEAD;
|
||
} else if (valuelen == 4 && memcmp("POST", value, 4) == 0) {
|
||
stream->request.method = HTTP_POST;
|
||
} else {
|
||
stream->request.method = HTTP_UNKNOWN;
|
||
}
|
||
} else if (namelen == 7 && memcmp(":scheme", name, 7) == 0) {
|
||
/* 忽略 scheme */
|
||
} else if (namelen == 10 && memcmp(":authority", name, 10) == 0) {
|
||
/* 忽略 authority,host 可以从 path 推断 */
|
||
} else {
|
||
/* 普通头字段 */
|
||
if (stream->request.num_headers < HTTP_MAX_HEADERS) {
|
||
int idx = stream->request.num_headers;
|
||
memcpy(stream->request.headers[idx].name, name,
|
||
namelen < sizeof(stream->request.headers[idx].name) - 1 ? namelen : sizeof(stream->request.headers[idx].name) - 1);
|
||
memcpy(stream->request.headers[idx].value, value,
|
||
valuelen < sizeof(stream->request.headers[idx].value) - 1 ? valuelen : sizeof(stream->request.headers[idx].value) - 1);
|
||
stream->request.num_headers++;
|
||
|
||
/* 设置缓存相关标志 */
|
||
if (namelen == 13 && memcmp("if-none-match", name, 13) == 0) {
|
||
stream->request.has_if_none_match = true;
|
||
size_t copy_len = valuelen < sizeof(stream->request.if_none_match) - 1
|
||
? valuelen : sizeof(stream->request.if_none_match) - 1;
|
||
memcpy(stream->request.if_none_match, value, copy_len);
|
||
stream->request.if_none_match[copy_len] = '\0';
|
||
} else if (namelen == 17 && memcmp("if-modified-since", name, 17) == 0) {
|
||
stream->request.has_if_modified_since = true;
|
||
size_t copy_len = valuelen < sizeof(stream->request.if_modified_since) - 1
|
||
? valuelen : sizeof(stream->request.if_modified_since) - 1;
|
||
memcpy(stream->request.if_modified_since, value, copy_len);
|
||
stream->request.if_modified_since[copy_len] = '\0';
|
||
}
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* on_frame_recv_callback - 帧接收完成回调
|
||
*
|
||
* 当收到 END_STREAM 标志时,标记请求完整并触发静态文件服务。
|
||
*
|
||
* @param session nghttp2 会话
|
||
* @param frame 当前帧
|
||
* @param user_data 会话指针
|
||
* @return 0 成功
|
||
*/
|
||
static int on_frame_recv_callback(nghttp2_session *session,
|
||
const nghttp2_frame *frame, void *user_data) {
|
||
http2_session_t *h2 = (http2_session_t *)user_data;
|
||
|
||
switch (frame->hd.type) {
|
||
case NGHTTP2_DATA:
|
||
case NGHTTP2_HEADERS:
|
||
/* 检查请求是否完整(END_STREAM) */
|
||
if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
|
||
http2_stream_data_t *stream =
|
||
nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
|
||
if (!stream) {
|
||
return 0;
|
||
}
|
||
stream->request_complete = true;
|
||
|
||
/* 根据请求方法选择处理方式 */
|
||
if (stream->request.method == HTTP_POST) {
|
||
http2_serve_post(h2, stream);
|
||
} else {
|
||
/* 处理静态文件请求(GET/HEAD/OPTIONS等) */
|
||
http2_serve_static(h2, stream);
|
||
}
|
||
http2_send_pending(h2);
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* on_stream_close_callback - 流关闭回调
|
||
*
|
||
* 从链表移除流,释放资源(关闭文件、释放 body、释放请求)。
|
||
*
|
||
* @param session nghttp2 会话(未使用)
|
||
* @param stream_id 流 ID
|
||
* @param error_code 错误码(未使用)
|
||
* @param user_data 会话指针
|
||
* @return 0 成功
|
||
*/
|
||
static int on_stream_close_callback(nghttp2_session *session __attribute__((unused)),
|
||
int32_t stream_id, uint32_t error_code __attribute__((unused)),
|
||
void *user_data) {
|
||
http2_session_t *h2 = (http2_session_t *)user_data;
|
||
|
||
/* 从链表中移除并释放 */
|
||
http2_stream_data_t **pp = &h2->streams;
|
||
while (*pp) {
|
||
if ((*pp)->stream_id == stream_id) {
|
||
http2_stream_data_t *to_free = *pp;
|
||
*pp = to_free->next;
|
||
|
||
if (to_free->file_fd >= 0) {
|
||
close(to_free->file_fd);
|
||
}
|
||
free(to_free->response_body);
|
||
http_request_free(&to_free->request);
|
||
free(to_free);
|
||
return 0;
|
||
}
|
||
pp = &(*pp)->next;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* on_data_chunk_recv_callback - DATA 帧数据块接收回调
|
||
*
|
||
* 目前为占位实现,TODO:将请求体数据追加到流缓冲区。
|
||
*
|
||
* @param session nghttp2 会话(未使用)
|
||
* @param flags 标志(未使用)
|
||
* @param stream_id 流 ID
|
||
* @param data 数据指针
|
||
* @param len 数据长度
|
||
* @param user_data 未使用
|
||
* @return 0 成功
|
||
*/
|
||
static int on_data_chunk_recv_callback(nghttp2_session *session __attribute__((unused)),
|
||
uint8_t flags __attribute__((unused)),
|
||
int32_t stream_id, const uint8_t *data,
|
||
size_t len, void *user_data __attribute__((unused))) {
|
||
http2_stream_data_t *stream =
|
||
nghttp2_session_get_stream_user_data(session, stream_id);
|
||
if (!stream) {
|
||
return 0;
|
||
}
|
||
|
||
/* 收集请求体数据 */
|
||
if (stream->request.body_len + len > HTTP_MAX_BODY) {
|
||
return 0; /* 超过最大限制,静默丢弃 */
|
||
}
|
||
char *new_body = (char *)realloc(stream->request.body, stream->request.body_len + len + 1);
|
||
if (!new_body) {
|
||
return 0; /* 内存不足,静默丢弃 */
|
||
}
|
||
memcpy(new_body + stream->request.body_len, data, len);
|
||
stream->request.body_len += len;
|
||
new_body[stream->request.body_len] = '\0';
|
||
stream->request.body = new_body;
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* ===================== HTTP/2 静态文件服务 ===================== */
|
||
|
||
/**
|
||
* format_http_time - 格式化 HTTP 时间字符串
|
||
*
|
||
* 将 time_t 转换为 "Mon, 01 Jan 2000 00:00: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 字符串
|
||
*
|
||
* 根据文件大小和修改时间生成强 ETag。
|
||
*
|
||
* @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);
|
||
}
|
||
|
||
/**
|
||
* parse_http_time - 解析 HTTP 时间字符串
|
||
*
|
||
* 支持三种格式:RFC 1123、RFC 850、ANSI C's asctime()。
|
||
*
|
||
* @param str 时间字符串
|
||
* @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;
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
const char *client = if_none_match;
|
||
if (strncmp(client, "W/", 2) == 0) client += 2;
|
||
return strcmp(client, etag) == 0;
|
||
}
|
||
|
||
/**
|
||
* http2_data_source_read_callback - nghttp2 数据读取回调
|
||
*
|
||
* 从流的 response_body 缓冲区读取数据,供 nghttp2 发送 DATA 帧。
|
||
* 数据发完后设置 EOF 标志。
|
||
*
|
||
* @param session nghttp2 会话(未使用)
|
||
* @param stream_id 流 ID(未使用)
|
||
* @param buf 输出缓冲区
|
||
* @param length 最大读取长度
|
||
* @param data_flags 输出标志(设置 EOF)
|
||
* @param source 数据源(含 stream 指针)
|
||
* @param user_data 未使用
|
||
* @return 读取字节数
|
||
*/
|
||
static ssize_t http2_data_source_read_callback(
|
||
nghttp2_session *session __attribute__((unused)),
|
||
int32_t stream_id __attribute__((unused)),
|
||
uint8_t *buf, size_t length,
|
||
uint32_t *data_flags,
|
||
nghttp2_data_source *source,
|
||
void *user_data __attribute__((unused))) {
|
||
|
||
http2_stream_data_t *stream = (http2_stream_data_t *)source->ptr;
|
||
if (!stream || !stream->response_body) {
|
||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||
return 0;
|
||
}
|
||
|
||
size_t remaining = stream->response_len - stream->response_sent;
|
||
if (remaining == 0) {
|
||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||
return 0;
|
||
}
|
||
|
||
size_t to_send = length < remaining ? length : remaining;
|
||
memcpy(buf, stream->response_body + stream->response_sent, to_send);
|
||
stream->response_sent += to_send;
|
||
|
||
if (stream->response_sent >= stream->response_len) {
|
||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||
}
|
||
return (ssize_t)to_send;
|
||
}
|
||
|
||
/**
|
||
* http2_serve_directory - 生成 HTTP/2 目录浏览页面
|
||
*
|
||
* 读取目录内容,生成 HTML 列表,通过 nghttp2 发送响应。
|
||
* 支持文件大小格式化、修改时间显示、HTML 转义、上级目录链接。
|
||
*
|
||
* @param h2 会话指针
|
||
* @param stream 流数据
|
||
* @param real_path 真实目录路径
|
||
* @param request_path 请求路径(用于显示)
|
||
* @return true 成功处理,false 不是目录或出错
|
||
*/
|
||
static bool http2_serve_directory(http2_session_t *h2, http2_stream_data_t *stream,
|
||
const char *real_path, const char *request_path) {
|
||
struct stat st;
|
||
if (stat(real_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||
return false;
|
||
}
|
||
|
||
DIR *dir = opendir(real_path);
|
||
if (!dir) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"403", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return true;
|
||
}
|
||
|
||
/* 收集所有目录项 */
|
||
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 = (char *)malloc(65536);
|
||
if (!html) {
|
||
for (int i = 0; i < num_entries; i++) free(entries[i]);
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"500", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return true;
|
||
}
|
||
|
||
int n = snprintf(html, 65536,
|
||
"<!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",
|
||
request_path, request_path);
|
||
|
||
/* 添加返回上级链接 */
|
||
if (strcmp(request_path, "/") != 0) {
|
||
n += snprintf(html + n, 65536 - n,
|
||
"<tr><td><a href=\"../\">../</a></td><td>-</td><td>-</td></tr>\n");
|
||
}
|
||
|
||
/* HTML 转义辅助 */
|
||
auto 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';
|
||
}
|
||
|
||
/* 添加目录项 */
|
||
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);
|
||
}
|
||
}
|
||
|
||
char escaped_name[512];
|
||
html_escape(entries[i], escaped_name, sizeof(escaped_name));
|
||
|
||
n += snprintf(html + n, 65536 - 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, 65536 - n,
|
||
"</table>\n"
|
||
"<hr>\n"
|
||
"<p><em>Cocoon Server</em></p>\n"
|
||
"</body></html>\n");
|
||
|
||
/* 存储响应并发送 */
|
||
stream->response_body = html;
|
||
stream->response_len = (size_t)n;
|
||
stream->response_sent = 0;
|
||
|
||
nghttp2_nv hdrs[8];
|
||
int num_hdrs = 0;
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)":status", (uint8_t *)"200", 7, 3, 0};
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"content-type", (uint8_t *)"text/html; charset=utf-8", 12, 24, 0};
|
||
|
||
char content_length_str[32];
|
||
snprintf(content_length_str, sizeof(content_length_str), "%d", n);
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"content-length", (uint8_t *)content_length_str, 14, strlen(content_length_str), 0};
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"server", (uint8_t *)"Cocoon/1.0", 6, 10, 0};
|
||
|
||
if (stream->request.method == HTTP_HEAD) {
|
||
free(stream->response_body);
|
||
stream->response_body = NULL;
|
||
stream->response_len = 0;
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, NULL);
|
||
} else {
|
||
nghttp2_data_provider provider;
|
||
provider.source.ptr = stream;
|
||
provider.read_callback = http2_data_source_read_callback;
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, &provider);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* http2_serve_post - 处理 HTTP/2 POST 请求
|
||
*
|
||
* 支持 multipart/form-data 文件上传、JSON 回显和表单回显。
|
||
* 构建 JSON 响应并通过 nghttp2 发送。
|
||
*
|
||
* @param h2 会话指针
|
||
* @param stream 流数据(含请求信息)
|
||
*/
|
||
static void http2_serve_post(http2_session_t *h2, http2_stream_data_t *stream) {
|
||
char response[4096];
|
||
int n = 0;
|
||
|
||
/* multipart/form-data 文件上传 */
|
||
if (strstr(stream->request.content_type, "multipart/form-data") != NULL) {
|
||
char boundary[256];
|
||
if (!multipart_extract_boundary(stream->request.content_type, boundary, sizeof(boundary))) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"400", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
multipart_part_t *parts = NULL;
|
||
int num_parts = 0;
|
||
if (multipart_parse(stream->request.body, stream->request.body_len, boundary, &parts, &num_parts) != 0) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"400", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
n += snprintf(response + n, sizeof(response) - n,
|
||
"{\"method\":\"%s\",\"path\":\"%s\",\"uploaded\":%d,\"files\":[",
|
||
http_method_str(stream->request.method), stream->request.path, num_parts);
|
||
|
||
int files_saved = 0;
|
||
for (int i = 0; i < num_parts; i++) {
|
||
if (parts[i].filename && parts[i].filename[0] && parts[i].data_len > 0) {
|
||
char upload_dir[4096];
|
||
int r = snprintf(upload_dir, sizeof(upload_dir), "%s/uploads", h2->root_dir);
|
||
if (r > 0 && r < (int)sizeof(upload_dir)) {
|
||
cocoon_mkdir(upload_dir);
|
||
char file_path[4096];
|
||
r = snprintf(file_path, sizeof(file_path), "%s/%s", upload_dir, parts[i].filename);
|
||
if (r > 0 && r < (int)sizeof(file_path)) {
|
||
FILE *fp = fopen(file_path, "wb");
|
||
if (fp) {
|
||
fwrite(parts[i].data, 1, parts[i].data_len, fp);
|
||
fclose(fp);
|
||
files_saved++;
|
||
if (files_saved > 1) {
|
||
n += snprintf(response + n, sizeof(response) - n, ",");
|
||
}
|
||
n += snprintf(response + n, sizeof(response) - n,
|
||
"{\"field\":\"%s\",\"filename\":\"%s\",\"size\":%zu,\"path\":\"%s\"}",
|
||
parts[i].name ? parts[i].name : "",
|
||
parts[i].filename,
|
||
parts[i].data_len,
|
||
file_path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
n += snprintf(response + n, sizeof(response) - n, "]}");
|
||
multipart_parts_free(parts, num_parts);
|
||
} else {
|
||
/* JSON / form-urlencoded 回显 */
|
||
n += snprintf(response + n, sizeof(response) - n,
|
||
"{\"method\":\"%s\",\"path\":\"%s\",\"content_type\":\"%s\",\"body_length\":%zu",
|
||
http_method_str(stream->request.method), stream->request.path,
|
||
stream->request.content_type, stream->request.body_len);
|
||
|
||
if (stream->request.body_len > 0) {
|
||
if (strstr(stream->request.content_type, "application/json") != NULL) {
|
||
n += snprintf(response + n, sizeof(response) - n, ",\"body\": ");
|
||
size_t body_copy = stream->request.body_len;
|
||
if (body_copy > sizeof(response) - n - 64) {
|
||
body_copy = sizeof(response) - n - 64;
|
||
}
|
||
memcpy(response + n, stream->request.body, body_copy);
|
||
n += (int)body_copy;
|
||
n += snprintf(response + n, sizeof(response) - n, "}");
|
||
} else if (strstr(stream->request.content_type, "x-www-form-urlencoded") != NULL) {
|
||
n += snprintf(response + n, sizeof(response) - n, ",\"body\":\"");
|
||
size_t body_copy = stream->request.body_len;
|
||
if (body_copy > sizeof(response) - n - 64) {
|
||
body_copy = sizeof(response) - n - 64;
|
||
}
|
||
memcpy(response + n, stream->request.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, "}");
|
||
}
|
||
}
|
||
|
||
/* 存储响应到流 */
|
||
stream->response_body = (char *)malloc(n);
|
||
if (!stream->response_body) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"500", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
memcpy(stream->response_body, response, n);
|
||
stream->response_len = (size_t)n;
|
||
stream->response_sent = 0;
|
||
|
||
/* 构建响应头 */
|
||
nghttp2_nv hdrs[8];
|
||
int num_hdrs = 0;
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)":status", (uint8_t *)"200", 7, 3, 0};
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"content-type", (uint8_t *)"application/json", 12, 16, 0};
|
||
|
||
char content_length_str[32];
|
||
snprintf(content_length_str, sizeof(content_length_str), "%d", n);
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"content-length", (uint8_t *)content_length_str, 14, strlen(content_length_str), 0};
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"server", (uint8_t *)"Cocoon/1.0", 6, 10, 0};
|
||
|
||
nghttp2_data_provider provider;
|
||
provider.source.ptr = stream;
|
||
provider.read_callback = http2_data_source_read_callback;
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, &provider);
|
||
}
|
||
|
||
/**
|
||
* http2_serve_static - 处理 HTTP/2 静态文件请求
|
||
*
|
||
* 路径安全检查、目录处理(index.html 或目录浏览)、缓存协商(304)、
|
||
* 文件读取、压缩(brotli/gzip)、响应构建。
|
||
* 支持 HEAD 请求(不发送 body)。
|
||
*
|
||
* @param h2 会话指针
|
||
* @param stream 流数据(含请求信息)
|
||
*/
|
||
static void http2_serve_static(http2_session_t *h2, http2_stream_data_t *stream) {
|
||
if (!h2->root_dir) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"503", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 构建真实路径 */
|
||
char real_path[4096];
|
||
char root_normalized[4096];
|
||
|
||
if (!realpath(h2->root_dir, root_normalized)) {
|
||
snprintf(root_normalized, sizeof(root_normalized), "%s", h2->root_dir);
|
||
}
|
||
|
||
int n = snprintf(real_path, sizeof(real_path), "%s%s", root_normalized, stream->request.path);
|
||
if (n < 0 || (size_t)n >= sizeof(real_path)) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"400", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 路径遍历检查 */
|
||
if (strstr(stream->request.path, "..") != NULL) {
|
||
char resolved[4096];
|
||
if (!realpath(real_path, resolved) ||
|
||
strncmp(resolved, root_normalized, strlen(root_normalized)) != 0) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"403", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
snprintf(real_path, sizeof(real_path), "%s", resolved);
|
||
}
|
||
|
||
struct stat st;
|
||
if (stat(real_path, &st) != 0) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"404", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 目录:尝试 index.html,否则生成目录列表 */
|
||
if (S_ISDIR(st.st_mode)) {
|
||
char index_path[4096];
|
||
size_t real_len = strlen(real_path);
|
||
if (real_len + 12 < sizeof(index_path)) {
|
||
snprintf(index_path, sizeof(index_path), "%s/index.html", real_path);
|
||
struct stat index_st;
|
||
if (stat(index_path, &index_st) == 0 && S_ISREG(index_st.st_mode)) {
|
||
snprintf(real_path, sizeof(real_path), "%s", index_path);
|
||
stat(real_path, &st);
|
||
} else {
|
||
/* 生成目录浏览页面 */
|
||
if (http2_serve_directory(h2, stream, real_path, stream->request.path)) {
|
||
return;
|
||
}
|
||
}
|
||
} else {
|
||
if (http2_serve_directory(h2, stream, real_path, stream->request.path)) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!S_ISREG(st.st_mode)) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"403", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 生成 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-None-Match */
|
||
if (stream->request.has_if_none_match && match_etag(etag, stream->request.if_none_match)) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"304", 7, 3, 0},
|
||
{(uint8_t *)"etag", (uint8_t *)etag, 4, strlen(etag), 0},
|
||
{(uint8_t *)"last-modified", (uint8_t *)last_modified, 13, strlen(last_modified), 0},
|
||
};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 3, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 检查 If-Modified-Since */
|
||
if (stream->request.has_if_modified_since) {
|
||
time_t client_time = parse_http_time(stream->request.if_modified_since);
|
||
if (client_time >= 0 && st.st_mtime <= client_time) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"304", 7, 3, 0},
|
||
{(uint8_t *)"etag", (uint8_t *)etag, 4, strlen(etag), 0},
|
||
{(uint8_t *)"last-modified", (uint8_t *)last_modified, 13, strlen(last_modified), 0},
|
||
};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 3, NULL);
|
||
return;
|
||
}
|
||
}
|
||
|
||
/* 打开文件 */
|
||
int file_fd = open(real_path, O_RDONLY);
|
||
if (file_fd < 0) {
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"403", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 读取文件内容到内存 */
|
||
int64_t file_size = st.st_size;
|
||
char *file_buf = (char *)malloc((size_t)file_size);
|
||
if (!file_buf) {
|
||
close(file_fd);
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"500", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
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;
|
||
}
|
||
close(file_fd);
|
||
|
||
if (read_total != file_size) {
|
||
free(file_buf);
|
||
nghttp2_nv hdrs[] = {
|
||
{(uint8_t *)":status", (uint8_t *)"500", 7, 3, 0}};
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 判断是否需要压缩 */
|
||
bool use_gzip = false;
|
||
bool use_brotli = false;
|
||
const char *mime = http_mime_type(real_path);
|
||
|
||
if (stream->request.method != HTTP_HEAD && is_compressible_mime(mime) && file_size > 256) {
|
||
char *compress_buf = (char *)malloc((size_t)file_size);
|
||
if (compress_buf) {
|
||
if (h2->brotli_enabled) {
|
||
ssize_t cl = brotli_compress(file_buf, (size_t)file_size, compress_buf, (size_t)file_size);
|
||
if (cl > 0) {
|
||
free(file_buf);
|
||
file_buf = compress_buf;
|
||
file_size = cl;
|
||
use_brotli = true;
|
||
} else {
|
||
free(compress_buf);
|
||
}
|
||
} else if (h2->gzip_enabled) {
|
||
ssize_t cl = gzip_compress(file_buf, (size_t)file_size, compress_buf, (size_t)file_size);
|
||
if (cl > 0) {
|
||
free(file_buf);
|
||
file_buf = compress_buf;
|
||
file_size = cl;
|
||
use_gzip = true;
|
||
} else {
|
||
free(compress_buf);
|
||
}
|
||
} else {
|
||
free(compress_buf);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 存储到流 */
|
||
stream->response_body = file_buf;
|
||
stream->response_len = (size_t)file_size;
|
||
stream->response_sent = 0;
|
||
|
||
/* 构建响应头 */
|
||
nghttp2_nv hdrs[16];
|
||
int num_hdrs = 0;
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)":status", (uint8_t *)"200", 7, 3, 0};
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"content-type", (uint8_t *)mime, 12, strlen(mime), 0};
|
||
|
||
char content_length_str[32];
|
||
snprintf(content_length_str, sizeof(content_length_str), "%ld", (long)file_size);
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"content-length", (uint8_t *)content_length_str, 14, strlen(content_length_str), 0};
|
||
|
||
if (use_brotli) {
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"content-encoding", (uint8_t *)"br", 16, 2, 0};
|
||
} else if (use_gzip) {
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"content-encoding", (uint8_t *)"gzip", 16, 4, 0};
|
||
}
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"etag", (uint8_t *)etag, 4, strlen(etag), 0};
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"last-modified", (uint8_t *)last_modified, 13, strlen(last_modified), 0};
|
||
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)"server", (uint8_t *)"Cocoon/1.0", 6, 10, 0};
|
||
|
||
/* HEAD 请求:不发送 body */
|
||
if (stream->request.method == HTTP_HEAD) {
|
||
free(stream->response_body);
|
||
stream->response_body = NULL;
|
||
stream->response_len = 0;
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, NULL);
|
||
} else {
|
||
nghttp2_data_provider provider;
|
||
provider.source.ptr = stream;
|
||
provider.read_callback = http2_data_source_read_callback;
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, &provider);
|
||
}
|
||
}
|
||
|
||
/* ===================== 连接处理 ===================== */
|
||
|
||
/**
|
||
* http2_on_connection_accepted - 新连接接入时初始化 HTTP/2
|
||
*
|
||
* TLS 模式:直接创建 HTTP/2 会话并发送前言。
|
||
* 明文模式:返回 0,由 server.c 在读取前几个字节后检测 PRI 魔术字。
|
||
*
|
||
* @param fd 客户端 socket
|
||
* @param tls_mode 是否为 TLS 模式
|
||
* @return 0 成功,-1 失败
|
||
*/
|
||
int http2_on_connection_accepted(int fd, bool tls_mode) {
|
||
/* 检查是否应启用 HTTP/2 */
|
||
if (!tls_mode) {
|
||
/* 明文模式:需要读取客户端魔术字 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" */
|
||
/* 这由 server.c 在读取前几个字节时检测 */
|
||
return 0;
|
||
}
|
||
|
||
/* TLS 模式:ALPN 已协商为 h2 */
|
||
http2_session_t *h2 = http2_session_create(fd, true);
|
||
if (!h2) {
|
||
return -1;
|
||
}
|
||
|
||
/* 发送服务器连接前言 */
|
||
if (http2_send_pending(h2) != 0) {
|
||
http2_session_destroy(h2);
|
||
return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|