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 res.keepAlive(thisReq.keepAlive()); 193 194 // Fetch the client IP address 195 readClientIp(); 196 197 // Check for HTTP version 1.1. 198 if (thisReq.version() == 11) 199 { 200 if (thisReq.getHeaderValue(boost::beast::http::field::host).empty()) 201 { 202 res.result(boost::beast::http::status::bad_request); 203 completeRequest(res); 204 return; 205 } 206 } 207 208 BMCWEB_LOG_INFO << "Request: " 209 << " " << this << " HTTP/" << thisReq.version() / 10 210 << "." << thisReq.version() % 10 << ' ' 211 << thisReq.methodString() << " " << thisReq.target() 212 << " " << thisReq.ipAddress.to_string(); 213 214 res.isAliveHelper = [this]() -> bool { return isAlive(); }; 215 216 thisReq.ioService = static_cast<decltype(thisReq.ioService)>( 217 &adaptor.get_executor().context()); 218 219 if (res.completed) 220 { 221 completeRequest(res); 222 return; 223 } 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 306 BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' ' 307 << res.resultInt() << " keepalive=" << res.keepAlive(); 308 309 addSecurityHeaders(*req, res); 310 311 crow::authentication::cleanupTempSession(*req); 312 313 if (!isAlive()) 314 { 315 // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " << 316 // isReading 317 // << ' ' << isWriting; 318 // delete this; 319 320 // delete lambda with self shared_ptr 321 // to enable connection destruction 322 res.setCompleteRequestHandler(nullptr); 323 return; 324 } 325 326 res.setHashAndHandleNotModified(); 327 328 if (res.body().empty() && !res.jsonValue.empty()) 329 { 330 using http_helpers::ContentType; 331 std::array<ContentType, 3> allowed{ 332 ContentType::CBOR, ContentType::JSON, ContentType::HTML}; 333 ContentType prefered = 334 getPreferedContentType(req->getHeaderValue("Accept"), allowed); 335 336 if (prefered == ContentType::HTML) 337 { 338 prettyPrintJson(res); 339 } 340 else if (prefered == ContentType::CBOR) 341 { 342 res.addHeader(boost::beast::http::field::content_type, 343 "application/cbor"); 344 nlohmann::json::to_cbor(res.jsonValue, res.body()); 345 } 346 else 347 { 348 // Technically prefered could also be NoMatch here, but we'd 349 // like to default to something rather than return 400 for 350 // backward compatibility. 351 res.addHeader(boost::beast::http::field::content_type, 352 "application/json"); 353 res.body() = res.jsonValue.dump( 354 2, ' ', true, nlohmann::json::error_handler_t::replace); 355 } 356 } 357 358 if (res.resultInt() >= 400 && res.body().empty()) 359 { 360 res.body() = std::string(res.reason()); 361 } 362 363 if (res.result() == boost::beast::http::status::no_content) 364 { 365 // Boost beast throws if content is provided on a no-content 366 // response. Ideally, this would never happen, but in the case that 367 // it does, we don't want to throw. 368 BMCWEB_LOG_CRITICAL 369 << this << " Response content provided but code was no-content"; 370 res.body().clear(); 371 } 372 373 res.addHeader(boost::beast::http::field::date, getCachedDateStr()); 374 375 doWrite(res); 376 377 // delete lambda with self shared_ptr 378 // to enable connection destruction 379 res.setCompleteRequestHandler(nullptr); 380 } 381 382 void readClientIp() 383 { 384 boost::asio::ip::address ip; 385 boost::system::error_code ec = getClientIp(ip); 386 if (ec) 387 { 388 return; 389 } 390 req->ipAddress = ip; 391 } 392 393 boost::system::error_code getClientIp(boost::asio::ip::address& ip) 394 { 395 boost::system::error_code ec; 396 BMCWEB_LOG_DEBUG << "Fetch the client IP address"; 397 boost::asio::ip::tcp::endpoint endpoint = 398 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec); 399 400 if (ec) 401 { 402 // If remote endpoint fails keep going. "ClientOriginIPAddress" 403 // will be empty. 404 BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : " 405 << ec; 406 return ec; 407 } 408 ip = endpoint.address(); 409 return ec; 410 } 411 412 private: 413 void doReadHeaders() 414 { 415 BMCWEB_LOG_DEBUG << this << " doReadHeaders"; 416 417 // Clean up any previous Connection. 418 boost::beast::http::async_read_header( 419 adaptor, buffer, *parser, 420 [this, 421 self(shared_from_this())](const boost::system::error_code& ec, 422 std::size_t bytesTransferred) { 423 BMCWEB_LOG_DEBUG << this << " async_read_header " 424 << bytesTransferred << " Bytes"; 425 bool errorWhileReading = false; 426 if (ec) 427 { 428 errorWhileReading = true; 429 if (ec == boost::beast::http::error::end_of_stream) 430 { 431 BMCWEB_LOG_WARNING 432 << this << " Error while reading: " << ec.message(); 433 } 434 else 435 { 436 BMCWEB_LOG_ERROR 437 << this << " Error while reading: " << ec.message(); 438 } 439 } 440 else 441 { 442 // if the adaptor isn't open anymore, and wasn't handed to a 443 // websocket, treat as an error 444 if (!isAlive() && 445 !boost::beast::websocket::is_upgrade(parser->get())) 446 { 447 errorWhileReading = true; 448 } 449 } 450 451 cancelDeadlineTimer(); 452 453 if (errorWhileReading) 454 { 455 close(); 456 BMCWEB_LOG_DEBUG << this << " from read(1)"; 457 return; 458 } 459 460 readClientIp(); 461 462 boost::asio::ip::address ip; 463 if (getClientIp(ip)) 464 { 465 BMCWEB_LOG_DEBUG << "Unable to get client IP"; 466 } 467 sessionIsFromTransport = false; 468 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX 469 boost::beast::http::verb method = parser->get().method(); 470 userSession = crow::authentication::authenticate( 471 ip, res, method, parser->get().base(), userSession); 472 473 bool loggedIn = userSession != nullptr; 474 if (!loggedIn) 475 { 476 const boost::optional<uint64_t> contentLength = 477 parser->content_length(); 478 if (contentLength && *contentLength > loggedOutPostBodyLimit) 479 { 480 BMCWEB_LOG_DEBUG << "Content length greater than limit " 481 << *contentLength; 482 close(); 483 return; 484 } 485 486 BMCWEB_LOG_DEBUG << "Starting quick deadline"; 487 } 488 #endif // BMCWEB_INSECURE_DISABLE_AUTHX 489 490 doRead(); 491 }); 492 } 493 494 void doRead() 495 { 496 BMCWEB_LOG_DEBUG << this << " doRead"; 497 startDeadline(); 498 boost::beast::http::async_read(adaptor, buffer, *parser, 499 [this, self(shared_from_this())]( 500 const boost::system::error_code& ec, 501 std::size_t bytesTransferred) { 502 BMCWEB_LOG_DEBUG << this << " async_read " << bytesTransferred 503 << " Bytes"; 504 cancelDeadlineTimer(); 505 if (ec) 506 { 507 BMCWEB_LOG_ERROR << this 508 << " Error while reading: " << ec.message(); 509 close(); 510 BMCWEB_LOG_DEBUG << this << " from read(1)"; 511 return; 512 } 513 handle(); 514 }); 515 } 516 517 void doWrite(crow::Response& thisRes) 518 { 519 BMCWEB_LOG_DEBUG << this << " doWrite"; 520 thisRes.preparePayload(); 521 serializer.emplace(*thisRes.stringResponse); 522 startDeadline(); 523 boost::beast::http::async_write(adaptor, *serializer, 524 [this, self(shared_from_this())]( 525 const boost::system::error_code& ec, 526 std::size_t bytesTransferred) { 527 BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred 528 << " bytes"; 529 530 cancelDeadlineTimer(); 531 532 if (ec) 533 { 534 BMCWEB_LOG_DEBUG << this << " from write(2)"; 535 return; 536 } 537 if (!serializer->get().keep_alive()) 538 { 539 close(); 540 BMCWEB_LOG_DEBUG << this << " from write(1)"; 541 return; 542 } 543 544 serializer.reset(); 545 BMCWEB_LOG_DEBUG << this << " Clearing response"; 546 res.clear(); 547 parser.emplace(std::piecewise_construct, std::make_tuple()); 548 parser->body_limit(httpReqBodyLimit); // reset body limit for 549 // newly created parser 550 buffer.consume(buffer.size()); 551 552 // If the session was built from the transport, we don't need to 553 // clear it. All other sessions are generated per request. 554 if (!sessionIsFromTransport) 555 { 556 userSession = nullptr; 557 } 558 559 // Destroy the Request via the std::optional 560 req.reset(); 561 doReadHeaders(); 562 }); 563 } 564 565 void cancelDeadlineTimer() 566 { 567 timer.cancel(); 568 } 569 570 void startDeadline() 571 { 572 cancelDeadlineTimer(); 573 574 std::chrono::seconds timeout(15); 575 // allow slow uploads for logged in users 576 bool loggedIn = userSession != nullptr; 577 if (loggedIn) 578 { 579 timeout = std::chrono::seconds(60); 580 return; 581 } 582 583 std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this(); 584 timer.expires_after(timeout); 585 timer.async_wait([weakSelf](const boost::system::error_code ec) { 586 // Note, we are ignoring other types of errors here; If the timer 587 // failed for any reason, we should still close the connection 588 589 std::shared_ptr<Connection<Adaptor, Handler>> self = 590 weakSelf.lock(); 591 if (!self) 592 { 593 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection"; 594 return; 595 } 596 if (ec == boost::asio::error::operation_aborted) 597 { 598 // Canceled wait means the path succeeeded. 599 return; 600 } 601 if (ec) 602 { 603 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec; 604 } 605 606 BMCWEB_LOG_WARNING << self << "Connection timed out, closing"; 607 608 self->close(); 609 }); 610 611 BMCWEB_LOG_DEBUG << this << " timer started"; 612 } 613 614 Adaptor adaptor; 615 Handler* handler; 616 // Making this a std::optional allows it to be efficiently destroyed and 617 // re-created on Connection reset 618 std::optional< 619 boost::beast::http::request_parser<boost::beast::http::string_body>> 620 parser; 621 622 boost::beast::flat_static_buffer<8192> buffer; 623 624 std::optional<boost::beast::http::response_serializer< 625 boost::beast::http::string_body>> 626 serializer; 627 628 std::optional<crow::Request> req; 629 crow::Response res; 630 631 bool sessionIsFromTransport = false; 632 std::shared_ptr<persistent_data::UserSession> userSession; 633 634 boost::asio::steady_timer timer; 635 636 std::function<std::string()>& getCachedDateStr; 637 638 using std::enable_shared_from_this< 639 Connection<Adaptor, Handler>>::shared_from_this; 640 641 using std::enable_shared_from_this< 642 Connection<Adaptor, Handler>>::weak_from_this; 643 }; 644 } // namespace crow 645