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:
Handler(const std::string & media,boost::asio::io_context & ios)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
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 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 {
NbdProxyServercrow::nbd_proxy::NbdProxyServer186 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
~NbdProxyServercrow::nbd_proxy::NbdProxyServer201 ~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
getEndpointIdcrow::nbd_proxy::NbdProxyServer222 std::string getEndpointId() const
223 {
224 return endpointId;
225 }
226
afterMountcrow::nbd_proxy::NbdProxyServer227 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
afterAcceptcrow::nbd_proxy::NbdProxyServer246 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
runcrow::nbd_proxy::NbdProxyServer270 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
sendcrow::nbd_proxy::NbdProxyServer284 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:
afterSendExcrow::nbd_proxy::NbdProxyServer294 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
afterReadcrow::nbd_proxy::NbdProxyServer304 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
doReadcrow::nbd_proxy::NbdProxyServer327 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
afterWritecrow::nbd_proxy::NbdProxyServer335 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
doWritecrow::nbd_proxy::NbdProxyServer365 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
afterGetSocket(crow::websocket::Connection & conn,const sdbusplus::message::object_path & path,const boost::system::error_code & ec,const dbus::utility::DBusPropertiesMap & propertiesList)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
onOpen(crow::websocket::Connection & conn)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
onClose(crow::websocket::Connection & conn,const std::string & reason)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
onMessage(crow::websocket::Connection & conn,std::string_view data,crow::websocket::MessageType,std::function<void ()> && whenComplete)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
requestRoutes(App & app)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