1 #pragma once 2 #include "bmcweb_config.h" 3 4 #include "authentication.hpp" 5 #include "http_response.hpp" 6 #include "http_utility.hpp" 7 #include "logging.hpp" 8 #include "utility.hpp" 9 10 #include <boost/algorithm/string/predicate.hpp> 11 #include <boost/asio/io_context.hpp> 12 #include <boost/asio/ip/tcp.hpp> 13 #include <boost/asio/ssl/stream.hpp> 14 #include <boost/asio/steady_timer.hpp> 15 #include <boost/beast/core/flat_static_buffer.hpp> 16 #include <boost/beast/http/parser.hpp> 17 #include <boost/beast/http/read.hpp> 18 #include <boost/beast/http/serializer.hpp> 19 #include <boost/beast/http/write.hpp> 20 #include <boost/beast/ssl/ssl_stream.hpp> 21 #include <boost/beast/websocket.hpp> 22 #include <boost/url/url_view.hpp> 23 #include <json_html_serializer.hpp> 24 #include <security_headers.hpp> 25 #include <ssl_key_handler.hpp> 26 27 #include <atomic> 28 #include <chrono> 29 #include <vector> 30 31 namespace crow 32 { 33 34 inline void prettyPrintJson(crow::Response& res) 35 { 36 json_html_util::dumpHtml(res.body(), res.jsonValue); 37 38 res.addHeader(boost::beast::http::field::content_type, 39 "text/html;charset=UTF-8"); 40 } 41 42 static int connectionCount = 0; 43 44 // request body limit size set by the bmcwebHttpReqBodyLimitMb option 45 constexpr uint64_t httpReqBodyLimit = 46 1024UL * 1024UL * bmcwebHttpReqBodyLimitMb; 47 48 constexpr uint64_t loggedOutPostBodyLimit = 4096; 49 50 constexpr uint32_t httpHeaderLimit = 8192; 51 52 template <typename Adaptor, typename Handler> 53 class Connection : 54 public std::enable_shared_from_this<Connection<Adaptor, Handler>> 55 { 56 public: 57 Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn, 58 std::function<std::string()>& getCachedDateStrF, 59 Adaptor adaptorIn) : 60 adaptor(std::move(adaptorIn)), 61 handler(handlerIn), timer(std::move(timerIn)), 62 getCachedDateStr(getCachedDateStrF) 63 { 64 parser.emplace(std::piecewise_construct, std::make_tuple()); 65 parser->body_limit(httpReqBodyLimit); 66 parser->header_limit(httpHeaderLimit); 67 68 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 69 prepareMutualTls(); 70 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 71 72 connectionCount++; 73 74 BMCWEB_LOG_DEBUG << this << " Connection open, total " 75 << connectionCount; 76 } 77 78 ~Connection() 79 { 80 res.setCompleteRequestHandler(nullptr); 81 cancelDeadlineTimer(); 82 83 connectionCount--; 84 BMCWEB_LOG_DEBUG << this << " Connection closed, total " 85 << connectionCount; 86 } 87 88 Connection(const Connection&) = delete; 89 Connection(Connection&&) = delete; 90 Connection& operator=(const Connection&) = delete; 91 Connection& operator=(Connection&&) = delete; 92 93 void prepareMutualTls() 94 { 95 std::error_code error; 96 std::filesystem::path caPath(ensuressl::trustStorePath); 97 auto caAvailable = !std::filesystem::is_empty(caPath, error); 98 caAvailable = caAvailable && !error; 99 if (caAvailable && persistent_data::SessionStore::getInstance() 100 .getAuthMethodsConfig() 101 .tls) 102 { 103 adaptor.set_verify_mode(boost::asio::ssl::verify_peer); 104 std::string id = "bmcweb"; 105 106 const char* cStr = id.c_str(); 107 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 108 const auto* idC = reinterpret_cast<const unsigned char*>(cStr); 109 int ret = SSL_set_session_id_context( 110 adaptor.native_handle(), idC, 111 static_cast<unsigned int>(id.length())); 112 if (ret == 0) 113 { 114 BMCWEB_LOG_ERROR << this << " failed to set SSL id"; 115 } 116 } 117 118 adaptor.set_verify_callback( 119 [this](bool preverified, boost::asio::ssl::verify_context& ctx) { 120 // do nothing if TLS is disabled 121 if (!persistent_data::SessionStore::getInstance() 122 .getAuthMethodsConfig() 123 .tls) 124 { 125 BMCWEB_LOG_DEBUG << this << " TLS auth_config is disabled"; 126 return true; 127 } 128 129 // We always return true to allow full auth flow 130 if (!preverified) 131 { 132 BMCWEB_LOG_DEBUG << this << " TLS preverification failed."; 133 return true; 134 } 135 136 X509_STORE_CTX* cts = ctx.native_handle(); 137 if (cts == nullptr) 138 { 139 BMCWEB_LOG_DEBUG << this << " Cannot get native TLS handle."; 140 return true; 141 } 142 143 // Get certificate 144 X509* peerCert = 145 X509_STORE_CTX_get_current_cert(ctx.native_handle()); 146 if (peerCert == nullptr) 147 { 148 BMCWEB_LOG_DEBUG << this 149 << " Cannot get current TLS certificate."; 150 return true; 151 } 152 153 // Check if certificate is OK 154 int ctxError = X509_STORE_CTX_get_error(cts); 155 if (ctxError != X509_V_OK) 156 { 157 BMCWEB_LOG_INFO << this << " Last TLS error is: " << ctxError; 158 return true; 159 } 160 // Check that we have reached final certificate in chain 161 int32_t depth = X509_STORE_CTX_get_error_depth(cts); 162 if (depth != 0) 163 164 { 165 BMCWEB_LOG_DEBUG 166 << this << " Certificate verification in progress (depth " 167 << depth << "), waiting to reach final depth"; 168 return true; 169 } 170 171 BMCWEB_LOG_DEBUG << this 172 << " Certificate verification of final depth"; 173 174 // Verify KeyUsage 175 bool isKeyUsageDigitalSignature = false; 176 bool isKeyUsageKeyAgreement = false; 177 178 ASN1_BIT_STRING* usage = static_cast<ASN1_BIT_STRING*>( 179 X509_get_ext_d2i(peerCert, NID_key_usage, nullptr, nullptr)); 180 181 if (usage == nullptr) 182 { 183 BMCWEB_LOG_DEBUG << this << " TLS usage is null"; 184 return true; 185 } 186 187 for (int i = 0; i < usage->length; i++) 188 { 189 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) 190 unsigned char usageChar = usage->data[i]; 191 if (KU_DIGITAL_SIGNATURE & usageChar) 192 { 193 isKeyUsageDigitalSignature = true; 194 } 195 if (KU_KEY_AGREEMENT & usageChar) 196 { 197 isKeyUsageKeyAgreement = true; 198 } 199 } 200 ASN1_BIT_STRING_free(usage); 201 202 if (!isKeyUsageDigitalSignature || !isKeyUsageKeyAgreement) 203 { 204 BMCWEB_LOG_DEBUG << this 205 << " Certificate ExtendedKeyUsage does " 206 "not allow provided certificate to " 207 "be used for user authentication"; 208 return true; 209 } 210 211 // Determine that ExtendedKeyUsage includes Client Auth 212 213 stack_st_ASN1_OBJECT* extUsage = 214 static_cast<stack_st_ASN1_OBJECT*>(X509_get_ext_d2i( 215 peerCert, NID_ext_key_usage, nullptr, nullptr)); 216 217 if (extUsage == nullptr) 218 { 219 BMCWEB_LOG_DEBUG << this << " TLS extUsage is null"; 220 return true; 221 } 222 223 bool isExKeyUsageClientAuth = false; 224 for (int i = 0; i < sk_ASN1_OBJECT_num(extUsage); i++) 225 { 226 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) 227 int nid = OBJ_obj2nid(sk_ASN1_OBJECT_value(extUsage, i)); 228 if (NID_client_auth == nid) 229 { 230 isExKeyUsageClientAuth = true; 231 break; 232 } 233 } 234 sk_ASN1_OBJECT_free(extUsage); 235 236 // Certificate has to have proper key usages set 237 if (!isExKeyUsageClientAuth) 238 { 239 BMCWEB_LOG_DEBUG << this 240 << " Certificate ExtendedKeyUsage does " 241 "not allow provided certificate to " 242 "be used for user authentication"; 243 return true; 244 } 245 std::string sslUser; 246 // Extract username contained in CommonName 247 sslUser.resize(256, '\0'); 248 249 int status = X509_NAME_get_text_by_NID( 250 X509_get_subject_name(peerCert), NID_commonName, sslUser.data(), 251 static_cast<int>(sslUser.size())); 252 253 if (status == -1) 254 { 255 BMCWEB_LOG_DEBUG 256 << this << " TLS cannot get username to create session"; 257 return true; 258 } 259 260 size_t lastChar = sslUser.find('\0'); 261 if (lastChar == std::string::npos || lastChar == 0) 262 { 263 BMCWEB_LOG_DEBUG << this << " Invalid TLS user name"; 264 return true; 265 } 266 sslUser.resize(lastChar); 267 sessionIsFromTransport = true; 268 userSession = persistent_data::SessionStore::getInstance() 269 .generateUserSession( 270 sslUser, req->ipAddress, std::nullopt, 271 persistent_data::PersistenceType::TIMEOUT); 272 if (userSession != nullptr) 273 { 274 BMCWEB_LOG_DEBUG 275 << this 276 << " Generating TLS session: " << userSession->uniqueId; 277 } 278 return true; 279 }); 280 } 281 282 Adaptor& socket() 283 { 284 return adaptor; 285 } 286 287 void start() 288 { 289 if (connectionCount >= 100) 290 { 291 BMCWEB_LOG_CRITICAL << this << "Max connection count exceeded."; 292 return; 293 } 294 295 startDeadline(); 296 297 // TODO(ed) Abstract this to a more clever class with the idea of an 298 // asynchronous "start" 299 if constexpr (std::is_same_v<Adaptor, 300 boost::beast::ssl_stream< 301 boost::asio::ip::tcp::socket>>) 302 { 303 adaptor.async_handshake(boost::asio::ssl::stream_base::server, 304 [this, self(shared_from_this())]( 305 const boost::system::error_code& ec) { 306 if (ec) 307 { 308 return; 309 } 310 doReadHeaders(); 311 }); 312 } 313 else 314 { 315 doReadHeaders(); 316 } 317 } 318 319 void handle() 320 { 321 std::error_code reqEc; 322 crow::Request& thisReq = req.emplace(parser->release(), reqEc); 323 if (reqEc) 324 { 325 BMCWEB_LOG_DEBUG << "Request failed to construct" << reqEc; 326 return; 327 } 328 thisReq.session = userSession; 329 330 // Fetch the client IP address 331 readClientIp(); 332 333 // Check for HTTP version 1.1. 334 if (thisReq.version() == 11) 335 { 336 if (thisReq.getHeaderValue(boost::beast::http::field::host).empty()) 337 { 338 res.result(boost::beast::http::status::bad_request); 339 completeRequest(res); 340 return; 341 } 342 } 343 344 BMCWEB_LOG_INFO << "Request: " 345 << " " << this << " HTTP/" << thisReq.version() / 10 346 << "." << thisReq.version() % 10 << ' ' 347 << thisReq.methodString() << " " << thisReq.target() 348 << " " << thisReq.ipAddress.to_string(); 349 350 res.isAliveHelper = [this]() -> bool { return isAlive(); }; 351 352 thisReq.ioService = static_cast<decltype(thisReq.ioService)>( 353 &adaptor.get_executor().context()); 354 355 if (res.completed) 356 { 357 completeRequest(res); 358 return; 359 } 360 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 361 if (!crow::authentication::isOnAllowlist(req->url, req->method()) && 362 thisReq.session == nullptr) 363 { 364 BMCWEB_LOG_WARNING << "Authentication failed"; 365 forward_unauthorized::sendUnauthorized( 366 req->url, req->getHeaderValue("X-Requested-With"), 367 req->getHeaderValue("Accept"), res); 368 completeRequest(res); 369 return; 370 } 371 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 372 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 373 BMCWEB_LOG_DEBUG << "Setting completion handler"; 374 asyncResp->res.setCompleteRequestHandler( 375 [self(shared_from_this())](crow::Response& thisRes) { 376 self->completeRequest(thisRes); 377 }); 378 379 if (thisReq.isUpgrade() && 380 boost::iequals( 381 thisReq.getHeaderValue(boost::beast::http::field::upgrade), 382 "websocket")) 383 { 384 handler->handleUpgrade(thisReq, res, std::move(adaptor)); 385 // delete lambda with self shared_ptr 386 // to enable connection destruction 387 asyncResp->res.setCompleteRequestHandler(nullptr); 388 return; 389 } 390 std::string_view expected = 391 req->getHeaderValue(boost::beast::http::field::if_none_match); 392 if (!expected.empty()) 393 { 394 res.setExpectedHash(expected); 395 } 396 handler->handle(thisReq, asyncResp); 397 } 398 399 bool isAlive() 400 { 401 if constexpr (std::is_same_v<Adaptor, 402 boost::beast::ssl_stream< 403 boost::asio::ip::tcp::socket>>) 404 { 405 return adaptor.next_layer().is_open(); 406 } 407 else 408 { 409 return adaptor.is_open(); 410 } 411 } 412 void close() 413 { 414 if constexpr (std::is_same_v<Adaptor, 415 boost::beast::ssl_stream< 416 boost::asio::ip::tcp::socket>>) 417 { 418 adaptor.next_layer().close(); 419 if (sessionIsFromTransport && userSession != nullptr) 420 { 421 BMCWEB_LOG_DEBUG 422 << this 423 << " Removing TLS session: " << userSession->uniqueId; 424 persistent_data::SessionStore::getInstance().removeSession( 425 userSession); 426 } 427 } 428 else 429 { 430 adaptor.close(); 431 } 432 } 433 434 void completeRequest(crow::Response& thisRes) 435 { 436 if (!req) 437 { 438 return; 439 } 440 res = std::move(thisRes); 441 BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' ' 442 << res.resultInt() << " keepalive=" << req->keepAlive(); 443 444 addSecurityHeaders(*req, res); 445 446 crow::authentication::cleanupTempSession(*req); 447 448 if (!isAlive()) 449 { 450 // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " << 451 // isReading 452 // << ' ' << isWriting; 453 // delete this; 454 455 // delete lambda with self shared_ptr 456 // to enable connection destruction 457 res.setCompleteRequestHandler(nullptr); 458 return; 459 } 460 461 res.setHashAndHandleNotModified(); 462 463 if (res.body().empty() && !res.jsonValue.empty()) 464 { 465 using http_helpers::ContentType; 466 std::array<ContentType, 2> allowed{ContentType::JSON, 467 ContentType::HTML}; 468 ContentType prefered = 469 getPreferedContentType(req->getHeaderValue("Accept"), allowed); 470 471 if (prefered == ContentType::HTML) 472 { 473 prettyPrintJson(res); 474 } 475 else 476 { 477 // Technically prefered could also be NoMatch here, but we'd 478 // like to default to something rather than return 400 for 479 // backward compatibility. 480 res.addHeader(boost::beast::http::field::content_type, 481 "application/json"); 482 res.body() = res.jsonValue.dump( 483 2, ' ', true, nlohmann::json::error_handler_t::replace); 484 } 485 } 486 487 if (res.resultInt() >= 400 && res.body().empty()) 488 { 489 res.body() = std::string(res.reason()); 490 } 491 492 if (res.result() == boost::beast::http::status::no_content) 493 { 494 // Boost beast throws if content is provided on a no-content 495 // response. Ideally, this would never happen, but in the case that 496 // it does, we don't want to throw. 497 BMCWEB_LOG_CRITICAL 498 << this << " Response content provided but code was no-content"; 499 res.body().clear(); 500 } 501 502 res.addHeader(boost::beast::http::field::date, getCachedDateStr()); 503 504 res.keepAlive(req->keepAlive()); 505 506 doWrite(res); 507 508 // delete lambda with self shared_ptr 509 // to enable connection destruction 510 res.setCompleteRequestHandler(nullptr); 511 } 512 513 void readClientIp() 514 { 515 boost::asio::ip::address ip; 516 boost::system::error_code ec = getClientIp(ip); 517 if (ec) 518 { 519 return; 520 } 521 req->ipAddress = ip; 522 } 523 524 boost::system::error_code getClientIp(boost::asio::ip::address& ip) 525 { 526 boost::system::error_code ec; 527 BMCWEB_LOG_DEBUG << "Fetch the client IP address"; 528 boost::asio::ip::tcp::endpoint endpoint = 529 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec); 530 531 if (ec) 532 { 533 // If remote endpoint fails keep going. "ClientOriginIPAddress" 534 // will be empty. 535 BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " 536 << ec; 537 return ec; 538 } 539 ip = endpoint.address(); 540 return ec; 541 } 542 543 private: 544 void doReadHeaders() 545 { 546 BMCWEB_LOG_DEBUG << this << " doReadHeaders"; 547 548 // Clean up any previous Connection. 549 boost::beast::http::async_read_header( 550 adaptor, buffer, *parser, 551 [this, 552 self(shared_from_this())](const boost::system::error_code& ec, 553 std::size_t bytesTransferred) { 554 BMCWEB_LOG_DEBUG << this << " async_read_header " 555 << bytesTransferred << " Bytes"; 556 bool errorWhileReading = false; 557 if (ec) 558 { 559 errorWhileReading = true; 560 if (ec == boost::asio::error::eof) 561 { 562 BMCWEB_LOG_WARNING 563 << this << " Error while reading: " << ec.message(); 564 } 565 else 566 { 567 BMCWEB_LOG_ERROR 568 << this << " Error while reading: " << ec.message(); 569 } 570 } 571 else 572 { 573 // if the adaptor isn't open anymore, and wasn't handed to a 574 // websocket, treat as an error 575 if (!isAlive() && 576 !boost::beast::websocket::is_upgrade(parser->get())) 577 { 578 errorWhileReading = true; 579 } 580 } 581 582 cancelDeadlineTimer(); 583 584 if (errorWhileReading) 585 { 586 close(); 587 BMCWEB_LOG_DEBUG << this << " from read(1)"; 588 return; 589 } 590 591 readClientIp(); 592 593 boost::asio::ip::address ip; 594 if (getClientIp(ip)) 595 { 596 BMCWEB_LOG_DEBUG << "Unable to get client IP"; 597 } 598 sessionIsFromTransport = false; 599 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 600 boost::beast::http::verb method = parser->get().method(); 601 userSession = crow::authentication::authenticate( 602 ip, res, method, parser->get().base(), userSession); 603 604 bool loggedIn = userSession != nullptr; 605 if (!loggedIn) 606 { 607 const boost::optional<uint64_t> contentLength = 608 parser->content_length(); 609 if (contentLength && *contentLength > loggedOutPostBodyLimit) 610 { 611 BMCWEB_LOG_DEBUG << "Content length greater than limit " 612 << *contentLength; 613 close(); 614 return; 615 } 616 617 BMCWEB_LOG_DEBUG << "Starting quick deadline"; 618 } 619 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 620 621 doRead(); 622 }); 623 } 624 625 void doRead() 626 { 627 BMCWEB_LOG_DEBUG << this << " doRead"; 628 startDeadline(); 629 boost::beast::http::async_read(adaptor, buffer, *parser, 630 [this, self(shared_from_this())]( 631 const boost::system::error_code& ec, 632 std::size_t bytesTransferred) { 633 BMCWEB_LOG_DEBUG << this << " async_read " << bytesTransferred 634 << " Bytes"; 635 cancelDeadlineTimer(); 636 if (ec) 637 { 638 BMCWEB_LOG_ERROR << this 639 << " Error while reading: " << ec.message(); 640 close(); 641 BMCWEB_LOG_DEBUG << this << " from read(1)"; 642 return; 643 } 644 handle(); 645 }); 646 } 647 648 void doWrite(crow::Response& thisRes) 649 { 650 BMCWEB_LOG_DEBUG << this << " doWrite"; 651 thisRes.preparePayload(); 652 serializer.emplace(*thisRes.stringResponse); 653 startDeadline(); 654 boost::beast::http::async_write(adaptor, *serializer, 655 [this, self(shared_from_this())]( 656 const boost::system::error_code& ec, 657 std::size_t bytesTransferred) { 658 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred 659 << " bytes"; 660 661 cancelDeadlineTimer(); 662 663 if (ec) 664 { 665 BMCWEB_LOG_DEBUG << this << " from write(2)"; 666 return; 667 } 668 if (!res.keepAlive()) 669 { 670 close(); 671 BMCWEB_LOG_DEBUG << this << " from write(1)"; 672 return; 673 } 674 675 serializer.reset(); 676 BMCWEB_LOG_DEBUG << this << " Clearing response"; 677 res.clear(); 678 parser.emplace(std::piecewise_construct, std::make_tuple()); 679 parser->body_limit(httpReqBodyLimit); // reset body limit for 680 // newly created parser 681 buffer.consume(buffer.size()); 682 683 // If the session was built from the transport, we don't need to 684 // clear it. All other sessions are generated per request. 685 if (!sessionIsFromTransport) 686 { 687 userSession = nullptr; 688 } 689 690 // Destroy the Request via the std::optional 691 req.reset(); 692 doReadHeaders(); 693 }); 694 } 695 696 void cancelDeadlineTimer() 697 { 698 timer.cancel(); 699 } 700 701 void startDeadline() 702 { 703 cancelDeadlineTimer(); 704 705 std::chrono::seconds timeout(15); 706 // allow slow uploads for logged in users 707 bool loggedIn = userSession != nullptr; 708 if (loggedIn) 709 { 710 timeout = std::chrono::seconds(60); 711 return; 712 } 713 714 std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this(); 715 timer.expires_after(timeout); 716 timer.async_wait([weakSelf](const boost::system::error_code ec) { 717 // Note, we are ignoring other types of errors here; If the timer 718 // failed for any reason, we should still close the connection 719 720 std::shared_ptr<Connection<Adaptor, Handler>> self = 721 weakSelf.lock(); 722 if (!self) 723 { 724 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 725 return; 726 } 727 if (ec == boost::asio::error::operation_aborted) 728 { 729 // Canceled wait means the path succeeeded. 730 return; 731 } 732 if (ec) 733 { 734 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec; 735 } 736 737 BMCWEB_LOG_WARNING << self << "Connection timed out, closing"; 738 739 self->close(); 740 }); 741 742 BMCWEB_LOG_DEBUG << this << " timer started"; 743 } 744 745 Adaptor adaptor; 746 Handler* handler; 747 // Making this a std::optional allows it to be efficiently destroyed and 748 // re-created on Connection reset 749 std::optional< 750 boost::beast::http::request_parser<boost::beast::http::string_body>> 751 parser; 752 753 boost::beast::flat_static_buffer<8192> buffer; 754 755 std::optional<boost::beast::http::response_serializer< 756 boost::beast::http::string_body>> 757 serializer; 758 759 std::optional<crow::Request> req; 760 crow::Response res; 761 762 bool sessionIsFromTransport = false; 763 std::shared_ptr<persistent_data::UserSession> userSession; 764 765 boost::asio::steady_timer timer; 766 767 std::function<std::string()>& getCachedDateStr; 768 769 using std::enable_shared_from_this< 770 Connection<Adaptor, Handler>>::shared_from_this; 771 772 using std::enable_shared_from_this< 773 Connection<Adaptor, Handler>>::weak_from_this; 774 }; 775 } // namespace crow 776