Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -552,6 +552,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
* \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);

Expand Down
Loading
Loading