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