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

- 升级 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;
}
@@ -57,12 +57,6 @@ void HttpConnectionLimit::releaseConnection(
const trantor::TcpConnectionPtr &conn)
{
assert(!conn->connected());
if (!conn->hasContext())
{
// If the connection is connected to the SSL port and then
// disconnected before the SSL handshake.
return;
}
connectionNum_.fetch_sub(1, std::memory_order_relaxed);
if (maxConnectionNumPerIP_ > 0)
{
@@ -63,6 +63,11 @@ void HttpControllersRouter::init(
initMiddlewaresAndCorsMethods(iter.second);
}
for (auto &router : wsCtrlVector_)
{
initMiddlewaresAndCorsMethods(router);
}
for (auto &router : ctrlVector_)
{
router.regex_ = std::regex(router.pathParameterPattern_,
@@ -85,6 +90,7 @@ void HttpControllersRouter::reset()
ctrlMap_.clear();
ctrlVector_.clear();
wsCtrlMap_.clear();
wsCtrlVector_.clear();
}
std::vector<HttpHandlerInfo> HttpControllersRouter::getHandlersInfo() const
+73 -12
View File
@@ -215,6 +215,18 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
case Patch:
output->append("PATCH ");
break;
case Propfind:
output->append("PROPFIND ");
break;
case Mkcol:
output->append("MKCOL ");
break;
case Copy:
output->append("COPY ");
break;
case Move:
output->append("MOVE ");
break;
default:
return;
}
@@ -236,7 +248,7 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
}
std::string content;
if (passThrough_ && !query_.empty())
if (!query_.empty())
{
output->append("?");
output->append(query_);
@@ -323,21 +335,31 @@ void HttpRequestImpl::appendToBuffer(trantor::MsgBuffer *output) const
content.append(type.data(), type.length());
}
content.append("\r\n\r\n");
std::ifstream infile(utils::toNativePath(file.path()),
std::ifstream::binary);
if (!infile)
if (file.data() && file.dataLength() > 0)
{
LOG_ERROR << file.path() << " not found";
content.append((const char *)file.data(),
file.dataLength());
}
else
{
std::streambuf *pbuf = infile.rdbuf();
std::streamsize filesize = pbuf->pubseekoff(0, infile.end);
pbuf->pubseekoff(0, infile.beg); // rewind
std::string str;
str.resize(filesize);
pbuf->sgetn(&str[0], filesize);
content.append(std::move(str));
std::ifstream infile(utils::toNativePath(file.path()),
std::ifstream::binary);
if (!infile)
{
LOG_ERROR << file.path() << " not found";
}
else
{
std::streambuf *pbuf = infile.rdbuf();
std::streamsize filesize =
pbuf->pubseekoff(0, infile.end);
pbuf->pubseekoff(0, infile.beg); // rewind
std::string str;
str.resize(filesize);
pbuf->sgetn(&str[0], filesize);
content.append(std::move(str));
}
}
content.append("\r\n");
}
@@ -648,6 +670,18 @@ const char *HttpRequestImpl::methodString() const
case Patch:
result = "PATCH";
break;
case Propfind:
result = "PROPFIND";
break;
case Mkcol:
result = "MKCOL";
break;
case Copy:
result = "COPY";
break;
case Move:
result = "MOVE";
break;
default:
break;
}
@@ -683,6 +717,14 @@ bool HttpRequestImpl::setMethod(const char *start, const char *end)
{
method_ = Head;
}
else if (m == "COPY")
{
method_ = Copy;
}
else if (m == "MOVE")
{
method_ = Move;
}
else
{
method_ = Invalid;
@@ -693,6 +735,10 @@ bool HttpRequestImpl::setMethod(const char *start, const char *end)
{
method_ = Patch;
}
else if (m == "MKCOL")
{
method_ = Mkcol;
}
else
{
method_ = Invalid;
@@ -718,6 +764,16 @@ bool HttpRequestImpl::setMethod(const char *start, const char *end)
method_ = Invalid;
}
break;
case 8:
if (m == "PROPFIND")
{
method_ = Propfind;
}
else
{
method_ = Invalid;
}
break;
default:
method_ = Invalid;
break;
@@ -753,6 +809,11 @@ void HttpRequestImpl::reserveBodySize(size_t length)
{
// Store data of body to a temporary file
createTmpFile();
if (!content_.empty())
{
cacheFilePtr_->append(content_);
content_.clear();
}
}
}
+26
View File
@@ -351,6 +351,11 @@ class HttpRequestImpl : public HttpRequest
headers_.erase(lowerKey);
}
void clearHeaders() override
{
headers_.clear();
}
const std::string &getHeader(std::string field) const override
{
std::transform(field.begin(),
@@ -408,6 +413,27 @@ class HttpRequestImpl : public HttpRequest
parameters_[key] = value;
}
void setQueryParameter(const std::string &key,
const std::string &value) override
{
if (!query_.empty())
{
query_.append("&");
}
query_.append(utils::urlEncodeComponent(key));
query_.append("=");
query_.append(utils::urlEncodeComponent(value));
}
void setBodyParameter(const std::string &key,
const std::string &value) override
{
assert(contentType_ == CT_MULTIPART_FORM_DATA ||
contentType_ == CT_APPLICATION_X_FORM);
flagForParsingParameters_ = true;
parameters_[key] = value;
}
const std::string &getContent() const
{
return content_;
+220
View File
@@ -54,6 +54,16 @@ static inline HttpResponsePtr genHttpResponse(const std::string &viewName,
}
} // namespace drogon
void HttpResponseImpl::setAllowCompression(bool allow)
{
allowCompression_ = allow;
}
bool HttpResponseImpl::allowCompression() const
{
return allowCompression_;
}
HttpResponsePtr HttpResponse::newHttpResponse()
{
auto res = std::make_shared<HttpResponseImpl>(k200OK, CT_TEXT_HTML);
@@ -473,6 +483,210 @@ HttpResponsePtr HttpResponse::newAsyncStreamResponse(
return resp;
}
HttpResponsePtr HttpResponse::newOptionsResponse(
const HttpRequestPtr &request,
const std::function<bool(std::string_view)> &originValidator,
bool allowNullOrigin,
bool allowCredentials,
bool allowPNA,
std::optional<unsigned int> maxAgeSeconds,
const std::optional<std::set<std::string_view>> &allowedHeaders)
{
if (!request || (request->method() != HttpMethod::Options))
return {};
// Allowed methods, set by drogon::HttpOptionsMiddlewareImpl
auto methods =
request->attributes()->get<std::string>("drogon.corsMethods");
if (methods.empty())
methods = "OPTIONS";
auto response = newHttpResponse(HttpStatusCode::k204NoContent,
drogon::ContentType::CT_NONE);
// Disable HTTP caching for OPTIONS responses
response->addHeader("Cache-Control"s, "no-store"s);
// Vary on Origin for bad proxies that do not respect no-store or want
// Pragma: no-cache instead
response->addHeader("Vary"s, "Origin");
// Generic OPTIONS response
if (!request->isCorsPreflightRequest())
{
response->addHeader("Allow", methods);
return response;
}
// CORS pre-flight response
std::string_view origin = drogon::utils::trim(request->getHeader("Origin"));
if (origin.empty())
{
response->setStatusCode(HttpStatusCode::k400BadRequest);
response->addHeader("X-Cors-Error",
"invalid empty Origin"); // diagnose help
return response;
}
// Check whether null origin is allowed (file://, sandboxed iframes, etc.)
if (drogon::utils::ci_equals(origin, "null") && !allowNullOrigin)
{
response->setStatusCode(HttpStatusCode::k403Forbidden);
response->addHeader("X-Cors-Error",
"null Origin not allowed"); // diagnose help
return response;
}
// Check whether the origin is allowed
if (originValidator && !originValidator(origin))
{
response->setStatusCode(HttpStatusCode::k403Forbidden);
response->addHeader("X-Cors-Error",
"origin not allowed"); // diagnose help
return response;
}
// Reflect the origin (acts like '*', that is forbidden when
// allowCredentials is true)
response->addHeader("Access-Control-Allow-Origin", std::string(origin));
response->addHeader("Access-Control-Allow-Methods", methods);
// Check requested method
// Policy: explicitly fail preflight with 40x + diagnostic header rather
// than silently returning allowed methods
auto acrMethod = drogon::utils::trim(
request->getHeader("Access-Control-Request-Method"));
if (acrMethod.empty())
{
response->setStatusCode(HttpStatusCode::k400BadRequest);
response->addHeader(
"X-Cors-Error",
"invalid empty Access-Control-Request-Method"); // diagnose help
return response;
}
const auto allowedMethods = drogon::utils::splitStringView(methods, ",");
if (std::find_if(allowedMethods.begin(),
allowedMethods.end(),
[&acrMethod](const std::string_view &method) {
return drogon::utils::ci_equals(method, acrMethod);
}) == allowedMethods.end())
{
response->setStatusCode(HttpStatusCode::k405MethodNotAllowed);
response->addHeader("Allow",
methods); // failing CORS pre-flight with 405 must
// also return the Allow header
response->addHeader("X-Cors-Error",
"method not allowed: "s.append(
acrMethod)); // diagnose help
return response;
}
// Allowed headers (intersection with requested ones on success, all allowed
// on error) Note: Browsers typically include only non-safelisted headers in
// Access-Control-Request-Headers We validate strictly against
// allowedHeaders Policy: explicitly fail preflight with 403 + diagnostic
// header rather than silently omitting forbidden CORS headers
auto requestedHeaders = drogon::utils::splitStringViewToSet(
request->getHeader("Access-Control-Request-Headers"), ",");
if (allowedHeaders.has_value())
{
auto &validHeaders = allowedHeaders.value();
if (requestedHeaders.empty()) // noisy, but helpful for diagnosis
requestedHeaders = {validHeaders.begin(), validHeaders.end()};
else
{
for (auto it = requestedHeaders.begin();
it != requestedHeaders.end();)
{
auto &reqHeader = *it;
if (std::find_if(validHeaders.begin(),
validHeaders.end(),
[&reqHeader](const std::string_view &header) {
return drogon::utils::ci_equals(
reqHeader,
drogon::utils::trim(header));
}) != validHeaders.end())
{
++it;
continue;
}
response->setStatusCode(
HttpStatusCode::k403Forbidden); // Forbidden header
response->addHeader("X-Cors-Error",
"disallowed header: "s.append(
reqHeader)); // diagnose help
// report all allowed headers to help diagnosing what's
// wrong
requestedHeaders = {validHeaders.begin(), validHeaders.end()};
break;
}
}
}
if (!requestedHeaders.empty())
response->addHeader("Access-Control-Allow-Headers",
drogon::utils::joinStringViews(requestedHeaders,
","));
if (response->statusCode() == HttpStatusCode::k403Forbidden)
return response;
// Allow credentials
if (allowCredentials)
response->addHeader("Access-Control-Allow-Credentials", "true");
// Chromium-based browsers require this header to allow Private Network
// Access requests
if (allowPNA &&
drogon::utils::ci_equals(request->getHeader(
"Access-Control-Request-Private-Network"),
"true"))
response->addHeader("Access-Control-Allow-Private-Network", "true");
// Set a max age only on success
if (maxAgeSeconds.has_value())
response->addHeader("Access-Control-Max-Age",
std::to_string(maxAgeSeconds.value()));
return response;
}
void HttpResponse::addCorsHeaders(
const HttpRequestPtr &request,
const std::set<std::string_view> &exposedHeaders,
const std::optional<bool> &allowCredentials)
{
if (!request || !request->isCorsRequest() ||
request->isCorsPreflightRequest())
return;
// add/set Origin to the Vary header (needed for cache proxies)
auto vary = drogon::utils::splitStringViewToSet(getHeader("Vary"), ",");
if (std::find_if(vary.begin(), vary.end(), [](const auto &val) {
return drogon::utils::ci_equals(val, "Origin");
}) == vary.end())
{
vary.insert("Origin");
addHeader("Vary", drogon::utils::joinStringViews(vary, ","));
}
// add _MISSING_ CORS header - do not overwrite existing one
if (headers().find("access-control-allow-origin") == headers().end())
addHeader("Access-Control-Allow-Origin",
std::string(
drogon::utils::trim(request->getHeader("Origin"))));
// set (or append) exposed headers
if (!exposedHeaders.empty())
{
auto exposed = drogon::utils::splitStringViewToSet(
getHeader("Access-Control-Expose-Headers"), ",");
bool changed = false;
for (auto &header : exposedHeaders)
{
if (std::find_if(exposed.begin(),
exposed.end(),
[&header](const auto &val) {
return drogon::utils::ci_equals(val, header);
}) != exposed.end())
continue;
exposed.insert(header);
changed = true;
}
if (changed)
addHeader("Access-Control-Expose-Headers",
drogon::utils::joinStringViews(exposed, ","));
}
if (!allowCredentials.has_value())
return;
if (allowCredentials.value())
addHeader("Access-Control-Allow-Credentials", "true");
else
removeHeader("Access-Control-Allow-Credentials");
}
void HttpResponseImpl::makeHeaderString(trantor::MsgBuffer &buffer)
{
buffer.ensureWritableBytes(128);
@@ -960,6 +1174,12 @@ void HttpResponseImpl::parseJson() const
bool HttpResponseImpl::shouldBeCompressed() const
{
// If the developer said "No" stop immediately.
if (!allowCompression_)
{
return false;
}
if (streamCallback_ || asyncStreamCallback_ || !sendfileName_.empty() ||
contentType() >= CT_APPLICATION_OCTET_STREAM ||
getBody().length() < 1024 ||
+6
View File
@@ -463,6 +463,12 @@ class DROGON_EXPORT HttpResponseImpl : public HttpResponse
}
private:
bool allowCompression_{true};
void setAllowCompression(bool allow) override;
bool allowCompression() const override;
void setBody(const char *body, size_t len) override
{
bodyPtr_ = std::make_shared<HttpMessageStringViewBody>(body, len);
+22 -6
View File
@@ -17,6 +17,8 @@
#include <trantor/utils/Logger.h>
#include <trantor/utils/MsgBuffer.h>
#include <algorithm>
#include <cerrno>
#include <cstdlib>
using namespace trantor;
using namespace drogon;
@@ -129,7 +131,16 @@ bool HttpResponseParser::parseResponse(MsgBuffer *buf)
// LOG_INFO << "content len=" << len;
if (!len.empty())
{
leftBodyLength_ = static_cast<size_t>(std::stoull(len));
try
{
leftBodyLength_ =
static_cast<size_t>(std::stoull(len));
}
catch (...)
{
// Malformed Content-Length from peer.
return false;
}
status_ = HttpResponseParseStatus::kExpectBody;
}
else
@@ -242,12 +253,17 @@ bool HttpResponseParser::parseResponse(MsgBuffer *buf)
const char *crlf = buf->findCRLF();
if (crlf)
{
// chunk length line
std::string len(buf->peek(), crlf - buf->peek());
char *end;
currentChunkLength_ = strtol(len.c_str(), &end, 16);
// LOG_TRACE << "chun length : " <<
// currentChunkLength_;
errno = 0;
char *end = nullptr;
unsigned long long parsed =
std::strtoull(len.c_str(), &end, 16);
if (errno == ERANGE || end == len.c_str() ||
(*end != '\0' && *end != ';'))
{
return false;
}
currentChunkLength_ = static_cast<size_t>(parsed);
if (currentChunkLength_ != 0)
{
status_ = HttpResponseParseStatus::kExpectChunkBody;
+19 -12
View File
@@ -130,15 +130,20 @@ void HttpServer::onConnection(const TcpConnectionPtr &conn)
else if (conn->disconnected())
{
LOG_TRACE << "conn disconnected!";
HttpConnectionLimit::instance().releaseConnection(conn);
auto requestParser = conn->getContext<HttpRequestParser>();
if (requestParser)
{
// NOTE: if tls handshake fails, `onConnection()` will only be
// called once with a broken conn. So we only call
// `releaseConnection()` for conn with context.
// Never call `conn->clearContext()` in other places
HttpConnectionLimit::instance().releaseConnection(conn);
if (requestParser->webSocketConn())
{
requestParser->webSocketConn()->onClose();
}
else if (requestParser->requestImpl()->isStreamMode())
else if (requestParser->requestImpl()->streamStatus() ==
ReqStreamStatus::Open)
{
requestParser->requestImpl()->streamError(
std::make_exception_ptr(
@@ -206,13 +211,9 @@ void HttpServer::onMessage(const TcpConnectionPtr &conn, MsgBuffer *buf)
statusCodeToString(code).data()));
}
buf->retrieveAll();
// NOTE: should we call conn->forceClose() instead?
// Calling shutdown() handles socket more elegantly.
// stop parser to ignore following illegal data from client
requestParser->stop();
conn->shutdown();
// We have to call clearContext() here in order to ignore following
// illegal data from client
conn->clearContext();
requestParser->reset();
return;
}
if (parseRes == 0)
@@ -576,12 +577,18 @@ void HttpServer::requestPassMiddlewares(const HttpRequestImplPtr &req,
template <typename Pack>
void HttpServer::requestPreHandling(const HttpRequestImplPtr &req, Pack &&pack)
{
// Handle CORS preflight request, except when custom handling is desired
if (req->method() == Options)
{
handleHttpOptions(req,
*pack.binderPtr->corsMethods_,
std::move(pack.callback));
return;
if (!req->attributes()->get<bool>("drogon.customCORShandling"))
{
handleHttpOptions(req,
*pack.binderPtr->corsMethods_,
std::move(pack.callback));
return;
}
req->attributes()->insert("drogon.corsMethods",
*pack.binderPtr->corsMethods_);
}
// pre-handling aop
+2 -25
View File
@@ -18,6 +18,7 @@
#include "HttpFileImpl.h"
#include <drogon/MultiPart.h>
#include <drogon/utils/Utilities.h>
#include "utils/ParsingUtils.h"
#include <drogon/config.h>
#include <algorithm>
#include <fcntl.h>
@@ -29,6 +30,7 @@
#endif
using namespace drogon;
using drogon::utils::parseLine;
const std::vector<HttpFile> &MultiPartParser::getFiles() const
{
@@ -87,31 +89,6 @@ int MultiPartParser::parse(const HttpRequestPtr &req)
return parse(req, contentType.data() + (pos + 9), pos2 - (pos + 9));
}
static std::pair<std::string_view, std::string_view> parseLine(
const char *begin,
const char *end)
{
auto p = begin;
while (p != end)
{
if (*p == ':')
{
if (p + 1 != end && *(p + 1) == ' ')
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 2, end - p - 2));
}
else
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 1, end - p - 1));
}
}
++p;
}
return std::make_pair(std::string_view(), std::string_view());
}
int MultiPartParser::parseEntity(const HttpRequestPtr &req,
const char *begin,
const char *end)
+4 -59
View File
@@ -14,41 +14,12 @@
#include "MultipartStreamParser.h"
#include <cassert>
#include "utils/ParsingUtils.h"
using namespace drogon;
static bool startsWith(const std::string_view &a, const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}
static bool startsWithIgnoreCase(const std::string_view &a,
const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
if (::tolower(a[i]) != ::tolower(b[i]))
{
return false;
}
}
return true;
}
using drogon::utils::parseLine;
using drogon::utils::startsWith;
using drogon::utils::startsWithIgnoreCase;
MultipartStreamParser::MultipartStreamParser(const std::string &contentType)
{
@@ -86,32 +57,6 @@ MultipartStreamParser::MultipartStreamParser(const std::string &contentType)
crlfDashBoundary_ = crlf_ + dash_ + boundary_;
}
// TODO: same function in HttpRequestParser.cc
static std::pair<std::string_view, std::string_view> parseLine(
const char *begin,
const char *end)
{
auto p = begin;
while (p != end)
{
if (*p == ':')
{
if (p + 1 != end && *(p + 1) == ' ')
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 2, end - p - 2));
}
else
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 1, end - p - 1));
}
}
++p;
}
return std::make_pair(std::string_view(), std::string_view());
}
void drogon::MultipartStreamParser::parse(
const char *data,
size_t length,
+94 -29
View File
@@ -17,10 +17,49 @@
#include <dirent.h>
#include <dlfcn.h>
#include <fstream>
#include <sstream>
#include <sys/types.h>
#include <sys/wait.h>
#include <trantor/utils/Logger.h>
#include <unistd.h>
// Safe exec helper: runs a program with explicit argv, no shell involved.
// Returns the exit status, or -1 on fork/exec failure.
static int safeExec(const std::vector<std::string> &args)
{
if (args.empty())
return -1;
std::vector<char *> argv;
argv.reserve(args.size() + 1);
for (auto &a : args)
argv.push_back(const_cast<char *>(a.c_str()));
argv.push_back(nullptr);
pid_t pid = fork();
if (pid == -1)
{
perror("fork");
return -1;
}
if (pid == 0)
{
// Child: replace image with the target program.
execvp(argv[0], argv.data());
// execvp only returns on error.
perror("execvp");
_exit(127);
}
// Parent: wait for child.
int status = 0;
if (waitpid(pid, &status, 0) == -1)
{
perror("waitpid");
return -1;
}
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
static void forEachFileIn(
const std::string &path,
const std::function<void(const std::string &, const struct stat &)> &cb)
@@ -153,22 +192,27 @@ void SharedLibManager::managerLibs()
else
{
// generate source code and compile it.
std::string cmd = "drogon_ctl create view ";
if (!outputPath_.empty())
{
cmd.append(filename).append(" -o ").append(
outputPath_);
}
else
{
cmd.append(filename).append(" -o ").append(
libPath);
}
const std::string &outDir =
!outputPath_.empty() ? outputPath_ : libPath;
std::vector<std::string> genArgs = {"drogon_ctl",
"create",
"view",
filename,
"-o",
outDir};
srcFile.append(".cc");
LOG_TRACE << cmd;
auto r = system(cmd.c_str());
// TODO: handle r
(void)(r);
LOG_TRACE << "drogon_ctl create view " << filename
<< " -o " << outDir;
auto r = safeExec(genArgs);
if (r != 0)
{
LOG_ERROR
<< "Failed to generate source code for "
<< filename;
dlStat.handle = oldHandle;
return;
}
dlStat.handle =
compileAndLoadLib(srcFile, oldHandle);
}
@@ -203,24 +247,45 @@ void *SharedLibManager::compileAndLoadLib(const std::string &sourceFile,
void *oldHld)
{
LOG_TRACE << "src:" << sourceFile;
std::string cmd = COMPILER_COMMAND;
cmd.append(" ")
.append(sourceFile)
.append(" ")
.append(COMPILATION_FLAGS)
.append(" ")
.append(INCLUDING_DIRS);
if (std::string(COMPILER_ID).find("Clang") != std::string::npos)
cmd.append(" -shared -fPIC -undefined dynamic_lookup -o ");
else
cmd.append(" -shared -fPIC --no-gnu-unique -o ");
auto pos = sourceFile.rfind('.');
auto soFile = sourceFile.substr(0, pos);
soFile.append(".so");
cmd.append(soFile);
LOG_TRACE << cmd;
if (system(cmd.c_str()) == 0)
// Build argv without invoking a shell so that metacharacters in
// sourceFile or soFile cannot be interpreted by /bin/sh.
std::vector<std::string> compileArgs;
compileArgs.push_back(COMPILER_COMMAND);
// COMPILATION_FLAGS and INCLUDING_DIRS are baked in at build time from
// trusted CMake variables; split them on whitespace into separate tokens.
auto splitIntoArgs = [&](const std::string &s) {
std::istringstream iss(s);
std::string token;
while (iss >> token)
compileArgs.push_back(token);
};
compileArgs.push_back(sourceFile);
splitIntoArgs(COMPILATION_FLAGS);
splitIntoArgs(INCLUDING_DIRS);
if (std::string(COMPILER_ID).find("Clang") != std::string::npos)
{
compileArgs.push_back("-shared");
compileArgs.push_back("-fPIC");
compileArgs.push_back("-undefined");
compileArgs.push_back("dynamic_lookup");
}
else
{
compileArgs.push_back("-shared");
compileArgs.push_back("-fPIC");
compileArgs.push_back("--no-gnu-unique");
}
compileArgs.push_back("-o");
compileArgs.push_back(soFile);
LOG_TRACE << COMPILER_COMMAND << " " << sourceFile << " ... -o " << soFile;
if (safeExec(compileArgs) == 0)
{
LOG_TRACE << "Compiled successfully:" << soFile;
return loadLib(soFile, oldHld);
+52 -11
View File
@@ -155,9 +155,24 @@ bool isInteger(std::string_view str)
bool isBase64(std::string_view str)
{
for (auto c : str)
if (!isBase64(c))
if (str.empty())
return false;
size_t padding = 0;
if (str.back() == '=')
padding++;
if (str.size() > 1 && str[str.size() - 2] == '=')
padding++;
for (size_t i = 0; i < str.size() - padding; ++i)
{
if (!isBase64(str[i]))
return false;
}
if (padding > 0 && (str.size() % 4 != 0))
return false;
return true;
}
@@ -1018,6 +1033,35 @@ std::string gzipDecompress(const char *data, const size_t ndata)
}
}
static int formatHttpDate(char *buf, size_t len, const trantor::Date &date)
{
static const char *const weekdays[] = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
static const char *const months[] = {"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"};
struct tm tm = date.tmStruct();
return snprintf(buf,
len,
"%s, %02d %s %04d %02d:%02d:%02d GMT",
weekdays[tm.tm_wday],
tm.tm_mday,
months[tm.tm_mon],
tm.tm_year + 1900,
tm.tm_hour,
tm.tm_min,
tm.tm_sec);
}
char *getHttpFullDate(const trantor::Date &date)
{
static thread_local int64_t lastSecond = 0;
@@ -1029,9 +1073,7 @@ char *getHttpFullDate(const trantor::Date &date)
return lastTimeString;
}
lastSecond = nowSecond;
date.toCustomFormattedString("%a, %d %b %Y %H:%M:%S GMT",
lastTimeString,
sizeof(lastTimeString));
formatHttpDate(lastTimeString, sizeof(lastTimeString), date);
return lastTimeString;
}
@@ -1039,8 +1081,6 @@ void dateToCustomFormattedString(const std::string &fmtStr,
std::string &str,
const trantor::Date &date)
{
auto nowSecond =
date.microSecondsSinceEpoch() / trantor::Date::MICRO_SECONDS_PER_SEC;
struct tm tm_LValue = date.tmStruct();
std::stringstream Out;
Out.imbue(std::locale{"C"});
@@ -1051,7 +1091,7 @@ void dateToCustomFormattedString(const std::string &fmtStr,
const std::string &getHttpFullDateStr(const trantor::Date &date)
{
static thread_local int64_t lastSecond = 0;
static thread_local std::string lastTimeString(128, 0);
static thread_local std::string lastTimeString;
auto nowSecond =
date.microSecondsSinceEpoch() / trantor::Date::MICRO_SECONDS_PER_SEC;
if (nowSecond == lastSecond)
@@ -1059,9 +1099,10 @@ const std::string &getHttpFullDateStr(const trantor::Date &date)
return lastTimeString;
}
lastSecond = nowSecond;
dateToCustomFormattedString("%a, %d %b %Y %H:%M:%S GMT",
lastTimeString,
date);
lastTimeString.resize(128);
int n = formatHttpDate(lastTimeString.data(), lastTimeString.size(), date);
n = std::clamp(n, 0, static_cast<int>(lastTimeString.size() - 1));
lastTimeString.resize(static_cast<size_t>(n));
return lastTimeString;
}
+100
View File
@@ -0,0 +1,100 @@
/**
*
* @file ParsingUtils.h
* Shared parsing utilities for HTTP and multipart parsing
*
* Copyright 2024, Drogon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <cctype>
#include <string_view>
#include <utility>
namespace drogon
{
namespace utils
{
/**
* @brief Check if a string_view starts with another string_view
*/
inline bool startsWith(const std::string_view &a, const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}
/**
* @brief Check if a string_view starts with another string_view
* (case-insensitive)
*/
inline bool startsWithIgnoreCase(const std::string_view &a,
const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
const auto lhs = std::tolower(static_cast<unsigned char>(a[i]));
const auto rhs = std::tolower(static_cast<unsigned char>(b[i]));
if (lhs != rhs)
{
return false;
}
}
return true;
}
/**
* @brief Parse a single HTTP header line into name and value
* @param begin Pointer to the start of the line
* @param end Pointer to the end of the line (not including CRLF)
* @return A pair of (header_name, header_value) string_views
*/
inline std::pair<std::string_view, std::string_view> parseLine(
const char *begin,
const char *end)
{
auto p = begin;
while (p != end)
{
if (*p == ':')
{
if (p + 1 != end && *(p + 1) == ' ')
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 2, end - p - 2));
}
else
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 1, end - p - 1));
}
}
++p;
}
return std::make_pair(std::string_view(), std::string_view());
}
} // namespace utils
} // namespace drogon
+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);
}