修复: 升级框架并完善报告导出
- 升级 Drogon 和 Trantor,修复畸形请求导致的连接计数泄漏\n- 增加第三方框架版本校验与自动重建\n- 完善完整报告导出和接口文档
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 ||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
+38
-14
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user