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