LCOV - code coverage report
Current view: top level - http/server - cors.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 98.9 % 92 91
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 12 12

            Line data    Source code
       1              : #include "http_application.hpp"
       2              : #include "response_compression.hpp"
       3              : #include "../common/http_request.hpp"
       4              : #include "../common/http_response.hpp"
       5              : #include "../common/syntax.hpp"
       6              : #include "../../util/ascii.hpp"
       7              : #include "../../util/logger.hpp"
       8              : #include <boost/algorithm/string/join.hpp>
       9              : #include <algorithm>
      10              : 
      11              : namespace thinger::http::detail {
      12              : 
      13              : // The options of http_application::enable_cors(), checked, with what does not depend on the
      14              : // request already computed. Immutable: read from any thread answering a request
      15              : struct cors_policy {
      16              :     bool any_origin = false;
      17              :     std::vector<std::string> origins;
      18              :     std::function<bool(std::string_view, const http_request&)> origin_filter;
      19              :     std::string methods;
      20              :     std::optional<std::string> allowed_headers;
      21              :     std::string exposed_headers;
      22              :     std::string max_age;
      23              :     bool credentials = false;
      24              :     bool preflight_continue = false;
      25              : };
      26              : 
      27              : namespace {
      28              : 
      29              :     constexpr const char* allow_origin = "Access-Control-Allow-Origin";
      30              :     constexpr const char* allow_credentials = "Access-Control-Allow-Credentials";
      31              :     constexpr const char* expose_headers = "Access-Control-Expose-Headers";
      32              :     constexpr const char* allow_methods = "Access-Control-Allow-Methods";
      33              :     constexpr const char* allow_headers = "Access-Control-Allow-Headers";
      34              :     constexpr const char* max_age = "Access-Control-Max-Age";
      35              :     constexpr const char* request_method = "Access-Control-Request-Method";
      36              :     constexpr const char* request_headers = "Access-Control-Request-Headers";
      37              : 
      38          979 :     bool all_tokens(const std::vector<std::string>& values, std::string_view what) {
      39         3880 :         for (const auto& value : values) {
      40         3000 :             if (!syntax::is_token(value)) {
      41           99 :                 LOG_ERROR("Invalid CORS options: '{}' is not a valid {}", value, what);
      42           99 :                 return false;
      43              :             }
      44              :         }
      45          880 :         return true;
      46              :     }
      47              : 
      48              :     // A value for Access-Control-Allow-Origin: no control character, HTAB included
      49          563 :     bool valid_origin(std::string_view origin) {
      50          563 :         return syntax::is_field_value(origin) && origin.find('\t') == std::string_view::npos;
      51              :     }
      52              : 
      53          518 :     bool valid_origins(const std::vector<std::string>& origins, bool credentials) {
      54          518 :         bool any = std::find(origins.begin(), origins.end(), "*") != origins.end();
      55          518 :         if (any && origins.size() > 1) {
      56           15 :             LOG_ERROR("Invalid CORS options: '*' cannot be listed with other origins");
      57           15 :             return false;
      58              :         }
      59              :         // Browsers reject this pair (Fetch, CORS check)
      60          503 :         if (any && credentials) {
      61           21 :             LOG_ERROR("Invalid CORS options: credentials cannot be used with any origin ('*')");
      62           21 :             return false;
      63              :         }
      64         1006 :         for (const auto& origin : origins) {
      65          563 :             if (!valid_origin(origin)) {
      66           39 :                 LOG_ERROR("Invalid CORS options: origin '{}' has control characters", origin);
      67           39 :                 return false;
      68              :             }
      69              :         }
      70          443 :         return true;
      71              :     }
      72              : 
      73          575 :     bool valid_options(const cors_options& options) {
      74              :         // With a filter the list is not used
      75          575 :         if (!options.origin_filter && !valid_origins(options.origins, options.credentials)) return false;
      76          500 :         if (!all_tokens(options.methods, "method")) return false;
      77          467 :         if (options.allowed_headers && !all_tokens(*options.allowed_headers, "header name")) return false;
      78          434 :         if (!all_tokens(options.exposed_headers, "header name")) return false;
      79          401 :         if (options.max_age && options.max_age->count() < 0) {
      80            9 :             LOG_ERROR("Invalid CORS options: max_age cannot be negative");
      81            9 :             return false;
      82              :         }
      83          392 :         return true;
      84              :     }
      85              : 
      86         1034 :     void add_unless_set(http_response& message, const char* name, std::string_view value) {
      87         5002 :         if (!message.has_header(name)) message.add_header(name, std::string(value));
      88         1034 :     }
      89              : 
      90              :     // The value of a header of the request, repeated ones as one list
      91          603 :     std::string joined(const http_request& request, const char* name) {
      92          603 :         std::string list;
      93         1098 :         for (const auto& value : request.get_headers_with_key(name)) {
      94          495 :             if (!list.empty()) list += ", ";
      95          495 :             list += value;
      96          603 :         }
      97          603 :         return list;
      98            0 :     }
      99              : 
     100              :     // What a preflight says besides what any response says
     101          135 :     void apply_preflight(const cors_policy& policy, const cors_decision& decision, http_response& message) {
     102          135 :         if (!policy.methods.empty()) add_unless_set(message, allow_methods, policy.methods);
     103          135 :         if (policy.allowed_headers) {
     104           12 :             if (!policy.allowed_headers->empty()) add_unless_set(message, allow_headers, *policy.allowed_headers);
     105              :         } else {
     106          123 :             compression::add_vary(message, request_headers);
     107          123 :             if (!decision.requested_headers.empty()) add_unless_set(message, allow_headers, decision.requested_headers);
     108              :         }
     109          135 :         if (!policy.max_age.empty()) add_unless_set(message, max_age, policy.max_age);
     110          135 :     }
     111              : 
     112              : }
     113              : 
     114          575 : std::shared_ptr<const cors_policy> make_cors_policy(cors_options options) {
     115          575 :     if (!valid_options(options)) return nullptr;
     116              : 
     117          392 :     auto policy = std::make_shared<cors_policy>();
     118          392 :     policy->any_origin = !options.origin_filter && options.origins.size() == 1 && options.origins.front() == "*";
     119          392 :     policy->origin_filter = std::move(options.origin_filter);
     120          392 :     if (!policy->any_origin && !policy->origin_filter) policy->origins = std::move(options.origins);
     121          392 :     policy->methods = boost::algorithm::join(options.methods, ", ");
     122          392 :     if (options.allowed_headers) policy->allowed_headers = boost::algorithm::join(*options.allowed_headers, ", ");
     123          392 :     policy->exposed_headers = boost::algorithm::join(options.exposed_headers, ", ");
     124          392 :     if (options.max_age) policy->max_age = std::to_string(options.max_age->count());
     125          392 :     policy->credentials = options.credentials;
     126          392 :     policy->preflight_continue = options.preflight_continue;
     127          392 :     return policy;
     128          392 : }
     129              : 
     130          767 : static std::string granted_origin(const cors_policy& policy, const http_request& request) {
     131         1371 :     if (policy.any_origin) return "*";
     132              : 
     133              :     // Repeated headers make one value, which no entry of the list equals: not a single origin
     134          465 :     std::string origin = joined(request, "Origin");
     135          465 :     if (origin.empty()) return origin;
     136              : 
     137          450 :     bool granted = policy.origin_filter
     138          450 :         ? policy.origin_filter(origin, request)
     139          381 :         : std::any_of(policy.origins.begin(), policy.origins.end(), [&](const std::string& listed) {
     140          429 :               return ::thinger::util::ascii::iequals(listed, origin);
     141          438 :           });
     142          438 :     if (!granted) origin.clear();
     143          438 :     return origin;
     144          465 : }
     145              : 
     146          767 : cors_decision decide_cors(const cors_policy& policy, const http_request& request) {
     147          767 :     cors_decision decision;
     148          767 :     decision.origin = granted_origin(policy, request);
     149              :     // As Fetch and rs/cors: any other OPTIONS is a request as any other
     150          755 :     if (request.get_method() == method::OPTIONS && !request.get_header(request_method).empty()) {
     151          150 :         decision.preflight = true;
     152          150 :         decision.answer_preflight = !policy.preflight_continue;
     153          150 :         if (!policy.allowed_headers) decision.requested_headers = joined(request, request_headers);
     154              :     }
     155          755 :     return decision;
     156           12 : }
     157              : 
     158          767 : void apply_cors(const cors_policy& policy, const cors_decision& decision, http_response& message) {
     159              :     // The answer depends on the Origin of the request, granted or not, and even without one
     160          767 :     if (!policy.any_origin) compression::add_vary(message, "Origin");
     161          767 :     if (decision.origin.empty()) return;
     162              : 
     163              :     // What the handler or a middleware set stays, as res.setHeader() after cors() in Express
     164          572 :     add_unless_set(message, allow_origin, decision.origin);
     165              :     // Never with "*", whoever set it: the filter may grant an Origin: *, or the handler set it
     166          572 :     if (const auto& allowed = message.get_header(allow_origin); policy.credentials && !allowed.empty() && allowed != "*") {
     167          216 :         add_unless_set(message, allow_credentials, "true");
     168              :     }
     169          572 :     if (!policy.exposed_headers.empty()) add_unless_set(message, expose_headers, policy.exposed_headers);
     170          572 :     if (decision.preflight) apply_preflight(policy, decision, message);
     171              : }
     172              : 
     173              : } // namespace thinger::http::detail
        

Generated by: LCOV version 2.0-1