Skip to content
Merged
31 changes: 31 additions & 0 deletions lib/inc/drogon/HttpRequest.h
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,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;
Expand Down
135 changes: 135 additions & 0 deletions lib/inc/drogon/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -558,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
Expand Down
159 changes: 159 additions & 0 deletions lib/inc/drogon/utils/Utilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Greisby marked this conversation as resolved.
* \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);
Comment thread
Greisby marked this conversation as resolved.
}
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);

Expand Down
Loading
Loading