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