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