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