xref: /openbmc/bmcweb/http/websocket_impl.hpp (revision beb96b0b7b1c4d9630dc329d86db119ed3ead086)
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::websocket::error::closed &&
251                     ec != boost::asio::error::eof &&
252                     ec != boost::asio::ssl::error::stream_truncated)
253                 {
254                     BMCWEB_LOG_ERROR("doRead error {}", ec);
255                 }
256                 if (closeHandler)
257                 {
258                     std::string reason{ws.reason().reason.c_str()};
259                     closeHandler(*this, reason);
260                 }
261                 return;
262             }
263 
264             handleMessage(bytesRead);
265         });
266     }
doWrite()267     void doWrite()
268     {
269         // If we're already doing a write, ignore the request, it will be picked
270         // up when the current write is complete
271         if (doingWrite)
272         {
273             return;
274         }
275 
276         if (outBuffer.size() == 0)
277         {
278             // Done for now
279             return;
280         }
281         doingWrite = true;
282         ws.async_write(outBuffer.data(), [this, self(shared_from_this())](
283                                              const boost::beast::error_code& ec,
284                                              size_t bytesSent) {
285             doingWrite = false;
286             outBuffer.consume(bytesSent);
287             if (ec == boost::beast::websocket::error::closed)
288             {
289                 // Do nothing here.  doRead handler will call the
290                 // closeHandler.
291                 close("Write error");
292                 return;
293             }
294             if (ec)
295             {
296                 BMCWEB_LOG_ERROR("Error in ws.async_write {}", ec);
297                 return;
298             }
299             doWrite();
300         });
301     }
302 
303   private:
handleMessage(size_t bytesRead)304     void handleMessage(size_t bytesRead)
305     {
306         if (messageExHandler)
307         {
308             // Note, because of the interactions with the read buffers,
309             // this message handler overrides the normal message handler
310             messageExHandler(*this, inString, MessageType::Binary,
311                              [this, self(shared_from_this()), bytesRead]() {
312                                  if (self == nullptr)
313                                  {
314                                      return;
315                                  }
316 
317                                  inBuffer.consume(bytesRead);
318                                  inString.clear();
319 
320                                  doRead();
321                              });
322             return;
323         }
324 
325         if (messageHandler)
326         {
327             messageHandler(*this, inString, ws.got_text());
328         }
329         inBuffer.consume(bytesRead);
330         inString.clear();
331         doRead();
332     }
333 
334     boost::urls::url uri;
335 
336     boost::beast::websocket::stream<Adaptor, false> ws;
337 
338     bool readingDefered = false;
339     std::string inString;
340     boost::asio::dynamic_string_buffer<std::string::value_type,
341                                        std::string::traits_type,
342                                        std::string::allocator_type>
343         inBuffer;
344 
345     boost::beast::multi_buffer outBuffer;
346     bool doingWrite = false;
347 
348     std::function<void(Connection&)> openHandler;
349     std::function<void(Connection&, const std::string&, bool)> messageHandler;
350     std::function<void(crow::websocket::Connection&, std::string_view,
351                        crow::websocket::MessageType type,
352                        std::function<void()>&& whenComplete)>
353         messageExHandler;
354     std::function<void(Connection&, const std::string&)> closeHandler;
355     std::function<void(Connection&)> errorHandler;
356     std::shared_ptr<persistent_data::UserSession> session;
357 
358     std::shared_ptr<Connection> selfOwned;
359 };
360 } // namespace websocket
361 } // namespace crow
362