From 897d1eb3966e995d0cd38a4fd9b32486388217e4 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 30 Nov 2025 18:16:53 -0800 Subject: [PATCH] feat: cors middleware --- include/boost/http_proto/server/cors.hpp | 53 +++++++ src/server/cors.cpp | 192 +++++++++++++++++++++++ test/unit/server/cors.cpp | 104 ++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 include/boost/http_proto/server/cors.hpp create mode 100644 src/server/cors.cpp create mode 100644 test/unit/server/cors.cpp diff --git a/include/boost/http_proto/server/cors.hpp b/include/boost/http_proto/server/cors.hpp new file mode 100644 index 00000000..eb6dd288 --- /dev/null +++ b/include/boost/http_proto/server/cors.hpp @@ -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 +#include +#include +#include + +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 diff --git a/src/server/cors.cpp b/src/server/cors.cpp new file mode 100644 index 00000000..4293f6a0 --- /dev/null +++ b/src/server/cors.cpp @@ -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 +#include + +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 diff --git a/test/unit/server/cors.cpp b/test/unit/server/cors.cpp new file mode 100644 index 00000000..730d9ee5 --- /dev/null +++ b/test/unit/server/cors.cpp @@ -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 +#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 +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))) + { + } + }; + +public: + list(std::initializer_list 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 v({ + field::access_control_allow_origin, + "example.com", + "example.org" + }); + } +}; + +TEST_SUITE( + cors_test, + "boost.http_proto.server.cors"); + +} // http_proto +} // boost