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("Content-Type", "text/html;charset=UTF-8"); 39 } 40 41 static int connectionCount = 0; 42 43 // request body limit size set by the bmcwebHttpReqBodyLimitMb option 44 constexpr uint64_t httpReqBodyLimit = 45 1024UL * 1024UL * bmcwebHttpReqBodyLimitMb; 46 47 constexpr uint64_t loggedOutPostBodyLimit = 4096; 48 49 constexpr uint32_t httpHeaderLimit = 8192; 50 51 template <typename Adaptor, typename Handler> 52 class Connection : 53 public std::enable_shared_from_this<Connection<Adaptor, Handler>> 54 { 55 public: 56 Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn, 57 std::function<std::string()>& getCachedDateStrF, 58 Adaptor adaptorIn) : 59 adaptor(std::move(adaptorIn)), 60 handler(handlerIn), timer(std::move(timerIn)), 61 getCachedDateStr(getCachedDateStrF) 62 { 63 parser.emplace(std::piecewise_construct, std::make_tuple()); 64 parser->body_limit(httpReqBodyLimit); 65 parser->header_limit(httpHeaderLimit); 66 67 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 68 prepareMutualTls(); 69 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 70 71 connectionCount++; 72 73 BMCWEB_LOG_DEBUG << this << " Connection open, total " 74 << connectionCount; 75 } 76 77 ~Connection() 78 { 79 res.setCompleteRequestHandler(nullptr); 80 cancelDeadlineTimer(); 81 82 connectionCount--; 83 BMCWEB_LOG_DEBUG << this << " Connection closed, total " 84 << connectionCount; 85 } 86 87 Connection(const Connection&) = delete; 88 Connection(Connection&&) = delete; 89 Connection& operator=(const Connection&) = delete; 90 Connection& operator=(Connection&&) = delete; 91 92 void prepareMutualTls() 93 { 94 std::error_code error; 95 std::filesystem::path caPath(ensuressl::trustStorePath); 96 auto caAvailable = !std::filesystem::is_empty(caPath, error); 97 caAvailable = caAvailable && !error; 98 if (caAvailable && persistent_data::SessionStore::getInstance() 99 .getAuthMethodsConfig() 100 .tls) 101 { 102 adaptor.set_verify_mode(boost::asio::ssl::verify_peer); 103 std::string id = "bmcweb"; 104 105 const char* cStr = id.c_str(); 106 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 107 const auto* idC = reinterpret_cast<const unsigned char*>(cStr); 108 int ret = SSL_set_session_id_context( 109 adaptor.native_handle(), idC, 110 static_cast<unsigned int>(id.length())); 111 if (ret == 0) 112 { 113 BMCWEB_LOG_ERROR << this << " failed to set SSL id"; 114 } 115 } 116 117 adaptor.set_verify_callback( 118 [this](bool preverified, boost::asio::ssl::verify_context& ctx) { 119 // do nothing if TLS is disabled 120 if (!persistent_data::SessionStore::getInstance() 121 .getAuthMethodsConfig() 122 .tls) 123 { 124 BMCWEB_LOG_DEBUG << this << " TLS auth_config is disabled"; 125 return true; 126 } 127 128 // We always return true to allow full auth flow 129 if (!preverified) 130 { 131 BMCWEB_LOG_DEBUG << this << " TLS preverification failed."; 132 return true; 133 } 134 135 X509_STORE_CTX* cts = ctx.native_handle(); 136 if (cts == nullptr) 137 { 138 BMCWEB_LOG_DEBUG << this << " Cannot get native TLS handle."; 139 return true; 140 } 141 142 // Get certificate 143 X509* peerCert = 144 X509_STORE_CTX_get_current_cert(ctx.native_handle()); 145 if (peerCert == nullptr) 146 { 147 BMCWEB_LOG_DEBUG << this 148 << " Cannot get current TLS certificate."; 149 return true; 150 } 151 152 // Check if certificate is OK 153 int ctxError = X509_STORE_CTX_get_error(cts); 154 if (ctxError != X509_V_OK) 155 { 156 BMCWEB_LOG_INFO << this << " Last TLS error is: " << ctxError; 157 return true; 158 } 159 // Check that we have reached final certificate in chain 160 int32_t depth = X509_STORE_CTX_get_error_depth(cts); 161 if (depth != 0) 162 163 { 164 BMCWEB_LOG_DEBUG 165 << this << " Certificate verification in progress (depth " 166 << depth << "), waiting to reach final depth"; 167 return true; 168 } 169 170 BMCWEB_LOG_DEBUG << this 171 << " Certificate verification of final depth"; 172 173 // Verify KeyUsage 174 bool isKeyUsageDigitalSignature = false; 175 bool isKeyUsageKeyAgreement = false; 176 177 ASN1_BIT_STRING* usage = static_cast<ASN1_BIT_STRING*>( 178 X509_get_ext_d2i(peerCert, NID_key_usage, nullptr, nullptr)); 179 180 if (usage == nullptr) 181 { 182 BMCWEB_LOG_DEBUG << this << " TLS usage is null"; 183 return true; 184 } 185 186 for (int i = 0; i < usage->length; i++) 187 { 188 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) 189 unsigned char usageChar = usage->data[i]; 190 if (KU_DIGITAL_SIGNATURE & usageChar) 191 { 192 isKeyUsageDigitalSignature = true; 193 } 194 if (KU_KEY_AGREEMENT & usageChar) 195 { 196 isKeyUsageKeyAgreement = true; 197 } 198 } 199 ASN1_BIT_STRING_free(usage); 200 201 if (!isKeyUsageDigitalSignature || !isKeyUsageKeyAgreement) 202 { 203 BMCWEB_LOG_DEBUG << this 204 << " Certificate ExtendedKeyUsage does " 205 "not allow provided certificate to " 206 "be used for user authentication"; 207 return true; 208 } 209 210 // Determine that ExtendedKeyUsage includes Client Auth 211 212 stack_st_ASN1_OBJECT* extUsage = 213 static_cast<stack_st_ASN1_OBJECT*>(X509_get_ext_d2i( 214 peerCert, NID_ext_key_usage, nullptr, nullptr)); 215 216 if (extUsage == nullptr) 217 { 218 BMCWEB_LOG_DEBUG << this << " TLS extUsage is null"; 219 return true; 220 } 221 222 bool isExKeyUsageClientAuth = false; 223 for (int i = 0; i < sk_ASN1_OBJECT_num(extUsage); i++) 224 { 225 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) 226 int nid = OBJ_obj2nid(sk_ASN1_OBJECT_value(extUsage, i)); 227 if (NID_client_auth == nid) 228 { 229 isExKeyUsageClientAuth = true; 230 break; 231 } 232 } 233 sk_ASN1_OBJECT_free(extUsage); 234 235 // Certificate has to have proper key usages set 236 if (!isExKeyUsageClientAuth) 237 { 238 BMCWEB_LOG_DEBUG << this 239 << " Certificate ExtendedKeyUsage does " 240 "not allow provided certificate to " 241 "be used for user authentication"; 242 return true; 243 } 244 std::string sslUser; 245 // Extract username contained in CommonName 246 sslUser.resize(256, '\0'); 247 248 int status = X509_NAME_get_text_by_NID( 249 X509_get_subject_name(peerCert), NID_commonName, sslUser.data(), 250 static_cast<int>(sslUser.size())); 251 252 if (status == -1) 253 { 254 BMCWEB_LOG_DEBUG 255 << this << " TLS cannot get username to create session"; 256 return true; 257 } 258 259 size_t lastChar = sslUser.find('\0'); 260 if (lastChar == std::string::npos || lastChar == 0) 261 { 262 BMCWEB_LOG_DEBUG << this << " Invalid TLS user name"; 263 return true; 264 } 265 sslUser.resize(lastChar); 266 std::string unsupportedClientId; 267 sessionIsFromTransport = true; 268 userSession = persistent_data::SessionStore::getInstance() 269 .generateUserSession( 270 sslUser, req->ipAddress, unsupportedClientId, 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 handler->handle(thisReq, asyncResp); 391 } 392 393 bool isAlive() 394 { 395 if constexpr (std::is_same_v<Adaptor, 396 boost::beast::ssl_stream< 397 boost::asio::ip::tcp::socket>>) 398 { 399 return adaptor.next_layer().is_open(); 400 } 401 else 402 { 403 return adaptor.is_open(); 404 } 405 } 406 void close() 407 { 408 if constexpr (std::is_same_v<Adaptor, 409 boost::beast::ssl_stream< 410 boost::asio::ip::tcp::socket>>) 411 { 412 adaptor.next_layer().close(); 413 if (sessionIsFromTransport && userSession != nullptr) 414 { 415 BMCWEB_LOG_DEBUG 416 << this 417 << " Removing TLS session: " << userSession->uniqueId; 418 persistent_data::SessionStore::getInstance().removeSession( 419 userSession); 420 } 421 } 422 else 423 { 424 adaptor.close(); 425 } 426 } 427 428 void completeRequest(crow::Response& thisRes) 429 { 430 if (!req) 431 { 432 return; 433 } 434 res = std::move(thisRes); 435 BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' ' 436 << res.resultInt() << " keepalive=" << req->keepAlive(); 437 438 addSecurityHeaders(*req, res); 439 440 crow::authentication::cleanupTempSession(*req); 441 442 if (!isAlive()) 443 { 444 // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " << 445 // isReading 446 // << ' ' << isWriting; 447 // delete this; 448 449 // delete lambda with self shared_ptr 450 // to enable connection destruction 451 res.setCompleteRequestHandler(nullptr); 452 return; 453 } 454 if (res.body().empty() && !res.jsonValue.empty()) 455 { 456 if (http_helpers::requestPrefersHtml(req->getHeaderValue("Accept"))) 457 { 458 prettyPrintJson(res); 459 } 460 else 461 { 462 res.jsonMode(); 463 res.body() = res.jsonValue.dump( 464 2, ' ', true, nlohmann::json::error_handler_t::replace); 465 } 466 } 467 468 if (res.resultInt() >= 400 && res.body().empty()) 469 { 470 res.body() = std::string(res.reason()); 471 } 472 473 if (res.result() == boost::beast::http::status::no_content) 474 { 475 // Boost beast throws if content is provided on a no-content 476 // response. Ideally, this would never happen, but in the case that 477 // it does, we don't want to throw. 478 BMCWEB_LOG_CRITICAL 479 << this << " Response content provided but code was no-content"; 480 res.body().clear(); 481 } 482 483 res.addHeader(boost::beast::http::field::date, getCachedDateStr()); 484 485 res.keepAlive(req->keepAlive()); 486 487 doWrite(res); 488 489 // delete lambda with self shared_ptr 490 // to enable connection destruction 491 res.setCompleteRequestHandler(nullptr); 492 } 493 494 void readClientIp() 495 { 496 boost::asio::ip::address ip; 497 boost::system::error_code ec = getClientIp(ip); 498 if (ec) 499 { 500 return; 501 } 502 req->ipAddress = ip; 503 } 504 505 boost::system::error_code getClientIp(boost::asio::ip::address& ip) 506 { 507 boost::system::error_code ec; 508 BMCWEB_LOG_DEBUG << "Fetch the client IP address"; 509 boost::asio::ip::tcp::endpoint endpoint = 510 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec); 511 512 if (ec) 513 { 514 // If remote endpoint fails keep going. "ClientOriginIPAddress" 515 // will be empty. 516 BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " 517 << ec; 518 return ec; 519 } 520 ip = endpoint.address(); 521 return ec; 522 } 523 524 private: 525 void doReadHeaders() 526 { 527 BMCWEB_LOG_DEBUG << this << " doReadHeaders"; 528 529 // Clean up any previous Connection. 530 boost::beast::http::async_read_header( 531 adaptor, buffer, *parser, 532 [this, 533 self(shared_from_this())](const boost::system::error_code& ec, 534 std::size_t bytesTransferred) { 535 BMCWEB_LOG_DEBUG << this << " async_read_header " 536 << bytesTransferred << " Bytes"; 537 bool errorWhileReading = false; 538 if (ec) 539 { 540 errorWhileReading = true; 541 if (ec == boost::asio::error::eof) 542 { 543 BMCWEB_LOG_WARNING 544 << this << " Error while reading: " << ec.message(); 545 } 546 else 547 { 548 BMCWEB_LOG_ERROR 549 << this << " Error while reading: " << ec.message(); 550 } 551 } 552 else 553 { 554 // if the adaptor isn't open anymore, and wasn't handed to a 555 // websocket, treat as an error 556 if (!isAlive() && 557 !boost::beast::websocket::is_upgrade(parser->get())) 558 { 559 errorWhileReading = true; 560 } 561 } 562 563 cancelDeadlineTimer(); 564 565 if (errorWhileReading) 566 { 567 close(); 568 BMCWEB_LOG_DEBUG << this << " from read(1)"; 569 return; 570 } 571 572 readClientIp(); 573 574 boost::asio::ip::address ip; 575 if (getClientIp(ip)) 576 { 577 BMCWEB_LOG_DEBUG << "Unable to get client IP"; 578 } 579 sessionIsFromTransport = false; 580 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 581 boost::beast::http::verb method = parser->get().method(); 582 userSession = crow::authentication::authenticate( 583 ip, res, method, parser->get().base(), userSession); 584 585 bool loggedIn = userSession != nullptr; 586 if (!loggedIn) 587 { 588 const boost::optional<uint64_t> contentLength = 589 parser->content_length(); 590 if (contentLength && *contentLength > loggedOutPostBodyLimit) 591 { 592 BMCWEB_LOG_DEBUG << "Content length greater than limit " 593 << *contentLength; 594 close(); 595 return; 596 } 597 598 BMCWEB_LOG_DEBUG << "Starting quick deadline"; 599 } 600 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 601 602 doRead(); 603 }); 604 } 605 606 void doRead() 607 { 608 BMCWEB_LOG_DEBUG << this << " doRead"; 609 startDeadline(); 610 boost::beast::http::async_read(adaptor, buffer, *parser, 611 [this, self(shared_from_this())]( 612 const boost::system::error_code& ec, 613 std::size_t bytesTransferred) { 614 BMCWEB_LOG_DEBUG << this << " async_read " << bytesTransferred 615 << " Bytes"; 616 cancelDeadlineTimer(); 617 if (ec) 618 { 619 BMCWEB_LOG_ERROR << this 620 << " Error while reading: " << ec.message(); 621 close(); 622 BMCWEB_LOG_DEBUG << this << " from read(1)"; 623 return; 624 } 625 handle(); 626 }); 627 } 628 629 void doWrite(crow::Response& thisRes) 630 { 631 BMCWEB_LOG_DEBUG << this << " doWrite"; 632 thisRes.preparePayload(); 633 serializer.emplace(*thisRes.stringResponse); 634 startDeadline(); 635 boost::beast::http::async_write(adaptor, *serializer, 636 [this, self(shared_from_this())]( 637 const boost::system::error_code& ec, 638 std::size_t bytesTransferred) { 639 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred 640 << " bytes"; 641 642 cancelDeadlineTimer(); 643 644 if (ec) 645 { 646 BMCWEB_LOG_DEBUG << this << " from write(2)"; 647 return; 648 } 649 if (!res.keepAlive()) 650 { 651 close(); 652 BMCWEB_LOG_DEBUG << this << " from write(1)"; 653 return; 654 } 655 656 serializer.reset(); 657 BMCWEB_LOG_DEBUG << this << " Clearing response"; 658 res.clear(); 659 parser.emplace(std::piecewise_construct, std::make_tuple()); 660 parser->body_limit(httpReqBodyLimit); // reset body limit for 661 // newly created parser 662 buffer.consume(buffer.size()); 663 664 // If the session was built from the transport, we don't need to 665 // clear it. All other sessions are generated per request. 666 if (!sessionIsFromTransport) 667 { 668 userSession = nullptr; 669 } 670 671 // Destroy the Request via the std::optional 672 req.reset(); 673 doReadHeaders(); 674 }); 675 } 676 677 void cancelDeadlineTimer() 678 { 679 timer.cancel(); 680 } 681 682 void startDeadline() 683 { 684 cancelDeadlineTimer(); 685 686 std::chrono::seconds timeout(15); 687 // allow slow uploads for logged in users 688 bool loggedIn = userSession != nullptr; 689 if (loggedIn) 690 { 691 timeout = std::chrono::seconds(60); 692 return; 693 } 694 695 std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this(); 696 timer.expires_after(timeout); 697 timer.async_wait([weakSelf](const boost::system::error_code ec) { 698 // Note, we are ignoring other types of errors here; If the timer 699 // failed for any reason, we should still close the connection 700 701 std::shared_ptr<Connection<Adaptor, Handler>> self = 702 weakSelf.lock(); 703 if (!self) 704 { 705 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 706 return; 707 } 708 if (ec == boost::asio::error::operation_aborted) 709 { 710 // Canceled wait means the path succeeeded. 711 return; 712 } 713 if (ec) 714 { 715 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec; 716 } 717 718 BMCWEB_LOG_WARNING << self << "Connection timed out, closing"; 719 720 self->close(); 721 }); 722 723 BMCWEB_LOG_DEBUG << this << " timer started"; 724 } 725 726 Adaptor adaptor; 727 Handler* handler; 728 // Making this a std::optional allows it to be efficiently destroyed and 729 // re-created on Connection reset 730 std::optional< 731 boost::beast::http::request_parser<boost::beast::http::string_body>> 732 parser; 733 734 boost::beast::flat_static_buffer<8192> buffer; 735 736 std::optional<boost::beast::http::response_serializer< 737 boost::beast::http::string_body>> 738 serializer; 739 740 std::optional<crow::Request> req; 741 crow::Response res; 742 743 bool sessionIsFromTransport = false; 744 std::shared_ptr<persistent_data::UserSession> userSession; 745 746 boost::asio::steady_timer timer; 747 748 std::function<std::string()>& getCachedDateStr; 749 750 using std::enable_shared_from_this< 751 Connection<Adaptor, Handler>>::shared_from_this; 752 753 using std::enable_shared_from_this< 754 Connection<Adaptor, Handler>>::weak_from_this; 755 }; 756 } // namespace crow 757