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