1 #pragma once 2 #include "bmcweb_config.h" 3 4 #include "authentication.hpp" 5 #include "http_response.hpp" 6 #include "http_utility.hpp" 7 #include "json_html_serializer.hpp" 8 #include "logging.hpp" 9 #include "mutual_tls.hpp" 10 #include "security_headers.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 #include <boost/url/url_view.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 userSession = verifyMtlsUser(req->ipAddress, ctx); 106 if (userSession) 107 { 108 sessionIsFromTransport = true; 109 } 110 } 111 return true; 112 } 113 114 void prepareMutualTls() 115 { 116 std::error_code error; 117 std::filesystem::path caPath(ensuressl::trustStorePath); 118 auto caAvailable = !std::filesystem::is_empty(caPath, error); 119 caAvailable = caAvailable && !error; 120 if (caAvailable && persistent_data::SessionStore::getInstance() 121 .getAuthMethodsConfig() 122 .tls) 123 { 124 adaptor.set_verify_mode(boost::asio::ssl::verify_peer); 125 std::string id = "bmcweb"; 126 127 const char* cStr = id.c_str(); 128 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) 129 const auto* idC = reinterpret_cast<const unsigned char*>(cStr); 130 int ret = SSL_set_session_id_context( 131 adaptor.native_handle(), idC, 132 static_cast<unsigned int>(id.length())); 133 if (ret == 0) 134 { 135 BMCWEB_LOG_ERROR << this << " failed to set SSL id"; 136 } 137 } 138 139 adaptor.set_verify_callback( 140 std::bind_front(&self_type::tlsVerifyCallback, this)); 141 } 142 143 Adaptor& socket() 144 { 145 return adaptor; 146 } 147 148 void start() 149 { 150 if (connectionCount >= 100) 151 { 152 BMCWEB_LOG_CRITICAL << this << "Max connection count exceeded."; 153 return; 154 } 155 156 startDeadline(); 157 158 // TODO(ed) Abstract this to a more clever class with the idea of an 159 // asynchronous "start" 160 if constexpr (std::is_same_v<Adaptor, 161 boost::beast::ssl_stream< 162 boost::asio::ip::tcp::socket>>) 163 { 164 adaptor.async_handshake(boost::asio::ssl::stream_base::server, 165 [this, self(shared_from_this())]( 166 const boost::system::error_code& ec) { 167 if (ec) 168 { 169 return; 170 } 171 doReadHeaders(); 172 }); 173 } 174 else 175 { 176 doReadHeaders(); 177 } 178 } 179 180 void handle() 181 { 182 std::error_code reqEc; 183 crow::Request& thisReq = req.emplace(parser->release(), reqEc); 184 if (reqEc) 185 { 186 BMCWEB_LOG_DEBUG << "Request failed to construct" << reqEc; 187 res.result(boost::beast::http::status::bad_request); 188 completeRequest(res); 189 return; 190 } 191 thisReq.session = userSession; 192 193 // Fetch the client IP address 194 readClientIp(); 195 196 // Check for HTTP version 1.1. 197 if (thisReq.version() == 11) 198 { 199 if (thisReq.getHeaderValue(boost::beast::http::field::host).empty()) 200 { 201 res.result(boost::beast::http::status::bad_request); 202 completeRequest(res); 203 return; 204 } 205 } 206 207 BMCWEB_LOG_INFO << "Request: " 208 << " " << this << " HTTP/" << thisReq.version() / 10 209 << "." << thisReq.version() % 10 << ' ' 210 << thisReq.methodString() << " " << thisReq.target() 211 << " " << thisReq.ipAddress.to_string(); 212 213 res.isAliveHelper = [this]() -> bool { return isAlive(); }; 214 215 thisReq.ioService = static_cast<decltype(thisReq.ioService)>( 216 &adaptor.get_executor().context()); 217 218 if (res.completed) 219 { 220 completeRequest(res); 221 return; 222 } 223 keepAlive = thisReq.keepAlive(); 224 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 225 if (!crow::authentication::isOnAllowlist(req->url, req->method()) && 226 thisReq.session == nullptr) 227 { 228 BMCWEB_LOG_WARNING << "Authentication failed"; 229 forward_unauthorized::sendUnauthorized( 230 req->url, req->getHeaderValue("X-Requested-With"), 231 req->getHeaderValue("Accept"), res); 232 completeRequest(res); 233 return; 234 } 235 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 236 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 237 BMCWEB_LOG_DEBUG << "Setting completion handler"; 238 asyncResp->res.setCompleteRequestHandler( 239 [self(shared_from_this())](crow::Response& thisRes) { 240 self->completeRequest(thisRes); 241 }); 242 243 if (thisReq.isUpgrade() && 244 boost::iequals( 245 thisReq.getHeaderValue(boost::beast::http::field::upgrade), 246 "websocket")) 247 { 248 handler->handleUpgrade(thisReq, res, std::move(adaptor)); 249 // delete lambda with self shared_ptr 250 // to enable connection destruction 251 asyncResp->res.setCompleteRequestHandler(nullptr); 252 return; 253 } 254 std::string_view expected = 255 req->getHeaderValue(boost::beast::http::field::if_none_match); 256 if (!expected.empty()) 257 { 258 res.setExpectedHash(expected); 259 } 260 handler->handle(thisReq, asyncResp); 261 } 262 263 bool isAlive() 264 { 265 if constexpr (std::is_same_v<Adaptor, 266 boost::beast::ssl_stream< 267 boost::asio::ip::tcp::socket>>) 268 { 269 return adaptor.next_layer().is_open(); 270 } 271 else 272 { 273 return adaptor.is_open(); 274 } 275 } 276 void close() 277 { 278 if constexpr (std::is_same_v<Adaptor, 279 boost::beast::ssl_stream< 280 boost::asio::ip::tcp::socket>>) 281 { 282 adaptor.next_layer().close(); 283 if (sessionIsFromTransport && userSession != nullptr) 284 { 285 BMCWEB_LOG_DEBUG 286 << this 287 << " Removing TLS session: " << userSession->uniqueId; 288 persistent_data::SessionStore::getInstance().removeSession( 289 userSession); 290 } 291 } 292 else 293 { 294 adaptor.close(); 295 } 296 } 297 298 void completeRequest(crow::Response& thisRes) 299 { 300 if (!req) 301 { 302 return; 303 } 304 res = std::move(thisRes); 305 res.keepAlive(keepAlive); 306 307 BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' ' 308 << res.resultInt() << " keepalive=" << keepAlive; 309 310 addSecurityHeaders(*req, res); 311 312 crow::authentication::cleanupTempSession(*req); 313 314 if (!isAlive()) 315 { 316 // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " << 317 // isReading 318 // << ' ' << isWriting; 319 // delete this; 320 321 // delete lambda with self shared_ptr 322 // to enable connection destruction 323 res.setCompleteRequestHandler(nullptr); 324 return; 325 } 326 327 res.setHashAndHandleNotModified(); 328 329 if (res.body().empty() && !res.jsonValue.empty()) 330 { 331 using http_helpers::ContentType; 332 std::array<ContentType, 3> allowed{ 333 ContentType::CBOR, ContentType::JSON, ContentType::HTML}; 334 ContentType prefered = 335 getPreferedContentType(req->getHeaderValue("Accept"), allowed); 336 337 if (prefered == ContentType::HTML) 338 { 339 prettyPrintJson(res); 340 } 341 else if (prefered == ContentType::CBOR) 342 { 343 res.addHeader(boost::beast::http::field::content_type, 344 "application/cbor"); 345 nlohmann::json::to_cbor(res.jsonValue, res.body()); 346 } 347 else 348 { 349 // Technically prefered could also be NoMatch here, but we'd 350 // like to default to something rather than return 400 for 351 // backward compatibility. 352 res.addHeader(boost::beast::http::field::content_type, 353 "application/json"); 354 res.body() = res.jsonValue.dump( 355 2, ' ', true, nlohmann::json::error_handler_t::replace); 356 } 357 } 358 359 if (res.resultInt() >= 400 && res.body().empty()) 360 { 361 res.body() = std::string(res.reason()); 362 } 363 364 if (res.result() == boost::beast::http::status::no_content) 365 { 366 // Boost beast throws if content is provided on a no-content 367 // response. Ideally, this would never happen, but in the case that 368 // it does, we don't want to throw. 369 BMCWEB_LOG_CRITICAL 370 << this << " Response content provided but code was no-content"; 371 res.body().clear(); 372 } 373 374 res.addHeader(boost::beast::http::field::date, getCachedDateStr()); 375 376 doWrite(res); 377 378 // delete lambda with self shared_ptr 379 // to enable connection destruction 380 res.setCompleteRequestHandler(nullptr); 381 } 382 383 void readClientIp() 384 { 385 boost::asio::ip::address ip; 386 boost::system::error_code ec = getClientIp(ip); 387 if (ec) 388 { 389 return; 390 } 391 req->ipAddress = ip; 392 } 393 394 boost::system::error_code getClientIp(boost::asio::ip::address& ip) 395 { 396 boost::system::error_code ec; 397 BMCWEB_LOG_DEBUG << "Fetch the client IP address"; 398 boost::asio::ip::tcp::endpoint endpoint = 399 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec); 400 401 if (ec) 402 { 403 // If remote endpoint fails keep going. "ClientOriginIPAddress" 404 // will be empty. 405 BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " 406 << ec; 407 return ec; 408 } 409 ip = endpoint.address(); 410 return ec; 411 } 412 413 private: 414 void doReadHeaders() 415 { 416 BMCWEB_LOG_DEBUG << this << " doReadHeaders"; 417 418 // Clean up any previous Connection. 419 boost::beast::http::async_read_header( 420 adaptor, buffer, *parser, 421 [this, 422 self(shared_from_this())](const boost::system::error_code& ec, 423 std::size_t bytesTransferred) { 424 BMCWEB_LOG_DEBUG << this << " async_read_header " 425 << bytesTransferred << " Bytes"; 426 bool errorWhileReading = false; 427 if (ec) 428 { 429 errorWhileReading = true; 430 if (ec == boost::beast::http::error::end_of_stream) 431 { 432 BMCWEB_LOG_WARNING 433 << this << " Error while reading: " << ec.message(); 434 } 435 else 436 { 437 BMCWEB_LOG_ERROR 438 << this << " Error while reading: " << ec.message(); 439 } 440 } 441 else 442 { 443 // if the adaptor isn't open anymore, and wasn't handed to a 444 // websocket, treat as an error 445 if (!isAlive() && 446 !boost::beast::websocket::is_upgrade(parser->get())) 447 { 448 errorWhileReading = true; 449 } 450 } 451 452 cancelDeadlineTimer(); 453 454 if (errorWhileReading) 455 { 456 close(); 457 BMCWEB_LOG_DEBUG << this << " from read(1)"; 458 return; 459 } 460 461 readClientIp(); 462 463 boost::asio::ip::address ip; 464 if (getClientIp(ip)) 465 { 466 BMCWEB_LOG_DEBUG << "Unable to get client IP"; 467 } 468 sessionIsFromTransport = false; 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(), userSession); 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 doRead(); 492 }); 493 } 494 495 void doRead() 496 { 497 BMCWEB_LOG_DEBUG << this << " doRead"; 498 startDeadline(); 499 boost::beast::http::async_read(adaptor, buffer, *parser, 500 [this, self(shared_from_this())]( 501 const boost::system::error_code& ec, 502 std::size_t bytesTransferred) { 503 BMCWEB_LOG_DEBUG << this << " async_read " << bytesTransferred 504 << " Bytes"; 505 cancelDeadlineTimer(); 506 if (ec) 507 { 508 BMCWEB_LOG_ERROR << this 509 << " Error while reading: " << ec.message(); 510 close(); 511 BMCWEB_LOG_DEBUG << this << " from read(1)"; 512 return; 513 } 514 handle(); 515 }); 516 } 517 518 void doWrite(crow::Response& thisRes) 519 { 520 BMCWEB_LOG_DEBUG << this << " doWrite"; 521 thisRes.preparePayload(); 522 serializer.emplace(*thisRes.stringResponse); 523 startDeadline(); 524 boost::beast::http::async_write(adaptor, *serializer, 525 [this, self(shared_from_this())]( 526 const boost::system::error_code& ec, 527 std::size_t bytesTransferred) { 528 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred 529 << " bytes"; 530 531 cancelDeadlineTimer(); 532 533 if (ec) 534 { 535 BMCWEB_LOG_DEBUG << this << " from write(2)"; 536 return; 537 } 538 if (!keepAlive) 539 { 540 close(); 541 BMCWEB_LOG_DEBUG << this << " from write(1)"; 542 return; 543 } 544 545 serializer.reset(); 546 BMCWEB_LOG_DEBUG << this << " Clearing response"; 547 res.clear(); 548 parser.emplace(std::piecewise_construct, std::make_tuple()); 549 parser->body_limit(httpReqBodyLimit); // reset body limit for 550 // newly created parser 551 buffer.consume(buffer.size()); 552 553 // If the session was built from the transport, we don't need to 554 // clear it. All other sessions are generated per request. 555 if (!sessionIsFromTransport) 556 { 557 userSession = nullptr; 558 } 559 560 // Destroy the Request via the std::optional 561 req.reset(); 562 doReadHeaders(); 563 }); 564 } 565 566 void cancelDeadlineTimer() 567 { 568 timer.cancel(); 569 } 570 571 void startDeadline() 572 { 573 cancelDeadlineTimer(); 574 575 std::chrono::seconds timeout(15); 576 // allow slow uploads for logged in users 577 bool loggedIn = userSession != nullptr; 578 if (loggedIn) 579 { 580 timeout = std::chrono::seconds(60); 581 return; 582 } 583 584 std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this(); 585 timer.expires_after(timeout); 586 timer.async_wait([weakSelf](const boost::system::error_code ec) { 587 // Note, we are ignoring other types of errors here; If the timer 588 // failed for any reason, we should still close the connection 589 590 std::shared_ptr<Connection<Adaptor, Handler>> self = 591 weakSelf.lock(); 592 if (!self) 593 { 594 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 595 return; 596 } 597 if (ec == boost::asio::error::operation_aborted) 598 { 599 // Canceled wait means the path succeeeded. 600 return; 601 } 602 if (ec) 603 { 604 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec; 605 } 606 607 BMCWEB_LOG_WARNING << self << "Connection timed out, closing"; 608 609 self->close(); 610 }); 611 612 BMCWEB_LOG_DEBUG << this << " timer started"; 613 } 614 615 Adaptor adaptor; 616 Handler* handler; 617 // Making this a std::optional allows it to be efficiently destroyed and 618 // re-created on Connection reset 619 std::optional< 620 boost::beast::http::request_parser<boost::beast::http::string_body>> 621 parser; 622 623 boost::beast::flat_static_buffer<8192> buffer; 624 625 std::optional<boost::beast::http::response_serializer< 626 boost::beast::http::string_body>> 627 serializer; 628 629 std::optional<crow::Request> req; 630 crow::Response res; 631 632 bool sessionIsFromTransport = false; 633 std::shared_ptr<persistent_data::UserSession> userSession; 634 635 boost::asio::steady_timer timer; 636 637 bool keepAlive = true; 638 639 std::function<std::string()>& getCachedDateStr; 640 641 using std::enable_shared_from_this< 642 Connection<Adaptor, Handler>>::shared_from_this; 643 644 using std::enable_shared_from_this< 645 Connection<Adaptor, Handler>>::weak_from_this; 646 }; 647 } // namespace crow 648