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