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