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

- 升级 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
@@ -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);
}