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), endpointId(endpointIdIn), path(pathIn), 192 193 peerSocket(connIn.getIoContext()), 194 acceptor(connIn.getIoContext(), stream_protocol::endpoint(socketId)), 195 connection(connIn) 196 {} 197 198 NbdProxyServer(const NbdProxyServer&) = delete; 199 NbdProxyServer(NbdProxyServer&&) = delete; 200 NbdProxyServer& operator=(const NbdProxyServer&) = delete; 201 NbdProxyServer& operator=(NbdProxyServer&&) = delete; 202 203 ~NbdProxyServer() 204 { 205 BMCWEB_LOG_DEBUG("NbdProxyServer destructor"); 206 207 BMCWEB_LOG_DEBUG("peerSocket->close()"); 208 boost::system::error_code ec; 209 peerSocket.close(ec); 210 211 BMCWEB_LOG_DEBUG("std::filesystem::remove({})", socketId); 212 std::error_code ec2; 213 std::filesystem::remove(socketId.c_str(), ec2); 214 if (ec2) 215 { 216 BMCWEB_LOG_DEBUG("Failed to remove file, ignoring"); 217 } 218 219 crow::connections::systemBus->async_method_call( 220 dbus::utility::logError, "xyz.openbmc_project.VirtualMedia", path, 221 "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount"); 222 } 223 224 std::string getEndpointId() const 225 { 226 return endpointId; 227 } 228 229 static void afterMount(const std::weak_ptr<NbdProxyServer>& weak, 230 const boost::system::error_code& ec, 231 bool /*isBinary*/) 232 { 233 std::shared_ptr<NbdProxyServer> self = weak.lock(); 234 if (self == nullptr) 235 { 236 return; 237 } 238 if (ec) 239 { 240 BMCWEB_LOG_ERROR("DBus error: cannot call mount method = {}", 241 ec.message()); 242 243 self->connection.close("Failed to mount media"); 244 return; 245 } 246 } 247 248 static void afterAccept(const std::weak_ptr<NbdProxyServer>& weak, 249 const boost::system::error_code& ec, 250 stream_protocol::socket socket) 251 { 252 if (ec) 253 { 254 BMCWEB_LOG_ERROR("UNIX socket: async_accept error = {}", 255 ec.message()); 256 return; 257 } 258 259 BMCWEB_LOG_DEBUG("Connection opened"); 260 std::shared_ptr<NbdProxyServer> self = weak.lock(); 261 if (self == nullptr) 262 { 263 return; 264 } 265 266 self->connection.resumeRead(); 267 self->peerSocket = std::move(socket); 268 // Start reading from socket 269 self->doRead(); 270 } 271 272 void run() 273 { 274 acceptor.async_accept( 275 std::bind_front(&NbdProxyServer::afterAccept, weak_from_this())); 276 277 crow::connections::systemBus->async_method_call( 278 [weak{weak_from_this()}](const boost::system::error_code& ec, 279 bool isBinary) { 280 afterMount(weak, ec, isBinary); 281 }, 282 "xyz.openbmc_project.VirtualMedia", path, 283 "xyz.openbmc_project.VirtualMedia.Proxy", "Mount"); 284 } 285 286 void send(std::string_view buffer, std::function<void()>&& onDone) 287 { 288 size_t copied = boost::asio::buffer_copy( 289 ws2uxBuf.prepare(buffer.size()), boost::asio::buffer(buffer)); 290 ws2uxBuf.commit(copied); 291 292 doWrite(std::move(onDone)); 293 } 294 295 private: 296 static void afterSendEx(const std::weak_ptr<NbdProxyServer>& weak) 297 { 298 std::shared_ptr<NbdProxyServer> self2 = weak.lock(); 299 if (self2 != nullptr) 300 { 301 self2->ux2wsBuf.consume(self2->ux2wsBuf.size()); 302 self2->doRead(); 303 } 304 } 305 306 void afterRead(const std::weak_ptr<NbdProxyServer>& weak, 307 const boost::system::error_code& ec, size_t bytesRead) 308 { 309 if (ec) 310 { 311 BMCWEB_LOG_ERROR("UNIX socket: async_read_some error = {}", 312 ec.message()); 313 return; 314 } 315 std::shared_ptr<NbdProxyServer> self = weak.lock(); 316 if (self == nullptr) 317 { 318 return; 319 } 320 321 // Send to websocket 322 self->ux2wsBuf.commit(bytesRead); 323 self->connection.sendEx( 324 crow::websocket::MessageType::Binary, 325 boost::beast::buffers_to_string(self->ux2wsBuf.data()), 326 std::bind_front(&NbdProxyServer::afterSendEx, weak_from_this())); 327 } 328 329 void doRead() 330 { 331 // Trigger async read 332 peerSocket.async_read_some(ux2wsBuf.prepare(nbdBufferSize), 333 std::bind_front(&NbdProxyServer::afterRead, 334 this, weak_from_this())); 335 } 336 337 static void afterWrite(const std::weak_ptr<NbdProxyServer>& weak, 338 std::function<void()>&& onDone, 339 const boost::system::error_code& ec, 340 size_t bytesWritten) 341 { 342 std::shared_ptr<NbdProxyServer> self = weak.lock(); 343 if (self == nullptr) 344 { 345 return; 346 } 347 348 self->ws2uxBuf.consume(bytesWritten); 349 self->uxWriteInProgress = false; 350 351 if (ec) 352 { 353 BMCWEB_LOG_ERROR("UNIX: async_write error = {}", ec.message()); 354 self->connection.close("Internal error"); 355 return; 356 } 357 358 // Retrigger doWrite if there is something in buffer 359 if (self->ws2uxBuf.size() > 0) 360 { 361 self->doWrite(std::move(onDone)); 362 return; 363 } 364 onDone(); 365 } 366 367 void doWrite(std::function<void()>&& onDone) 368 { 369 if (uxWriteInProgress) 370 { 371 BMCWEB_LOG_ERROR("Write in progress"); 372 return; 373 } 374 375 if (ws2uxBuf.size() == 0) 376 { 377 BMCWEB_LOG_ERROR("No data to write to UNIX socket"); 378 return; 379 } 380 381 uxWriteInProgress = true; 382 peerSocket.async_write_some( 383 ws2uxBuf.data(), 384 std::bind_front(&NbdProxyServer::afterWrite, weak_from_this(), 385 std::move(onDone))); 386 } 387 388 // Keeps UNIX socket endpoint file path 389 const std::string socketId; 390 const std::string endpointId; 391 const std::string path; 392 393 bool uxWriteInProgress = false; 394 395 // UNIX => WebSocket buffer 396 boost::beast::flat_static_buffer<nbdBufferSize> ux2wsBuf; 397 398 // WebSocket => UNIX buffer 399 boost::beast::flat_static_buffer<nbdBufferSize> ws2uxBuf; 400 401 // The socket used to communicate with the client. 402 stream_protocol::socket peerSocket; 403 404 // Default acceptor for UNIX socket 405 stream_protocol::acceptor acceptor; 406 407 crow::websocket::Connection& connection; 408 }; 409 410 using SessionMap = boost::container::flat_map<crow::websocket::Connection*, 411 std::shared_ptr<NbdProxyServer>>; 412 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 413 static SessionMap sessions; 414 415 inline void 416 afterGetSocket(crow::websocket::Connection& conn, 417 const sdbusplus::message::object_path& path, 418 const boost::system::error_code& ec, 419 const dbus::utility::DBusPropertiesMap& propertiesList) 420 { 421 if (ec) 422 { 423 BMCWEB_LOG_ERROR("DBus getAllProperties error: {}", ec.message()); 424 conn.close("Internal Error"); 425 return; 426 } 427 std::string endpointId; 428 std::string socket; 429 430 bool success = sdbusplus::unpackPropertiesNoThrow( 431 redfish::dbus_utils::UnpackErrorPrinter(), propertiesList, "EndpointId", 432 endpointId, "Socket", socket); 433 434 if (!success) 435 { 436 BMCWEB_LOG_ERROR("Failed to unpack properties"); 437 conn.close("Internal Error"); 438 return; 439 } 440 441 for (const auto& session : sessions) 442 { 443 if (session.second->getEndpointId() == conn.url().path()) 444 { 445 BMCWEB_LOG_ERROR("Cannot open new connection - socket is in use"); 446 conn.close("Slot is in use"); 447 return; 448 } 449 } 450 451 // If the socket file exists (i.e. after bmcweb crash), 452 // we cannot reuse it. 453 std::error_code ec2; 454 std::filesystem::remove(socket.c_str(), ec2); 455 // Ignore failures. File might not exist. 456 457 sessions[&conn] = 458 std::make_shared<NbdProxyServer>(conn, socket, endpointId, path); 459 sessions[&conn]->run(); 460 } 461 462 inline void onOpen(crow::websocket::Connection& conn) 463 { 464 BMCWEB_LOG_DEBUG("nbd-proxy.onopen({})", logPtr(&conn)); 465 466 if (conn.url().segments().size() < 2) 467 { 468 BMCWEB_LOG_ERROR("Invalid path - \"{}\"", conn.url().path()); 469 conn.close("Internal error"); 470 return; 471 } 472 473 std::string index = conn.url().segments().back(); 474 std::string path = 475 std::format("/xyz/openbmc_project/VirtualMedia/Proxy/Slot_{}", index); 476 477 dbus::utility::getAllProperties( 478 "xyz.openbmc_project.VirtualMedia", path, 479 "xyz.openbmc_project.VirtualMedia.MountPoint", 480 [&conn, path](const boost::system::error_code& ec, 481 const dbus::utility::DBusPropertiesMap& propertiesList) { 482 afterGetSocket(conn, path, ec, propertiesList); 483 }); 484 485 // We need to wait for dbus and the websockets to hook up before data is 486 // sent/received. Tell the core to hold off messages until the sockets are 487 // up 488 conn.deferRead(); 489 } 490 491 inline void onClose(crow::websocket::Connection& conn, 492 const std::string& reason) 493 { 494 BMCWEB_LOG_DEBUG("nbd-proxy.onclose(reason = '{}')", reason); 495 auto session = sessions.find(&conn); 496 if (session == sessions.end()) 497 { 498 BMCWEB_LOG_DEBUG("No session to close"); 499 return; 500 } 501 // Remove reference to session in global map 502 sessions.erase(session); 503 } 504 505 inline void onMessage(crow::websocket::Connection& conn, std::string_view data, 506 crow::websocket::MessageType /*type*/, 507 std::function<void()>&& whenComplete) 508 { 509 BMCWEB_LOG_DEBUG("nbd-proxy.onMessage(len = {})", data.size()); 510 511 // Acquire proxy from sessions 512 auto session = sessions.find(&conn); 513 if (session == sessions.end() || session->second == nullptr) 514 { 515 whenComplete(); 516 return; 517 } 518 519 session->second->send(data, std::move(whenComplete)); 520 } 521 } // namespace nbd_proxy 522 523 namespace obmc_vm 524 { 525 526 inline void requestRoutes(App& app) 527 { 528 static_assert( 529 !(BMCWEB_VM_WEBSOCKET && BMCWEB_VM_NBDPROXY), 530 "nbd proxy cannot be turned on at the same time as vm websocket."); 531 532 if constexpr (BMCWEB_VM_NBDPROXY) 533 { 534 BMCWEB_ROUTE(app, "/nbd/<str>") 535 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 536 .websocket() 537 .onopen(nbd_proxy::onOpen) 538 .onclose(nbd_proxy::onClose) 539 .onmessageex(nbd_proxy::onMessage); 540 541 BMCWEB_ROUTE(app, "/vm/0/0") 542 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 543 .websocket() 544 .onopen(nbd_proxy::onOpen) 545 .onclose(nbd_proxy::onClose) 546 .onmessageex(nbd_proxy::onMessage); 547 } 548 if constexpr (BMCWEB_VM_WEBSOCKET) 549 { 550 BMCWEB_ROUTE(app, "/vm/0/0") 551 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 552 .websocket() 553 .onopen([](crow::websocket::Connection& conn) { 554 BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn)); 555 556 if (session != nullptr) 557 { 558 conn.close("Session already connected"); 559 return; 560 } 561 562 if (handler != nullptr) 563 { 564 conn.close("Handler already running"); 565 return; 566 } 567 568 session = &conn; 569 570 // media is the last digit of the endpoint /vm/0/0. A future 571 // enhancement can include supporting different endpoint values. 572 const char* media = "0"; 573 handler = std::make_shared<Handler>(media, conn.getIoContext()); 574 handler->connect(); 575 }) 576 .onclose([](crow::websocket::Connection& conn, 577 const std::string& /*reason*/) { 578 if (&conn != session) 579 { 580 return; 581 } 582 583 session = nullptr; 584 handler->doClose(); 585 handler->inputBuffer->clear(); 586 handler->outputBuffer->clear(); 587 handler.reset(); 588 }) 589 .onmessage([](crow::websocket::Connection& conn, 590 const std::string& data, bool) { 591 if (data.length() > handler->inputBuffer->capacity() - 592 handler->inputBuffer->size()) 593 { 594 BMCWEB_LOG_ERROR("Buffer overrun when writing {} bytes", 595 data.length()); 596 conn.close("Buffer overrun"); 597 return; 598 } 599 600 size_t copied = boost::asio::buffer_copy( 601 handler->inputBuffer->prepare(data.size()), 602 boost::asio::buffer(data)); 603 handler->inputBuffer->commit(copied); 604 handler->doWrite(); 605 }); 606 } 607 } 608 609 } // namespace obmc_vm 610 611 } // namespace crow 612