From 1e2826e38311553f341046f6e9855a381b3e37fd Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Tue, 27 Jan 2026 17:33:19 +0100 Subject: [PATCH 01/10] add helper functions to respond to OPTIONS requests --- lib/inc/drogon/HttpRequest.h | 20 ++++ lib/inc/drogon/HttpResponse.h | 86 +++++++++++++++ lib/inc/drogon/utils/Utilities.h | 107 +++++++++++++++++- lib/src/HttpResponseImpl.cc | 152 ++++++++++++++++++++++++++ lib/tests/unittests/HttpHeaderTest.cc | 122 +++++++++++++++++++++ trantor | 2 +- 6 files changed, 487 insertions(+), 2 deletions(-) diff --git a/lib/inc/drogon/HttpRequest.h b/lib/inc/drogon/HttpRequest.h index 24d8c7579d..45aab75833 100644 --- a/lib/inc/drogon/HttpRequest.h +++ b/lib/inc/drogon/HttpRequest.h @@ -501,6 +501,26 @@ class DROGON_EXPORT HttpRequest return toRequest(std::forward(obj)); } + /*! \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 + * \param[in] request Drogon 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 headers().find("origin") != headers().end() && + headers().find("access-control-request-method") != + headers().end(); + } + virtual bool isOnSecureConnection() const noexcept = 0; virtual void setContentTypeString(const char *typeString, size_t typeStringLength) = 0; diff --git a/lib/inc/drogon/HttpResponse.h b/lib/inc/drogon/HttpResponse.h index 798055437f..433240e3ff 100644 --- a/lib/inc/drogon/HttpResponse.h +++ b/lib/inc/drogon/HttpResponse.h @@ -552,6 +552,92 @@ class DROGON_EXPORT HttpResponse return toResponse(std::forward(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. 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 &originValidator = nullptr, + bool allowNullOrigin = false, + bool allowCredentials = false, + bool allowPNA = true, + std::optional maxAgeSeconds = {}, + const std::optional> &allowedHeaders = std::nullopt); + // Helper when specifing 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 &allowedHeaders, + const std::function &originValidator = nullptr, + bool allowNullOrigin = false, + bool allowCredentials = false, + bool allowPNA = true, + std::optional maxAgeSeconds = {}) + { + return newOptionsResponse(request, + originValidator, + allowNullOrigin, + allowCredentials, + allowPNA, + maxAgeSeconds, + allowedHeaders); + } + /** * @brief If the response is a file response (i.e. created by * newFileResponse) returns the path on the filesystem. Otherwise a diff --git a/lib/inc/drogon/utils/Utilities.h b/lib/inc/drogon/utils/Utilities.h index 23412500e5..342053b9c5 100644 --- a/lib/inc/drogon/utils/Utilities.h +++ b/lib/inc/drogon/utils/Utilities.h @@ -124,7 +124,112 @@ DROGON_EXPORT std::set splitStringToSet( const std::string &str, const std::string &separator); -/// Get UUID string. +/// Compare two string_views for equality, ignoring case. +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(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); }); +} + +/// Trim leading and trailing spaces and tabs from a string_view. +inline std::string_view &trim_inplace(std::string_view &str) +{ +#ifndef min + // On Windows, min is defined as a macro since the origins - NOMINMAX should + // be added to the preprocessor definitions of drogon to remove this + // historical macro - meanwhile, use std namespace to keep using this macro + // on Windows and std::min on other platforms. + using namespace std; +#endif + auto pos = str.find_first_not_of(" \t"); + str.remove_prefix(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; +} +inline std::string_view trim(std::string_view str) +{ + return trim_inplace(str); +} +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; +} + +/// Split a string_view into a vector of string_views. +inline std::vector splitStringView( + std::string_view str, + std::string_view separator, + bool trimValues = true, + bool acceptEmptyString = false) +{ + std::vector 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; +} + +/// Split a string_view into a set of string_views. +inline std::set 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(v.begin(), v.end()); +} + +/// Join a vector of string_view into a string. +inline std::string joinStringViews(const std::vector &strs, + std::string_view separator) +{ + std::string result; + for (auto& str: strs) + { + if (!result.empty()) + result.append(separator); + result.append(str); + } + return result; +} + +inline std::string joinStringViews(const std::set &strs, + std::string_view separator) +{ + return joinStringViews(std::vector{strs.begin(), + strs.end()}, + separator); +} + + /// Get UUID string. DROGON_EXPORT std::string getUuid(bool lowercase = true); /// Get the encoded length of base64. diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index 64b785c2c3..7c1075d4f9 100644 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -473,6 +473,158 @@ HttpResponsePtr HttpResponse::newAsyncStreamResponse( return resp; } +HttpResponsePtr HttpResponse::newOptionsResponse( + const HttpRequestPtr &request, + const std::function &originValidator, + bool allowNullOrigin, + bool allowCredentials, + bool allowPNA, + std::optional maxAgeSeconds, + const std::optional> &allowedHeaders) +{ + if (!request || (request->method() != HttpMethod::Options)) + return {}; + // Allowed methods, set by drogon::HttpOptionsMiddlewareImpl + auto methods = + request->attributes()->get("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 HttpResponseImpl::makeHeaderString(trantor::MsgBuffer &buffer) { buffer.ensureWritableBytes(128); diff --git a/lib/tests/unittests/HttpHeaderTest.cc b/lib/tests/unittests/HttpHeaderTest.cc index 4a46bb193a..2ba4b46c38 100644 --- a/lib/tests/unittests/HttpHeaderTest.cc +++ b/lib/tests/unittests/HttpHeaderTest.cc @@ -66,3 +66,125 @@ 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); + + req->setMethod(HttpMethod::Options); + resp = HttpResponse::newOptionsResponse(req); + CHECK(resp->getStatusCode() == HttpStatusCode::k400BadRequest); + + 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"); + + req->addHeader("Origin", "http://somepage"); + req->addHeader("Access-Control-Request-Method", ""); + resp = HttpResponse::newOptionsResponse(req, {}, true); + CHECK(resp->getStatusCode() == HttpStatusCode::k400BadRequest); + + 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"); + + 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); + + 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"); + + 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") == ""); + + 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"); + + resp = HttpResponse::newOptionsResponse(req, {"X-Foo"}); + CHECK(resp->getStatusCode() == HttpStatusCode::k403Forbidden); + CHECK(resp->getHeader("Access-Control-Allow-Headers") == "X-Foo"); + + resp = HttpResponse::newOptionsResponse(req, {"X-Foo", "X-Bar"}); + CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent); + CHECK(resp->getHeader("Access-Control-Allow-Headers") == "X-Bar,X-Foo"); + + resp = HttpResponse::newOptionsResponse(req, nullptr, false, true); + CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent); + CHECK(resp->getHeader("Access-Control-Allow-Credentials") == "true"); + + 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"); + + resp = + HttpResponse::newOptionsResponse(req, nullptr, false, false, false, 600); + CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent); + CHECK(resp->getHeader("Access-Control-Max-Age") == "600"); +} diff --git a/trantor b/trantor index 5000e2a726..41ab439edc 160000 --- a/trantor +++ b/trantor @@ -1 +1 @@ -Subproject commit 5000e2a72687232c8675b28ce86a29ed7d44309e +Subproject commit 41ab439edcf679a7dc374a4d18de503f49ccba5b From 78ab67239f98904c432dfbf635ba645eaa2e872b Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 28 Jan 2026 08:22:28 +0100 Subject: [PATCH 02/10] Added unit tests + fixes + documentation --- lib/inc/drogon/HttpRequest.h | 2 +- lib/inc/drogon/HttpResponse.h | 13 ++++- lib/inc/drogon/utils/Utilities.h | 94 +++++++++++++++++++++++++------- 3 files changed, 85 insertions(+), 24 deletions(-) diff --git a/lib/inc/drogon/HttpRequest.h b/lib/inc/drogon/HttpRequest.h index 45aab75833..639100ce3d 100644 --- a/lib/inc/drogon/HttpRequest.h +++ b/lib/inc/drogon/HttpRequest.h @@ -501,7 +501,7 @@ class DROGON_EXPORT HttpRequest return toRequest(std::forward(obj)); } - /*! \details Check if the method of the request is OPTIONS and if it is + /*! \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 diff --git a/lib/inc/drogon/HttpResponse.h b/lib/inc/drogon/HttpResponse.h index 433240e3ff..b7a15cc449 100644 --- a/lib/inc/drogon/HttpResponse.h +++ b/lib/inc/drogon/HttpResponse.h @@ -552,7 +552,7 @@ class DROGON_EXPORT HttpResponse return toResponse(std::forward(obj)); } - /*! \brief Create an OPTIONS or CORS pre-flight response + /*! \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 @@ -618,8 +618,15 @@ class DROGON_EXPORT HttpResponse bool allowPNA = true, std::optional maxAgeSeconds = {}, const std::optional> &allowedHeaders = std::nullopt); - // Helper when specifing the allowed headers, when other parameters may be - // default, to avoid having to specify them all + + /*! \copydoc newOptionsResponse(const HttpRequestPtr&, + * const std::function&, + * bool, bool, bool, + * std::optional, + * const std::optional>&) + * \remarks Helper when specifing 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 &allowedHeaders, diff --git a/lib/inc/drogon/utils/Utilities.h b/lib/inc/drogon/utils/Utilities.h index 342053b9c5..370febed6d 100644 --- a/lib/inc/drogon/utils/Utilities.h +++ b/lib/inc/drogon/utils/Utilities.h @@ -124,38 +124,55 @@ DROGON_EXPORT std::set splitStringToSet( const std::string &str, const std::string &separator); -/// Compare two string_views for equality, ignoring case. +/*! \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(), - [](char a, char b) { return std::tolower(a) == std::tolower(b); }); -} - -/// Trim leading and trailing spaces and tabs from a string_view. + 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) { -#ifndef min - // On Windows, min is defined as a macro since the origins - NOMINMAX should - // be added to the preprocessor definitions of drogon to remove this - // historical macro - meanwhile, use std namespace to keep using this macro - // on Windows and std::min on other platforms. - using namespace std; -#endif auto pos = str.find_first_not_of(" \t"); - str.remove_prefix(min(pos, str.size())); + // 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); } -inline std::string trim(std::string&& 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) @@ -167,7 +184,15 @@ inline std::string trim(std::string&& str) return str; } -/// Split a string_view into a vector of string_views. +/*! \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 splitStringView( std::string_view str, std::string_view separator, @@ -175,6 +200,14 @@ inline std::vector splitStringView( bool acceptEmptyString = false) { std::vector 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) @@ -196,7 +229,13 @@ inline std::vector splitStringView( return result; } -/// Split a string_view into a set of string_views. +/*! \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 splitStringViewToSet( std::string_view str, std::string_view separator, @@ -207,13 +246,21 @@ inline std::set splitStringViewToSet( return std::set(v.begin(), v.end()); } -/// Join a vector of string_view into a string. +/*! \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 &strs, std::string_view separator) { std::string result; - for (auto& str: strs) + for (std::string_view str : strs) { + if (trim_inplace(str).empty()) + continue; if (!result.empty()) result.append(separator); result.append(str); @@ -221,6 +268,13 @@ inline std::string joinStringViews(const std::vector &strs, 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 &strs, std::string_view separator) { @@ -229,7 +283,7 @@ inline std::string joinStringViews(const std::set &strs, separator); } - /// Get UUID string. +/// Get UUID string. DROGON_EXPORT std::string getUuid(bool lowercase = true); /// Get the encoded length of base64. From afbb018add523c5f669d800e5bcc3d2970995a12 Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 28 Jan 2026 10:51:31 +0100 Subject: [PATCH 03/10] Added new CORS helper --- lib/inc/drogon/HttpRequest.h | 14 ++++++++-- lib/inc/drogon/HttpResponse.h | 45 +++++++++++++++++++++++++++++-- lib/src/HttpResponseImpl.cc | 51 +++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/lib/inc/drogon/HttpRequest.h b/lib/inc/drogon/HttpRequest.h index 639100ce3d..32901e8aaf 100644 --- a/lib/inc/drogon/HttpRequest.h +++ b/lib/inc/drogon/HttpRequest.h @@ -501,13 +501,23 @@ class DROGON_EXPORT HttpRequest return toRequest(std::forward(obj)); } + /*! \details Check if the request a CORS request.\n + * 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(); + } + /*! \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 - * \param[in] request Drogon request * \returns true if the method is OPTIONS and the required CORS pre-flight * headers are present */ @@ -516,7 +526,7 @@ class DROGON_EXPORT HttpRequest if (method() != HttpMethod::Options) return false; // Check presence of required headers - return headers().find("origin") != headers().end() && + return isCorsRequest() && headers().find("access-control-request-method") != headers().end(); } diff --git a/lib/inc/drogon/HttpResponse.h b/lib/inc/drogon/HttpResponse.h index b7a15cc449..612335aab6 100644 --- a/lib/inc/drogon/HttpResponse.h +++ b/lib/inc/drogon/HttpResponse.h @@ -572,8 +572,8 @@ class DROGON_EXPORT HttpResponse * 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. Enforce access control - * independently. + * 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 @@ -645,6 +645,47 @@ class DROGON_EXPORT HttpResponse 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 createOptionsResponse() + * 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 &exposedHeaders = {}, + const std::optional &allowCredentials = {}); + /** * @brief If the response is a file response (i.e. created by * newFileResponse) returns the path on the filesystem. Otherwise a diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index 7c1075d4f9..435906719e 100644 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -625,6 +625,57 @@ HttpResponsePtr HttpResponse::newOptionsResponse( return response; } +void HttpResponse::addCorsHeaders( + const HttpRequestPtr &request, + const std::set &exposedHeaders, + const std::optional &allowCredentials) +{ + if (!request || !request->isCorsRequest() || + request->isCorsPreflightRequest()) + return; + // add/set Origin to the Vary header (needed for cache proxies) + auto vary = drogon::utils::splitStringViewToSet(request->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); + }) != vary.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); From 983465e0ab1fa2399810d825f211d750cad2210b Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 28 Jan 2026 12:10:46 +0100 Subject: [PATCH 04/10] Addedunit test + fix --- lib/src/HttpResponseImpl.cc | 2 +- lib/tests/unittests/HttpHeaderTest.cc | 46 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index 435906719e..cf1d85b1f7 100644 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -659,7 +659,7 @@ void HttpResponse::addCorsHeaders( exposed.end(), [&header](const auto &val) { return drogon::utils::ci_equals(val, header); - }) != vary.end()) + }) != exposed.end()) continue; exposed.insert(header); changed = true; diff --git a/lib/tests/unittests/HttpHeaderTest.cc b/lib/tests/unittests/HttpHeaderTest.cc index 2ba4b46c38..3592689579 100644 --- a/lib/tests/unittests/HttpHeaderTest.cc +++ b/lib/tests/unittests/HttpHeaderTest.cc @@ -106,10 +106,12 @@ DROGON_TEST(HttpCorsHeadersResponse) 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); @@ -117,11 +119,13 @@ DROGON_TEST(HttpCorsHeadersResponse) 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); @@ -130,6 +134,7 @@ DROGON_TEST(HttpCorsHeadersResponse) 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"; }); @@ -139,6 +144,7 @@ DROGON_TEST(HttpCorsHeadersResponse) }); 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); @@ -147,6 +153,7 @@ DROGON_TEST(HttpCorsHeadersResponse) 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); @@ -158,23 +165,28 @@ DROGON_TEST(HttpCorsHeadersResponse) 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); @@ -183,8 +195,42 @@ DROGON_TEST(HttpCorsHeadersResponse) 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 + Expose-Headers + req->addHeader("Origin", "http://somepage"); + resp->addCorsHeaders(req, {"X-Foo"}); + CHECK(resp->getHeader("Vary") == "Origin"); + 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"); + + // 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"); +} From 48deabff7889c300c65352e0735d4beaba782e1c Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 28 Jan 2026 12:23:09 +0100 Subject: [PATCH 05/10] minor documentation typo --- lib/inc/drogon/HttpRequest.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/inc/drogon/HttpRequest.h b/lib/inc/drogon/HttpRequest.h index 32901e8aaf..169a4e9299 100644 --- a/lib/inc/drogon/HttpRequest.h +++ b/lib/inc/drogon/HttpRequest.h @@ -501,8 +501,8 @@ class DROGON_EXPORT HttpRequest return toRequest(std::forward(obj)); } - /*! \details Check if the request a CORS request.\n - * It should contain: + /*! \brief Check if the request is a CORS request. + * \details It should contain: * - Origin: origination page * \returns true if the Origin header is present */ @@ -512,7 +512,8 @@ class DROGON_EXPORT HttpRequest return headers().find("origin") != headers().end(); } - /*! \details Check if the method of the request is OPTIONS and if it is + /*! \brief Check if the request is a CORS 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 From cdb81a2bd128c03f0d44fc02012e5e8122544cba Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 28 Jan 2026 12:56:47 +0100 Subject: [PATCH 06/10] improved unit test + bug fix --- lib/inc/drogon/HttpRequest.h | 4 ++-- lib/src/HttpResponseImpl.cc | 2 +- lib/tests/unittests/HttpHeaderTest.cc | 13 +++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/inc/drogon/HttpRequest.h b/lib/inc/drogon/HttpRequest.h index 169a4e9299..4433c1c84f 100644 --- a/lib/inc/drogon/HttpRequest.h +++ b/lib/inc/drogon/HttpRequest.h @@ -512,8 +512,8 @@ class DROGON_EXPORT HttpRequest return headers().find("origin") != headers().end(); } - /*! \brief Check if the request is a CORS request. - /* \details Check if the method of the request is OPTIONS and if it is + /*! \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 diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index cf1d85b1f7..4bf7567f92 100644 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -634,7 +634,7 @@ void HttpResponse::addCorsHeaders( request->isCorsPreflightRequest()) return; // add/set Origin to the Vary header (needed for cache proxies) - auto vary = drogon::utils::splitStringViewToSet(request->getHeader("Vary"), ","); + 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"); diff --git a/lib/tests/unittests/HttpHeaderTest.cc b/lib/tests/unittests/HttpHeaderTest.cc index 3592689579..3b7b92ff95 100644 --- a/lib/tests/unittests/HttpHeaderTest.cc +++ b/lib/tests/unittests/HttpHeaderTest.cc @@ -81,7 +81,8 @@ DROGON_TEST(HttpOptionsHeadersResponse) CHECK(resp->getHeader("Access-Control-Allow-Origin") == ""); CHECK(resp->getHeader("Access-Control-Allow-Methods") == ""); - req->attributes()->insert("drogon.corsMethods", std::string("GET, POST, OPTIONS")); + req->attributes()->insert("drogon.corsMethods", + std::string("GET, POST, OPTIONS")); resp = HttpResponse::newOptionsResponse(req); CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent); CHECK(resp->getHeader("Vary") == "Origin"); @@ -146,7 +147,8 @@ DROGON_TEST(HttpCorsHeadersResponse) // unallowed method req->addHeader("Access-Control-Request-Method", "PUT"); - req->attributes()->insert("drogon.corsMethods", std::string("GET,POST,OPTIONS")); + 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"); @@ -213,16 +215,19 @@ DROGON_TEST(AddHttpCorsHeaders) resp->addCorsHeaders(req, {"X-Foo"}, true); CHECK(resp->headers().empty()); - // with Origin -> Allow-Origin + Vary + Expose-Headers + // 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"); + 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); From ccca74563d292b896a387ae63e8c8398509e3e41 Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 28 Jan 2026 14:58:13 +0100 Subject: [PATCH 07/10] fixed formatting & codespell --- lib/inc/drogon/HttpResponse.h | 2 +- lib/src/HttpResponseImpl.cc | 10 +++++----- lib/tests/unittests/HttpHeaderTest.cc | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/inc/drogon/HttpResponse.h b/lib/inc/drogon/HttpResponse.h index 612335aab6..f350621cf9 100644 --- a/lib/inc/drogon/HttpResponse.h +++ b/lib/inc/drogon/HttpResponse.h @@ -624,7 +624,7 @@ class DROGON_EXPORT HttpResponse * bool, bool, bool, * std::optional, * const std::optional>&) - * \remarks Helper when specifing the allowed headers, when other + * \remarks Helper when specifying the allowed headers, when other * parameters may be default, to avoid having to specify them all */ inline static HttpResponsePtr newOptionsResponse( diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index 4bf7567f92..53abb98150 100644 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -635,10 +635,9 @@ void HttpResponse::addCorsHeaders( 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()) + 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, ",")); @@ -646,7 +645,8 @@ void HttpResponse::addCorsHeaders( // 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")))); + std::string( + drogon::utils::trim(request->getHeader("Origin")))); // set (or append) exposed headers if (!exposedHeaders.empty()) { diff --git a/lib/tests/unittests/HttpHeaderTest.cc b/lib/tests/unittests/HttpHeaderTest.cc index 3b7b92ff95..67d75677f4 100644 --- a/lib/tests/unittests/HttpHeaderTest.cc +++ b/lib/tests/unittests/HttpHeaderTest.cc @@ -198,8 +198,8 @@ DROGON_TEST(HttpCorsHeadersResponse) CHECK(resp->getHeader("Access-Control-Allow-Private-Network") == "true"); // CORS max age - resp = - HttpResponse::newOptionsResponse(req, nullptr, false, false, false, 600); + resp = HttpResponse::newOptionsResponse( + req, nullptr, false, false, false, 600); CHECK(resp->getStatusCode() == HttpStatusCode::k204NoContent); CHECK(resp->getHeader("Access-Control-Max-Age") == "600"); } From 1585852b67d930f6cf8ae1ef1f808fb22702c72f Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 28 Jan 2026 15:01:59 +0100 Subject: [PATCH 08/10] more formatting fixes --- lib/inc/drogon/HttpResponse.h | 5 +++-- lib/src/HttpResponseImpl.cc | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/inc/drogon/HttpResponse.h b/lib/inc/drogon/HttpResponse.h index f350621cf9..0c227c23fd 100644 --- a/lib/inc/drogon/HttpResponse.h +++ b/lib/inc/drogon/HttpResponse.h @@ -617,7 +617,8 @@ class DROGON_EXPORT HttpResponse bool allowCredentials = false, bool allowPNA = true, std::optional maxAgeSeconds = {}, - const std::optional> &allowedHeaders = std::nullopt); + const std::optional> &allowedHeaders = + std::nullopt); /*! \copydoc newOptionsResponse(const HttpRequestPtr&, * const std::function&, @@ -645,7 +646,7 @@ class DROGON_EXPORT HttpResponse allowedHeaders); } - /*! \brief Add CORS headers to a response + /*! \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 diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index 53abb98150..83545b74fd 100644 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -536,7 +536,8 @@ HttpResponsePtr HttpResponse::newOptionsResponse( // 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")); + auto acrMethod = drogon::utils::trim( + request->getHeader("Access-Control-Request-Method")); if (acrMethod.empty()) { response->setStatusCode(HttpStatusCode::k400BadRequest); @@ -605,7 +606,7 @@ HttpResponsePtr HttpResponse::newOptionsResponse( if (!requestedHeaders.empty()) response->addHeader("Access-Control-Allow-Headers", drogon::utils::joinStringViews(requestedHeaders, - ",")); + ",")); if (response->statusCode() == HttpStatusCode::k403Forbidden) return response; // Allow credentials From 033ea962988c4545c26548997e6d08543088e848 Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 28 Jan 2026 15:28:11 +0100 Subject: [PATCH 09/10] fixed typos identified by Copilot --- lib/inc/drogon/HttpResponse.h | 2 +- lib/src/HttpResponseImpl.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/inc/drogon/HttpResponse.h b/lib/inc/drogon/HttpResponse.h index 0c227c23fd..69017dbfd5 100644 --- a/lib/inc/drogon/HttpResponse.h +++ b/lib/inc/drogon/HttpResponse.h @@ -667,7 +667,7 @@ class DROGON_EXPORT HttpResponse * If not set, leaves the * "Access-Control-Allow-Credentials" header * untouched\n - * *MUST MATCH THE createOptionsResponse() + * *MUST MATCH THE newOptionsResponse() * PRE-FLIGHT RESPONSE VALUE* * \param[in] exposedHeaders Set of exposed headers (for * Access-Control-Expose-Headers header)\n diff --git a/lib/src/HttpResponseImpl.cc b/lib/src/HttpResponseImpl.cc index 83545b74fd..2aa980c7f1 100644 --- a/lib/src/HttpResponseImpl.cc +++ b/lib/src/HttpResponseImpl.cc @@ -484,7 +484,7 @@ HttpResponsePtr HttpResponse::newOptionsResponse( { if (!request || (request->method() != HttpMethod::Options)) return {}; - // Allowed methods, set by drogon::HttpOptionsMiddlewareImpl + // Allowed methods, set by drogon::HttpOptionsMiddlewareImpl auto methods = request->attributes()->get("drogon.corsMethods"); if (methods.empty()) From e22a29fbce1d314086119ac81f89824949535706 Mon Sep 17 00:00:00 2001 From: Greisberger Christophe Date: Wed, 4 Feb 2026 09:57:06 +0100 Subject: [PATCH 10/10] rebased trantor --- trantor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trantor b/trantor index 41ab439edc..5000e2a726 160000 --- a/trantor +++ b/trantor @@ -1 +1 @@ -Subproject commit 41ab439edcf679a7dc374a4d18de503f49ccba5b +Subproject commit 5000e2a72687232c8675b28ce86a29ed7d44309e