xref: /openbmc/bmcweb/http/http_response.hpp (revision 2ae81db9)
1 #pragma once
2 #include "http_file_body.hpp"
3 #include "logging.hpp"
4 #include "utils/hex_utils.hpp"
5 
6 #include <boost/beast/http/message.hpp>
7 #include <boost/beast/http/message_generator.hpp>
8 #include <boost/beast/http/string_body.hpp>
9 #include <boost/variant2/variant.hpp>
10 #include <nlohmann/json.hpp>
11 
12 #include <optional>
13 #include <string>
14 #include <string_view>
15 #include <utility>
16 namespace crow
17 {
18 
19 template <typename Adaptor, typename Handler>
20 class Connection;
21 
22 namespace http = boost::beast::http;
23 
24 struct Response
25 {
26     template <typename Adaptor, typename Handler>
27     friend class crow::Connection;
28 
29     using string_response = http::response<http::string_body>;
30     using file_response = http::response<bmcweb::FileBody>;
31 
32     // Use boost variant2 because it doesn't have valueless by exception
33     boost::variant2::variant<string_response, file_response> response;
34 
35     nlohmann::json jsonValue;
36     using fields_type = http::header<false, http::fields>;
37     fields_type& fields()
38     {
39         return boost::variant2::visit(
40             [](auto&& r) -> fields_type& { return r.base(); }, response);
41     }
42 
43     const fields_type& fields() const
44     {
45         return boost::variant2::visit(
46             [](auto&& r) -> const fields_type& { return r.base(); }, response);
47     }
48 
49     void addHeader(std::string_view key, std::string_view value)
50     {
51         fields().insert(key, value);
52     }
53 
54     void addHeader(http::field key, std::string_view value)
55     {
56         fields().insert(key, value);
57     }
58 
59     void clearHeader(http::field key)
60     {
61         fields().erase(key);
62     }
63 
64     Response() : response(string_response()) {}
65     Response(Response&& res) noexcept :
66         response(std::move(res.response)), jsonValue(std::move(res.jsonValue)),
67         completed(res.completed)
68     {
69         // See note in operator= move handler for why this is needed.
70         if (!res.completed)
71         {
72             completeRequestHandler = std::move(res.completeRequestHandler);
73             res.completeRequestHandler = nullptr;
74         }
75         isAliveHelper = res.isAliveHelper;
76         res.isAliveHelper = nullptr;
77     }
78 
79     ~Response() = default;
80 
81     Response(const Response&) = delete;
82     Response& operator=(const Response& r) = delete;
83 
84     Response& operator=(Response&& r) noexcept
85     {
86         BMCWEB_LOG_DEBUG("Moving response containers; this: {}; other: {}",
87                          logPtr(this), logPtr(&r));
88         if (this == &r)
89         {
90             return *this;
91         }
92         response = std::move(r.response);
93         jsonValue = std::move(r.jsonValue);
94 
95         // Only need to move completion handler if not already completed
96         // Note, there are cases where we might move out of a Response object
97         // while in a completion handler for that response object.  This check
98         // is intended to prevent destructing the functor we are currently
99         // executing from in that case.
100         if (!r.completed)
101         {
102             completeRequestHandler = std::move(r.completeRequestHandler);
103             r.completeRequestHandler = nullptr;
104         }
105         else
106         {
107             completeRequestHandler = nullptr;
108         }
109         completed = r.completed;
110         isAliveHelper = std::move(r.isAliveHelper);
111         r.isAliveHelper = nullptr;
112         return *this;
113     }
114 
115     void result(unsigned v)
116     {
117         fields().result(v);
118     }
119 
120     void result(http::status v)
121     {
122         fields().result(v);
123     }
124 
125     void copyBody(const Response& res)
126     {
127         const string_response* s =
128             boost::variant2::get_if<string_response>(&(res.response));
129         if (s == nullptr)
130         {
131             BMCWEB_LOG_ERROR("Unable to copy a file");
132             return;
133         }
134         string_response* myString =
135             boost::variant2::get_if<string_response>(&response);
136         if (myString == nullptr)
137         {
138             myString = &response.emplace<string_response>();
139         }
140         myString->body() = s->body();
141     }
142 
143     http::status result() const
144     {
145         return fields().result();
146     }
147 
148     unsigned resultInt() const
149     {
150         return fields().result_int();
151     }
152 
153     std::string_view reason() const
154     {
155         return fields().reason();
156     }
157 
158     bool isCompleted() const noexcept
159     {
160         return completed;
161     }
162 
163     const std::string* body()
164     {
165         string_response* body =
166             boost::variant2::get_if<string_response>(&response);
167         if (body == nullptr)
168         {
169             return nullptr;
170         }
171         return &body->body();
172     }
173 
174     std::string_view getHeaderValue(std::string_view key) const
175     {
176         return fields()[key];
177     }
178 
179     void keepAlive(bool k)
180     {
181         return boost::variant2::visit([k](auto&& r) { r.keep_alive(k); },
182                                       response);
183     }
184 
185     bool keepAlive() const
186     {
187         return boost::variant2::visit([](auto&& r) { return r.keep_alive(); },
188                                       response);
189     }
190 
191     uint64_t getContentLength(boost::optional<uint64_t> pSize)
192     {
193         // This code is a throw-free equivalent to
194         // beast::http::message::prepare_payload
195         using http::status;
196         using http::status_class;
197         using http::to_status_class;
198         if (!pSize)
199         {
200             return 0;
201         }
202         bool is1XXReturn = to_status_class(result()) ==
203                            status_class::informational;
204         if (*pSize > 0 && (is1XXReturn || result() == status::no_content ||
205                            result() == status::not_modified))
206         {
207             BMCWEB_LOG_CRITICAL("{} Response content provided but code was "
208                                 "no-content or not_modified, which aren't "
209                                 "allowed to have a body",
210                                 logPtr(this));
211             return 0;
212         }
213         return *pSize;
214     }
215 
216     uint64_t size()
217     {
218         return boost::variant2::visit(
219             [](auto&& res) -> uint64_t { return res.body().size(); }, response);
220     }
221 
222     void preparePayload()
223     {
224         boost::variant2::visit(
225             [this](auto&& r) {
226             r.content_length(getContentLength(r.payload_size()));
227         },
228             response);
229     }
230 
231     void clear()
232     {
233         BMCWEB_LOG_DEBUG("{} Clearing response containers", logPtr(this));
234         response.emplace<string_response>();
235         jsonValue = nullptr;
236         completed = false;
237         expectedHash = std::nullopt;
238     }
239 
240     std::string computeEtag() const
241     {
242         // Only set etag if this request succeeded
243         if (result() != http::status::ok)
244         {
245             return "";
246         }
247         // and the json response isn't empty
248         if (jsonValue.empty())
249         {
250             return "";
251         }
252         size_t hashval = std::hash<nlohmann::json>{}(jsonValue);
253         return "\"" + intToHexString(hashval, 8) + "\"";
254     }
255 
256     void write(std::string&& bodyPart)
257     {
258         string_response* str =
259             boost::variant2::get_if<string_response>(&response);
260         if (str != nullptr)
261         {
262             str->body() += bodyPart;
263             return;
264         }
265         http::header<false> headTemp = std::move(fields());
266         string_response& stringResponse =
267             response.emplace<string_response>(std::move(headTemp));
268         stringResponse.body() = std::move(bodyPart);
269     }
270 
271     void end()
272     {
273         std::string etag = computeEtag();
274         if (!etag.empty())
275         {
276             addHeader(http::field::etag, etag);
277         }
278         if (completed)
279         {
280             BMCWEB_LOG_ERROR("{} Response was ended twice", logPtr(this));
281             return;
282         }
283         completed = true;
284         BMCWEB_LOG_DEBUG("{} calling completion handler", logPtr(this));
285         if (completeRequestHandler)
286         {
287             BMCWEB_LOG_DEBUG("{} completion handler was valid", logPtr(this));
288             completeRequestHandler(*this);
289         }
290     }
291 
292     bool isAlive() const
293     {
294         return isAliveHelper && isAliveHelper();
295     }
296 
297     void setCompleteRequestHandler(std::function<void(Response&)>&& handler)
298     {
299         BMCWEB_LOG_DEBUG("{} setting completion handler", logPtr(this));
300         completeRequestHandler = std::move(handler);
301 
302         // Now that we have a new completion handler attached, we're no longer
303         // complete
304         completed = false;
305     }
306 
307     std::function<void(Response&)> releaseCompleteRequestHandler()
308     {
309         BMCWEB_LOG_DEBUG("{} releasing completion handler{}", logPtr(this),
310                          static_cast<bool>(completeRequestHandler));
311         std::function<void(Response&)> ret = completeRequestHandler;
312         completeRequestHandler = nullptr;
313         completed = true;
314         return ret;
315     }
316 
317     void setIsAliveHelper(std::function<bool()>&& handler)
318     {
319         isAliveHelper = std::move(handler);
320     }
321 
322     std::function<bool()> releaseIsAliveHelper()
323     {
324         std::function<bool()> ret = std::move(isAliveHelper);
325         isAliveHelper = nullptr;
326         return ret;
327     }
328 
329     void setHashAndHandleNotModified()
330     {
331         // Can only hash if we have content that's valid
332         if (jsonValue.empty() || result() != http::status::ok)
333         {
334             return;
335         }
336         size_t hashval = std::hash<nlohmann::json>{}(jsonValue);
337         std::string hexVal = "\"" + intToHexString(hashval, 8) + "\"";
338         addHeader(http::field::etag, hexVal);
339         if (expectedHash && hexVal == *expectedHash)
340         {
341             jsonValue = nullptr;
342             result(http::status::not_modified);
343         }
344     }
345 
346     void setExpectedHash(std::string_view hash)
347     {
348         expectedHash = hash;
349     }
350 
351     using message_generator = http::message_generator;
352     message_generator generator()
353     {
354         return boost::variant2::visit(
355             [](auto& r) -> message_generator { return std::move(r); },
356             response);
357     }
358 
359     bool openFile(const std::filesystem::path& path,
360                   bmcweb::EncodingType enc = bmcweb::EncodingType::Raw)
361     {
362         file_response::body_type::value_type body(enc);
363         boost::beast::error_code ec;
364         body.open(path.c_str(), boost::beast::file_mode::read, ec);
365         if (ec)
366         {
367             return false;
368         }
369         updateFileBody(std::move(body));
370         return true;
371     }
372 
373     bool openFd(int fd, bmcweb::EncodingType enc = bmcweb::EncodingType::Raw)
374     {
375         file_response::body_type::value_type body(enc);
376         boost::beast::error_code ec;
377         body.setFd(fd, ec);
378         if (ec)
379         {
380             BMCWEB_LOG_ERROR("Failed to set fd");
381             return false;
382         }
383         updateFileBody(std::move(body));
384         return true;
385     }
386 
387   private:
388     void updateFileBody(file_response::body_type::value_type file)
389     {
390         // store the headers on stack temporarily so we can reconstruct the new
391         // base with the old headers copied in.
392         http::header<false> headTemp = std::move(fields());
393         file_response& fileResponse =
394             response.emplace<file_response>(std::move(headTemp));
395         fileResponse.body() = std::move(file);
396     }
397 
398     std::optional<std::string> expectedHash;
399     bool completed = false;
400     std::function<void(Response&)> completeRequestHandler;
401     std::function<bool()> isAliveHelper;
402 };
403 } // namespace crow
404