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