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