xref: /openbmc/bmcweb/http/http_connection.hpp (revision 3bfa3b29c0515a9e77c7c69fe072b7ff2e0fc302)
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 "http2_connection.hpp"
8 #include "http_body.hpp"
9 #include "http_response.hpp"
10 #include "http_utility.hpp"
11 #include "logging.hpp"
12 #include "mutual_tls.hpp"
13 #include "ssl_key_handler.hpp"
14 #include "str_utility.hpp"
15 #include "utility.hpp"
16 
17 #include <boost/asio/io_context.hpp>
18 #include <boost/asio/ip/tcp.hpp>
19 #include <boost/asio/ssl/stream.hpp>
20 #include <boost/asio/steady_timer.hpp>
21 #include <boost/beast/_experimental/test/stream.hpp>
22 #include <boost/beast/core/buffers_generator.hpp>
23 #include <boost/beast/core/flat_static_buffer.hpp>
24 #include <boost/beast/http/error.hpp>
25 #include <boost/beast/http/message_generator.hpp>
26 #include <boost/beast/http/parser.hpp>
27 #include <boost/beast/http/read.hpp>
28 #include <boost/beast/http/write.hpp>
29 #include <boost/beast/websocket.hpp>
30 
31 #include <atomic>
32 #include <chrono>
33 #include <vector>
34 
35 namespace crow
36 {
37 
38 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
39 static int connectionCount = 0;
40 
41 // request body limit size set by the bmcwebHttpReqBodyLimitMb option
42 constexpr uint64_t httpReqBodyLimit = 1024UL * 1024UL *
43                                       bmcwebHttpReqBodyLimitMb;
44 
45 constexpr uint64_t loggedOutPostBodyLimit = 4096U;
46 
47 constexpr uint32_t httpHeaderLimit = 8192U;
48 
49 template <typename>
50 struct IsTls : std::false_type
51 {};
52 
53 template <typename T>
54 struct IsTls<boost::asio::ssl::stream<T>> : std::true_type
55 {};
56 
57 template <typename Adaptor, typename Handler>
58 class Connection :
59     public std::enable_shared_from_this<Connection<Adaptor, Handler>>
60 {
61     using self_type = Connection<Adaptor, Handler>;
62 
63   public:
64     Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn,
65                std::function<std::string()>& getCachedDateStrF,
66                Adaptor adaptorIn) :
67         adaptor(std::move(adaptorIn)),
68         handler(handlerIn), timer(std::move(timerIn)),
69         getCachedDateStr(getCachedDateStrF)
70     {
71         initParser();
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("{} Connection created, total {}", logPtr(this),
80                          connectionCount);
81     }
82 
83     ~Connection()
84     {
85         res.releaseCompleteRequestHandler();
86         cancelDeadlineTimer();
87 
88         connectionCount--;
89         BMCWEB_LOG_DEBUG("{} Connection closed, total {}", logPtr(this),
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             mtlsSession = verifyMtlsUser(ip, ctx);
106             if (mtlsSession)
107             {
108                 BMCWEB_LOG_DEBUG("{} Generating TLS session: {}", logPtr(this),
109                                  mtlsSession->uniqueId);
110             }
111         }
112         return true;
113     }
114 
115     void prepareMutualTls()
116     {
117         if constexpr (IsTls<Adaptor>::value)
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("{} failed to set SSL id", logPtr(this));
139                 }
140             }
141 
142             adaptor.set_verify_callback(
143                 std::bind_front(&self_type::tlsVerifyCallback, this));
144         }
145     }
146 
147     Adaptor& socket()
148     {
149         return adaptor;
150     }
151 
152     void start()
153     {
154         BMCWEB_LOG_DEBUG("{} Connection started, total {}", logPtr(this),
155                          connectionCount);
156         if (connectionCount >= 200)
157         {
158             BMCWEB_LOG_CRITICAL("{} Max connection count exceeded.",
159                                 logPtr(this));
160             return;
161         }
162 
163         startDeadline();
164 
165         readClientIp();
166 
167         // TODO(ed) Abstract this to a more clever class with the idea of an
168         // asynchronous "start"
169         if constexpr (IsTls<Adaptor>::value)
170         {
171             adaptor.async_handshake(boost::asio::ssl::stream_base::server,
172                                     [this, self(shared_from_this())](
173                                         const boost::system::error_code& ec) {
174                 if (ec)
175                 {
176                     return;
177                 }
178                 afterSslHandshake();
179             });
180         }
181         else
182         {
183             doReadHeaders();
184         }
185     }
186 
187     void afterSslHandshake()
188     {
189         // If http2 is enabled, negotiate the protocol
190         if constexpr (bmcwebEnableHTTP2)
191         {
192             const unsigned char* alpn = nullptr;
193             unsigned int alpnlen = 0;
194             SSL_get0_alpn_selected(adaptor.native_handle(), &alpn, &alpnlen);
195             if (alpn != nullptr)
196             {
197                 std::string_view selectedProtocol(
198                     std::bit_cast<const char*>(alpn), alpnlen);
199                 BMCWEB_LOG_DEBUG("ALPN selected protocol \"{}\" len: {}",
200                                  selectedProtocol, alpnlen);
201                 if (selectedProtocol == "h2")
202                 {
203                     auto http2 =
204                         std::make_shared<HTTP2Connection<Adaptor, Handler>>(
205                             std::move(adaptor), handler, getCachedDateStr);
206                     http2->start();
207                     return;
208                 }
209             }
210         }
211 
212         doReadHeaders();
213     }
214 
215     void initParser()
216     {
217         boost::beast::http::request_parser<bmcweb::HttpBody>& instance =
218             parser.emplace(std::piecewise_construct, std::make_tuple());
219 
220         // reset header limit for newly created parser
221         instance.header_limit(httpHeaderLimit);
222 
223         // Initially set no body limit. We don't yet know if the user is
224         // authenticated.
225         instance.body_limit(boost::none);
226     }
227 
228     void handle()
229     {
230         std::error_code reqEc;
231         if (!parser)
232         {
233             return;
234         }
235         req = crow::Request(parser->release(), reqEc);
236         if (reqEc)
237         {
238             BMCWEB_LOG_DEBUG("Request failed to construct{}", reqEc.message());
239             res.result(boost::beast::http::status::bad_request);
240             completeRequest(res);
241             return;
242         }
243         req.session = userSession;
244 
245         // Fetch the client IP address
246         req.ipAddress = ip;
247 
248         // Check for HTTP version 1.1.
249         if (req.version() == 11)
250         {
251             if (req.getHeaderValue(boost::beast::http::field::host).empty())
252             {
253                 res.result(boost::beast::http::status::bad_request);
254                 completeRequest(res);
255                 return;
256             }
257         }
258 
259         BMCWEB_LOG_INFO("Request:  {} HTTP/{}.{} {} {} {}", logPtr(this),
260                         req.version() / 10, req.version() % 10,
261                         req.methodString(), req.target(),
262                         req.ipAddress.to_string());
263 
264         req.ioService = static_cast<decltype(req.ioService)>(
265             &adaptor.get_executor().context());
266 
267         if (res.completed)
268         {
269             completeRequest(res);
270             return;
271         }
272         keepAlive = req.keepAlive();
273         if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
274         {
275 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
276             if (!crow::authentication::isOnAllowlist(req.url().path(),
277                                                      req.method()) &&
278                 req.session == nullptr)
279             {
280                 BMCWEB_LOG_WARNING("Authentication failed");
281                 forward_unauthorized::sendUnauthorized(
282                     req.url().encoded_path(),
283                     req.getHeaderValue("X-Requested-With"),
284                     req.getHeaderValue("Accept"), res);
285                 completeRequest(res);
286                 return;
287             }
288 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
289         }
290         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
291         BMCWEB_LOG_DEBUG("Setting completion handler");
292         asyncResp->res.setCompleteRequestHandler(
293             [self(shared_from_this())](crow::Response& thisRes) {
294             self->completeRequest(thisRes);
295         });
296         bool isSse =
297             isContentTypeAllowed(req.getHeaderValue("Accept"),
298                                  http_helpers::ContentType::EventStream, false);
299         std::string_view upgradeType(
300             req.getHeaderValue(boost::beast::http::field::upgrade));
301         if ((req.isUpgrade() &&
302              bmcweb::asciiIEquals(upgradeType, "websocket")) ||
303             isSse)
304         {
305             asyncResp->res.setCompleteRequestHandler(
306                 [self(shared_from_this())](crow::Response& thisRes) {
307                 if (thisRes.result() != boost::beast::http::status::ok)
308                 {
309                     // When any error occurs before handle upgradation,
310                     // the result in response will be set to respective
311                     // error. By default the Result will be OK (200),
312                     // which implies successful handle upgrade. Response
313                     // needs to be sent over this connection only on
314                     // failure.
315                     self->completeRequest(thisRes);
316                     return;
317                 }
318             });
319             handler->handleUpgrade(req, asyncResp, std::move(adaptor));
320             return;
321         }
322         std::string_view expected =
323             req.getHeaderValue(boost::beast::http::field::if_none_match);
324         if (!expected.empty())
325         {
326             res.setExpectedHash(expected);
327         }
328         handler->handle(req, asyncResp);
329     }
330 
331     void hardClose()
332     {
333         BMCWEB_LOG_DEBUG("{} Closing socket", logPtr(this));
334         boost::beast::get_lowest_layer(adaptor).close();
335     }
336 
337     void tlsShutdownComplete(const std::shared_ptr<self_type>& self,
338                              const boost::system::error_code& ec)
339     {
340         if (ec)
341         {
342             BMCWEB_LOG_WARNING("{} Failed to shut down TLS cleanly {}",
343                                logPtr(self.get()), ec);
344         }
345         self->hardClose();
346     }
347 
348     void gracefulClose()
349     {
350         BMCWEB_LOG_DEBUG("{} Socket close requested", logPtr(this));
351         if (mtlsSession != nullptr)
352         {
353             BMCWEB_LOG_DEBUG("{} Removing TLS session: {}", logPtr(this),
354                              mtlsSession->uniqueId);
355             persistent_data::SessionStore::getInstance().removeSession(
356                 mtlsSession);
357         }
358         if constexpr (IsTls<Adaptor>::value)
359         {
360             adaptor.async_shutdown(std::bind_front(
361                 &self_type::tlsShutdownComplete, this, shared_from_this()));
362         }
363         else
364         {
365             hardClose();
366         }
367     }
368 
369     void completeRequest(crow::Response& thisRes)
370     {
371         res = std::move(thisRes);
372         res.keepAlive(keepAlive);
373 
374         completeResponseFields(req, res);
375         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
376 
377         doWrite();
378 
379         // delete lambda with self shared_ptr
380         // to enable connection destruction
381         res.setCompleteRequestHandler(nullptr);
382     }
383 
384     void readClientIp()
385     {
386         boost::system::error_code ec;
387 
388         if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
389         {
390             boost::asio::ip::tcp::endpoint endpoint =
391                 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
392 
393             if (ec)
394             {
395                 // If remote endpoint fails keep going. "ClientOriginIPAddress"
396                 // will be empty.
397                 BMCWEB_LOG_ERROR(
398                     "Failed to get the client's IP Address. ec : {}", ec);
399                 return;
400             }
401             ip = endpoint.address();
402         }
403     }
404 
405   private:
406     uint64_t getContentLengthLimit()
407     {
408 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
409         if (userSession == nullptr)
410         {
411             return loggedOutPostBodyLimit;
412         }
413 #endif
414 
415         return httpReqBodyLimit;
416     }
417 
418     // Returns true if content length was within limits
419     // Returns false if content length error has been returned
420     bool handleContentLengthError()
421     {
422         if (!parser)
423         {
424             BMCWEB_LOG_CRITICAL("Paser was null");
425             return false;
426         }
427         const boost::optional<uint64_t> contentLength =
428             parser->content_length();
429         if (!contentLength)
430         {
431             BMCWEB_LOG_DEBUG("{} No content length available", logPtr(this));
432             return true;
433         }
434 
435         uint64_t maxAllowedContentLength = getContentLengthLimit();
436 
437         if (*contentLength > maxAllowedContentLength)
438         {
439             // If the users content limit is between the logged in
440             // and logged out limits They probably just didn't log
441             // in
442             if (*contentLength > loggedOutPostBodyLimit &&
443                 *contentLength < httpReqBodyLimit)
444             {
445                 BMCWEB_LOG_DEBUG(
446                     "{} Content length {} valid, but greater than logged out"
447                     " limit of {}. Setting unauthorized",
448                     logPtr(this), *contentLength, loggedOutPostBodyLimit);
449                 res.result(boost::beast::http::status::unauthorized);
450             }
451             else
452             {
453                 // Otherwise they're over both limits, so inform
454                 // them
455                 BMCWEB_LOG_DEBUG(
456                     "{} Content length {} was greater than global limit {}."
457                     " Setting payload too large",
458                     logPtr(this), *contentLength, httpReqBodyLimit);
459                 res.result(boost::beast::http::status::payload_too_large);
460             }
461 
462             keepAlive = false;
463             doWrite();
464             return false;
465         }
466 
467         return true;
468     }
469 
470     void doReadHeaders()
471     {
472         BMCWEB_LOG_DEBUG("{} doReadHeaders", logPtr(this));
473         if (!parser)
474         {
475             BMCWEB_LOG_CRITICAL("Parser was not initialized.");
476             return;
477         }
478         // Clean up any previous Connection.
479         boost::beast::http::async_read_header(
480             adaptor, buffer, *parser,
481             [this,
482              self(shared_from_this())](const boost::system::error_code& ec,
483                                        std::size_t bytesTransferred) {
484             BMCWEB_LOG_DEBUG("{} async_read_header {} Bytes", logPtr(this),
485                              bytesTransferred);
486 
487             if (ec)
488             {
489                 cancelDeadlineTimer();
490 
491                 if (ec == boost::beast::http::error::header_limit)
492                 {
493                     BMCWEB_LOG_ERROR("{} Header field too large, closing",
494                                      logPtr(this), ec.message());
495 
496                     res.result(boost::beast::http::status::
497                                    request_header_fields_too_large);
498                     keepAlive = false;
499                     doWrite();
500                     return;
501                 }
502                 if (ec == boost::beast::http::error::end_of_stream)
503                 {
504                     BMCWEB_LOG_WARNING("{} End of stream, closing {}",
505                                        logPtr(this), ec);
506                     hardClose();
507                     return;
508                 }
509 
510                 BMCWEB_LOG_DEBUG("{} Closing socket due to read error {}",
511                                  logPtr(this), ec.message());
512                 gracefulClose();
513 
514                 return;
515             }
516 
517             std::string_view expect =
518                 req.getHeaderValue(boost::beast::http::field::expect);
519             if (bmcweb::asciiIEquals(expect, "100-continue"))
520             {
521                 res.result(boost::beast::http::status::continue_);
522                 doWrite();
523                 return;
524             }
525 
526             if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
527             {
528 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
529                 boost::beast::http::verb method = parser->get().method();
530                 userSession = crow::authentication::authenticate(
531                     ip, res, method, parser->get().base(), mtlsSession);
532 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
533             }
534 
535             if (!handleContentLengthError())
536             {
537                 return;
538             }
539 
540             parser->body_limit(getContentLengthLimit());
541 
542             if (parser->is_done())
543             {
544                 handle();
545                 return;
546             }
547 
548             doRead();
549         });
550     }
551 
552     void doRead()
553     {
554         BMCWEB_LOG_DEBUG("{} doRead", logPtr(this));
555         if (!parser)
556         {
557             return;
558         }
559         startDeadline();
560         boost::beast::http::async_read_some(
561             adaptor, buffer, *parser,
562             [this,
563              self(shared_from_this())](const boost::system::error_code& ec,
564                                        std::size_t bytesTransferred) {
565             BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this),
566                              bytesTransferred);
567 
568             if (ec)
569             {
570                 BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
571                                  ec.message());
572                 if (ec == boost::beast::http::error::body_limit)
573                 {
574                     if (handleContentLengthError())
575                     {
576                         BMCWEB_LOG_CRITICAL("Body length limit reached, "
577                                             "but no content-length "
578                                             "available?  Should never happen");
579                         res.result(
580                             boost::beast::http::status::internal_server_error);
581                         keepAlive = false;
582                         doWrite();
583                     }
584                     return;
585                 }
586 
587                 gracefulClose();
588                 return;
589             }
590 
591             // If the user is logged in, allow them to send files incrementally
592             // one piece at a time. If authentication is disabled then there is
593             // no user session hence always allow to send one piece at a time.
594             if (userSession != nullptr)
595             {
596                 cancelDeadlineTimer();
597             }
598             if (!parser->is_done())
599             {
600                 doRead();
601                 return;
602             }
603 
604             cancelDeadlineTimer();
605             handle();
606         });
607     }
608 
609     void afterDoWrite(const std::shared_ptr<self_type>& /*self*/,
610                       const boost::system::error_code& ec,
611                       std::size_t bytesTransferred)
612     {
613         BMCWEB_LOG_DEBUG("{} async_write wrote {} bytes, ec=", logPtr(this),
614                          bytesTransferred, ec);
615 
616         cancelDeadlineTimer();
617 
618         if (ec)
619         {
620             BMCWEB_LOG_DEBUG("{} from write(2)", logPtr(this));
621             return;
622         }
623 
624         if (res.result() == boost::beast::http::status::continue_)
625         {
626             // Reset the result to ok
627             res.result(boost::beast::http::status::ok);
628             doRead();
629             return;
630         }
631 
632         if (!keepAlive)
633         {
634             BMCWEB_LOG_DEBUG("{} keepalive not set.  Closing socket",
635                              logPtr(this));
636 
637             gracefulClose();
638             return;
639         }
640 
641         BMCWEB_LOG_DEBUG("{} Clearing response", logPtr(this));
642         res.clear();
643         initParser();
644 
645         userSession = nullptr;
646 
647         // Destroy the Request via the std::optional
648         req.clear();
649         doReadHeaders();
650     }
651 
652     void doWrite()
653     {
654         BMCWEB_LOG_DEBUG("{} doWrite", logPtr(this));
655         res.preparePayload();
656 
657         startDeadline();
658         boost::beast::async_write(
659             adaptor,
660             boost::beast::http::message_generator(std::move(res.response)),
661             std::bind_front(&self_type::afterDoWrite, this,
662                             shared_from_this()));
663     }
664 
665     void cancelDeadlineTimer()
666     {
667         timer.cancel();
668     }
669 
670     void startDeadline()
671     {
672         // Timer is already started so no further action is required.
673         if (timerStarted)
674         {
675             return;
676         }
677 
678         std::chrono::seconds timeout(15);
679 
680         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
681         timer.expires_after(timeout);
682         timer.async_wait([weakSelf](const boost::system::error_code& ec) {
683             // Note, we are ignoring other types of errors here;  If the timer
684             // failed for any reason, we should still close the connection
685             std::shared_ptr<Connection<Adaptor, Handler>> self =
686                 weakSelf.lock();
687             if (!self)
688             {
689                 if (ec == boost::asio::error::operation_aborted)
690                 {
691                     BMCWEB_LOG_DEBUG(
692                         "{} Timer canceled on connection being destroyed",
693                         logPtr(self.get()));
694                     return;
695                 }
696                 BMCWEB_LOG_CRITICAL("{} Failed to capture connection",
697                                     logPtr(self.get()));
698                 return;
699             }
700 
701             self->timerStarted = false;
702 
703             if (ec)
704             {
705                 if (ec == boost::asio::error::operation_aborted)
706                 {
707                     BMCWEB_LOG_DEBUG("{} Timer canceled", logPtr(self.get()));
708                     return;
709                 }
710                 BMCWEB_LOG_CRITICAL("{} Timer failed {}", logPtr(self.get()),
711                                     ec);
712             }
713 
714             BMCWEB_LOG_WARNING("{} Connection timed out, hard closing",
715                                logPtr(self.get()));
716 
717             self->hardClose();
718         });
719 
720         timerStarted = true;
721         BMCWEB_LOG_DEBUG("{} timer started", logPtr(this));
722     }
723 
724     Adaptor adaptor;
725     Handler* handler;
726 
727     boost::asio::ip::address ip;
728 
729     // Making this a std::optional allows it to be efficiently destroyed and
730     // re-created on Connection reset
731     std::optional<boost::beast::http::request_parser<bmcweb::HttpBody>> parser;
732 
733     boost::beast::flat_static_buffer<8192> buffer;
734 
735     crow::Request req;
736     crow::Response res;
737 
738     std::shared_ptr<persistent_data::UserSession> userSession;
739     std::shared_ptr<persistent_data::UserSession> mtlsSession;
740 
741     boost::asio::steady_timer timer;
742 
743     bool keepAlive = true;
744 
745     bool timerStarted = false;
746 
747     std::function<std::string()>& getCachedDateStr;
748 
749     using std::enable_shared_from_this<
750         Connection<Adaptor, Handler>>::shared_from_this;
751 
752     using std::enable_shared_from_this<
753         Connection<Adaptor, Handler>>::weak_from_this;
754 };
755 } // namespace crow
756