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:
Handler(const std::string & media,boost::asio::io_context & ios)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
doClose()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
connect()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
doWrite()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
doRead()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 {
NbdProxyServercrow::nbd_proxy::NbdProxyServer184 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
~NbdProxyServercrow::nbd_proxy::NbdProxyServer199 ~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
getEndpointIdcrow::nbd_proxy::NbdProxyServer220 std::string getEndpointId() const
221 {
222 return endpointId;
223 }
224
afterMountcrow::nbd_proxy::NbdProxyServer225 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
afterAcceptcrow::nbd_proxy::NbdProxyServer244 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
runcrow::nbd_proxy::NbdProxyServer268 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
sendcrow::nbd_proxy::NbdProxyServer282 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:
afterSendExcrow::nbd_proxy::NbdProxyServer292 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
afterReadcrow::nbd_proxy::NbdProxyServer302 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
doReadcrow::nbd_proxy::NbdProxyServer325 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
afterWritecrow::nbd_proxy::NbdProxyServer333 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
doWritecrow::nbd_proxy::NbdProxyServer363 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
afterGetSocket(crow::websocket::Connection & conn,const sdbusplus::message::object_path & path,const boost::system::error_code & ec,const dbus::utility::DBusPropertiesMap & propertiesList)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
onOpen(crow::websocket::Connection & conn)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
onClose(crow::websocket::Connection & conn,const std::string & reason)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
onMessage(crow::websocket::Connection & conn,std::string_view data,crow::websocket::MessageType,std::function<void ()> && whenComplete)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
requestRoutes(App & app)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