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 bool isSse = 247 isContentTypeAllowed(req->getHeaderValue("Accept"), 248 http_helpers::ContentType::EventStream, false); 249 if ((thisReq.isUpgrade() && 250 boost::iequals( 251 thisReq.getHeaderValue(boost::beast::http::field::upgrade), 252 "websocket")) || 253 isSse) 254 { 255 asyncResp->res.setCompleteRequestHandler( 256 [self(shared_from_this())](crow::Response& thisRes) { 257 if (thisRes.result() != boost::beast::http::status::ok) 258 { 259 // When any error occurs before handle upgradation, 260 // the result in response will be set to respective 261 // error. By default the Result will be OK (200), 262 // which implies successful handle upgrade. Response 263 // needs to be sent over this connection only on 264 // failure. 265 self->completeRequest(thisRes); 266 return; 267 } 268 }); 269 handler->handleUpgrade(thisReq, asyncResp, std::move(adaptor)); 270 return; 271 } 272 std::string_view expected = 273 req->getHeaderValue(boost::beast::http::field::if_none_match); 274 if (!expected.empty()) 275 { 276 res.setExpectedHash(expected); 277 } 278 handler->handle(thisReq, asyncResp); 279 } 280 281 bool isAlive() 282 { 283 if constexpr (std::is_same_v<Adaptor, 284 boost::beast::ssl_stream< 285 boost::asio::ip::tcp::socket>>) 286 { 287 return adaptor.next_layer().is_open(); 288 } 289 else 290 { 291 return adaptor.is_open(); 292 } 293 } 294 void close() 295 { 296 if constexpr (std::is_same_v<Adaptor, 297 boost::beast::ssl_stream< 298 boost::asio::ip::tcp::socket>>) 299 { 300 adaptor.next_layer().close(); 301 if (mtlsSession != nullptr) 302 { 303 BMCWEB_LOG_DEBUG 304 << this 305 << " Removing TLS session: " << mtlsSession->uniqueId; 306 persistent_data::SessionStore::getInstance().removeSession( 307 mtlsSession); 308 } 309 } 310 else 311 { 312 adaptor.close(); 313 } 314 } 315 316 void completeRequest(crow::Response& thisRes) 317 { 318 if (!req) 319 { 320 return; 321 } 322 res = std::move(thisRes); 323 res.keepAlive(keepAlive); 324 325 BMCWEB_LOG_INFO << "Response: " << this << ' ' 326 << req->url().encoded_path() << ' ' << res.resultInt() 327 << " keepalive=" << keepAlive; 328 329 addSecurityHeaders(*req, res); 330 331 crow::authentication::cleanupTempSession(*req); 332 333 if (!isAlive()) 334 { 335 // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " << 336 // isReading 337 // << ' ' << isWriting; 338 // delete this; 339 340 // delete lambda with self shared_ptr 341 // to enable connection destruction 342 res.setCompleteRequestHandler(nullptr); 343 return; 344 } 345 346 res.setHashAndHandleNotModified(); 347 348 if (res.body().empty() && res.jsonValue.is_structured()) 349 { 350 using http_helpers::ContentType; 351 std::array<ContentType, 3> allowed{ 352 ContentType::CBOR, ContentType::JSON, ContentType::HTML}; 353 ContentType prefered = 354 getPreferedContentType(req->getHeaderValue("Accept"), allowed); 355 356 if (prefered == ContentType::HTML) 357 { 358 prettyPrintJson(res); 359 } 360 else if (prefered == ContentType::CBOR) 361 { 362 res.addHeader(boost::beast::http::field::content_type, 363 "application/cbor"); 364 nlohmann::json::to_cbor(res.jsonValue, res.body()); 365 } 366 else 367 { 368 // Technically prefered could also be NoMatch here, but we'd 369 // like to default to something rather than return 400 for 370 // backward compatibility. 371 res.addHeader(boost::beast::http::field::content_type, 372 "application/json"); 373 res.body() = res.jsonValue.dump( 374 2, ' ', true, nlohmann::json::error_handler_t::replace); 375 } 376 } 377 378 if (res.resultInt() >= 400 && res.body().empty()) 379 { 380 res.body() = std::string(res.reason()); 381 } 382 383 res.addHeader(boost::beast::http::field::date, getCachedDateStr()); 384 385 doWrite(res); 386 387 // delete lambda with self shared_ptr 388 // to enable connection destruction 389 res.setCompleteRequestHandler(nullptr); 390 } 391 392 void readClientIp() 393 { 394 boost::asio::ip::address ip; 395 boost::system::error_code ec = getClientIp(ip); 396 if (ec) 397 { 398 return; 399 } 400 req->ipAddress = ip; 401 } 402 403 boost::system::error_code getClientIp(boost::asio::ip::address& ip) 404 { 405 boost::system::error_code ec; 406 BMCWEB_LOG_DEBUG << "Fetch the client IP address"; 407 boost::asio::ip::tcp::endpoint endpoint = 408 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec); 409 410 if (ec) 411 { 412 // If remote endpoint fails keep going. "ClientOriginIPAddress" 413 // will be empty. 414 BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " 415 << ec; 416 return ec; 417 } 418 ip = endpoint.address(); 419 return ec; 420 } 421 422 private: 423 void doReadHeaders() 424 { 425 BMCWEB_LOG_DEBUG << this << " doReadHeaders"; 426 427 // Clean up any previous Connection. 428 boost::beast::http::async_read_header( 429 adaptor, buffer, *parser, 430 [this, 431 self(shared_from_this())](const boost::system::error_code& ec, 432 std::size_t bytesTransferred) { 433 BMCWEB_LOG_DEBUG << this << " async_read_header " 434 << bytesTransferred << " Bytes"; 435 bool errorWhileReading = false; 436 if (ec) 437 { 438 errorWhileReading = true; 439 if (ec == boost::beast::http::error::end_of_stream) 440 { 441 BMCWEB_LOG_WARNING 442 << this << " Error while reading: " << ec.message(); 443 } 444 else 445 { 446 BMCWEB_LOG_ERROR 447 << this << " Error while reading: " << ec.message(); 448 } 449 } 450 else 451 { 452 // if the adaptor isn't open anymore, and wasn't handed to a 453 // websocket, treat as an error 454 if (!isAlive() && 455 !boost::beast::websocket::is_upgrade(parser->get())) 456 { 457 errorWhileReading = true; 458 } 459 } 460 461 cancelDeadlineTimer(); 462 463 if (errorWhileReading) 464 { 465 close(); 466 BMCWEB_LOG_DEBUG << this << " from read(1)"; 467 return; 468 } 469 470 readClientIp(); 471 472 boost::asio::ip::address ip; 473 if (getClientIp(ip)) 474 { 475 BMCWEB_LOG_DEBUG << "Unable to get client IP"; 476 } 477 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 478 boost::beast::http::verb method = parser->get().method(); 479 userSession = crow::authentication::authenticate( 480 ip, res, method, parser->get().base(), mtlsSession); 481 482 bool loggedIn = userSession != nullptr; 483 if (!loggedIn) 484 { 485 const boost::optional<uint64_t> contentLength = 486 parser->content_length(); 487 if (contentLength && *contentLength > loggedOutPostBodyLimit) 488 { 489 BMCWEB_LOG_DEBUG << "Content length greater than limit " 490 << *contentLength; 491 close(); 492 return; 493 } 494 495 BMCWEB_LOG_DEBUG << "Starting quick deadline"; 496 } 497 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 498 499 if (parser->is_done()) 500 { 501 handle(); 502 return; 503 } 504 505 doRead(); 506 }); 507 } 508 509 void doRead() 510 { 511 BMCWEB_LOG_DEBUG << this << " doRead"; 512 startDeadline(); 513 boost::beast::http::async_read_some( 514 adaptor, buffer, *parser, 515 [this, 516 self(shared_from_this())](const boost::system::error_code& ec, 517 std::size_t bytesTransferred) { 518 BMCWEB_LOG_DEBUG << this << " async_read_some " << bytesTransferred 519 << " Bytes"; 520 521 if (ec) 522 { 523 BMCWEB_LOG_ERROR << this 524 << " Error while reading: " << ec.message(); 525 close(); 526 BMCWEB_LOG_DEBUG << this << " from read(1)"; 527 return; 528 } 529 530 // If the user is logged in, allow them to send files incrementally 531 // one piece at a time. If authentication is disabled then there is 532 // no user session hence always allow to send one piece at a time. 533 if (userSession != nullptr) 534 { 535 cancelDeadlineTimer(); 536 } 537 if (!parser->is_done()) 538 { 539 doRead(); 540 return; 541 } 542 543 cancelDeadlineTimer(); 544 handle(); 545 }); 546 } 547 548 void doWrite(crow::Response& thisRes) 549 { 550 BMCWEB_LOG_DEBUG << this << " doWrite"; 551 thisRes.preparePayload(); 552 serializer.emplace(*thisRes.stringResponse); 553 startDeadline(); 554 boost::beast::http::async_write(adaptor, *serializer, 555 [this, self(shared_from_this())]( 556 const boost::system::error_code& ec, 557 std::size_t bytesTransferred) { 558 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred 559 << " bytes"; 560 561 cancelDeadlineTimer(); 562 563 if (ec) 564 { 565 BMCWEB_LOG_DEBUG << this << " from write(2)"; 566 return; 567 } 568 if (!keepAlive) 569 { 570 close(); 571 BMCWEB_LOG_DEBUG << this << " from write(1)"; 572 return; 573 } 574 575 serializer.reset(); 576 BMCWEB_LOG_DEBUG << this << " Clearing response"; 577 res.clear(); 578 parser.emplace(std::piecewise_construct, std::make_tuple()); 579 parser->body_limit(httpReqBodyLimit); // reset body limit for 580 // newly created parser 581 buffer.consume(buffer.size()); 582 583 userSession = nullptr; 584 585 // Destroy the Request via the std::optional 586 req.reset(); 587 doReadHeaders(); 588 }); 589 } 590 591 void cancelDeadlineTimer() 592 { 593 timer.cancel(); 594 } 595 596 void startDeadline() 597 { 598 // Timer is already started so no further action is required. 599 if (timerStarted) 600 { 601 return; 602 } 603 604 std::chrono::seconds timeout(15); 605 606 std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this(); 607 timer.expires_after(timeout); 608 timer.async_wait([weakSelf](const boost::system::error_code& ec) { 609 // Note, we are ignoring other types of errors here; If the timer 610 // failed for any reason, we should still close the connection 611 std::shared_ptr<Connection<Adaptor, Handler>> self = 612 weakSelf.lock(); 613 if (!self) 614 { 615 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 616 return; 617 } 618 619 self->timerStarted = false; 620 621 if (ec == boost::asio::error::operation_aborted) 622 { 623 // Canceled wait means the path succeeeded. 624 return; 625 } 626 if (ec) 627 { 628 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec; 629 } 630 631 BMCWEB_LOG_WARNING << self << "Connection timed out, closing"; 632 633 self->close(); 634 }); 635 636 timerStarted = true; 637 BMCWEB_LOG_DEBUG << this << " timer started"; 638 } 639 640 Adaptor adaptor; 641 Handler* handler; 642 // Making this a std::optional allows it to be efficiently destroyed and 643 // re-created on Connection reset 644 std::optional< 645 boost::beast::http::request_parser<boost::beast::http::string_body>> 646 parser; 647 648 boost::beast::flat_static_buffer<8192> buffer; 649 650 std::optional<boost::beast::http::response_serializer< 651 boost::beast::http::string_body>> 652 serializer; 653 654 std::optional<crow::Request> req; 655 crow::Response res; 656 657 std::shared_ptr<persistent_data::UserSession> userSession; 658 std::shared_ptr<persistent_data::UserSession> mtlsSession; 659 660 boost::asio::steady_timer timer; 661 662 bool keepAlive = true; 663 664 bool timerStarted = false; 665 666 std::function<std::string()>& getCachedDateStr; 667 668 using std::enable_shared_from_this< 669 Connection<Adaptor, Handler>>::shared_from_this; 670 671 using std::enable_shared_from_this< 672 Connection<Adaptor, Handler>>::weak_from_this; 673 }; 674 } // namespace crow 675