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