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