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 = 1024UL * 1024UL * 49 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.is_structured()) 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 res.addHeader(boost::beast::http::field::date, getCachedDateStr()); 381 382 doWrite(res); 383 384 // delete lambda with self shared_ptr 385 // to enable connection destruction 386 res.setCompleteRequestHandler(nullptr); 387 } 388 389 void readClientIp() 390 { 391 boost::asio::ip::address ip; 392 boost::system::error_code ec = getClientIp(ip); 393 if (ec) 394 { 395 return; 396 } 397 req->ipAddress = ip; 398 } 399 400 boost::system::error_code getClientIp(boost::asio::ip::address& ip) 401 { 402 boost::system::error_code ec; 403 BMCWEB_LOG_DEBUG << "Fetch the client IP address"; 404 boost::asio::ip::tcp::endpoint endpoint = 405 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec); 406 407 if (ec) 408 { 409 // If remote endpoint fails keep going. "ClientOriginIPAddress" 410 // will be empty. 411 BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " 412 << ec; 413 return ec; 414 } 415 ip = endpoint.address(); 416 return ec; 417 } 418 419 private: 420 void doReadHeaders() 421 { 422 BMCWEB_LOG_DEBUG << this << " doReadHeaders"; 423 424 // Clean up any previous Connection. 425 boost::beast::http::async_read_header( 426 adaptor, buffer, *parser, 427 [this, 428 self(shared_from_this())](const boost::system::error_code& ec, 429 std::size_t bytesTransferred) { 430 BMCWEB_LOG_DEBUG << this << " async_read_header " 431 << bytesTransferred << " Bytes"; 432 bool errorWhileReading = false; 433 if (ec) 434 { 435 errorWhileReading = true; 436 if (ec == boost::beast::http::error::end_of_stream) 437 { 438 BMCWEB_LOG_WARNING 439 << this << " Error while reading: " << ec.message(); 440 } 441 else 442 { 443 BMCWEB_LOG_ERROR 444 << this << " Error while reading: " << ec.message(); 445 } 446 } 447 else 448 { 449 // if the adaptor isn't open anymore, and wasn't handed to a 450 // websocket, treat as an error 451 if (!isAlive() && 452 !boost::beast::websocket::is_upgrade(parser->get())) 453 { 454 errorWhileReading = true; 455 } 456 } 457 458 cancelDeadlineTimer(); 459 460 if (errorWhileReading) 461 { 462 close(); 463 BMCWEB_LOG_DEBUG << this << " from read(1)"; 464 return; 465 } 466 467 readClientIp(); 468 469 boost::asio::ip::address ip; 470 if (getClientIp(ip)) 471 { 472 BMCWEB_LOG_DEBUG << "Unable to get client IP"; 473 } 474 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 475 boost::beast::http::verb method = parser->get().method(); 476 userSession = crow::authentication::authenticate( 477 ip, res, method, parser->get().base(), mtlsSession); 478 479 bool loggedIn = userSession != nullptr; 480 if (!loggedIn) 481 { 482 const boost::optional<uint64_t> contentLength = 483 parser->content_length(); 484 if (contentLength && *contentLength > loggedOutPostBodyLimit) 485 { 486 BMCWEB_LOG_DEBUG << "Content length greater than limit " 487 << *contentLength; 488 close(); 489 return; 490 } 491 492 BMCWEB_LOG_DEBUG << "Starting quick deadline"; 493 } 494 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 495 496 if (parser->is_done()) 497 { 498 handle(); 499 return; 500 } 501 502 doRead(); 503 }); 504 } 505 506 void doRead() 507 { 508 BMCWEB_LOG_DEBUG << this << " doRead"; 509 startDeadline(); 510 boost::beast::http::async_read_some( 511 adaptor, buffer, *parser, 512 [this, 513 self(shared_from_this())](const boost::system::error_code& ec, 514 std::size_t bytesTransferred) { 515 BMCWEB_LOG_DEBUG << this << " async_read_some " << bytesTransferred 516 << " Bytes"; 517 518 if (ec) 519 { 520 BMCWEB_LOG_ERROR << this 521 << " Error while reading: " << ec.message(); 522 close(); 523 BMCWEB_LOG_DEBUG << this << " from read(1)"; 524 return; 525 } 526 527 // If the user is logged in, allow them to send files incrementally 528 // one piece at a time. If authentication is disabled then there is 529 // no user session hence always allow to send one piece at a time. 530 if (userSession != nullptr) 531 { 532 cancelDeadlineTimer(); 533 } 534 if (!parser->is_done()) 535 { 536 doRead(); 537 return; 538 } 539 540 cancelDeadlineTimer(); 541 handle(); 542 }); 543 } 544 545 void doWrite(crow::Response& thisRes) 546 { 547 BMCWEB_LOG_DEBUG << this << " doWrite"; 548 thisRes.preparePayload(); 549 serializer.emplace(*thisRes.stringResponse); 550 startDeadline(); 551 boost::beast::http::async_write(adaptor, *serializer, 552 [this, self(shared_from_this())]( 553 const boost::system::error_code& ec, 554 std::size_t bytesTransferred) { 555 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred 556 << " bytes"; 557 558 cancelDeadlineTimer(); 559 560 if (ec) 561 { 562 BMCWEB_LOG_DEBUG << this << " from write(2)"; 563 return; 564 } 565 if (!keepAlive) 566 { 567 close(); 568 BMCWEB_LOG_DEBUG << this << " from write(1)"; 569 return; 570 } 571 572 serializer.reset(); 573 BMCWEB_LOG_DEBUG << this << " Clearing response"; 574 res.clear(); 575 parser.emplace(std::piecewise_construct, std::make_tuple()); 576 parser->body_limit(httpReqBodyLimit); // reset body limit for 577 // newly created parser 578 buffer.consume(buffer.size()); 579 580 userSession = nullptr; 581 582 // Destroy the Request via the std::optional 583 req.reset(); 584 doReadHeaders(); 585 }); 586 } 587 588 void cancelDeadlineTimer() 589 { 590 timer.cancel(); 591 } 592 593 void startDeadline() 594 { 595 // Timer is already started so no further action is required. 596 if (timerStarted) 597 { 598 return; 599 } 600 601 std::chrono::seconds timeout(15); 602 603 std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this(); 604 timer.expires_after(timeout); 605 timer.async_wait([weakSelf](const boost::system::error_code& ec) { 606 // Note, we are ignoring other types of errors here; If the timer 607 // failed for any reason, we should still close the connection 608 std::shared_ptr<Connection<Adaptor, Handler>> self = 609 weakSelf.lock(); 610 if (!self) 611 { 612 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 613 return; 614 } 615 616 self->timerStarted = false; 617 618 if (ec == boost::asio::error::operation_aborted) 619 { 620 // Canceled wait means the path succeeeded. 621 return; 622 } 623 if (ec) 624 { 625 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec; 626 } 627 628 BMCWEB_LOG_WARNING << self << "Connection timed out, closing"; 629 630 self->close(); 631 }); 632 633 timerStarted = true; 634 BMCWEB_LOG_DEBUG << this << " timer started"; 635 } 636 637 Adaptor adaptor; 638 Handler* handler; 639 // Making this a std::optional allows it to be efficiently destroyed and 640 // re-created on Connection reset 641 std::optional< 642 boost::beast::http::request_parser<boost::beast::http::string_body>> 643 parser; 644 645 boost::beast::flat_static_buffer<8192> buffer; 646 647 std::optional<boost::beast::http::response_serializer< 648 boost::beast::http::string_body>> 649 serializer; 650 651 std::optional<crow::Request> req; 652 crow::Response res; 653 654 std::shared_ptr<persistent_data::UserSession> userSession; 655 std::shared_ptr<persistent_data::UserSession> mtlsSession; 656 657 boost::asio::steady_timer timer; 658 659 bool keepAlive = true; 660 661 bool timerStarted = false; 662 663 std::function<std::string()>& getCachedDateStr; 664 665 using std::enable_shared_from_this< 666 Connection<Adaptor, Handler>>::shared_from_this; 667 668 using std::enable_shared_from_this< 669 Connection<Adaptor, Handler>>::weak_from_this; 670 }; 671 } // namespace crow 672