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