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
|