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