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