修复: 升级框架并完善报告导出

- 升级 Drogon 和 Trantor,修复畸形请求导致的连接计数泄漏\n- 增加第三方框架版本校验与自动重建\n- 完善完整报告导出和接口文档
This commit is contained in:
cloud
2026-08-10 09:50:09 +08:00
parent 99ed321d24
commit 0e28826073
82 changed files with 3095 additions and 566 deletions
+1
View File
@@ -43,6 +43,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC" AND BUILD_SHARED_LIBS)
else()
set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpFileImpl.cc
unittests/HttpFileTest.cc
unittests/HttpMethodTest.cc
unittests/WebsocketResponseTest.cc)
endif()
@@ -5,6 +5,7 @@
#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>
using namespace drogon;
@@ -100,23 +101,46 @@ DROGON_TEST(RequestStreamTest)
LOG_INFO << "Test request stream";
std::string filePath = "./中文.txt";
std::ifstream file(filePath);
std::stringstream content;
REQUIRE(file.is_open());
content << file.rdbuf();
const auto uniqueSuffix = std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count());
auto tempDir = std::make_shared<std::filesystem::path>(
std::filesystem::temp_directory_path() /
("request_stream_upload_test_" + uniqueSuffix));
std::filesystem::create_directories(*tempDir);
auto tempPath = std::make_shared<std::filesystem::path>(
*tempDir / std::filesystem::path(u8"中文.txt"));
tempPath->make_preferred();
{
std::ofstream out(*tempPath, std::ios::binary | std::ios::trunc);
REQUIRE(out.is_open());
out << "request-stream-upload-content\nline2\n";
}
req = HttpRequest::newFileUploadRequest({UploadFile{filePath}});
std::ifstream in(*tempPath, std::ios::binary);
REQUIRE(in.is_open());
std::stringstream ss;
ss << in.rdbuf();
const auto uploadContent = std::make_shared<std::string>(ss.str());
const auto uploadPathUtf8 = std::make_shared<std::string>([&tempPath]() {
auto u8Path = tempPath->u8string();
return std::string(reinterpret_cast<const char *>(u8Path.data()),
u8Path.size());
}());
req = HttpRequest::newFileUploadRequest({UploadFile{*uploadPathUtf8}});
req->setPath("/stream_upload_echo");
req->setMethod(Post);
client->sendRequest(req,
[TEST_CTX,
content = content.str()](ReqResult r,
const HttpResponsePtr &resp) {
CHECK(r == ReqResult::Ok);
CHECK(resp->statusCode() == k200OK);
CHECK(resp->body() == content);
});
client->sendRequest(
req,
[TEST_CTX, tempPath, tempDir, uploadPathUtf8, content = uploadContent](
ReqResult r, const HttpResponsePtr &resp) {
CHECK(r == ReqResult::Ok);
CHECK(resp->statusCode() == k200OK);
CHECK(resp->body() == *content);
std::error_code ec;
std::filesystem::remove(*tempPath, ec);
std::filesystem::remove(*tempDir, ec);
});
checkStreamRequest(TEST_CTX,
client->getLoop(),
@@ -728,6 +728,23 @@ void doTest(const HttpClientPtr &client, std::shared_ptr<test::Case> TEST_CTX)
CHECK((*json)["P2"] == "test");
});
// Test file upload from memory
auto hello = std::make_shared<std::string>("hello world!");
UploadFile memfile(hello->data(),
hello->length(),
"hello_world.txt",
"hellofile",
ContentType::CT_TEXT_PLAIN);
req = HttpRequest::newFileUploadRequest({memfile});
req->setPath("/api/attachment/uploadMemory");
client->sendRequest(req,
[req, TEST_CTX, hello](ReqResult result,
const HttpResponsePtr &resp) {
REQUIRE(result == ReqResult::Ok);
REQUIRE(resp->contentType() == CT_TEXT_PLAIN);
CHECK(resp->getBody() == *hello);
});
// Test newFileResponse
req = HttpRequest::newHttpRequest();
req->setPath("/RangeTestController/");
@@ -103,6 +103,32 @@ void Attachment::uploadImage(
callback(resp);
}
void Attachment::uploadMemory(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback)
{
MultiPartParser fileUpload;
if (fileUpload.parse(req) == 0 && fileUpload.getFiles().size() == 1)
{
auto &file = fileUpload.getFiles()[0];
if (file.getItemName() == "hellofile")
{
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(HttpStatusCode::k200OK);
resp->setContentTypeCode(ContentType::CT_TEXT_PLAIN);
std::string hello = std::string(file.fileData(), file.fileLength());
resp->setBody(std::move(hello));
callback(resp);
return;
}
}
LOG_DEBUG << "upload text from memory error!";
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(HttpStatusCode::k400BadRequest);
callback(resp);
}
void Attachment::download(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback)
@@ -12,6 +12,7 @@ class Attachment : public drogon::HttpController<Attachment>
METHOD_ADD(Attachment::get, "", Get); // Path is '/api/attachment'
METHOD_ADD(Attachment::upload, "/upload", Post);
METHOD_ADD(Attachment::uploadImage, "/uploadImage", Post);
METHOD_ADD(Attachment::uploadMemory, "/uploadMemory", Post);
METHOD_ADD(Attachment::download, "/download", Get);
METHOD_LIST_END
// your declaration of processing function maybe like this:
@@ -21,6 +22,8 @@ class Attachment : public drogon::HttpController<Attachment>
std::function<void(const HttpResponsePtr &)> &&callback);
void uploadImage(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
void uploadMemory(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
void download(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
};
@@ -9,6 +9,7 @@ DROGON_TEST(Base64)
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(encoded == "ZHJvZ29uIGZyYW1ld29yaw==");
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
SUBSECTION(InvalidChars)
{
@@ -31,6 +32,7 @@ DROGON_TEST(Base64)
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(encoded == "ZHJvZ29uIGZyYW1ld29yaw");
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(LongString)
@@ -46,6 +48,9 @@ DROGON_TEST(Base64)
auto encoded = drogon::utils::base64Encode(in);
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(decoded == in);
CHECK(out == encoded);
CHECK(drogon::utils::isBase64(out));
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(URLSafe)
@@ -55,6 +60,7 @@ DROGON_TEST(Base64)
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(encoded == "ZHJvZ29uIGZyYW1ld29yaw==");
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(UnpaddedURLSafe)
@@ -64,6 +70,7 @@ DROGON_TEST(Base64)
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(encoded == "ZHJvZ29uIGZyYW1ld29yaw");
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(LongURLSafe)
@@ -77,5 +84,24 @@ DROGON_TEST(Base64)
auto encoded = drogon::utils::base64Encode(in, true);
auto decoded = drogon::utils::base64Decode(encoded);
CHECK(decoded == in);
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(emptyString)
{
auto encoded = "";
CHECK(!drogon::utils::isBase64(encoded));
}
SUBSECTION(size1Padding)
{
auto encoded = "ZHJvZ29uIGZyYW1ld29=";
CHECK(drogon::utils::isBase64(encoded));
}
SUBSECTION(size1PaddingNotModulo4)
{
auto encoded = "ZHJvZ29uIGZyYW1ld29ya=";
CHECK(!drogon::utils::isBase64(encoded));
}
}
@@ -66,3 +66,195 @@ DROGON_TEST(ResquestSetCustomContentTypeString)
req->setContentTypeString("thisdoesnotexist/unknown");
CHECK(req->getContentType() == CT_CUSTOM);
}
DROGON_TEST(HttpOptionsHeadersResponse)
{
auto req = HttpRequest::newHttpRequest();
auto resp = HttpResponse::newOptionsResponse(req);
CHECK(!resp);
req->setMethod(HttpMethod::Options);
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Vary") == "Origin");
CHECK(resp->getHeader("Allow") == "OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "");
CHECK(resp->getHeader("Access-Control-Allow-Methods") == "");
req->attributes()->insert("drogon.corsMethods",
std::string("GET, POST, OPTIONS"));
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Vary") == "Origin");
CHECK(resp->getHeader("Allow") == "GET, POST, OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "");
CHECK(resp->getHeader("Access-Control-Allow-Methods") == "");
req->addHeader("Origin", "http://somepage");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Vary") == "Origin");
CHECK(resp->getHeader("Allow") == "GET, POST, OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "");
CHECK(resp->getHeader("Access-Control-Allow-Methods") == "");
}
DROGON_TEST(HttpCorsHeadersResponse)
{
auto req = HttpRequest::newHttpRequest();
req->addHeader("Origin", "");
req->addHeader("Access-Control-Request-Method", "OPTIONS");
auto resp = HttpResponse::newOptionsResponse(req);
CHECK(!resp);
// empty origin -> error
req->setMethod(HttpMethod::Options);
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k400BadRequest);
// null origin -> check if allowed or not
req->addHeader("Origin", "null");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k403Forbidden);
resp = HttpResponse::newOptionsResponse(req, {}, true);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "null");
// normal origin but no requested method -> error
req->addHeader("Origin", "http://somepage");
req->addHeader("Access-Control-Request-Method", "");
resp = HttpResponse::newOptionsResponse(req, {}, true);
CHECK(resp->getStatusCode() == HttpStatusCode::k400BadRequest);
// valid CORS preflight request
req->addHeader("Access-Control-Request-Method", "OPTIONS");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Vary") == "Origin");
CHECK(resp->getHeader("Allow") == "");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "http://somepage");
CHECK(resp->getHeader("Access-Control-Allow-Methods") == "OPTIONS");
// origin validator
resp = HttpResponse::newOptionsResponse(req, [](std::string_view origin) {
return origin == "http://somepage";
});
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
resp = HttpResponse::newOptionsResponse(req, [](std::string_view origin) {
return origin != "http://somepage";
});
CHECK(resp->getStatusCode() == HttpStatusCode::k403Forbidden);
// unallowed method
req->addHeader("Access-Control-Request-Method", "PUT");
req->attributes()->insert("drogon.corsMethods",
std::string("GET,POST,OPTIONS"));
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k405MethodNotAllowed);
CHECK(resp->getHeader("Allow") == "GET,POST,OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Methods") ==
"GET,POST,OPTIONS");
// allowed method
req->addHeader("Access-Control-Request-Method", "GET");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Allow") == "");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "http://somepage");
CHECK(resp->getHeader("Access-Control-Allow-Methods") ==
"GET,POST,OPTIONS");
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "");
CHECK(resp->getHeader("Access-Control-Allow-Private-Network") == "");
CHECK(resp->getHeader("Access-Control-Max-Age") == "");
// no restriction on requested headers
req->addHeader("Access-Control-Request-Headers", "X-Foo, X-Bar");
resp = HttpResponse::newOptionsResponse(req);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Headers") == "X-Bar,X-Foo");
// unallowed header
resp = HttpResponse::newOptionsResponse(req, {"X-Foo"});
CHECK(resp->getStatusCode() == HttpStatusCode::k403Forbidden);
CHECK(resp->getHeader("Access-Control-Allow-Headers") == "X-Foo");
// all requested headers allowed
resp = HttpResponse::newOptionsResponse(req, {"X-Foo", "X-Bar"});
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Headers") == "X-Bar,X-Foo");
// allow credentials
resp = HttpResponse::newOptionsResponse(req, nullptr, false, true);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "true");
// private network access
req->addHeader("Access-Control-Request-Private-Network", "true");
resp = HttpResponse::newOptionsResponse(req, nullptr, false, false, false);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Private-Network") == "");
resp = HttpResponse::newOptionsResponse(req, nullptr, false, false, true);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Allow-Private-Network") == "true");
// CORS max age
resp = HttpResponse::newOptionsResponse(
req, nullptr, false, false, false, 600);
CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent);
CHECK(resp->getHeader("Access-Control-Max-Age") == "600");
}
DROGON_TEST(AddHttpCorsHeaders)
{
using namespace std::literals;
// no Origin -> do nothing
auto req = HttpRequest::newHttpRequest();
req->setMethod(Get);
auto resp = HttpResponse::newHttpResponse();
resp->addCorsHeaders(req, {"X-Foo"}, true);
CHECK(resp->headers().empty());
// with Origin -> Allow-Origin + Vary (not overwritten) + Expose-Headers
req->addHeader("Origin", "http://somepage");
resp->addHeader("Vary", "X-SomeHeader");
resp->addCorsHeaders(req, {"X-Foo"});
CHECK(resp->getHeader("Vary") == "Origin,X-SomeHeader");
CHECK(resp->getHeader("Access-Control-Allow-Origin") == "http://somepage");
CHECK(resp->getHeader("Access-Control-Expose-Headers") == "X-Foo");
// add a new exposed header
resp->addCorsHeaders(req, {"X-Bar"});
CHECK(resp->getHeader("Access-Control-Expose-Headers") == "X-Bar,X-Foo");
// no duplicate Origin in Vary
CHECK(resp->getHeader("Vary") == "Origin,X-SomeHeader");
// check credentials (true/false/unchanged)
resp->addCorsHeaders(req, {}, true);
CHECK(resp->getHeader("Access-Control-Expose-Headers") == "X-Bar,X-Foo");
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "true");
resp->addCorsHeaders(req, {}, false);
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "");
resp->addCorsHeaders(req, {}, true);
resp->addCorsHeaders(req);
CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "true");
}
DROGON_TEST(ClearHeaders)
{
auto req = HttpRequest::newHttpRequest();
// set a custom path to ensure it is not cleared
req->setPath("/api/test");
req->addHeader("X-Test", "value");
req->addHeader("Authorization", "Bearer token");
CHECK(req->headers().size() == 2);
CHECK(req->getHeader("X-Test") == "value");
CHECK(req->getHeader("Authorization") == "Bearer token");
req->clearHeaders();
CHECK(req->headers().empty());
// verify path unchanged
CHECK(req->path() == "/api/test");
}
@@ -0,0 +1,120 @@
#include <drogon/drogon_test.h>
#include <drogon/HttpTypes.h>
#include <trantor/utils/MsgBuffer.h>
#include "../../lib/src/HttpRequestImpl.h"
using namespace drogon;
// Helper: parse a method string through HttpRequestImpl::setMethod
static std::pair<bool, HttpMethod> parseMethod(const std::string &str)
{
HttpRequestImpl req(nullptr);
bool ok = req.setMethod(str.data(), str.data() + str.size());
return {ok, req.method()};
}
DROGON_TEST(StandardHttpMethods)
{
auto [ok, m] = parseMethod("GET");
CHECK(ok);
CHECK(m == Get);
std::tie(ok, m) = parseMethod("POST");
CHECK(ok);
CHECK(m == Post);
std::tie(ok, m) = parseMethod("PUT");
CHECK(ok);
CHECK(m == Put);
std::tie(ok, m) = parseMethod("DELETE");
CHECK(ok);
CHECK(m == Delete);
std::tie(ok, m) = parseMethod("HEAD");
CHECK(ok);
CHECK(m == Head);
std::tie(ok, m) = parseMethod("OPTIONS");
CHECK(ok);
CHECK(m == Options);
std::tie(ok, m) = parseMethod("PATCH");
CHECK(ok);
CHECK(m == Patch);
}
DROGON_TEST(WebDavMethods)
{
auto [ok, m] = parseMethod("PROPFIND");
CHECK(ok);
CHECK(m == Propfind);
std::tie(ok, m) = parseMethod("MKCOL");
CHECK(ok);
CHECK(m == Mkcol);
std::tie(ok, m) = parseMethod("COPY");
CHECK(ok);
CHECK(m == Copy);
std::tie(ok, m) = parseMethod("MOVE");
CHECK(ok);
CHECK(m == Move);
}
DROGON_TEST(WebDavMethodStrings)
{
CHECK(to_string_view(Propfind) == "PROPFIND");
CHECK(to_string_view(Mkcol) == "MKCOL");
CHECK(to_string_view(Copy) == "COPY");
CHECK(to_string_view(Move) == "MOVE");
}
// Helper: serialize a request and return the first line (method + path)
static std::string serializeMethod(HttpMethod method)
{
HttpRequestImpl req(nullptr);
req.setMethod(method);
req.setPath("/test");
trantor::MsgBuffer buf;
req.appendToBuffer(&buf);
std::string result(buf.peek(), buf.readableBytes());
// Return just up to the first space after the method
auto pos = result.find(' ');
return result.substr(0, pos);
}
DROGON_TEST(MethodSerialization)
{
CHECK(serializeMethod(Get) == "GET");
CHECK(serializeMethod(Post) == "POST");
CHECK(serializeMethod(Put) == "PUT");
CHECK(serializeMethod(Delete) == "DELETE");
CHECK(serializeMethod(Head) == "HEAD");
CHECK(serializeMethod(Options) == "OPTIONS");
CHECK(serializeMethod(Patch) == "PATCH");
CHECK(serializeMethod(Propfind) == "PROPFIND");
CHECK(serializeMethod(Mkcol) == "MKCOL");
CHECK(serializeMethod(Copy) == "COPY");
CHECK(serializeMethod(Move) == "MOVE");
}
DROGON_TEST(InvalidMethodsRejected)
{
auto [ok, m] = parseMethod("INVALID");
CHECK(!ok);
CHECK(m == Invalid);
std::tie(ok, m) = parseMethod("LOCK");
CHECK(!ok);
CHECK(m == Invalid);
std::tie(ok, m) = parseMethod("");
CHECK(!ok);
CHECK(m == Invalid);
std::tie(ok, m) = parseMethod("G");
CHECK(!ok);
CHECK(m == Invalid);
}