feat(http): decouple upload size limit from maxReqSize and harden file upload (#4780)

- Add maxUploadSize config (default 1GB) for custom HttpBody streaming uploads;
  maxReqSize now only controls in-memory buffering threshold
- Reject uploads exceeding maxUploadSize with 413 before writing any data
- Delete incomplete files on HttpFileStorage destruction
- Open HttpFileStorage in "wb" mode instead of "ab" to avoid stale data
- Pass content_size to writeData() for integrity validation
- Use uint64_t for content_len to support large file uploads
- Wrap HttpBody _on_completed callback in try-catch to prevent unhandled exceptions
- Make GET_CONFIG global variables read-only
- Update ZLToolKit submodule

---------

Co-authored-by: xiongziliang <771730766@qq.com>
This commit is contained in:
YuLi
2026-07-22 22:55:35 -07:00
committed by GitHub
parent daaa74a72e
commit 4a24057e73
8 changed files with 100 additions and 32 deletions

View File

@@ -401,6 +401,10 @@ keepAliveSecond=30
# http请求体最大字节数如果post的body太大则不适合缓存body在内存
# Maximum number of bytes for the HTTP request body. If the POST body is too large, it is not suitable to cache the body in memory.
maxReqSize=40960
# 自定义HttpBody流式接收请求体的最大字节数maxReqSize仍仅控制内存缓存阈值
# Maximum request body size streamed into a custom HttpBody; maxReqSize remains an in-memory buffering threshold
maxUploadSize=1073741824
# 404网页内容用户可以自定义404网页
# Custom 404 page content. Users can customize the 404 response page here.
#notFound=<html><head><title>404 Not Found</title></head><body bgcolor="white"><center><h1>您访问的资源不存在!</h1></center><hr><center>ZLMediaKit-4.0</center></body></html>

View File

@@ -1488,7 +1488,8 @@ void installWebApi() {
// Compatible with old version requests, the new version removes the only_audio parameter and adds the only_track parameter
only_track = 1;
}
GET_CONFIG(std::string, local_ip, General::kListenIP)
GET_CONFIG(std::string, s_local_ip, General::kListenIP)
auto local_ip = s_local_ip;
if (!allArgs["local_ip"].empty()) {
local_ip = allArgs["local_ip"];
}

View File

@@ -191,6 +191,7 @@ namespace Http {
#define HTTP_FIELD "http."
const string kSendBufSize = HTTP_FIELD "sendBufSize";
const string kMaxReqSize = HTTP_FIELD "maxReqSize";
const string kMaxUploadSize = HTTP_FIELD "maxUploadSize";
const string kKeepAliveSecond = HTTP_FIELD "keepAliveSecond";
const string kCharSet = HTTP_FIELD "charSet";
const string kRootPath = HTTP_FIELD "rootPath";
@@ -205,6 +206,7 @@ const string kAllowIPRange = HTTP_FIELD "allow_ip_range";
static onceToken token([]() {
mINI::Instance()[kSendBufSize] = 64 * 1024;
mINI::Instance()[kMaxReqSize] = 4 * 10240;
mINI::Instance()[kMaxUploadSize] = 1024ULL * 1024 * 1024;
mINI::Instance()[kKeepAliveSecond] = 15;
mINI::Instance()[kDirMenu] = true;
mINI::Instance()[kVirtualPath] = "";

View File

@@ -194,19 +194,21 @@ extern const std::string kBroadcastCreateMuxer;
} while (0)
#define GET_CONFIG(type, arg, key) \
static type arg = ::toolkit::mINI::Instance()[key]; \
LISTEN_RELOAD_KEY(arg, key, { RELOAD_KEY(arg, key); });
static type arg##_storage_ = ::toolkit::mINI::Instance()[key]; \
static const type &arg = arg##_storage_; \
LISTEN_RELOAD_KEY(arg##_storage_, key, { RELOAD_KEY(arg##_storage_, key); });
#define GET_CONFIG_FUNC(type, arg, key, ...) \
static type arg; \
static type arg##_storage_; \
static const type &arg = arg##_storage_; \
do { \
static ::toolkit::onceToken s_token_set([]() { \
static auto lam = __VA_ARGS__; \
static auto arg##_str = ::toolkit::mINI::Instance()[key]; \
arg = lam(arg##_str); \
LISTEN_RELOAD_KEY(arg, key, { \
arg##_storage_ = lam(arg##_str); \
LISTEN_RELOAD_KEY(arg##_str, key, { \
RELOAD_KEY(arg##_str, key); \
arg = lam(arg##_str); \
arg##_storage_ = lam(arg##_str); \
}); \
}); \
} while (0)
@@ -356,6 +358,9 @@ extern const std::string kSendBufSize;
// http 最大请求字节数 [AUTO-TRANSLATED:8239eb9c]
// HTTP maximum request byte size
extern const std::string kMaxReqSize;
// 自定义HttpBody流式接收请求体的最大字节数
// Maximum request body size streamed into a custom HttpBody
extern const std::string kMaxUploadSize;
// http keep-alive秒数 [AUTO-TRANSLATED:d4930c66]
// HTTP keep-alive seconds
extern const std::string kKeepAliveSecond;

View File

@@ -405,7 +405,7 @@ Buffer::Ptr HttpBufferBody::readData(size_t size) {
// ─────────────────────────────────────────────────────────────────────────────
HttpFileStorage::HttpFileStorage(std::string file_path) {
_fp = fopen(file_path.data(), "ab");
_fp = fopen(file_path.data(), "wb");
if (!_fp) {
throw std::runtime_error("HttpFileStorage: failed to open file: " + file_path + ", err: " + toolkit::get_uv_errmsg());
}
@@ -418,9 +418,14 @@ HttpFileStorage::~HttpFileStorage() {
fclose(_fp);
_fp = nullptr;
}
if (_written != _content_size || !_written) {
// 删除不完整的文件
File::delete_file(_path);
}
}
void HttpFileStorage::writeData(const char *data, size_t size) {
void HttpFileStorage::writeData(const char *data, size_t size, uint64_t content_size) {
_content_size = content_size;
if (!_fp) {
throw std::runtime_error("HttpFileStorage: file not open");
}

View File

@@ -35,7 +35,11 @@ public:
using Ptr = std::shared_ptr<HttpBody>;
virtual ~HttpBody() {
if (_on_completed) {
try {
_on_completed();
} catch (std::exception &ex) {
ErrorL << ex.what();
}
}
}
@@ -97,7 +101,7 @@ public:
* 写入数据,默认抛出异常表示不支持写入
* Write data, default throws exception indicating write not supported
*/
virtual void writeData(const char *data, size_t size) {
virtual void writeData(const char *data, size_t size, uint64_t content_size) {
throw std::runtime_error("HttpBody::writeData not supported");
}
@@ -207,20 +211,21 @@ public:
using Ptr = std::shared_ptr<HttpFileStorage>;
/**
* @param file_path 文件路径,文件以二进制追加模式打开
* @param file_path File path, opened in binary append mode
* @param file_path 文件路径,文件以二进制写入模式打开(覆盖已有内容)
* @param file_path File path, opened in binary write mode (truncates existing content)
*/
HttpFileStorage(std::string file_path);
~HttpFileStorage() override;
void writeData(const char *data, size_t size) override;
void writeData(const char *data, size_t size, uint64_t content_size) override;
int64_t remainSize() override;
toolkit::Buffer::Ptr readData(size_t size) override;
const std::string& filePath() const;
private:
FILE *_fp = nullptr;
int64_t _written = 0;
uint64_t _written = 0;
uint64_t _content_size = 0;
std::string _path;
};

View File

@@ -82,7 +82,7 @@ ssize_t HttpSession::onRecvHeader(const char *header, size_t len) {
return 0;
}
size_t content_len;
uint64_t content_len;
auto &content_len_str = _parser["Content-Length"];
if (content_len_str.empty()) {
if (it->first == "POST") {
@@ -114,23 +114,75 @@ ssize_t HttpSession::onRecvHeader(const char *header, size_t len) {
NOTICE_EMIT(BroadcastBeforeHttpRequestArgs, Broadcast::kBroadcastBeforeHttpRequest, _parser, body, *this);
}
if (content_len > _max_req_size || body) {
// 自定义HttpBody直接消费网络分片不经过HttpRequestSplitter的内存缓存因此maxReqSize只负责选择缓存策略
// 不能作为这里的上传上限否则未携带Content-Length的正常流式上传会在默认40KB左右被错误拒绝。
// A custom HttpBody consumes network fragments directly without HttpRequestSplitter buffering. Therefore maxReqSize
// only selects the buffering strategy and must not be used as the upload limit, or normal unknown-length uploads
// would be rejected at roughly the default 40KB threshold.
if (body) {
GET_CONFIG(uint64_t, max_upload_size_config, Http::kMaxUploadSize);
// 已知长度可以在接收body前判断是否超限避免先向HttpBody写入数据再拒绝请求;
// 如果上传文件时不指定content-len也直接拒绝因为后续没法触发文件上传完毕事件
if (content_len > max_upload_size_config) {
WarnL << "Http upload size is too huge or no content-len provided: " << content_len << " > " << max_upload_size_config
<< ", please set " << Http::kMaxUploadSize << " in config.ini file.";
sendResponse(413, true);
_parser.clear();
// 仍返回不定长body模式并丢弃连接关闭前可能到达的数据防止splitter把body误当成下一条请求头。
// Keep the splitter in variable-body mode and discard data arriving before close, so body bytes are not
// interpreted as another request header.
_on_recv_body = [](const char *, size_t) { return true; };
return -1;
}
size_t received = 0;
_on_recv_body = [this, received, content_len, body, it](const char *data, size_t len) mutable {
auto remain = content_len - received;
if (len > remain) {
// 告知HttpFileStorage写入的数据长度超过content_len触发文件删除操作
body->writeData(data, len, content_len);
// 上传的数据超过声明的content-len 直接拒绝
sendResponse(413, true);
WarnL << "Upload file size larger than content_len: " << received + len << " > " << content_len;
return false;
}
received += len;
body->writeData(data, len, content_len);
if (received < content_len) {
// 还没收满 [AUTO-TRANSLATED:cecc867e]
// Not yet received
return true;
}
// 收满了 [AUTO-TRANSLATED:0c9cebd7]
// Received full
setContentLen(0);
_parser.setBody(std::move(body));
(this->*(it->second))();
_parser.clear();
return false;
};
// 声明后续都是bodyHttp body在本对象缓冲不通过HttpRequestSplitter保存 [AUTO-TRANSLATED:0012b6c1]
// Declare that the following is all body; Http body is buffered in this object, not saved through HttpRequestSplitter
return -1;
}
// 未提供自定义HttpBody时保持原有语义maxReqSize仅决定是否放弃内存整包缓存并改为分片回调
// 不在本分支中把它扩展为普通请求体的硬上限。
// Without a custom HttpBody, preserve the original behavior: maxReqSize only switches from whole-body buffering
// to fragment callbacks; it is not extended into a hard limit for ordinary request bodies in this branch.
if (content_len > _max_req_size) {
// // 不定长body或超大body //// [AUTO-TRANSLATED:8d66ee77]
// // Indefinite length body or oversized body ////
if (content_len != SIZE_MAX && !body) {
if (content_len != SIZE_MAX) {
WarnL << "Http body size is too huge: " << content_len << " > " << _max_req_size
<< ", please set " << Http::kMaxReqSize << " in config.ini file.";
}
size_t received = 0;
_on_recv_body = [this, received, content_len, body, it](const char *data, size_t len) mutable {
_on_recv_body = [this, received, content_len](const char *data, size_t len) mutable {
received += len;
if (body) {
body->writeData(data, len);
} else {
onRecvUnlimitedContent(_parser, data, len, content_len, received);
}
if (received < content_len) {
// 还没收满 [AUTO-TRANSLATED:cecc867e]
// Not yet received
@@ -140,13 +192,7 @@ ssize_t HttpSession::onRecvHeader(const char *header, size_t len) {
// 收满了 [AUTO-TRANSLATED:0c9cebd7]
// Received full
setContentLen(0);
if (body) {
_parser.setBody(std::move(body));
(this->*(it->second))();
}
_parser.clear();
return false;
};
// 声明后续都是bodyHttp body在本对象缓冲不通过HttpRequestSplitter保存 [AUTO-TRANSLATED:0012b6c1]