diff --git a/include/boost/http_proto/server/helmet.hpp b/include/boost/http_proto/server/helmet.hpp new file mode 100644 index 00000000..9861a616 --- /dev/null +++ b/include/boost/http_proto/server/helmet.hpp @@ -0,0 +1,366 @@ +// +// Copyright (c) 2025 Amlal El Mahrouss (amlal at nekernel dot org) +// +// 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_HELMET_HPP +#define BOOST_HTTP_PROTO_SERVER_HELMET_HPP + +#include +#include +#include + +namespace boost { +namespace http_proto { + +/** Configuration options for helmet security middleware. + + This structure holds a collection of HTTP security headers + that will be applied to responses. Headers are stored as + key-value pairs where each key can have multiple values. + + @see helmet +*/ +struct helmet_options +{ + using pair_type = std::pair>; + using map_type = std::vector; + + /** Collection of security headers to apply */ + map_type headers; + + /** Set or update a security header. + + If a header with the same name already exists, it will be replaced. + Otherwise, the new header is appended to the collection. + @param helmet_hdr The header name and values to set. + + @return A reference to this object for chaining. + */ + helmet_options& set(const pair_type& helmet_hdr); + + helmet_options(); + ~helmet_options(); +}; + +/** Download behavior for X-Download-Options header. + + Controls how browsers handle file downloads. +*/ +enum class helmet_download_type +{ + /** Prevent opening files directly in IE8+ */ + noopen, + + /** Disable the X-Download-Options header */ + disabled +}; + +/** Frame origin policy for X-Frame-Options header. + + Controls whether the page can be embedded in frames. +*/ +enum class helmet_origin_type +{ + /** Prevent all framing */ + deny, + + /** Allow framing from same origin only */ + sameorigin +}; + +/** Content Security Policy directive values. + + Defines standard CSP source expressions. + + @see helmet::csp_policy +*/ +enum class csp_type +{ + /** Refers to the current origin */ + self, + + /** Blocks all sources */ + none, + + /** Allows inline scripts/styles */ + unsafe_inline, + + /** Allows eval() and similar */ + unsafe_eval, + + /** Enables strict dynamic mode */ + strict_dynamic, + + /** Enables sandbox restrictions */ + sandbox, + + /** Includes samples in violation reports */ + report_sample +}; + +/** Cross-Origin-Opener-Policy values. + + Controls cross-origin window isolation. +*/ +enum class coop_policy_type +{ + /** No isolation (default browser behavior) */ + unsafe_none, + + /** Allow popups from same origin */ + same_origin_allow_popups, + + /** Strict same-origin isolation */ + same_origin +}; + +/** Cross-Origin-Resource-Policy values. + + Controls cross-origin resource sharing. +*/ +enum class corp_policy_type +{ + /** Only same-origin requests allowed */ + same_origin, + + /** Same-site requests allowed */ + same_site, + + /** Cross-origin requests allowed */ + cross_origin +}; + +/** Cross-Origin-Embedder-Policy values. + + Controls embedding of cross-origin resources. +*/ +enum class coep_policy_type +{ + /** No restrictions (default) */ + unsafe_none, + + /** Require CORP header on cross-origin resources */ + require_corp, + + /** Load cross-origin resources without credentials */ + credentialless +}; + +/** Referrer-Policy values. + + Controls how much referrer information is sent with requests. +*/ +enum class referrer_policy_type +{ + /** Never send referrer */ + no_referrer, + + /** Send referrer for HTTPS→HTTPS, not HTTPS→HTTP */ + no_referrer_when_downgrade, + + /** Send referrer for same-origin requests only */ + same_origin, + + /** Send origin only */ + origin, + + /** Send origin only for HTTPS→HTTPS */ + strict_origin, + + /** Send full URL for same-origin, origin for cross-origin */ + origin_when_cross_origin, + + /** Send origin only for HTTPS→HTTPS, nothing for HTTPS→HTTP */ + strict_origin_when_cross_origin, + + /** Always send full URL (unsafe) */ + unsafe_url +}; + +/** X-Permitted-Cross-Domain-Policies values. + + Controls cross-domain policy files (Flash, Acrobat). +*/ +enum class cross_domain_policy_type +{ + /** No policy files allowed */ + none, + + /** Only master policy file */ + master_only, + + /** Policy files with matching content-type */ + by_content_type, + + /** Policy files via FTP filename */ + by_ftp_filename, + + /** All policy files allowed */ + all +}; + +/** Groups the hsts constants here. */ +namespace hsts { + inline static constexpr bool preload = true; + inline static constexpr bool no_preload = false; + inline static constexpr bool no_subdomains = false; + inline static constexpr bool include_subdomains = true; + inline static constexpr size_t default_age = 31536000; +} + +/** List of allowed sources for CSP directives */ +using csp_allow_list = std::vector; + +/** Header name and values pair */ +using option_pair = std::pair>; + +/** Security middleware inspired by helmet.js. + + Helmet helps secure HTTP responses by setting various + HTTP security headers. It provides protection against + common web vulnerabilities like XSS, clickjacking, + and other attacks. + + @par Example + @code + helmet_options opts; + opts.set(x_frame_origin(helmet_origin_type::deny)); + + helmet::csp_policy csp; + csp.allow("script-src", csp_type::self); + opts.set(content_security_policy(csp)); + + srv.wwwroot.use(helmet(opts)); + @endcode + + @see helmet_options +*/ +class helmet +{ + struct impl; + impl* impl_{}; + +public: + BOOST_HTTP_PROTO_DECL + explicit helmet( + helmet_options options = {}); + + BOOST_HTTP_PROTO_DECL + ~helmet(); + + helmet& operator=(helmet&&) noexcept; + helmet(helmet&&) noexcept; + + BOOST_HTTP_PROTO_DECL + route_result + operator()(route_params& p) const; + + /** Content Security Policy builder. + + Provides a fluent interface for constructing CSP headers + with multiple directives and sources. + + @par Example + @code + helmet::csp_policy policy; + policy.allow("script-src", csp_type::self) + .allow("style-src", "https://example.com") + .allow("img-src", csp_type::none); + @endcode + */ + struct csp_policy + { + /** List of CSP directives */ + csp_allow_list list; + + BOOST_HTTP_PROTO_DECL + csp_policy(); + + BOOST_HTTP_PROTO_DECL + ~csp_policy(); + + /** Allows a CSP directive with a predefined type. + + @param allow The directive name (e.g., "script-src"). + @param type The CSP source expression type. + + @return A reference to this object for chaining. + */ + BOOST_HTTP_PROTO_DECL + csp_policy& allow(const core::string_view& allow, + const csp_type& type); + + /** Remove a CSP directive. + + @param allow The directive name to remove. + + @return A reference to this object for chaining. + + @throws std::invalid_argument if the directive is not found. + */ + BOOST_HTTP_PROTO_DECL + csp_policy& remove(const core::string_view& allow); + + /** Append a CSP directive with a URL source. + + @param allow The directive name (e.g., "script-src"). + @param source The URL to allow. + + @return A reference to this object for chaining. + + @throws std::invalid_argument if parameters are empty. + */ + BOOST_HTTP_PROTO_DECL + csp_policy& allow(const core::string_view& allow, + const urls::url_view& source); + }; +}; + +option_pair x_download_options(const helmet_download_type& type); + +option_pair x_frame_origin(const helmet_origin_type& origin); + +option_pair x_xss_protection(); + +option_pair x_content_type_options(); + +option_pair content_security_policy(const helmet::csp_policy& sp); + +/** Return HSTS configuration for the host. + @param age Maximum age in seconds for HSTS enforcement. + @param include_subdomains either hsts::include_subdomains or hsts::no_subdomains + @param preload either hsts::preload or hsts::no_preload + @note Use constants from the hsts namespace. + @return Header name and value pair. +*/ +option_pair strict_transport_security( + std::size_t age, + bool include_subdomains = hsts::include_subdomains, + bool preload = hsts::no_preload); + +option_pair cross_origin_opener_policy(const coop_policy_type& policy = coop_policy_type::same_origin); + +option_pair cross_origin_resource_policy(const corp_policy_type& policy = corp_policy_type::same_origin); + +option_pair cross_origin_embedder_policy(const coep_policy_type& policy = coep_policy_type::require_corp); + +option_pair referrer_policy(const referrer_policy_type& policy = referrer_policy_type::no_referrer); + +option_pair origin_agent_cluster(); + +option_pair dns_prefetch_control(bool allow = false); + +option_pair permitted_cross_domain_policies(const cross_domain_policy_type& policy = cross_domain_policy_type::none); + +option_pair hide_powered_by(); + +} + +} + +#endif diff --git a/src/server/helmet.cpp b/src/server/helmet.cpp new file mode 100644 index 00000000..8080d65d --- /dev/null +++ b/src/server/helmet.cpp @@ -0,0 +1,474 @@ +// +// Copyright (c) 2025 Amlal El Mahrouss (amlal at nekernel dot org) +// +// 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 { + +namespace detail { + + auto setHeaderValues(const std::vector& fields_value) -> std::string + { + if (fields_value.empty()) + return {}; + + std::string cached_fields; + + for (const auto& field : fields_value) + { + cached_fields += field; + cached_fields += "; "; + } + + return cached_fields; + } + +} + +helmet_options::helmet_options() +{ + // Default headers as per helmet.js defaults + this->set(x_download_options(helmet_download_type::noopen)); + this->set(x_frame_origin(helmet_origin_type::deny)); + this->set(x_xss_protection()); + this->set(x_content_type_options()); + this->set(strict_transport_security(hsts::default_age, hsts::include_subdomains, hsts::preload)); + this->set(cross_origin_opener_policy()); + this->set(cross_origin_resource_policy()); + this->set(origin_agent_cluster()); + this->set(referrer_policy()); + this->set(dns_prefetch_control()); + this->set(permitted_cross_domain_policies()); + this->set(hide_powered_by()); +} + +helmet_options::~helmet_options() = default; + +helmet_options& helmet_options::set(const pair_type& helmet_hdr) +{ + auto it_hdr = std::find_if(headers.begin(), headers.end(), [&helmet_hdr](const pair_type& pair) { + return pair.first == helmet_hdr.first; + }); + + if (it_hdr != headers.end()) + { + *it_hdr = helmet_hdr; + + return *this; + } + + headers.emplace_back(helmet_hdr); + return *this; +} + +/** + @brief private implementation details for the `helmet` middleware. +*/ +struct helmet::impl +{ + using pair_type = std::pair; + using vector_type = std::vector; + + helmet_options options_; + vector_type cached_headers_{}; +}; + +helmet:: +helmet( + helmet_options helmet_options) + : impl_(new impl()) +{ + impl_->options_ = std::move(helmet_options); + + std::for_each(impl_->options_.headers.begin(), impl_->options_.headers.end(), + [this] (const helmet_options::pair_type& hdr) + { + this->impl_->cached_headers_.emplace_back(hdr.first, + detail::setHeaderValues(hdr.second)); + }); +} + +helmet::~helmet() +{ + delete impl_; + impl_ = nullptr; +} + +helmet& helmet::operator=(helmet&& other) noexcept +{ + delete impl_; + impl_ = other.impl_; + other.impl_ = nullptr; + + return *this; +} + +helmet:: +helmet(helmet&& other) noexcept + : impl_(other.impl_) +{ + other.impl_ = nullptr; +} + +route_result +helmet:: +operator()( + route_params& p) const +{ + std::for_each(impl_->cached_headers_.begin(), impl_->cached_headers_.end(), + [&p] (const impl::pair_type& hdr) + { + // If value is empty, it means we should remove the header (e.g., X-Powered-By) + if (hdr.second.empty()) + { + p.res.erase(hdr.first); + } + else + { + p.res.set(hdr.first, hdr.second); + } + }); + + return route::next; +} + +helmet::csp_policy& helmet::csp_policy::allow(const core::string_view& allow, + const urls::url_view& source) +{ + if (!source.scheme().starts_with("http")) + detail::throw_invalid_argument(); + + if (allow.empty()) + detail::throw_invalid_argument(); + + std::string value = allow; + + value += " "; + value += source.data(); + + auto it = std::find_if(list.begin(), list.end(), [&allow](const std::string& in) { + std::string allow_with_space = std::string(allow) + " "; + return in.find(allow_with_space) != std::string::npos; + }); + + if (it == list.end()) + { + list.emplace_back(value); + } + else + { + auto& it_elem = *it; + + it_elem += " "; + it_elem += source.data(); + } + + return *this; +} + +helmet::csp_policy& helmet::csp_policy::allow(const core::string_view& allow, const csp_type& type) +{ + if (allow.empty()) + detail::throw_invalid_argument(); + + std::string value; + + switch (type) + { + case csp_type::sandbox: + { + value += " 'sandbox'"; + break; + } + case csp_type::none: + { + value += " 'none'"; + break; + } + case csp_type::unsafe_inline: + { + value += " 'unsafe-inline'"; + break; + } + case csp_type::unsafe_eval: + { + value += " 'unsafe-eval'"; + break; + } + case csp_type::strict_dynamic: + { + value += " 'strict-dynamic'"; + break; + } + case csp_type::report_sample: + { + value += " 'report-sample'"; + break; + } + case csp_type::self: + { + value += " 'self'"; + break; + } + default: + detail::throw_invalid_argument(); + return *this; + } + + auto it = std::find_if(list.begin(), list.end(), [&allow](const std::string& in) { + std::string allow_with_space = std::string(allow) + " "; + return in.find(allow_with_space) != std::string::npos; + }); + + if (it == list.end()) + { + std::string final_result = allow; + final_result += value; + + list.emplace_back(final_result); + } + else + { + auto& it_elem = *it; + it_elem.push_back(' '); + it_elem += value; + } + + return *this; +} + +helmet::csp_policy::csp_policy() = default; +helmet::csp_policy::~csp_policy() = default; + +helmet::csp_policy& helmet::csp_policy::remove(const core::string_view& allow) +{ + if (auto it = std::find_if(list.cbegin(), list.cend(), [&allow](const std::string& in) { + std::string allow_with_space = std::string(allow) + " "; + return in.find(allow_with_space) != std::string::npos; + }); it != list.cend()) + { + list.erase(it); + } + else + { + detail::throw_invalid_argument(); + } + + return *this; +} + +option_pair x_xss_protection() +{ + return {"X-XSS-Protection", {"0"}}; +} + +option_pair x_content_type_options() +{ + return {"X-Content-Type-Options", {"nosniff"}}; +} + +option_pair content_security_policy(const helmet::csp_policy& sp) +{ + if (sp.list.empty()) + detail::throw_invalid_argument(); + + return {"Content-Security-Policy", sp.list}; +} + +option_pair x_frame_origin(const helmet_origin_type& origin) +{ + if (origin == helmet_origin_type::sameorigin) + { + return {"X-Frame-Options", {"SAMEORIGIN"}}; + } + else if (origin == helmet_origin_type::deny) + { + return {"X-Frame-Options", {"DENY"}}; + } + + return {}; +} + +option_pair x_download_options(const helmet_download_type& type) +{ + if (type == helmet_download_type::noopen) + { + return {"X-Download-Options", {"noopen"}}; + } + + return {}; +} + +option_pair cross_origin_opener_policy(const coop_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case coop_policy_type::unsafe_none: + value = "unsafe-none"; + break; + case coop_policy_type::same_origin_allow_popups: + value = "same-origin-allow-popups"; + break; + case coop_policy_type::same_origin: + value = "same-origin"; + break; + default: + return {}; + } + + return {"Cross-Origin-Opener-Policy", {value}}; +} + +option_pair cross_origin_resource_policy(const corp_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case corp_policy_type::same_origin: + value = "same-origin"; + break; + case corp_policy_type::same_site: + value = "same-site"; + break; + case corp_policy_type::cross_origin: + value = "cross-origin"; + break; + default: + return {}; + } + + return {"Cross-Origin-Resource-Policy", {value}}; +} + +option_pair cross_origin_embedder_policy(const coep_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case coep_policy_type::unsafe_none: + value = "unsafe-none"; + break; + case coep_policy_type::require_corp: + value = "require-corp"; + break; + case coep_policy_type::credentialless: + value = "credentialless"; + break; + default: + return {}; + } + + return {"Cross-Origin-Embedder-Policy", {value}}; +} + +option_pair strict_transport_security(std::size_t age, bool include_domains, bool preload) +{ + std::string value = "max-age=" + std::to_string(age); + + if (include_domains) + { + value += "; includeSubDomains"; + } + + if (preload) + { + value += "; preload"; + } + + return {"Strict-Transport-Security", {value}}; +} + +option_pair referrer_policy(const referrer_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case referrer_policy_type::no_referrer: + value = "no-referrer"; + break; + case referrer_policy_type::no_referrer_when_downgrade: + value = "no-referrer-when-downgrade"; + break; + case referrer_policy_type::same_origin: + value = "same-origin"; + break; + case referrer_policy_type::origin: + value = "origin"; + break; + case referrer_policy_type::strict_origin: + value = "strict-origin"; + break; + case referrer_policy_type::origin_when_cross_origin: + value = "origin-when-cross-origin"; + break; + case referrer_policy_type::strict_origin_when_cross_origin: + value = "strict-origin-when-cross-origin"; + break; + case referrer_policy_type::unsafe_url: + value = "unsafe-url"; + break; + default: + return {}; + } + + return {"Referrer-Policy", {value}}; +} + +option_pair origin_agent_cluster() +{ + return {"Origin-Agent-Cluster", {"?1"}}; +} + +option_pair dns_prefetch_control(bool allow) +{ + return {"X-DNS-Prefetch-Control", {allow ? "on" : "off"}}; +} + +option_pair permitted_cross_domain_policies(const cross_domain_policy_type& policy) +{ + std::string value; + + switch (policy) + { + case cross_domain_policy_type::none: + value = "none"; + break; + case cross_domain_policy_type::master_only: + value = "master-only"; + break; + case cross_domain_policy_type::by_content_type: + value = "by-content-type"; + break; + case cross_domain_policy_type::by_ftp_filename: + value = "by-ftp-filename"; + break; + case cross_domain_policy_type::all: + value = "all"; + break; + default: + return {}; + } + + return {"X-Permitted-Cross-Domain-Policies", {value}}; +} + +option_pair hide_powered_by() +{ + return {"X-Powered-By", {}}; +} +} + +} diff --git a/test/unit/server/helmet.cpp b/test/unit/server/helmet.cpp new file mode 100644 index 00000000..530e4907 --- /dev/null +++ b/test/unit/server/helmet.cpp @@ -0,0 +1,300 @@ +// +// Copyright (c) 2025 Amlal El Mahrouss (amlal at nekernel dot org) +// +// 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 "test_suite.hpp" + +namespace boost { +namespace http_proto { + +struct helmet_test +{ + void + run() + { + // Test X-Download-Options + { + helmet_options opt; + + opt.set(x_download_options(helmet_download_type::noopen)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Download-Options") > 0); + } + + // Test X-Frame-Options with DENY + { + helmet_options opt; + + opt.set(x_frame_origin(helmet_origin_type::deny)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Frame-Options") > 0); + } + + // Test X-Frame-Options with SAMEORIGIN + { + helmet_options opt; + + opt.set(x_frame_origin(helmet_origin_type::sameorigin)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Frame-Options") > 0); + } + + // Test X-XSS-Protection + { + helmet_options opt; + + opt.set(x_xss_protection()); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-XSS-Protection") > 0); + } + + // Test X-Content-Type-Options + { + helmet_options opt; + + opt.set(x_content_type_options()); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Content-Type-Options") > 0); + } + + // Test Strict-Transport-Security with defaults + { + helmet_options opt; + + opt.set(strict_transport_security(hsts::default_age)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Strict-Transport-Security") > 0); + } + + // Test Strict-Transport-Security with custom options + { + helmet_options opt; + + opt.set(strict_transport_security(86400, false, false)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Strict-Transport-Security") > 0); + } + + // Test Cross-Origin-Opener-Policy + { + helmet_options opt; + + opt.set(cross_origin_opener_policy(coop_policy_type::same_origin)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Cross-Origin-Opener-Policy") > 0); + } + + // Test Cross-Origin-Resource-Policy + { + helmet_options opt; + + opt.set(cross_origin_resource_policy(corp_policy_type::same_origin)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Cross-Origin-Resource-Policy") > 0); + } + + // Test Cross-Origin-Embedder-Policy + { + helmet_options opt; + + opt.set(cross_origin_embedder_policy(coep_policy_type::require_corp)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Cross-Origin-Embedder-Policy") > 0); + } + + // Test Referrer-Policy + { + helmet_options opt; + + opt.set(referrer_policy(referrer_policy_type::no_referrer)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Referrer-Policy") > 0); + } + + // Test Origin-Agent-Cluster + { + helmet_options opt; + + opt.set(origin_agent_cluster()); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Origin-Agent-Cluster") > 0); + } + + // Test X-DNS-Prefetch-Control + { + helmet_options opt; + + opt.set(dns_prefetch_control(false)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-DNS-Prefetch-Control") > 0); + } + + // Test X-Permitted-Cross-Domain-Policies + { + helmet_options opt; + + opt.set(permitted_cross_domain_policies(cross_domain_policy_type::none)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("X-Permitted-Cross-Domain-Policies") > 0); + } + + // Test hide_powered_by + { + helmet_options opt; + + opt.set(hide_powered_by()); + + helmet helmet{opt}; + route_params p; + + // Add X-Powered-By header first + p.res.set("X-Powered-By", "Express"); + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + // After helmet middleware, X-Powered-By should be removed + BOOST_TEST(p.res.count("X-Powered-By") == 0); + } + + // Test Content-Security-Policy with CSP builder + { + helmet_options opt; + helmet::csp_policy csp; + + csp.allow("default-src", csp_type::self) + .allow("script-src", csp_type::self) + .allow("style-src", csp_type::self); + + opt.set(content_security_policy(csp)); + + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + BOOST_TEST(p.res.count("Content-Security-Policy") > 0); + } + + // Test default helmet configuration + { + helmet_options opt; + helmet helmet{opt}; + route_params p; + + auto ec = helmet(p); + + BOOST_TEST(ec == route::next); + // Check that default headers are set + BOOST_TEST(p.res.count("X-Download-Options") > 0); + BOOST_TEST(p.res.count("X-Frame-Options") > 0); + BOOST_TEST(p.res.count("X-XSS-Protection") > 0); + BOOST_TEST(p.res.count("X-Content-Type-Options") > 0); + BOOST_TEST(p.res.count("Strict-Transport-Security") > 0); + BOOST_TEST(p.res.count("Cross-Origin-Opener-Policy") > 0); + BOOST_TEST(p.res.count("Cross-Origin-Resource-Policy") > 0); + BOOST_TEST(p.res.count("Origin-Agent-Cluster") > 0); + BOOST_TEST(p.res.count("Referrer-Policy") > 0); + BOOST_TEST(p.res.count("X-DNS-Prefetch-Control") > 0); + BOOST_TEST(p.res.count("X-Permitted-Cross-Domain-Policies") > 0); + } + } +}; + +TEST_SUITE( + helmet_test, + "boost.http_proto.server.helmet"); +} + +}