Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions include/boost/http_proto/server/cors.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/http_proto
//

#ifndef BOOST_HTTP_PROTO_SERVER_CORS_HPP
#define BOOST_HTTP_PROTO_SERVER_CORS_HPP

#include <boost/http_proto/detail/config.hpp>
#include <boost/http_proto/server/route_handler.hpp>
#include <boost/http_proto/status.hpp>
#include <chrono>

namespace boost {
namespace http_proto {

struct cors_options
{
std::string origin;
std::string methods;
std::string allowedHeaders;
std::string exposedHeaders;
std::chrono::seconds max_age{ 0 };
status result = status::no_content;
bool preFligthContinue = false;
bool credentials = false;
};

class cors
{
public:
BOOST_HTTP_PROTO_DECL
explicit cors(
cors_options options = {}) noexcept;

BOOST_HTTP_PROTO_DECL
route_result
operator()(
Request& req,
Response& res) const;

private:
cors_options options_;
};

} // http_proto
} // boost

#endif
192 changes: 192 additions & 0 deletions src/server/cors.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/http_proto
//

#include <boost/http_proto/server/cors.hpp>
#include <utility>

namespace boost {
namespace http_proto {

cors::
cors(
cors_options options) noexcept
: options_(std::move(options))
{
// VFALCO TODO Validate the strings in options against RFC
}

namespace {

struct Vary
{
Vary(Response& res)
: res_(res)
{
}

void set(field f, core::string_view s)
{
res_.message.set(f, s);
}

void append(field f, core::string_view v)
{
auto it = res_.message.find(f);
if (it != res_.message.end())
{
std::string s = it->value;
s += ", ";
s += v;
res_.message.set(it, s);
}
else
{
res_.message.set(f, v);
}
}

private:
Response& res_;
std::string v_;
};

} // (anon)

// Access-Control-Allow-Origin
static void setOrigin(
Vary& v,
Request const&,
cors_options const& options)
{
if( options.origin.empty() ||
options.origin == "*")
{
v.set(field::access_control_allow_origin, "*");
return;
}

v.set(
field::access_control_allow_origin,
options.origin);
v.append(field::vary, to_string(field::origin));
}

// Access-Control-Allow-Methods
static void setMethods(
Vary& v,
cors_options const& options)
{
if(! options.methods.empty())
{
v.set(
field::access_control_allow_methods,
options.methods);
return;
}
v.set(
field::access_control_allow_methods,
"GET,HEAD,PUT,PATCH,POST,DELETE");
}

// Access-Control-Allow-Credentials
static void setCredentials(
Vary& v,
cors_options const& options)
{
if(! options.credentials)
return;
v.set(
field::access_control_allow_credentials,
"true");
}

// Access-Control-Allowed-Headers
static void setAllowedHeaders(
Vary& v,
Request const& req,
cors_options const& options)
{
if(! options.allowedHeaders.empty())
{
v.set(
field::access_control_allow_headers,
options.allowedHeaders);
return;
}
auto s = req.message.value_or(
field::access_control_request_headers, "");
if(! s.empty())
{
v.set(
field::access_control_allow_headers,
s);
v.append(field::vary, s);
}
}

// Access-Control-Expose-Headers
static void setExposeHeaders(
Vary& v,
cors_options const& options)
{
if(options.exposedHeaders.empty())
return;
v.set(
field::access_control_expose_headers,
options.exposedHeaders);
}

// Access-Control-Max-Age
static void setMaxAge(
Vary& v,
cors_options const& options)
{
if(options.max_age.count() == 0)
return;
v.set(
field::access_control_max_age,
std::to_string(
options.max_age.count()));
}

route_result
cors::
operator()(
Request& req,
Response& res) const
{
Vary v(res);
if(req.message.method() ==
method::options)
{
// preflight
setOrigin(v, req, options_);
setMethods(v, options_);
setCredentials(v, options_);
setAllowedHeaders(v, req, options_);
setMaxAge(v, options_);
setExposeHeaders(v, options_);

if(options_.preFligthContinue)
return route::next;
// Safari and others need this for 204 or may hang
res.message.set_status(options_.result);
res.message.set_content_length(0);
res.serializer.start(res.message);
return route::send;
}
// actual response
setOrigin(v, req, options_);
setCredentials(v, options_);
setExposeHeaders(v, options_);
return route::next;
}

} // http_proto
} // boost
104 changes: 104 additions & 0 deletions test/unit/server/cors.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/http_proto
//

// Test that header file is self-contained.
#include <boost/http_proto/server/cors.hpp>
#include "src/rfc/detail/rules.hpp"

#include "test_suite.hpp"

namespace boost {
namespace http_proto {

class field_item
{
public:
field_item(
core::string_view s)
: s_(s)
{
grammar::parse(s_,
detail::field_name_rule).value();
}

field_item(
field f) noexcept
: s_(to_string(f))
{
}

operator core::string_view() const noexcept
{
return s_;
}

private:
core::string_view s_;
};

template<class Element>
struct list
{
struct item
{
core::string_view s;

template<
class T,
class = typename std::enable_if<
std::is_constructible<
Element, T>::value>::type>
item(T&& t)
: s(Element(std::forward<T>(t)))
{
}
};

public:
list(std::initializer_list<item> init)
{
if(init.size() == 0)
return;
auto it = init.begin();
s_ = it->s;
while(++it != init.end())
{
s_.push_back(',');
s_.append(it->s.data(),
it->s.size());
}
}

core::string_view get() const noexcept
{
return s_;
}

private:
std::string s_;
};

struct cors_test
{
void run()
{
list<field_item> v({
field::access_control_allow_origin,
"example.com",
"example.org"
});
}
};

TEST_SUITE(
cors_test,
"boost.http_proto.server.cors");

} // http_proto
} // boost
Loading