xref: /openbmc/bmcweb/http/http_connection.hpp (revision 93cf0ac2)
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/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         req = crow::Request(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         req.session = userSession;
234 
235         // Fetch the client IP address
236         readClientIp();
237 
238         // Check for HTTP version 1.1.
239         if (req.version() == 11)
240         {
241             if (req.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                         req.version() / 10, req.version() % 10,
251                         req.methodString(), req.target(),
252                         req.ipAddress.to_string());
253 
254         req.ioService = static_cast<decltype(req.ioService)>(
255             &adaptor.get_executor().context());
256 
257         if (res.completed)
258         {
259             completeRequest(res);
260             return;
261         }
262         keepAlive = req.keepAlive();
263         if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
264         {
265 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
266             if (!crow::authentication::isOnAllowlist(req.url().path(),
267                                                      req.method()) &&
268                 req.session == nullptr)
269             {
270                 BMCWEB_LOG_WARNING("Authentication failed");
271                 forward_unauthorized::sendUnauthorized(
272                     req.url().encoded_path(),
273                     req.getHeaderValue("X-Requested-With"),
274                     req.getHeaderValue("Accept"), res);
275                 completeRequest(res);
276                 return;
277             }
278 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
279         }
280         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
281         BMCWEB_LOG_DEBUG("Setting completion handler");
282         asyncResp->res.setCompleteRequestHandler(
283             [self(shared_from_this())](crow::Response& thisRes) {
284             self->completeRequest(thisRes);
285         });
286         bool isSse =
287             isContentTypeAllowed(req.getHeaderValue("Accept"),
288                                  http_helpers::ContentType::EventStream, false);
289         std::string_view upgradeType(
290             req.getHeaderValue(boost::beast::http::field::upgrade));
291         if ((req.isUpgrade() &&
292              bmcweb::asciiIEquals(upgradeType, "websocket")) ||
293             isSse)
294         {
295             asyncResp->res.setCompleteRequestHandler(
296                 [self(shared_from_this())](crow::Response& thisRes) {
297                 if (thisRes.result() != boost::beast::http::status::ok)
298                 {
299                     // When any error occurs before handle upgradation,
300                     // the result in response will be set to respective
301                     // error. By default the Result will be OK (200),
302                     // which implies successful handle upgrade. Response
303                     // needs to be sent over this connection only on
304                     // failure.
305                     self->completeRequest(thisRes);
306                     return;
307                 }
308             });
309             handler->handleUpgrade(req, asyncResp, std::move(adaptor));
310             return;
311         }
312         std::string_view expected =
313             req.getHeaderValue(boost::beast::http::field::if_none_match);
314         if (!expected.empty())
315         {
316             res.setExpectedHash(expected);
317         }
318         handler->handle(req, asyncResp);
319     }
320 
321     void close()
322     {
323         if constexpr (IsTls<Adaptor>::value)
324         {
325             adaptor.next_layer().close();
326             if (mtlsSession != nullptr)
327             {
328                 BMCWEB_LOG_DEBUG("{} Removing TLS session: {}", logPtr(this),
329                                  mtlsSession->uniqueId);
330                 persistent_data::SessionStore::getInstance().removeSession(
331                     mtlsSession);
332             }
333         }
334         else
335         {
336             adaptor.close();
337         }
338     }
339 
340     void completeRequest(crow::Response& thisRes)
341     {
342         res = std::move(thisRes);
343         res.keepAlive(keepAlive);
344 
345         completeResponseFields(req, res);
346         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
347 
348         doWrite();
349 
350         // delete lambda with self shared_ptr
351         // to enable connection destruction
352         res.setCompleteRequestHandler(nullptr);
353     }
354 
355     void readClientIp()
356     {
357         boost::asio::ip::address ip;
358         boost::system::error_code ec = getClientIp(ip);
359         if (ec)
360         {
361             return;
362         }
363         req.ipAddress = ip;
364     }
365 
366     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
367     {
368         boost::system::error_code ec;
369         BMCWEB_LOG_DEBUG("Fetch the client IP address");
370 
371         if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
372         {
373             boost::asio::ip::tcp::endpoint endpoint =
374                 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
375 
376             if (ec)
377             {
378                 // If remote endpoint fails keep going. "ClientOriginIPAddress"
379                 // will be empty.
380                 BMCWEB_LOG_ERROR(
381                     "Failed to get the client's IP Address. ec : {}", ec);
382                 return ec;
383             }
384             ip = endpoint.address();
385         }
386         return ec;
387     }
388 
389   private:
390     void doReadHeaders()
391     {
392         BMCWEB_LOG_DEBUG("{} doReadHeaders", logPtr(this));
393         if (!parser)
394         {
395             return;
396         }
397         // Clean up any previous Connection.
398         boost::beast::http::async_read_header(
399             adaptor, buffer, *parser,
400             [this,
401              self(shared_from_this())](const boost::system::error_code& ec,
402                                        std::size_t bytesTransferred) {
403             BMCWEB_LOG_DEBUG("{} async_read_header {} Bytes", logPtr(this),
404                              bytesTransferred);
405 
406             if (ec)
407             {
408                 cancelDeadlineTimer();
409 
410                 if (ec == boost::beast::http::error::end_of_stream)
411                 {
412                     BMCWEB_LOG_WARNING("{} Error while reading: {}",
413                                        logPtr(this), ec.message());
414                 }
415                 else
416                 {
417                     BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
418                                      ec.message());
419                 }
420                 close();
421                 BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
422                 return;
423             }
424 
425             readClientIp();
426 
427             boost::asio::ip::address ip;
428             if (getClientIp(ip))
429             {
430                 BMCWEB_LOG_DEBUG("Unable to get client IP");
431             }
432             if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
433             {
434 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
435                 boost::beast::http::verb method = parser->get().method();
436                 userSession = crow::authentication::authenticate(
437                     ip, res, method, parser->get().base(), mtlsSession);
438 
439                 bool loggedIn = userSession != nullptr;
440                 if (!loggedIn)
441                 {
442                     const boost::optional<uint64_t> contentLength =
443                         parser->content_length();
444                     if (contentLength &&
445                         *contentLength > loggedOutPostBodyLimit)
446                     {
447                         BMCWEB_LOG_DEBUG("Content length greater than limit {}",
448                                          *contentLength);
449                         close();
450                         return;
451                     }
452 
453                     BMCWEB_LOG_DEBUG("Starting quick deadline");
454                 }
455 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
456             }
457 
458             if (parser->is_done())
459             {
460                 handle();
461                 return;
462             }
463 
464             doRead();
465         });
466     }
467 
468     void doRead()
469     {
470         BMCWEB_LOG_DEBUG("{} doRead", logPtr(this));
471         if (!parser)
472         {
473             return;
474         }
475         startDeadline();
476         boost::beast::http::async_read_some(
477             adaptor, buffer, *parser,
478             [this,
479              self(shared_from_this())](const boost::system::error_code& ec,
480                                        std::size_t bytesTransferred) {
481             BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this),
482                              bytesTransferred);
483 
484             if (ec)
485             {
486                 BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
487                                  ec.message());
488                 close();
489                 BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
490                 return;
491             }
492 
493             // If the user is logged in, allow them to send files incrementally
494             // one piece at a time. If authentication is disabled then there is
495             // no user session hence always allow to send one piece at a time.
496             if (userSession != nullptr)
497             {
498                 cancelDeadlineTimer();
499             }
500             if (!parser->is_done())
501             {
502                 doRead();
503                 return;
504             }
505 
506             cancelDeadlineTimer();
507             handle();
508         });
509     }
510 
511     void afterDoWrite(const std::shared_ptr<self_type>& /*self*/,
512                       const boost::system::error_code& ec,
513                       std::size_t bytesTransferred)
514     {
515         BMCWEB_LOG_DEBUG("{} async_write {} bytes", logPtr(this),
516                          bytesTransferred);
517 
518         cancelDeadlineTimer();
519 
520         if (ec)
521         {
522             BMCWEB_LOG_DEBUG("{} from write(2)", logPtr(this));
523             return;
524         }
525         if (!keepAlive)
526         {
527             close();
528             BMCWEB_LOG_DEBUG("{} from write(1)", logPtr(this));
529             return;
530         }
531 
532         BMCWEB_LOG_DEBUG("{} Clearing response", logPtr(this));
533         res.clear();
534         parser.emplace(std::piecewise_construct, std::make_tuple());
535         parser->body_limit(httpReqBodyLimit); // reset body limit for
536                                               // newly created parser
537         buffer.consume(buffer.size());
538 
539         userSession = nullptr;
540 
541         // Destroy the Request via the std::optional
542         req.clear();
543         doReadHeaders();
544     }
545 
546     void doWrite()
547     {
548         BMCWEB_LOG_DEBUG("{} doWrite", logPtr(this));
549         res.preparePayload();
550 
551         startDeadline();
552         serializer.emplace(res.response);
553         boost::beast::http::async_write(
554             adaptor, *serializer,
555             std::bind_front(&self_type::afterDoWrite, this,
556                             shared_from_this()));
557     }
558 
559     void cancelDeadlineTimer()
560     {
561         timer.cancel();
562     }
563 
564     void startDeadline()
565     {
566         // Timer is already started so no further action is required.
567         if (timerStarted)
568         {
569             return;
570         }
571 
572         std::chrono::seconds timeout(15);
573 
574         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
575         timer.expires_after(timeout);
576         timer.async_wait([weakSelf](const boost::system::error_code& ec) {
577             // Note, we are ignoring other types of errors here;  If the timer
578             // failed for any reason, we should still close the connection
579             std::shared_ptr<Connection<Adaptor, Handler>> self =
580                 weakSelf.lock();
581             if (!self)
582             {
583                 BMCWEB_LOG_CRITICAL("{} Failed to capture connection",
584                                     logPtr(self.get()));
585                 return;
586             }
587 
588             self->timerStarted = false;
589 
590             if (ec == boost::asio::error::operation_aborted)
591             {
592                 // Canceled wait means the path succeeded.
593                 return;
594             }
595             if (ec)
596             {
597                 BMCWEB_LOG_CRITICAL("{} timer failed {}", logPtr(self.get()),
598                                     ec);
599             }
600 
601             BMCWEB_LOG_WARNING("{}Connection timed out, closing",
602                                logPtr(self.get()));
603 
604             self->close();
605         });
606 
607         timerStarted = true;
608         BMCWEB_LOG_DEBUG("{} timer started", logPtr(this));
609     }
610 
611     Adaptor adaptor;
612     Handler* handler;
613     // Making this a std::optional allows it to be efficiently destroyed and
614     // re-created on Connection reset
615     std::optional<boost::beast::http::request_parser<bmcweb::HttpBody>> parser;
616     std::optional<boost::beast::http::response_serializer<bmcweb::HttpBody>>
617         serializer;
618 
619     boost::beast::flat_static_buffer<8192> buffer;
620 
621     crow::Request req;
622     crow::Response res;
623 
624     std::shared_ptr<persistent_data::UserSession> userSession;
625     std::shared_ptr<persistent_data::UserSession> mtlsSession;
626 
627     boost::asio::steady_timer timer;
628 
629     bool keepAlive = true;
630 
631     bool timerStarted = false;
632 
633     std::function<std::string()>& getCachedDateStr;
634 
635     using std::enable_shared_from_this<
636         Connection<Adaptor, Handler>>::shared_from_this;
637 
638     using std::enable_shared_from_this<
639         Connection<Adaptor, Handler>>::weak_from_this;
640 };
641 } // namespace crow
642