xref: /openbmc/bmcweb/include/vm_websocket.hpp (revision cb103130)
1 #pragma once
2 
3 #include <crow/app.h>
4 #include <crow/websocket.h>
5 #include <signal.h>
6 
7 #include <boost/beast/core/flat_static_buffer.hpp>
8 #include <boost/process.hpp>
9 #include <webserver_common.hpp>
10 
11 namespace crow
12 {
13 namespace obmc_vm
14 {
15 
16 static crow::websocket::Connection* session = nullptr;
17 
18 // The max network block device buffer size is 128kb plus 16bytes
19 // for the message header:
20 // https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message
21 static constexpr auto nbdBufferSize = 131088;
22 
23 class Handler : public std::enable_shared_from_this<Handler>
24 {
25   public:
26     Handler(const std::string& mediaIn, boost::asio::io_context& ios) :
27         pipeOut(ios), pipeIn(ios), media(mediaIn), doingWrite(false),
28         outputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>),
29         inputBuffer(new boost::beast::flat_static_buffer<nbdBufferSize>)
30     {
31     }
32 
33     ~Handler()
34     {
35     }
36 
37     void doClose()
38     {
39         // boost::process::child::terminate uses SIGKILL, need to send SIGTERM
40         // to allow the proxy to stop nbd-client and the USB device gadget.
41         int rc = kill(proxy.id(), SIGTERM);
42         if (rc)
43         {
44             return;
45         }
46         proxy.wait();
47     }
48 
49     void connect()
50     {
51         std::error_code ec;
52         proxy = boost::process::child("/usr/sbin/nbd-proxy", media,
53                                       boost::process::std_out > pipeOut,
54                                       boost::process::std_in < pipeIn, ec);
55         if (ec)
56         {
57             BMCWEB_LOG_ERROR << "Couldn't connect to nbd-proxy: "
58                              << ec.message();
59             if (session != nullptr)
60             {
61                 session->close("Error connecting to nbd-proxy");
62             }
63             return;
64         }
65         doWrite();
66         doRead();
67     }
68 
69     void doWrite()
70     {
71         if (doingWrite)
72         {
73             BMCWEB_LOG_DEBUG << "Already writing.  Bailing out";
74             return;
75         }
76 
77         if (inputBuffer->size() == 0)
78         {
79             BMCWEB_LOG_DEBUG << "inputBuffer empty.  Bailing out";
80             return;
81         }
82 
83         doingWrite = true;
84         pipeIn.async_write_some(
85             inputBuffer->data(),
86             [this, self(shared_from_this())](boost::beast::error_code ec,
87                                              std::size_t bytesWritten) {
88                 BMCWEB_LOG_DEBUG << "Wrote " << bytesWritten << "bytes";
89                 doingWrite = false;
90                 inputBuffer->consume(bytesWritten);
91 
92                 if (session == nullptr)
93                 {
94                     return;
95                 }
96                 if (ec == boost::asio::error::eof)
97                 {
98                     session->close("VM socket port closed");
99                     return;
100                 }
101                 if (ec)
102                 {
103                     session->close("Error in writing to proxy port");
104                     BMCWEB_LOG_ERROR << "Error in VM socket write " << ec;
105                     return;
106                 }
107                 doWrite();
108             });
109     }
110 
111     void doRead()
112     {
113         std::size_t bytes = outputBuffer->capacity() - outputBuffer->size();
114 
115         pipeOut.async_read_some(
116             outputBuffer->prepare(bytes),
117             [this, self(shared_from_this())](
118                 const boost::system::error_code& ec, std::size_t bytesRead) {
119                 BMCWEB_LOG_DEBUG << "Read done.  Read " << bytesRead
120                                  << " bytes";
121                 if (ec)
122                 {
123                     BMCWEB_LOG_ERROR << "Couldn't read from VM port: " << ec;
124                     if (session != nullptr)
125                     {
126                         session->close("Error in connecting to VM port");
127                     }
128                     return;
129                 }
130                 if (session == nullptr)
131                 {
132                     return;
133                 }
134 
135                 outputBuffer->commit(bytesRead);
136                 std::string_view payload(
137                     static_cast<const char*>(outputBuffer->data().data()),
138                     bytesRead);
139                 session->sendBinary(payload);
140                 outputBuffer->consume(bytesRead);
141 
142                 doRead();
143             });
144     }
145 
146     boost::process::async_pipe pipeOut;
147     boost::process::async_pipe pipeIn;
148     boost::process::child proxy;
149     std::string media;
150     bool doingWrite;
151 
152     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
153         outputBuffer;
154     std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>>
155         inputBuffer;
156 };
157 
158 static std::shared_ptr<Handler> handler;
159 
160 template <typename... Middlewares> void requestRoutes(Crow<Middlewares...>& app)
161 {
162     BMCWEB_ROUTE(app, "/vm/0/0")
163         .websocket()
164         .onopen([](crow::websocket::Connection& conn) {
165             BMCWEB_LOG_DEBUG << "Connection " << &conn << " opened";
166 
167             if (session != nullptr)
168             {
169                 conn.close("Session already connected");
170                 return;
171             }
172 
173             if (handler != nullptr)
174             {
175                 conn.close("Handler already running");
176                 return;
177             }
178 
179             session = &conn;
180 
181             // media is the last digit of the endpoint /vm/0/0. A future
182             // enhancement can include supporting different endpoint values.
183             const char* media = "0";
184             handler = std::make_shared<Handler>(media, conn.get_io_context());
185             handler->connect();
186         })
187         .onclose(
188             [](crow::websocket::Connection& conn, const std::string& reason) {
189                 session = nullptr;
190                 handler->doClose();
191 #if BOOST_VERSION >= 107000
192                 handler->inputBuffer->clear();
193                 handler->outputBuffer->clear();
194 #else
195                 handler->inputBuffer->reset();
196                 handler->outputBuffer->reset();
197 #endif
198                 handler.reset();
199             })
200         .onmessage([](crow::websocket::Connection& conn,
201                       const std::string& data, bool is_binary) {
202             if (data.length() > handler->inputBuffer->capacity())
203             {
204                 BMCWEB_LOG_ERROR << "Buffer overrun when writing "
205                                  << data.length() << " bytes";
206                 conn.close("Buffer overrun");
207                 return;
208             }
209 
210             boost::asio::buffer_copy(handler->inputBuffer->prepare(data.size()),
211                                      boost::asio::buffer(data));
212             handler->inputBuffer->commit(data.size());
213             handler->doWrite();
214         });
215 }
216 
217 } // namespace obmc_vm
218 } // namespace crow
219