Skip to content

Commit cff7ccc

Browse files
authored
Enhancement/options cutom handling (#2447)
1 parent 9b9c49b commit cff7ccc

5 files changed

Lines changed: 702 additions & 0 deletions

File tree

lib/inc/drogon/HttpRequest.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,37 @@ class DROGON_EXPORT HttpRequest
501501
return toRequest(std::forward<T>(obj));
502502
}
503503

504+
/*! \brief Check if the request is a CORS request.
505+
* \details It should contain:
506+
* - Origin: origination page
507+
* \returns true if the Origin header is present
508+
*/
509+
inline bool isCorsRequest() const
510+
{
511+
// Check presence of required headers
512+
return headers().find("origin") != headers().end();
513+
}
514+
515+
/*! \brief Check if the request is a CORS pre-flight request.
516+
* \details Check if the method of the request is OPTIONS and if it is
517+
* a CORS pre-flight request.\n
518+
* It should contain:
519+
* - Origin: origination page
520+
* - Access-Control-Request-Method: method to be used in the
521+
* actual request
522+
* \returns true if the method is OPTIONS and the required CORS pre-flight
523+
* headers are present
524+
*/
525+
inline bool isCorsPreflightRequest() const
526+
{
527+
if (method() != HttpMethod::Options)
528+
return false;
529+
// Check presence of required headers
530+
return isCorsRequest() &&
531+
headers().find("access-control-request-method") !=
532+
headers().end();
533+
}
534+
504535
virtual bool isOnSecureConnection() const noexcept = 0;
505536
virtual void setContentTypeString(const char *typeString,
506537
size_t typeStringLength) = 0;

lib/inc/drogon/HttpResponse.h

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,141 @@ class DROGON_EXPORT HttpResponse
558558
return toResponse(std::forward<T>(obj));
559559
}
560560

561+
/*! \brief Create an OPTIONS or CORS pre-flight response
562+
* \details If the request is not an OPTIONS request, returns a NULL
563+
* response\n
564+
* If it is a generic OPTIONS request, returns a 204 No Content
565+
* response with the Allow header\n
566+
* If it is a CORS pre-flight request, returns a 204 No Content
567+
* response with the CORS headers set
568+
*
569+
* Other status codes for CORS pre-flight answers:
570+
* - 400 Bad Request: if the request is malformed (missing
571+
* required headers)
572+
* - 403 Forbidden: if the Origin is not allowed + reason
573+
* in a X-Cors-Error header
574+
* - 403 Forbidden: if one of the headers in
575+
* Access-Control-Request-Headers is not allowed + reason in
576+
* a X-Cors-Error header
577+
* - 405 Method Not Allowed: if the requested method is
578+
* not allowed
579+
* \note CORS is a browser-side security mechanism.\n
580+
* Do not rely on Origin for authentication/authorization:
581+
* non-browser clients can spoof or omit it.\n
582+
* Enforce access control independently.
583+
* \param[in] request Drogon (OPTIONS) request
584+
* \param[in] allowedHeaders Set of allowed headers (for
585+
* Access-Control-Allow-Headers header)\n
586+
* (headers allowed by the controller path
587+
* handler)
588+
* \param[in] originValidator Function to validate the Origin header value
589+
* (allow the origin or not)\n
590+
* If allowCredentials is true, originValidator
591+
* _SHOULD_ enforce a strict allowlist
592+
* \param[in] allowNullOrigin Should be true to accept the "Origin: null"
593+
* header\n
594+
* (set for local file:// pages, sandboxed
595+
* iframes, opaque origins, data: URIs)
596+
* \param[in] allowCredentials Should be true to add the header
597+
* "Access-Control-Allow-Credentials: true"
598+
* (controls whether the browser may include
599+
* credentials such as cookies, HTTP auth, or
600+
* client certificates)\n
601+
* Note: Authorization (bearer) is not a
602+
* credential header; allow it via
603+
* allowedHeaders when needed
604+
* \param[in] allowPNA Should be true to accept the header
605+
* "Access-Control-Request-Private-Network"
606+
* (when a page from a less private address
607+
* space is trying to reach a more private
608+
* one, like internet -> intranet)\n
609+
* Note: specific to Chromium & derivatives
610+
* (Edge, Opera, Brave, ...), not in Firefox
611+
* or Safari
612+
* \param[in] maxAgeSeconds If set, adds the "Access-Control-Max-Age"
613+
* header with the given value (in seconds,
614+
* how long the results of a preflight
615+
* request can be cached by the navigator)
616+
* \returns the OPTIONS or CORS pre-flight response, or a null pointer if
617+
* the request is not an OPTIONS request
618+
*/
619+
static HttpResponsePtr newOptionsResponse(
620+
const HttpRequestPtr &request,
621+
const std::function<bool(std::string_view)> &originValidator = nullptr,
622+
bool allowNullOrigin = false,
623+
bool allowCredentials = false,
624+
bool allowPNA = true,
625+
std::optional<unsigned int> maxAgeSeconds = {},
626+
const std::optional<std::set<std::string_view>> &allowedHeaders =
627+
std::nullopt);
628+
629+
/*! \copydoc newOptionsResponse(const HttpRequestPtr&,
630+
* const std::function<bool(std::string_view)>&,
631+
* bool, bool, bool,
632+
* std::optional<unsigned int>,
633+
* const std::optional<std::set<std::string_view>>&)
634+
* \remarks Helper when specifying the allowed headers, when other
635+
* parameters may be default, to avoid having to specify them all
636+
*/
637+
inline static HttpResponsePtr newOptionsResponse(
638+
const HttpRequestPtr &request,
639+
const std::set<std::string_view> &allowedHeaders,
640+
const std::function<bool(std::string_view)> &originValidator = nullptr,
641+
bool allowNullOrigin = false,
642+
bool allowCredentials = false,
643+
bool allowPNA = true,
644+
std::optional<unsigned int> maxAgeSeconds = {})
645+
{
646+
return newOptionsResponse(request,
647+
originValidator,
648+
allowNullOrigin,
649+
allowCredentials,
650+
allowPNA,
651+
maxAgeSeconds,
652+
allowedHeaders);
653+
}
654+
655+
/*! \brief Add CORS headers to a response
656+
* \details Adds the CORS headers to a response for a normal request (a
657+
* CORS request but not a CORS preflight request):
658+
* - does nothing if it's an OPTIONS request, or
659+
* - if it's not a CORS request, or
660+
* - if it's a CORS preflight request
661+
* Else:
662+
* - adds Access-Control-Allow-Origin (if not yet present)
663+
* - adds Origin to the Vary header,
664+
* - sets or clears Access-Control-Allow-Credentials (if
665+
* allowCredentials is set)
666+
* - completes Access-Control-Expose-Headers
667+
* \param[in] request Drogon request (to get Origin)
668+
* \param[in] allowCredentials If set and true, adds the
669+
* "Access-Control-Allow-Credentials: true
670+
* header"\n
671+
* If set and false, removes the
672+
* "Access-Control-Allow-Credentials" header\n
673+
* If not set, leaves the
674+
* "Access-Control-Allow-Credentials" header
675+
* untouched\n
676+
* *MUST MATCH THE newOptionsResponse()
677+
* PRE-FLIGHT RESPONSE VALUE*
678+
* \param[in] exposedHeaders Set of exposed headers (for
679+
* Access-Control-Expose-Headers header)\n
680+
* These are the headers allowed to be exposed
681+
* to javascript by the remote browser\n
682+
* Note: they are *APPENDED* to any already
683+
* present in the response, they are not
684+
* REPLACED.\n
685+
* This allows to complete them in the
686+
* controller path handler.\n
687+
* If you want to REPLACE them, remove the
688+
* header before calling this function.
689+
* \note may be use both in the controller path handler and in a
690+
* pre-sending advice
691+
*/
692+
void addCorsHeaders(const HttpRequestPtr &request,
693+
const std::set<std::string_view> &exposedHeaders = {},
694+
const std::optional<bool> &allowCredentials = {});
695+
561696
/**
562697
* @brief If the response is a file response (i.e. created by
563698
* newFileResponse) returns the path on the filesystem. Otherwise a

lib/inc/drogon/utils/Utilities.h

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,165 @@ DROGON_EXPORT std::set<std::string> splitStringToSet(
124124
const std::string &str,
125125
const std::string &separator);
126126

127+
/*! \brief Compare two string_views for equality, ignoring case.
128+
* \warning This is locale dependent
129+
* \param[in] str1 The first string_view.
130+
* \param[in] str2 The second string_view.
131+
* \return true if the string_views are equal, ignoring case; false otherwise.
132+
*/
133+
inline bool ci_equals(std::string_view str1, std::string_view str2)
134+
{
135+
if (str1.size() != str2.size())
136+
return false;
137+
return std::equal(str1.begin(),
138+
str1.end(),
139+
str2.begin(),
140+
[](unsigned char a, unsigned char b) {
141+
return std::tolower(a) == std::tolower(b);
142+
});
143+
}
144+
145+
/*! \details Trim leading and trailing spaces and tabs from a string_view,
146+
* modifying it.
147+
* \param[in,out] str The string_view to trim.
148+
* \return The trimmed string_view.
149+
*/
150+
inline std::string_view &trim_inplace(std::string_view &str)
151+
{
152+
auto pos = str.find_first_not_of(" \t");
153+
// defeat Windows macro "min"
154+
str.remove_prefix((std::min)(pos, str.size()));
155+
if (str.empty())
156+
return str;
157+
pos = str.find_last_not_of(" \t");
158+
str.remove_suffix(str.size() - pos - 1);
159+
return str;
160+
}
161+
162+
/*! \brief Trim leading and trailing spaces and tabs from a string_view.
163+
* \param[in] str The string_view to trim.
164+
* \return A string_view with leading and trailing spaces and tabs removed.
165+
*/
166+
inline std::string_view trim(std::string_view str)
167+
{
168+
return trim_inplace(str);
169+
}
170+
171+
/*! \brief Trim leading and trailing spaces and tabs from a rvalue string.
172+
* \param[in] str The string to trim.
173+
* \return The string with leading and trailing spaces and tabs removed.
174+
*/
175+
inline std::string trim(std::string &&str)
176+
{
177+
auto pos = str.find_last_not_of(" \t");
178+
if (pos == std::string::npos)
179+
return {};
180+
str.resize(pos + 1);
181+
pos = str.find_first_not_of(" \t");
182+
if (pos > 0)
183+
str.erase(0, pos);
184+
return str;
185+
}
186+
187+
/*! \brief Split a string_view into a vector of string_views.
188+
* \param[in] str The string_view to split.
189+
* \param[in] separator The separator to use for splitting.
190+
* \param[in] trimValues Whether to trim whitespace from the resulting
191+
* string_views.
192+
* \param[in] acceptEmptyString Whether to include empty strings in the result.
193+
* \return A vector of string_views obtained by splitting the input
194+
* string_view.
195+
*/
196+
inline std::vector<std::string_view> splitStringView(
197+
std::string_view str,
198+
std::string_view separator,
199+
bool trimValues = true,
200+
bool acceptEmptyString = false)
201+
{
202+
std::vector<std::string_view> result;
203+
if (separator.empty())
204+
{
205+
if (trimValues)
206+
trim_inplace(str);
207+
if (acceptEmptyString || !str.empty())
208+
result.push_back(str);
209+
return result;
210+
}
211+
size_t start = 0;
212+
size_t end = 0;
213+
while ((end = str.find(separator, start)) != std::string_view::npos)
214+
{
215+
auto token = str.substr(start, end - start);
216+
if (trimValues)
217+
trim_inplace(token);
218+
if (acceptEmptyString || !token.empty())
219+
result.push_back(token);
220+
start = end + separator.size();
221+
}
222+
auto token = str.substr(start);
223+
if (trimValues)
224+
trim_inplace(token);
225+
if (acceptEmptyString || !token.empty())
226+
{
227+
result.push_back(token);
228+
}
229+
return result;
230+
}
231+
232+
/*! \brief Split a string_view into a set of string_views.
233+
* \copyparams splitStringView
234+
* \return A set of (unique) string_views obtained by splitting the input
235+
* string_view.
236+
* \note Uniqueness is case-sensitive: "A" and "a" are considered different
237+
* values.
238+
*/
239+
inline std::set<std::string_view> splitStringViewToSet(
240+
std::string_view str,
241+
std::string_view separator,
242+
bool trimValues = true,
243+
bool acceptEmptyString = false)
244+
{
245+
auto v = splitStringView(str, separator, trimValues, acceptEmptyString);
246+
return std::set<std::string_view>(v.begin(), v.end());
247+
}
248+
249+
/*! \brief Join a vector of string_view into a string.
250+
* \param[in] strs The vector of string_views to join.
251+
* \param[in] separator The separator to use between string_views.
252+
* \return A single string obtained by joining the input string_views with the
253+
* specified separator.
254+
* \note Empty values are skipped.
255+
*/
256+
inline std::string joinStringViews(const std::vector<std::string_view> &strs,
257+
std::string_view separator)
258+
{
259+
std::string result;
260+
for (std::string_view str : strs)
261+
{
262+
if (trim_inplace(str).empty())
263+
continue;
264+
if (!result.empty())
265+
result.append(separator);
266+
result.append(str);
267+
}
268+
return result;
269+
}
270+
271+
/*! \brief Join a set of string_view into a string.
272+
* \param[in] strs The set of string_views to join.
273+
* \param[in] separator The separator to use between string_views.
274+
* \return A single string obtained by joining the input string_views with the
275+
* specified separator.
276+
* \note Empty values are skipped.
277+
*/
278+
inline std::string joinStringViews(const std::set<std::string_view> &strs,
279+
std::string_view separator)
280+
{
281+
return joinStringViews(std::vector<std::string_view>{strs.begin(),
282+
strs.end()},
283+
separator);
284+
}
285+
127286
/// Get UUID string.
128287
DROGON_EXPORT std::string getUuid(bool lowercase = true);
129288

0 commit comments

Comments
 (0)