xref: /openbmc/bmcweb/http/http_connection.hpp (revision 2ae81db9)
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_response.hpp"
9 #include "http_utility.hpp"
10 #include "logging.hpp"
11 #include "mutual_tls.hpp"
12 #include "ssl_key_handler.hpp"
13 #include "str_utility.hpp"
14 #include "utility.hpp"
15 
16 #include <boost/asio/io_context.hpp>
17 #include <boost/asio/ip/tcp.hpp>
18 #include <boost/asio/ssl/stream.hpp>
19 #include <boost/asio/steady_timer.hpp>
20 #include <boost/beast/_experimental/test/stream.hpp>
21 #include <boost/beast/core/buffers_generator.hpp>
22 #include <boost/beast/core/flat_static_buffer.hpp>
23 #include <boost/beast/http/error.hpp>
24 #include <boost/beast/http/parser.hpp>
25 #include <boost/beast/http/read.hpp>
26 #include <boost/beast/http/write.hpp>
27 #include <boost/beast/ssl/ssl_stream.hpp>
28 #include <boost/beast/websocket.hpp>
29 
30 #include <atomic>
31 #include <chrono>
32 #include <vector>
33 
34 namespace crow
35 {
36 
37 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
38 static int connectionCount = 0;
39 
40 // request body limit size set by the bmcwebHttpReqBodyLimitMb option
41 constexpr uint64_t httpReqBodyLimit = 1024UL * 1024UL *
42                                       bmcwebHttpReqBodyLimitMb;
43 
44 constexpr uint64_t loggedOutPostBodyLimit = 4096;
45 
46 constexpr uint32_t httpHeaderLimit = 8192;
47 
48 template <typename>
49 struct IsTls : std::false_type
50 {};
51 
52 template <typename T>
53 struct IsTls<boost::beast::ssl_stream<T>> : std::true_type
54 {};
55 
56 template <typename Adaptor, typename Handler>
57 class Connection :
58     public std::enable_shared_from_this<Connection<Adaptor, Handler>>
59 {
60     using self_type = Connection<Adaptor, Handler>;
61 
62   public:
63     Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn,
64                std::function<std::string()>& getCachedDateStrF,
65                Adaptor adaptorIn) :
66         adaptor(std::move(adaptorIn)),
67         handler(handlerIn), timer(std::move(timerIn)),
68         getCachedDateStr(getCachedDateStrF)
69     {
70         parser.emplace(std::piecewise_construct, std::make_tuple());
71         parser->body_limit(httpReqBodyLimit);
72         parser->header_limit(httpHeaderLimit);
73 
74 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
75         prepareMutualTls();
76 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
77 
78         connectionCount++;
79 
80         BMCWEB_LOG_DEBUG("{} Connection open, total {}", logPtr(this),
81                          connectionCount);
82     }
83 
84     ~Connection()
85     {
86         res.setCompleteRequestHandler(nullptr);
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             boost::asio::ip::address ipAddress;
107             if (getClientIp(ipAddress))
108             {
109                 return true;
110             }
111 
112             mtlsSession = verifyMtlsUser(ipAddress, ctx);
113             if (mtlsSession)
114             {
115                 BMCWEB_LOG_DEBUG("{} Generating TLS session: {}", logPtr(this),
116                                  mtlsSession->uniqueId);
117             }
118         }
119         return true;
120     }
121 
122     void prepareMutualTls()
123     {
124         if constexpr (IsTls<Adaptor>::value)
125         {
126             std::error_code error;
127             std::filesystem::path caPath(ensuressl::trustStorePath);
128             auto caAvailable = !std::filesystem::is_empty(caPath, error);
129             caAvailable = caAvailable && !error;
130             if (caAvailable && persistent_data::SessionStore::getInstance()
131                                    .getAuthMethodsConfig()
132                                    .tls)
133             {
134                 adaptor.set_verify_mode(boost::asio::ssl::verify_peer);
135                 std::string id = "bmcweb";
136 
137                 const char* cStr = id.c_str();
138                 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
139                 const auto* idC = reinterpret_cast<const unsigned char*>(cStr);
140                 int ret = SSL_set_session_id_context(
141                     adaptor.native_handle(), idC,
142                     static_cast<unsigned int>(id.length()));
143                 if (ret == 0)
144                 {
145                     BMCWEB_LOG_ERROR("{} failed to set SSL id", logPtr(this));
146                 }
147             }
148 
149             adaptor.set_verify_callback(
150                 std::bind_front(&self_type::tlsVerifyCallback, this));
151         }
152     }
153 
154     Adaptor& socket()
155     {
156         return adaptor;
157     }
158 
159     void start()
160     {
161         if (connectionCount >= 100)
162         {
163             BMCWEB_LOG_CRITICAL("{}Max connection count exceeded.",
164                                 logPtr(this));
165             return;
166         }
167 
168         startDeadline();
169 
170         // TODO(ed) Abstract this to a more clever class with the idea of an
171         // asynchronous "start"
172         if constexpr (IsTls<Adaptor>::value)
173         {
174             adaptor.async_handshake(boost::asio::ssl::stream_base::server,
175                                     [this, self(shared_from_this())](
176                                         const boost::system::error_code& ec) {
177                 if (ec)
178                 {
179                     return;
180                 }
181                 afterSslHandshake();
182             });
183         }
184         else
185         {
186             doReadHeaders();
187         }
188     }
189 
190     void afterSslHandshake()
191     {
192         // If http2 is enabled, negotiate the protocol
193         if constexpr (bmcwebEnableHTTP2)
194         {
195             const unsigned char* alpn = nullptr;
196             unsigned int alpnlen = 0;
197             SSL_get0_alpn_selected(adaptor.native_handle(), &alpn, &alpnlen);
198             if (alpn != nullptr)
199             {
200                 std::string_view selectedProtocol(
201                     std::bit_cast<const char*>(alpn), alpnlen);
202                 BMCWEB_LOG_DEBUG("ALPN selected protocol \"{}\" len: {}",
203                                  selectedProtocol, alpnlen);
204                 if (selectedProtocol == "h2")
205                 {
206                     auto http2 =
207                         std::make_shared<HTTP2Connection<Adaptor, Handler>>(
208                             std::move(adaptor), handler, getCachedDateStr);
209                     http2->start();
210                     return;
211                 }
212             }
213         }
214 
215         doReadHeaders();
216     }
217 
218     void handle()
219     {
220         std::error_code reqEc;
221         if (!parser)
222         {
223             return;
224         }
225         crow::Request& thisReq = req.emplace(parser->release(), reqEc);
226         if (reqEc)
227         {
228             BMCWEB_LOG_DEBUG("Request failed to construct{}", reqEc.message());
229             res.result(boost::beast::http::status::bad_request);
230             completeRequest(res);
231             return;
232         }
233         thisReq.session = userSession;
234 
235         // Fetch the client IP address
236         readClientIp();
237 
238         // Check for HTTP version 1.1.
239         if (thisReq.version() == 11)
240         {
241             if (thisReq.getHeaderValue(boost::beast::http::field::host).empty())
242             {
243                 res.result(boost::beast::http::status::bad_request);
244                 completeRequest(res);
245                 return;
246             }
247         }
248 
249         BMCWEB_LOG_INFO("Request:  {} HTTP/{}.{} {} {} {}", logPtr(this),
250                         thisReq.version() / 10, thisReq.version() % 10,
251                         thisReq.methodString(), thisReq.target(),
252                         thisReq.ipAddress.to_string());
253 
254         res.isAliveHelper = [this]() -> bool { return isAlive(); };
255 
256         thisReq.ioService = static_cast<decltype(thisReq.ioService)>(
257             &adaptor.get_executor().context());
258 
259         if (res.completed)
260         {
261             completeRequest(res);
262             return;
263         }
264         keepAlive = thisReq.keepAlive();
265         if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
266         {
267 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
268             if (!crow::authentication::isOnAllowlist(req->url().path(),
269                                                      req->method()) &&
270                 thisReq.session == nullptr)
271             {
272                 BMCWEB_LOG_WARNING("Authentication failed");
273                 forward_unauthorized::sendUnauthorized(
274                     req->url().encoded_path(),
275                     req->getHeaderValue("X-Requested-With"),
276                     req->getHeaderValue("Accept"), res);
277                 completeRequest(res);
278                 return;
279             }
280 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
281         }
282         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
283         BMCWEB_LOG_DEBUG("Setting completion handler");
284         asyncResp->res.setCompleteRequestHandler(
285             [self(shared_from_this())](crow::Response& thisRes) {
286             self->completeRequest(thisRes);
287         });
288         bool isSse =
289             isContentTypeAllowed(req->getHeaderValue("Accept"),
290                                  http_helpers::ContentType::EventStream, false);
291         std::string_view upgradeType(
292             thisReq.getHeaderValue(boost::beast::http::field::upgrade));
293         if ((thisReq.isUpgrade() &&
294              bmcweb::asciiIEquals(upgradeType, "websocket")) ||
295             isSse)
296         {
297             asyncResp->res.setCompleteRequestHandler(
298                 [self(shared_from_this())](crow::Response& thisRes) {
299                 if (thisRes.result() != boost::beast::http::status::ok)
300                 {
301                     // When any error occurs before handle upgradation,
302                     // the result in response will be set to respective
303                     // error. By default the Result will be OK (200),
304                     // which implies successful handle upgrade. Response
305                     // needs to be sent over this connection only on
306                     // failure.
307                     self->completeRequest(thisRes);
308                     return;
309                 }
310             });
311             handler->handleUpgrade(thisReq, asyncResp, std::move(adaptor));
312             return;
313         }
314         std::string_view expected =
315             req->getHeaderValue(boost::beast::http::field::if_none_match);
316         if (!expected.empty())
317         {
318             res.setExpectedHash(expected);
319         }
320         handler->handle(thisReq, asyncResp);
321     }
322 
323     bool isAlive()
324     {
325         if constexpr (IsTls<Adaptor>::value)
326         {
327             return adaptor.next_layer().is_open();
328         }
329         else if constexpr (std::is_same_v<Adaptor, boost::beast::test::stream>)
330         {
331             return true;
332         }
333         else
334         {
335             return adaptor.is_open();
336         }
337     }
338     void close()
339     {
340         if constexpr (IsTls<Adaptor>::value)
341         {
342             adaptor.next_layer().close();
343             if (mtlsSession != nullptr)
344             {
345                 BMCWEB_LOG_DEBUG("{} Removing TLS session: {}", logPtr(this),
346                                  mtlsSession->uniqueId);
347                 persistent_data::SessionStore::getInstance().removeSession(
348                     mtlsSession);
349             }
350         }
351         else
352         {
353             adaptor.close();
354         }
355     }
356 
357     void completeRequest(crow::Response& thisRes)
358     {
359         if (!req)
360         {
361             return;
362         }
363         res = std::move(thisRes);
364         res.keepAlive(keepAlive);
365 
366         completeResponseFields(*req, res);
367         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
368         if (!isAlive())
369         {
370             res.setCompleteRequestHandler(nullptr);
371             return;
372         }
373 
374         doWrite(res);
375 
376         // delete lambda with self shared_ptr
377         // to enable connection destruction
378         res.setCompleteRequestHandler(nullptr);
379     }
380 
381     void readClientIp()
382     {
383         boost::asio::ip::address ip;
384         boost::system::error_code ec = getClientIp(ip);
385         if (ec)
386         {
387             return;
388         }
389         if (!req)
390         {
391             return;
392         }
393         req->ipAddress = ip;
394     }
395 
396     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
397     {
398         boost::system::error_code ec;
399         BMCWEB_LOG_DEBUG("Fetch the client IP address");
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 ec;
413             }
414             ip = endpoint.address();
415         }
416         return ec;
417     }
418 
419   private:
420     void doReadHeaders()
421     {
422         BMCWEB_LOG_DEBUG("{} doReadHeaders", logPtr(this));
423         if (!parser)
424         {
425             return;
426         }
427         // Clean up any previous Connection.
428         boost::beast::http::async_read_header(
429             adaptor, buffer, *parser,
430             [this,
431              self(shared_from_this())](const boost::system::error_code& ec,
432                                        std::size_t bytesTransferred) {
433             BMCWEB_LOG_DEBUG("{} async_read_header {} Bytes", logPtr(this),
434                              bytesTransferred);
435             bool errorWhileReading = false;
436             if (ec)
437             {
438                 errorWhileReading = true;
439                 if (ec == boost::beast::http::error::end_of_stream)
440                 {
441                     BMCWEB_LOG_WARNING("{} Error while reading: {}",
442                                        logPtr(this), ec.message());
443                 }
444                 else
445                 {
446                     BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
447                                      ec.message());
448                 }
449             }
450             else
451             {
452                 // if the adaptor isn't open anymore, and wasn't handed to a
453                 // websocket, treat as an error
454                 if (!isAlive() &&
455                     !boost::beast::websocket::is_upgrade(parser->get()))
456                 {
457                     errorWhileReading = true;
458                 }
459             }
460 
461             cancelDeadlineTimer();
462 
463             if (errorWhileReading)
464             {
465                 close();
466                 BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
467                 return;
468             }
469 
470             readClientIp();
471 
472             boost::asio::ip::address ip;
473             if (getClientIp(ip))
474             {
475                 BMCWEB_LOG_DEBUG("Unable to get client IP");
476             }
477             if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
478             {
479 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
480                 boost::beast::http::verb method = parser->get().method();
481                 userSession = crow::authentication::authenticate(
482                     ip, res, method, parser->get().base(), mtlsSession);
483 
484                 bool loggedIn = userSession != nullptr;
485                 if (!loggedIn)
486                 {
487                     const boost::optional<uint64_t> contentLength =
488                         parser->content_length();
489                     if (contentLength &&
490                         *contentLength > loggedOutPostBodyLimit)
491                     {
492                         BMCWEB_LOG_DEBUG("Content length greater than limit {}",
493                                          *contentLength);
494                         close();
495                         return;
496                     }
497 
498                     BMCWEB_LOG_DEBUG("Starting quick deadline");
499                 }
500 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
501             }
502 
503             if (parser->is_done())
504             {
505                 handle();
506                 return;
507             }
508 
509             doRead();
510         });
511     }
512 
513     void doRead()
514     {
515         BMCWEB_LOG_DEBUG("{} doRead", logPtr(this));
516         if (!parser)
517         {
518             return;
519         }
520         startDeadline();
521         boost::beast::http::async_read_some(
522             adaptor, buffer, *parser,
523             [this,
524              self(shared_from_this())](const boost::system::error_code& ec,
525                                        std::size_t bytesTransferred) {
526             BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this),
527                              bytesTransferred);
528 
529             if (ec)
530             {
531                 BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
532                                  ec.message());
533                 close();
534                 BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
535                 return;
536             }
537 
538             // If the user is logged in, allow them to send files incrementally
539             // one piece at a time. If authentication is disabled then there is
540             // no user session hence always allow to send one piece at a time.
541             if (userSession != nullptr)
542             {
543                 cancelDeadlineTimer();
544             }
545             if (!parser->is_done())
546             {
547                 doRead();
548                 return;
549             }
550 
551             cancelDeadlineTimer();
552             handle();
553         });
554     }
555 
556     void afterDoWrite(const std::shared_ptr<self_type>& /*self*/,
557                       const boost::system::error_code& ec,
558                       std::size_t bytesTransferred)
559     {
560         BMCWEB_LOG_DEBUG("{} async_write {} bytes", logPtr(this),
561                          bytesTransferred);
562 
563         cancelDeadlineTimer();
564 
565         if (ec)
566         {
567             BMCWEB_LOG_DEBUG("{} from write(2)", logPtr(this));
568             return;
569         }
570         if (!keepAlive)
571         {
572             close();
573             BMCWEB_LOG_DEBUG("{} from write(1)", logPtr(this));
574             return;
575         }
576 
577         BMCWEB_LOG_DEBUG("{} Clearing response", logPtr(this));
578         res.clear();
579         parser.emplace(std::piecewise_construct, std::make_tuple());
580         parser->body_limit(httpReqBodyLimit); // reset body limit for
581                                               // newly created parser
582         buffer.consume(buffer.size());
583 
584         userSession = nullptr;
585 
586         // Destroy the Request via the std::optional
587         req.reset();
588         doReadHeaders();
589     }
590 
591     void doWrite(crow::Response& thisRes)
592     {
593         BMCWEB_LOG_DEBUG("{} doWrite", logPtr(this));
594         thisRes.preparePayload();
595 
596         startDeadline();
597         boost::beast::async_write(adaptor, thisRes.generator(),
598                                   std::bind_front(&self_type::afterDoWrite,
599                                                   this, shared_from_this()));
600     }
601 
602     void cancelDeadlineTimer()
603     {
604         timer.cancel();
605     }
606 
607     void startDeadline()
608     {
609         // Timer is already started so no further action is required.
610         if (timerStarted)
611         {
612             return;
613         }
614 
615         std::chrono::seconds timeout(15);
616 
617         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
618         timer.expires_after(timeout);
619         timer.async_wait([weakSelf](const boost::system::error_code& ec) {
620             // Note, we are ignoring other types of errors here;  If the timer
621             // failed for any reason, we should still close the connection
622             std::shared_ptr<Connection<Adaptor, Handler>> self =
623                 weakSelf.lock();
624             if (!self)
625             {
626                 BMCWEB_LOG_CRITICAL("{} Failed to capture connection",
627                                     logPtr(self.get()));
628                 return;
629             }
630 
631             self->timerStarted = false;
632 
633             if (ec == boost::asio::error::operation_aborted)
634             {
635                 // Canceled wait means the path succeeded.
636                 return;
637             }
638             if (ec)
639             {
640                 BMCWEB_LOG_CRITICAL("{} timer failed {}", logPtr(self.get()),
641                                     ec);
642             }
643 
644             BMCWEB_LOG_WARNING("{}Connection timed out, closing",
645                                logPtr(self.get()));
646 
647             self->close();
648         });
649 
650         timerStarted = true;
651         BMCWEB_LOG_DEBUG("{} timer started", logPtr(this));
652     }
653 
654     Adaptor adaptor;
655     Handler* handler;
656     // Making this a std::optional allows it to be efficiently destroyed and
657     // re-created on Connection reset
658     std::optional<
659         boost::beast::http::request_parser<boost::beast::http::string_body>>
660         parser;
661 
662     boost::beast::flat_static_buffer<8192> buffer;
663 
664     std::optional<crow::Request> req;
665     crow::Response res;
666 
667     std::shared_ptr<persistent_data::UserSession> userSession;
668     std::shared_ptr<persistent_data::UserSession> mtlsSession;
669 
670     boost::asio::steady_timer timer;
671 
672     bool keepAlive = true;
673 
674     bool timerStarted = false;
675 
676     std::function<std::string()>& getCachedDateStr;
677 
678     using std::enable_shared_from_this<
679         Connection<Adaptor, Handler>>::shared_from_this;
680 
681     using std::enable_shared_from_this<
682         Connection<Adaptor, Handler>>::weak_from_this;
683 };
684 } // namespace crow
685