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

- 升级 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
+42
View File
@@ -148,4 +148,46 @@ class HttpCoroMiddleware : public DrObject<T>, public HttpMiddlewareBase
#endif
/**
* @brief Simple middleware that tags OPTIONS requests
* @details It adds the attribute "drogon.customCORShandling" to the request, so
* that HttpServer does not handle CORS for them internally.
*
* This allows custom CORS handling via the path handlers.
* For example to restrict the origins, headers allowed, specify a max age to
* avoid OPTIONS on every request, etc.
*
* Just register it:
* 1. globally via
* app().registerMiddleware(std::make_shared<drogon::HttpOptionsMiddleware>())
* 2. on every path handlers that need non-default handling, with
* ADD_METHOD_TO(..., drogon::Options, "drogon::HttpOptionsMiddleware")
*/
template <class Derived, bool AutoCreation = true>
class HttpOptionsMiddlewareImpl
: public drogon::HttpMiddleware<Derived, AutoCreation>
{
public:
void invoke(const HttpRequestPtr &req,
MiddlewareNextCallback &&nextCb,
MiddlewareCallback &&mcb) override
{
// Tag OPTIONS
if (req->method() == drogon::HttpMethod::Options)
req->attributes()->insert("drogon.customCORShandling", true);
// continue with next middleware (no post-processing here)
nextCb(std::move(mcb));
}
};
class HttpOptionsMiddlewareAuto
: public HttpOptionsMiddlewareImpl<HttpOptionsMiddlewareAuto, true>
{
};
class HttpOptionsMiddleware
: public HttpOptionsMiddlewareImpl<HttpOptionsMiddleware, false>
{
};
} // namespace drogon
+50
View File
@@ -159,6 +159,9 @@ class DROGON_EXPORT HttpRequest
*/
virtual void removeHeader(std::string key) = 0;
// Clear all HTTP headers
virtual void clearHeaders() = 0;
/// Get the cookie string identified by the field parameter
virtual const std::string &getCookie(const std::string &field) const = 0;
@@ -415,6 +418,8 @@ class DROGON_EXPORT HttpRequest
virtual void setMethod(const HttpMethod method) = 0;
/// Set the path of the request
/// @note The path is automatically encoded. use
/// @c setPathEncode(false) to avoid this.
virtual void setPath(const std::string &path) = 0;
virtual void setPath(std::string &&path) = 0;
@@ -432,6 +437,20 @@ class DROGON_EXPORT HttpRequest
virtual void setParameter(const std::string &key,
const std::string &value) = 0;
/**
* Set the parameter to the query,
* regardless of the HTTP method or content type
*/
virtual void setQueryParameter(const std::string &key,
const std::string &value) = 0;
/**
* Set the parameter to the request body.
* @warning The content type must be @c application/x-www-form-urlencoded
* or @c multipart/form-data
*/
virtual void setBodyParameter(const std::string &key,
const std::string &value) = 0;
/// Set or get the content type
virtual void setContentTypeCode(const ContentType type) = 0;
@@ -501,6 +520,37 @@ class DROGON_EXPORT HttpRequest
return toRequest(std::forward<T>(obj));
}
/*! \brief Check if the request is a CORS request.
* \details It should contain:
* - Origin: origination page
* \returns true if the Origin header is present
*/
inline bool isCorsRequest() const
{
// Check presence of required headers
return headers().find("origin") != headers().end();
}
/*! \brief Check if the request is a CORS pre-flight request.
* \details Check if the method of the request is OPTIONS and if it is
* a CORS pre-flight request.\n
* It should contain:
* - Origin: origination page
* - Access-Control-Request-Method: method to be used in the
* actual request
* \returns true if the method is OPTIONS and the required CORS pre-flight
* headers are present
*/
inline bool isCorsPreflightRequest() const
{
if (method() != HttpMethod::Options)
return false;
// Check presence of required headers
return isCorsRequest() &&
headers().find("access-control-request-method") !=
headers().end();
}
virtual bool isOnSecureConnection() const noexcept = 0;
virtual void setContentTypeString(const char *typeString,
size_t typeStringLength) = 0;
+143 -2
View File
@@ -161,6 +161,12 @@ class DROGON_EXPORT HttpResponse
setCustomStatusCode(code, message.data(), message.length());
}
/// Set whether the response should be compress.
virtual void setAllowCompression(bool allow) = 0;
/// Get whether the response allow compression.
virtual bool allowCompression() const = 0;
/// Get the creation timestamp of the response.
virtual const trantor::Date &creationDate() const = 0;
@@ -552,6 +558,141 @@ class DROGON_EXPORT HttpResponse
return toResponse(std::forward<T>(obj));
}
/*! \brief Create an OPTIONS or CORS pre-flight response
* \details If the request is not an OPTIONS request, returns a NULL
* response\n
* If it is a generic OPTIONS request, returns a 204 No Content
* response with the Allow header\n
* If it is a CORS pre-flight request, returns a 204 No Content
* response with the CORS headers set
*
* Other status codes for CORS pre-flight answers:
* - 400 Bad Request: if the request is malformed (missing
* required headers)
* - 403 Forbidden: if the Origin is not allowed + reason
* in a X-Cors-Error header
* - 403 Forbidden: if one of the headers in
* Access-Control-Request-Headers is not allowed + reason in
* a X-Cors-Error header
* - 405 Method Not Allowed: if the requested method is
* not allowed
* \note CORS is a browser-side security mechanism.\n
* Do not rely on Origin for authentication/authorization:
* non-browser clients can spoof or omit it.\n
* Enforce access control independently.
* \param[in] request Drogon (OPTIONS) request
* \param[in] allowedHeaders Set of allowed headers (for
* Access-Control-Allow-Headers header)\n
* (headers allowed by the controller path
* handler)
* \param[in] originValidator Function to validate the Origin header value
* (allow the origin or not)\n
* If allowCredentials is true, originValidator
* _SHOULD_ enforce a strict allowlist
* \param[in] allowNullOrigin Should be true to accept the "Origin: null"
* header\n
* (set for local file:// pages, sandboxed
* iframes, opaque origins, data: URIs)
* \param[in] allowCredentials Should be true to add the header
* "Access-Control-Allow-Credentials: true"
* (controls whether the browser may include
* credentials such as cookies, HTTP auth, or
* client certificates)\n
* Note: Authorization (bearer) is not a
* credential header; allow it via
* allowedHeaders when needed
* \param[in] allowPNA Should be true to accept the header
* "Access-Control-Request-Private-Network"
* (when a page from a less private address
* space is trying to reach a more private
* one, like internet -> intranet)\n
* Note: specific to Chromium & derivatives
* (Edge, Opera, Brave, ...), not in Firefox
* or Safari
* \param[in] maxAgeSeconds If set, adds the "Access-Control-Max-Age"
* header with the given value (in seconds,
* how long the results of a preflight
* request can be cached by the navigator)
* \returns the OPTIONS or CORS pre-flight response, or a null pointer if
* the request is not an OPTIONS request
*/
static HttpResponsePtr newOptionsResponse(
const HttpRequestPtr &request,
const std::function<bool(std::string_view)> &originValidator = nullptr,
bool allowNullOrigin = false,
bool allowCredentials = false,
bool allowPNA = true,
std::optional<unsigned int> maxAgeSeconds = {},
const std::optional<std::set<std::string_view>> &allowedHeaders =
std::nullopt);
/*! \copydoc newOptionsResponse(const HttpRequestPtr&,
* const std::function<bool(std::string_view)>&,
* bool, bool, bool,
* std::optional<unsigned int>,
* const std::optional<std::set<std::string_view>>&)
* \remarks Helper when specifying the allowed headers, when other
* parameters may be default, to avoid having to specify them all
*/
inline static HttpResponsePtr newOptionsResponse(
const HttpRequestPtr &request,
const std::set<std::string_view> &allowedHeaders,
const std::function<bool(std::string_view)> &originValidator = nullptr,
bool allowNullOrigin = false,
bool allowCredentials = false,
bool allowPNA = true,
std::optional<unsigned int> maxAgeSeconds = {})
{
return newOptionsResponse(request,
originValidator,
allowNullOrigin,
allowCredentials,
allowPNA,
maxAgeSeconds,
allowedHeaders);
}
/*! \brief Add CORS headers to a response
* \details Adds the CORS headers to a response for a normal request (a
* CORS request but not a CORS preflight request):
* - does nothing if it's an OPTIONS request, or
* - if it's not a CORS request, or
* - if it's a CORS preflight request
* Else:
* - adds Access-Control-Allow-Origin (if not yet present)
* - adds Origin to the Vary header,
* - sets or clears Access-Control-Allow-Credentials (if
* allowCredentials is set)
* - completes Access-Control-Expose-Headers
* \param[in] request Drogon request (to get Origin)
* \param[in] allowCredentials If set and true, adds the
* "Access-Control-Allow-Credentials: true
* header"\n
* If set and false, removes the
* "Access-Control-Allow-Credentials" header\n
* If not set, leaves the
* "Access-Control-Allow-Credentials" header
* untouched\n
* *MUST MATCH THE newOptionsResponse()
* PRE-FLIGHT RESPONSE VALUE*
* \param[in] exposedHeaders Set of exposed headers (for
* Access-Control-Expose-Headers header)\n
* These are the headers allowed to be exposed
* to javascript by the remote browser\n
* Note: they are *APPENDED* to any already
* present in the response, they are not
* REPLACED.\n
* This allows to complete them in the
* controller path handler.\n
* If you want to REPLACE them, remove the
* header before calling this function.
* \note may be use both in the controller path handler and in a
* pre-sending advice
*/
void addCorsHeaders(const HttpRequestPtr &request,
const std::set<std::string_view> &exposedHeaders = {},
const std::optional<bool> &allowCredentials = {});
/**
* @brief If the response is a file response (i.e. created by
* newFileResponse) returns the path on the filesystem. Otherwise a
@@ -560,9 +701,9 @@ class DROGON_EXPORT HttpResponse
virtual const std::string &sendfileName() const = 0;
/**
* @brief Returns the range of the file response as a pair ot size_t
* @brief Returns the range of the file response as a pair of size_t
* (offset, length). Length of 0 means the entire file is sent. Behavior of
* this function is undefined if the response if not a file response
* this function is undefined if the response is not a file response
*/
using SendfileRange = std::pair<size_t, size_t>; // { offset, length }
virtual const SendfileRange &sendfileRange() const = 0;
+12
View File
@@ -195,6 +195,10 @@ enum HttpMethod
Delete,
Options,
Patch,
Propfind,
Mkcol,
Copy,
Move,
Invalid
};
@@ -280,6 +284,14 @@ inline std::string_view to_string_view(drogon::HttpMethod method)
return "OPTIONS";
case drogon::HttpMethod::Patch:
return "PATCH";
case drogon::HttpMethod::Propfind:
return "PROPFIND";
case drogon::HttpMethod::Mkcol:
return "MKCOL";
case drogon::HttpMethod::Copy:
return "COPY";
case drogon::HttpMethod::Move:
return "MOVE";
default:
return "INVALID";
}
+33
View File
@@ -56,6 +56,27 @@ class UploadFile
}
}
/// Constructor
/**
* @param data Pointer to the data
* @param len Data length in bytes
* @param fileName The file name provided to the server.
* @param itemName The item name on the browser form.
* @param contentType The Mime content type for the part
*/
explicit UploadFile(const void *data,
const size_t len,
const std::string &fileName = "memory.bin",
const std::string &itemName = "file",
ContentType contentType = CT_APPLICATION_OCTET_STREAM)
: data_(data),
len_(len),
fileName_(fileName),
itemName_(itemName),
contentType_(contentType)
{
}
const std::string &path() const
{
return path_;
@@ -76,7 +97,19 @@ class UploadFile
return contentType_;
}
const void *data() const
{
return data_;
}
size_t dataLength() const
{
return len_;
}
private:
const void *data_ = nullptr;
size_t len_ = 0;
std::string path_;
std::string fileName_;
std::string itemName_;
@@ -11,6 +11,7 @@
#include <drogon/plugins/Plugin.h>
#include <trantor/utils/AsyncFileLogger.h>
#include <vector>
#include <regex>
namespace drogon
{
+162 -2
View File
@@ -124,6 +124,165 @@ DROGON_EXPORT std::set<std::string> splitStringToSet(
const std::string &str,
const std::string &separator);
/*! \brief Compare two string_views for equality, ignoring case.
* \warning This is locale dependent
* \param[in] str1 The first string_view.
* \param[in] str2 The second string_view.
* \return true if the string_views are equal, ignoring case; false otherwise.
*/
inline bool ci_equals(std::string_view str1, std::string_view str2)
{
if (str1.size() != str2.size())
return false;
return std::equal(str1.begin(),
str1.end(),
str2.begin(),
[](unsigned char a, unsigned char b) {
return std::tolower(a) == std::tolower(b);
});
}
/*! \details Trim leading and trailing spaces and tabs from a string_view,
* modifying it.
* \param[in,out] str The string_view to trim.
* \return The trimmed string_view.
*/
inline std::string_view &trim_inplace(std::string_view &str)
{
auto pos = str.find_first_not_of(" \t");
// defeat Windows macro "min"
str.remove_prefix((std::min)(pos, str.size()));
if (str.empty())
return str;
pos = str.find_last_not_of(" \t");
str.remove_suffix(str.size() - pos - 1);
return str;
}
/*! \brief Trim leading and trailing spaces and tabs from a string_view.
* \param[in] str The string_view to trim.
* \return A string_view with leading and trailing spaces and tabs removed.
*/
inline std::string_view trim(std::string_view str)
{
return trim_inplace(str);
}
/*! \brief Trim leading and trailing spaces and tabs from a rvalue string.
* \param[in] str The string to trim.
* \return The string with leading and trailing spaces and tabs removed.
*/
inline std::string trim(std::string &&str)
{
auto pos = str.find_last_not_of(" \t");
if (pos == std::string::npos)
return {};
str.resize(pos + 1);
pos = str.find_first_not_of(" \t");
if (pos > 0)
str.erase(0, pos);
return str;
}
/*! \brief Split a string_view into a vector of string_views.
* \param[in] str The string_view to split.
* \param[in] separator The separator to use for splitting.
* \param[in] trimValues Whether to trim whitespace from the resulting
* string_views.
* \param[in] acceptEmptyString Whether to include empty strings in the result.
* \return A vector of string_views obtained by splitting the input
* string_view.
*/
inline std::vector<std::string_view> splitStringView(
std::string_view str,
std::string_view separator,
bool trimValues = true,
bool acceptEmptyString = false)
{
std::vector<std::string_view> result;
if (separator.empty())
{
if (trimValues)
trim_inplace(str);
if (acceptEmptyString || !str.empty())
result.push_back(str);
return result;
}
size_t start = 0;
size_t end = 0;
while ((end = str.find(separator, start)) != std::string_view::npos)
{
auto token = str.substr(start, end - start);
if (trimValues)
trim_inplace(token);
if (acceptEmptyString || !token.empty())
result.push_back(token);
start = end + separator.size();
}
auto token = str.substr(start);
if (trimValues)
trim_inplace(token);
if (acceptEmptyString || !token.empty())
{
result.push_back(token);
}
return result;
}
/*! \brief Split a string_view into a set of string_views.
* \copyparams splitStringView
* \return A set of (unique) string_views obtained by splitting the input
* string_view.
* \note Uniqueness is case-sensitive: "A" and "a" are considered different
* values.
*/
inline std::set<std::string_view> splitStringViewToSet(
std::string_view str,
std::string_view separator,
bool trimValues = true,
bool acceptEmptyString = false)
{
auto v = splitStringView(str, separator, trimValues, acceptEmptyString);
return std::set<std::string_view>(v.begin(), v.end());
}
/*! \brief Join a vector of string_view into a string.
* \param[in] strs The vector of string_views to join.
* \param[in] separator The separator to use between string_views.
* \return A single string obtained by joining the input string_views with the
* specified separator.
* \note Empty values are skipped.
*/
inline std::string joinStringViews(const std::vector<std::string_view> &strs,
std::string_view separator)
{
std::string result;
for (std::string_view str : strs)
{
if (trim_inplace(str).empty())
continue;
if (!result.empty())
result.append(separator);
result.append(str);
}
return result;
}
/*! \brief Join a set of string_view into a string.
* \param[in] strs The set of string_views to join.
* \param[in] separator The separator to use between string_views.
* \return A single string obtained by joining the input string_views with the
* specified separator.
* \note Empty values are skipped.
*/
inline std::string joinStringViews(const std::set<std::string_view> &strs,
std::string_view separator)
{
return joinStringViews(std::vector<std::string_view>{strs.begin(),
strs.end()},
separator);
}
/// Get UUID string.
DROGON_EXPORT std::string getUuid(bool lowercase = true);
@@ -497,7 +656,8 @@ T fromString(const std::string &p) noexcept(false)
// ("1a" should not return 1)
if (pos != p.size())
throw std::invalid_argument("Invalid value");
if ((v < static_cast<long double>((std::numeric_limits<T>::min)())) ||
if ((v <
static_cast<long double>((std::numeric_limits<T>::lowest)())) ||
(v > static_cast<long double>((std::numeric_limits<T>::max)())))
throw std::out_of_range("Value out of range");
return static_cast<T>(v);
@@ -516,7 +676,7 @@ T fromString(const std::string &p) noexcept(false)
// throw if the whole string could not be parsed
// ("1a" should not return 1)
if (!ss.eof())
std::runtime_error("Bad type conversion");
throw std::runtime_error("Bad type conversion");
}
return value;
}