fix(libhttp): WebSocket 发送/代理队列上限防 OOM (P1-6)

- 新增 setMaxWsQueueSize()(default 1000),每个入队点检查
- wsSend / onProxyReadClient / onProxyReadBackend 超限关闭连接
- 更新审计文档 P1-6 状态、README v3.4.3
This commit is contained in:
qing committed 2026-08-04 18:27:50 +08:00
1 parent 37c6d401d6
commit 4d9bb5d940
4 files changed
+38 -4

No files matched your search

@@ -11,9 +11,9 @@
| 优先级 | 总数 | 已修复 | 部分修复 | 未修复 |
|--------|------|--------|----------|--------|
| P0 | 0 | 0 | 0 | 0 |
| P1 | 6 | 5 | 0 | 1 |
| P1 | 6 | 6 | 0 | 0 |
| P2 | 4 | 0 | 0 | 4 |
| **合计** | **10** | **7** | **0** | **3** |
| **合计** | **10** | **8** | **0** | **2** |
---
@@ -26,7 +26,7 @@
| P1-3 | WebSocket 消息无大小限制 | ✅ 已修复 | `HttpServer` 新增 `max_ws_message_size_`(默认 16MB,与 Beast 默认一致)与 `setMaxWsMessageSize()`/`getMaxWsMessageSize()`。`doWsUpgrade`/`doWssUpgrade` 及代理流 `onProxyConnect`/`onProxySslHandshake` 创建后调用 `read_message_max()`,超限消息由 Beast 在分配内存前拒绝并关闭连接,防 OOM |
| P1-4 | 服务器无读写超时(Slowloris) | ✅ 已修复 | 入站 `stream_` 现设置超时:`doSSLHandshake` 设头超时(覆盖握手),`doRead` 两阶段读(`async_read_header` 头超时 → `async_read` 体超时),`doWsRead` 每个 WS 读取设空闲超时。`HttpServer` 新增 `setHeaderTimeout()`(默认 10s)/`setReadTimeout()`(默认 30s),对齐 WAF ReadHeaderTimeout/ReadTimeout。慢速/空闲连接被清理,防 Slowloris/连接耗尽 |
| P1-5 | 静态文件目录路径遍历 | ✅ 已修复 | 新增 `isSafeStaticPath()`(`HttpServer_Request.cpp` L139-156),静态文件服务前校验 target_path(拒绝 `..`、空字节、非 `/` 开头、URL 编码 `%2e`/`%2f`/`%5c`),逻辑与 gateway `is_safe_target()` 对齐(`HttpServer.cpp` L94) |
| P1-6 | 发送队列无界(慢消费者 OOM) | ❌ 未修复 | `ws_send_queue_`、`proxy_to_backend_queue_`、`proxy_to_client_queue_`(`HttpServer_WsSession.cpp` L8-16/L362-367/L438-443)均为无界 `std::deque`。若客户端消费慢,服务端持续入队可导致内存耗尽 |
| P1-6 | 发送队列无界(慢消费者 OOM) | ✅ 已修复 | `HttpServer` 新增 `setMaxWsQueueSize()`(默认 1000),`wsSend` / `onProxyReadClient` / `onProxyReadBackend` 入队前检查,超过上限判定慢消费者/慢后端,关闭连接并清空队列,防止内存耗尽 |
---
@@ -74,7 +74,7 @@
### 短期(72小时)
- [x] P1-3 WS 消息大小限制:设置 `read_message_max`(可配置)→ ✅ 已实现(`setMaxWsMessageSize()`,默认 16MB,服务端与代理流均生效)
- [x] P1-5 静态文件路径遍历:新增 `isSafeStaticPath()` 拼接前校验 `target_path`,拒绝 `..`/空字节/编码遍历 → ✅ 已实现(`HttpServer_Request.cpp` L139-157)
- [ ] P1-6 发送队列上限:`ws_send_queue_`/代理队列设最大值,超限断开慢消费者
- [x] P1-6 发送队列上限:`ws_send_queue_`/代理队列设最大值,超限断开慢消费者 → ✅ 已实现(`setMaxWsQueueSize()`,默认 1000,入队前检查超限即关闭)
- [ ] P2-1 CORS 白名单:origin 白名单校验,替代反射任意 Origin
### 中期
+6
View File
@@ -15,6 +15,7 @@ C++20 network library for the UniServe microservice framework, providing unified
- **Client Management**: WebSocket client tracking, broadcast, and targeted send
- **WS Message Size Limit**: `setMaxWsMessageSize()` caps single WebSocket message (default 16MB, aligned with Beast; applies to server and proxy streams) to prevent OOM
- **Read Timeouts (Slowloris Protection)**: `setHeaderTimeout()` (default 10s) / `setReadTimeout()` (default 30s) enforce two-stage header/body read timeouts plus WS idle timeout, aligned with WAF
- **WS Queue Size Limit**: `setMaxWsQueueSize()` (default 1000) caps WS send / proxy queues; slow consumers get disconnected to prevent unbounded-memory OOM
### HttpClient
- **HTTP Client**: Synchronous HTTP requests via libcurl (GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS)
@@ -179,6 +180,10 @@ std::chrono::seconds getHeaderTimeout() const;
void setReadTimeout(std::chrono::seconds t);
std::chrono::seconds getReadTimeout() const;
// WS 发送/代理队列上限(条数)。默认 1000;超限判定慢消费者并关闭连接(防 OOM)。
void setMaxWsQueueSize(std::size_t n);
std::size_t getMaxWsQueueSize() const;
**WebSocket Routes**
```cpp
void ws(const std::string& path, WebSocketMessageHandler handler);
@@ -295,6 +300,7 @@ using WebSocketClientBase = HttpClient;
## Version History
- **v3.4.3**: WebSocket 发送/代理队列上限(P1-6)。新增 `setMaxWsQueueSize()`(默认 1000);`wsSend` / `onProxyReadClient` / `onProxyReadBackend` 入队前检查,超限判定慢消费者并关闭连接,防止无界队列内存耗尽。
- **v3.4.2**: 服务器读超时防 Slowloris(P1-4)。新增 `setHeaderTimeout()`(默认 10s)/`setReadTimeout()`(默认 30s);`doRead` 两阶段读(`async_read_header` 头超时 → `async_read` 体超时),`doSSLHandshake` 覆盖握手超时,`doWsRead` 设 WS 空闲超时,对齐 WAF ReadHeaderTimeout/ReadTimeout。
- **v3.4.1**: WebSocket 单条消息大小限制(P1-3)。新增 `setMaxWsMessageSize()`/`getMaxWsMessageSize()`(默认 16MB,与 Beast 默认一致),服务端 `doWsUpgrade`/`doWssUpgrade` 与代理 `onProxyConnect`/`onProxySslHandshake` 创建流后调用 `read_message_max()`,超限消息由 Beast 在分配内存前拒绝并关闭连接,防 OOM。
- **v3.4.0**: HttpClient 响应大小限制(防 OOM)。`request_stream_sync` 改为真流式(按块回调,不二次缓冲);新增 `set_max_response_size()`/`get_max_response_size()`(默认 10MB,对齐 WAF,0 表示不限制),超限时 `write_callback` 返回 0 中止传输并抛异常。
+6
View File
@@ -132,6 +132,11 @@ public:
void setReadTimeout(std::chrono::seconds t) { read_timeout_ = t; }
std::chrono::seconds getReadTimeout() const { return read_timeout_; }
// WS 发送/代理队列上限(条数)。默认 1000。防止慢消费者导致无界队列内存耗尽(P1-6)。
// 超限时判定为慢消费者,关闭对应连接。
void setMaxWsQueueSize(std::size_t n) { max_ws_queue_size_ = n; }
std::size_t getMaxWsQueueSize() const { return max_ws_queue_size_; }
class debug {
public:
static void print_all_header_fields(const http::request<http::string_body>& req);
@@ -150,6 +155,7 @@ private:
std::size_t max_ws_message_size_ = 16 * 1024 * 1024; // 默认 16MB,与 Beast 默认一致
std::chrono::seconds header_timeout_{10}; // 头/握手超时,防 Slowloris(对齐 WAF ReadHeaderTimeout)
std::chrono::seconds read_timeout_{30}; // 体超时(对齐 WAF ReadTimeout)
std::size_t max_ws_queue_size_ = 1000; // WS 发送/代理队列上限,防慢消费者 OOM(P1-6)
bool ssl_enabled_{false};
std::string cert_file_;
+22
View File
@@ -8,6 +8,14 @@ namespace net {
void HttpServer::Session::wsSend(const std::string& message) {
auto exec = stream_.get_executor();
asio::post(exec, [self = shared_from_this(), message]() {
// P1-6:超过队列上限,判定慢消费者,关闭连接防内存耗尽
if (self->ws_send_queue_.size() >= self->server_->getMaxWsQueueSize()) {
std::cerr << "[WS] ws_send_queue_ overflow (" << self->ws_send_queue_.size()
<< " >= " << self->server_->getMaxWsQueueSize()
<< ") from " << self->getRemoteAddress() << ", closing" << std::endl;
self->close();
return;
}
self->ws_send_queue_.push_back(message);
if (self->ws_send_queue_.size() == 1) {
self->doWsWrite();
@@ -373,6 +381,13 @@ void HttpServer::Session::onProxyReadClient(beast::error_code ec, std::size_t by
// Queue to backend
auto exec = stream_.get_executor();
asio::post(exec, [self = shared_from_this(), data = std::move(data), is_binary]() {
// P1-6:超过队列上限,判定慢后端,关闭代理连接防内存耗尽
if (self->proxy_to_backend_queue_.size() >= self->server_->getMaxWsQueueSize()) {
std::cerr << "[WS-Proxy] proxy_to_backend_queue_ overflow ("
<< self->proxy_to_backend_queue_.size() << ") closing" << std::endl;
self->closeProxy(websocket::close_code::internal_error);
return;
}
self->proxy_to_backend_queue_.emplace_back(std::move(data), is_binary);
if (self->proxy_to_backend_queue_.size() == 1) {
self->doProxyWriteBackend();
@@ -449,6 +464,13 @@ void HttpServer::Session::onProxyReadBackend(beast::error_code ec, std::size_t b
// Queue to client
auto exec = stream_.get_executor();
asio::post(exec, [self = shared_from_this(), data = std::move(data), is_binary]() {
// P1-6:超过队列上限,判定慢客户端,关闭代理连接防内存耗尽
if (self->proxy_to_client_queue_.size() >= self->server_->getMaxWsQueueSize()) {
std::cerr << "[WS-Proxy] proxy_to_client_queue_ overflow ("
<< self->proxy_to_client_queue_.size() << ") closing" << std::endl;
self->closeProxy(websocket::close_code::internal_error);
return;
}
self->proxy_to_client_queue_.emplace_back(std::move(data), is_binary);
if (self->proxy_to_client_queue_.size() == 1) {
self->doProxyWriteClient();