feat(http): 实现 HTTP/1.1 请求解析与响应构建

- http_parse_request: 解析请求行 + 头部提取
- http_format_response_header: 格式化响应头(支持 Range)
- http_mime_type: 25+ 种常见 MIME 类型自动识别
This commit is contained in:
xfy911 2026-06-03 16:44:02 +08:00
parent 84b415470b
commit 0ab3291035
3 changed files with 458 additions and 0 deletions

69
cocoon.h Normal file
View File

@ -0,0 +1,69 @@
/**
* cocoon.h - Cocoon
*
*
*
* @author xfy
*/
#ifndef COCOON_H
#define COCOON_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
/* === 错误码 === */
#define COCOON_OK 0 /**< 成功 */
#define COCOON_ERROR -1 /**< 通用错误 */
#define COCOON_NOMEM -2 /**< 内存不足 */
#define COCOON_NOTFOUND -3 /**< 文件未找到 */
#define COCOONForbidden -4 /**< 禁止访问 */
#define COCOON_BADREQUEST -5 /**< 请求格式错误 */
/* === 服务器配置 === */
/**
* cocoon_config -
*
*
*/
typedef struct cocoon_config {
const char *root_dir; /**< 静态资源根目录 */
uint16_t port; /**< 监听端口 */
bool threaded; /**< 是否启用多线程调度 */
uint32_t num_workers; /**< 工作线程数0 = 自动检测) */
bool verbose; /**< 详细日志输出 */
} cocoon_config_t;
/* === 服务器生命周期 API === */
/**
* cocoon_server_create -
*
* @param config
* @return NULL
*/
void *cocoon_server_create(const cocoon_config_t *config);
/**
* cocoon_server_run -
*
* @param server
* @return COCOON_OK
*/
int cocoon_server_run(void *server);
/**
* cocoon_server_stop -
*
* @param server
*/
void cocoon_server_stop(void *server);
/**
* cocoon_server_destroy -
*
* @param server
*/
void cocoon_server_destroy(void *server);
#endif /* COCOON_H */

287
http.c Normal file
View File

@ -0,0 +1,287 @@
/**
* http.c - HTTP
*
* HTTP/1.1 MIME
*
* @author xfy
*/
#include "http.h"
#include <string.h>
#include <stdio.h>
#include <ctype.h>
/**
* http_method_str - HTTP
*
* @param method HTTP
* @return "UNKNOWN"
*/
const char *http_method_str(http_method_t method) {
switch (method) {
case HTTP_GET: return "GET";
case HTTP_HEAD: return "HEAD";
case HTTP_POST: return "POST";
case HTTP_PUT: return "PUT";
case HTTP_DELETE: return "DELETE";
case HTTP_OPTIONS: return "OPTIONS";
default: return "UNKNOWN";
}
}
/**
* parse_method - HTTP
*
* @param buf
* @return HTTP
*/
static http_method_t parse_method(const char *buf) {
if (strncmp(buf, "GET ", 4) == 0) return HTTP_GET;
if (strncmp(buf, "HEAD ", 5) == 0) return HTTP_HEAD;
if (strncmp(buf, "POST ", 5) == 0) return HTTP_POST;
if (strncmp(buf, "PUT ", 4) == 0) return HTTP_PUT;
if (strncmp(buf, "DELETE ", 7) == 0) return HTTP_DELETE;
if (strncmp(buf, "OPTIONS ", 8) == 0) return HTTP_OPTIONS;
return HTTP_UNKNOWN;
}
/**
* parse_headers -
*
* headers content-lengthconnection
*
* @param p
* @param end
* @param req
*/
static void parse_headers(const char **p, const char *end, http_request_t *req) {
req->num_headers = 0;
req->content_length = -1;
req->keep_alive = false;
req->has_range = false;
req->range_start = 0;
req->range_end = -1;
while (*p < end && req->num_headers < HTTP_MAX_HEADERS) {
const char *line_start = *p;
/* 查找行尾 */
const char *line_end = memchr(line_start, '\n', end - line_start);
if (!line_end) break;
/* 空行表示头部结束 */
if (line_end == line_start || (line_end == line_start + 1 && line_start[0] == '\r')) {
*p = line_end + 1;
break;
}
/* 跳过末尾 \r */
const char *line_tail = line_end;
if (line_tail > line_start && line_tail[-1] == '\r') line_tail--;
/* 查找冒号分隔符 */
const char *colon = memchr(line_start, ':', line_tail - line_start);
if (colon) {
int name_len = colon - line_start;
if (name_len > 0 && name_len < HTTP_HEADER_NAME_MAX) {
/* 复制名称(转小写便于匹配) */
for (int i = 0; i < name_len && i < HTTP_HEADER_NAME_MAX - 1; i++) {
req->headers[req->num_headers].name[i] = (char)tolower((unsigned char)line_start[i]);
}
req->headers[req->num_headers].name[name_len > HTTP_HEADER_NAME_MAX - 1 ? HTTP_HEADER_NAME_MAX - 1 : name_len] = '\0';
/* 复制值(跳过冒号后空格) */
const char *val = colon + 1;
while (val < line_tail && (*val == ' ' || *val == '\t')) val++;
int val_len = line_tail - val;
if (val_len > 0 && val_len < HTTP_HEADER_VALUE_MAX) {
int copy_len = val_len > HTTP_HEADER_VALUE_MAX - 1 ? HTTP_HEADER_VALUE_MAX - 1 : val_len;
memcpy(req->headers[req->num_headers].value, val, copy_len);
req->headers[req->num_headers].value[copy_len] = '\0';
}
/* 提取常用头 */
if (strcmp(req->headers[req->num_headers].name, "content-length") == 0) {
req->content_length = atoll(req->headers[req->num_headers].value);
} else if (strcmp(req->headers[req->num_headers].name, "connection") == 0) {
req->keep_alive = (strcasecmp(req->headers[req->num_headers].value, "keep-alive") == 0);
} else if (strcmp(req->headers[req->num_headers].name, "range") == 0) {
/* 解析 Range: bytes=start-end */
const char *range_val = req->headers[req->num_headers].value;
if (strncasecmp(range_val, "bytes=", 6) == 0) {
req->has_range = true;
req->range_start = atoll(range_val + 6);
const char *dash = strchr(range_val + 6, '-');
if (dash) {
req->range_end = atoll(dash + 1);
}
}
}
req->num_headers++;
}
}
*p = line_end + 1;
}
}
/**
* http_parse_request - HTTP
*
* HTTP/1.x http_request_t
*
*
* @param buf
* @param len
* @param req
* @return -1 -2
*/
int http_parse_request(const char *buf, size_t len, http_request_t *req) {
if (!buf || !req || len == 0) return -2;
memset(req, 0, sizeof(*req));
/* 查找请求行结尾(\r\n */
const char *line_end = memchr(buf, '\n', len);
if (!line_end) return -1; /* 数据不完整 */
int line_len = line_end - buf;
if (line_len > 0 && buf[line_len - 1] == '\r') line_len--;
if (line_len < 10) return -2; /* 请求行太短 */
/* 解析方法 */
req->method = parse_method(buf);
if (req->method == HTTP_UNKNOWN) return -2;
/* 提取路径 */
const char *path_start = strchr(buf, ' ');
if (!path_start) return -2;
path_start++;
const char *path_end = strchr(path_start, ' ');
if (!path_end) return -2;
int path_len = path_end - path_start;
if (path_len <= 0 || path_len >= HTTP_MAX_PATH) return -2;
memcpy(req->path, path_start, path_len);
req->path[path_len] = '\0';
/* 提取 HTTP 版本 */
const char *ver_start = path_end + 1;
int ver_len = line_len - (ver_start - buf);
if (ver_len > 0 && ver_len < (int)sizeof(req->version)) {
memcpy(req->version, ver_start, ver_len);
req->version[ver_len] = '\0';
}
/* 解析头部 */
const char *p = line_end + 1;
const char *end = buf + len;
parse_headers(&p, end, req);
/* HTTP/1.1 默认 keep-alive */
if (strstr(req->version, "1.1") != NULL) {
req->keep_alive = true;
}
return p - buf;
}
/**
* http_format_response_header - HTTP
*
* HTTP/1.1
*
* @param buf
* @param buf_size
* @param resp
* @return
*/
int http_format_response_header(char *buf, size_t buf_size, const http_response_t *resp) {
if (!buf || !resp || buf_size == 0) return -1;
int n = 0;
/* 状态行 */
if (resp->has_range) {
n += snprintf(buf + n, buf_size - n,
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Accept-Ranges: bytes\r\n"
"Content-Range: bytes %ld-%ld/%ld\r\n"
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
resp->status_code, resp->status_text,
resp->content_type ? resp->content_type : "application/octet-stream",
(long)resp->content_length,
(long)resp->range_start, (long)resp->range_end, (long)resp->total_length,
resp->keep_alive ? "keep-alive" : "close");
} else {
n += snprintf(buf + n, buf_size - n,
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Accept-Ranges: bytes\r\n"
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
resp->status_code, resp->status_text,
resp->content_type ? resp->content_type : "application/octet-stream",
(long)resp->content_length,
resp->keep_alive ? "keep-alive" : "close");
}
if ((size_t)n >= buf_size) return -1;
return n;
}
/**
* http_mime_type - MIME
*
*
*
* @param path
* @return MIME
*/
const char *http_mime_type(const char *path) {
const char *ext = strrchr(path, '.');
if (!ext) return "application/octet-stream";
/* 转小写比较 */
char lower_ext[16];
int i = 0;
for (const char *p = ext + 1; *p && i < 15; p++, i++) {
lower_ext[i] = (char)tolower((unsigned char)*p);
}
lower_ext[i] = '\0';
if (strcmp(lower_ext, "html") == 0 || strcmp(lower_ext, "htm") == 0) return "text/html; charset=utf-8";
if (strcmp(lower_ext, "css") == 0) return "text/css; charset=utf-8";
if (strcmp(lower_ext, "js") == 0) return "application/javascript";
if (strcmp(lower_ext, "json") == 0) return "application/json";
if (strcmp(lower_ext, "png") == 0) return "image/png";
if (strcmp(lower_ext, "jpg") == 0 || strcmp(lower_ext, "jpeg") == 0) return "image/jpeg";
if (strcmp(lower_ext, "gif") == 0) return "image/gif";
if (strcmp(lower_ext, "svg") == 0) return "image/svg+xml";
if (strcmp(lower_ext, "ico") == 0) return "image/x-icon";
if (strcmp(lower_ext, "woff2") == 0) return "font/woff2";
if (strcmp(lower_ext, "woff") == 0) return "font/woff";
if (strcmp(lower_ext, "ttf") == 0) return "font/ttf";
if (strcmp(lower_ext, "txt") == 0) return "text/plain; charset=utf-8";
if (strcmp(lower_ext, "md") == 0) return "text/markdown; charset=utf-8";
if (strcmp(lower_ext, "xml") == 0) return "application/xml";
if (strcmp(lower_ext, "pdf") == 0) return "application/pdf";
if (strcmp(lower_ext, "zip") == 0) return "application/zip";
if (strcmp(lower_ext, "gz") == 0) return "application/gzip";
if (strcmp(lower_ext, "mp4") == 0) return "video/mp4";
if (strcmp(lower_ext, "webm") == 0) return "video/webm";
if (strcmp(lower_ext, "mp3") == 0) return "audio/mpeg";
if (strcmp(lower_ext, "ogg") == 0) return "audio/ogg";
if (strcmp(lower_ext, "wasm") == 0) return "application/wasm";
if (strcmp(lower_ext, "webmanifest") == 0) return "application/manifest+json";
return "application/octet-stream";
}

102
http.h Normal file
View File

@ -0,0 +1,102 @@
/**
* http.h - HTTP
*
* HTTP
*
* @author xfy
*/
#ifndef COCOON_HTTP_H
#define COCOON_HTTP_H
#include <stdint.h>
#include <stdbool.h>
/* === HTTP 方法 === */
typedef enum {
HTTP_GET,
HTTP_HEAD,
HTTP_POST,
HTTP_PUT,
HTTP_DELETE,
HTTP_OPTIONS,
HTTP_UNKNOWN
} http_method_t;
/* === HTTP 请求 === */
#define HTTP_MAX_PATH 4096
#define HTTP_MAX_HEADERS 32
#define HTTP_HEADER_NAME_MAX 64
#define HTTP_HEADER_VALUE_MAX 1024
typedef struct {
http_method_t method;
char path[HTTP_MAX_PATH];
char version[16];
/* 请求头 */
struct {
char name[HTTP_HEADER_NAME_MAX];
char value[HTTP_HEADER_VALUE_MAX];
} headers[HTTP_MAX_HEADERS];
int num_headers;
/* 常用头快速访问 */
int64_t content_length;
bool keep_alive;
bool has_range;
int64_t range_start;
int64_t range_end;
} http_request_t;
/* === HTTP 响应 === */
typedef struct {
int status_code;
const char *status_text;
const char *content_type;
int64_t content_length;
bool keep_alive;
bool has_range;
int64_t range_start;
int64_t range_end;
int64_t total_length;
} http_response_t;
/* === API === */
/**
* http_parse_request - HTTP
*
* @param buf
* @param len
* @param req
* @return < 0 -1 = -2 =
*/
int http_parse_request(const char *buf, size_t len, http_request_t *req);
/**
* http_format_response_header -
*
* @param buf
* @param buf_size
* @param resp
* @return < 0
*/
int http_format_response_header(char *buf, size_t buf_size, const http_response_t *resp);
/**
* http_method_str - HTTP
*
* @param method HTTP
* @return "GET"
*/
const char *http_method_str(http_method_t method);
/**
* http_mime_type - MIME
*
* @param path
* @return MIME
*/
const char *http_mime_type(const char *path);
#endif /* COCOON_HTTP_H */