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