xref: /openbmc/bmcweb/http/websocket.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
1*40e9b92eSEd Tanous // SPDX-License-Identifier: Apache-2.0
2*40e9b92eSEd Tanous // SPDX-FileCopyrightText: Copyright OpenBMC Authors
304e438cbSEd Tanous #pragma once
43ccb3adbSEd Tanous #include "async_resp.hpp"
5b2896149SEd Tanous #include "http_body.hpp"
604e438cbSEd Tanous #include "http_request.hpp"
704e438cbSEd Tanous 
804e438cbSEd Tanous #include <boost/asio/buffer.hpp>
9ad6dd39bSLei YU #include <boost/asio/ssl/error.hpp>
10863c1c2eSEd Tanous #include <boost/beast/core/multi_buffer.hpp>
1104e438cbSEd Tanous #include <boost/beast/websocket.hpp>
128db83747SEd Tanous #include <boost/beast/websocket/ssl.hpp>
1304e438cbSEd Tanous 
1404e438cbSEd Tanous #include <array>
1504e438cbSEd Tanous #include <functional>
1604e438cbSEd Tanous 
1704e438cbSEd Tanous namespace crow
1804e438cbSEd Tanous {
1904e438cbSEd Tanous namespace websocket
2004e438cbSEd Tanous {
2104e438cbSEd Tanous 
22863c1c2eSEd Tanous enum class MessageType
23863c1c2eSEd Tanous {
24863c1c2eSEd Tanous     Binary,
25863c1c2eSEd Tanous     Text,
26863c1c2eSEd Tanous };
27863c1c2eSEd Tanous 
2804e438cbSEd Tanous struct Connection : std::enable_shared_from_this<Connection>
2904e438cbSEd Tanous {
3004e438cbSEd Tanous   public:
315ebb9d33SEd Tanous     Connection() = default;
3204e438cbSEd Tanous 
33ecd6a3a2SEd Tanous     Connection(const Connection&) = delete;
34ecd6a3a2SEd Tanous     Connection(Connection&&) = delete;
35ecd6a3a2SEd Tanous     Connection& operator=(const Connection&) = delete;
36ecd6a3a2SEd Tanous     Connection& operator=(const Connection&&) = delete;
37ecd6a3a2SEd Tanous 
389eb808c1SEd Tanous     virtual void sendBinary(std::string_view msg) = 0;
39863c1c2eSEd Tanous     virtual void sendEx(MessageType type, std::string_view msg,
40863c1c2eSEd Tanous                         std::function<void()>&& onDone) = 0;
419eb808c1SEd Tanous     virtual void sendText(std::string_view msg) = 0;
429eb808c1SEd Tanous     virtual void close(std::string_view msg = "quit") = 0;
43863c1c2eSEd Tanous     virtual void deferRead() = 0;
44863c1c2eSEd Tanous     virtual void resumeRead() = 0;
4504e438cbSEd Tanous     virtual boost::asio::io_context& getIoContext() = 0;
4604e438cbSEd Tanous     virtual ~Connection() = default;
47052bcbf4SNinad Palsule     virtual boost::urls::url_view url() = 0;
4804e438cbSEd Tanous };
4904e438cbSEd Tanous 
5004e438cbSEd Tanous template <typename Adaptor>
5104e438cbSEd Tanous class ConnectionImpl : public Connection
5204e438cbSEd Tanous {
535ebb9d33SEd Tanous     using self_t = ConnectionImpl<Adaptor>;
545ebb9d33SEd Tanous 
5504e438cbSEd Tanous   public:
ConnectionImpl(const boost::urls::url_view & urlViewIn,const std::shared_ptr<persistent_data::UserSession> & sessionIn,Adaptor adaptorIn,std::function<void (Connection &)> openHandlerIn,std::function<void (Connection &,const std::string &,bool)> messageHandlerIn,std::function<void (crow::websocket::Connection &,std::string_view,crow::websocket::MessageType type,std::function<void ()> && whenComplete)> messageExHandlerIn,std::function<void (Connection &,const std::string &)> closeHandlerIn,std::function<void (Connection &)> errorHandlerIn)5604e438cbSEd Tanous     ConnectionImpl(
575ebb9d33SEd Tanous         const boost::urls::url_view& urlViewIn,
585ebb9d33SEd Tanous         const std::shared_ptr<persistent_data::UserSession>& sessionIn,
59052bcbf4SNinad Palsule         Adaptor adaptorIn, std::function<void(Connection&)> openHandlerIn,
6004e438cbSEd Tanous         std::function<void(Connection&, const std::string&, bool)>
618a592810SEd Tanous             messageHandlerIn,
62863c1c2eSEd Tanous         std::function<void(crow::websocket::Connection&, std::string_view,
63863c1c2eSEd Tanous                            crow::websocket::MessageType type,
64863c1c2eSEd Tanous                            std::function<void()>&& whenComplete)>
65863c1c2eSEd Tanous             messageExHandlerIn,
668a592810SEd Tanous         std::function<void(Connection&, const std::string&)> closeHandlerIn,
678a592810SEd Tanous         std::function<void(Connection&)> errorHandlerIn) :
68bd79bce8SPatrick Williams         uri(urlViewIn), ws(std::move(adaptorIn)), inBuffer(inString, 131088),
698a592810SEd Tanous         openHandler(std::move(openHandlerIn)),
708a592810SEd Tanous         messageHandler(std::move(messageHandlerIn)),
71863c1c2eSEd Tanous         messageExHandler(std::move(messageExHandlerIn)),
728a592810SEd Tanous         closeHandler(std::move(closeHandlerIn)),
735ebb9d33SEd Tanous         errorHandler(std::move(errorHandlerIn)), session(sessionIn)
7404e438cbSEd Tanous     {
7502bdd967Sdhineskumare         /* Turn on the timeouts on websocket stream to server role */
7602bdd967Sdhineskumare         ws.set_option(boost::beast::websocket::stream_base::timeout::suggested(
7702bdd967Sdhineskumare             boost::beast::role_type::server));
7862598e31SEd Tanous         BMCWEB_LOG_DEBUG("Creating new connection {}", logPtr(this));
7904e438cbSEd Tanous     }
8004e438cbSEd Tanous 
getIoContext()8104e438cbSEd Tanous     boost::asio::io_context& getIoContext() override
8204e438cbSEd Tanous     {
8304e438cbSEd Tanous         return static_cast<boost::asio::io_context&>(
8404e438cbSEd Tanous             ws.get_executor().context());
8504e438cbSEd Tanous     }
8604e438cbSEd Tanous 
start(const crow::Request & req)875ebb9d33SEd Tanous     void start(const crow::Request& req)
8804e438cbSEd Tanous     {
8962598e31SEd Tanous         BMCWEB_LOG_DEBUG("starting connection {}", logPtr(this));
9004e438cbSEd Tanous 
9104e438cbSEd Tanous         using bf = boost::beast::http::field;
921873a04fSMyung Bae         std::string protocolHeader{
931873a04fSMyung Bae             req.getHeaderValue(bf::sec_websocket_protocol)};
9404e438cbSEd Tanous 
9504e438cbSEd Tanous         ws.set_option(boost::beast::websocket::stream_base::decorator(
965ebb9d33SEd Tanous             [session{session},
975ebb9d33SEd Tanous              protocolHeader](boost::beast::websocket::response_type& m) {
9883328316SEd Tanous                 if constexpr (!BMCWEB_INSECURE_DISABLE_CSRF)
9983328316SEd Tanous                 {
100a90daf18SEd Tanous                     if (session != nullptr)
101a90daf18SEd Tanous                     {
10204e438cbSEd Tanous                         // use protocol for csrf checking
1037e9c08edSEd Tanous                         if (session->cookieAuth &&
104bd79bce8SPatrick Williams                             !bmcweb::constantTimeStringCompare(
105bd79bce8SPatrick Williams                                 protocolHeader, session->csrfToken))
10604e438cbSEd Tanous                         {
10762598e31SEd Tanous                             BMCWEB_LOG_ERROR("Websocket CSRF error");
10804e438cbSEd Tanous                             m.result(boost::beast::http::status::unauthorized);
10904e438cbSEd Tanous                             return;
11004e438cbSEd Tanous                         }
111a90daf18SEd Tanous                     }
11283328316SEd Tanous                 }
1135ebb9d33SEd Tanous                 if (!protocolHeader.empty())
11404e438cbSEd Tanous                 {
1155ebb9d33SEd Tanous                     m.insert(bf::sec_websocket_protocol, protocolHeader);
11604e438cbSEd Tanous                 }
11704e438cbSEd Tanous 
118bd79bce8SPatrick Williams                 m.insert(bf::strict_transport_security,
119bd79bce8SPatrick Williams                          "max-age=31536000; "
12004e438cbSEd Tanous                          "includeSubdomains; "
12104e438cbSEd Tanous                          "preload");
12204e438cbSEd Tanous                 m.insert(bf::pragma, "no-cache");
12304e438cbSEd Tanous                 m.insert(bf::cache_control, "no-Store,no-Cache");
12404e438cbSEd Tanous                 m.insert("Content-Security-Policy", "default-src 'self'");
12504e438cbSEd Tanous                 m.insert("X-XSS-Protection", "1; "
12604e438cbSEd Tanous                                              "mode=block");
12704e438cbSEd Tanous                 m.insert("X-Content-Type-Options", "nosniff");
12804e438cbSEd Tanous             }));
12904e438cbSEd Tanous 
1305ebb9d33SEd Tanous         // Make a pointer to keep the req alive while we accept it.
131b2896149SEd Tanous         using Body = boost::beast::http::request<bmcweb::HttpBody>;
1325ebb9d33SEd Tanous         std::unique_ptr<Body> mobile = std::make_unique<Body>(req.req);
1335ebb9d33SEd Tanous         Body* ptr = mobile.get();
13404e438cbSEd Tanous         // Perform the websocket upgrade
1355ebb9d33SEd Tanous         ws.async_accept(*ptr,
1365ebb9d33SEd Tanous                         std::bind_front(&self_t::acceptDone, this,
1375ebb9d33SEd Tanous                                         shared_from_this(), std::move(mobile)));
13804e438cbSEd Tanous     }
13904e438cbSEd Tanous 
sendBinary(std::string_view msg)14026ccae32SEd Tanous     void sendBinary(std::string_view msg) override
14104e438cbSEd Tanous     {
14204e438cbSEd Tanous         ws.binary(true);
143863c1c2eSEd Tanous         outBuffer.commit(boost::asio::buffer_copy(outBuffer.prepare(msg.size()),
144863c1c2eSEd Tanous                                                   boost::asio::buffer(msg)));
14504e438cbSEd Tanous         doWrite();
14604e438cbSEd Tanous     }
14704e438cbSEd Tanous 
sendEx(MessageType type,std::string_view msg,std::function<void ()> && onDone)148863c1c2eSEd Tanous     void sendEx(MessageType type, std::string_view msg,
149863c1c2eSEd Tanous                 std::function<void()>&& onDone) override
150863c1c2eSEd Tanous     {
151863c1c2eSEd Tanous         if (doingWrite)
152863c1c2eSEd Tanous         {
15362598e31SEd Tanous             BMCWEB_LOG_CRITICAL(
15462598e31SEd Tanous                 "Cannot mix sendEx usage with sendBinary or sendText");
155863c1c2eSEd Tanous             onDone();
156863c1c2eSEd Tanous             return;
157863c1c2eSEd Tanous         }
158863c1c2eSEd Tanous         ws.binary(type == MessageType::Binary);
159863c1c2eSEd Tanous 
160863c1c2eSEd Tanous         ws.async_write(boost::asio::buffer(msg),
161863c1c2eSEd Tanous                        [weak(weak_from_this()), onDone{std::move(onDone)}](
162863c1c2eSEd Tanous                            const boost::beast::error_code& ec, size_t) {
163863c1c2eSEd Tanous                            std::shared_ptr<Connection> self = weak.lock();
164a8894201Szhaogang.0108                            if (!self)
165a8894201Szhaogang.0108                            {
166a8894201Szhaogang.0108                                BMCWEB_LOG_ERROR("Connection went away");
167a8894201Szhaogang.0108                                return;
168a8894201Szhaogang.0108                            }
169863c1c2eSEd Tanous 
170863c1c2eSEd Tanous                            // Call the done handler regardless of whether we
171863c1c2eSEd Tanous                            // errored, but before we close things out
172863c1c2eSEd Tanous                            onDone();
173863c1c2eSEd Tanous 
174863c1c2eSEd Tanous                            if (ec)
175863c1c2eSEd Tanous                            {
176bd79bce8SPatrick Williams                                BMCWEB_LOG_ERROR("Error in ws.async_write {}",
177bd79bce8SPatrick Williams                                                 ec);
178863c1c2eSEd Tanous                                self->close("write error");
179863c1c2eSEd Tanous                            }
180863c1c2eSEd Tanous                        });
181863c1c2eSEd Tanous     }
182863c1c2eSEd Tanous 
sendText(std::string_view msg)18326ccae32SEd Tanous     void sendText(std::string_view msg) override
18404e438cbSEd Tanous     {
18504e438cbSEd Tanous         ws.text(true);
186863c1c2eSEd Tanous         outBuffer.commit(boost::asio::buffer_copy(outBuffer.prepare(msg.size()),
187863c1c2eSEd Tanous                                                   boost::asio::buffer(msg)));
18804e438cbSEd Tanous         doWrite();
18904e438cbSEd Tanous     }
19004e438cbSEd Tanous 
close(std::string_view msg)19126ccae32SEd Tanous     void close(std::string_view msg) override
19204e438cbSEd Tanous     {
19304e438cbSEd Tanous         ws.async_close(
19404e438cbSEd Tanous             {boost::beast::websocket::close_code::normal, msg},
1955e7e2dc5SEd Tanous             [self(shared_from_this())](const boost::system::error_code& ec) {
19604e438cbSEd Tanous                 if (ec == boost::asio::error::operation_aborted)
19704e438cbSEd Tanous                 {
19804e438cbSEd Tanous                     return;
19904e438cbSEd Tanous                 }
20004e438cbSEd Tanous                 if (ec)
20104e438cbSEd Tanous                 {
20262598e31SEd Tanous                     BMCWEB_LOG_ERROR("Error closing websocket {}", ec);
20304e438cbSEd Tanous                     return;
20404e438cbSEd Tanous                 }
20504e438cbSEd Tanous             });
20604e438cbSEd Tanous     }
20704e438cbSEd Tanous 
url()208052bcbf4SNinad Palsule     boost::urls::url_view url() override
209052bcbf4SNinad Palsule     {
210052bcbf4SNinad Palsule         return uri;
211052bcbf4SNinad Palsule     }
212052bcbf4SNinad Palsule 
acceptDone(const std::shared_ptr<Connection> &,const std::unique_ptr<boost::beast::http::request<bmcweb::HttpBody>> &,const boost::system::error_code & ec)2135ebb9d33SEd Tanous     void acceptDone(const std::shared_ptr<Connection>& /*self*/,
21452e31629SEd Tanous                     const std::unique_ptr<
215b2896149SEd Tanous                         boost::beast::http::request<bmcweb::HttpBody>>& /*req*/,
2165ebb9d33SEd Tanous                     const boost::system::error_code& ec)
21704e438cbSEd Tanous     {
2185ebb9d33SEd Tanous         if (ec)
2195ebb9d33SEd Tanous         {
2205ebb9d33SEd Tanous             BMCWEB_LOG_ERROR("Error in ws.async_accept {}", ec);
2215ebb9d33SEd Tanous             return;
2225ebb9d33SEd Tanous         }
22362598e31SEd Tanous         BMCWEB_LOG_DEBUG("Websocket accepted connection");
22404e438cbSEd Tanous 
22504e438cbSEd Tanous         if (openHandler)
22604e438cbSEd Tanous         {
2277772638eSzhanghch05             openHandler(*this);
22804e438cbSEd Tanous         }
229863c1c2eSEd Tanous         doRead();
230863c1c2eSEd Tanous     }
231863c1c2eSEd Tanous 
deferRead()232863c1c2eSEd Tanous     void deferRead() override
233863c1c2eSEd Tanous     {
234863c1c2eSEd Tanous         readingDefered = true;
235863c1c2eSEd Tanous 
236863c1c2eSEd Tanous         // If we're not actively reading, we need to take ownership of
237863c1c2eSEd Tanous         // ourselves for a small portion of time, do that, and clear when we
238863c1c2eSEd Tanous         // resume.
239863c1c2eSEd Tanous         selfOwned = shared_from_this();
240863c1c2eSEd Tanous     }
241863c1c2eSEd Tanous 
resumeRead()242863c1c2eSEd Tanous     void resumeRead() override
243863c1c2eSEd Tanous     {
244863c1c2eSEd Tanous         readingDefered = false;
245863c1c2eSEd Tanous         doRead();
246863c1c2eSEd Tanous 
247863c1c2eSEd Tanous         // No longer need to keep ourselves alive now that read is active.
248863c1c2eSEd Tanous         selfOwned.reset();
24904e438cbSEd Tanous     }
25004e438cbSEd Tanous 
doRead()25104e438cbSEd Tanous     void doRead()
25204e438cbSEd Tanous     {
253863c1c2eSEd Tanous         if (readingDefered)
254863c1c2eSEd Tanous         {
255863c1c2eSEd Tanous             return;
256863c1c2eSEd Tanous         }
257863c1c2eSEd Tanous         ws.async_read(inBuffer, [this, self(shared_from_this())](
258863c1c2eSEd Tanous                                     const boost::beast::error_code& ec,
259863c1c2eSEd Tanous                                     size_t bytesRead) {
26004e438cbSEd Tanous             if (ec)
26104e438cbSEd Tanous             {
262ad6dd39bSLei YU                 if (ec != boost::beast::websocket::error::closed &&
263ad6dd39bSLei YU                     ec != boost::asio::error::eof &&
264ad6dd39bSLei YU                     ec != boost::asio::ssl::error::stream_truncated)
26504e438cbSEd Tanous                 {
26662598e31SEd Tanous                     BMCWEB_LOG_ERROR("doRead error {}", ec);
26704e438cbSEd Tanous                 }
26804e438cbSEd Tanous                 if (closeHandler)
26904e438cbSEd Tanous                 {
270079360aeSEd Tanous                     std::string reason{ws.reason().reason.c_str()};
271079360aeSEd Tanous                     closeHandler(*this, reason);
27204e438cbSEd Tanous                 }
27304e438cbSEd Tanous                 return;
27404e438cbSEd Tanous             }
275863c1c2eSEd Tanous 
276863c1c2eSEd Tanous             handleMessage(bytesRead);
27704e438cbSEd Tanous         });
27804e438cbSEd Tanous     }
doWrite()27904e438cbSEd Tanous     void doWrite()
28004e438cbSEd Tanous     {
28104e438cbSEd Tanous         // If we're already doing a write, ignore the request, it will be picked
28204e438cbSEd Tanous         // up when the current write is complete
28304e438cbSEd Tanous         if (doingWrite)
28404e438cbSEd Tanous         {
28504e438cbSEd Tanous             return;
28604e438cbSEd Tanous         }
28704e438cbSEd Tanous 
288863c1c2eSEd Tanous         if (outBuffer.size() == 0)
28904e438cbSEd Tanous         {
29004e438cbSEd Tanous             // Done for now
29104e438cbSEd Tanous             return;
29204e438cbSEd Tanous         }
29304e438cbSEd Tanous         doingWrite = true;
294863c1c2eSEd Tanous         ws.async_write(outBuffer.data(), [this, self(shared_from_this())](
295863c1c2eSEd Tanous                                              const boost::beast::error_code& ec,
296863c1c2eSEd Tanous                                              size_t bytesSent) {
29704e438cbSEd Tanous             doingWrite = false;
298863c1c2eSEd Tanous             outBuffer.consume(bytesSent);
29904e438cbSEd Tanous             if (ec == boost::beast::websocket::error::closed)
30004e438cbSEd Tanous             {
30104e438cbSEd Tanous                 // Do nothing here.  doRead handler will call the
30204e438cbSEd Tanous                 // closeHandler.
30304e438cbSEd Tanous                 close("Write error");
30404e438cbSEd Tanous                 return;
30504e438cbSEd Tanous             }
30604e438cbSEd Tanous             if (ec)
30704e438cbSEd Tanous             {
30862598e31SEd Tanous                 BMCWEB_LOG_ERROR("Error in ws.async_write {}", ec);
30904e438cbSEd Tanous                 return;
31004e438cbSEd Tanous             }
31104e438cbSEd Tanous             doWrite();
31204e438cbSEd Tanous         });
31304e438cbSEd Tanous     }
31404e438cbSEd Tanous 
31504e438cbSEd Tanous   private:
handleMessage(size_t bytesRead)316863c1c2eSEd Tanous     void handleMessage(size_t bytesRead)
317863c1c2eSEd Tanous     {
318863c1c2eSEd Tanous         if (messageExHandler)
319863c1c2eSEd Tanous         {
320863c1c2eSEd Tanous             // Note, because of the interactions with the read buffers,
321863c1c2eSEd Tanous             // this message handler overrides the normal message handler
322863c1c2eSEd Tanous             messageExHandler(*this, inString, MessageType::Binary,
323863c1c2eSEd Tanous                              [this, self(shared_from_this()), bytesRead]() {
324863c1c2eSEd Tanous                                  if (self == nullptr)
325863c1c2eSEd Tanous                                  {
326863c1c2eSEd Tanous                                      return;
327863c1c2eSEd Tanous                                  }
328863c1c2eSEd Tanous 
329863c1c2eSEd Tanous                                  inBuffer.consume(bytesRead);
330863c1c2eSEd Tanous                                  inString.clear();
331863c1c2eSEd Tanous 
332863c1c2eSEd Tanous                                  doRead();
333863c1c2eSEd Tanous                              });
334863c1c2eSEd Tanous             return;
335863c1c2eSEd Tanous         }
336863c1c2eSEd Tanous 
337863c1c2eSEd Tanous         if (messageHandler)
338863c1c2eSEd Tanous         {
339863c1c2eSEd Tanous             messageHandler(*this, inString, ws.got_text());
340863c1c2eSEd Tanous         }
341863c1c2eSEd Tanous         inBuffer.consume(bytesRead);
342863c1c2eSEd Tanous         inString.clear();
343863c1c2eSEd Tanous         doRead();
344863c1c2eSEd Tanous     }
345863c1c2eSEd Tanous 
346052bcbf4SNinad Palsule     boost::urls::url uri;
347052bcbf4SNinad Palsule 
3482aee6ca2SEd Tanous     boost::beast::websocket::stream<Adaptor, false> ws;
34904e438cbSEd Tanous 
350863c1c2eSEd Tanous     bool readingDefered = false;
35104e438cbSEd Tanous     std::string inString;
35204e438cbSEd Tanous     boost::asio::dynamic_string_buffer<std::string::value_type,
35304e438cbSEd Tanous                                        std::string::traits_type,
35404e438cbSEd Tanous                                        std::string::allocator_type>
35504e438cbSEd Tanous         inBuffer;
356863c1c2eSEd Tanous 
357863c1c2eSEd Tanous     boost::beast::multi_buffer outBuffer;
35804e438cbSEd Tanous     bool doingWrite = false;
35904e438cbSEd Tanous 
3607772638eSzhanghch05     std::function<void(Connection&)> openHandler;
36104e438cbSEd Tanous     std::function<void(Connection&, const std::string&, bool)> messageHandler;
362863c1c2eSEd Tanous     std::function<void(crow::websocket::Connection&, std::string_view,
363863c1c2eSEd Tanous                        crow::websocket::MessageType type,
364863c1c2eSEd Tanous                        std::function<void()>&& whenComplete)>
365863c1c2eSEd Tanous         messageExHandler;
36604e438cbSEd Tanous     std::function<void(Connection&, const std::string&)> closeHandler;
36704e438cbSEd Tanous     std::function<void(Connection&)> errorHandler;
36804e438cbSEd Tanous     std::shared_ptr<persistent_data::UserSession> session;
369863c1c2eSEd Tanous 
370863c1c2eSEd Tanous     std::shared_ptr<Connection> selfOwned;
37104e438cbSEd Tanous };
37204e438cbSEd Tanous } // namespace websocket
37304e438cbSEd Tanous } // namespace crow
374