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