Skip to content

Commit 0bee02e

Browse files
committed
feat server: add CORS middleware
commit_hash:de4a82d74e763267d7c8eb82e8ead14af182bd5e
1 parent 2116bf9 commit 0bee02e

File tree

16 files changed

+528
-1
lines changed

16 files changed

+528
-1
lines changed

.mapping.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,7 @@
10901090
"core/include/userver/server/http/http_status.hpp":"taxi/uservices/userver/core/include/userver/server/http/http_status.hpp",
10911091
"core/include/userver/server/middlewares/builtin.hpp":"taxi/uservices/userver/core/include/userver/server/middlewares/builtin.hpp",
10921092
"core/include/userver/server/middlewares/configuration.hpp":"taxi/uservices/userver/core/include/userver/server/middlewares/configuration.hpp",
1093+
"core/include/userver/server/middlewares/cors.hpp":"taxi/uservices/userver/core/include/userver/server/middlewares/cors.hpp",
10931094
"core/include/userver/server/middlewares/headers_propagator.hpp":"taxi/uservices/userver/core/include/userver/server/middlewares/headers_propagator.hpp",
10941095
"core/include/userver/server/middlewares/http_middleware_base.hpp":"taxi/uservices/userver/core/include/userver/server/middlewares/http_middleware_base.hpp",
10951096
"core/include/userver/server/request/request_config.hpp":"taxi/uservices/userver/core/include/userver/server/request/request_config.hpp",
@@ -1900,6 +1901,8 @@
19001901
"core/src/server/middlewares/baggage.hpp":"taxi/uservices/userver/core/src/server/middlewares/baggage.hpp",
19011902
"core/src/server/middlewares/configuration.cpp":"taxi/uservices/userver/core/src/server/middlewares/configuration.cpp",
19021903
"core/src/server/middlewares/configuration.yaml":"taxi/uservices/userver/core/src/server/middlewares/configuration.yaml",
1904+
"core/src/server/middlewares/cors.cpp":"taxi/uservices/userver/core/src/server/middlewares/cors.cpp",
1905+
"core/src/server/middlewares/cors.yaml":"taxi/uservices/userver/core/src/server/middlewares/cors.yaml",
19031906
"core/src/server/middlewares/deadline_propagation.cpp":"taxi/uservices/userver/core/src/server/middlewares/deadline_propagation.cpp",
19041907
"core/src/server/middlewares/deadline_propagation.hpp":"taxi/uservices/userver/core/src/server/middlewares/deadline_propagation.hpp",
19051908
"core/src/server/middlewares/decompression.cpp":"taxi/uservices/userver/core/src/server/middlewares/decompression.cpp",
@@ -3944,6 +3947,11 @@
39443947
"samples/config_service/static_config.yaml":"taxi/uservices/userver/samples/config_service/static_config.yaml",
39453948
"samples/config_service/tests/conftest.py":"taxi/uservices/userver/samples/config_service/tests/conftest.py",
39463949
"samples/config_service/tests/test_config.py":"taxi/uservices/userver/samples/config_service/tests/test_config.py",
3950+
"samples/cors_service/CMakeLists.txt":"taxi/uservices/userver/samples/cors_service/CMakeLists.txt",
3951+
"samples/cors_service/main.cpp":"taxi/uservices/userver/samples/cors_service/main.cpp",
3952+
"samples/cors_service/static_config.yaml":"taxi/uservices/userver/samples/cors_service/static_config.yaml",
3953+
"samples/cors_service/testsuite/conftest.py":"taxi/uservices/userver/samples/cors_service/testsuite/conftest.py",
3954+
"samples/cors_service/testsuite/test_hello.py":"taxi/uservices/userver/samples/cors_service/testsuite/test_hello.py",
39473955
"samples/digest_auth_service/CMakeLists.txt":"taxi/uservices/userver/samples/digest_auth_service/CMakeLists.txt",
39483956
"samples/digest_auth_service/auth_digest.cpp":"taxi/uservices/userver/samples/digest_auth_service/auth_digest.cpp",
39493957
"samples/digest_auth_service/auth_digest.hpp":"taxi/uservices/userver/samples/digest_auth_service/auth_digest.hpp",

core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ list(APPEND EMBED_FILES
153153
src/server/handlers/ping.yaml
154154
src/server/handlers/server_monitor.yaml
155155
src/server/handlers/tests_control.yaml
156+
src/server/middlewares/cors.yaml
156157
src/server/middlewares/configuration.yaml
157158
src/server/middlewares/headers_propagator.yaml
158159
src/server/websocket/websocket_handler.yaml

core/include/userver/server/http/http_method.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
#include <string>
77

88
#include <fmt/core.h>
9+
#include <userver/formats/parse/to.hpp>
910
#include <userver/utils/fmt_compat.hpp>
1011

1112
USERVER_NAMESPACE_BEGIN
1213

14+
namespace yaml_config {
15+
class YamlConfig;
16+
}
17+
1318
namespace server::http {
1419

1520
/// @brief List of HTTP methods
@@ -31,6 +36,8 @@ const std::string& ToString(HttpMethod method) noexcept;
3136
/// @brief Convert HTTP method string to enum value
3237
HttpMethod HttpMethodFromString(std::string_view method_str);
3338

39+
HttpMethod Parse(const yaml_config::YamlConfig& value, formats::parse::To<HttpMethod>);
40+
3441
} // namespace server::http
3542

3643
USERVER_NAMESPACE_END
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#pragma once
2+
3+
/// @file userver/server/middlewares/cors.hpp
4+
/// @brief CORS (Cross-Origin Resource Sharing) middleware
5+
6+
#include <string>
7+
#include <vector>
8+
9+
#include <userver/formats/parse/to.hpp>
10+
#include <userver/http/common_headers.hpp>
11+
#include <userver/server/http/http_method.hpp>
12+
#include <userver/server/middlewares/builtin.hpp>
13+
#include <userver/server/middlewares/http_middleware_base.hpp>
14+
#include <userver/yaml_config/schema.hpp>
15+
16+
USERVER_NAMESPACE_BEGIN
17+
18+
namespace server::middlewares {
19+
20+
/// @brief CORS (Cross-Origin Resource Sharing) middleware
21+
///
22+
/// This middleware handles CORS preflight requests and adds appropriate CORS headers
23+
/// to responses to allow cross-origin requests from web browsers.
24+
///
25+
/// @note The middleware is usually created by @ref server::middlewares::CorsFactory
26+
class Cors final : public HttpMiddlewareBase {
27+
public:
28+
struct Config {
29+
/// Allowed origins. Use "*" to allow all origins (not recommended for production)
30+
std::vector<std::string> allowed_origins;
31+
32+
/// Allowed HTTP methods
33+
std::vector<std::string> allowed_methods = {
34+
ToString(http::HttpMethod::kGet),
35+
ToString(http::HttpMethod::kPost),
36+
ToString(http::HttpMethod::kPut),
37+
ToString(http::HttpMethod::kPatch),
38+
ToString(http::HttpMethod::kDelete),
39+
ToString(http::HttpMethod::kHead),
40+
ToString(http::HttpMethod::kOptions),
41+
};
42+
43+
/// Allowed headers
44+
std::vector<std::string> allowed_headers = {
45+
std::string(USERVER_NAMESPACE::http::headers::kAccept),
46+
std::string(USERVER_NAMESPACE::http::headers::kAcceptLanguage),
47+
std::string(USERVER_NAMESPACE::http::headers::kContentLanguage),
48+
std::string(USERVER_NAMESPACE::http::headers::kContentType),
49+
};
50+
51+
/// Headers that can be exposed to the browser
52+
std::vector<std::string> exposed_headers;
53+
54+
/// Whether to allow credentials (cookies, authorization headers)
55+
bool allow_credentials{false};
56+
57+
/// Maximum age for preflight cache
58+
std::chrono::seconds max_age{std::chrono::hours(24)};
59+
};
60+
61+
explicit Cors(const Config& config);
62+
63+
private:
64+
void HandleRequest(http::HttpRequest& request, request::RequestContext& context) const override;
65+
66+
bool IsPreflightRequest(const http::HttpRequest& request) const;
67+
void HandlePreflightRequest(http::HttpRequest& request) const;
68+
void AddCorsHeaders(http::HttpRequest& request, const std::string& origin) const;
69+
bool IsOriginAllowed(const std::string& origin) const;
70+
const std::string& GetOriginHeader(const http::HttpRequest& request) const;
71+
72+
const Config config_;
73+
};
74+
75+
/// @ingroup userver_components
76+
///
77+
/// @brief Factory for @ref server::middlewares::Cors
78+
///
79+
/// The middleware handles CORS preflight requests and adds appropriate CORS headers
80+
/// to responses to allow cross-origin requests from web browsers.
81+
/// For additional information about CORS please see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS.
82+
///
83+
/// ## Static options of `CorsFactory`:
84+
///
85+
/// @include{doc} scripts/docs/en/components_schema/core/src/server/middlewares/cors.md
86+
///
87+
/// Options inherited from @ref components::RawComponentBase :
88+
/// @include{doc} scripts/docs/en/components_schema/core/src/components/impl/component_base.md
89+
///
90+
/// @see @ref server::middlewares::Cors
91+
class CorsFactory final : public HttpMiddlewareFactoryBase {
92+
public:
93+
/// @ingroup userver_component_names
94+
/// @brief The default name of @ref server::middlewares::CorsFactory component
95+
static constexpr std::string_view kName = "cors-middleware";
96+
97+
CorsFactory(const components::ComponentConfig&, const components::ComponentContext&);
98+
99+
static yaml_config::Schema GetStaticConfigSchema();
100+
101+
yaml_config::Schema GetMiddlewareConfigSchema() const override;
102+
103+
private:
104+
std::unique_ptr<HttpMiddlewareBase> Create(
105+
const handlers::HttpHandlerBase& handler,
106+
yaml_config::YamlConfig middleware_config
107+
) const override;
108+
109+
const yaml_config::YamlConfig global_config_;
110+
};
111+
112+
/// Parse CORS configuration from YAML
113+
Cors::Config Parse(const yaml_config::YamlConfig& value, formats::parse::To<Cors::Config>);
114+
115+
} // namespace server::middlewares
116+
117+
template <>
118+
inline constexpr bool components::kHasValidate<server::middlewares::CorsFactory> = true;
119+
120+
template <>
121+
inline constexpr auto components::kConfigFileMode<server::middlewares::CorsFactory> = ConfigFileMode::kRequired;
122+
123+
USERVER_NAMESPACE_END

core/src/server/http/http_method.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include <userver/server/http/http_method.hpp>
22

3+
#include <userver/yaml_config/yaml_config.hpp>
4+
35
USERVER_NAMESPACE_BEGIN
46

57
namespace server::http {
@@ -89,6 +91,11 @@ HttpMethod HttpMethodFromString(std::string_view method_str) {
8991
return result;
9092
}
9193

94+
HttpMethod Parse(const yaml_config::YamlConfig& value, formats::parse::To<HttpMethod>)
95+
{
96+
return HttpMethodFromString(value.As<std::string>());
97+
}
98+
9299
const std::string& ToString(HttpMethod method) noexcept {
93100
const auto& strings = GetHttpMethodStrings();
94101

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#include <userver/server/middlewares/cors.hpp>
2+
3+
#include <algorithm>
4+
5+
#include <userver/components/component_config.hpp>
6+
#include <userver/components/component_context.hpp>
7+
#include <userver/formats/common/merge.hpp>
8+
#include <userver/formats/yaml/serialize.hpp>
9+
#include <userver/formats/yaml/value_builder.hpp>
10+
#include <userver/logging/log.hpp>
11+
#include <userver/server/handlers/exceptions.hpp>
12+
#include <userver/server/http/http_request.hpp>
13+
#include <userver/server/http/http_response.hpp>
14+
#include <userver/server/http/http_status.hpp>
15+
#include <userver/utils/algo.hpp>
16+
#include <userver/utils/text_light.hpp>
17+
#include <userver/yaml_config/merge_schemas.hpp>
18+
19+
#ifndef ARCADIA_ROOT
20+
#include "generated/src/server/middlewares/cors.yaml.hpp" // Y_IGNORE
21+
#endif
22+
23+
USERVER_NAMESPACE_BEGIN
24+
25+
namespace server::middlewares {
26+
27+
namespace {
28+
29+
constexpr std::string_view kAccessControlRequestMethod = "Access-Control-Request-Method";
30+
constexpr std::string_view kAccessControlAllowMethods = "Access-Control-Allow-Methods";
31+
constexpr std::string_view kAccessControlAllowHeaders = "Access-Control-Allow-Headers";
32+
constexpr std::string_view kAccessControlMaxAge = "Access-Control-Max-Age";
33+
constexpr std::string_view kAccessControlAllowOrigin = "Access-Control-Allow-Origin";
34+
constexpr std::string_view kAccessControlAllowCredentials = "Access-Control-Allow-Credentials";
35+
constexpr std::string_view kAccessControlExposeHeaders = "Access-Control-Expose-Headers";
36+
37+
} // namespace
38+
39+
Cors::Cors(const Config& config)
40+
: config_(config)
41+
{}
42+
43+
void Cors::HandleRequest(http::HttpRequest& request, request::RequestContext& context) const {
44+
const auto& origin = GetOriginHeader(request);
45+
46+
if (IsPreflightRequest(request)) {
47+
HandlePreflightRequest(request);
48+
return; // Don't call Next() for preflight requests
49+
}
50+
51+
// For actual requests, add CORS headers and continue processing
52+
if (!origin.empty()) {
53+
if (IsOriginAllowed(origin)) {
54+
AddCorsHeaders(request, origin);
55+
} else {
56+
// NOLINTNEXTLINE(google-build-using-namespace)
57+
using namespace server::handlers;
58+
throw ClientError(
59+
HandlerErrorCode::kUnauthorized,
60+
ServiceErrorCode{"Access forbidden"},
61+
InternalMessage{"Origin is forbidden"},
62+
ExternalBody{"Bad Origin header"}
63+
);
64+
}
65+
}
66+
67+
Next(request, context);
68+
}
69+
70+
bool Cors::IsPreflightRequest(const http::HttpRequest& request) const {
71+
return request.GetMethod() == http::HttpMethod::kOptions && request.HasHeader(kAccessControlRequestMethod);
72+
}
73+
74+
void Cors::HandlePreflightRequest(http::HttpRequest& request) const {
75+
const auto& origin = GetOriginHeader(request);
76+
77+
if (origin.empty() || !IsOriginAllowed(origin)) {
78+
request.GetHttpResponse().SetStatus(http::HttpStatus::kForbidden);
79+
return;
80+
}
81+
82+
const auto& requested_method = request.GetHeader(kAccessControlRequestMethod);
83+
if (requested_method.empty()) {
84+
request.GetHttpResponse().SetStatus(http::HttpStatus::kBadRequest);
85+
return;
86+
}
87+
88+
if (!utils::Contains(config_.allowed_methods, requested_method)) {
89+
request.GetHttpResponse().SetStatus(http::HttpStatus::kMethodNotAllowed);
90+
return;
91+
}
92+
93+
// All checks passed, send successful preflight response
94+
auto& response = request.GetHttpResponse();
95+
response.SetStatus(http::HttpStatus::kNoContent);
96+
97+
AddCorsHeaders(request, origin);
98+
99+
// Add preflight-specific headers
100+
response.SetHeader(kAccessControlAllowMethods, utils::text::Join(config_.allowed_methods, " ,"));
101+
102+
if (!config_.allowed_headers.empty()) {
103+
response.SetHeader(kAccessControlAllowHeaders, utils::text::Join(config_.allowed_headers, ", "));
104+
}
105+
106+
response.SetHeader(kAccessControlMaxAge, std::to_string(config_.max_age.count()));
107+
}
108+
109+
void Cors::AddCorsHeaders(http::HttpRequest& request, const std::string& origin) const {
110+
auto& response = request.GetHttpResponse();
111+
112+
// Always set the origin for allowed requests
113+
response.SetHeader(kAccessControlAllowOrigin, origin);
114+
115+
// Set credentials header if allowed
116+
if (config_.allow_credentials) {
117+
response.SetHeader(kAccessControlAllowCredentials, std::string{"true"});
118+
}
119+
120+
// Set exposed headers if any
121+
if (!config_.exposed_headers.empty()) {
122+
response.SetHeader(kAccessControlExposeHeaders, utils::text::Join(config_.exposed_headers, ", "));
123+
}
124+
125+
response.SetHeader(USERVER_NAMESPACE::http::headers::kVary, std::string{"Origin"});
126+
}
127+
128+
bool Cors::IsOriginAllowed(const std::string& origin) const {
129+
if (origin.empty()) {
130+
return false;
131+
}
132+
133+
// Check if wildcard is allowed
134+
if (utils::Contains(config_.allowed_origins, "*")) {
135+
return true;
136+
}
137+
138+
LOG_INFO() << config_.allowed_origins;
139+
// Check for exact matches
140+
return utils::Contains(config_.allowed_origins, origin);
141+
}
142+
143+
const std::string& Cors::GetOriginHeader(const http::HttpRequest& request) const { return request.GetHeader("Origin"); }
144+
145+
Cors::Config Parse(const yaml_config::YamlConfig& value, formats::parse::To<Cors::Config>) {
146+
Cors::Config config;
147+
148+
config.allowed_origins = value["allowed-origins"].As<std::vector<std::string>>();
149+
config.allow_credentials = value["allow-credentials"].As<bool>(config.allow_credentials);
150+
config.max_age = std::chrono::seconds(value["max-age-seconds"].As<int>(config.max_age.count()));
151+
152+
config.allowed_methods = value["allowed-methods"].As<std::vector<std::string>>(config.allowed_methods);
153+
std::sort(config.allowed_methods.begin(), config.allowed_methods.end());
154+
155+
config.allowed_headers = value["allowed-headers"].As<std::vector<std::string>>(config.allowed_headers);
156+
config.exposed_headers = value["exposed-headers"].As<std::vector<std::string>>({});
157+
158+
return config;
159+
}
160+
161+
CorsFactory::CorsFactory(const components::ComponentConfig& config, const components::ComponentContext& context)
162+
: HttpMiddlewareFactoryBase{config, context},
163+
global_config_{(const yaml_config::YamlConfig&)config} // Explicit slicing
164+
{}
165+
166+
std::unique_ptr<HttpMiddlewareBase> CorsFactory::Create(
167+
const handlers::HttpHandlerBase&,
168+
yaml_config::YamlConfig middleware_config
169+
) const {
170+
formats::yaml::ValueBuilder builder = global_config_.GetRawYamlWithoutConfigVars();
171+
formats::common::Merge(builder, middleware_config.GetRawYamlWithoutConfigVars());
172+
yaml_config::YamlConfig config(builder.ExtractValue(), middleware_config.GetRawConfigVars());
173+
174+
const auto cfg = config.As<Cors::Config>();
175+
return std::make_unique<Cors>(cfg);
176+
}
177+
178+
yaml_config::Schema CorsFactory::GetStaticConfigSchema() {
179+
return yaml_config::MergeSchemasFromResource<ComponentBase>("src/server/middlewares/cors.yaml");
180+
}
181+
182+
yaml_config::Schema CorsFactory::GetMiddlewareConfigSchema() const {
183+
return yaml_config::MergeSchemasFromResource<ComponentBase>("src/server/middlewares/cors.yaml");
184+
}
185+
186+
} // namespace server::middlewares
187+
188+
USERVER_NAMESPACE_END

0 commit comments

Comments
 (0)