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