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