- 新增 proxy_data_source_read_callback:实时从后端读取数据流式转发 - http2_serve_proxy 重构:解析响应头后立即发送,body 通过 DATA 帧实时转发 - 解除 64KB 响应缓冲区限制,支持任意大小响应体 - 流结构新增代理字段(backend_fd/tls_conn/eof/close_backend/backend) - 已缓存 body(recv_headers 预读部分)优先发送,再续读后端 - 连接池归还/关闭自动处理
2142 lines
73 KiB
C
2142 lines
73 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 void http2_serve_proxy(http2_session_t *h2, http2_stream_data_t *stream);
|
||
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);
|
||
}
|
||
if (stream->proxy_backend_fd != COCOON_INVALID_SOCKET) {
|
||
cocoon_socket_close(stream->proxy_backend_fd);
|
||
}
|
||
if (stream->proxy_tls_conn) {
|
||
proxy_tls_close(stream->proxy_tls_conn);
|
||
free(stream->proxy_tls_conn);
|
||
}
|
||
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_set_proxy_config - 设置会话的反向代理配置
|
||
*
|
||
* @param h2 会话指针
|
||
* @param proxy_config 反向代理配置
|
||
* @param client_addr 客户端地址
|
||
*/
|
||
void http2_session_set_proxy_config(http2_session_t *h2, cocoon_proxy_config_t *proxy_config, struct sockaddr_storage *client_addr) {
|
||
if (!h2) return;
|
||
h2->proxy_config = proxy_config;
|
||
h2->client_addr = client_addr;
|
||
}
|
||
|
||
void http2_session_set_server_config(http2_session_t *h2, cocoon_config_t *server_config) {
|
||
if (!h2) return;
|
||
h2->server_config = server_config;
|
||
}
|
||
|
||
/**
|
||
* vhost_match_root_dir - 根据 Host 头匹配虚拟主机根目录
|
||
*
|
||
* @param config 服务器配置
|
||
* @param host Host 头值
|
||
* @return 匹配的 root_dir,未匹配则返回全局 root_dir
|
||
*/
|
||
static const char *vhost_match_root_dir(const cocoon_config_t *config, const char *host) {
|
||
if (!host || !config) return config ? config->root_dir : NULL;
|
||
for (size_t i = 0; i < config->num_vhosts; i++) {
|
||
if (strcmp(config->vhosts[i].server_name, host) == 0) {
|
||
return config->vhosts[i].root_dir;
|
||
}
|
||
}
|
||
return config->root_dir;
|
||
}
|
||
|
||
/**
|
||
* 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->proxy_backend_fd = COCOON_INVALID_SOCKET;
|
||
stream->proxy_tls_conn = NULL;
|
||
stream->proxy_use_https = false;
|
||
stream->proxy_eof = false;
|
||
stream->proxy_close_backend = false;
|
||
stream->proxy_backend = NULL;
|
||
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->proxy_backend_fd = COCOON_INVALID_SOCKET;
|
||
stream->proxy_tls_conn = NULL;
|
||
stream->proxy_use_https = false;
|
||
stream->proxy_eof = false;
|
||
stream->proxy_close_backend = false;
|
||
stream->proxy_backend = NULL;
|
||
|
||
/* 插入链表头部 */
|
||
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 头,供虚拟主机匹配使用 */
|
||
if (stream->request.num_headers < HTTP_MAX_HEADERS) {
|
||
int idx = stream->request.num_headers;
|
||
memcpy(stream->request.headers[idx].name, "Host", 5);
|
||
size_t vlen = valuelen < sizeof(stream->request.headers[idx].value) - 1
|
||
? valuelen : sizeof(stream->request.headers[idx].value) - 1;
|
||
memcpy(stream->request.headers[idx].value, value, vlen);
|
||
stream->request.headers[idx].value[vlen] = '\0';
|
||
stream->request.num_headers++;
|
||
}
|
||
} 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 (h2->proxy_config && proxy_match(h2->proxy_config, stream->request.path)) {
|
||
http2_serve_proxy(h2, stream);
|
||
} else 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 帧数据块接收回调
|
||
*
|
||
* 将请求体数据追加到流缓冲区。支持 Content-Length 和分块传输。
|
||
*
|
||
* @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;
|
||
}
|
||
|
||
/**
|
||
* proxy_data_source_read_callback - 流式代理数据读取回调
|
||
*
|
||
* 从后端 socket/TLS 连接实时读取数据,供 nghttp2 发送 DATA 帧。
|
||
* 实现 HTTP/2 代理响应的流式转发,避免完整缓冲大响应体。
|
||
* 数据读取完毕后关闭后端连接并归还连接池。
|
||
*
|
||
* @param session nghttp2 会话(未使用)
|
||
* @param stream_id 流 ID(未使用)
|
||
* @param buf 输出缓冲区
|
||
* @param length 最大读取长度
|
||
* @param data_flags 输出标志(设置 EOF)
|
||
* @param source 数据源(含 stream 指针)
|
||
* @param user_data 未使用
|
||
* @return 读取字节数,0 表示 EOF,NGHTTP2_ERR_DEFERRED 表示暂时无数据
|
||
*/
|
||
static ssize_t proxy_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->proxy_eof) {
|
||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||
return 0;
|
||
}
|
||
|
||
/* 先返回已缓存的 body(recv_headers 已读取的部分) */
|
||
if (stream->response_body && stream->response_sent < stream->response_len) {
|
||
size_t remaining = stream->response_len - stream->response_sent;
|
||
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) {
|
||
/* 缓存已发完,释放 */
|
||
free(stream->response_body);
|
||
stream->response_body = NULL;
|
||
stream->response_len = 0;
|
||
stream->response_sent = 0;
|
||
}
|
||
return (ssize_t)to_send;
|
||
}
|
||
|
||
ssize_t r = 0;
|
||
if (stream->proxy_use_https && stream->proxy_tls_conn) {
|
||
r = proxy_tls_read(stream->proxy_tls_conn, buf, length);
|
||
} else if (stream->proxy_backend_fd != COCOON_INVALID_SOCKET) {
|
||
r = recv(stream->proxy_backend_fd, buf, length, 0);
|
||
} else {
|
||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||
return 0;
|
||
}
|
||
|
||
if (r > 0) {
|
||
return (ssize_t)r;
|
||
} else if (r == 0) {
|
||
/* 后端关闭连接 */
|
||
stream->proxy_eof = true;
|
||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||
} else {
|
||
if (errno == EAGAIN || errno == EINTR) {
|
||
/* 暂时无数据,让 nghttp2 稍后重试 */
|
||
return NGHTTP2_ERR_DEFERRED;
|
||
}
|
||
/* 读取错误,结束流 */
|
||
stream->proxy_eof = true;
|
||
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||
}
|
||
|
||
/* 关闭后端连接或归还连接池 */
|
||
if (stream->proxy_use_https && stream->proxy_tls_conn) {
|
||
if (stream->proxy_close_backend) {
|
||
proxy_tls_close(stream->proxy_tls_conn);
|
||
free(stream->proxy_tls_conn);
|
||
}
|
||
/* 注意:连接池不支持 HTTPS,所以直接关闭 */
|
||
stream->proxy_tls_conn = NULL;
|
||
} else if (stream->proxy_backend_fd != COCOON_INVALID_SOCKET) {
|
||
if (stream->proxy_close_backend) {
|
||
cocoon_socket_close(stream->proxy_backend_fd);
|
||
} else if (stream->proxy_backend) {
|
||
proxy_pool_release(stream->proxy_backend, stream->proxy_backend_fd, NULL);
|
||
} else {
|
||
cocoon_socket_close(stream->proxy_backend_fd);
|
||
}
|
||
stream->proxy_backend_fd = COCOON_INVALID_SOCKET;
|
||
stream->proxy_backend = NULL;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* 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) {
|
||
/* 虚拟主机匹配:根据 Host 头选择 root_dir */
|
||
const char *host = NULL;
|
||
for (int i = 0; i < stream->request.num_headers; i++) {
|
||
if (strcasecmp(stream->request.headers[i].name, "Host") == 0) {
|
||
host = stream->request.headers[i].value;
|
||
break;
|
||
}
|
||
}
|
||
const char *root_dir = h2->root_dir;
|
||
if (h2->server_config && host) {
|
||
root_dir = vhost_match_root_dir(h2->server_config, host);
|
||
}
|
||
|
||
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", 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);
|
||
}
|
||
|
||
/* ===================== HTTP/2 代理转发 ===================== */
|
||
|
||
/**
|
||
* is_hop_by_hop_header - 判断是否是 hop-by-hop 头
|
||
*
|
||
* 这些头在 HTTP/2 中不允许出现,需要从后端响应中过滤掉。
|
||
*/
|
||
static bool is_hop_by_hop_header(const char *name) {
|
||
return (strcasecmp(name, "connection") == 0 ||
|
||
strcasecmp(name, "keep-alive") == 0 ||
|
||
strcasecmp(name, "transfer-encoding") == 0 ||
|
||
strcasecmp(name, "proxy-connection") == 0 ||
|
||
strcasecmp(name, "upgrade") == 0 ||
|
||
strcasecmp(name, "te") == 0);
|
||
}
|
||
|
||
/**
|
||
* build_forwarded_path - 构建转发路径(复制 proxy.c 逻辑)
|
||
*/
|
||
static void build_forwarded_path(const cocoon_proxy_rule_t *rule,
|
||
const cocoon_proxy_backend_t *backend,
|
||
const char *original_path,
|
||
char *out, size_t out_len) {
|
||
size_t prefix_len = strlen(rule->path_prefix);
|
||
const char *remaining = original_path + prefix_len;
|
||
|
||
if (backend->target_path[0] == '\0') {
|
||
if (remaining[0] == '\0') {
|
||
strncpy(out, "/", out_len - 1);
|
||
} else {
|
||
strncpy(out, remaining, out_len - 1);
|
||
}
|
||
} else {
|
||
int n = snprintf(out, out_len, "%s%s", backend->target_path, remaining);
|
||
if (n < 0 || (size_t)n >= out_len) {
|
||
out[out_len - 1] = '\0';
|
||
}
|
||
}
|
||
out[out_len - 1] = '\0';
|
||
}
|
||
|
||
/**
|
||
* build_xff - 构建 X-Forwarded-For 值
|
||
*/
|
||
static void build_xff(const struct sockaddr_storage *client_addr,
|
||
char *out, size_t out_len) {
|
||
if (client_addr->ss_family == AF_INET) {
|
||
struct sockaddr_in *sin = (struct sockaddr_in *)client_addr;
|
||
inet_ntop(AF_INET, &sin->sin_addr, out, (socklen_t)out_len);
|
||
} else if (client_addr->ss_family == AF_INET6) {
|
||
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)client_addr;
|
||
inet_ntop(AF_INET6, &sin6->sin6_addr, out, (socklen_t)out_len);
|
||
} else {
|
||
strncpy(out, "unknown", out_len - 1);
|
||
out[out_len - 1] = '\0';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* send_all_fd - 确保数据全部通过 socket 发送
|
||
*/
|
||
static int send_all_fd(cocoon_socket_t fd, const char *data, size_t len) {
|
||
size_t sent = 0;
|
||
while (sent < len) {
|
||
ssize_t n = send(fd, data + sent, len - sent, 0);
|
||
if (n < 0) {
|
||
if (errno == EAGAIN || errno == EINTR) continue;
|
||
return -1;
|
||
}
|
||
if (n == 0) return -1;
|
||
sent += (size_t)n;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* proxy_send_all_tls - 通过 TLS 连接发送全部数据
|
||
*/
|
||
static int proxy_send_all_tls(proxy_tls_conn_t *conn, const char *data, size_t len) {
|
||
size_t sent = 0;
|
||
while (sent < len) {
|
||
ssize_t n = proxy_tls_write(conn, data + sent, len - sent);
|
||
if (n > 0) {
|
||
sent += (size_t)n;
|
||
} else if (n < 0) {
|
||
if (errno == EAGAIN || errno == EINTR) continue;
|
||
return -1;
|
||
} else {
|
||
return -1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* recv_headers - 读取 HTTP 响应头(直到 \r\n\r\n)
|
||
*/
|
||
static ssize_t recv_headers(cocoon_socket_t fd, char *buf, size_t max_len) {
|
||
size_t total = 0;
|
||
while (total < max_len) {
|
||
ssize_t n = recv(fd, buf + total, max_len - total, 0);
|
||
if (n > 0) {
|
||
total += (size_t)n;
|
||
if (total >= 4) {
|
||
for (size_t i = 0; i <= total - 4; i++) {
|
||
if (memcmp(buf + i, "\r\n\r\n", 4) == 0) {
|
||
return (ssize_t)total;
|
||
}
|
||
}
|
||
}
|
||
} else if (n == 0) {
|
||
return (ssize_t)total;
|
||
} else {
|
||
if (errno == EAGAIN || errno == EINTR) continue;
|
||
return -1;
|
||
}
|
||
}
|
||
return (ssize_t)total;
|
||
}
|
||
|
||
/**
|
||
* recv_headers_tls - 通过 TLS 读取 HTTP 响应头
|
||
*/
|
||
static ssize_t recv_headers_tls(proxy_tls_conn_t *conn, char *buf, size_t max_len) {
|
||
size_t total = 0;
|
||
while (total < max_len) {
|
||
ssize_t n = proxy_tls_read(conn, buf + total, max_len - total);
|
||
if (n > 0) {
|
||
total += (size_t)n;
|
||
if (total >= 4) {
|
||
for (size_t i = 0; i <= total - 4; i++) {
|
||
if (memcmp(buf + i, "\r\n\r\n", 4) == 0) {
|
||
return (ssize_t)total;
|
||
}
|
||
}
|
||
}
|
||
} else if (n == 0) {
|
||
return (ssize_t)total;
|
||
} else {
|
||
if (errno == EAGAIN || errno == EINTR) continue;
|
||
return -1;
|
||
}
|
||
}
|
||
return (ssize_t)total;
|
||
}
|
||
|
||
/**
|
||
* parse_http1_response - 解析 HTTP/1.1 响应
|
||
*
|
||
* 解析状态行、头、body。body 指向 buf 内部。
|
||
*/
|
||
static bool parse_http1_response(const char *data, size_t len,
|
||
int *status, char *headers, size_t *headers_len,
|
||
const char **body, size_t *body_len) {
|
||
const char *p = data;
|
||
const char *end = data + len;
|
||
|
||
/* 找第一行 \r\n */
|
||
const char *line_end = memmem(p, end - p, "\r\n", 2);
|
||
if (!line_end) return false;
|
||
|
||
/* 解析状态行 */
|
||
if (strncmp(p, "HTTP/1.1 ", 9) != 0 && strncmp(p, "HTTP/1.0 ", 9) != 0) {
|
||
return false;
|
||
}
|
||
|
||
*status = atoi(p + 9);
|
||
|
||
/* 找空行 */
|
||
p = line_end + 2;
|
||
const char *blank = memmem(p, end - p, "\r\n\r\n", 4);
|
||
if (!blank) return false;
|
||
|
||
size_t hdr_len = (size_t)(blank - p);
|
||
if (hdr_len >= *headers_len) hdr_len = *headers_len - 1;
|
||
memcpy(headers, p, hdr_len);
|
||
headers[hdr_len] = '\0';
|
||
*headers_len = hdr_len;
|
||
|
||
*body = blank + 4;
|
||
size_t body_offset = (size_t)(blank + 4 - data);
|
||
if (len > body_offset) {
|
||
*body_len = len - body_offset;
|
||
} else {
|
||
*body_len = 0;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* parse_content_length - 从头中解析 Content-Length
|
||
*/
|
||
static long parse_content_length(const char *headers) {
|
||
const char *p = headers;
|
||
while (*p) {
|
||
if (strncasecmp(p, "content-length:", 15) == 0) {
|
||
p += 15;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
return atol(p);
|
||
}
|
||
const char *nl = strstr(p, "\r\n");
|
||
if (!nl) break;
|
||
p = nl + 2;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/**
|
||
* http2_serve_proxy - 处理 HTTP/2 代理转发
|
||
*
|
||
* 将 HTTP/2 请求转换为 HTTP/1.1 请求发送给后端,
|
||
* 收集完整响应后转换为 HTTP/2 响应发回客户端。
|
||
*
|
||
* @param h2 会话指针
|
||
* @param stream 流数据
|
||
*/
|
||
static void http2_serve_proxy(http2_session_t *h2, http2_stream_data_t *stream) {
|
||
if (!h2->proxy_config) {
|
||
nghttp2_nv hdrs[] = { {(uint8_t *)":status", (uint8_t *)"502", 7, 3, 0} };
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
cocoon_proxy_rule_t *rule = proxy_match(h2->proxy_config, stream->request.path);
|
||
if (!rule || rule->backend_count == 0) {
|
||
nghttp2_nv hdrs[] = { {(uint8_t *)":status", (uint8_t *)"502", 7, 3, 0} };
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 选择后端:优先健康后端 */
|
||
cocoon_proxy_backend_t *backend = NULL;
|
||
for (size_t i = 0; i < rule->backend_count; i++) {
|
||
if (rule->backends[i].healthy) {
|
||
backend = &rule->backends[i];
|
||
break;
|
||
}
|
||
}
|
||
if (!backend) {
|
||
backend = &rule->backends[0];
|
||
}
|
||
|
||
bool use_https = backend->target_https;
|
||
cocoon_socket_t backend_fd = COCOON_INVALID_SOCKET;
|
||
proxy_tls_conn_t *tls_conn = NULL;
|
||
|
||
if (!proxy_pool_acquire(backend, &backend_fd, &tls_conn)) {
|
||
log_warn("HTTP/2 代理无法获取后端连接: %s", stream->request.path);
|
||
nghttp2_nv hdrs[] = { {(uint8_t *)":status", (uint8_t *)"502", 7, 3, 0} };
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 构建转发路径 */
|
||
char forwarded_path[512];
|
||
build_forwarded_path(rule, backend, stream->request.path, forwarded_path, sizeof(forwarded_path));
|
||
|
||
/* 构建 X-Forwarded-For */
|
||
char xff[64] = "unknown";
|
||
if (h2->client_addr) {
|
||
build_xff(h2->client_addr, xff, sizeof(xff));
|
||
}
|
||
|
||
/* 构建 HTTP/1.1 请求 */
|
||
char request_buf[4096];
|
||
int n = snprintf(request_buf, sizeof(request_buf),
|
||
"%s %s HTTP/1.1\r\n"
|
||
"Host: %s:%d\r\n"
|
||
"X-Forwarded-For: %s\r\n"
|
||
"X-Forwarded-Proto: %s\r\n"
|
||
"Connection: keep-alive\r\n",
|
||
http_method_str(stream->request.method),
|
||
forwarded_path,
|
||
backend->target_host,
|
||
backend->target_port,
|
||
xff,
|
||
use_https ? "https" : "http");
|
||
|
||
/* 透传普通请求头(过滤 hop-by-hop) */
|
||
for (int i = 0; i < stream->request.num_headers; i++) {
|
||
if (is_hop_by_hop_header(stream->request.headers[i].name)) continue;
|
||
n += snprintf(request_buf + n, sizeof(request_buf) - n,
|
||
"%s: %s\r\n",
|
||
stream->request.headers[i].name,
|
||
stream->request.headers[i].value);
|
||
}
|
||
|
||
if (stream->request.content_length > 0) {
|
||
n += snprintf(request_buf + n, sizeof(request_buf) - n,
|
||
"Content-Length: %ld\r\n", (long)stream->request.content_length);
|
||
}
|
||
|
||
n += snprintf(request_buf + n, sizeof(request_buf) - n, "\r\n");
|
||
|
||
/* 发送请求头 */
|
||
bool send_ok = true;
|
||
if (use_https) {
|
||
if (proxy_send_all_tls(tls_conn, request_buf, (size_t)n) != 0) send_ok = false;
|
||
} else {
|
||
if (send_all_fd(backend_fd, request_buf, (size_t)n) != 0) send_ok = false;
|
||
}
|
||
|
||
/* 发送请求体 */
|
||
if (send_ok && stream->request.body && stream->request.body_len > 0) {
|
||
if (use_https) {
|
||
if (proxy_send_all_tls(tls_conn, stream->request.body, stream->request.body_len) != 0) send_ok = false;
|
||
} else {
|
||
if (send_all_fd(backend_fd, stream->request.body, stream->request.body_len) != 0) send_ok = false;
|
||
}
|
||
}
|
||
|
||
if (!send_ok) {
|
||
log_warn("HTTP/2 代理发送请求到后端失败: %s", stream->request.path);
|
||
if (use_https && tls_conn) proxy_tls_close(tls_conn);
|
||
else if (backend_fd != COCOON_INVALID_SOCKET) cocoon_socket_close(backend_fd);
|
||
nghttp2_nv hdrs[] = { {(uint8_t *)":status", (uint8_t *)"502", 7, 3, 0} };
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 接收响应头 */
|
||
char response_buf[65536];
|
||
ssize_t received = 0;
|
||
|
||
if (use_https) {
|
||
received = recv_headers_tls(tls_conn, response_buf, sizeof(response_buf));
|
||
} else {
|
||
received = recv_headers(backend_fd, response_buf, sizeof(response_buf));
|
||
}
|
||
|
||
if (received <= 0) {
|
||
log_warn("HTTP/2 代理接收后端响应头失败: %s", stream->request.path);
|
||
if (use_https && tls_conn) proxy_tls_close(tls_conn);
|
||
else if (backend_fd != COCOON_INVALID_SOCKET) cocoon_socket_close(backend_fd);
|
||
nghttp2_nv hdrs[] = { {(uint8_t *)":status", (uint8_t *)"502", 7, 3, 0} };
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 解析响应 */
|
||
int status = 0;
|
||
char headers[32768];
|
||
size_t headers_len = sizeof(headers);
|
||
const char *body = NULL;
|
||
size_t body_len = 0;
|
||
|
||
if (!parse_http1_response(response_buf, (size_t)received, &status, headers, &headers_len, &body, &body_len)) {
|
||
log_warn("HTTP/2 代理解析后端响应失败: %s", stream->request.path);
|
||
if (use_https && tls_conn) proxy_tls_close(tls_conn);
|
||
else if (backend_fd != COCOON_INVALID_SOCKET) cocoon_socket_close(backend_fd);
|
||
nghttp2_nv hdrs[] = { {(uint8_t *)":status", (uint8_t *)"502", 7, 3, 0} };
|
||
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||
return;
|
||
}
|
||
|
||
/* 检查是否需要继续读取 body */
|
||
long content_length = parse_content_length(headers);
|
||
if (content_length > 0) {
|
||
size_t total_body = body_len;
|
||
size_t offset = (size_t)(body - response_buf);
|
||
|
||
while (total_body < (size_t)content_length &&
|
||
offset + total_body < sizeof(response_buf)) {
|
||
ssize_t need = (ssize_t)content_length - (ssize_t)total_body;
|
||
if (need > (ssize_t)(sizeof(response_buf) - offset - total_body)) {
|
||
need = (ssize_t)(sizeof(response_buf) - offset - total_body);
|
||
}
|
||
if (need <= 0) break;
|
||
|
||
ssize_t r;
|
||
if (use_https) {
|
||
r = proxy_tls_read(tls_conn, response_buf + offset + total_body, (size_t)need);
|
||
} else {
|
||
r = recv(backend_fd, response_buf + offset + total_body, (size_t)need, 0);
|
||
}
|
||
if (r > 0) {
|
||
total_body += (size_t)r;
|
||
} else if (r == 0) {
|
||
break;
|
||
} else {
|
||
if (errno == EAGAIN || errno == EINTR) continue;
|
||
break;
|
||
}
|
||
}
|
||
body_len = total_body;
|
||
} else if (content_length < 0) {
|
||
/* 无 Content-Length,尝试读取剩余数据 */
|
||
size_t total_body = body_len;
|
||
size_t offset = (size_t)(body - response_buf);
|
||
while (offset + total_body < sizeof(response_buf)) {
|
||
ssize_t r;
|
||
if (use_https) {
|
||
r = proxy_tls_read(tls_conn, response_buf + offset + total_body, sizeof(response_buf) - offset - total_body);
|
||
} else {
|
||
r = recv(backend_fd, response_buf + offset + total_body, sizeof(response_buf) - offset - total_body, 0);
|
||
}
|
||
if (r > 0) {
|
||
total_body += (size_t)r;
|
||
} else if (r == 0) {
|
||
break;
|
||
} else {
|
||
if (errno == EAGAIN || errno == EINTR) continue;
|
||
break;
|
||
}
|
||
}
|
||
body_len = total_body;
|
||
}
|
||
|
||
/* 判断后端是否发送 Connection: close */
|
||
bool backend_wants_close = false;
|
||
bool is_http10 = strncmp(response_buf, "HTTP/1.0", 8) == 0;
|
||
{
|
||
const char *p = headers;
|
||
while (*p) {
|
||
if (strncasecmp(p, "connection:", 11) == 0) {
|
||
p += 11;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (strncasecmp(p, "close", 5) == 0) {
|
||
backend_wants_close = true;
|
||
} else if (strncasecmp(p, "keep-alive", 10) == 0) {
|
||
backend_wants_close = false; /* 显式 keep-alive */
|
||
}
|
||
break;
|
||
}
|
||
const char *nl = strstr(p, "\r\n");
|
||
if (!nl) break;
|
||
p = nl + 2;
|
||
}
|
||
}
|
||
/* HTTP/1.0 默认关闭,除非显式 keep-alive */
|
||
if (is_http10 && !backend_wants_close) {
|
||
backend_wants_close = true;
|
||
}
|
||
|
||
/* 保存后端连接信息到流,供流式回调使用 */
|
||
stream->proxy_backend_fd = backend_fd;
|
||
stream->proxy_tls_conn = tls_conn;
|
||
stream->proxy_use_https = use_https;
|
||
stream->proxy_eof = false;
|
||
stream->proxy_close_backend = backend_wants_close;
|
||
stream->proxy_backend = backend;
|
||
|
||
/* 缓存已读取的 body(recv_headers 可能已经读取部分 body) */
|
||
if (body_len > 0) {
|
||
stream->response_body = (char *)malloc(body_len);
|
||
if (stream->response_body) {
|
||
memcpy(stream->response_body, body, body_len);
|
||
stream->response_len = body_len;
|
||
stream->response_sent = 0;
|
||
}
|
||
}
|
||
|
||
/* 构建 HTTP/2 响应 */
|
||
nghttp2_nv hdrs[32];
|
||
int num_hdrs = 0;
|
||
|
||
char status_str[4];
|
||
snprintf(status_str, sizeof(status_str), "%d", status);
|
||
hdrs[num_hdrs++] = (nghttp2_nv){
|
||
(uint8_t *)":status", (uint8_t *)status_str, 7, strlen(status_str), 0};
|
||
|
||
/* 解析 HTTP/1.1 头为 nghttp2_nv */
|
||
const char *p = headers;
|
||
while (*p && num_hdrs < 32) {
|
||
const char *nl = strstr(p, "\r\n");
|
||
if (!nl) break;
|
||
size_t line_len = (size_t)(nl - p);
|
||
if (line_len == 0) break;
|
||
|
||
const char *colon = memchr(p, ':', line_len);
|
||
if (!colon) {
|
||
p = nl + 2;
|
||
continue;
|
||
}
|
||
|
||
size_t name_len = (size_t)(colon - p);
|
||
size_t value_len = line_len - name_len - 1;
|
||
const char *value = colon + 1;
|
||
while (value_len > 0 && (*value == ' ' || *value == '\t')) {
|
||
value++;
|
||
value_len--;
|
||
}
|
||
|
||
/* 过滤 hop-by-hop 头 */
|
||
char name_lower[64];
|
||
size_t copy_len = name_len < sizeof(name_lower) - 1 ? name_len : sizeof(name_lower) - 1;
|
||
memcpy(name_lower, p, copy_len);
|
||
name_lower[copy_len] = '\0';
|
||
for (size_t i = 0; i < copy_len; i++) {
|
||
if (name_lower[i] >= 'A' && name_lower[i] <= 'Z') {
|
||
name_lower[i] += 'a' - 'A';
|
||
}
|
||
}
|
||
|
||
if (is_hop_by_hop_header(name_lower)) {
|
||
p = nl + 2;
|
||
continue;
|
||
}
|
||
|
||
/* 跳过 content-length(流式转发不需要) */
|
||
if (strcmp(name_lower, "content-length") == 0) {
|
||
p = nl + 2;
|
||
continue;
|
||
}
|
||
|
||
/* 创建 nghttp2_nv(nghttp2 不会修改这些内存,但要求非 const) */
|
||
hdrs[num_hdrs].name = (uint8_t *)p; /* 指向 headers 内部,生命周期足够 */
|
||
hdrs[num_hdrs].value = (uint8_t *)value;
|
||
hdrs[num_hdrs].namelen = name_len;
|
||
hdrs[num_hdrs].valuelen = value_len;
|
||
hdrs[num_hdrs].flags = 0;
|
||
num_hdrs++;
|
||
p = nl + 2;
|
||
}
|
||
|
||
/* 使用流式回调发送响应 */
|
||
nghttp2_data_provider provider;
|
||
provider.source.ptr = stream;
|
||
provider.read_callback = proxy_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) {
|
||
/* 虚拟主机匹配:根据 Host 头选择 root_dir */
|
||
const char *host = NULL;
|
||
for (int i = 0; i < stream->request.num_headers; i++) {
|
||
if (strcasecmp(stream->request.headers[i].name, "Host") == 0) {
|
||
host = stream->request.headers[i].value;
|
||
break;
|
||
}
|
||
}
|
||
const char *root_dir = h2->root_dir;
|
||
if (h2->server_config && host) {
|
||
root_dir = vhost_match_root_dir(h2->server_config, host);
|
||
}
|
||
|
||
if (!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(root_dir, root_normalized)) {
|
||
snprintf(root_normalized, sizeof(root_normalized), "%s", 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;
|
||
}
|