cocoon/proxy.c
xfy911 77979b44d8 feat(proxy): 后端被动健康检查 + 修复 SIGPIPE 崩溃
- 在 cocoon_proxy_backend_t 中增加健康状态字段(healthy/fail_count/success_count/last_check)
- 实现 proxy_relay_backend() 辅助函数,支持流式转发响应
- 代理请求时优先尝试 healthy 后端,全部失败后再 fallback 到所有后端
- 健康阈值:连续 2 次成功恢复,连续 3 次失败标记不健康
- 修复 SIGPIPE 信号导致进程在客户端提前关闭连接时崩溃的问题
- 全部 142 单元测试 + 90 集成测试通过
2026-06-08 21:26:33 +08:00

482 lines
16 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.

/**
* proxy.c - 反向代理实现
*
* 轻量级 HTTP/1.1 反向代理,支持流式转发。
*
* @author xfy
*/
#include "proxy.h"
#include "proxy_tls.h"
#include "log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <time.h>
void proxy_init(cocoon_proxy_config_t *cfg) {
memset(cfg, 0, sizeof(*cfg));
cfg->count = 0;
}
static void backend_init_health(cocoon_proxy_backend_t *backend) {
backend->healthy = true;
backend->fail_count = 0;
backend->success_count = 0;
backend->last_check = 0;
}
static void parse_backend_url(const char *target_url, cocoon_proxy_backend_t *backend) {
/* 解析目标URL */
bool https = false;
const char *url = target_url;
backend->target_https = false;
backend->target_port = 80;
backend->target_host[0] = '\0';
backend->target_path[0] = '\0';
if (strncmp(url, "http://", 7) == 0) {
url += 7;
backend->target_https = false;
} else if (strncmp(url, "https://", 8) == 0) {
url += 8;
backend->target_https = true;
https = true;
} else {
/* 默认 http */
backend->target_https = false;
}
/* 提取 host:port 和剩余路径 */
const char *slash = strchr(url, '/');
char host_port[256];
if (slash) {
size_t host_len = (size_t)(slash - url);
if (host_len >= sizeof(host_port)) host_len = sizeof(host_port) - 1;
memcpy(host_port, url, host_len);
host_port[host_len] = '\0';
strncpy(backend->target_path, slash, sizeof(backend->target_path) - 1);
} else {
strncpy(host_port, url, sizeof(host_port) - 1);
host_port[sizeof(host_port) - 1] = '\0';
backend->target_path[0] = '\0';
}
/* 解析 host 和 port */
const char *colon = strrchr(host_port, ':');
if (colon) {
size_t host_len = (size_t)(colon - host_port);
if (host_len >= sizeof(backend->target_host)) host_len = sizeof(backend->target_host) - 1;
memcpy(backend->target_host, host_port, host_len);
backend->target_host[host_len] = '\0';
backend->target_port = (uint16_t)atoi(colon + 1);
} else {
size_t host_len = strlen(host_port);
if (host_len >= sizeof(backend->target_host)) host_len = sizeof(backend->target_host) - 1;
memcpy(backend->target_host, host_port, host_len);
backend->target_host[host_len] = '\0';
backend->target_port = https ? 443 : 80;
}
if (backend->target_port == 0) {
backend->target_port = https ? 443 : 80;
}
}
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url) {
if (!prefix || !target_url || prefix[0] == '\0' || target_url[0] == '\0') {
return false;
}
/* 查找是否已有相同前缀的规则 */
cocoon_proxy_rule_t *rule = NULL;
for (size_t i = 0; i < cfg->count; i++) {
if (strcmp(cfg->rules[i].path_prefix, prefix) == 0) {
rule = &cfg->rules[i];
break;
}
}
if (rule) {
/* 追加后端 */
if (rule->backend_count >= COCOON_MAX_PROXY_BACKENDS) {
log_error("后端数量超过上限 %d", COCOON_MAX_PROXY_BACKENDS);
return false;
}
cocoon_proxy_backend_t *backend = &rule->backends[rule->backend_count];
parse_backend_url(target_url, backend);
backend_init_health(backend);
log_info("追加后端: %s -> %s://%s:%d%s",
rule->path_prefix,
backend->target_https ? "https" : "http",
backend->target_host,
backend->target_port,
backend->target_path);
rule->backend_count++;
return true;
}
/* 新建规则 */
if (cfg->count >= COCOON_MAX_PROXY_RULES) {
log_error("代理规则数量超过上限 %d", COCOON_MAX_PROXY_RULES);
return false;
}
rule = &cfg->rules[cfg->count];
strncpy(rule->path_prefix, prefix, sizeof(rule->path_prefix) - 1);
rule->path_prefix[sizeof(rule->path_prefix) - 1] = '\0';
rule->backend_count = 0;
rule->current_index = 0;
cocoon_proxy_backend_t *backend = &rule->backends[rule->backend_count];
parse_backend_url(target_url, backend);
backend_init_health(backend);
rule->backend_count++;
log_info("添加代理规则: %s -> %s://%s:%d%s",
rule->path_prefix,
backend->target_https ? "https" : "http",
backend->target_host,
backend->target_port,
backend->target_path);
cfg->count++;
return true;
}
cocoon_proxy_rule_t *proxy_match(cocoon_proxy_config_t *cfg, const char *path) {
if (!cfg || !path) return NULL;
for (size_t i = 0; i < cfg->count; i++) {
cocoon_proxy_rule_t *rule = &cfg->rules[i];
size_t prefix_len = strlen(rule->path_prefix);
if (strncmp(path, rule->path_prefix, prefix_len) == 0) {
return rule;
}
}
return NULL;
}
/**
* proxy_connect_backend - 连接到指定后端
*/
static void proxy_update_health(cocoon_proxy_backend_t *backend, bool success) {
time_t now = time(NULL);
backend->last_check = now;
if (success) {
backend->fail_count = 0;
backend->success_count++;
if (!backend->healthy && backend->success_count >= COCOON_HEALTHY_THRESHOLD) {
backend->healthy = true;
log_info("后端恢复健康: %s:%d", backend->target_host, backend->target_port);
}
} else {
backend->success_count = 0;
backend->fail_count++;
if (backend->healthy && backend->fail_count >= COCOON_UNHEALTHY_THRESHOLD) {
backend->healthy = false;
log_warn("后端标记不健康: %s:%d (连续失败 %d 次)",
backend->target_host, backend->target_port, backend->fail_count);
}
}
}
static cocoon_socket_t proxy_connect_backend(const cocoon_proxy_backend_t *backend) {
struct hostent *host = gethostbyname(backend->target_host);
if (!host) {
log_error("无法解析主机: %s", backend->target_host);
return COCOON_INVALID_SOCKET;
}
cocoon_socket_t fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == COCOON_INVALID_SOCKET) {
log_error("创建 socket 失败");
return COCOON_INVALID_SOCKET;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(backend->target_port);
memcpy(&addr.sin_addr, host->h_addr_list[0], (size_t)host->h_length);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
log_warn("连接后端失败: %s:%d", backend->target_host, backend->target_port);
cocoon_socket_close(fd);
return COCOON_INVALID_SOCKET;
}
return fd;
}
/**
* proxy_build_forwarded_path - 构建转发路径
*/
static void proxy_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';
}
/**
* proxy_build_xff - 构建 X-Forwarded-For 值
*/
static void proxy_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 - 确保数据全部发送
*/
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;
}
/**
* proxy_relay_backend - 尝试连接并转发请求到单个后端
*
* 建立连接、发送请求、转发响应回客户端。
* 调用者负责不传入已关闭或无效的 fd。
*
* @return true 成功false 失败(连接、发送或转发响应出错)
*/
static bool proxy_relay_backend(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_proxy_rule_t *rule, cocoon_proxy_backend_t *backend,
const struct sockaddr_storage *client_addr,
ssize_t *total_forwarded) {
cocoon_socket_t backend_fd = COCOON_INVALID_SOCKET;
proxy_tls_conn_t *tls_conn = NULL;
bool use_https = backend->target_https;
if (use_https) {
tls_conn = proxy_tls_connect(backend->target_host, backend->target_port);
if (!tls_conn) {
log_warn("后端 %s:%d 连接失败", backend->target_host, backend->target_port);
return false;
}
} else {
backend_fd = proxy_connect_backend(backend);
if (backend_fd == COCOON_INVALID_SOCKET) {
log_warn("后端 %s:%d 连接失败", backend->target_host, backend->target_port);
return false;
}
}
/* 构建转发路径 */
char forwarded_path[512];
proxy_build_forwarded_path(rule, backend, req->path, forwarded_path, sizeof(forwarded_path));
/* 构建 X-Forwarded-For */
char xff[64];
proxy_build_xff(client_addr, xff, sizeof(xff));
/* 构建转发请求 */
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: close\r\n",
http_method_str(req->method),
forwarded_path,
backend->target_host,
backend->target_port,
xff,
use_https ? "https" : "http");
/* 透传常见请求头 */
if (req->content_type[0] != '\0') {
n += snprintf(request_buf + n, sizeof(request_buf) - n,
"Content-Type: %s\r\n", req->content_type);
}
if (req->content_length > 0) {
n += snprintf(request_buf + n, sizeof(request_buf) - n,
"Content-Length: %ld\r\n", (long)req->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) {
log_warn("转发请求头到 HTTPS 后端失败: %s:%d", backend->target_host, backend->target_port);
send_ok = false;
}
} else {
if (send_all_fd(backend_fd, request_buf, (size_t)n) != 0) {
log_warn("转发请求头到后端失败: %s:%d", backend->target_host, backend->target_port);
send_ok = false;
}
}
/* 发送请求体 */
if (send_ok && req->body && req->body_len > 0) {
if (use_https) {
if (proxy_send_all_tls(tls_conn, req->body, req->body_len) != 0) {
log_warn("转发请求体到 HTTPS 后端失败");
send_ok = false;
}
} else {
if (send_all_fd(backend_fd, req->body, req->body_len) != 0) {
log_warn("转发请求体到后端失败");
send_ok = false;
}
}
}
/* 流式转发响应回客户端 */
bool success = false;
ssize_t total = 0;
if (send_ok) {
char relay_buf[8192];
bool recv_ok = true;
while (1) {
ssize_t r;
if (use_https) {
r = proxy_tls_read(tls_conn, relay_buf, sizeof(relay_buf));
} else {
r = recv(backend_fd, relay_buf, sizeof(relay_buf), 0);
}
if (r < 0) {
if (errno == EAGAIN || errno == EINTR) continue;
recv_ok = false;
break;
}
if (r == 0) break;
if (send_all_fd(client_fd, relay_buf, (size_t)r) != 0) {
log_error("转发响应到客户端失败");
recv_ok = false;
break;
}
total += r;
}
if (recv_ok) {
log_debug("代理完成: %s -> 后端 %s:%d%s, 转发 %zd bytes",
req->path, backend->target_host, backend->target_port,
forwarded_path, total);
success = true;
}
}
if (total_forwarded) *total_forwarded = total;
/* 清理后端连接 */
if (use_https) {
proxy_tls_close(tls_conn);
} else {
cocoon_socket_close(backend_fd);
}
return success;
}
bool proxy_forward(cocoon_socket_t client_fd, const http_request_t *req,
cocoon_proxy_rule_t *rule,
const struct sockaddr_storage *client_addr) {
if (!rule || rule->backend_count == 0) {
log_error("代理规则无后端");
return false;
}
size_t start_idx = rule->current_index;
/* 第一轮:优先尝试 healthy 后端 */
for (size_t t = 0; t < rule->backend_count; t++) {
size_t idx = (start_idx + t) % rule->backend_count;
cocoon_proxy_backend_t *backend = &rule->backends[idx];
if (!backend->healthy) continue;
ssize_t total = 0;
if (proxy_relay_backend(client_fd, req, rule, backend, client_addr, &total)) {
proxy_update_health(backend, true);
rule->current_index = (idx + 1) % rule->backend_count;
return req->keep_alive;
}
proxy_update_health(backend, false);
}
/* 第二轮fallback 到所有后端(包括 unhealthy */
for (size_t t = 0; t < rule->backend_count; t++) {
size_t idx = (start_idx + t) % rule->backend_count;
cocoon_proxy_backend_t *backend = &rule->backends[idx];
ssize_t total = 0;
if (proxy_relay_backend(client_fd, req, rule, backend, client_addr, &total)) {
proxy_update_health(backend, true);
rule->current_index = (idx + 1) % rule->backend_count;
return req->keep_alive;
}
proxy_update_health(backend, false);
}
log_error("所有后端均不可用: %s", rule->path_prefix);
return false;
}