diff options
-rw-r--r-- | http/http2_connection.hpp | 28 | ||||
-rw-r--r-- | http/http_response.hpp | 6 | ||||
-rw-r--r-- | include/security_headers.hpp | 5 | ||||
-rw-r--r-- | include/webassets.hpp | 66 |
4 files changed, 90 insertions, 15 deletions
diff --git a/http/http2_connection.hpp b/http/http2_connection.hpp index 4b2d186f07..2c60c1268e 100644 --- a/http/http2_connection.hpp +++ b/http/http2_connection.hpp @@ -167,17 +167,18 @@ class HTTP2Connection : close(); return -1; } - Response& thisRes = it->second.res; - thisRes = std::move(completedRes); - crow::Request& thisReq = it->second.req; - std::vector<nghttp2_nv> hdr; + Http2StreamData& stream = it->second; + Response& res = stream.res; + res = std::move(completedRes); + crow::Request& thisReq = stream.req; - completeResponseFields(thisReq, thisRes); - thisRes.addHeader(boost::beast::http::field::date, getCachedDateStr()); - thisRes.preparePayload(); + completeResponseFields(thisReq, res); + res.addHeader(boost::beast::http::field::date, getCachedDateStr()); + res.preparePayload(); - boost::beast::http::fields& fields = thisRes.fields(); - std::string code = std::to_string(thisRes.resultInt()); + boost::beast::http::fields& fields = res.fields(); + std::string code = std::to_string(res.resultInt()); + std::vector<nghttp2_nv> hdr; hdr.emplace_back( headerFromStringViews(":status", code, NGHTTP2_NV_FLAG_NONE)); for (const boost::beast::http::fields::value_type& header : fields) @@ -185,8 +186,6 @@ class HTTP2Connection : hdr.emplace_back(headerFromStringViews( header.name_string(), header.value(), NGHTTP2_NV_FLAG_NONE)); } - Http2StreamData& stream = it->second; - crow::Response& res = stream.res; http::response<bmcweb::HttpBody>& fbody = res.response; stream.writer.emplace(fbody.base(), fbody.body()); @@ -279,6 +278,13 @@ class HTTP2Connection : else #endif // BMCWEB_INSECURE_DISABLE_AUTHX { + std::string_view expected = thisReq.getHeaderValue( + boost::beast::http::field::if_none_match); + BMCWEB_LOG_DEBUG("Setting expected hash {}", expected); + if (!expected.empty()) + { + asyncResp->res.setExpectedHash(expected); + } handler->handle(thisReq, asyncResp); } return 0; diff --git a/http/http_response.hpp b/http/http_response.hpp index c93d60d685..18266ec93d 100644 --- a/http/http_response.hpp +++ b/http/http_response.hpp @@ -80,6 +80,7 @@ struct Response } response = std::move(r.response); jsonValue = std::move(r.jsonValue); + expectedHash = std::move(r.expectedHash); // Only need to move completion handler if not already completed // Note, there are cases where we might move out of a Response object @@ -144,6 +145,11 @@ struct Response return fields()[key]; } + std::string_view getHeaderValue(boost::beast::http::field key) const + { + return fields()[key]; + } + void keepAlive(bool k) { response.keep_alive(k); diff --git a/include/security_headers.hpp b/include/security_headers.hpp index c0855f439d..2a2eb40d7d 100644 --- a/include/security_headers.hpp +++ b/include/security_headers.hpp @@ -16,8 +16,11 @@ inline void addSecurityHeaders(const crow::Request& req [[maybe_unused]], "includeSubdomains"); res.addHeader(bf::pragma, "no-cache"); - res.addHeader(bf::cache_control, "no-store, max-age=0"); + if (res.getHeaderValue(bf::cache_control).empty()) + { + res.addHeader(bf::cache_control, "no-store, max-age=0"); + } res.addHeader("X-Content-Type-Options", "nosniff"); std::string_view contentType = res.getHeaderValue("Content-Type"); diff --git a/include/webassets.hpp b/include/webassets.hpp index c5c7228ede..4bcc8cb69b 100644 --- a/include/webassets.hpp +++ b/include/webassets.hpp @@ -25,6 +25,37 @@ struct CmpStr } }; +inline std::string getStaticEtag(const std::filesystem::path& webpath) +{ + // webpack outputs production chunks in the form: + // <filename>.<hash>.<extension> + // For example app.63e2c453.css + // Try to detect this, so we can use the hash as the ETAG + std::vector<std::string> split; + bmcweb::split(split, webpath.filename().string(), '.'); + BMCWEB_LOG_DEBUG("Checking {} split.size() {}", webpath.filename().string(), + split.size()); + if (split.size() < 3) + { + return ""; + } + + // get the second to last element + std::string hash = split.rbegin()[1]; + + // Webpack hashes are 8 characters long + if (hash.size() != 8) + { + return ""; + } + // Webpack hashes only include hex printable characters + if (hash.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) + { + return ""; + } + return std::format("\"{}\"", hash); +} + inline void requestRoutes(App& app) { constexpr static std::array<std::pair<const char*, const char*>, 17> @@ -99,6 +130,9 @@ inline void requestRoutes(App& app) contentEncoding = "gzip"; } + std::string etag = getStaticEtag(webpath); + + bool renamed = false; if (webpath.filename().string().starts_with("index.")) { webpath = webpath.parent_path(); @@ -107,6 +141,7 @@ inline void requestRoutes(App& app) // insert the non-directory version of this path webroutes::routes.insert(webpath); webpath += "/"; + renamed = true; } } @@ -147,9 +182,9 @@ inline void requestRoutes(App& app) } app.routeDynamic(webpath)( - [absolutePath, contentType, contentEncoding]( - const crow::Request&, - const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { + [absolutePath, contentType, contentEncoding, etag, + renamed](const crow::Request& req, + const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { if (contentType != nullptr) { asyncResp->res.addHeader( @@ -163,6 +198,31 @@ inline void requestRoutes(App& app) contentEncoding); } + if (!etag.empty()) + { + asyncResp->res.addHeader(boost::beast::http::field::etag, + etag); + // Don't cache paths that don't have the etag in them, like + // index, which gets transformed to / + if (!renamed) + { + // Anything with a hash can be cached forever and is + // immutable + asyncResp->res.addHeader( + boost::beast::http::field::cache_control, + "max-age=31556926, immutable"); + } + + std::string_view cachedEtag = req.getHeaderValue( + boost::beast::http::field::if_none_match); + if (cachedEtag == etag) + { + asyncResp->res.result( + boost::beast::http::status::not_modified); + return; + } + } + // res.set_header("Cache-Control", "public, max-age=86400"); if (!asyncResp->res.openFile(absolutePath)) { |