xref: /openbmc/bmcweb/http/http_response.hpp (revision 06a63974)
1 #pragma once
2 #include "logging.hpp"
3 #include "utils/hex_utils.hpp"
4 
5 #include <boost/beast/http/file_body.hpp>
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 
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<http::file_body>;
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         response.emplace<string_response>(result(), 11, std::move(bodyPart));
266     }
267 
268     void end()
269     {
270         std::string etag = computeEtag();
271         if (!etag.empty())
272         {
273             addHeader(http::field::etag, etag);
274         }
275         if (completed)
276         {
277             BMCWEB_LOG_ERROR("{} Response was ended twice", logPtr(this));
278             return;
279         }
280         completed = true;
281         BMCWEB_LOG_DEBUG("{} calling completion handler", logPtr(this));
282         if (completeRequestHandler)
283         {
284             BMCWEB_LOG_DEBUG("{} completion handler was valid", logPtr(this));
285             completeRequestHandler(*this);
286         }
287     }
288 
289     bool isAlive() const
290     {
291         return isAliveHelper && isAliveHelper();
292     }
293 
294     void setCompleteRequestHandler(std::function<void(Response&)>&& handler)
295     {
296         BMCWEB_LOG_DEBUG("{} setting completion handler", logPtr(this));
297         completeRequestHandler = std::move(handler);
298 
299         // Now that we have a new completion handler attached, we're no longer
300         // complete
301         completed = false;
302     }
303 
304     std::function<void(Response&)> releaseCompleteRequestHandler()
305     {
306         BMCWEB_LOG_DEBUG("{} releasing completion handler{}", logPtr(this),
307                          static_cast<bool>(completeRequestHandler));
308         std::function<void(Response&)> ret = completeRequestHandler;
309         completeRequestHandler = nullptr;
310         completed = true;
311         return ret;
312     }
313 
314     void setIsAliveHelper(std::function<bool()>&& handler)
315     {
316         isAliveHelper = std::move(handler);
317     }
318 
319     std::function<bool()> releaseIsAliveHelper()
320     {
321         std::function<bool()> ret = std::move(isAliveHelper);
322         isAliveHelper = nullptr;
323         return ret;
324     }
325 
326     void setHashAndHandleNotModified()
327     {
328         // Can only hash if we have content that's valid
329         if (jsonValue.empty() || result() != http::status::ok)
330         {
331             return;
332         }
333         size_t hashval = std::hash<nlohmann::json>{}(jsonValue);
334         std::string hexVal = "\"" + intToHexString(hashval, 8) + "\"";
335         addHeader(http::field::etag, hexVal);
336         if (expectedHash && hexVal == *expectedHash)
337         {
338             jsonValue = nullptr;
339             result(http::status::not_modified);
340         }
341     }
342 
343     void setExpectedHash(std::string_view hash)
344     {
345         expectedHash = hash;
346     }
347 
348     using message_generator = http::message_generator;
349     message_generator generator()
350     {
351         return boost::variant2::visit(
352             [](auto& r) -> message_generator { return std::move(r); },
353             response);
354     }
355 
356     bool openFile(const std::filesystem::path& path)
357     {
358         http::file_body::value_type file;
359         boost::beast::error_code ec;
360         file.open(path.c_str(), boost::beast::file_mode::read, ec);
361         if (ec)
362         {
363             return false;
364         }
365         // store the headers on stack temporarily so we can reconstruct the new
366         // base with the old headers copied in.
367         http::header<false> headTemp = std::move(fields());
368         file_response& fileResponse =
369             response.emplace<file_response>(std::move(headTemp));
370         fileResponse.body() = std::move(file);
371         return true;
372     }
373 
374   private:
375     std::optional<std::string> expectedHash;
376     bool completed = false;
377     std::function<void(Response&)> completeRequestHandler;
378     std::function<bool()> isAliveHelper;
379 };
380 } // namespace crow
381