1 #pragma once 2 #include "bmcweb_config.h" 3 4 #include "async_resp.hpp" 5 #include "authentication.hpp" 6 #include "complete_response_fields.hpp" 7 #include "http_body.hpp" 8 #include "http_response.hpp" 9 #include "http_utility.hpp" 10 #include "logging.hpp" 11 #include "mutual_tls.hpp" 12 #include "nghttp2_adapters.hpp" 13 #include "ssl_key_handler.hpp" 14 #include "utility.hpp" 15 16 #include <boost/asio/io_context.hpp> 17 #include <boost/asio/ip/tcp.hpp> 18 #include <boost/asio/ssl/stream.hpp> 19 #include <boost/asio/steady_timer.hpp> 20 #include <boost/beast/http/error.hpp> 21 #include <boost/beast/http/parser.hpp> 22 #include <boost/beast/http/read.hpp> 23 #include <boost/beast/http/serializer.hpp> 24 #include <boost/beast/http/write.hpp> 25 #include <boost/beast/ssl/ssl_stream.hpp> 26 #include <boost/beast/websocket.hpp> 27 #include <boost/system/error_code.hpp> 28 29 #include <array> 30 #include <atomic> 31 #include <chrono> 32 #include <functional> 33 #include <memory> 34 #include <vector> 35 36 namespace crow 37 { 38 39 struct Http2StreamData 40 { 41 Request req; 42 std::optional<bmcweb::HttpBody::reader> reqReader; 43 Response res; 44 std::optional<bmcweb::HttpBody::writer> writer; 45 }; 46 47 template <typename Adaptor, typename Handler> 48 class HTTP2Connection : 49 public std::enable_shared_from_this<HTTP2Connection<Adaptor, Handler>> 50 { 51 using self_type = HTTP2Connection<Adaptor, Handler>; 52 53 public: 54 HTTP2Connection(Adaptor&& adaptorIn, Handler* handlerIn, 55 std::function<std::string()>& getCachedDateStrF) : 56 adaptor(std::move(adaptorIn)), 57 ngSession(initializeNghttp2Session()), handler(handlerIn), 58 getCachedDateStr(getCachedDateStrF) 59 {} 60 61 void start() 62 { 63 // Create the control stream 64 streams[0]; 65 66 if (sendServerConnectionHeader() != 0) 67 { 68 BMCWEB_LOG_ERROR("send_server_connection_header failed"); 69 return; 70 } 71 doRead(); 72 } 73 74 int sendServerConnectionHeader() 75 { 76 BMCWEB_LOG_DEBUG("send_server_connection_header()"); 77 78 uint32_t maxStreams = 4; 79 std::array<nghttp2_settings_entry, 2> iv = { 80 {{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, maxStreams}, 81 {NGHTTP2_SETTINGS_ENABLE_PUSH, 0}}}; 82 int rv = ngSession.submitSettings(iv); 83 if (rv != 0) 84 { 85 BMCWEB_LOG_ERROR("Fatal error: {}", nghttp2_strerror(rv)); 86 return -1; 87 } 88 writeBuffer(); 89 return 0; 90 } 91 92 static ssize_t fileReadCallback(nghttp2_session* /* session */, 93 int32_t streamId, uint8_t* buf, 94 size_t length, uint32_t* dataFlags, 95 nghttp2_data_source* /*source*/, 96 void* userPtr) 97 { 98 self_type& self = userPtrToSelf(userPtr); 99 100 auto streamIt = self.streams.find(streamId); 101 if (streamIt == self.streams.end()) 102 { 103 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; 104 } 105 Http2StreamData& stream = streamIt->second; 106 BMCWEB_LOG_DEBUG("File read callback length: {}", length); 107 if (!stream.writer) 108 { 109 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; 110 } 111 boost::beast::error_code ec; 112 boost::optional<std::pair<boost::asio::const_buffer, bool>> out = 113 stream.writer->getWithMaxSize(ec, length); 114 if (ec) 115 { 116 BMCWEB_LOG_CRITICAL("Failed to get buffer"); 117 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; 118 } 119 if (!out) 120 { 121 BMCWEB_LOG_ERROR("Empty file, setting EOF"); 122 *dataFlags |= NGHTTP2_DATA_FLAG_EOF; 123 return 0; 124 } 125 126 BMCWEB_LOG_DEBUG("Send chunk of size: {}", out->first.size()); 127 if (length < out->first.size()) 128 { 129 BMCWEB_LOG_CRITICAL( 130 "Buffer overflow that should never happen happened"); 131 // Should never happen because of length limit on get() above 132 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; 133 } 134 boost::asio::mutable_buffer writeableBuf(buf, length); 135 BMCWEB_LOG_DEBUG("Copying {} bytes to buf", out->first.size()); 136 size_t copied = boost::asio::buffer_copy(writeableBuf, out->first); 137 if (copied != out->first.size()) 138 { 139 BMCWEB_LOG_ERROR( 140 "Couldn't copy all {} bytes into buffer, only copied {}", 141 out->first.size(), copied); 142 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; 143 } 144 145 if (!out->second) 146 { 147 BMCWEB_LOG_DEBUG("Setting EOF flag"); 148 *dataFlags |= NGHTTP2_DATA_FLAG_EOF; 149 } 150 return static_cast<ssize_t>(copied); 151 } 152 153 nghttp2_nv headerFromStringViews(std::string_view name, 154 std::string_view value, uint8_t flags) 155 { 156 uint8_t* nameData = std::bit_cast<uint8_t*>(name.data()); 157 uint8_t* valueData = std::bit_cast<uint8_t*>(value.data()); 158 return {nameData, valueData, name.size(), value.size(), flags}; 159 } 160 161 int sendResponse(Response& completedRes, int32_t streamId) 162 { 163 BMCWEB_LOG_DEBUG("send_response stream_id:{}", streamId); 164 165 auto it = streams.find(streamId); 166 if (it == streams.end()) 167 { 168 close(); 169 return -1; 170 } 171 Response& thisRes = it->second.res; 172 thisRes = std::move(completedRes); 173 crow::Request& thisReq = it->second.req; 174 std::vector<nghttp2_nv> hdr; 175 176 completeResponseFields(thisReq, thisRes); 177 thisRes.addHeader(boost::beast::http::field::date, getCachedDateStr()); 178 thisRes.preparePayload(); 179 180 boost::beast::http::fields& fields = thisRes.fields(); 181 std::string code = std::to_string(thisRes.resultInt()); 182 hdr.emplace_back( 183 headerFromStringViews(":status", code, NGHTTP2_NV_FLAG_NONE)); 184 for (const boost::beast::http::fields::value_type& header : fields) 185 { 186 hdr.emplace_back(headerFromStringViews( 187 header.name_string(), header.value(), NGHTTP2_NV_FLAG_NONE)); 188 } 189 Http2StreamData& stream = it->second; 190 crow::Response& res = stream.res; 191 http::response<bmcweb::HttpBody>& fbody = res.response; 192 stream.writer.emplace(fbody.base(), fbody.body()); 193 194 nghttp2_data_provider dataPrd{ 195 .source = {.fd = 0}, 196 .read_callback = fileReadCallback, 197 }; 198 199 int rv = ngSession.submitResponse(streamId, hdr, &dataPrd); 200 if (rv != 0) 201 { 202 BMCWEB_LOG_ERROR("Fatal error: {}", nghttp2_strerror(rv)); 203 close(); 204 return -1; 205 } 206 writeBuffer(); 207 208 return 0; 209 } 210 211 nghttp2_session initializeNghttp2Session() 212 { 213 nghttp2_session_callbacks callbacks; 214 callbacks.setOnFrameRecvCallback(onFrameRecvCallbackStatic); 215 callbacks.setOnStreamCloseCallback(onStreamCloseCallbackStatic); 216 callbacks.setOnHeaderCallback(onHeaderCallbackStatic); 217 callbacks.setOnBeginHeadersCallback(onBeginHeadersCallbackStatic); 218 callbacks.setOnDataChunkRecvCallback(onDataChunkRecvStatic); 219 220 nghttp2_session session(callbacks); 221 session.setUserData(this); 222 223 return session; 224 } 225 226 int onRequestRecv(int32_t streamId) 227 { 228 BMCWEB_LOG_DEBUG("on_request_recv"); 229 230 auto it = streams.find(streamId); 231 if (it == streams.end()) 232 { 233 close(); 234 return -1; 235 } 236 auto& reqReader = it->second.reqReader; 237 if (reqReader) 238 { 239 boost::beast::error_code ec; 240 reqReader->finish(ec); 241 if (ec) 242 { 243 BMCWEB_LOG_CRITICAL("Failed to finalize payload"); 244 close(); 245 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; 246 } 247 } 248 crow::Request& thisReq = it->second.req; 249 BMCWEB_LOG_DEBUG("Handling {} \"{}\"", logPtr(&thisReq), 250 thisReq.url().encoded_path()); 251 252 crow::Response& thisRes = it->second.res; 253 254 thisRes.setCompleteRequestHandler( 255 [this, streamId](Response& completeRes) { 256 BMCWEB_LOG_DEBUG("res.completeRequestHandler called"); 257 if (sendResponse(completeRes, streamId) != 0) 258 { 259 close(); 260 return; 261 } 262 }); 263 auto asyncResp = 264 std::make_shared<bmcweb::AsyncResp>(std::move(it->second.res)); 265 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 266 thisReq.session = crow::authentication::authenticate( 267 {}, thisRes, thisReq.method(), thisReq.req, nullptr); 268 if (!crow::authentication::isOnAllowlist(thisReq.url().path(), 269 thisReq.method()) && 270 thisReq.session == nullptr) 271 { 272 BMCWEB_LOG_WARNING("Authentication failed"); 273 forward_unauthorized::sendUnauthorized( 274 thisReq.url().encoded_path(), 275 thisReq.getHeaderValue("X-Requested-With"), 276 thisReq.getHeaderValue("Accept"), thisRes); 277 } 278 else 279 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 280 { 281 handler->handle(thisReq, asyncResp); 282 } 283 return 0; 284 } 285 286 int onDataChunkRecvCallback(uint8_t /*flags*/, int32_t streamId, 287 const uint8_t* data, size_t len) 288 { 289 auto thisStream = streams.find(streamId); 290 if (thisStream == streams.end()) 291 { 292 BMCWEB_LOG_ERROR("Unknown stream{}", streamId); 293 close(); 294 return -1; 295 } 296 297 std::optional<bmcweb::HttpBody::reader>& reqReader = 298 thisStream->second.reqReader; 299 if (!reqReader) 300 { 301 BMCWEB_LOG_ERROR("No reader init {}", streamId); 302 close(); 303 return -1; 304 } 305 boost::beast::error_code ec; 306 reqReader->put(boost::asio::const_buffer(data, len), ec); 307 if (ec) 308 { 309 BMCWEB_LOG_CRITICAL("Failed to write payload"); 310 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; 311 } 312 return 0; 313 } 314 315 static int onDataChunkRecvStatic(nghttp2_session* /* session */, 316 uint8_t flags, int32_t streamId, 317 const uint8_t* data, size_t len, 318 void* userData) 319 { 320 BMCWEB_LOG_DEBUG("on_frame_recv_callback"); 321 if (userData == nullptr) 322 { 323 BMCWEB_LOG_CRITICAL("user data was null?"); 324 return NGHTTP2_ERR_CALLBACK_FAILURE; 325 } 326 return userPtrToSelf(userData).onDataChunkRecvCallback(flags, streamId, 327 data, len); 328 } 329 330 int onFrameRecvCallback(const nghttp2_frame& frame) 331 { 332 BMCWEB_LOG_DEBUG("frame type {}", static_cast<int>(frame.hd.type)); 333 switch (frame.hd.type) 334 { 335 case NGHTTP2_DATA: 336 case NGHTTP2_HEADERS: 337 // Check that the client request has finished 338 if ((frame.hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) 339 { 340 return onRequestRecv(frame.hd.stream_id); 341 } 342 break; 343 default: 344 break; 345 } 346 return 0; 347 } 348 349 static int onFrameRecvCallbackStatic(nghttp2_session* /* session */, 350 const nghttp2_frame* frame, 351 void* userData) 352 { 353 BMCWEB_LOG_DEBUG("on_frame_recv_callback"); 354 if (userData == nullptr) 355 { 356 BMCWEB_LOG_CRITICAL("user data was null?"); 357 return NGHTTP2_ERR_CALLBACK_FAILURE; 358 } 359 if (frame == nullptr) 360 { 361 BMCWEB_LOG_CRITICAL("frame was null?"); 362 return NGHTTP2_ERR_CALLBACK_FAILURE; 363 } 364 return userPtrToSelf(userData).onFrameRecvCallback(*frame); 365 } 366 367 static self_type& userPtrToSelf(void* userData) 368 { 369 // This method exists to keep the unsafe reinterpret cast in one 370 // place. 371 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 372 return *reinterpret_cast<self_type*>(userData); 373 } 374 375 static int onStreamCloseCallbackStatic(nghttp2_session* /* session */, 376 int32_t streamId, 377 uint32_t /*unused*/, void* userData) 378 { 379 BMCWEB_LOG_DEBUG("on_stream_close_callback stream {}", streamId); 380 if (userData == nullptr) 381 { 382 BMCWEB_LOG_CRITICAL("user data was null?"); 383 return NGHTTP2_ERR_CALLBACK_FAILURE; 384 } 385 if (userPtrToSelf(userData).streams.erase(streamId) <= 0) 386 { 387 return -1; 388 } 389 return 0; 390 } 391 392 int onHeaderCallback(const nghttp2_frame& frame, 393 std::span<const uint8_t> name, 394 std::span<const uint8_t> value) 395 { 396 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 397 std::string_view nameSv(reinterpret_cast<const char*>(name.data()), 398 name.size()); 399 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 400 std::string_view valueSv(reinterpret_cast<const char*>(value.data()), 401 value.size()); 402 403 BMCWEB_LOG_DEBUG("on_header_callback name: {} value {}", nameSv, 404 valueSv); 405 if (frame.hd.type != NGHTTP2_HEADERS) 406 { 407 return 0; 408 } 409 if (frame.headers.cat != NGHTTP2_HCAT_REQUEST) 410 { 411 return 0; 412 } 413 auto thisStream = streams.find(frame.hd.stream_id); 414 if (thisStream == streams.end()) 415 { 416 BMCWEB_LOG_ERROR("Unknown stream{}", frame.hd.stream_id); 417 close(); 418 return -1; 419 } 420 421 crow::Request& thisReq = thisStream->second.req; 422 423 if (nameSv == ":path") 424 { 425 thisReq.target(valueSv); 426 } 427 else if (nameSv == ":method") 428 { 429 boost::beast::http::verb verb = 430 boost::beast::http::string_to_verb(valueSv); 431 if (verb == boost::beast::http::verb::unknown) 432 { 433 BMCWEB_LOG_ERROR("Unknown http verb {}", valueSv); 434 close(); 435 return -1; 436 } 437 thisReq.req.method(verb); 438 } 439 else if (nameSv == ":scheme") 440 { 441 // Nothing to check on scheme 442 } 443 else 444 { 445 thisReq.req.set(nameSv, valueSv); 446 } 447 return 0; 448 } 449 450 static int onHeaderCallbackStatic(nghttp2_session* /* session */, 451 const nghttp2_frame* frame, 452 const uint8_t* name, size_t namelen, 453 const uint8_t* value, size_t vallen, 454 uint8_t /* flags */, void* userData) 455 { 456 if (userData == nullptr) 457 { 458 BMCWEB_LOG_CRITICAL("user data was null?"); 459 return NGHTTP2_ERR_CALLBACK_FAILURE; 460 } 461 if (frame == nullptr) 462 { 463 BMCWEB_LOG_CRITICAL("frame was null?"); 464 return NGHTTP2_ERR_CALLBACK_FAILURE; 465 } 466 if (name == nullptr) 467 { 468 BMCWEB_LOG_CRITICAL("name was null?"); 469 return NGHTTP2_ERR_CALLBACK_FAILURE; 470 } 471 if (value == nullptr) 472 { 473 BMCWEB_LOG_CRITICAL("value was null?"); 474 return NGHTTP2_ERR_CALLBACK_FAILURE; 475 } 476 return userPtrToSelf(userData).onHeaderCallback(*frame, {name, namelen}, 477 {value, vallen}); 478 } 479 480 int onBeginHeadersCallback(const nghttp2_frame& frame) 481 { 482 if (frame.hd.type == NGHTTP2_HEADERS && 483 frame.headers.cat == NGHTTP2_HCAT_REQUEST) 484 { 485 BMCWEB_LOG_DEBUG("create stream for id {}", frame.hd.stream_id); 486 487 Http2StreamData& stream = streams[frame.hd.stream_id]; 488 // http2 is by definition always tls 489 stream.req.isSecure = true; 490 } 491 return 0; 492 } 493 494 static int onBeginHeadersCallbackStatic(nghttp2_session* /* session */, 495 const nghttp2_frame* frame, 496 void* userData) 497 { 498 BMCWEB_LOG_DEBUG("on_begin_headers_callback"); 499 if (userData == nullptr) 500 { 501 BMCWEB_LOG_CRITICAL("user data was null?"); 502 return NGHTTP2_ERR_CALLBACK_FAILURE; 503 } 504 if (frame == nullptr) 505 { 506 BMCWEB_LOG_CRITICAL("frame was null?"); 507 return NGHTTP2_ERR_CALLBACK_FAILURE; 508 } 509 return userPtrToSelf(userData).onBeginHeadersCallback(*frame); 510 } 511 512 static void afterWriteBuffer(const std::shared_ptr<self_type>& self, 513 const boost::system::error_code& ec, 514 size_t sendLength) 515 { 516 self->isWriting = false; 517 BMCWEB_LOG_DEBUG("Sent {}", sendLength); 518 if (ec) 519 { 520 self->close(); 521 return; 522 } 523 self->writeBuffer(); 524 } 525 526 void writeBuffer() 527 { 528 if (isWriting) 529 { 530 return; 531 } 532 std::span<const uint8_t> data = ngSession.memSend(); 533 if (data.empty()) 534 { 535 return; 536 } 537 isWriting = true; 538 boost::asio::async_write( 539 adaptor, boost::asio::const_buffer(data.data(), data.size()), 540 std::bind_front(afterWriteBuffer, shared_from_this())); 541 } 542 543 void close() 544 { 545 if constexpr (std::is_same_v<Adaptor, 546 boost::beast::ssl_stream< 547 boost::asio::ip::tcp::socket>>) 548 { 549 adaptor.next_layer().close(); 550 } 551 else 552 { 553 adaptor.close(); 554 } 555 } 556 557 void afterDoRead(const std::shared_ptr<self_type>& /*self*/, 558 const boost::system::error_code& ec, 559 size_t bytesTransferred) 560 { 561 BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this), 562 bytesTransferred); 563 564 if (ec) 565 { 566 BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this), 567 ec.message()); 568 close(); 569 BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this)); 570 return; 571 } 572 std::span<uint8_t> bufferSpan{inBuffer.data(), bytesTransferred}; 573 574 ssize_t readLen = ngSession.memRecv(bufferSpan); 575 if (readLen < 0) 576 { 577 BMCWEB_LOG_ERROR("nghttp2_session_mem_recv returned {}", readLen); 578 close(); 579 return; 580 } 581 writeBuffer(); 582 583 doRead(); 584 } 585 586 void doRead() 587 { 588 BMCWEB_LOG_DEBUG("{} doRead", logPtr(this)); 589 adaptor.async_read_some( 590 boost::asio::buffer(inBuffer), 591 std::bind_front(&self_type::afterDoRead, this, shared_from_this())); 592 } 593 594 // A mapping from http2 stream ID to Stream Data 595 std::map<int32_t, Http2StreamData> streams; 596 597 std::array<uint8_t, 8192> inBuffer{}; 598 599 Adaptor adaptor; 600 bool isWriting = false; 601 602 nghttp2_session ngSession; 603 604 Handler* handler; 605 std::function<std::string()>& getCachedDateStr; 606 607 using std::enable_shared_from_this< 608 HTTP2Connection<Adaptor, Handler>>::shared_from_this; 609 610 using std::enable_shared_from_this< 611 HTTP2Connection<Adaptor, Handler>>::weak_from_this; 612 }; 613 } // namespace crow 614