Skip to content

Commit 4233c34

Browse files
committed
feat: cors middleware
1 parent a351fe9 commit 4233c34

File tree

3 files changed

+349
-0
lines changed

3 files changed

+349
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/http_proto
8+
//
9+
10+
#ifndef BOOST_HTTP_PROTO_SERVER_CORS_HPP
11+
#define BOOST_HTTP_PROTO_SERVER_CORS_HPP
12+
13+
#include <boost/http_proto/detail/config.hpp>
14+
#include <boost/http_proto/server/route_handler.hpp>
15+
#include <boost/http_proto/status.hpp>
16+
#include <chrono>
17+
18+
namespace boost {
19+
namespace http_proto {
20+
21+
struct cors_options
22+
{
23+
std::string origin;
24+
std::string methods;
25+
std::string allowedHeaders;
26+
std::string exposedHeaders;
27+
std::chrono::seconds max_age{ 0 };
28+
status result = status::no_content;
29+
bool preFligthContinue = false;
30+
bool credentials = false;
31+
};
32+
33+
class cors
34+
{
35+
public:
36+
BOOST_HTTP_PROTO_DECL
37+
explicit cors(
38+
cors_options options = {}) noexcept;
39+
40+
BOOST_HTTP_PROTO_DECL
41+
route_result
42+
operator()(
43+
Request& req,
44+
Response& res) const;
45+
46+
private:
47+
cors_options options_;
48+
};
49+
50+
} // http_proto
51+
} // boost
52+
53+
#endif

src/server/cors.cpp

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
//
2+
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/http_proto
8+
//
9+
10+
#include <boost/http_proto/server/cors.hpp>
11+
#include <utility>
12+
13+
namespace boost {
14+
namespace http_proto {
15+
16+
cors::
17+
cors(
18+
cors_options options) noexcept
19+
: options_(std::move(options))
20+
{
21+
// VFALCO TODO Validate the strings in options against RFC
22+
}
23+
24+
namespace {
25+
26+
struct Vary
27+
{
28+
Vary(Response& res)
29+
: res_(res)
30+
{
31+
}
32+
33+
void set(field f, core::string_view s)
34+
{
35+
res_.message.set(f, s);
36+
}
37+
38+
void append(field f, core::string_view v)
39+
{
40+
auto it = res_.message.find(f);
41+
if (it != res_.message.end())
42+
{
43+
std::string s = it->value;
44+
s += ", ";
45+
s += v;
46+
res_.message.set(it, s);
47+
}
48+
else
49+
{
50+
res_.message.set(f, v);
51+
}
52+
}
53+
54+
private:
55+
Response& res_;
56+
std::string v_;
57+
};
58+
59+
} // (anon)
60+
61+
// Access-Control-Allow-Origin
62+
static void setOrigin(
63+
Vary& v,
64+
Request const&,
65+
cors_options const& options)
66+
{
67+
if( options.origin.empty() ||
68+
options.origin == "*")
69+
{
70+
v.set(field::access_control_allow_origin, "*");
71+
return;
72+
}
73+
74+
v.set(
75+
field::access_control_allow_origin,
76+
options.origin);
77+
v.append(field::vary, to_string(field::origin));
78+
}
79+
80+
// Access-Control-Allow-Methods
81+
static void setMethods(
82+
Vary& v,
83+
cors_options const& options)
84+
{
85+
if(! options.methods.empty())
86+
{
87+
v.set(
88+
field::access_control_allow_methods,
89+
options.methods);
90+
return;
91+
}
92+
v.set(
93+
field::access_control_allow_methods,
94+
"GET,HEAD,PUT,PATCH,POST,DELETE");
95+
}
96+
97+
// Access-Control-Allow-Credentials
98+
static void setCredentials(
99+
Vary& v,
100+
cors_options const& options)
101+
{
102+
if(! options.credentials)
103+
return;
104+
v.set(
105+
field::access_control_allow_credentials,
106+
"true");
107+
}
108+
109+
// Access-Control-Allowed-Headers
110+
static void setAllowedHeaders(
111+
Vary& v,
112+
Request const& req,
113+
cors_options const& options)
114+
{
115+
if(! options.allowedHeaders.empty())
116+
{
117+
v.set(
118+
field::access_control_allow_headers,
119+
options.allowedHeaders);
120+
return;
121+
}
122+
auto s = req.message.value_or(
123+
field::access_control_request_headers, "");
124+
if(! s.empty())
125+
{
126+
v.set(
127+
field::access_control_allow_headers,
128+
s);
129+
v.append(field::vary, s);
130+
}
131+
}
132+
133+
// Access-Control-Expose-Headers
134+
static void setExposeHeaders(
135+
Vary& v,
136+
cors_options const& options)
137+
{
138+
if(options.exposedHeaders.empty())
139+
return;
140+
v.set(
141+
field::access_control_expose_headers,
142+
options.exposedHeaders);
143+
}
144+
145+
// Access-Control-Max-Age
146+
static void setMaxAge(
147+
Vary& v,
148+
cors_options const& options)
149+
{
150+
if(options.max_age.count() == 0)
151+
return;
152+
v.set(
153+
field::access_control_max_age,
154+
std::to_string(
155+
options.max_age.count()));
156+
}
157+
158+
route_result
159+
cors::
160+
operator()(
161+
Request& req,
162+
Response& res) const
163+
{
164+
Vary v(res);
165+
if(req.message.method() ==
166+
method::options)
167+
{
168+
// preflight
169+
setOrigin(v, req, options_);
170+
setMethods(v, options_);
171+
setCredentials(v, options_);
172+
setAllowedHeaders(v, req, options_);
173+
setMaxAge(v, options_);
174+
setExposeHeaders(v, options_);
175+
176+
if(options_.preFligthContinue)
177+
return route::next;
178+
// Safari and others need this for 204 or may hang
179+
res.message.set_status(options_.result);
180+
res.message.set_content_length(0);
181+
res.serializer.start(res.message);
182+
return route::send;
183+
}
184+
// actual response
185+
setOrigin(v, req, options_);
186+
setCredentials(v, options_);
187+
setExposeHeaders(v, options_);
188+
return route::next;
189+
}
190+
191+
} // http_proto
192+
} // boost

test/unit/server/cors.cpp

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//
2+
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/http_proto
8+
//
9+
10+
// Test that header file is self-contained.
11+
#include <boost/http_proto/server/cors.hpp>
12+
#include "src/rfc/detail/rules.hpp"
13+
14+
#include "test_suite.hpp"
15+
16+
namespace boost {
17+
namespace http_proto {
18+
19+
class field_item
20+
{
21+
public:
22+
field_item(
23+
core::string_view s)
24+
: s_(s)
25+
{
26+
grammar::parse(s_,
27+
detail::field_name_rule).value();
28+
}
29+
30+
field_item(
31+
field f) noexcept
32+
: s_(to_string(f))
33+
{
34+
}
35+
36+
operator core::string_view() const noexcept
37+
{
38+
return s_;
39+
}
40+
41+
private:
42+
core::string_view s_;
43+
};
44+
45+
template<class Element>
46+
struct list
47+
{
48+
struct item
49+
{
50+
core::string_view s;
51+
52+
template<
53+
class T,
54+
class = typename std::enable_if<
55+
std::is_constructible<
56+
Element, T>::value>::type>
57+
item(T&& t)
58+
: s(Element(std::forward<T>(t)))
59+
{
60+
}
61+
};
62+
63+
public:
64+
list(std::initializer_list<item> init)
65+
{
66+
if(init.size() == 0)
67+
return;
68+
auto it = init.begin();
69+
s_ = it->s;
70+
while(++it != init.end())
71+
{
72+
s_.push_back(',');
73+
s_.append(it->s.data(),
74+
it->s.size());
75+
}
76+
}
77+
78+
core::string_view get() const noexcept
79+
{
80+
return s_;
81+
}
82+
83+
private:
84+
std::string s_;
85+
};
86+
87+
struct cors_test
88+
{
89+
void run()
90+
{
91+
list<field_item> v({
92+
field::access_control_allow_origin,
93+
"example.com",
94+
"example.org"
95+
});
96+
}
97+
};
98+
99+
TEST_SUITE(
100+
cors_test,
101+
"boost.http_proto.server.cors");
102+
103+
} // http_proto
104+
} // boost

0 commit comments

Comments
 (0)