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 73 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 74 prepareMutualTls(); 75 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 76 77 #ifdef BMCWEB_ENABLE_DEBUG 78 connectionCount++; 79 BMCWEB_LOG_DEBUG << this << " Connection open, total " 80 << connectionCount; 81 #endif 82 } 83 84 ~Connection() 85 { 86 res.setCompleteRequestHandler(nullptr); 87 cancelDeadlineTimer(); 88 #ifdef BMCWEB_ENABLE_DEBUG 89 connectionCount--; 90 BMCWEB_LOG_DEBUG << this << " Connection closed, total " 91 << connectionCount; 92 #endif 93 } 94 95 void prepareMutualTls() 96 { 97 std::error_code error; 98 std::filesystem::path caPath(ensuressl::trustStorePath); 99 auto caAvailable = !std::filesystem::is_empty(caPath, error); 100 caAvailable = caAvailable && !error; 101 if (caAvailable && persistent_data::SessionStore::getInstance() 102 .getAuthMethodsConfig() 103 .tls) 104 { 105 adaptor.set_verify_mode(boost::asio::ssl::verify_peer); 106 std::string id = "bmcweb"; 107 int ret = SSL_set_session_id_context( 108 adaptor.native_handle(), 109 reinterpret_cast<const unsigned char*>(id.c_str()), 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([this]( 118 bool preverified, 119 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 error = X509_STORE_CTX_get_error(cts); 155 if (error != X509_V_OK) 156 { 157 BMCWEB_LOG_INFO << this << " Last TLS error is: " << error; 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 if (KU_DIGITAL_SIGNATURE & usage->data[i]) 190 { 191 isKeyUsageDigitalSignature = true; 192 } 193 if (KU_KEY_AGREEMENT & usage->data[i]) 194 { 195 isKeyUsageKeyAgreement = true; 196 } 197 } 198 ASN1_BIT_STRING_free(usage); 199 200 if (!isKeyUsageDigitalSignature || !isKeyUsageKeyAgreement) 201 { 202 BMCWEB_LOG_DEBUG << this 203 << " Certificate ExtendedKeyUsage does " 204 "not allow provided certificate to " 205 "be used for user authentication"; 206 return true; 207 } 208 209 // Determine that ExtendedKeyUsage includes Client Auth 210 211 stack_st_ASN1_OBJECT* extUsage = 212 static_cast<stack_st_ASN1_OBJECT*>(X509_get_ext_d2i( 213 peerCert, NID_ext_key_usage, nullptr, nullptr)); 214 215 if (extUsage == nullptr) 216 { 217 BMCWEB_LOG_DEBUG << this << " TLS extUsage is null"; 218 return true; 219 } 220 221 bool isExKeyUsageClientAuth = false; 222 for (int i = 0; i < sk_ASN1_OBJECT_num(extUsage); i++) 223 { 224 if (NID_client_auth == 225 OBJ_obj2nid(sk_ASN1_OBJECT_value(extUsage, i))) 226 { 227 isExKeyUsageClientAuth = true; 228 break; 229 } 230 } 231 sk_ASN1_OBJECT_free(extUsage); 232 233 // Certificate has to have proper key usages set 234 if (!isExKeyUsageClientAuth) 235 { 236 BMCWEB_LOG_DEBUG << this 237 << " Certificate ExtendedKeyUsage does " 238 "not allow provided certificate to " 239 "be used for user authentication"; 240 return true; 241 } 242 std::string sslUser; 243 // Extract username contained in CommonName 244 sslUser.resize(256, '\0'); 245 246 int status = X509_NAME_get_text_by_NID( 247 X509_get_subject_name(peerCert), NID_commonName, sslUser.data(), 248 static_cast<int>(sslUser.size())); 249 250 if (status == -1) 251 { 252 BMCWEB_LOG_DEBUG 253 << this << " TLS cannot get username to create session"; 254 return true; 255 } 256 257 size_t lastChar = sslUser.find('\0'); 258 if (lastChar == std::string::npos || lastChar == 0) 259 { 260 BMCWEB_LOG_DEBUG << this << " Invalid TLS user name"; 261 return true; 262 } 263 sslUser.resize(lastChar); 264 std::string unsupportedClientId = ""; 265 sessionIsFromTransport = true; 266 userSession = persistent_data::SessionStore::getInstance() 267 .generateUserSession( 268 sslUser, req->ipAddress.to_string(), 269 unsupportedClientId, 270 persistent_data::PersistenceType::TIMEOUT); 271 if (userSession != nullptr) 272 { 273 BMCWEB_LOG_DEBUG 274 << this 275 << " Generating TLS session: " << userSession->uniqueId; 276 } 277 return true; 278 }); 279 } 280 281 Adaptor& socket() 282 { 283 return adaptor; 284 } 285 286 void start() 287 { 288 289 startDeadline(0); 290 291 // TODO(ed) Abstract this to a more clever class with the idea of an 292 // asynchronous "start" 293 if constexpr (std::is_same_v<Adaptor, 294 boost::beast::ssl_stream< 295 boost::asio::ip::tcp::socket>>) 296 { 297 adaptor.async_handshake(boost::asio::ssl::stream_base::server, 298 [this, self(shared_from_this())]( 299 const boost::system::error_code& ec) { 300 if (ec) 301 { 302 return; 303 } 304 doReadHeaders(); 305 }); 306 } 307 else 308 { 309 doReadHeaders(); 310 } 311 } 312 313 void handle() 314 { 315 cancelDeadlineTimer(); 316 317 crow::Request& thisReq = req.emplace(parser->release()); 318 thisReq.session = userSession; 319 320 // Fetch the client IP address 321 readClientIp(); 322 323 // Check for HTTP version 1.1. 324 if (thisReq.version() == 11) 325 { 326 if (thisReq.getHeaderValue(boost::beast::http::field::host).empty()) 327 { 328 res.result(boost::beast::http::status::bad_request); 329 completeRequest(); 330 return; 331 } 332 } 333 334 BMCWEB_LOG_INFO << "Request: " 335 << " " << this << " HTTP/" << thisReq.version() / 10 336 << "." << thisReq.version() % 10 << ' ' 337 << thisReq.methodString() << " " << thisReq.target() 338 << " " << thisReq.ipAddress; 339 try 340 { 341 thisReq.urlView = boost::urls::url_view(thisReq.target()); 342 thisReq.url = thisReq.urlView.encoded_path(); 343 } 344 catch (std::exception& p) 345 { 346 BMCWEB_LOG_ERROR << p.what(); 347 } 348 349 res.setCompleteRequestHandler(nullptr); 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(); 358 return; 359 } 360 res.setCompleteRequestHandler([self(shared_from_this())] { 361 boost::asio::post(self->adaptor.get_executor(), 362 [self] { self->completeRequest(); }); 363 }); 364 365 if (thisReq.isUpgrade() && 366 boost::iequals( 367 thisReq.getHeaderValue(boost::beast::http::field::upgrade), 368 "websocket")) 369 { 370 handler->handleUpgrade(thisReq, res, std::move(adaptor)); 371 // delete lambda with self shared_ptr 372 // to enable connection destruction 373 res.setCompleteRequestHandler(nullptr); 374 return; 375 } 376 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(res); 377 handler->handle(thisReq, asyncResp); 378 } 379 380 bool isAlive() 381 { 382 if constexpr (std::is_same_v<Adaptor, 383 boost::beast::ssl_stream< 384 boost::asio::ip::tcp::socket>>) 385 { 386 return adaptor.next_layer().is_open(); 387 } 388 else 389 { 390 return adaptor.is_open(); 391 } 392 } 393 void close() 394 { 395 if constexpr (std::is_same_v<Adaptor, 396 boost::beast::ssl_stream< 397 boost::asio::ip::tcp::socket>>) 398 { 399 adaptor.next_layer().close(); 400 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 401 if (userSession != nullptr) 402 { 403 BMCWEB_LOG_DEBUG 404 << this 405 << " Removing TLS session: " << userSession->uniqueId; 406 persistent_data::SessionStore::getInstance().removeSession( 407 userSession); 408 } 409 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION 410 } 411 else 412 { 413 adaptor.close(); 414 } 415 } 416 417 void completeRequest() 418 { 419 BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' ' 420 << res.resultInt() << " keepalive=" << req->keepAlive(); 421 422 addSecurityHeaders(*req, res); 423 424 crow::authorization::cleanupTempSession(*req); 425 426 if (!isAlive()) 427 { 428 // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " << 429 // isReading 430 // << ' ' << isWriting; 431 // delete this; 432 433 // delete lambda with self shared_ptr 434 // to enable connection destruction 435 res.setCompleteRequestHandler(nullptr); 436 return; 437 } 438 if (res.body().empty() && !res.jsonValue.empty()) 439 { 440 if (http_helpers::requestPrefersHtml(req->getHeaderValue("Accept"))) 441 { 442 prettyPrintJson(res); 443 } 444 else 445 { 446 res.jsonMode(); 447 res.body() = res.jsonValue.dump( 448 2, ' ', true, nlohmann::json::error_handler_t::replace); 449 } 450 } 451 452 if (res.resultInt() >= 400 && res.body().empty()) 453 { 454 res.body() = std::string(res.reason()); 455 } 456 457 if (res.result() == boost::beast::http::status::no_content) 458 { 459 // Boost beast throws if content is provided on a no-content 460 // response. Ideally, this would never happen, but in the case that 461 // it does, we don't want to throw. 462 BMCWEB_LOG_CRITICAL 463 << this << " Response content provided but code was no-content"; 464 res.body().clear(); 465 } 466 467 res.addHeader(boost::beast::http::field::date, getCachedDateStr()); 468 469 res.keepAlive(req->keepAlive()); 470 471 doWrite(); 472 473 // delete lambda with self shared_ptr 474 // to enable connection destruction 475 res.setCompleteRequestHandler(nullptr); 476 } 477 478 void readClientIp() 479 { 480 boost::asio::ip::address ip; 481 boost::system::error_code ec = getClientIp(ip); 482 if (ec) 483 { 484 return; 485 } 486 req->ipAddress = ip; 487 } 488 489 boost::system::error_code getClientIp(boost::asio::ip::address& ip) 490 { 491 boost::system::error_code ec; 492 BMCWEB_LOG_DEBUG << "Fetch the client IP address"; 493 boost::asio::ip::tcp::endpoint endpoint = 494 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec); 495 496 if (ec) 497 { 498 // If remote endpoint fails keep going. "ClientOriginIPAddress" 499 // will be empty. 500 BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " 501 << ec; 502 return ec; 503 } 504 ip = endpoint.address(); 505 return ec; 506 } 507 508 private: 509 void doReadHeaders() 510 { 511 BMCWEB_LOG_DEBUG << this << " doReadHeaders"; 512 513 // Clean up any previous Connection. 514 boost::beast::http::async_read_header( 515 adaptor, buffer, *parser, 516 [this, 517 self(shared_from_this())](const boost::system::error_code& ec, 518 std::size_t bytesTransferred) { 519 BMCWEB_LOG_ERROR << this << " async_read_header " 520 << bytesTransferred << " Bytes"; 521 bool errorWhileReading = false; 522 if (ec) 523 { 524 errorWhileReading = true; 525 BMCWEB_LOG_ERROR 526 << this << " Error while reading: " << ec.message(); 527 } 528 else 529 { 530 // if the adaptor isn't open anymore, and wasn't handed to a 531 // websocket, treat as an error 532 if (!isAlive() && 533 !boost::beast::websocket::is_upgrade(parser->get())) 534 { 535 errorWhileReading = true; 536 } 537 } 538 539 cancelDeadlineTimer(); 540 541 if (errorWhileReading) 542 { 543 close(); 544 BMCWEB_LOG_DEBUG << this << " from read(1)"; 545 return; 546 } 547 548 boost::beast::http::verb method = parser->get().method(); 549 readClientIp(); 550 try 551 { 552 req->urlView = 553 boost::urls::url_view(parser->get().target()); 554 req->url = req->urlView.encoded_path(); 555 } 556 catch (std::exception& p) 557 { 558 BMCWEB_LOG_ERROR << p.what(); 559 } 560 561 boost::asio::ip::address ip; 562 if (getClientIp(ip)) 563 { 564 BMCWEB_LOG_DEBUG << "Unable to get client IP"; 565 } 566 sessionIsFromTransport = false; 567 userSession = crow::authorization::authenticate( 568 req->url, ip, res, method, parser->get().base(), 569 userSession); 570 bool loggedIn = userSession != nullptr; 571 if (loggedIn) 572 { 573 startDeadline(loggedInAttempts); 574 BMCWEB_LOG_DEBUG << "Starting slow deadline"; 575 576 req->urlParams = req->urlView.params(); 577 578 #ifdef BMCWEB_ENABLE_DEBUG 579 std::string paramList = ""; 580 for (const auto param : req->urlParams) 581 { 582 paramList += param->key() + " " + param->value() + " "; 583 } 584 BMCWEB_LOG_DEBUG << "QueryParams: " << paramList; 585 #endif 586 } 587 else 588 { 589 const boost::optional<uint64_t> contentLength = 590 parser->content_length(); 591 if (contentLength && 592 *contentLength > loggedOutPostBodyLimit) 593 { 594 BMCWEB_LOG_DEBUG << "Content length greater than limit " 595 << *contentLength; 596 close(); 597 return; 598 } 599 600 startDeadline(loggedOutAttempts); 601 BMCWEB_LOG_DEBUG << "Starting quick deadline"; 602 } 603 doRead(); 604 }); 605 } 606 607 void doRead() 608 { 609 BMCWEB_LOG_DEBUG << this << " doRead"; 610 611 boost::beast::http::async_read( 612 adaptor, buffer, *parser, 613 [this, 614 self(shared_from_this())](const boost::system::error_code& ec, 615 std::size_t bytesTransferred) { 616 BMCWEB_LOG_DEBUG << this << " async_read " << bytesTransferred 617 << " Bytes"; 618 619 bool errorWhileReading = false; 620 if (ec) 621 { 622 BMCWEB_LOG_ERROR 623 << this << " Error while reading: " << ec.message(); 624 errorWhileReading = true; 625 } 626 else 627 { 628 if (isAlive()) 629 { 630 cancelDeadlineTimer(); 631 if (userSession != nullptr) 632 { 633 startDeadline(loggedInAttempts); 634 } 635 else 636 { 637 startDeadline(loggedOutAttempts); 638 } 639 } 640 else 641 { 642 errorWhileReading = true; 643 } 644 } 645 if (errorWhileReading) 646 { 647 cancelDeadlineTimer(); 648 close(); 649 BMCWEB_LOG_DEBUG << this << " from read(1)"; 650 return; 651 } 652 handle(); 653 }); 654 } 655 656 void doWrite() 657 { 658 bool loggedIn = req && req->session; 659 if (loggedIn) 660 { 661 startDeadline(loggedInAttempts); 662 } 663 else 664 { 665 startDeadline(loggedOutAttempts); 666 } 667 BMCWEB_LOG_DEBUG << this << " doWrite"; 668 res.preparePayload(); 669 serializer.emplace(*res.stringResponse); 670 boost::beast::http::async_write( 671 adaptor, *serializer, 672 [this, 673 self(shared_from_this())](const boost::system::error_code& ec, 674 std::size_t bytesTransferred) { 675 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred 676 << " bytes"; 677 678 cancelDeadlineTimer(); 679 680 if (ec) 681 { 682 BMCWEB_LOG_DEBUG << this << " from write(2)"; 683 return; 684 } 685 if (!res.keepAlive()) 686 { 687 close(); 688 BMCWEB_LOG_DEBUG << this << " from write(1)"; 689 return; 690 } 691 692 serializer.reset(); 693 BMCWEB_LOG_DEBUG << this << " Clearing response"; 694 res.clear(); 695 parser.emplace(std::piecewise_construct, std::make_tuple()); 696 parser->body_limit(httpReqBodyLimit); // reset body limit for 697 // newly created parser 698 buffer.consume(buffer.size()); 699 700 // If the session was built from the transport, we don't need to 701 // clear it. All other sessions are generated per request. 702 if (!sessionIsFromTransport) 703 { 704 userSession = nullptr; 705 } 706 707 req.emplace(parser->release()); 708 doReadHeaders(); 709 }); 710 } 711 712 void cancelDeadlineTimer() 713 { 714 if (timerCancelKey) 715 { 716 BMCWEB_LOG_DEBUG << this << " timer cancelled: " << &timerQueue 717 << ' ' << *timerCancelKey; 718 timerQueue.cancel(*timerCancelKey); 719 timerCancelKey.reset(); 720 } 721 } 722 723 void startDeadline(size_t timerIterations) 724 { 725 cancelDeadlineTimer(); 726 727 if (timerIterations) 728 { 729 timerIterations--; 730 } 731 732 timerCancelKey = 733 timerQueue.add([self(shared_from_this()), timerIterations, 734 readCount{parser->get().body().size()}] { 735 // Mark timer as not active to avoid canceling it during 736 // Connection destructor which leads to double free issue 737 self->timerCancelKey.reset(); 738 if (!self->isAlive()) 739 { 740 return; 741 } 742 743 bool loggedIn = self->req && self->req->session; 744 // allow slow uploads for logged in users 745 if (loggedIn && self->parser->get().body().size() > readCount) 746 { 747 BMCWEB_LOG_DEBUG << self.get() 748 << " restart timer - read in progress"; 749 self->startDeadline(timerIterations); 750 return; 751 } 752 753 // Threshold can be used to drop slow connections 754 // to protect against slow-rate DoS attack 755 if (timerIterations) 756 { 757 BMCWEB_LOG_DEBUG << self.get() << " restart timer"; 758 self->startDeadline(timerIterations); 759 return; 760 } 761 762 self->close(); 763 }); 764 765 if (!timerCancelKey) 766 { 767 close(); 768 return; 769 } 770 BMCWEB_LOG_DEBUG << this << " timer added: " << &timerQueue << ' ' 771 << *timerCancelKey; 772 } 773 774 private: 775 Adaptor adaptor; 776 Handler* handler; 777 // Making this a std::optional allows it to be efficiently destroyed and 778 // re-created on Connection reset 779 std::optional< 780 boost::beast::http::request_parser<boost::beast::http::string_body>> 781 parser; 782 783 boost::beast::flat_static_buffer<8192> buffer; 784 785 std::optional<boost::beast::http::response_serializer< 786 boost::beast::http::string_body>> 787 serializer; 788 789 std::optional<crow::Request> req; 790 crow::Response res; 791 792 bool sessionIsFromTransport = false; 793 std::shared_ptr<persistent_data::UserSession> userSession; 794 795 std::optional<size_t> timerCancelKey; 796 797 std::function<std::string()>& getCachedDateStr; 798 detail::TimerQueue& timerQueue; 799 800 using std::enable_shared_from_this< 801 Connection<Adaptor, Handler>>::shared_from_this; 802 }; 803 } // namespace crow 804