xref: /openbmc/bmcweb/http/http_connection.hpp (revision 852432ac)
1 #pragma once
2 #include "bmcweb_config.h"
3 
4 #include "authentication.hpp"
5 #include "http_response.hpp"
6 #include "http_utility.hpp"
7 #include "logging.hpp"
8 #include "utility.hpp"
9 
10 #include <boost/algorithm/string/predicate.hpp>
11 #include <boost/asio/io_context.hpp>
12 #include <boost/asio/ip/tcp.hpp>
13 #include <boost/asio/ssl/stream.hpp>
14 #include <boost/asio/steady_timer.hpp>
15 #include <boost/beast/core/flat_static_buffer.hpp>
16 #include <boost/beast/http/parser.hpp>
17 #include <boost/beast/http/read.hpp>
18 #include <boost/beast/http/serializer.hpp>
19 #include <boost/beast/http/write.hpp>
20 #include <boost/beast/ssl/ssl_stream.hpp>
21 #include <boost/beast/websocket.hpp>
22 #include <boost/url/url_view.hpp>
23 #include <json_html_serializer.hpp>
24 #include <security_headers.hpp>
25 #include <ssl_key_handler.hpp>
26 
27 #include <atomic>
28 #include <chrono>
29 #include <vector>
30 
31 namespace crow
32 {
33 
34 inline void prettyPrintJson(crow::Response& res)
35 {
36     json_html_util::dumpHtml(res.body(), res.jsonValue);
37 
38     res.addHeader("Content-Type", "text/html;charset=UTF-8");
39 }
40 
41 static int connectionCount = 0;
42 
43 // request body limit size set by the bmcwebHttpReqBodyLimitMb option
44 constexpr uint64_t httpReqBodyLimit =
45     1024UL * 1024UL * bmcwebHttpReqBodyLimitMb;
46 
47 constexpr uint64_t loggedOutPostBodyLimit = 4096;
48 
49 constexpr uint32_t httpHeaderLimit = 8192;
50 
51 template <typename Adaptor, typename Handler>
52 class Connection :
53     public std::enable_shared_from_this<Connection<Adaptor, Handler>>
54 {
55   public:
56     Connection(Handler* handlerIn, boost::asio::steady_timer&& timerIn,
57                std::function<std::string()>& getCachedDateStrF,
58                Adaptor adaptorIn) :
59         adaptor(std::move(adaptorIn)),
60         handler(handlerIn), timer(std::move(timerIn)),
61         getCachedDateStr(getCachedDateStrF)
62     {
63         parser.emplace(std::piecewise_construct, std::make_tuple());
64         parser->body_limit(httpReqBodyLimit);
65         parser->header_limit(httpHeaderLimit);
66 
67 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
68         prepareMutualTls();
69 #endif // BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
70 
71         connectionCount++;
72 
73         BMCWEB_LOG_DEBUG << this << " Connection open, total "
74                          << connectionCount;
75     }
76 
77     ~Connection()
78     {
79         res.setCompleteRequestHandler(nullptr);
80         cancelDeadlineTimer();
81 
82         connectionCount--;
83         BMCWEB_LOG_DEBUG << this << " Connection closed, total "
84                          << connectionCount;
85     }
86 
87     Connection(const Connection&) = delete;
88     Connection(Connection&&) = delete;
89     Connection& operator=(const Connection&) = delete;
90     Connection& operator=(Connection&&) = delete;
91 
92     void prepareMutualTls()
93     {
94         std::error_code error;
95         std::filesystem::path caPath(ensuressl::trustStorePath);
96         auto caAvailable = !std::filesystem::is_empty(caPath, error);
97         caAvailable = caAvailable && !error;
98         if (caAvailable && persistent_data::SessionStore::getInstance()
99                                .getAuthMethodsConfig()
100                                .tls)
101         {
102             adaptor.set_verify_mode(boost::asio::ssl::verify_peer);
103             std::string id = "bmcweb";
104 
105             const char* cStr = id.c_str();
106             // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
107             const auto* idC = reinterpret_cast<const unsigned char*>(cStr);
108             int ret = SSL_set_session_id_context(
109                 adaptor.native_handle(), idC,
110                 static_cast<unsigned int>(id.length()));
111             if (ret == 0)
112             {
113                 BMCWEB_LOG_ERROR << this << " failed to set SSL id";
114             }
115         }
116 
117         adaptor.set_verify_callback(
118             [this](bool preverified, boost::asio::ssl::verify_context& ctx) {
119             // do nothing if TLS is disabled
120             if (!persistent_data::SessionStore::getInstance()
121                      .getAuthMethodsConfig()
122                      .tls)
123             {
124                 BMCWEB_LOG_DEBUG << this << " TLS auth_config is disabled";
125                 return true;
126             }
127 
128             // We always return true to allow full auth flow
129             if (!preverified)
130             {
131                 BMCWEB_LOG_DEBUG << this << " TLS preverification failed.";
132                 return true;
133             }
134 
135             X509_STORE_CTX* cts = ctx.native_handle();
136             if (cts == nullptr)
137             {
138                 BMCWEB_LOG_DEBUG << this << " Cannot get native TLS handle.";
139                 return true;
140             }
141 
142             // Get certificate
143             X509* peerCert =
144                 X509_STORE_CTX_get_current_cert(ctx.native_handle());
145             if (peerCert == nullptr)
146             {
147                 BMCWEB_LOG_DEBUG << this
148                                  << " Cannot get current TLS certificate.";
149                 return true;
150             }
151 
152             // Check if certificate is OK
153             int ctxError = X509_STORE_CTX_get_error(cts);
154             if (ctxError != X509_V_OK)
155             {
156                 BMCWEB_LOG_INFO << this << " Last TLS error is: " << ctxError;
157                 return true;
158             }
159             // Check that we have reached final certificate in chain
160             int32_t depth = X509_STORE_CTX_get_error_depth(cts);
161             if (depth != 0)
162 
163             {
164                 BMCWEB_LOG_DEBUG
165                     << this << " Certificate verification in progress (depth "
166                     << depth << "), waiting to reach final depth";
167                 return true;
168             }
169 
170             BMCWEB_LOG_DEBUG << this
171                              << " Certificate verification of final depth";
172 
173             // Verify KeyUsage
174             bool isKeyUsageDigitalSignature = false;
175             bool isKeyUsageKeyAgreement = false;
176 
177             ASN1_BIT_STRING* usage = static_cast<ASN1_BIT_STRING*>(
178                 X509_get_ext_d2i(peerCert, NID_key_usage, nullptr, nullptr));
179 
180             if (usage == nullptr)
181             {
182                 BMCWEB_LOG_DEBUG << this << " TLS usage is null";
183                 return true;
184             }
185 
186             for (int i = 0; i < usage->length; i++)
187             {
188                 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
189                 unsigned char usageChar = usage->data[i];
190                 if (KU_DIGITAL_SIGNATURE & usageChar)
191                 {
192                     isKeyUsageDigitalSignature = true;
193                 }
194                 if (KU_KEY_AGREEMENT & usageChar)
195                 {
196                     isKeyUsageKeyAgreement = true;
197                 }
198             }
199             ASN1_BIT_STRING_free(usage);
200 
201             if (!isKeyUsageDigitalSignature || !isKeyUsageKeyAgreement)
202             {
203                 BMCWEB_LOG_DEBUG << this
204                                  << " Certificate ExtendedKeyUsage does "
205                                     "not allow provided certificate to "
206                                     "be used for user authentication";
207                 return true;
208             }
209 
210             // Determine that ExtendedKeyUsage includes Client Auth
211 
212             stack_st_ASN1_OBJECT* extUsage =
213                 static_cast<stack_st_ASN1_OBJECT*>(X509_get_ext_d2i(
214                     peerCert, NID_ext_key_usage, nullptr, nullptr));
215 
216             if (extUsage == nullptr)
217             {
218                 BMCWEB_LOG_DEBUG << this << " TLS extUsage is null";
219                 return true;
220             }
221 
222             bool isExKeyUsageClientAuth = false;
223             for (int i = 0; i < sk_ASN1_OBJECT_num(extUsage); i++)
224             {
225                 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
226                 int nid = OBJ_obj2nid(sk_ASN1_OBJECT_value(extUsage, i));
227                 if (NID_client_auth == nid)
228                 {
229                     isExKeyUsageClientAuth = true;
230                     break;
231                 }
232             }
233             sk_ASN1_OBJECT_free(extUsage);
234 
235             // Certificate has to have proper key usages set
236             if (!isExKeyUsageClientAuth)
237             {
238                 BMCWEB_LOG_DEBUG << this
239                                  << " Certificate ExtendedKeyUsage does "
240                                     "not allow provided certificate to "
241                                     "be used for user authentication";
242                 return true;
243             }
244             std::string sslUser;
245             // Extract username contained in CommonName
246             sslUser.resize(256, '\0');
247 
248             int status = X509_NAME_get_text_by_NID(
249                 X509_get_subject_name(peerCert), NID_commonName, sslUser.data(),
250                 static_cast<int>(sslUser.size()));
251 
252             if (status == -1)
253             {
254                 BMCWEB_LOG_DEBUG
255                     << this << " TLS cannot get username to create session";
256                 return true;
257             }
258 
259             size_t lastChar = sslUser.find('\0');
260             if (lastChar == std::string::npos || lastChar == 0)
261             {
262                 BMCWEB_LOG_DEBUG << this << " Invalid TLS user name";
263                 return true;
264             }
265             sslUser.resize(lastChar);
266             std::string unsupportedClientId;
267             sessionIsFromTransport = true;
268             userSession = persistent_data::SessionStore::getInstance()
269                               .generateUserSession(
270                                   sslUser, req->ipAddress, unsupportedClientId,
271                                   persistent_data::PersistenceType::TIMEOUT);
272             if (userSession != nullptr)
273             {
274                 BMCWEB_LOG_DEBUG
275                     << this
276                     << " Generating TLS session: " << userSession->uniqueId;
277             }
278             return true;
279         });
280     }
281 
282     Adaptor& socket()
283     {
284         return adaptor;
285     }
286 
287     void start()
288     {
289         if (connectionCount >= 100)
290         {
291             BMCWEB_LOG_CRITICAL << this << "Max connection count exceeded.";
292             return;
293         }
294 
295         startDeadline();
296 
297         // TODO(ed) Abstract this to a more clever class with the idea of an
298         // asynchronous "start"
299         if constexpr (std::is_same_v<Adaptor,
300                                      boost::beast::ssl_stream<
301                                          boost::asio::ip::tcp::socket>>)
302         {
303             adaptor.async_handshake(boost::asio::ssl::stream_base::server,
304                                     [this, self(shared_from_this())](
305                                         const boost::system::error_code& ec) {
306                 if (ec)
307                 {
308                     return;
309                 }
310                 doReadHeaders();
311             });
312         }
313         else
314         {
315             doReadHeaders();
316         }
317     }
318 
319     void handle()
320     {
321         std::error_code reqEc;
322         crow::Request& thisReq = req.emplace(parser->release(), reqEc);
323         if (reqEc)
324         {
325             BMCWEB_LOG_DEBUG << "Request failed to construct" << reqEc;
326             return;
327         }
328         thisReq.session = userSession;
329 
330         // Fetch the client IP address
331         readClientIp();
332 
333         // Check for HTTP version 1.1.
334         if (thisReq.version() == 11)
335         {
336             if (thisReq.getHeaderValue(boost::beast::http::field::host).empty())
337             {
338                 res.result(boost::beast::http::status::bad_request);
339                 completeRequest(res);
340                 return;
341             }
342         }
343 
344         BMCWEB_LOG_INFO << "Request: "
345                         << " " << this << " HTTP/" << thisReq.version() / 10
346                         << "." << thisReq.version() % 10 << ' '
347                         << thisReq.methodString() << " " << thisReq.target()
348                         << " " << thisReq.ipAddress.to_string();
349 
350         res.isAliveHelper = [this]() -> bool { return isAlive(); };
351 
352         thisReq.ioService = static_cast<decltype(thisReq.ioService)>(
353             &adaptor.get_executor().context());
354 
355         if (res.completed)
356         {
357             completeRequest(res);
358             return;
359         }
360 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
361         if (!crow::authentication::isOnAllowlist(req->url, req->method()) &&
362             thisReq.session == nullptr)
363         {
364             BMCWEB_LOG_WARNING << "Authentication failed";
365             forward_unauthorized::sendUnauthorized(
366                 req->url, req->getHeaderValue("X-Requested-With"),
367                 req->getHeaderValue("Accept"), res);
368             completeRequest(res);
369             return;
370         }
371 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
372         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
373         BMCWEB_LOG_DEBUG << "Setting completion handler";
374         asyncResp->res.setCompleteRequestHandler(
375             [self(shared_from_this())](crow::Response& thisRes) {
376             self->completeRequest(thisRes);
377         });
378 
379         if (thisReq.isUpgrade() &&
380             boost::iequals(
381                 thisReq.getHeaderValue(boost::beast::http::field::upgrade),
382                 "websocket"))
383         {
384             handler->handleUpgrade(thisReq, res, std::move(adaptor));
385             // delete lambda with self shared_ptr
386             // to enable connection destruction
387             asyncResp->res.setCompleteRequestHandler(nullptr);
388             return;
389         }
390         handler->handle(thisReq, asyncResp);
391     }
392 
393     bool isAlive()
394     {
395         if constexpr (std::is_same_v<Adaptor,
396                                      boost::beast::ssl_stream<
397                                          boost::asio::ip::tcp::socket>>)
398         {
399             return adaptor.next_layer().is_open();
400         }
401         else
402         {
403             return adaptor.is_open();
404         }
405     }
406     void close()
407     {
408         if constexpr (std::is_same_v<Adaptor,
409                                      boost::beast::ssl_stream<
410                                          boost::asio::ip::tcp::socket>>)
411         {
412             adaptor.next_layer().close();
413             if (sessionIsFromTransport && userSession != nullptr)
414             {
415                 BMCWEB_LOG_DEBUG
416                     << this
417                     << " Removing TLS session: " << userSession->uniqueId;
418                 persistent_data::SessionStore::getInstance().removeSession(
419                     userSession);
420             }
421         }
422         else
423         {
424             adaptor.close();
425         }
426     }
427 
428     void completeRequest(crow::Response& thisRes)
429     {
430         if (!req)
431         {
432             return;
433         }
434         res = std::move(thisRes);
435         BMCWEB_LOG_INFO << "Response: " << this << ' ' << req->url << ' '
436                         << res.resultInt() << " keepalive=" << req->keepAlive();
437 
438         addSecurityHeaders(*req, res);
439 
440         crow::authentication::cleanupTempSession(*req);
441 
442         if (!isAlive())
443         {
444             // BMCWEB_LOG_DEBUG << this << " delete (socket is closed) " <<
445             // isReading
446             // << ' ' << isWriting;
447             // delete this;
448 
449             // delete lambda with self shared_ptr
450             // to enable connection destruction
451             res.setCompleteRequestHandler(nullptr);
452             return;
453         }
454         if (res.body().empty() && !res.jsonValue.empty())
455         {
456             if (http_helpers::requestPrefersHtml(req->getHeaderValue("Accept")))
457             {
458                 prettyPrintJson(res);
459             }
460             else
461             {
462                 res.jsonMode();
463                 res.body() = res.jsonValue.dump(
464                     2, ' ', true, nlohmann::json::error_handler_t::replace);
465             }
466         }
467 
468         if (res.resultInt() >= 400 && res.body().empty())
469         {
470             res.body() = std::string(res.reason());
471         }
472 
473         if (res.result() == boost::beast::http::status::no_content)
474         {
475             // Boost beast throws if content is provided on a no-content
476             // response.  Ideally, this would never happen, but in the case that
477             // it does, we don't want to throw.
478             BMCWEB_LOG_CRITICAL
479                 << this << " Response content provided but code was no-content";
480             res.body().clear();
481         }
482 
483         res.addHeader(boost::beast::http::field::date, getCachedDateStr());
484 
485         res.keepAlive(req->keepAlive());
486 
487         doWrite(res);
488 
489         // delete lambda with self shared_ptr
490         // to enable connection destruction
491         res.setCompleteRequestHandler(nullptr);
492     }
493 
494     void readClientIp()
495     {
496         boost::asio::ip::address ip;
497         boost::system::error_code ec = getClientIp(ip);
498         if (ec)
499         {
500             return;
501         }
502         req->ipAddress = ip;
503     }
504 
505     boost::system::error_code getClientIp(boost::asio::ip::address& ip)
506     {
507         boost::system::error_code ec;
508         BMCWEB_LOG_DEBUG << "Fetch the client IP address";
509         boost::asio::ip::tcp::endpoint endpoint =
510             boost::beast::get_lowest_layer(adaptor).remote_endpoint(ec);
511 
512         if (ec)
513         {
514             // If remote endpoint fails keep going. "ClientOriginIPAddress"
515             // will be empty.
516             BMCWEB_LOG_ERROR << "Failed to get the client's IP Address. ec : "
517                              << ec;
518             return ec;
519         }
520         ip = endpoint.address();
521         return ec;
522     }
523 
524   private:
525     void doReadHeaders()
526     {
527         BMCWEB_LOG_DEBUG << this << " doReadHeaders";
528 
529         // Clean up any previous Connection.
530         boost::beast::http::async_read_header(
531             adaptor, buffer, *parser,
532             [this,
533              self(shared_from_this())](const boost::system::error_code& ec,
534                                        std::size_t bytesTransferred) {
535             BMCWEB_LOG_DEBUG << this << " async_read_header "
536                              << bytesTransferred << " Bytes";
537             bool errorWhileReading = false;
538             if (ec)
539             {
540                 errorWhileReading = true;
541                 if (ec == boost::asio::error::eof)
542                 {
543                     BMCWEB_LOG_WARNING
544                         << this << " Error while reading: " << ec.message();
545                 }
546                 else
547                 {
548                     BMCWEB_LOG_ERROR
549                         << this << " Error while reading: " << ec.message();
550                 }
551             }
552             else
553             {
554                 // if the adaptor isn't open anymore, and wasn't handed to a
555                 // websocket, treat as an error
556                 if (!isAlive() &&
557                     !boost::beast::websocket::is_upgrade(parser->get()))
558                 {
559                     errorWhileReading = true;
560                 }
561             }
562 
563             cancelDeadlineTimer();
564 
565             if (errorWhileReading)
566             {
567                 close();
568                 BMCWEB_LOG_DEBUG << this << " from read(1)";
569                 return;
570             }
571 
572             readClientIp();
573 
574             boost::asio::ip::address ip;
575             if (getClientIp(ip))
576             {
577                 BMCWEB_LOG_DEBUG << "Unable to get client IP";
578             }
579             sessionIsFromTransport = false;
580 #ifndef BMCWEB_INSECURE_DISABLE_AUTHX
581             boost::beast::http::verb method = parser->get().method();
582             userSession = crow::authentication::authenticate(
583                 ip, res, method, parser->get().base(), userSession);
584 
585             bool loggedIn = userSession != nullptr;
586             if (!loggedIn)
587             {
588                 const boost::optional<uint64_t> contentLength =
589                     parser->content_length();
590                 if (contentLength && *contentLength > loggedOutPostBodyLimit)
591                 {
592                     BMCWEB_LOG_DEBUG << "Content length greater than limit "
593                                      << *contentLength;
594                     close();
595                     return;
596                 }
597 
598                 BMCWEB_LOG_DEBUG << "Starting quick deadline";
599             }
600 #endif // BMCWEB_INSECURE_DISABLE_AUTHX
601 
602             doRead();
603             });
604     }
605 
606     void doRead()
607     {
608         BMCWEB_LOG_DEBUG << this << " doRead";
609         startDeadline();
610         boost::beast::http::async_read(adaptor, buffer, *parser,
611                                        [this, self(shared_from_this())](
612                                            const boost::system::error_code& ec,
613                                            std::size_t bytesTransferred) {
614             BMCWEB_LOG_DEBUG << this << " async_read " << bytesTransferred
615                              << " Bytes";
616             cancelDeadlineTimer();
617             if (ec)
618             {
619                 BMCWEB_LOG_ERROR << this
620                                  << " Error while reading: " << ec.message();
621                 close();
622                 BMCWEB_LOG_DEBUG << this << " from read(1)";
623                 return;
624             }
625             handle();
626         });
627     }
628 
629     void doWrite(crow::Response& thisRes)
630     {
631         BMCWEB_LOG_DEBUG << this << " doWrite";
632         thisRes.preparePayload();
633         serializer.emplace(*thisRes.stringResponse);
634         startDeadline();
635         boost::beast::http::async_write(adaptor, *serializer,
636                                         [this, self(shared_from_this())](
637                                             const boost::system::error_code& ec,
638                                             std::size_t bytesTransferred) {
639             BMCWEB_LOG_DEBUG << this << " async_write " << bytesTransferred
640                              << " bytes";
641 
642             cancelDeadlineTimer();
643 
644             if (ec)
645             {
646                 BMCWEB_LOG_DEBUG << this << " from write(2)";
647                 return;
648             }
649             if (!res.keepAlive())
650             {
651                 close();
652                 BMCWEB_LOG_DEBUG << this << " from write(1)";
653                 return;
654             }
655 
656             serializer.reset();
657             BMCWEB_LOG_DEBUG << this << " Clearing response";
658             res.clear();
659             parser.emplace(std::piecewise_construct, std::make_tuple());
660             parser->body_limit(httpReqBodyLimit); // reset body limit for
661                                                   // newly created parser
662             buffer.consume(buffer.size());
663 
664             // If the session was built from the transport, we don't need to
665             // clear it.  All other sessions are generated per request.
666             if (!sessionIsFromTransport)
667             {
668                 userSession = nullptr;
669             }
670 
671             // Destroy the Request via the std::optional
672             req.reset();
673             doReadHeaders();
674         });
675     }
676 
677     void cancelDeadlineTimer()
678     {
679         timer.cancel();
680     }
681 
682     void startDeadline()
683     {
684         cancelDeadlineTimer();
685 
686         std::chrono::seconds timeout(15);
687         // allow slow uploads for logged in users
688         bool loggedIn = userSession != nullptr;
689         if (loggedIn)
690         {
691             timeout = std::chrono::seconds(60);
692             return;
693         }
694 
695         std::weak_ptr<Connection<Adaptor, Handler>> weakSelf = weak_from_this();
696         timer.expires_after(timeout);
697         timer.async_wait([weakSelf](const boost::system::error_code ec) {
698             // Note, we are ignoring other types of errors here;  If the timer
699             // failed for any reason, we should still close the connection
700 
701             std::shared_ptr<Connection<Adaptor, Handler>> self =
702                 weakSelf.lock();
703             if (!self)
704             {
705                 BMCWEB_LOG_CRITICAL << self << " Failed to capture connection";
706                 return;
707             }
708             if (ec == boost::asio::error::operation_aborted)
709             {
710                 // Canceled wait means the path succeeeded.
711                 return;
712             }
713             if (ec)
714             {
715                 BMCWEB_LOG_CRITICAL << self << " timer failed " << ec;
716             }
717 
718             BMCWEB_LOG_WARNING << self << "Connection timed out, closing";
719 
720             self->close();
721         });
722 
723         BMCWEB_LOG_DEBUG << this << " timer started";
724     }
725 
726     Adaptor adaptor;
727     Handler* handler;
728     // Making this a std::optional allows it to be efficiently destroyed and
729     // re-created on Connection reset
730     std::optional<
731         boost::beast::http::request_parser<boost::beast::http::string_body>>
732         parser;
733 
734     boost::beast::flat_static_buffer<8192> buffer;
735 
736     std::optional<boost::beast::http::response_serializer<
737         boost::beast::http::string_body>>
738         serializer;
739 
740     std::optional<crow::Request> req;
741     crow::Response res;
742 
743     bool sessionIsFromTransport = false;
744     std::shared_ptr<persistent_data::UserSession> userSession;
745 
746     boost::asio::steady_timer timer;
747 
748     std::function<std::string()>& getCachedDateStr;
749 
750     using std::enable_shared_from_this<
751         Connection<Adaptor, Handler>>::shared_from_this;
752 
753     using std::enable_shared_from_this<
754         Connection<Adaptor, Handler>>::weak_from_this;
755 };
756 } // namespace crow
757