cocoon/http2.c
xfy911 e6b4a11d16 feat(http2): HTTP/2 代理流式转发
- 新增 proxy_data_source_read_callback:实时从后端读取数据流式转发
- http2_serve_proxy 重构:解析响应头后立即发送,body 通过 DATA 帧实时转发
- 解除 64KB 响应缓冲区限制,支持任意大小响应体
- 流结构新增代理字段(backend_fd/tls_conn/eof/close_backend/backend)
- 已缓存 body(recv_headers 预读部分)优先发送,再续读后端
- 连接池归还/关闭自动处理
2026-06-10 00:22:46 +08:00

2142 lines
73 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 表示 EOFNGHTTP2_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;
}
/* 先返回已缓存的 bodyrecv_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, "&amp;", 5); j += 5; }
break;
case '<':
if (j + 4 < dst_size) { memcpy(dst + j, "&lt;", 4); j += 4; }
break;
case '>':
if (j + 4 < dst_size) { memcpy(dst + j, "&gt;", 4); j += 4; }
break;
case '"':
if (j + 6 < dst_size) { memcpy(dst + j, "&quot;", 6); j += 6; }
break;
default:
dst[j++] = src[i];
}
}
dst[j] = '\0';
}
/* 添加目录项 */
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;
/* 缓存已读取的 bodyrecv_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_nvnghttp2 不会修改这些内存,但要求非 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;
}