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