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