1 #pragma once 2 #include "bmcweb_config.h" 3 4 #include "async_resp.hpp" 5 #include "authentication.hpp" 6 #include "http_response.hpp" 7 #include "http_utility.hpp" 8 #include "json_html_serializer.hpp" 9 #include "logging.hpp" 10 #include "mutual_tls.hpp" 11 #include "security_headers.hpp" 12 #include "ssl_key_handler.hpp" 13 #include "utility.hpp" 14 15 #include <boost/algorithm/string/predicate.hpp> 16 #include <boost/asio/io_context.hpp> 17 #include <boost/asio/ip/tcp.hpp> 18 #include <boost/asio/ssl/stream.hpp> 19 #include <boost/asio/steady_timer.hpp> 20 #include <boost/beast/core/flat_static_buffer.hpp> 21 #include <boost/beast/http/error.hpp> 22 #include <boost/beast/http/parser.hpp> 23 #include <boost/beast/http/read.hpp> 24 #include <boost/beast/http/serializer.hpp> 25 #include <boost/beast/http/write.hpp> 26 #include <boost/beast/ssl/ssl_stream.hpp> 27 #include <boost/beast/websocket.hpp> 28 29 #include <atomic> 30 #include <chrono> 31 #include <vector> 32 33 namespace crow 34 { 35 36 inline void prettyPrintJson(crow::Response& res) 37 { 38 json_html_util::dumpHtml(res.body(), res.jsonValue); 39 40 res.addHeader(boost::beast::http::field::content_type, 41 "text/html;charset=UTF-8"); 42 } 43 44 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 45 static int connectionCount = 0; 46 47 // request body limit size set by the bmcwebHttpReqBodyLimitMb option 48 constexpr uint64_t httpReqBodyLimit = 49 1024UL * 1024UL * bmcwebHttpReqBodyLimitMb; 50 51 constexpr uint64_t loggedOutPostBodyLimit = 4096; 52 53 constexpr uint32_t httpHeaderLimit = 8192; 54 55 template <typename Adaptor, typename Handler> 56 class Connection : 57 public std::enable_shared_from_this<Connection<Adaptor, Handler>> 58 { 59 using self_type = Connection<Adaptor, Handler>; 60 61 public: 62 Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn, 63 std::function<std::string()>& getCachedDateStrF, 64 Adaptor adaptorIn) : 65 adaptor(std::move(adaptorIn)), 66 handler(handlerIn), timer(std::move(timerIn)), 67 getCachedDateStr(getCachedDateStrF) 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 connectionCount++; 78 79 BMCWEB_LOG_DEBUG << this << " Connection open, total " 80 << connectionCount; 81 } 82 83 ~Connection() 84 { 85 res.setCompleteRequestHandler(nullptr); 86 cancelDeadlineTimer(); 87 88 connectionCount--; 89 BMCWEB_LOG_DEBUG << this << " Connection closed, total " 90 << connectionCount; 91 } 92 93 Connection(const Connection&) = delete; 94 Connection(Connection&&) = delete; 95 Connection& operator=(const Connection&) = delete; 96 Connection& operator=(Connection&&) = delete; 97 98 bool tlsVerifyCallback(bool preverified, 99 boost::asio::ssl::verify_context& ctx) 100 { 101 // We always return true to allow full auth flow for resources that 102 // don't require auth 103 if (preverified) 104 { 105 mtlsSession = verifyMtlsUser(req->ipAddress, ctx); 106 if (mtlsSession) 107 { 108 BMCWEB_LOG_DEBUG 109 << this 110 << " Generating TLS session: " << mtlsSession->uniqueId; 111 } 112 } 113 return true; 114 } 115 116 void prepareMutualTls() 117 { 118 std::error_code error; 119 std::filesystem::path caPath(ensuressl::trustStorePath); 120 auto caAvailable = !std::filesystem::is_empty(caPath, error); 121 caAvailable = caAvailable && !error; 122 if (caAvailable && persistent_data::SessionStore::getInstance() 123 .getAuthMethodsConfig() 124 .tls) 125 { 126 adaptor.set_verify_mode(boost::asio::ssl::verify_peer); 127 std::string id = "bmcweb"; 128 129 const char* cStr = id.c_str(); 130 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 131 const auto* idC = reinterpret_cast<const unsigned char*>(cStr); 132 int ret = SSL_set_session_id_context( 133 adaptor.native_handle(), idC, 134 static_cast<unsigned int>(id.length())); 135 if (ret == 0) 136 { 137 BMCWEB_LOG_ERROR << this << " failed to set SSL id"; 138 } 139 } 140 141 adaptor.set_verify_callback( 142 std::bind_front(&self_type::tlsVerifyCallback, this)); 143 } 144 145 Adaptor& socket() 146 { 147 return adaptor; 148 } 149 150 void start() 151 { 152 if (connectionCount >= 100) 153 { 154 BMCWEB_LOG_CRITICAL << this << "Max connection count exceeded."; 155 return; 156 } 157 158 startDeadline(); 159 160 // TODO(ed) Abstract this to a more clever class with the idea of an 161 // asynchronous "start" 162 if constexpr (std::is_same_v<Adaptor, 163 boost::beast::ssl_stream< 164 boost::asio::ip::tcp::socket>>) 165 { 166 adaptor.async_handshake(boost::asio::ssl::stream_base::server, 167 [this, self(shared_from_this())]( 168 const boost::system::error_code& ec) { 169 if (ec) 170 { 171 return; 172 } 173 doReadHeaders(); 174 }); 175 } 176 else 177 { 178 doReadHeaders(); 179 } 180 } 181 182 void handle() 183 { 184 std::error_code reqEc; 185 crow::Request& thisReq = req.emplace(parser->release(), reqEc); 186 if (reqEc) 187 { 188 BMCWEB_LOG_DEBUG << "Request failed to construct" << reqEc; 189 res.result(boost::beast::http::status::bad_request); 190 completeRequest(res); 191 return; 192 } 193 thisReq.session = userSession; 194 195 // Fetch the client IP address 196 readClientIp(); 197 198 // Check for HTTP version 1.1. 199 if (thisReq.version() == 11) 200 { 201 if (thisReq.getHeaderValue(boost::beast::http::field::host).empty()) 202 { 203 res.result(boost::beast::http::status::bad_request); 204 completeRequest(res); 205 return; 206 } 207 } 208 209 BMCWEB_LOG_INFO << "Request: " 210 << " " << this << " HTTP/" << thisReq.version() / 10 211 << "." << thisReq.version() % 10 << ' ' 212 << thisReq.methodString() << " " << thisReq.target() 213 << " " << thisReq.ipAddress.to_string(); 214 215 res.isAliveHelper = [this]() -> bool { return isAlive(); }; 216 217 thisReq.ioService = static_cast<decltype(thisReq.ioService)>( 218 &adaptor.get_executor().context()); 219 220 if (res.completed) 221 { 222 completeRequest(res); 223 return; 224 } 225 keepAlive = thisReq.keepAlive(); 226 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 227 if (!crow::authentication::isOnAllowlist(req->url().buffer(), 228 req->method()) && 229 thisReq.session == nullptr) 230 { 231 BMCWEB_LOG_WARNING << "Authentication failed"; 232 forward_unauthorized::sendUnauthorized( 233 req->url().encoded_path(), 234 req->getHeaderValue("X-Requested-With"), 235 req->getHeaderValue("Accept"), res); 236 completeRequest(res); 237 return; 238 } 239 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 240 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 241 BMCWEB_LOG_DEBUG << "Setting completion handler"; 242 asyncResp->res.setCompleteRequestHandler( 243 [self(shared_from_this())](crow::Response& thisRes) { 244 self->completeRequest(thisRes); 245 }); 246 247 if (thisReq.isUpgrade() && 248 boost::iequals( 249 thisReq.getHeaderValue(boost::beast::http::field::upgrade), 250 "websocket")) 251 { 252 asyncResp->res.setCompleteRequestHandler( 253 [self(shared_from_this())](crow::Response& thisRes) { 254 if (thisRes.result() != boost::beast::http::status::ok) 255 { 256 // When any error occurs before handle upgradation, 257 // the result in response will be set to respective 258 // error. By default the Result will be OK (200), 259 // which implies successful handle upgrade. Response 260 // needs to be sent over this connection only on 261 // failure. 262 self->completeRequest(thisRes); 263 return; 264 } 265 }); 266 handler->handleUpgrade(thisReq, asyncResp, std::move(adaptor)); 267 return; 268 } 269 std::string_view expected = 270 req->getHeaderValue(boost::beast::http::field::if_none_match); 271 if (!expected.empty()) 272 { 273 res.setExpectedHash(expected); 274 } 275 handler->handle(thisReq, asyncResp); 276 } 277 278 bool isAlive() 279 { 280 if constexpr (std::is_same_v<Adaptor, 281 boost::beast::ssl_stream< 282 boost::asio::ip::tcp::socket>>) 283 { 284 return adaptor.next_layer().is_open(); 285 } 286 else 287 { 288 return adaptor.is_open(); 289 } 290 } 291 void close() 292 { 293 if constexpr (std::is_same_v<Adaptor, 294 boost::beast::ssl_stream< 295 boost::asio::ip::tcp::socket>>) 296 { 297 adaptor.next_layer().close(); 298 if (mtlsSession != nullptr) 299 { 300 BMCWEB_LOG_DEBUG 301 << this 302 << " Removing TLS session: " << mtlsSession->uniqueId; 303 persistent_data::SessionStore::getInstance().removeSession( 304 mtlsSession); 305 } 306 } 307 else 308 { 309 adaptor.close(); 310 } 311 } 312 313 void completeRequest(crow::Response& thisRes) 314 { 315 if (!req) 316 { 317 return; 318 } 319 res = std::move(thisRes); 320 res.keepAlive(keepAlive); 321 322 BMCWEB_LOG_INFO << "Response: " << this << ' ' 323 << req->url().encoded_path() << ' ' << res.resultInt() 324 << " keepalive=" << keepAlive; 325 326 addSecurityHeaders(*req, res); 327 328 crow::authentication::cleanupTempSession(*req); 329 330 if (!isAlive()) 331 { 332 // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " << 333 // isReading 334 // << ' ' << isWriting; 335 // delete this; 336 337 // delete lambda with self shared_ptr 338 // to enable connection destruction 339 res.setCompleteRequestHandler(nullptr); 340 return; 341 } 342 343 res.setHashAndHandleNotModified(); 344 345 if (res.body().empty() && !res.jsonValue.empty()) 346 { 347 using http_helpers::ContentType; 348 std::array<ContentType, 3> allowed{ 349 ContentType::CBOR, ContentType::JSON, ContentType::HTML}; 350 ContentType prefered = 351 getPreferedContentType(req->getHeaderValue("Accept"), allowed); 352 353 if (prefered == ContentType::HTML) 354 { 355 prettyPrintJson(res); 356 } 357 else if (prefered == ContentType::CBOR) 358 { 359 res.addHeader(boost::beast::http::field::content_type, 360 "application/cbor"); 361 nlohmann::json::to_cbor(res.jsonValue, res.body()); 362 } 363 else 364 { 365 // Technically prefered could also be NoMatch here, but we'd 366 // like to default to something rather than return 400 for 367 // backward compatibility. 368 res.addHeader(boost::beast::http::field::content_type, 369 "application/json"); 370 res.body() = res.jsonValue.dump( 371 2, ' ', true, nlohmann::json::error_handler_t::replace); 372 } 373 } 374 375 if (res.resultInt() >= 400 && res.body().empty()) 376 { 377 res.body() = std::string(res.reason()); 378 } 379 380 if (res.result() == boost::beast::http::status::no_content) 381 { 382 // Boost beast throws if content is provided on a no-content 383 // response. Ideally, this would never happen, but in the case that 384 // it does, we don't want to throw. 385 BMCWEB_LOG_CRITICAL 386 << this << " Response content provided but code was no-content"; 387 res.body().clear(); 388 } 389 390 res.addHeader(boost::beast::http::field::date, getCachedDateStr()); 391 392 doWrite(res); 393 394 // delete lambda with self shared_ptr 395 // to enable connection destruction 396 res.setCompleteRequestHandler(nullptr); 397 } 398 399 void readClientIp() 400 { 401 boost::asio::ip::address ip; 402 boost::system::error_code ec = getClientIp(ip); 403 if (ec) 404 { 405 return; 406 } 407 req->ipAddress = ip; 408 } 409 410 boost::system::error_code getClientIp(boost::asio::ip::address& ip) 411 { 412 boost::system::error_code ec; 413 BMCWEB_LOG_DEBUG << "Fetch the client IP address"; 414 boost::asio::ip::tcp::endpoint endpoint = 415 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec); 416 417 if (ec) 418 { 419 // If remote endpoint fails keep going. "ClientOriginIPAddress" 420 // will be empty. 421 BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " 422 << ec; 423 return ec; 424 } 425 ip = endpoint.address(); 426 return ec; 427 } 428 429 private: 430 void doReadHeaders() 431 { 432 BMCWEB_LOG_DEBUG << this << " doReadHeaders"; 433 434 // Clean up any previous Connection. 435 boost::beast::http::async_read_header( 436 adaptor, buffer, *parser, 437 [this, 438 self(shared_from_this())](const boost::system::error_code& ec, 439 std::size_t bytesTransferred) { 440 BMCWEB_LOG_DEBUG << this << " async_read_header " 441 << bytesTransferred << " Bytes"; 442 bool errorWhileReading = false; 443 if (ec) 444 { 445 errorWhileReading = true; 446 if (ec == boost::beast::http::error::end_of_stream) 447 { 448 BMCWEB_LOG_WARNING 449 << this << " Error while reading: " << ec.message(); 450 } 451 else 452 { 453 BMCWEB_LOG_ERROR 454 << this << " Error while reading: " << ec.message(); 455 } 456 } 457 else 458 { 459 // if the adaptor isn't open anymore, and wasn't handed to a 460 // websocket, treat as an error 461 if (!isAlive() && 462 !boost::beast::websocket::is_upgrade(parser->get())) 463 { 464 errorWhileReading = true; 465 } 466 } 467 468 cancelDeadlineTimer(); 469 470 if (errorWhileReading) 471 { 472 close(); 473 BMCWEB_LOG_DEBUG << this << " from read(1)"; 474 return; 475 } 476 477 readClientIp(); 478 479 boost::asio::ip::address ip; 480 if (getClientIp(ip)) 481 { 482 BMCWEB_LOG_DEBUG << "Unable to get client IP"; 483 } 484 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 485 boost::beast::http::verb method = parser->get().method(); 486 userSession = crow::authentication::authenticate( 487 ip, res, method, parser->get().base(), mtlsSession); 488 489 bool loggedIn = userSession != nullptr; 490 if (!loggedIn) 491 { 492 const boost::optional<uint64_t> contentLength = 493 parser->content_length(); 494 if (contentLength && *contentLength > loggedOutPostBodyLimit) 495 { 496 BMCWEB_LOG_DEBUG << "Content length greater than limit " 497 << *contentLength; 498 close(); 499 return; 500 } 501 502 BMCWEB_LOG_DEBUG << "Starting quick deadline"; 503 } 504 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 505 506 if (parser->is_done()) 507 { 508 handle(); 509 return; 510 } 511 512 doRead(); 513 }); 514 } 515 516 void doRead() 517 { 518 BMCWEB_LOG_DEBUG << this << " doRead"; 519 startDeadline(); 520 boost::beast::http::async_read_some( 521 adaptor, buffer, *parser, 522 [this, 523 self(shared_from_this())](const boost::system::error_code& ec, 524 std::size_t bytesTransferred) { 525 BMCWEB_LOG_DEBUG << this << " async_read_some " << bytesTransferred 526 << " Bytes"; 527 528 if (ec) 529 { 530 BMCWEB_LOG_ERROR << this 531 << " Error while reading: " << ec.message(); 532 close(); 533 BMCWEB_LOG_DEBUG << this << " from read(1)"; 534 return; 535 } 536 537 // If the user is logged in, allow them to send files incrementally 538 // one piece at a time. If authentication is disabled then there is 539 // no user session hence always allow to send one piece at a time. 540 if (userSession != nullptr) 541 { 542 cancelDeadlineTimer(); 543 } 544 if (!parser->is_done()) 545 { 546 doRead(); 547 return; 548 } 549 550 cancelDeadlineTimer(); 551 handle(); 552 }); 553 } 554 555 void doWrite(crow::Response& thisRes) 556 { 557 BMCWEB_LOG_DEBUG << this << " doWrite"; 558 thisRes.preparePayload(); 559 serializer.emplace(*thisRes.stringResponse); 560 startDeadline(); 561 boost::beast::http::async_write(adaptor, *serializer, 562 [this, self(shared_from_this())]( 563 const boost::system::error_code& ec, 564 std::size_t bytesTransferred) { 565 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred 566 << " bytes"; 567 568 cancelDeadlineTimer(); 569 570 if (ec) 571 { 572 BMCWEB_LOG_DEBUG << this << " from write(2)"; 573 return; 574 } 575 if (!keepAlive) 576 { 577 close(); 578 BMCWEB_LOG_DEBUG << this << " from write(1)"; 579 return; 580 } 581 582 serializer.reset(); 583 BMCWEB_LOG_DEBUG << this << " Clearing response"; 584 res.clear(); 585 parser.emplace(std::piecewise_construct, std::make_tuple()); 586 parser->body_limit(httpReqBodyLimit); // reset body limit for 587 // newly created parser 588 buffer.consume(buffer.size()); 589 590 userSession = nullptr; 591 592 // Destroy the Request via the std::optional 593 req.reset(); 594 doReadHeaders(); 595 }); 596 } 597 598 void cancelDeadlineTimer() 599 { 600 timer.cancel(); 601 } 602 603 void startDeadline() 604 { 605 // Timer is already started so no further action is required. 606 if (timerStarted) 607 { 608 return; 609 } 610 611 std::chrono::seconds timeout(15); 612 613 std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this(); 614 timer.expires_after(timeout); 615 timer.async_wait([weakSelf](const boost::system::error_code ec) { 616 // Note, we are ignoring other types of errors here; If the timer 617 // failed for any reason, we should still close the connection 618 std::shared_ptr<Connection<Adaptor, Handler>> self = 619 weakSelf.lock(); 620 if (!self) 621 { 622 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 623 return; 624 } 625 626 self->timerStarted = false; 627 628 if (ec == boost::asio::error::operation_aborted) 629 { 630 // Canceled wait means the path succeeeded. 631 return; 632 } 633 if (ec) 634 { 635 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec; 636 } 637 638 BMCWEB_LOG_WARNING << self << "Connection timed out, closing"; 639 640 self->close(); 641 }); 642 643 timerStarted = true; 644 BMCWEB_LOG_DEBUG << this << " timer started"; 645 } 646 647 Adaptor adaptor; 648 Handler* handler; 649 // Making this a std::optional allows it to be efficiently destroyed and 650 // re-created on Connection reset 651 std::optional< 652 boost::beast::http::request_parser<boost::beast::http::string_body>> 653 parser; 654 655 boost::beast::flat_static_buffer<8192> buffer; 656 657 std::optional<boost::beast::http::response_serializer< 658 boost::beast::http::string_body>> 659 serializer; 660 661 std::optional<crow::Request> req; 662 crow::Response res; 663 664 std::shared_ptr<persistent_data::UserSession> userSession; 665 std::shared_ptr<persistent_data::UserSession> mtlsSession; 666 667 boost::asio::steady_timer timer; 668 669 bool keepAlive = true; 670 671 bool timerStarted = false; 672 673 std::function<std::string()>& getCachedDateStr; 674 675 using std::enable_shared_from_this< 676 Connection<Adaptor, Handler>>::shared_from_this; 677 678 using std::enable_shared_from_this< 679 Connection<Adaptor, Handler>>::weak_from_this; 680 }; 681 } // namespace crow 682