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