summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--http/http2_connection.hpp28
-rw-r--r--http/http_response.hpp6
-rw-r--r--include/security_headers.hpp5
-rw-r--r--include/webassets.hpp66
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))
{