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