xref: /openbmc/bmcweb/http/http_connection.hpp (revision 757178a551c2dcc24730dccb6ed1cd11005b3b8f)
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/ssl/ssl_stream.hpp>
30 #include <boost/beast/websocket.hpp>
31 
32 #include <atomic>
33 #include <chrono>
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 bmcwebHttpReqBodyLimitMb option
43 constexpr uint64_t httpReqBodyLimit = 1024UL * 1024UL *
44                                       bmcwebHttpReqBodyLimitMb;
45 
46 constexpr uint64_t loggedOutPostBodyLimit = 4096;
47 
48 constexpr uint32_t httpHeaderLimit = 8192;
49 
50 template <typename>
51 struct IsTls : std::false_type
52 {};
53 
54 template <typename T>
55 struct IsTls<boost::beast::ssl_stream<T>> : std::true_type
56 {};
57 
58 template <typename Adaptor, typename Handler>
59 class Connection :
60     public std::enable_shared_from_this<Connection<Adaptor, Handler>>
61 {
62     using self_type = Connection<Adaptor, Handler>;
63 
64   public:
65     Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn,
66                std::function<std::string()>& getCachedDateStrF,
67                Adaptor adaptorIn) :
68         adaptor(std::move(adaptorIn)),
69         handler(handlerIn), timer(std::move(timerIn)),
70         getCachedDateStr(getCachedDateStrF)
71     {
72         parser.emplace(std::piecewise_construct, std::make_tuple());
73         parser->body_limit(httpReqBodyLimit);
74         parser->header_limit(httpHeaderLimit);
75 
76 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
77         prepareMutualTls();
78 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
79 
80         connectionCount++;
81 
82         BMCWEB_LOG_DEBUG("{} Connection open, total {}", logPtr(this),
83                          connectionCount);
84     }
85 
86     ~Connection()
87     {
88         res.setCompleteRequestHandler(nullptr);
89         cancelDeadlineTimer();
90 
91         connectionCount--;
92         BMCWEB_LOG_DEBUG("{} Connection closed, total {}", logPtr(this),
93                          connectionCount);
94     }
95 
96     Connection(const Connection&) = delete;
97     Connection(Connection&&) = delete;
98     Connection& operator=(const Connection&) = delete;
99     Connection& operator=(Connection&&) = delete;
100 
101     bool tlsVerifyCallback(bool preverified,
102                            boost::asio::ssl::verify_context& ctx)
103     {
104         // We always return true to allow full auth flow for resources that
105         // don't require auth
106         if (preverified)
107         {
108             boost::asio::ip::address ipAddress;
109             if (getClientIp(ipAddress))
110             {
111                 return true;
112             }
113 
114             mtlsSession = verifyMtlsUser(ipAddress, ctx);
115             if (mtlsSession)
116             {
117                 BMCWEB_LOG_DEBUG("{} Generating TLS session: {}", logPtr(this),
118                                  mtlsSession->uniqueId);
119             }
120         }
121         return true;
122     }
123 
124     void prepareMutualTls()
125     {
126         if constexpr (IsTls<Adaptor>::value)
127         {
128             std::error_code error;
129             std::filesystem::path caPath(ensuressl::trustStorePath);
130             auto caAvailable = !std::filesystem::is_empty(caPath, error);
131             caAvailable = caAvailable && !error;
132             if (caAvailable && persistent_data::SessionStore::getInstance()
133                                    .getAuthMethodsConfig()
134                                    .tls)
135             {
136                 adaptor.set_verify_mode(boost::asio::ssl::verify_peer);
137                 std::string id = "bmcweb";
138 
139                 const char* cStr = id.c_str();
140                 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
141                 const auto* idC = reinterpret_cast<const unsigned char*>(cStr);
142                 int ret = SSL_set_session_id_context(
143                     adaptor.native_handle(), idC,
144                     static_cast<unsigned int>(id.length()));
145                 if (ret == 0)
146                 {
147                     BMCWEB_LOG_ERROR("{} failed to set SSL id", logPtr(this));
148                 }
149             }
150 
151             adaptor.set_verify_callback(
152                 std::bind_front(&self_type::tlsVerifyCallback, this));
153         }
154     }
155 
156     Adaptor& socket()
157     {
158         return adaptor;
159     }
160 
161     void start()
162     {
163         if (connectionCount >= 200)
164         {
165             BMCWEB_LOG_CRITICAL("{}Max connection count exceeded.",
166                                 logPtr(this));
167             return;
168         }
169 
170         startDeadline();
171 
172         // TODO(ed) Abstract this to a more clever class with the idea of an
173         // asynchronous "start"
174         if constexpr (IsTls<Adaptor>::value)
175         {
176             adaptor.async_handshake(boost::asio::ssl::stream_base::server,
177                                     [this, self(shared_from_this())](
178                                         const boost::system::error_code& ec) {
179                 if (ec)
180                 {
181                     return;
182                 }
183                 afterSslHandshake();
184             });
185         }
186         else
187         {
188             doReadHeaders();
189         }
190     }
191 
192     void afterSslHandshake()
193     {
194         // If http2 is enabled, negotiate the protocol
195         if constexpr (bmcwebEnableHTTP2)
196         {
197             const unsigned char* alpn = nullptr;
198             unsigned int alpnlen = 0;
199             SSL_get0_alpn_selected(adaptor.native_handle(), &alpn, &alpnlen);
200             if (alpn != nullptr)
201             {
202                 std::string_view selectedProtocol(
203                     std::bit_cast<const char*>(alpn), alpnlen);
204                 BMCWEB_LOG_DEBUG("ALPN selected protocol \"{}\" len: {}",
205                                  selectedProtocol, alpnlen);
206                 if (selectedProtocol == "h2")
207                 {
208                     auto http2 =
209                         std::make_shared<HTTP2Connection<Adaptor, Handler>>(
210                             std::move(adaptor), handler, getCachedDateStr);
211                     http2->start();
212                     return;
213                 }
214             }
215         }
216 
217         doReadHeaders();
218     }
219 
220     void handle()
221     {
222         std::error_code reqEc;
223         if (!parser)
224         {
225             return;
226         }
227         req = crow::Request(parser->release(), reqEc);
228         if (reqEc)
229         {
230             BMCWEB_LOG_DEBUG("Request failed to construct{}", reqEc.message());
231             res.result(boost::beast::http::status::bad_request);
232             completeRequest(res);
233             return;
234         }
235         req.session = userSession;
236 
237         // Fetch the client IP address
238         readClientIp();
239 
240         // Check for HTTP version 1.1.
241         if (req.version() == 11)
242         {
243             if (req.getHeaderValue(boost::beast::http::field::host).empty())
244             {
245                 res.result(boost::beast::http::status::bad_request);
246                 completeRequest(res);
247                 return;
248             }
249         }
250 
251         BMCWEB_LOG_INFO("Request:  {} HTTP/{}.{} {} {} {}", logPtr(this),
252                         req.version() / 10, req.version() % 10,
253                         req.methodString(), req.target(),
254                         req.ipAddress.to_string());
255 
256         req.ioService = static_cast<decltype(req.ioService)>(
257             &adaptor.get_executor().context());
258 
259         if (res.completed)
260         {
261             completeRequest(res);
262             return;
263         }
264         keepAlive = req.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                 req.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             req.getHeaderValue(boost::beast::http::field::upgrade));
293         if ((req.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(req, 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(req, asyncResp);
321     }
322 
323     void close()
324     {
325         if constexpr (IsTls<Adaptor>::value)
326         {
327             adaptor.next_layer().close();
328             if (mtlsSession != nullptr)
329             {
330                 BMCWEB_LOG_DEBUG("{} Removing TLS session: {}", logPtr(this),
331                                  mtlsSession->uniqueId);
332                 persistent_data::SessionStore::getInstance().removeSession(
333                     mtlsSession);
334             }
335         }
336         else
337         {
338             adaptor.close();
339         }
340     }
341 
342     void completeRequest(crow::Response& thisRes)
343     {
344         res = std::move(thisRes);
345         res.keepAlive(keepAlive);
346 
347         completeResponseFields(req, res);
348         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
349 
350         doWrite();
351 
352         // delete lambda with self shared_ptr
353         // to enable connection destruction
354         res.setCompleteRequestHandler(nullptr);
355     }
356 
357     void readClientIp()
358     {
359         boost::asio::ip::address ip;
360         boost::system::error_code ec = getClientIp(ip);
361         if (ec)
362         {
363             return;
364         }
365         req.ipAddress = ip;
366     }
367 
368     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
369     {
370         boost::system::error_code ec;
371         BMCWEB_LOG_DEBUG("Fetch the client IP address");
372 
373         if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
374         {
375             boost::asio::ip::tcp::endpoint endpoint =
376                 boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
377 
378             if (ec)
379             {
380                 // If remote endpoint fails keep going. "ClientOriginIPAddress"
381                 // will be empty.
382                 BMCWEB_LOG_ERROR(
383                     "Failed to get the client's IP Address. ec : {}", ec);
384                 return ec;
385             }
386             ip = endpoint.address();
387         }
388         return ec;
389     }
390 
391   private:
392     void doReadHeaders()
393     {
394         BMCWEB_LOG_DEBUG("{} doReadHeaders", logPtr(this));
395         if (!parser)
396         {
397             return;
398         }
399         // Clean up any previous Connection.
400         boost::beast::http::async_read_header(
401             adaptor, buffer, *parser,
402             [this,
403              self(shared_from_this())](const boost::system::error_code& ec,
404                                        std::size_t bytesTransferred) {
405             BMCWEB_LOG_DEBUG("{} async_read_header {} Bytes", logPtr(this),
406                              bytesTransferred);
407 
408             if (ec)
409             {
410                 cancelDeadlineTimer();
411 
412                 if (ec == boost::beast::http::error::end_of_stream)
413                 {
414                     BMCWEB_LOG_WARNING("{} Error while reading: {}",
415                                        logPtr(this), ec.message());
416                 }
417                 else
418                 {
419                     BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
420                                      ec.message());
421                 }
422                 close();
423                 BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
424                 return;
425             }
426 
427             readClientIp();
428 
429             boost::asio::ip::address ip;
430             if (getClientIp(ip))
431             {
432                 BMCWEB_LOG_DEBUG("Unable to get client IP");
433             }
434             if constexpr (!std::is_same_v<Adaptor, boost::beast::test::stream>)
435             {
436 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
437                 boost::beast::http::verb method = parser->get().method();
438                 userSession = crow::authentication::authenticate(
439                     ip, res, method, parser->get().base(), mtlsSession);
440 
441                 bool loggedIn = userSession != nullptr;
442                 if (!loggedIn)
443                 {
444                     const boost::optional<uint64_t> contentLength =
445                         parser->content_length();
446                     if (contentLength &&
447                         *contentLength > loggedOutPostBodyLimit)
448                     {
449                         BMCWEB_LOG_DEBUG("Content length greater than limit {}",
450                                          *contentLength);
451                         close();
452                         return;
453                     }
454 
455                     BMCWEB_LOG_DEBUG("Starting quick deadline");
456                 }
457 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
458             }
459 
460             if (parser->is_done())
461             {
462                 handle();
463                 return;
464             }
465 
466             doRead();
467         });
468     }
469 
470     void doRead()
471     {
472         BMCWEB_LOG_DEBUG("{} doRead", logPtr(this));
473         if (!parser)
474         {
475             return;
476         }
477         startDeadline();
478         boost::beast::http::async_read_some(
479             adaptor, buffer, *parser,
480             [this,
481              self(shared_from_this())](const boost::system::error_code& ec,
482                                        std::size_t bytesTransferred) {
483             BMCWEB_LOG_DEBUG("{} async_read_some {} Bytes", logPtr(this),
484                              bytesTransferred);
485 
486             if (ec)
487             {
488                 BMCWEB_LOG_ERROR("{} Error while reading: {}", logPtr(this),
489                                  ec.message());
490                 close();
491                 BMCWEB_LOG_DEBUG("{} from read(1)", logPtr(this));
492                 return;
493             }
494 
495             // If the user is logged in, allow them to send files incrementally
496             // one piece at a time. If authentication is disabled then there is
497             // no user session hence always allow to send one piece at a time.
498             if (userSession != nullptr)
499             {
500                 cancelDeadlineTimer();
501             }
502             if (!parser->is_done())
503             {
504                 doRead();
505                 return;
506             }
507 
508             cancelDeadlineTimer();
509             handle();
510         });
511     }
512 
513     void afterDoWrite(const std::shared_ptr<self_type>& /*self*/,
514                       const boost::system::error_code& ec,
515                       std::size_t bytesTransferred)
516     {
517         BMCWEB_LOG_DEBUG("{} async_write {} bytes", logPtr(this),
518                          bytesTransferred);
519 
520         cancelDeadlineTimer();
521 
522         if (ec)
523         {
524             BMCWEB_LOG_DEBUG("{} from write(2)", logPtr(this));
525             return;
526         }
527         if (!keepAlive)
528         {
529             close();
530             BMCWEB_LOG_DEBUG("{} from write(1)", logPtr(this));
531             return;
532         }
533 
534         BMCWEB_LOG_DEBUG("{} Clearing response", logPtr(this));
535         res.clear();
536         parser.emplace(std::piecewise_construct, std::make_tuple());
537         parser->body_limit(httpReqBodyLimit); // reset body limit for
538                                               // newly created parser
539         buffer.consume(buffer.size());
540 
541         userSession = nullptr;
542 
543         // Destroy the Request via the std::optional
544         req.clear();
545         doReadHeaders();
546     }
547 
548     void doWrite()
549     {
550         BMCWEB_LOG_DEBUG("{} doWrite", logPtr(this));
551         res.preparePayload();
552 
553         startDeadline();
554         boost::beast::async_write(
555             adaptor,
556             boost::beast::http::message_generator(std::move(res.response)),
557             std::bind_front(&self_type::afterDoWrite, this,
558                             shared_from_this()));
559     }
560 
561     void cancelDeadlineTimer()
562     {
563         timer.cancel();
564     }
565 
566     void startDeadline()
567     {
568         // Timer is already started so no further action is required.
569         if (timerStarted)
570         {
571             return;
572         }
573 
574         std::chrono::seconds timeout(15);
575 
576         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
577         timer.expires_after(timeout);
578         timer.async_wait([weakSelf](const boost::system::error_code& ec) {
579             // Note, we are ignoring other types of errors here;  If the timer
580             // failed for any reason, we should still close the connection
581             std::shared_ptr<Connection<Adaptor, Handler>> self =
582                 weakSelf.lock();
583             if (!self)
584             {
585                 BMCWEB_LOG_CRITICAL("{} Failed to capture connection",
586                                     logPtr(self.get()));
587                 return;
588             }
589 
590             self->timerStarted = false;
591 
592             if (ec == boost::asio::error::operation_aborted)
593             {
594                 // Canceled wait means the path succeeded.
595                 return;
596             }
597             if (ec)
598             {
599                 BMCWEB_LOG_CRITICAL("{} timer failed {}", logPtr(self.get()),
600                                     ec);
601             }
602 
603             BMCWEB_LOG_WARNING("{}Connection timed out, closing",
604                                logPtr(self.get()));
605 
606             self->close();
607         });
608 
609         timerStarted = true;
610         BMCWEB_LOG_DEBUG("{} timer started", logPtr(this));
611     }
612 
613     Adaptor adaptor;
614     Handler* handler;
615     // Making this a std::optional allows it to be efficiently destroyed and
616     // re-created on Connection reset
617     std::optional<boost::beast::http::request_parser<bmcweb::HttpBody>> parser;
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