diff --git a/docs/guides/included-middleware.md b/docs/guides/included-middleware.md index d7819d9e04..dd0833744a 100644 --- a/docs/guides/included-middleware.md +++ b/docs/guides/included-middleware.md @@ -90,7 +90,7 @@ This middleware allows to set CORS policies by using `CORSHandler`. Once enabled The CORS rules can be modified by first getting the middleware via `#!cpp auto& cors = app.get_middleware();`. The rules can be set per URL prefix using `prefix()`, per blueprint using `blueprint()`, or globally via `global()`. These will return a `CORSRules` object which contains the actual rules for the prefix, blueprint, or application. For more details go [here](../reference/structcrow_1_1_c_o_r_s_handler.html). -`CORSRules` can be modified using the methods `origin()`, `methods()`, `headers()`, `max_age()`, `allow_credentials()`, or `ignore()`. For more details on these methods and what default values they take go [here](../reference/structcrow_1_1_c_o_r_s_rules.html). +`CORSRules` can be modified using the methods `origin()`, `origins()`, `methods()`, `headers()`, `max_age()`, `allow_credentials()`, or `ignore()`. For more details on these methods and what default values they take go [here](../reference/structcrow_1_1_c_o_r_s_rules.html). ```cpp auto& cors = app.get_middleware(); @@ -99,5 +99,5 @@ cors .headers("X-Custom-Header", "Upgrade-Insecure-Requests") .methods("POST"_method, "GET"_method) .prefix("/cors") - .origin("example.com"); + .origins("https://example.com", "https://api.example.com"); ``` diff --git a/examples/middlewares/example_cors.cpp b/examples/middlewares/example_cors.cpp index 9b4fb50ca5..34f818fe55 100644 --- a/examples/middlewares/example_cors.cpp +++ b/examples/middlewares/example_cors.cpp @@ -23,7 +23,7 @@ int main() .headers("X-Custom-Header", "Upgrade-Insecure-Requests") .methods("POST"_method, "GET"_method) .prefix("/cors") - .origin("example.com") + .origins("https://example.com", "https://api.example.com") .prefix("/nocors") .ignore(); // clang-format on diff --git a/include/crow/http_connection.h b/include/crow/http_connection.h index fe748ec937..3faef940e6 100644 --- a/include/crow/http_connection.h +++ b/include/crow/http_connection.h @@ -113,7 +113,7 @@ namespace crow { routing_handle_result_ = handler_->handle_initial(req_, res); // if no route is found for the request method, return the response without parsing or processing anything further. - if (!routing_handle_result_->rule_index && !routing_handle_result_->catch_all) + if (!routing_handle_result_->rule_index && !routing_handle_result_->catch_all && req_.method != HTTPMethod::Options) { parser_.done(); need_to_call_after_handlers_ = true; diff --git a/include/crow/middlewares/cors.h b/include/crow/middlewares/cors.h index 15e7d42131..9d9fe10cc4 100644 --- a/include/crow/middlewares/cors.h +++ b/include/crow/middlewares/cors.h @@ -1,4 +1,5 @@ #pragma once +#include #include "crow/common.h" #include "crow/http_request.h" #include "crow/http_response.h" @@ -17,6 +18,28 @@ namespace crow CORSRules& origin(const std::string& origin) { origin_ = origin; + origins_.clear(); + return *this; + } + + /// Set dynamically selected Access-Control-Allow-Origin values + /// If the request Origin header matches one of these values, that + /// origin is echoed back + CORSRules& origins(const std::string& origin) + { + if (std::find(origins_.begin(), origins_.end(), origin) == origins_.end()) + { + origins_.emplace_back(origin); + } + return *this; + } + + /// Set dynamically selected Access-Control-Allow-Origin values + template + CORSRules& origins(const std::string& origin, Origins... origin_list) + { + origins(origin); + origins(origin_list...); return *this; } @@ -123,34 +146,68 @@ namespace crow { if (ignore_) return; + const std::string& request_origin = req.get_header_value("Origin"); + std::string allow_origin_value; + bool dynamic_origin = false; + + if (!origins_.empty()) + { + if (request_origin.empty()) return; + if (std::find(origins_.begin(), origins_.end(), request_origin) == origins_.end()) return; + + allow_origin_value = request_origin; + dynamic_origin = true; + } + else if (allow_credentials_ && origin_ == "*" && !request_origin.empty()) + { + // credentials are not compatible with a wildcard origin value + allow_origin_value = request_origin; + dynamic_origin = true; + } + else + { + allow_origin_value = origin_; + } + set_header_no_override("Access-Control-Allow-Methods", methods_, res); set_header_no_override("Access-Control-Allow-Headers", headers_, res); set_header_no_override("Access-Control-Expose-Headers", exposed_headers_, res); set_header_no_override("Access-Control-Max-Age", max_age_, res); - bool origin_set = false; + set_header_no_override("Access-Control-Allow-Origin", allow_origin_value, res); + if (allow_credentials_ && allow_origin_value != "*") + { + set_header_no_override("Access-Control-Allow-Credentials", "true", res); + } + if (dynamic_origin) + { + add_vary_origin_no_override(res); + } + } + + void add_vary_origin_no_override(crow::response& res) + { + const std::string& vary_header = get_header_value(res.headers, "Vary"); + if (vary_header.empty()) + { + res.add_header("Vary", "Origin"); + return; + } - if (req.method != HTTPMethod::Options) + for (const std::string& current : utility::split(vary_header, ",")) { - if (allow_credentials_) + if (utility::string_equals(utility::trim(current), "Origin")) { - set_header_no_override("Access-Control-Allow-Credentials", "true", res); - if (origin_ == "*") - { - set_header_no_override("Access-Control-Allow-Origin", req.get_header_value("Origin"), res); - origin_set = true; - } + return; } } - if( !origin_set){ - set_header_no_override("Access-Control-Allow-Origin", origin_, res); - } + res.set_header("Vary", vary_header + ", Origin"); } bool ignore_ = false; - // TODO: support multiple origins that are dynamically selected std::string origin_ = "*"; + std::vector origins_; std::string methods_ = "*"; std::string headers_ = "*"; std::string exposed_headers_; diff --git a/tests/unittest.cpp b/tests/unittest.cpp index 88f730894f..244aa4377a 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -1234,6 +1234,9 @@ TEST_CASE("middleware_cors") .origin("test.test") .prefix("/auth-origin") .allow_credentials() + .prefix("/multi-origin") + .origins("https://a.test", "https://b.test") + .allow_credentials() .prefix("/expose") .expose("exposed-header") .prefix("/nocors") @@ -1259,6 +1262,15 @@ TEST_CASE("middleware_cors") return "-"; }); + CROW_ROUTE(app, "/multi-origin") + ([&](const request&) { + return "-"; + }); + + CROW_ROUTE(app, "/multi-origin").methods(crow::HTTPMethod::Options)([&](const request&) { + return "-"; + }); + CROW_ROUTE(app, "/expose") ([&](const request&) { return "-"; @@ -1273,7 +1285,7 @@ TEST_CASE("middleware_cors") auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(port).run_async(); app.wait_for_server_start(); auto resp = HttpClient::request(LOCALHOST_ADDRESS, port, - "OPTIONS / HTTP/1.1\r\n\r\n"); + "OPTIONS / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"); CHECK(resp.find("Access-Control-Allow-Origin: *") != std::string::npos); @@ -1291,9 +1303,26 @@ TEST_CASE("middleware_cors") CHECK(resp.find("Access-Control-Allow-Credentials: true") != std::string::npos); resp = HttpClient::request(LOCALHOST_ADDRESS, port, - "OPTIONS /auth-origin / HTTP/1.1 \r\n\r\n"); - CHECK(resp.find("Access-Control-Allow-Origin: *") != std::string::npos); - CHECK(resp.find("Access-Control-Allow-Credentials: true") == std::string::npos); + "OPTIONS /auth-origin HTTP/1.0\r\nOrigin: test-client\r\nConnection: close\r\n\r\n"); + CHECK(resp.find("Access-Control-Allow-Origin: test-client") != std::string::npos); + CHECK(resp.find("Access-Control-Allow-Credentials: true") != std::string::npos); + + resp = HttpClient::request(LOCALHOST_ADDRESS, port, + "GET /multi-origin HTTP/1.0\r\nOrigin: https://a.test\r\nConnection: close\r\n\r\n"); + CHECK(resp.find("Access-Control-Allow-Origin: https://a.test") != std::string::npos); + CHECK(resp.find("Access-Control-Allow-Credentials: true") != std::string::npos); + CHECK(resp.find("Vary: Origin") != std::string::npos); + + resp = HttpClient::request(LOCALHOST_ADDRESS, port, + "OPTIONS /multi-origin HTTP/1.0\r\nOrigin: https://b.test\r\nConnection: close\r\n\r\n"); + CHECK(resp.find("Access-Control-Allow-Origin: https://b.test") != std::string::npos); + CHECK(resp.find("Access-Control-Allow-Credentials: true") != std::string::npos); + CHECK(resp.find("Vary: Origin") != std::string::npos); + + resp = HttpClient::request(LOCALHOST_ADDRESS, port, + "GET /multi-origin HTTP/1.0\r\nOrigin: https://c.test\r\nConnection: close\r\n\r\n"); + CHECK(resp.find("Access-Control-Allow-Origin:") == std::string::npos); + CHECK(resp.find("Access-Control-Allow-Credentials:") == std::string::npos); resp = HttpClient::request(LOCALHOST_ADDRESS, port, "GET /expose\r\n\r\n"); @@ -2800,7 +2829,9 @@ TEST_CASE("option_header_passed_in_full") }; std::string request = - "OPTIONS /echo HTTP/1.1\r\n"; + "OPTIONS /echo HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n\r\n"; auto res = make_request(request); CHECK(res.find(ServerName) != std::string::npos);