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; 49 explicit value_type(std::string_view s) : strBody(s) {} 50 explicit value_type(EncodingType e) : encodingType(e) {} 51 EncodingType encodingType = EncodingType::Raw; 52 53 const boost::beast::file_posix& file() const 54 { 55 return fileHandle.fileHandle; 56 } 57 58 std::string& str() 59 { 60 return strBody; 61 } 62 63 const std::string& str() const 64 { 65 return strBody; 66 } 67 68 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 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 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 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> 159 writer(boost::beast::http::header<IsRequest, Fields>& /*header*/, 160 value_type& bodyIn) : body(bodyIn) 161 {} 162 163 static void init(boost::beast::error_code& ec) 164 { 165 ec = {}; 166 } 167 168 boost::optional<std::pair<const_buffers_type, bool>> 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>> 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> 237 reader(boost::beast::http::header<IsRequest, Fields>& /*headers*/, 238 value_type& body) : value(body) 239 {} 240 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> 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 268 static void finish(boost::system::error_code& ec) 269 { 270 ec = {}; 271 } 272 }; 273 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