xref: /openbmc/bmcweb/http/websocket.hpp (revision 3ccb3adb)
1 #pragma once
2 #include "async_resp.hpp"
3 #include "http_request.hpp"
4 
5 #include <boost/asio/buffer.hpp>
6 #include <boost/beast/websocket.hpp>
7 
8 #include <array>
9 #include <functional>
10 
11 #ifdef BMCWEB_ENABLE_SSL
12 #include <boost/beast/websocket/ssl.hpp>
13 #endif
14 
15 namespace crow
16 {
17 namespace websocket
18 {
19 
20 struct Connection : std::enable_shared_from_this<Connection>
21 {
22   public:
23     explicit Connection(const crow::Request& reqIn) :
24         req(reqIn.req), userdataPtr(nullptr)
25     {}
26 
27     explicit Connection(const crow::Request& reqIn, std::string user) :
28         req(reqIn.req), userName{std::move(user)}, userdataPtr(nullptr)
29     {}
30 
31     Connection(const Connection&) = delete;
32     Connection(Connection&&) = delete;
33     Connection& operator=(const Connection&) = delete;
34     Connection& operator=(const Connection&&) = delete;
35 
36     virtual void sendBinary(std::string_view msg) = 0;
37     virtual void sendBinary(std::string&& msg) = 0;
38     virtual void sendText(std::string_view msg) = 0;
39     virtual void sendText(std::string&& msg) = 0;
40     virtual void close(std::string_view msg = "quit") = 0;
41     virtual boost::asio::io_context& getIoContext() = 0;
42     virtual ~Connection() = default;
43 
44     void userdata(void* u)
45     {
46         userdataPtr = u;
47     }
48     void* userdata()
49     {
50         return userdataPtr;
51     }
52 
53     const std::string& getUserName() const
54     {
55         return userName;
56     }
57 
58     boost::beast::http::request<boost::beast::http::string_body> req;
59     crow::Response res;
60 
61   private:
62     std::string userName{};
63     void* userdataPtr;
64 };
65 
66 template <typename Adaptor>
67 class ConnectionImpl : public Connection
68 {
69   public:
70     ConnectionImpl(
71         const crow::Request& reqIn, Adaptor adaptorIn,
72         std::function<void(Connection&)> openHandlerIn,
73         std::function<void(Connection&, const std::string&, bool)>
74             messageHandlerIn,
75         std::function<void(Connection&, const std::string&)> closeHandlerIn,
76         std::function<void(Connection&)> errorHandlerIn) :
77         Connection(reqIn, reqIn.session == nullptr ? std::string{}
78                                                    : reqIn.session->username),
79         ws(std::move(adaptorIn)), inBuffer(inString, 131088),
80         openHandler(std::move(openHandlerIn)),
81         messageHandler(std::move(messageHandlerIn)),
82         closeHandler(std::move(closeHandlerIn)),
83         errorHandler(std::move(errorHandlerIn)), session(reqIn.session)
84     {
85         /* Turn on the timeouts on websocket stream to server role */
86         ws.set_option(boost::beast::websocket::stream_base::timeout::suggested(
87             boost::beast::role_type::server));
88         BMCWEB_LOG_DEBUG << "Creating new connection " << this;
89     }
90 
91     boost::asio::io_context& getIoContext() override
92     {
93         return static_cast<boost::asio::io_context&>(
94             ws.get_executor().context());
95     }
96 
97     void start()
98     {
99         BMCWEB_LOG_DEBUG << "starting connection " << this;
100 
101         using bf = boost::beast::http::field;
102 
103         std::string_view protocol = req[bf::sec_websocket_protocol];
104 
105         ws.set_option(boost::beast::websocket::stream_base::decorator(
106             [session{session}, protocol{std::string(protocol)}](
107                 boost::beast::websocket::response_type& m) {
108 
109 #ifndef BMCWEB_INSECURE_DISABLE_CSRF_PREVENTION
110             if (session != nullptr)
111             {
112                 // use protocol for csrf checking
113                 if (session->cookieAuth &&
114                     !crow::utility::constantTimeStringCompare(
115                         protocol, session->csrfToken))
116                 {
117                     BMCWEB_LOG_ERROR << "Websocket CSRF error";
118                     m.result(boost::beast::http::status::unauthorized);
119                     return;
120                 }
121             }
122 #endif
123             if (!protocol.empty())
124             {
125                 m.insert(bf::sec_websocket_protocol, protocol);
126             }
127 
128             m.insert(bf::strict_transport_security, "max-age=31536000; "
129                                                     "includeSubdomains; "
130                                                     "preload");
131             m.insert(bf::pragma, "no-cache");
132             m.insert(bf::cache_control, "no-Store,no-Cache");
133             m.insert("Content-Security-Policy", "default-src 'self'");
134             m.insert("X-XSS-Protection", "1; "
135                                          "mode=block");
136             m.insert("X-Content-Type-Options", "nosniff");
137         }));
138 
139         // Perform the websocket upgrade
140         ws.async_accept(req, [this, self(shared_from_this())](
141                                  boost::system::error_code ec) {
142             if (ec)
143             {
144                 BMCWEB_LOG_ERROR << "Error in ws.async_accept " << ec;
145                 return;
146             }
147             acceptDone();
148         });
149     }
150 
151     void sendBinary(const std::string_view msg) override
152     {
153         ws.binary(true);
154         outBuffer.emplace_back(msg);
155         doWrite();
156     }
157 
158     void sendBinary(std::string&& msg) override
159     {
160         ws.binary(true);
161         outBuffer.emplace_back(std::move(msg));
162         doWrite();
163     }
164 
165     void sendText(const std::string_view msg) override
166     {
167         ws.text(true);
168         outBuffer.emplace_back(msg);
169         doWrite();
170     }
171 
172     void sendText(std::string&& msg) override
173     {
174         ws.text(true);
175         outBuffer.emplace_back(std::move(msg));
176         doWrite();
177     }
178 
179     void close(const std::string_view msg) override
180     {
181         ws.async_close(
182             {boost::beast::websocket::close_code::normal, msg},
183             [self(shared_from_this())](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 
196     void acceptDone()
197     {
198         BMCWEB_LOG_DEBUG << "Websocket accepted connection";
199 
200         doRead();
201 
202         if (openHandler)
203         {
204             openHandler(*this);
205         }
206     }
207 
208     void doRead()
209     {
210         ws.async_read(inBuffer,
211                       [this, self(shared_from_this())](
212                           boost::beast::error_code ec, std::size_t bytesRead) {
213             if (ec)
214             {
215                 if (ec != boost::beast::websocket::error::closed)
216                 {
217                     BMCWEB_LOG_ERROR << "doRead error " << ec;
218                 }
219                 if (closeHandler)
220                 {
221                     std::string reason{ws.reason().reason.c_str()};
222                     closeHandler(*this, reason);
223                 }
224                 return;
225             }
226             if (messageHandler)
227             {
228                 messageHandler(*this, inString, ws.got_text());
229             }
230             inBuffer.consume(bytesRead);
231             inString.clear();
232             doRead();
233         });
234     }
235 
236     void doWrite()
237     {
238         // If we're already doing a write, ignore the request, it will be picked
239         // up when the current write is complete
240         if (doingWrite)
241         {
242             return;
243         }
244 
245         if (outBuffer.empty())
246         {
247             // Done for now
248             return;
249         }
250         doingWrite = true;
251         ws.async_write(boost::asio::buffer(outBuffer.front()),
252                        [this, self(shared_from_this())](
253                            boost::beast::error_code ec, std::size_t) {
254             doingWrite = false;
255             outBuffer.erase(outBuffer.begin());
256             if (ec == boost::beast::websocket::error::closed)
257             {
258                 // Do nothing here.  doRead handler will call the
259                 // closeHandler.
260                 close("Write error");
261                 return;
262             }
263             if (ec)
264             {
265                 BMCWEB_LOG_ERROR << "Error in ws.async_write " << ec;
266                 return;
267             }
268             doWrite();
269         });
270     }
271 
272   private:
273     boost::beast::websocket::stream<Adaptor, false> ws;
274 
275     std::string inString;
276     boost::asio::dynamic_string_buffer<std::string::value_type,
277                                        std::string::traits_type,
278                                        std::string::allocator_type>
279         inBuffer;
280     std::vector<std::string> outBuffer;
281     bool doingWrite = false;
282 
283     std::function<void(Connection&)> openHandler;
284     std::function<void(Connection&, const std::string&, bool)> messageHandler;
285     std::function<void(Connection&, const std::string&)> closeHandler;
286     std::function<void(Connection&)> errorHandler;
287     std::shared_ptr<persistent_data::UserSession> session;
288 };
289 } // namespace websocket
290 } // namespace crow
291