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