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