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