diff --git a/cocoon.h b/cocoon.h new file mode 100644 index 0000000..29337bb --- /dev/null +++ b/cocoon.h @@ -0,0 +1,69 @@ +/** + * cocoon.h - Cocoon 公共头文件 + * + * 定义错误码、配置结构体、函数声明。 + * + * @author xfy + */ + +#ifndef COCOON_H +#define COCOON_H + +#include +#include +#include + +/* === 错误码 === */ +#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 */ diff --git a/http.c b/http.c new file mode 100644 index 0000000..272e1c3 --- /dev/null +++ b/http.c @@ -0,0 +1,287 @@ +/** + * http.c - HTTP 解析与响应构建 + * + * 轻量级 HTTP/1.1 解析器,支持请求解析、响应头格式化、MIME 类型识别。 + * + * @author xfy + */ + +#include "http.h" +#include +#include +#include + +/** + * 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-length、connection 等常用头。 + * + * @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"; +} diff --git a/http.h b/http.h new file mode 100644 index 0000000..7a5cd33 --- /dev/null +++ b/http.h @@ -0,0 +1,102 @@ +/** + * http.h - HTTP 解析模块接口 + * + * 提供 HTTP 请求解析和响应构建的接口。 + * + * @author xfy + */ + +#ifndef COCOON_HTTP_H +#define COCOON_HTTP_H + +#include +#include + +/* === 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 */