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