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