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