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