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