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