blob: 1812f0ff437f18211a289a4265e9a4ee7956a746 [file] [log] [blame]
Ed Tanousbeb96b02025-02-09 09:22:46 -08001// 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
39namespace crow
40{
41namespace websocket
42{
43
44template <typename Adaptor>
45class ConnectionImpl : public Connection
46{
47 using self_t = ConnectionImpl<Adaptor>;
48
49 public:
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
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
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
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
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
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
196 boost::urls::url_view url() override
197 {
198 return uri;
199 }
200
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
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
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
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 }
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:
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