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