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