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