xref: /openbmc/bmcweb/http/http_body.hpp (revision cdf25ffb6b2d99c829094c9a4c4907aec46e3a2e)
1 #pragma once
2 
3 #include "duplicatable_file_handle.hpp"
4 #include "logging.hpp"
5 #include "utility.hpp"
6 
7 #include <fcntl.h>
8 #include <unistd.h>
9 
10 #include <boost/beast/core/buffers_range.hpp>
11 #include <boost/beast/core/file_posix.hpp>
12 #include <boost/beast/http/message.hpp>
13 #include <boost/system/error_code.hpp>
14 
15 #include <cstdint>
16 #include <optional>
17 #include <string_view>
18 
19 namespace bmcweb
20 {
21 struct HttpBody
22 {
23     // Body concept requires specific naming of classes
24     // NOLINTBEGIN(readability-identifier-naming)
25     class writer;
26     class reader;
27     class value_type;
28     // NOLINTEND(readability-identifier-naming)
29 
30     static std::uint64_t size(const value_type& body);
31 };
32 
33 enum class EncodingType
34 {
35     Raw,
36     Base64,
37 };
38 
39 class HttpBody::value_type
40 {
41     DuplicatableFileHandle fileHandle;
42     std::optional<size_t> fileSize;
43     std::string strBody;
44 
45   public:
46     value_type() = default;
47     explicit value_type(std::string_view s) : strBody(s) {}
48     explicit value_type(EncodingType e) : encodingType(e) {}
49     EncodingType encodingType = EncodingType::Raw;
50 
51     const boost::beast::file_posix& file() const
52     {
53         return fileHandle.fileHandle;
54     }
55 
56     std::string& str()
57     {
58         return strBody;
59     }
60 
61     const std::string& str() const
62     {
63         return strBody;
64     }
65 
66     std::optional<size_t> payloadSize() const
67     {
68         if (!fileHandle.fileHandle.is_open())
69         {
70             return strBody.size();
71         }
72         if (fileSize)
73         {
74             if (encodingType == EncodingType::Base64)
75             {
76                 return crow::utility::Base64Encoder::encodedSize(*fileSize);
77             }
78         }
79         return fileSize;
80     }
81 
82     void clear()
83     {
84         strBody.clear();
85         strBody.shrink_to_fit();
86         fileHandle.fileHandle = boost::beast::file_posix();
87         fileSize = std::nullopt;
88         encodingType = EncodingType::Raw;
89     }
90 
91     void open(const char* path, boost::beast::file_mode mode,
92               boost::system::error_code& ec)
93     {
94         fileHandle.fileHandle.open(path, mode, ec);
95         if (ec)
96         {
97             return;
98         }
99         boost::system::error_code ec2;
100         uint64_t size = fileHandle.fileHandle.size(ec2);
101         if (!ec2)
102         {
103             BMCWEB_LOG_INFO("File size was {} bytes", size);
104             fileSize = static_cast<size_t>(size);
105         }
106         else
107         {
108             BMCWEB_LOG_WARNING("Failed to read file size on {}", path);
109         }
110 
111         int fadvise = posix_fadvise(fileHandle.fileHandle.native_handle(), 0, 0,
112                                     POSIX_FADV_SEQUENTIAL);
113         if (fadvise != 0)
114         {
115             BMCWEB_LOG_WARNING("Fasvise returned {} ignoring", fadvise);
116         }
117         ec = {};
118     }
119 
120     void setFd(int fd, boost::system::error_code& ec)
121     {
122         fileHandle.fileHandle.native_handle(fd);
123 
124         boost::system::error_code ec2;
125         uint64_t size = fileHandle.fileHandle.size(ec2);
126         if (!ec2)
127         {
128             if (size != 0 && size < std::numeric_limits<size_t>::max())
129             {
130                 fileSize = static_cast<size_t>(size);
131             }
132         }
133         ec = {};
134     }
135 };
136 
137 class HttpBody::writer
138 {
139   public:
140     using const_buffers_type = boost::asio::const_buffer;
141 
142   private:
143     std::string buf;
144     crow::utility::Base64Encoder encoder;
145 
146     value_type& body;
147     size_t sent = 0;
148     // 64KB This number is arbitrary, and selected to try to optimize for larger
149     // files and fewer loops over per-connection reduction in memory usage.
150     // Nginx uses 16-32KB here, so we're in the range of what other webservers
151     // do.
152     constexpr static size_t readBufSize = 1024UL * 64UL;
153     std::array<char, readBufSize> fileReadBuf{};
154 
155   public:
156     template <bool IsRequest, class Fields>
157     writer(boost::beast::http::header<IsRequest, Fields>& /*header*/,
158            value_type& bodyIn) :
159         body(bodyIn)
160     {}
161 
162     static void init(boost::beast::error_code& ec)
163     {
164         ec = {};
165     }
166 
167     boost::optional<std::pair<const_buffers_type, bool>>
168         get(boost::beast::error_code& ec)
169     {
170         return getWithMaxSize(ec, std::numeric_limits<size_t>::max());
171     }
172 
173     boost::optional<std::pair<const_buffers_type, bool>>
174         getWithMaxSize(boost::beast::error_code& ec, size_t maxSize)
175     {
176         std::pair<const_buffers_type, bool> ret;
177         if (!body.file().is_open())
178         {
179             size_t remain = body.str().size() - sent;
180             size_t toReturn = std::min(maxSize, remain);
181             ret.first = const_buffers_type(&body.str()[sent], toReturn);
182 
183             sent += toReturn;
184             ret.second = sent < body.str().size();
185             BMCWEB_LOG_INFO("Returning {} bytes more={}", ret.first.size(),
186                             ret.second);
187             return ret;
188         }
189         size_t readReq = std::min(fileReadBuf.size(), maxSize);
190         BMCWEB_LOG_INFO("Reading {}", readReq);
191         boost::system::error_code readEc;
192         size_t read = body.file().read(fileReadBuf.data(), readReq, readEc);
193         if (readEc)
194         {
195             if (readEc != boost::system::errc::operation_would_block &&
196                 readEc != boost::system::errc::resource_unavailable_try_again)
197             {
198                 BMCWEB_LOG_CRITICAL("Failed to read from file {}",
199                                     readEc.message());
200                 ec = readEc;
201                 return boost::none;
202             }
203         }
204 
205         std::string_view chunkView(fileReadBuf.data(), read);
206         BMCWEB_LOG_INFO("Read {} bytes from file", read);
207         // If the number of bytes read equals the amount requested, we haven't
208         // reached EOF yet
209         ret.second = read == readReq;
210         if (body.encodingType == EncodingType::Base64)
211         {
212             buf.clear();
213             buf.reserve(
214                 crow::utility::Base64Encoder::encodedSize(chunkView.size()));
215             encoder.encode(chunkView, buf);
216             if (!ret.second)
217             {
218                 encoder.finalize(buf);
219             }
220             ret.first = const_buffers_type(buf.data(), buf.size());
221         }
222         else
223         {
224             ret.first = const_buffers_type(chunkView.data(), chunkView.size());
225         }
226         return ret;
227     }
228 };
229 
230 class HttpBody::reader
231 {
232     value_type& value;
233 
234   public:
235     template <bool IsRequest, class Fields>
236     reader(boost::beast::http::header<IsRequest, Fields>& /*headers*/,
237            value_type& body) :
238         value(body)
239     {}
240 
241     void init(const boost::optional<std::uint64_t>& contentLength,
242               boost::beast::error_code& ec)
243     {
244         if (contentLength)
245         {
246             if (!value.file().is_open())
247             {
248                 value.str().reserve(static_cast<size_t>(*contentLength));
249             }
250         }
251         ec = {};
252     }
253 
254     template <class ConstBufferSequence>
255     std::size_t put(const ConstBufferSequence& buffers,
256                     boost::system::error_code& ec)
257     {
258         size_t extra = boost::beast::buffer_bytes(buffers);
259         for (const auto b : boost::beast::buffers_range_ref(buffers))
260         {
261             const char* ptr = static_cast<const char*>(b.data());
262             value.str() += std::string_view(ptr, b.size());
263         }
264         ec = {};
265         return extra;
266     }
267 
268     static void finish(boost::system::error_code& ec)
269     {
270         ec = {};
271     }
272 };
273 
274 inline std::uint64_t HttpBody::size(const value_type& body)
275 {
276     std::optional<size_t> payloadSize = body.payloadSize();
277     return payloadSize.value_or(0U);
278 }
279 
280 } // namespace bmcweb
281