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