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