cocoon/http.c
xfy911 0dc2bb5b4e feat(server): 连接空闲超时 + 最大并发限制 + 分级日志
- 新增 log.c/log.h 分级日志系统(error/warn/info/debug)
- 新增连接空闲超时管理:使用 coco_timer + shutdown + coco_cancel
  自动清理长时间无数据的僵尸连接,默认 30 秒
- 新增最大并发连接数限制(-m <num>),超限返回 503
- 命令行新增选项:-m 最大连接数, -o 超时毫秒, -l 日志级别
- 替换所有 printf 为 log_info/log_error/log_debug 等分级输出
- 编译通过,单线程/多线程模式均正常
2026-06-03 18:13:29 +08:00

319 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

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

#define _GNU_SOURCE
/**
* http.c - HTTP 解析与响应构建
*
* 轻量级 HTTP/1.1 解析器支持请求解析、响应头格式化、MIME 类型识别。
*
* @author xfy
*/
#include "http.h"
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <stdlib.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-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;
req->has_if_none_match = false;
req->has_if_modified_since = false;
req->accept_gzip = false;
req->accept_deflate = false;
req->has_accept_encoding = false;
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, "if-none-match") == 0) {
req->has_if_none_match = true;
int copy_len = strlen(req->headers[req->num_headers].value);
if (copy_len >= 64) copy_len = 63;
memcpy(req->if_none_match, req->headers[req->num_headers].value, copy_len);
req->if_none_match[copy_len] = '\0';
} else if (strcmp(req->headers[req->num_headers].name, "if-modified-since") == 0) {
req->has_if_modified_since = true;
int copy_len = strlen(req->headers[req->num_headers].value);
if (copy_len >= 64) copy_len = 63;
memcpy(req->if_modified_since, req->headers[req->num_headers].value, copy_len);
req->if_modified_since[copy_len] = '\0';
} else if (strcmp(req->headers[req->num_headers].name, "accept-encoding") == 0) {
req->has_accept_encoding = true;
const char *val = req->headers[req->num_headers].value;
if (strstr(val, "gzip") != NULL) req->accept_gzip = true;
if (strstr(val, "deflate") != NULL) req->accept_deflate = true;
/* 解析 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",
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);
} 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",
resp->status_code, resp->status_text,
resp->content_type ? resp->content_type : "application/octet-stream",
(long)resp->content_length);
}
/* 缓存头 */
if (resp->etag && resp->etag[0]) {
n += snprintf(buf + n, buf_size - n, "ETag: %s\r\n", resp->etag);
}
if (resp->last_modified && resp->last_modified[0]) {
n += snprintf(buf + n, buf_size - n, "Last-Modified: %s\r\n", resp->last_modified);
}
/* 连接头 + 空行 */
n += snprintf(buf + n, buf_size - n,
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
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";
}