1 #pragma once 2 3 #include "app.hpp" 4 #include "dbus_utility.hpp" 5 #include "privileges.hpp" 6 #include "websocket.hpp" 7 8 #include <boost/asio/local/stream_protocol.hpp> 9 #include <boost/asio/readable_pipe.hpp> 10 #include <boost/asio/writable_pipe.hpp> 11 #include <boost/asio/write.hpp> 12 #include <boost/beast/core/buffers_to_string.hpp> 13 #include <boost/beast/core/flat_static_buffer.hpp> 14 #include <boost/container/flat_map.hpp> 15 #include <boost/process/v2/process.hpp> 16 #include <boost/process/v2/stdio.hpp> 17 #include <sdbusplus/asio/property.hpp> 18 19 #include <csignal> 20 #include <string_view> 21 22 namespace crow 23 { 24 25 namespace obmc_vm 26 { 27 28 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 29 static crow::websocket::Connection* session = nullptr; 30 31 // The max network block device buffer size is 128kb plus 16bytes 32 // for the message header: 33 // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message 34 static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4; 35 36 class Handler : public std::enable_shared_from_this<Handler> 37 { 38 public: 39 Handler(const std::string& media, boost::asio::io_context& ios) : 40 pipeOut(ios), pipeIn(ios), 41 proxy(ios, "/usr/bin/nbd-proxy", {media}, 42 boost::process::v2::process_stdio{ 43 .in = pipeIn, .out = pipeOut, .err = nullptr}), 44 outputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>), 45 inputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>) 46 {} 47 48 ~Handler() = default; 49 50 Handler(const Handler&) = delete; 51 Handler(Handler&&) = delete; 52 Handler& operator=(const Handler&) = delete; 53 Handler& operator=(Handler&&) = delete; 54 55 void doClose() 56 { 57 // boost::process::child::terminate uses SIGKILL, need to send SIGTERM 58 // to allow the proxy to stop nbd-client and the USB device gadget. 59 int rc = kill(proxy.id(), SIGTERM); 60 if (rc != 0) 61 { 62 BMCWEB_LOG_ERROR("Failed to terminate nbd-proxy: {}", errno); 63 return; 64 } 65 66 proxy.wait(); 67 } 68 69 void connect() 70 { 71 std::error_code ec; 72 if (ec) 73 { 74 BMCWEB_LOG_ERROR("Couldn't connect to nbd-proxy: {}", ec.message()); 75 if (session != nullptr) 76 { 77 session->close("Error connecting to nbd-proxy"); 78 } 79 return; 80 } 81 doWrite(); 82 doRead(); 83 } 84 85 void doWrite() 86 { 87 if (doingWrite) 88 { 89 BMCWEB_LOG_DEBUG("Already writing. Bailing out"); 90 return; 91 } 92 93 if (inputBuffer->size() == 0) 94 { 95 BMCWEB_LOG_DEBUG("inputBuffer empty. Bailing out"); 96 return; 97 } 98 99 doingWrite = true; 100 pipeIn.async_write_some( 101 inputBuffer->data(), 102 [this, self(shared_from_this())](const boost::beast::error_code& ec, 103 std::size_t bytesWritten) { 104 BMCWEB_LOG_DEBUG("Wrote {}bytes", bytesWritten); 105 doingWrite = false; 106 inputBuffer->consume(bytesWritten); 107 108 if (session == nullptr) 109 { 110 return; 111 } 112 if (ec == boost::asio::error::eof) 113 { 114 session->close("VM socket port closed"); 115 return; 116 } 117 if (ec) 118 { 119 session->close("Error in writing to proxy port"); 120 BMCWEB_LOG_ERROR("Error in VM socket write {}", ec); 121 return; 122 } 123 doWrite(); 124 }); 125 } 126 127 void doRead() 128 { 129 std::size_t bytes = outputBuffer->capacity() - outputBuffer->size(); 130 131 pipeOut.async_read_some( 132 outputBuffer->prepare(bytes), 133 [this, self(shared_from_this())]( 134 const boost::system::error_code& ec, std::size_t bytesRead) { 135 BMCWEB_LOG_DEBUG("Read done. Read {} bytes", bytesRead); 136 if (ec) 137 { 138 BMCWEB_LOG_ERROR("Couldn't read from VM port: {}", ec); 139 if (session != nullptr) 140 { 141 session->close("Error in connecting to VM port"); 142 } 143 return; 144 } 145 if (session == nullptr) 146 { 147 return; 148 } 149 150 outputBuffer->commit(bytesRead); 151 std::string_view payload( 152 static_cast<const char*>(outputBuffer->data().data()), 153 bytesRead); 154 session->sendBinary(payload); 155 outputBuffer->consume(bytesRead); 156 157 doRead(); 158 }); 159 } 160 161 boost::asio::readable_pipe pipeOut; 162 boost::asio::writable_pipe pipeIn; 163 boost::process::v2::process proxy; 164 bool doingWrite{false}; 165 166 std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>> 167 outputBuffer; 168 std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>> 169 inputBuffer; 170 }; 171 172 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 173 static std::shared_ptr<Handler> handler; 174 175 } // namespace obmc_vm 176 177 namespace nbd_proxy 178 { 179 using boost::asio::local::stream_protocol; 180 181 // The max network block device buffer size is 128kb plus 16bytes 182 // for the message header: 183 // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message 184 static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4; 185 186 struct NbdProxyServer : std::enable_shared_from_this<NbdProxyServer> 187 { 188 NbdProxyServer(crow::websocket::Connection& connIn, 189 const std::string& socketIdIn, 190 const std::string& endpointIdIn, const std::string& pathIn) : 191 socketId(socketIdIn), 192 endpointId(endpointIdIn), path(pathIn), 193 194 peerSocket(connIn.getIoContext()), 195 acceptor(connIn.getIoContext(), stream_protocol::endpoint(socketId)), 196 connection(connIn) 197 {} 198 199 NbdProxyServer(const NbdProxyServer&) = delete; 200 NbdProxyServer(NbdProxyServer&&) = delete; 201 NbdProxyServer& operator=(const NbdProxyServer&) = delete; 202 NbdProxyServer& operator=(NbdProxyServer&&) = delete; 203 204 ~NbdProxyServer() 205 { 206 BMCWEB_LOG_DEBUG("NbdProxyServer destructor"); 207 208 BMCWEB_LOG_DEBUG("peerSocket->close()"); 209 boost::system::error_code ec; 210 peerSocket.close(ec); 211 212 BMCWEB_LOG_DEBUG("std::filesystem::remove({})", socketId); 213 std::error_code ec2; 214 std::filesystem::remove(socketId.c_str(), ec2); 215 if (ec2) 216 { 217 BMCWEB_LOG_DEBUG("Failed to remove file, ignoring"); 218 } 219 220 crow::connections::systemBus->async_method_call( 221 dbus::utility::logError, "xyz.openbmc_project.VirtualMedia", path, 222 "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount"); 223 } 224 225 std::string getEndpointId() const 226 { 227 return endpointId; 228 } 229 230 static void afterMount(const std::weak_ptr<NbdProxyServer>& weak, 231 const boost::system::error_code& ec, 232 bool /*isBinary*/) 233 { 234 std::shared_ptr<NbdProxyServer> self = weak.lock(); 235 if (self == nullptr) 236 { 237 return; 238 } 239 if (ec) 240 { 241 BMCWEB_LOG_ERROR("DBus error: cannot call mount method = {}", 242 ec.message()); 243 244 self->connection.close("Failed to mount media"); 245 return; 246 } 247 } 248 249 static void afterAccept(const std::weak_ptr<NbdProxyServer>& weak, 250 const boost::system::error_code& ec, 251 stream_protocol::socket socket) 252 { 253 if (ec) 254 { 255 BMCWEB_LOG_ERROR("UNIX socket: async_accept error = {}", 256 ec.message()); 257 return; 258 } 259 260 BMCWEB_LOG_DEBUG("Connection opened"); 261 std::shared_ptr<NbdProxyServer> self = weak.lock(); 262 if (self == nullptr) 263 { 264 return; 265 } 266 267 self->connection.resumeRead(); 268 self->peerSocket = std::move(socket); 269 // Start reading from socket 270 self->doRead(); 271 } 272 273 void run() 274 { 275 acceptor.async_accept( 276 std::bind_front(&NbdProxyServer::afterAccept, weak_from_this())); 277 278 crow::connections::systemBus->async_method_call( 279 [weak{weak_from_this()}](const boost::system::error_code& ec, 280 bool isBinary) { 281 afterMount(weak, ec, isBinary); 282 }, 283 "xyz.openbmc_project.VirtualMedia", path, 284 "xyz.openbmc_project.VirtualMedia.Proxy", "Mount"); 285 } 286 287 void send(std::string_view buffer, std::function<void()>&& onDone) 288 { 289 size_t copied = boost::asio::buffer_copy( 290 ws2uxBuf.prepare(buffer.size()), boost::asio::buffer(buffer)); 291 ws2uxBuf.commit(copied); 292 293 doWrite(std::move(onDone)); 294 } 295 296 private: 297 static void afterSendEx(const std::weak_ptr<NbdProxyServer>& weak) 298 { 299 std::shared_ptr<NbdProxyServer> self2 = weak.lock(); 300 if (self2 != nullptr) 301 { 302 self2->ux2wsBuf.consume(self2->ux2wsBuf.size()); 303 self2->doRead(); 304 } 305 } 306 307 inline void afterRead(const std::weak_ptr<NbdProxyServer>& weak, 308 const boost::system::error_code& ec, size_t bytesRead) 309 { 310 if (ec) 311 { 312 BMCWEB_LOG_ERROR("UNIX socket: async_read_some error = {}", 313 ec.message()); 314 return; 315 } 316 std::shared_ptr<NbdProxyServer> self = weak.lock(); 317 if (self == nullptr) 318 { 319 return; 320 } 321 322 // Send to websocket 323 self->ux2wsBuf.commit(bytesRead); 324 self->connection.sendEx( 325 crow::websocket::MessageType::Binary, 326 boost::beast::buffers_to_string(self->ux2wsBuf.data()), 327 std::bind_front(&NbdProxyServer::afterSendEx, weak_from_this())); 328 } 329 330 void doRead() 331 { 332 // Trigger async read 333 peerSocket.async_read_some(ux2wsBuf.prepare(nbdBufferSize), 334 std::bind_front(&NbdProxyServer::afterRead, 335 this, weak_from_this())); 336 } 337 338 static void afterWrite(const std::weak_ptr<NbdProxyServer>& weak, 339 std::function<void()>&& onDone, 340 const boost::system::error_code& ec, 341 size_t bytesWritten) 342 { 343 std::shared_ptr<NbdProxyServer> self = weak.lock(); 344 if (self == nullptr) 345 { 346 return; 347 } 348 349 self->ws2uxBuf.consume(bytesWritten); 350 self->uxWriteInProgress = false; 351 352 if (ec) 353 { 354 BMCWEB_LOG_ERROR("UNIX: async_write error = {}", ec.message()); 355 self->connection.close("Internal error"); 356 return; 357 } 358 359 // Retrigger doWrite if there is something in buffer 360 if (self->ws2uxBuf.size() > 0) 361 { 362 self->doWrite(std::move(onDone)); 363 return; 364 } 365 onDone(); 366 } 367 368 void doWrite(std::function<void()>&& onDone) 369 { 370 if (uxWriteInProgress) 371 { 372 BMCWEB_LOG_ERROR("Write in progress"); 373 return; 374 } 375 376 if (ws2uxBuf.size() == 0) 377 { 378 BMCWEB_LOG_ERROR("No data to write to UNIX socket"); 379 return; 380 } 381 382 uxWriteInProgress = true; 383 peerSocket.async_write_some(ws2uxBuf.data(), 384 std::bind_front(&NbdProxyServer::afterWrite, 385 weak_from_this(), 386 std::move(onDone))); 387 } 388 389 // Keeps UNIX socket endpoint file path 390 const std::string socketId; 391 const std::string endpointId; 392 const std::string path; 393 394 bool uxWriteInProgress = false; 395 396 // UNIX => WebSocket buffer 397 boost::beast::flat_static_buffer<nbdBufferSize> ux2wsBuf; 398 399 // WebSocket => UNIX buffer 400 boost::beast::flat_static_buffer<nbdBufferSize> ws2uxBuf; 401 402 // The socket used to communicate with the client. 403 stream_protocol::socket peerSocket; 404 405 // Default acceptor for UNIX socket 406 stream_protocol::acceptor acceptor; 407 408 crow::websocket::Connection& connection; 409 }; 410 411 using SessionMap = boost::container::flat_map<crow::websocket::Connection*, 412 std::shared_ptr<NbdProxyServer>>; 413 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 414 static SessionMap sessions; 415 416 inline void 417 afterGetSocket(crow::websocket::Connection& conn, 418 const sdbusplus::message::object_path& path, 419 const boost::system::error_code& ec, 420 const dbus::utility::DBusPropertiesMap& propertiesList) 421 { 422 if (ec) 423 { 424 BMCWEB_LOG_ERROR("DBus getAllProperties error: {}", ec.message()); 425 conn.close("Internal Error"); 426 return; 427 } 428 std::string endpointId; 429 std::string socket; 430 431 bool success = sdbusplus::unpackPropertiesNoThrow( 432 redfish::dbus_utils::UnpackErrorPrinter(), propertiesList, "EndpointId", 433 endpointId, "Socket", socket); 434 435 if (!success) 436 { 437 BMCWEB_LOG_ERROR("Failed to unpack properties"); 438 conn.close("Internal Error"); 439 return; 440 } 441 442 for (const auto& session : sessions) 443 { 444 if (session.second->getEndpointId() == conn.url().path()) 445 { 446 BMCWEB_LOG_ERROR("Cannot open new connection - socket is in use"); 447 conn.close("Slot is in use"); 448 return; 449 } 450 } 451 452 // If the socket file exists (i.e. after bmcweb crash), 453 // we cannot reuse it. 454 std::error_code ec2; 455 std::filesystem::remove(socket.c_str(), ec2); 456 // Ignore failures. File might not exist. 457 458 sessions[&conn] = std::make_shared<NbdProxyServer>(conn, socket, endpointId, 459 path); 460 sessions[&conn]->run(); 461 } 462 463 inline void onOpen(crow::websocket::Connection& conn) 464 { 465 BMCWEB_LOG_DEBUG("nbd-proxy.onopen({})", logPtr(&conn)); 466 467 if (conn.url().segments().size() < 2) 468 { 469 BMCWEB_LOG_ERROR("Invalid path - \"{}\"", conn.url().path()); 470 conn.close("Internal error"); 471 return; 472 } 473 474 std::string index = conn.url().segments().back(); 475 std::string path = 476 std::format("/xyz/openbmc_project/VirtualMedia/Proxy/Slot_{}", index); 477 478 sdbusplus::asio::getAllProperties( 479 *crow::connections::systemBus, "xyz.openbmc_project.VirtualMedia", path, 480 "xyz.openbmc_project.VirtualMedia.MountPoint", 481 [&conn, path](const boost::system::error_code& ec, 482 const dbus::utility::DBusPropertiesMap& propertiesList) { 483 afterGetSocket(conn, path, ec, propertiesList); 484 }); 485 486 // We need to wait for dbus and the websockets to hook up before data is 487 // sent/received. Tell the core to hold off messages until the sockets are 488 // up 489 conn.deferRead(); 490 } 491 492 inline void onClose(crow::websocket::Connection& conn, 493 const std::string& reason) 494 { 495 BMCWEB_LOG_DEBUG("nbd-proxy.onclose(reason = '{}')", reason); 496 auto session = sessions.find(&conn); 497 if (session == sessions.end()) 498 { 499 BMCWEB_LOG_DEBUG("No session to close"); 500 return; 501 } 502 // Remove reference to session in global map 503 sessions.erase(session); 504 } 505 506 inline void onMessage(crow::websocket::Connection& conn, std::string_view data, 507 crow::websocket::MessageType /*type*/, 508 std::function<void()>&& whenComplete) 509 { 510 BMCWEB_LOG_DEBUG("nbd-proxy.onMessage(len = {})", data.size()); 511 512 // Acquire proxy from sessions 513 auto session = sessions.find(&conn); 514 if (session == sessions.end() || session->second == nullptr) 515 { 516 whenComplete(); 517 return; 518 } 519 520 session->second->send(data, std::move(whenComplete)); 521 } 522 } // namespace nbd_proxy 523 524 namespace obmc_vm 525 { 526 527 inline void requestRoutes(App& app) 528 { 529 static_assert( 530 !(BMCWEB_VM_WEBSOCKET && BMCWEB_VM_NBDPROXY), 531 "nbd proxy cannot be turned on at the same time as vm websocket."); 532 533 if constexpr (BMCWEB_VM_NBDPROXY) 534 { 535 BMCWEB_ROUTE(app, "/nbd/<str>") 536 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 537 .websocket() 538 .onopen(nbd_proxy::onOpen) 539 .onclose(nbd_proxy::onClose) 540 .onmessageex(nbd_proxy::onMessage); 541 542 BMCWEB_ROUTE(app, "/vm/0/0") 543 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 544 .websocket() 545 .onopen(nbd_proxy::onOpen) 546 .onclose(nbd_proxy::onClose) 547 .onmessageex(nbd_proxy::onMessage); 548 } 549 if constexpr (BMCWEB_VM_WEBSOCKET) 550 { 551 BMCWEB_ROUTE(app, "/vm/0/0") 552 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 553 .websocket() 554 .onopen([](crow::websocket::Connection& conn) { 555 BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn)); 556 557 if (session != nullptr) 558 { 559 conn.close("Session already connected"); 560 return; 561 } 562 563 if (handler != nullptr) 564 { 565 conn.close("Handler already running"); 566 return; 567 } 568 569 session = &conn; 570 571 // media is the last digit of the endpoint /vm/0/0. A future 572 // enhancement can include supporting different endpoint values. 573 const char* media = "0"; 574 handler = std::make_shared<Handler>(media, conn.getIoContext()); 575 handler->connect(); 576 }) 577 .onclose([](crow::websocket::Connection& conn, 578 const std::string& /*reason*/) { 579 if (&conn != session) 580 { 581 return; 582 } 583 584 session = nullptr; 585 handler->doClose(); 586 handler->inputBuffer->clear(); 587 handler->outputBuffer->clear(); 588 handler.reset(); 589 }) 590 .onmessage([](crow::websocket::Connection& conn, 591 const std::string& data, bool) { 592 if (data.length() > 593 handler->inputBuffer->capacity() - handler->inputBuffer->size()) 594 { 595 BMCWEB_LOG_ERROR("Buffer overrun when writing {} bytes", 596 data.length()); 597 conn.close("Buffer overrun"); 598 return; 599 } 600 601 size_t copied = boost::asio::buffer_copy( 602 handler->inputBuffer->prepare(data.size()), 603 boost::asio::buffer(data)); 604 handler->inputBuffer->commit(copied); 605 handler->doWrite(); 606 }); 607 } 608 } 609 610 } // namespace obmc_vm 611 612 } // namespace crow 613