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