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