blob: 3cb5a994fec4721992469f53dcd04befdde7f664 [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 {
Myung Baeae518792025-06-08 11:05:03 -0500250 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)
Ed Tanousbeb96b02025-02-09 09:22:46 -0800257 {
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 }
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:
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