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