| #pragma once |
| #include "logging.hpp" |
| #include "utils/hex_utils.hpp" |
| |
| #include <boost/beast/http/file_body.hpp> |
| #include <boost/beast/http/message.hpp> |
| #include <boost/beast/http/message_generator.hpp> |
| #include <boost/beast/http/string_body.hpp> |
| #include <boost/variant2/variant.hpp> |
| #include <nlohmann/json.hpp> |
| |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| namespace crow |
| { |
| |
| template <typename Adaptor, typename Handler> |
| class Connection; |
| |
| namespace http = boost::beast::http; |
| |
| struct Response |
| { |
| template <typename Adaptor, typename Handler> |
| friend class crow::Connection; |
| |
| using string_response = http::response<http::string_body>; |
| using file_response = http::response<http::file_body>; |
| |
| // Use boost variant2 because it doesn't have valueless by exception |
| boost::variant2::variant<string_response, file_response> response; |
| |
| nlohmann::json jsonValue; |
| using fields_type = http::header<false, http::fields>; |
| fields_type& fields() |
| { |
| return boost::variant2::visit( |
| [](auto&& r) -> fields_type& { return r.base(); }, response); |
| } |
| |
| const fields_type& fields() const |
| { |
| return boost::variant2::visit( |
| [](auto&& r) -> const fields_type& { return r.base(); }, response); |
| } |
| |
| void addHeader(std::string_view key, std::string_view value) |
| { |
| fields().insert(key, value); |
| } |
| |
| void addHeader(http::field key, std::string_view value) |
| { |
| fields().insert(key, value); |
| } |
| |
| void clearHeader(http::field key) |
| { |
| fields().erase(key); |
| } |
| |
| Response() : response(string_response()) {} |
| Response(Response&& res) noexcept : |
| response(std::move(res.response)), jsonValue(std::move(res.jsonValue)), |
| completed(res.completed) |
| { |
| // See note in operator= move handler for why this is needed. |
| if (!res.completed) |
| { |
| completeRequestHandler = std::move(res.completeRequestHandler); |
| res.completeRequestHandler = nullptr; |
| } |
| isAliveHelper = res.isAliveHelper; |
| res.isAliveHelper = nullptr; |
| } |
| |
| ~Response() = default; |
| |
| Response(const Response&) = delete; |
| Response& operator=(const Response& r) = delete; |
| |
| Response& operator=(Response&& r) noexcept |
| { |
| BMCWEB_LOG_DEBUG("Moving response containers; this: {}; other: {}", |
| logPtr(this), logPtr(&r)); |
| if (this == &r) |
| { |
| return *this; |
| } |
| response = std::move(r.response); |
| jsonValue = std::move(r.jsonValue); |
| |
| // Only need to move completion handler if not already completed |
| // Note, there are cases where we might move out of a Response object |
| // while in a completion handler for that response object. This check |
| // is intended to prevent destructing the functor we are currently |
| // executing from in that case. |
| if (!r.completed) |
| { |
| completeRequestHandler = std::move(r.completeRequestHandler); |
| r.completeRequestHandler = nullptr; |
| } |
| else |
| { |
| completeRequestHandler = nullptr; |
| } |
| completed = r.completed; |
| isAliveHelper = std::move(r.isAliveHelper); |
| r.isAliveHelper = nullptr; |
| return *this; |
| } |
| |
| void result(unsigned v) |
| { |
| fields().result(v); |
| } |
| |
| void result(http::status v) |
| { |
| fields().result(v); |
| } |
| |
| void copyBody(const Response& res) |
| { |
| const string_response* s = |
| boost::variant2::get_if<string_response>(&(res.response)); |
| if (s == nullptr) |
| { |
| BMCWEB_LOG_ERROR("Unable to copy a file"); |
| return; |
| } |
| string_response* myString = |
| boost::variant2::get_if<string_response>(&response); |
| if (myString == nullptr) |
| { |
| myString = &response.emplace<string_response>(); |
| } |
| myString->body() = s->body(); |
| } |
| |
| http::status result() const |
| { |
| return fields().result(); |
| } |
| |
| unsigned resultInt() const |
| { |
| return fields().result_int(); |
| } |
| |
| std::string_view reason() const |
| { |
| return fields().reason(); |
| } |
| |
| bool isCompleted() const noexcept |
| { |
| return completed; |
| } |
| |
| const std::string* body() |
| { |
| string_response* body = |
| boost::variant2::get_if<string_response>(&response); |
| if (body == nullptr) |
| { |
| return nullptr; |
| } |
| return &body->body(); |
| } |
| |
| std::string_view getHeaderValue(std::string_view key) const |
| { |
| return fields()[key]; |
| } |
| |
| void keepAlive(bool k) |
| { |
| return boost::variant2::visit([k](auto&& r) { r.keep_alive(k); }, |
| response); |
| } |
| |
| bool keepAlive() const |
| { |
| return boost::variant2::visit([](auto&& r) { return r.keep_alive(); }, |
| response); |
| } |
| |
| uint64_t getContentLength(boost::optional<uint64_t> pSize) |
| { |
| // This code is a throw-free equivalent to |
| // beast::http::message::prepare_payload |
| using http::status; |
| using http::status_class; |
| using http::to_status_class; |
| if (!pSize) |
| { |
| return 0; |
| } |
| bool is1XXReturn = to_status_class(result()) == |
| status_class::informational; |
| if (*pSize > 0 && (is1XXReturn || result() == status::no_content || |
| result() == status::not_modified)) |
| { |
| BMCWEB_LOG_CRITICAL("{} Response content provided but code was " |
| "no-content or not_modified, which aren't " |
| "allowed to have a body", |
| logPtr(this)); |
| return 0; |
| } |
| return *pSize; |
| } |
| |
| uint64_t size() |
| { |
| return boost::variant2::visit( |
| [](auto&& res) -> uint64_t { return res.body().size(); }, response); |
| } |
| |
| void preparePayload() |
| { |
| boost::variant2::visit( |
| [this](auto&& r) { |
| r.content_length(getContentLength(r.payload_size())); |
| }, |
| response); |
| } |
| |
| void clear() |
| { |
| BMCWEB_LOG_DEBUG("{} Clearing response containers", logPtr(this)); |
| response.emplace<string_response>(); |
| jsonValue = nullptr; |
| completed = false; |
| expectedHash = std::nullopt; |
| } |
| |
| std::string computeEtag() const |
| { |
| // Only set etag if this request succeeded |
| if (result() != http::status::ok) |
| { |
| return ""; |
| } |
| // and the json response isn't empty |
| if (jsonValue.empty()) |
| { |
| return ""; |
| } |
| size_t hashval = std::hash<nlohmann::json>{}(jsonValue); |
| return "\"" + intToHexString(hashval, 8) + "\""; |
| } |
| |
| void write(std::string&& bodyPart) |
| { |
| string_response* str = |
| boost::variant2::get_if<string_response>(&response); |
| if (str != nullptr) |
| { |
| str->body() += bodyPart; |
| return; |
| } |
| http::header<false> headTemp = std::move(fields()); |
| string_response& stringResponse = |
| response.emplace<string_response>(std::move(headTemp)); |
| stringResponse.body() = std::move(bodyPart); |
| } |
| |
| void end() |
| { |
| std::string etag = computeEtag(); |
| if (!etag.empty()) |
| { |
| addHeader(http::field::etag, etag); |
| } |
| if (completed) |
| { |
| BMCWEB_LOG_ERROR("{} Response was ended twice", logPtr(this)); |
| return; |
| } |
| completed = true; |
| BMCWEB_LOG_DEBUG("{} calling completion handler", logPtr(this)); |
| if (completeRequestHandler) |
| { |
| BMCWEB_LOG_DEBUG("{} completion handler was valid", logPtr(this)); |
| completeRequestHandler(*this); |
| } |
| } |
| |
| bool isAlive() const |
| { |
| return isAliveHelper && isAliveHelper(); |
| } |
| |
| void setCompleteRequestHandler(std::function<void(Response&)>&& handler) |
| { |
| BMCWEB_LOG_DEBUG("{} setting completion handler", logPtr(this)); |
| completeRequestHandler = std::move(handler); |
| |
| // Now that we have a new completion handler attached, we're no longer |
| // complete |
| completed = false; |
| } |
| |
| std::function<void(Response&)> releaseCompleteRequestHandler() |
| { |
| BMCWEB_LOG_DEBUG("{} releasing completion handler{}", logPtr(this), |
| static_cast<bool>(completeRequestHandler)); |
| std::function<void(Response&)> ret = completeRequestHandler; |
| completeRequestHandler = nullptr; |
| completed = true; |
| return ret; |
| } |
| |
| void setIsAliveHelper(std::function<bool()>&& handler) |
| { |
| isAliveHelper = std::move(handler); |
| } |
| |
| std::function<bool()> releaseIsAliveHelper() |
| { |
| std::function<bool()> ret = std::move(isAliveHelper); |
| isAliveHelper = nullptr; |
| return ret; |
| } |
| |
| void setHashAndHandleNotModified() |
| { |
| // Can only hash if we have content that's valid |
| if (jsonValue.empty() || result() != http::status::ok) |
| { |
| return; |
| } |
| size_t hashval = std::hash<nlohmann::json>{}(jsonValue); |
| std::string hexVal = "\"" + intToHexString(hashval, 8) + "\""; |
| addHeader(http::field::etag, hexVal); |
| if (expectedHash && hexVal == *expectedHash) |
| { |
| jsonValue = nullptr; |
| result(http::status::not_modified); |
| } |
| } |
| |
| void setExpectedHash(std::string_view hash) |
| { |
| expectedHash = hash; |
| } |
| |
| using message_generator = http::message_generator; |
| message_generator generator() |
| { |
| return boost::variant2::visit( |
| [](auto& r) -> message_generator { return std::move(r); }, |
| response); |
| } |
| |
| bool openFile(const std::filesystem::path& path) |
| { |
| http::file_body::value_type file; |
| boost::beast::error_code ec; |
| file.open(path.c_str(), boost::beast::file_mode::read, ec); |
| if (ec) |
| { |
| return false; |
| } |
| // store the headers on stack temporarily so we can reconstruct the new |
| // base with the old headers copied in. |
| http::header<false> headTemp = std::move(fields()); |
| file_response& fileResponse = |
| response.emplace<file_response>(std::move(headTemp)); |
| fileResponse.body() = std::move(file); |
| return true; |
| } |
| |
| private: |
| std::optional<std::string> expectedHash; |
| bool completed = false; |
| std::function<void(Response&)> completeRequestHandler; |
| std::function<bool()> isAliveHelper; |
| }; |
| } // namespace crow |