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