feat(http2): 实现 HTTP/2 静态文件服务,修复 ALPN 协商与 send_callback 返回值
主要变更: - http2.c: 实现完整的 HTTP/2 静态文件服务(GET/HEAD、路径安全、ETag、304 缓存) - http2.c: 修复 :path 伪头长度解析 bug(4→5)和路径空终止问题 - http2.c: 修复 send_callback 返回值 bug(send_all 返回 0 而非已发送字节数) - http2.c: 添加 If-None-Match / If-Modified-Since 缓存头解析 - http2.h: 扩展 http2_session_t 结构体,添加 root_dir/gzip/brotli 字段 - http2.h: 新增 http2_session_set_context() 接口 - server.c: 在 handle_http2() 中调用 set_context 传入连接配置 - tls.c: 实现 ALPN 选择回调,优先协商 h2 次选 http/1.1 - tls.c: 修正 ALPN 协议格式解析(长度前缀字符串序列) - tests/integration_test.sh: 新增 6 项 HTTP/2 集成测试(ALPN、GET、HEAD、404、缓存) 集成测试:45 项全部通过(新增 6 项 HTTP/2 测试) 单元测试:127 项全部通过
This commit is contained in:
parent
832ff3dbf6
commit
22c9f74e76
290
http2.c
290
http2.c
@ -11,12 +11,14 @@
|
|||||||
#include "static.h"
|
#include "static.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "tls.h"
|
#include "tls.h"
|
||||||
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
/* 最大 HTTP/2 会话数 */
|
/* 最大 HTTP/2 会话数 */
|
||||||
#define MAX_HTTP2_SESSIONS 1024
|
#define MAX_HTTP2_SESSIONS 1024
|
||||||
@ -24,6 +26,12 @@
|
|||||||
static http2_session_t *g_sessions[MAX_HTTP2_SESSIONS];
|
static http2_session_t *g_sessions[MAX_HTTP2_SESSIONS];
|
||||||
static int g_session_count = 0;
|
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,
|
static ssize_t send_callback(nghttp2_session *session __attribute__((unused)), const uint8_t *data,
|
||||||
size_t length, int flags __attribute__((unused)), void *user_data);
|
size_t length, int flags __attribute__((unused)), void *user_data);
|
||||||
@ -44,6 +52,10 @@ static int on_data_chunk_recv_callback(nghttp2_session *session,
|
|||||||
uint8_t flags, int32_t stream_id,
|
uint8_t flags, int32_t stream_id,
|
||||||
const uint8_t *data,
|
const uint8_t *data,
|
||||||
size_t len, void *user_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_static(http2_session_t *h2, http2_stream_data_t *stream);
|
||||||
|
|
||||||
/* ===================== 会话管理 ===================== */
|
/* ===================== 会话管理 ===================== */
|
||||||
|
|
||||||
@ -162,6 +174,13 @@ http2_session_t *http2_session_get(int fd) {
|
|||||||
return g_sessions[fd];
|
return g_sessions[fd];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== 数据收发 ===================== */
|
/* ===================== 数据收发 ===================== */
|
||||||
|
|
||||||
int http2_recv(http2_session_t *h2, const uint8_t *buf, size_t len) {
|
int http2_recv(http2_session_t *h2, const uint8_t *buf, size_t len) {
|
||||||
@ -200,11 +219,6 @@ bool http2_want_write(http2_session_t *h2) {
|
|||||||
|
|
||||||
/* ===================== nghttp2 回调 ===================== */
|
/* ===================== nghttp2 回调 ===================== */
|
||||||
|
|
||||||
/**
|
|
||||||
* send_callback - nghttp2 发送回调
|
|
||||||
*
|
|
||||||
* 将 nghttp2 序列化后的帧数据发送到 socket。
|
|
||||||
*/
|
|
||||||
static ssize_t send_callback(nghttp2_session *session __attribute__((unused)),
|
static ssize_t send_callback(nghttp2_session *session __attribute__((unused)),
|
||||||
const uint8_t *data, size_t length,
|
const uint8_t *data, size_t length,
|
||||||
int flags __attribute__((unused)),
|
int flags __attribute__((unused)),
|
||||||
@ -216,7 +230,10 @@ static ssize_t send_callback(nghttp2_session *session __attribute__((unused)),
|
|||||||
if (h2->tls_mode && tls_has_connection(h2->fd)) {
|
if (h2->tls_mode && tls_has_connection(h2->fd)) {
|
||||||
sent = tls_write(h2->fd, (const char *)data, length);
|
sent = tls_write(h2->fd, (const char *)data, length);
|
||||||
} else {
|
} else {
|
||||||
sent = send_all(h2->fd, (const char *)data, length);
|
if (send_all(h2->fd, (const char *)data, length) != 0) {
|
||||||
|
return NGHTTP2_ERR_CALLBACK_FAILURE;
|
||||||
|
}
|
||||||
|
sent = (ssize_t)length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sent < 0) {
|
if (sent < 0) {
|
||||||
@ -225,9 +242,6 @@ static ssize_t send_callback(nghttp2_session *session __attribute__((unused)),
|
|||||||
return (int)sent;
|
return (int)sent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* on_begin_headers_callback - 开始接收请求头
|
|
||||||
*/
|
|
||||||
static int on_begin_headers_callback(nghttp2_session *session __attribute__((unused)),
|
static int on_begin_headers_callback(nghttp2_session *session __attribute__((unused)),
|
||||||
const nghttp2_frame *frame,
|
const nghttp2_frame *frame,
|
||||||
void *user_data) {
|
void *user_data) {
|
||||||
@ -257,9 +271,6 @@ static int on_begin_headers_callback(nghttp2_session *session __attribute__((unu
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* on_header_callback - 接收单个请求头
|
|
||||||
*/
|
|
||||||
static int on_header_callback(nghttp2_session *session,
|
static int on_header_callback(nghttp2_session *session,
|
||||||
const nghttp2_frame *frame, const uint8_t *name,
|
const nghttp2_frame *frame, const uint8_t *name,
|
||||||
size_t namelen, const uint8_t *value,
|
size_t namelen, const uint8_t *value,
|
||||||
@ -277,7 +288,7 @@ static int on_header_callback(nghttp2_session *session,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 收集伪头和普通头 */
|
/* 收集伪头和普通头 */
|
||||||
if (namelen == 4 && memcmp(":path", name, 4) == 0) {
|
if (namelen == 5 && memcmp(":path", name, 5) == 0) {
|
||||||
/* 解析路径(可能包含 query string) */
|
/* 解析路径(可能包含 query string) */
|
||||||
size_t path_len = valuelen;
|
size_t path_len = valuelen;
|
||||||
for (size_t i = 0; i < valuelen; i++) {
|
for (size_t i = 0; i < valuelen; i++) {
|
||||||
@ -287,10 +298,14 @@ static int on_header_callback(nghttp2_session *session,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (path_len > 0) {
|
if (path_len > 0) {
|
||||||
memcpy(stream->request.path, value,
|
size_t copy_len = path_len < sizeof(stream->request.path) - 1
|
||||||
path_len < sizeof(stream->request.path) - 1
|
? path_len
|
||||||
? path_len
|
: sizeof(stream->request.path) - 1;
|
||||||
: 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) {
|
} else if (namelen == 7 && memcmp(":method", name, 7) == 0) {
|
||||||
if (valuelen == 3 && memcmp("GET", value, 3) == 0) {
|
if (valuelen == 3 && memcmp("GET", value, 3) == 0) {
|
||||||
@ -315,15 +330,27 @@ static int on_header_callback(nghttp2_session *session,
|
|||||||
memcpy(stream->request.headers[idx].value, value,
|
memcpy(stream->request.headers[idx].value, value,
|
||||||
valuelen < sizeof(stream->request.headers[idx].value) - 1 ? valuelen : sizeof(stream->request.headers[idx].value) - 1);
|
valuelen < sizeof(stream->request.headers[idx].value) - 1 ? valuelen : sizeof(stream->request.headers[idx].value) - 1);
|
||||||
stream->request.num_headers++;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* on_frame_recv_callback - 帧接收完成
|
|
||||||
*/
|
|
||||||
static int on_frame_recv_callback(nghttp2_session *session,
|
static int on_frame_recv_callback(nghttp2_session *session,
|
||||||
const nghttp2_frame *frame, void *user_data) {
|
const nghttp2_frame *frame, void *user_data) {
|
||||||
http2_session_t *h2 = (http2_session_t *)user_data;
|
http2_session_t *h2 = (http2_session_t *)user_data;
|
||||||
@ -340,12 +367,8 @@ static int on_frame_recv_callback(nghttp2_session *session,
|
|||||||
}
|
}
|
||||||
stream->request_complete = true;
|
stream->request_complete = true;
|
||||||
|
|
||||||
/* TODO: 处理请求并生成响应 */
|
/* 处理静态文件请求 */
|
||||||
/* 目前返回 501 Not Implemented */
|
http2_serve_static(h2, stream);
|
||||||
nghttp2_nv hdrs[] = {
|
|
||||||
{(uint8_t *)":status", (uint8_t *)"501", 7, 3, 0}};
|
|
||||||
nghttp2_submit_response(session, frame->hd.stream_id, hdrs, 1,
|
|
||||||
NULL);
|
|
||||||
http2_send_pending(h2);
|
http2_send_pending(h2);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -356,9 +379,6 @@ static int on_frame_recv_callback(nghttp2_session *session,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* on_stream_close_callback - 流关闭
|
|
||||||
*/
|
|
||||||
static int on_stream_close_callback(nghttp2_session *session __attribute__((unused)),
|
static int on_stream_close_callback(nghttp2_session *session __attribute__((unused)),
|
||||||
int32_t stream_id, uint32_t error_code __attribute__((unused)),
|
int32_t stream_id, uint32_t error_code __attribute__((unused)),
|
||||||
void *user_data) {
|
void *user_data) {
|
||||||
@ -385,9 +405,6 @@ static int on_stream_close_callback(nghttp2_session *session __attribute__((unus
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* on_data_chunk_recv_callback - 接收请求体数据
|
|
||||||
*/
|
|
||||||
static int on_data_chunk_recv_callback(nghttp2_session *session __attribute__((unused)),
|
static int on_data_chunk_recv_callback(nghttp2_session *session __attribute__((unused)),
|
||||||
uint8_t flags __attribute__((unused)),
|
uint8_t flags __attribute__((unused)),
|
||||||
int32_t stream_id, const uint8_t *data,
|
int32_t stream_id, const uint8_t *data,
|
||||||
@ -405,6 +422,215 @@ static int on_data_chunk_recv_callback(nghttp2_session *session __attribute__((u
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================== HTTP/2 静态文件服务 ===================== */
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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))) {
|
||||||
|
|
||||||
|
if (source->fd < 0) {
|
||||||
|
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t n = read(source->fd, buf, length);
|
||||||
|
if (n < 0) {
|
||||||
|
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
|
||||||
|
}
|
||||||
|
if (n == 0) {
|
||||||
|
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
|
||||||
|
}
|
||||||
|
return (ssize_t)n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void http2_serve_static(http2_session_t *h2, http2_stream_data_t *stream) {
|
||||||
|
if (!h2->root_dir) {
|
||||||
|
nghttp2_nv hdrs[] = {
|
||||||
|
{(uint8_t *)":status", (uint8_t *)"503", 7, 3, 0}};
|
||||||
|
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 构建真实路径 */
|
||||||
|
char real_path[4096];
|
||||||
|
char root_normalized[4096];
|
||||||
|
|
||||||
|
if (!realpath(h2->root_dir, root_normalized)) {
|
||||||
|
snprintf(root_normalized, sizeof(root_normalized), "%s", h2->root_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = snprintf(real_path, sizeof(real_path), "%s%s", root_normalized, stream->request.path);
|
||||||
|
if (n < 0 || (size_t)n >= sizeof(real_path)) {
|
||||||
|
nghttp2_nv hdrs[] = {
|
||||||
|
{(uint8_t *)":status", (uint8_t *)"400", 7, 3, 0}};
|
||||||
|
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 路径遍历检查 */
|
||||||
|
if (strstr(stream->request.path, "..") != NULL) {
|
||||||
|
char resolved[4096];
|
||||||
|
if (!realpath(real_path, resolved) ||
|
||||||
|
strncmp(resolved, root_normalized, strlen(root_normalized)) != 0) {
|
||||||
|
nghttp2_nv hdrs[] = {
|
||||||
|
{(uint8_t *)":status", (uint8_t *)"403", 7, 3, 0}};
|
||||||
|
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snprintf(real_path, sizeof(real_path), "%s", resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(real_path, &st) != 0) {
|
||||||
|
nghttp2_nv hdrs[] = {
|
||||||
|
{(uint8_t *)":status", (uint8_t *)"404", 7, 3, 0}};
|
||||||
|
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 目录:尝试 index.html */
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
char index_path[4096];
|
||||||
|
size_t real_len = strlen(real_path);
|
||||||
|
if (real_len + 12 >= sizeof(index_path)) {
|
||||||
|
nghttp2_nv hdrs[] = {
|
||||||
|
{(uint8_t *)":status", (uint8_t *)"404", 7, 3, 0}};
|
||||||
|
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
nghttp2_nv hdrs[] = {
|
||||||
|
{(uint8_t *)":status", (uint8_t *)"404", 7, 3, 0}};
|
||||||
|
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, 1, NULL);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 构建响应头 */
|
||||||
|
nghttp2_nv hdrs[16];
|
||||||
|
int num_hdrs = 0;
|
||||||
|
|
||||||
|
hdrs[num_hdrs++] = (nghttp2_nv){
|
||||||
|
(uint8_t *)":status", (uint8_t *)"200", 7, 3, 0};
|
||||||
|
|
||||||
|
const char *mime = http_mime_type(real_path);
|
||||||
|
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)st.st_size);
|
||||||
|
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 *)"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) {
|
||||||
|
close(file_fd);
|
||||||
|
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, NULL);
|
||||||
|
} else {
|
||||||
|
nghttp2_data_provider provider;
|
||||||
|
provider.source.fd = file_fd;
|
||||||
|
provider.read_callback = http2_data_source_read_callback;
|
||||||
|
nghttp2_submit_response(h2->session, stream->stream_id, hdrs, num_hdrs, &provider);
|
||||||
|
stream->file_fd = file_fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== 连接处理 ===================== */
|
/* ===================== 连接处理 ===================== */
|
||||||
|
|
||||||
int http2_on_connection_accepted(int fd, bool tls_mode) {
|
int http2_on_connection_accepted(int fd, bool tls_mode) {
|
||||||
|
|||||||
13
http2.h
13
http2.h
@ -30,6 +30,9 @@ typedef struct {
|
|||||||
int fd; /**< 底层 socket fd */
|
int fd; /**< 底层 socket fd */
|
||||||
bool tls_mode; /**< 是否通过 TLS ALPN 协商 */
|
bool tls_mode; /**< 是否通过 TLS ALPN 协商 */
|
||||||
struct http2_stream_data *streams; /**< 活跃的流列表(头节点) */
|
struct http2_stream_data *streams; /**< 活跃的流列表(头节点) */
|
||||||
|
const char *root_dir; /**< 静态资源根目录 */
|
||||||
|
bool gzip_enabled; /**< 是否启用 gzip 压缩 */
|
||||||
|
bool brotli_enabled; /**< 是否启用 brotli 压缩 */
|
||||||
} http2_session_t;
|
} http2_session_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -144,6 +147,16 @@ bool http2_want_write(http2_session_t *h2);
|
|||||||
*/
|
*/
|
||||||
int http2_on_connection_accepted(int fd, bool tls_mode);
|
int http2_on_connection_accepted(int fd, bool tls_mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* http2_session_set_context - 设置 HTTP/2 会话的服务上下文
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
1
server.c
1
server.c
@ -521,6 +521,7 @@ static void conn_cancel_timer(connection_t *conn) {
|
|||||||
static void handle_http2(connection_t *conn) {
|
static void handle_http2(connection_t *conn) {
|
||||||
http2_session_t *h2 = http2_session_get(conn->fd);
|
http2_session_t *h2 = http2_session_get(conn->fd);
|
||||||
if (!h2) return;
|
if (!h2) return;
|
||||||
|
http2_session_set_context(h2, conn->root_dir, conn->gzip_enabled, conn->brotli_enabled);
|
||||||
|
|
||||||
uint32_t timeout_ms = conn->timeout_ms > 0 ? conn->timeout_ms : CONN_TIMEOUT_MS;
|
uint32_t timeout_ms = conn->timeout_ms > 0 ? conn->timeout_ms : CONN_TIMEOUT_MS;
|
||||||
|
|
||||||
|
|||||||
@ -447,7 +447,86 @@ fi
|
|||||||
# 使用 curl -k 验证 HTTPS 响应体
|
# 使用 curl -k 验证 HTTPS 响应体
|
||||||
assert_body_contains "https://$HOST/" "Cocoon" "HTTPS 首页响应"
|
assert_body_contains "https://$HOST/" "Cocoon" "HTTPS 首页响应"
|
||||||
|
|
||||||
# 停止 HTTPS 服务器,恢复 HTTP 服务器
|
echo ""
|
||||||
|
echo "=== HTTP/2 测试 ==="
|
||||||
|
# 使用已启动的 TLS 服务器测试 HTTP/2(ALPN 协商)
|
||||||
|
kill_server
|
||||||
|
sleep 1
|
||||||
|
$SERVER -r "$ROOT" -p 9999 --cert tests/server.crt --key tests/server.key > "$TMPDIR/server_h2.log" 2>&1 &
|
||||||
|
pid_h2=$!
|
||||||
|
|
||||||
|
# 等待服务器就绪
|
||||||
|
for i in {1..50}; do
|
||||||
|
if curl -s -o /dev/null -k --max-time 2 "https://$HOST/" 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
|
||||||
|
# 验证 HTTP/2 协议版本
|
||||||
|
h2_version=$(curl --http2 -k -s -o /dev/null -w "%{http_version}" "https://$HOST/")
|
||||||
|
if [[ "$h2_version" == "2" ]]; then
|
||||||
|
echo " ✓ HTTP/2 ALPN 协商 — 协议版本 HTTP/2"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ HTTP/2 ALPN 协商 — 期望 HTTP/2, 实际 HTTP/$h2_version"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
|
||||||
|
# HTTP/2 GET 首页
|
||||||
|
h2_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/")
|
||||||
|
if [[ "$h2_status" == "200" ]]; then
|
||||||
|
echo " ✓ HTTP/2 GET 首页 — HTTP 200"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ HTTP/2 GET 首页 — 期望 200, 实际 $h2_status"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
|
||||||
|
# HTTP/2 响应体
|
||||||
|
h2_body=$(curl --http2 -k -s "https://$HOST/")
|
||||||
|
if echo "$h2_body" | grep -q "Cocoon"; then
|
||||||
|
echo " ✓ HTTP/2 响应体 — 包含 'Cocoon'"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ HTTP/2 响应体 — 未包含 'Cocoon'"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
|
||||||
|
# HTTP/2 HEAD 请求
|
||||||
|
h2_head_status=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" -I "https://$HOST/index.html")
|
||||||
|
if [[ "$h2_head_status" == "200" ]]; then
|
||||||
|
echo " ✓ HTTP/2 HEAD 请求 — HTTP 200"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ HTTP/2 HEAD 请求 — 期望 200, 实际 $h2_head_status"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
|
||||||
|
# HTTP/2 404
|
||||||
|
h2_404=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" "https://$HOST/nonexist.html")
|
||||||
|
if [[ "$h2_404" == "404" ]]; then
|
||||||
|
echo " ✓ HTTP/2 404 请求 — HTTP 404"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ HTTP/2 404 请求 — 期望 404, 实际 $h2_404"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
|
||||||
|
# HTTP/2 缓存协商(ETag 304)
|
||||||
|
H2_ETAG=$(curl --http2 -k -sI "https://$HOST/index.html" | grep -i "ETag:" | awk '{print $2}' | tr -d '\r')
|
||||||
|
if [[ -n "$H2_ETAG" ]]; then
|
||||||
|
h2_304=$(curl --http2 -k -s -o /dev/null -w "%{http_code}" -H "If-None-Match: $H2_ETAG" "https://$HOST/index.html")
|
||||||
|
if [[ "$h2_304" == "304" ]]; then
|
||||||
|
echo " ✓ HTTP/2 If-None-Match 304 — 缓存命中"
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
echo " ✗ HTTP/2 If-None-Match 304 — 期望 304, 实际 $h2_304"
|
||||||
|
fail
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 停止 HTTP/2 服务器,恢复 HTTP 服务器
|
||||||
kill_server
|
kill_server
|
||||||
sleep 1
|
sleep 1
|
||||||
start_server
|
start_server
|
||||||
|
|||||||
42
tls.c
42
tls.c
@ -105,6 +105,45 @@ static int flush_wbio(int fd, BIO *wbio) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 内部:ALPN 选择回调,优先选择 h2,否则 http/1.1 */
|
||||||
|
static int tls_alpn_select_cb(SSL *ssl __attribute__((unused)), const unsigned char **out,
|
||||||
|
unsigned char *outlen, const unsigned char *in,
|
||||||
|
unsigned int inlen, void *arg __attribute__((unused))) {
|
||||||
|
const unsigned char *h2 = (const unsigned char *)"h2";
|
||||||
|
const unsigned char *http11 = (const unsigned char *)"http/1.1";
|
||||||
|
unsigned int h2_len = 2;
|
||||||
|
unsigned int http11_len = 8;
|
||||||
|
|
||||||
|
/* 客户端发送的 ALPN 列表格式:长度前缀字符串序列 */
|
||||||
|
const unsigned char *p = in;
|
||||||
|
unsigned int remaining = inlen;
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
unsigned char len = *p;
|
||||||
|
p++;
|
||||||
|
remaining--;
|
||||||
|
if (len > remaining) break;
|
||||||
|
|
||||||
|
/* 优先匹配 h2 */
|
||||||
|
if (len == h2_len && memcmp(p, h2, h2_len) == 0) {
|
||||||
|
*out = p;
|
||||||
|
*outlen = h2_len;
|
||||||
|
return SSL_TLSEXT_ERR_OK;
|
||||||
|
}
|
||||||
|
/* 次选 http/1.1 */
|
||||||
|
if (len == http11_len && memcmp(p, http11, http11_len) == 0) {
|
||||||
|
*out = p;
|
||||||
|
*outlen = http11_len;
|
||||||
|
return SSL_TLSEXT_ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
p += len;
|
||||||
|
remaining -= len;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SSL_TLSEXT_ERR_NOACK;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 公共 API ===== */
|
/* ===== 公共 API ===== */
|
||||||
|
|
||||||
int tls_create_context(const char *cert_path, const char *key_path) {
|
int tls_create_context(const char *cert_path, const char *key_path) {
|
||||||
@ -147,6 +186,9 @@ int tls_create_context(const char *cert_path, const char *key_path) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 注册 ALPN 回调,支持 HTTP/2 和 HTTP/1.1 协商 */
|
||||||
|
SSL_CTX_set_alpn_select_cb(g_ctx, tls_alpn_select_cb, NULL);
|
||||||
|
|
||||||
log_info("TLS 上下文已初始化: cert=%s", cert_path);
|
log_info("TLS 上下文已初始化: cert=%s", cert_path);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user