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