xref: /openbmc/bmcweb/http/websocket.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 #include "async_resp.hpp"
5 #include "http_body.hpp"
6 #include "http_request.hpp"
7 
8 #include <boost/asio/buffer.hpp>
9 #include <boost/asio/ssl/error.hpp>
10 #include <boost/beast/core/multi_buffer.hpp>
11 #include <boost/beast/websocket.hpp>
12 #include <boost/beast/websocket/ssl.hpp>
13 
14 #include <array>
15 #include <functional>
16 
17 namespace crow
18 {
19 namespace websocket
20 {
21 
22 enum class MessageType
23 {
24     Binary,
25     Text,
26 };
27 
28 struct Connection : std::enable_shared_from_this<Connection>
29 {
30   public:
31     Connection() = default;
32 
33     Connection(const Connection&) = delete;
34     Connection(Connection&&) = delete;
35     Connection& operator=(const Connection&) = delete;
36     Connection& operator=(const Connection&&) = delete;
37 
38     virtual void sendBinary(std::string_view msg) = 0;
39     virtual void sendEx(MessageType type, std::string_view msg,
40                         std::function<void()>&& onDone) = 0;
41     virtual void sendText(std::string_view msg) = 0;
42     virtual void close(std::string_view msg = "quit") = 0;
43     virtual void deferRead() = 0;
44     virtual void resumeRead() = 0;
45     virtual boost::asio::io_context& getIoContext() = 0;
46     virtual ~Connection() = default;
47     virtual boost::urls::url_view url() = 0;
48 };
49 
50 template <typename Adaptor>
51 class ConnectionImpl : public Connection
52 {
53     using self_t = ConnectionImpl<Adaptor>;
54 
55   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)56     ConnectionImpl(
57         const boost::urls::url_view& urlViewIn,
58         const std::shared_ptr<persistent_data::UserSession>& sessionIn,
59         Adaptor adaptorIn, std::function<void(Connection&)> openHandlerIn,
60         std::function<void(Connection&, const std::string&, bool)>
61             messageHandlerIn,
62         std::function<void(crow::websocket::Connection&, std::string_view,
63                            crow::websocket::MessageType type,
64                            std::function<void()>&& whenComplete)>
65             messageExHandlerIn,
66         std::function<void(Connection&, const std::string&)> closeHandlerIn,
67         std::function<void(Connection&)> errorHandlerIn) :
68         uri(urlViewIn), ws(std::move(adaptorIn)), inBuffer(inString, 131088),
69         openHandler(std::move(openHandlerIn)),
70         messageHandler(std::move(messageHandlerIn)),
71         messageExHandler(std::move(messageExHandlerIn)),
72         closeHandler(std::move(closeHandlerIn)),
73         errorHandler(std::move(errorHandlerIn)), session(sessionIn)
74     {
75         /* Turn on the timeouts on websocket stream to server role */
76         ws.set_option(boost::beast::websocket::stream_base::timeout::suggested(
77             boost::beast::role_type::server));
78         BMCWEB_LOG_DEBUG("Creating new connection {}", logPtr(this));
79     }
80 
getIoContext()81     boost::asio::io_context& getIoContext() override
82     {
83         return static_cast<boost::asio::io_context&>(
84             ws.get_executor().context());
85     }
86 
start(const crow::Request & req)87     void start(const crow::Request& req)
88     {
89         BMCWEB_LOG_DEBUG("starting connection {}", logPtr(this));
90 
91         using bf = boost::beast::http::field;
92         std::string protocolHeader{
93             req.getHeaderValue(bf::sec_websocket_protocol)};
94 
95         ws.set_option(boost::beast::websocket::stream_base::decorator(
96             [session{session},
97              protocolHeader](boost::beast::websocket::response_type& m) {
98                 if constexpr (!BMCWEB_INSECURE_DISABLE_CSRF)
99                 {
100                     if (session != nullptr)
101                     {
102                         // use protocol for csrf checking
103                         if (session->cookieAuth &&
104                             !bmcweb::constantTimeStringCompare(
105                                 protocolHeader, session->csrfToken))
106                         {
107                             BMCWEB_LOG_ERROR("Websocket CSRF error");
108                             m.result(boost::beast::http::status::unauthorized);
109                             return;
110                         }
111                     }
112                 }
113                 if (!protocolHeader.empty())
114                 {
115                     m.insert(bf::sec_websocket_protocol, protocolHeader);
116                 }
117 
118                 m.insert(bf::strict_transport_security,
119                          "max-age=31536000; "
120                          "includeSubdomains; "
121                          "preload");
122                 m.insert(bf::pragma, "no-cache");
123                 m.insert(bf::cache_control, "no-Store,no-Cache");
124                 m.insert("Content-Security-Policy", "default-src 'self'");
125                 m.insert("X-XSS-Protection", "1; "
126                                              "mode=block");
127                 m.insert("X-Content-Type-Options", "nosniff");
128             }));
129 
130         // Make a pointer to keep the req alive while we accept it.
131         using Body = boost::beast::http::request<bmcweb::HttpBody>;
132         std::unique_ptr<Body> mobile = std::make_unique<Body>(req.req);
133         Body* ptr = mobile.get();
134         // Perform the websocket upgrade
135         ws.async_accept(*ptr,
136                         std::bind_front(&self_t::acceptDone, this,
137                                         shared_from_this(), std::move(mobile)));
138     }
139 
sendBinary(std::string_view msg)140     void sendBinary(std::string_view msg) override
141     {
142         ws.binary(true);
143         outBuffer.commit(boost::asio::buffer_copy(outBuffer.prepare(msg.size()),
144                                                   boost::asio::buffer(msg)));
145         doWrite();
146     }
147 
sendEx(MessageType type,std::string_view msg,std::function<void ()> && onDone)148     void sendEx(MessageType type, std::string_view msg,
149                 std::function<void()>&& onDone) override
150     {
151         if (doingWrite)
152         {
153             BMCWEB_LOG_CRITICAL(
154                 "Cannot mix sendEx usage with sendBinary or sendText");
155             onDone();
156             return;
157         }
158         ws.binary(type == MessageType::Binary);
159 
160         ws.async_write(boost::asio::buffer(msg),
161                        [weak(weak_from_this()), onDone{std::move(onDone)}](
162                            const boost::beast::error_code& ec, size_t) {
163                            std::shared_ptr<Connection> self = weak.lock();
164                            if (!self)
165                            {
166                                BMCWEB_LOG_ERROR("Connection went away");
167                                return;
168                            }
169 
170                            // Call the done handler regardless of whether we
171                            // errored, but before we close things out
172                            onDone();
173 
174                            if (ec)
175                            {
176                                BMCWEB_LOG_ERROR("Error in ws.async_write {}",
177                                                 ec);
178                                self->close("write error");
179                            }
180                        });
181     }
182 
sendText(std::string_view msg)183     void sendText(std::string_view msg) override
184     {
185         ws.text(true);
186         outBuffer.commit(boost::asio::buffer_copy(outBuffer.prepare(msg.size()),
187                                                   boost::asio::buffer(msg)));
188         doWrite();
189     }
190 
close(std::string_view msg)191     void close(std::string_view msg) override
192     {
193         ws.async_close(
194             {boost::beast::websocket::close_code::normal, msg},
195             [self(shared_from_this())](const boost::system::error_code& ec) {
196                 if (ec == boost::asio::error::operation_aborted)
197                 {
198                     return;
199                 }
200                 if (ec)
201                 {
202                     BMCWEB_LOG_ERROR("Error closing websocket {}", ec);
203                     return;
204                 }
205             });
206     }
207 
url()208     boost::urls::url_view url() override
209     {
210         return uri;
211     }
212 
acceptDone(const std::shared_ptr<Connection> &,const std::unique_ptr<boost::beast::http::request<bmcweb::HttpBody>> &,const boost::system::error_code & ec)213     void acceptDone(const std::shared_ptr<Connection>& /*self*/,
214                     const std::unique_ptr<
215                         boost::beast::http::request<bmcweb::HttpBody>>& /*req*/,
216                     const boost::system::error_code& ec)
217     {
218         if (ec)
219         {
220             BMCWEB_LOG_ERROR("Error in ws.async_accept {}", ec);
221             return;
222         }
223         BMCWEB_LOG_DEBUG("Websocket accepted connection");
224 
225         if (openHandler)
226         {
227             openHandler(*this);
228         }
229         doRead();
230     }
231 
deferRead()232     void deferRead() override
233     {
234         readingDefered = true;
235 
236         // If we're not actively reading, we need to take ownership of
237         // ourselves for a small portion of time, do that, and clear when we
238         // resume.
239         selfOwned = shared_from_this();
240     }
241 
resumeRead()242     void resumeRead() override
243     {
244         readingDefered = false;
245         doRead();
246 
247         // No longer need to keep ourselves alive now that read is active.
248         selfOwned.reset();
249     }
250 
doRead()251     void doRead()
252     {
253         if (readingDefered)
254         {
255             return;
256         }
257         ws.async_read(inBuffer, [this, self(shared_from_this())](
258                                     const boost::beast::error_code& ec,
259                                     size_t bytesRead) {
260             if (ec)
261             {
262                 if (ec != boost::beast::websocket::error::closed &&
263                     ec != boost::asio::error::eof &&
264                     ec != boost::asio::ssl::error::stream_truncated)
265                 {
266                     BMCWEB_LOG_ERROR("doRead error {}", ec);
267                 }
268                 if (closeHandler)
269                 {
270                     std::string reason{ws.reason().reason.c_str()};
271                     closeHandler(*this, reason);
272                 }
273                 return;
274             }
275 
276             handleMessage(bytesRead);
277         });
278     }
doWrite()279     void doWrite()
280     {
281         // If we're already doing a write, ignore the request, it will be picked
282         // up when the current write is complete
283         if (doingWrite)
284         {
285             return;
286         }
287 
288         if (outBuffer.size() == 0)
289         {
290             // Done for now
291             return;
292         }
293         doingWrite = true;
294         ws.async_write(outBuffer.data(), [this, self(shared_from_this())](
295                                              const boost::beast::error_code& ec,
296                                              size_t bytesSent) {
297             doingWrite = false;
298             outBuffer.consume(bytesSent);
299             if (ec == boost::beast::websocket::error::closed)
300             {
301                 // Do nothing here.  doRead handler will call the
302                 // closeHandler.
303                 close("Write error");
304                 return;
305             }
306             if (ec)
307             {
308                 BMCWEB_LOG_ERROR("Error in ws.async_write {}", ec);
309                 return;
310             }
311             doWrite();
312         });
313     }
314 
315   private:
handleMessage(size_t bytesRead)316     void handleMessage(size_t bytesRead)
317     {
318         if (messageExHandler)
319         {
320             // Note, because of the interactions with the read buffers,
321             // this message handler overrides the normal message handler
322             messageExHandler(*this, inString, MessageType::Binary,
323                              [this, self(shared_from_this()), bytesRead]() {
324                                  if (self == nullptr)
325                                  {
326                                      return;
327                                  }
328 
329                                  inBuffer.consume(bytesRead);
330                                  inString.clear();
331 
332                                  doRead();
333                              });
334             return;
335         }
336 
337         if (messageHandler)
338         {
339             messageHandler(*this, inString, ws.got_text());
340         }
341         inBuffer.consume(bytesRead);
342         inString.clear();
343         doRead();
344     }
345 
346     boost::urls::url uri;
347 
348     boost::beast::websocket::stream<Adaptor, false> ws;
349 
350     bool readingDefered = false;
351     std::string inString;
352     boost::asio::dynamic_string_buffer<std::string::value_type,
353                                        std::string::traits_type,
354                                        std::string::allocator_type>
355         inBuffer;
356 
357     boost::beast::multi_buffer outBuffer;
358     bool doingWrite = false;
359 
360     std::function<void(Connection&)> openHandler;
361     std::function<void(Connection&, const std::string&, bool)> messageHandler;
362     std::function<void(crow::websocket::Connection&, std::string_view,
363                        crow::websocket::MessageType type,
364                        std::function<void()>&& whenComplete)>
365         messageExHandler;
366     std::function<void(Connection&, const std::string&)> closeHandler;
367     std::function<void(Connection&)> errorHandler;
368     std::shared_ptr<persistent_data::UserSession> session;
369 
370     std::shared_ptr<Connection> selfOwned;
371 };
372 } // namespace websocket
373 } // namespace crow
374