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 it->second.accept = thisReq.getHeaderValue("Accept"); 285 286 BMCWEB_LOG_DEBUG("Handling {} \"{}\"", logPtr(&thisReq), 287 thisReq.url().encoded_path()); 288 289 crow::Response& thisRes = it->second.res; 290 291 thisRes.setCompleteRequestHandler( 292 [this, streamId](Response& completeRes) { 293 BMCWEB_LOG_DEBUG("res.completeRequestHandler called"); 294 if (sendResponse(completeRes, streamId) != 0) 295 { 296 close(); 297 return; 298 } 299 }); 300 auto asyncResp = 301 std::make_shared<bmcweb::AsyncResp>(std::move(it->second.res)); 302 if constexpr (!BMCWEB_INSECURE_DISABLE_AUTH) 303 { 304 thisReq.session = crow::authentication::authenticate( 305 {}, asyncResp->res, thisReq.method(), thisReq.req, nullptr); 306 if (!crow::authentication::isOnAllowlist(thisReq.url().path(), 307 thisReq.method()) && 308 thisReq.session == nullptr) 309 { 310 BMCWEB_LOG_WARNING("Authentication failed"); 311 forward_unauthorized::sendUnauthorized( 312 thisReq.url().encoded_path(), 313 thisReq.getHeaderValue("X-Requested-With"), 314 thisReq.getHeaderValue("Accept"), asyncResp->res); 315 return 0; 316 } 317 } 318 std::string_view expected = 319 thisReq.getHeaderValue(boost::beast::http::field::if_none_match); 320 BMCWEB_LOG_DEBUG("Setting expected hash {}", expected); 321 if (!expected.empty()) 322 { 323 asyncResp->res.setExpectedHash(expected); 324 } 325 handler->handle(it->second.req, asyncResp); 326 return 0; 327 } 328 329 int onDataChunkRecvCallback(uint8_t /*flags*/, int32_t streamId, 330 const uint8_t* data, size_t len) 331 { 332 auto thisStream = streams.find(streamId); 333 if (thisStream == streams.end()) 334 { 335 BMCWEB_LOG_ERROR("Unknown stream{}", streamId); 336 close(); 337 return -1; 338 } 339 340 std::optional<bmcweb::HttpBody::reader>& reqReader = 341 thisStream->second.reqReader; 342 if (!reqReader) 343 { 344 reqReader.emplace( 345 bmcweb::HttpBody::reader(thisStream->second.req->req.base(), 346 thisStream->second.req->req.body())); 347 } 348 boost::beast::error_code ec; 349 reqReader->put(boost::asio::const_buffer(data, len), ec); 350 if (ec) 351 { 352 BMCWEB_LOG_CRITICAL("Failed to write payload"); 353 return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; 354 } 355 return 0; 356 } 357 358 static int onDataChunkRecvStatic( 359 nghttp2_session* /* session */, uint8_t flags, int32_t streamId, 360 const uint8_t* data, size_t len, void* userData) 361 { 362 BMCWEB_LOG_DEBUG("on_frame_recv_callback"); 363 if (userData == nullptr) 364 { 365 BMCWEB_LOG_CRITICAL("user data was null?"); 366 return NGHTTP2_ERR_CALLBACK_FAILURE; 367 } 368 return userPtrToSelf(userData).onDataChunkRecvCallback( 369 flags, streamId, data, len); 370 } 371 372 int onFrameRecvCallback(const nghttp2_frame& frame) 373 { 374 BMCWEB_LOG_DEBUG("frame type {}", static_cast<int>(frame.hd.type)); 375 switch (frame.hd.type) 376 { 377 case NGHTTP2_DATA: 378 case NGHTTP2_HEADERS: 379 // Check that the client request has finished 380 if ((frame.hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) 381 { 382 return onRequestRecv(frame.hd.stream_id); 383 } 384 break; 385 default: 386 break; 387 } 388 return 0; 389 } 390 391 static int onFrameRecvCallbackStatic(nghttp2_session* /* session */, 392 const nghttp2_frame* frame, 393 void* userData) 394 { 395 BMCWEB_LOG_DEBUG("on_frame_recv_callback"); 396 if (userData == nullptr) 397 { 398 BMCWEB_LOG_CRITICAL("user data was null?"); 399 return NGHTTP2_ERR_CALLBACK_FAILURE; 400 } 401 if (frame == nullptr) 402 { 403 BMCWEB_LOG_CRITICAL("frame was null?"); 404 return NGHTTP2_ERR_CALLBACK_FAILURE; 405 } 406 return userPtrToSelf(userData).onFrameRecvCallback(*frame); 407 } 408 409 static self_type& userPtrToSelf(void* userData) 410 { 411 // This method exists to keep the unsafe reinterpret cast in one 412 // place. 413 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 414 return *reinterpret_cast<self_type*>(userData); 415 } 416 417 static int onStreamCloseCallbackStatic(nghttp2_session* /* session */, 418 int32_t streamId, 419 uint32_t /*unused*/, void* userData) 420 { 421 BMCWEB_LOG_DEBUG("on_stream_close_callback stream {}", streamId); 422 if (userData == nullptr) 423 { 424 BMCWEB_LOG_CRITICAL("user data was null?"); 425 return NGHTTP2_ERR_CALLBACK_FAILURE; 426 } 427 if (userPtrToSelf(userData).streams.erase(streamId) <= 0) 428 { 429 return -1; 430 } 431 return 0; 432 } 433 434 int onHeaderCallback(const nghttp2_frame& frame, 435 std::span<const uint8_t> name, 436 std::span<const uint8_t> value) 437 { 438 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 439 std::string_view nameSv(reinterpret_cast<const char*>(name.data()), 440 name.size()); 441 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 442 std::string_view valueSv(reinterpret_cast<const char*>(value.data()), 443 value.size()); 444 445 BMCWEB_LOG_DEBUG("on_header_callback name: {} value {}", nameSv, 446 valueSv); 447 if (frame.hd.type != NGHTTP2_HEADERS) 448 { 449 return 0; 450 } 451 if (frame.headers.cat != NGHTTP2_HCAT_REQUEST) 452 { 453 return 0; 454 } 455 auto thisStream = streams.find(frame.hd.stream_id); 456 if (thisStream == streams.end()) 457 { 458 BMCWEB_LOG_ERROR("Unknown stream{}", frame.hd.stream_id); 459 close(); 460 return -1; 461 } 462 463 crow::Request& thisReq = *thisStream->second.req; 464 465 if (nameSv == ":path") 466 { 467 thisReq.target(valueSv); 468 } 469 else if (nameSv == ":method") 470 { 471 boost::beast::http::verb verb = 472 boost::beast::http::string_to_verb(valueSv); 473 if (verb == boost::beast::http::verb::unknown) 474 { 475 BMCWEB_LOG_ERROR("Unknown http verb {}", valueSv); 476 verb = boost::beast::http::verb::trace; 477 } 478 thisReq.method(verb); 479 } 480 else if (nameSv == ":scheme") 481 { 482 // Nothing to check on scheme 483 } 484 else 485 { 486 thisReq.addHeader(nameSv, valueSv); 487 } 488 return 0; 489 } 490 491 static int onHeaderCallbackStatic( 492 nghttp2_session* /* session */, const nghttp2_frame* frame, 493 const uint8_t* name, size_t namelen, const uint8_t* value, 494 size_t vallen, uint8_t /* flags */, void* userData) 495 { 496 if (userData == nullptr) 497 { 498 BMCWEB_LOG_CRITICAL("user data was null?"); 499 return NGHTTP2_ERR_CALLBACK_FAILURE; 500 } 501 if (frame == nullptr) 502 { 503 BMCWEB_LOG_CRITICAL("frame was null?"); 504 return NGHTTP2_ERR_CALLBACK_FAILURE; 505 } 506 if (name == nullptr) 507 { 508 BMCWEB_LOG_CRITICAL("name was null?"); 509 return NGHTTP2_ERR_CALLBACK_FAILURE; 510 } 511 if (value == nullptr) 512 { 513 BMCWEB_LOG_CRITICAL("value was null?"); 514 return NGHTTP2_ERR_CALLBACK_FAILURE; 515 } 516 return userPtrToSelf(userData).onHeaderCallback(*frame, {name, namelen}, 517 {value, vallen}); 518 } 519 520 int onBeginHeadersCallback(const nghttp2_frame& frame) 521 { 522 if (frame.hd.type == NGHTTP2_HEADERS && 523 frame.headers.cat == NGHTTP2_HCAT_REQUEST) 524 { 525 BMCWEB_LOG_DEBUG("create stream for id {}", frame.hd.stream_id); 526 527 streams.emplace(frame.hd.stream_id, Http2StreamData()); 528 } 529 return 0; 530 } 531 532 static int onBeginHeadersCallbackStatic(nghttp2_session* /* session */, 533 const nghttp2_frame* frame, 534 void* userData) 535 { 536 BMCWEB_LOG_DEBUG("on_begin_headers_callback"); 537 if (userData == nullptr) 538 { 539 BMCWEB_LOG_CRITICAL("user data was null?"); 540 return NGHTTP2_ERR_CALLBACK_FAILURE; 541 } 542 if (frame == nullptr) 543 { 544 BMCWEB_LOG_CRITICAL("frame was null?"); 545 return NGHTTP2_ERR_CALLBACK_FAILURE; 546 } 547 return userPtrToSelf(userData).onBeginHeadersCallback(*frame); 548 } 549 550 static void afterWriteBuffer(const std::shared_ptr<self_type>& self, 551 const boost::system::error_code& ec, 552 size_t sendLength) 553 { 554 self->isWriting = false; 555 BMCWEB_LOG_DEBUG("Sent {}", sendLength); 556 if (ec) 557 { 558 self->close(); 559 return; 560 } 561 self->writeBuffer(); 562 } 563 564 void writeBuffer() 565 { 566 if (isWriting) 567 { 568 return; 569 } 570 std::span<const uint8_t> data = ngSession.memSend(); 571 if (data.empty()) 572 { 573 return; 574 } 575 isWriting = true; 576 boost::asio::async_write( 577 adaptor, boost::asio::const_buffer(data.data(), data.size()), 578 std::bind_front(afterWriteBuffer, shared_from_this())); 579 } 580 581 void close() 582 { 583 if constexpr (IsTls<Adaptor>::value) 584 { 585 adaptor.next_layer().close(); 586 } 587 else 588 { 589 adaptor.close(); 590 } 591 } 592 593 void afterDoRead(const std::shared_ptr<self_type>& /*self*/, 594 const boost::system::error_code& ec, 595 size_t bytesTransferred) 596 { 597 BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this), 598 bytesTransferred); 599 600 if (ec) 601 { 602 BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this), 603 ec.message()); 604 close(); 605 BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this)); 606 return; 607 } 608 std::span<uint8_t> bufferSpan{inBuffer.data(), bytesTransferred}; 609 610 ssize_t readLen = ngSession.memRecv(bufferSpan); 611 if (readLen < 0) 612 { 613 BMCWEB_LOG_ERROR("nghttp2_session_mem_recv returned {}", readLen); 614 close(); 615 return; 616 } 617 writeBuffer(); 618 619 doRead(); 620 } 621 622 void doRead() 623 { 624 BMCWEB_LOG_DEBUG("{} doRead", logPtr(this)); 625 adaptor.async_read_some( 626 boost::asio::buffer(inBuffer), 627 std::bind_front(&self_type::afterDoRead, this, shared_from_this())); 628 } 629 630 // A mapping from http2 stream ID to Stream Data 631 std::map<int32_t, Http2StreamData> streams; 632 633 std::array<uint8_t, 8192> inBuffer{}; 634 635 Adaptor adaptor; 636 bool isWriting = false; 637 638 nghttp2_session ngSession; 639 640 Handler* handler; 641 std::function<std::string()>& getCachedDateStr; 642 643 using std::enable_shared_from_this< 644 HTTP2Connection<Adaptor, Handler>>::shared_from_this; 645 646 using std::enable_shared_from_this< 647 HTTP2Connection<Adaptor, Handler>>::weak_from_this; 648 }; 649 } // namespace crow 650