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