/** * 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 #include #include #include #include #include #include #include #include #include #include /* 最大 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, "\n" "\n" "\n" "Index of %s\n" "\n" "\n" "

Index of %s

\n" "\n" "\n", request_path, request_path); /* 添加返回上级链接 */ if (strcmp(request_path, "/") != 0) { n += snprintf(html + n, 65536 - n, "\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, "\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, "
NameSizeModified
../--
%s%s%s%s
\n" "
\n" "

Cocoon Server

\n" "\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; }