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_singleton.hpp"
9 #include "dbus_utility.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:
Handler(const std::string & media,boost::asio::io_context & ios)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
doClose()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
connect()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
doWrite()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
doRead()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 {
NbdProxyServercrow::nbd_proxy::NbdProxyServer203 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(connIn.getIoContext()),
209 acceptor(connIn.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
~NbdProxyServercrow::nbd_proxy::NbdProxyServer218 ~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 crow::connections::systemBus->async_method_call(
235 dbus::utility::logError, "xyz.openbmc_project.VirtualMedia", path,
236 "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount");
237 }
238
getEndpointIdcrow::nbd_proxy::NbdProxyServer239 std::string getEndpointId() const
240 {
241 return endpointId;
242 }
243
afterMountcrow::nbd_proxy::NbdProxyServer244 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
afterAcceptcrow::nbd_proxy::NbdProxyServer263 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
runcrow::nbd_proxy::NbdProxyServer287 void run()
288 {
289 acceptor.async_accept(
290 std::bind_front(&NbdProxyServer::afterAccept, weak_from_this()));
291
292 crow::connections::systemBus->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
sendcrow::nbd_proxy::NbdProxyServer301 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:
afterSendExcrow::nbd_proxy::NbdProxyServer311 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
afterReadcrow::nbd_proxy::NbdProxyServer321 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
doReadcrow::nbd_proxy::NbdProxyServer344 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
afterWritecrow::nbd_proxy::NbdProxyServer352 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
doWritecrow::nbd_proxy::NbdProxyServer382 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
afterGetSocket(crow::websocket::Connection & conn,const sdbusplus::message::object_path & path,const boost::system::error_code & ec,const dbus::utility::DBusPropertiesMap & propertiesList)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
onOpen(crow::websocket::Connection & conn)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
onClose(crow::websocket::Connection & conn,const std::string & reason)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
onMessage(crow::websocket::Connection & conn,std::string_view data,crow::websocket::MessageType,std::function<void ()> && whenComplete)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
requestRoutes(App & app)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, conn.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