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