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/bin/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: {}", ec.message()); 65 if (session != nullptr) 66 { 67 session->close("Error connecting to nbd-proxy"); 68 } 69 return; 70 } 71 doWrite(); 72 doRead(); 73 } 74 75 void doWrite() 76 { 77 if (doingWrite) 78 { 79 BMCWEB_LOG_DEBUG("Already writing. Bailing out"); 80 return; 81 } 82 83 if (inputBuffer->size() == 0) 84 { 85 BMCWEB_LOG_DEBUG("inputBuffer empty. Bailing out"); 86 return; 87 } 88 89 doingWrite = true; 90 pipeIn.async_write_some( 91 inputBuffer->data(), 92 [this, self(shared_from_this())](const boost::beast::error_code& ec, 93 std::size_t bytesWritten) { 94 BMCWEB_LOG_DEBUG("Wrote {}bytes", bytesWritten); 95 doingWrite = false; 96 inputBuffer->consume(bytesWritten); 97 98 if (session == nullptr) 99 { 100 return; 101 } 102 if (ec == boost::asio::error::eof) 103 { 104 session->close("VM socket port closed"); 105 return; 106 } 107 if (ec) 108 { 109 session->close("Error in writing to proxy port"); 110 BMCWEB_LOG_ERROR("Error in VM socket write {}", ec); 111 return; 112 } 113 doWrite(); 114 }); 115 } 116 117 void doRead() 118 { 119 std::size_t bytes = outputBuffer->capacity() - outputBuffer->size(); 120 121 pipeOut.async_read_some( 122 outputBuffer->prepare(bytes), 123 [this, self(shared_from_this())]( 124 const boost::system::error_code& ec, std::size_t bytesRead) { 125 BMCWEB_LOG_DEBUG("Read done. Read {} bytes", bytesRead); 126 if (ec) 127 { 128 BMCWEB_LOG_ERROR("Couldn't read from VM port: {}", ec); 129 if (session != nullptr) 130 { 131 session->close("Error in connecting to VM port"); 132 } 133 return; 134 } 135 if (session == nullptr) 136 { 137 return; 138 } 139 140 outputBuffer->commit(bytesRead); 141 std::string_view payload( 142 static_cast<const char*>(outputBuffer->data().data()), 143 bytesRead); 144 session->sendBinary(payload); 145 outputBuffer->consume(bytesRead); 146 147 doRead(); 148 }); 149 } 150 151 boost::process::async_pipe pipeOut; 152 boost::process::async_pipe pipeIn; 153 boost::process::child proxy; 154 std::string media; 155 bool doingWrite{false}; 156 157 std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>> 158 outputBuffer; 159 std::unique_ptr<boost::beast::flat_static_buffer<nbdBufferSize>> 160 inputBuffer; 161 }; 162 163 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 164 static std::shared_ptr<Handler> handler; 165 166 inline void requestRoutes(App& app) 167 { 168 BMCWEB_ROUTE(app, "/vm/0/0") 169 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 170 .websocket() 171 .onopen([](crow::websocket::Connection& conn) { 172 BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn)); 173 174 if (session != nullptr) 175 { 176 conn.close("Session already connected"); 177 return; 178 } 179 180 if (handler != nullptr) 181 { 182 conn.close("Handler already running"); 183 return; 184 } 185 186 session = &conn; 187 188 // media is the last digit of the endpoint /vm/0/0. A future 189 // enhancement can include supporting different endpoint values. 190 const char* media = "0"; 191 handler = std::make_shared<Handler>(media, conn.getIoContext()); 192 handler->connect(); 193 }) 194 .onclose([](crow::websocket::Connection& conn, 195 const std::string& /*reason*/) { 196 if (&conn != session) 197 { 198 return; 199 } 200 201 session = nullptr; 202 handler->doClose(); 203 handler->inputBuffer->clear(); 204 handler->outputBuffer->clear(); 205 handler.reset(); 206 }) 207 .onmessage([](crow::websocket::Connection& conn, 208 const std::string& data, bool) { 209 if (data.length() > 210 handler->inputBuffer->capacity() - handler->inputBuffer->size()) 211 { 212 BMCWEB_LOG_ERROR("Buffer overrun when writing {} bytes", 213 data.length()); 214 conn.close("Buffer overrun"); 215 return; 216 } 217 218 size_t copied = 219 boost::asio::buffer_copy(handler->inputBuffer->prepare(data.size()), 220 boost::asio::buffer(data)); 221 handler->inputBuffer->commit(copied); 222 handler->doWrite(); 223 }); 224 } 225 226 } // namespace obmc_vm 227 } // namespace crow 228