blob: 64578f2fe206da6fc1843b6010306865c8d6f538 [file] [log] [blame]
Iwona Klimaszewskac0a1c8a2019-07-12 18:26:38 +02001/*
2// Copyright (c) 2019 Intel Corporation
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15*/
16#pragma once
17#include <app.h>
18#include <websocket.h>
19
20#include <boost/asio.hpp>
21#include <boost/beast/core/buffers_to_string.hpp>
22#include <boost/beast/core/multi_buffer.hpp>
23#include <boost/container/flat_map.hpp>
24#include <dbus_utility.hpp>
25#include <experimental/filesystem>
26#include <variant>
27#include <webserver_common.hpp>
28
29namespace crow
30{
31
32namespace nbd_proxy
33{
34
35using boost::asio::local::stream_protocol;
36
37static constexpr auto nbdBufferSize = 131088;
38
39struct NbdProxyServer : std::enable_shared_from_this<NbdProxyServer>
40{
41 NbdProxyServer(crow::websocket::Connection& connIn,
42 const std::string& socketIdIn,
43 const std::string& endpointIdIn, const std::string& pathIn) :
44 socketId(socketIdIn),
45 endpointId(endpointIdIn), path(pathIn),
46 acceptor(connIn.get_io_context(), stream_protocol::endpoint(socketId)),
47 connection(connIn)
48 {
49 }
50
51 ~NbdProxyServer()
52 {
53 BMCWEB_LOG_DEBUG << "NbdProxyServer destructor";
54 close();
55 connection.close();
56
57 if (peerSocket)
58 {
59 BMCWEB_LOG_DEBUG << "peerSocket->close()";
60 peerSocket->close();
61 peerSocket.reset();
62 BMCWEB_LOG_DEBUG << "std::remove(" << socketId << ")";
63 std::remove(socketId.c_str());
64 }
65 }
66
67 std::string getEndpointId() const
68 {
69 return endpointId;
70 }
71
72 void run()
73 {
74 acceptor.async_accept(
75 [this, self(shared_from_this())](boost::system::error_code ec,
76 stream_protocol::socket socket) {
77 if (ec)
78 {
79 BMCWEB_LOG_ERROR << "Cannot accept new connection: " << ec;
80 return;
81 }
82 if (peerSocket)
83 {
84 // Something is wrong - socket shouldn't be acquired at this
85 // point
86 BMCWEB_LOG_ERROR
87 << "Failed to open connection - socket already used";
88 return;
89 }
90
91 BMCWEB_LOG_DEBUG << "Connection opened";
92 peerSocket = std::move(socket);
93 doRead();
94
95 // Trigger Write if any data was sent from server
96 // Initially this is negotiation chunk
97 doWrite();
98 });
99
100 auto mountHandler = [](const boost::system::error_code ec,
101 const bool status) {
102 if (ec)
103 {
104 BMCWEB_LOG_ERROR << "DBus error: " << ec
105 << ", cannot call mount method";
106 return;
107 }
108 };
109
110 crow::connections::systemBus->async_method_call(
111 std::move(mountHandler), "xyz.openbmc_project.VirtualMedia", path,
112 "xyz.openbmc_project.VirtualMedia.Proxy", "Mount");
113 }
114
115 void send(const std::string_view data)
116 {
117 boost::asio::buffer_copy(ws2uxBuf.prepare(data.size()),
118 boost::asio::buffer(data));
119 ws2uxBuf.commit(data.size());
120 doWrite();
121 }
122
123 void close()
124 {
125 // The reference to session should exists until unmount is
126 // called
127 auto unmountHandler = [](const boost::system::error_code ec) {
128 if (ec)
129 {
130 BMCWEB_LOG_ERROR << "DBus error: " << ec
131 << ", cannot call unmount method";
132 return;
133 }
134 };
135
136 crow::connections::systemBus->async_method_call(
137 std::move(unmountHandler), "xyz.openbmc_project.VirtualMedia", path,
138 "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount");
139 }
140
141 private:
142 void doRead()
143 {
144 if (!peerSocket)
145 {
146 BMCWEB_LOG_DEBUG << "UNIX socket isn't created yet";
147 // Skip if UNIX socket is not created yet.
148 return;
149 }
150
151 // Trigger async read
152 peerSocket->async_read_some(
153 ux2wsBuf.prepare(nbdBufferSize),
154 [this, self(shared_from_this())](boost::system::error_code ec,
155 std::size_t bytesRead) {
156 if (ec)
157 {
158 BMCWEB_LOG_ERROR << "UNIX socket: async_read_some error = "
159 << ec;
160 // UNIX socket has been closed by peer, best we can do is to
161 // break all connections
162 close();
163 return;
164 }
165
166 // Fetch data from UNIX socket
167
168 ux2wsBuf.commit(bytesRead);
169
170 // Paste it to WebSocket as binary
171 connection.sendBinary(
172 boost::beast::buffers_to_string(ux2wsBuf.data()));
173 ux2wsBuf.consume(bytesRead);
174
175 // Allow further reads
176 doRead();
177 });
178 }
179
180 void doWrite()
181 {
182 if (!peerSocket)
183 {
184 BMCWEB_LOG_DEBUG << "UNIX socket isn't created yet";
185 // Skip if UNIX socket is not created yet. Collect data, and wait
186 // for nbd-client connection
187 return;
188 }
189
190 if (uxWriteInProgress)
191 {
192 BMCWEB_LOG_ERROR << "Write in progress";
193 return;
194 }
195
196 if (ws2uxBuf.size() == 0)
197 {
198 BMCWEB_LOG_ERROR << "No data to write to UNIX socket";
199 return;
200 }
201
202 uxWriteInProgress = true;
203 boost::asio::async_write(
204 *peerSocket, ws2uxBuf.data(),
205 [this, self(shared_from_this())](boost::system::error_code ec,
206 std::size_t bytesWritten) {
207 ws2uxBuf.consume(bytesWritten);
208 uxWriteInProgress = false;
209 if (ec)
210 {
211 BMCWEB_LOG_ERROR << "UNIX: async_write error = " << ec;
212 return;
213 }
214 // Retrigger doWrite if there is something in buffer
215 doWrite();
216 });
217 }
218
219 // Keeps UNIX socket endpoint file path
220 const std::string socketId;
221 const std::string endpointId;
222 const std::string path;
223
224 bool uxWriteInProgress = false;
225
226 // UNIX => WebSocket buffer
227 boost::beast::multi_buffer ux2wsBuf;
228
229 // WebSocket <= UNIX buffer
230 boost::beast::multi_buffer ws2uxBuf;
231
232 // Default acceptor for UNIX socket
233 stream_protocol::acceptor acceptor;
234
235 // The socket used to communicate with the client.
236 std::optional<stream_protocol::socket> peerSocket;
237
238 crow::websocket::Connection& connection;
239};
240
241static boost::container::flat_map<crow::websocket::Connection*,
242 std::shared_ptr<NbdProxyServer>>
243 sessions;
244
245void requestRoutes(CrowApp& app)
246{
247 BMCWEB_ROUTE(app, "/nbd/<str>")
248 .websocket()
249 .onopen([&app](crow::websocket::Connection& conn,
250 std::shared_ptr<bmcweb::AsyncResp> asyncResp) {
251 BMCWEB_LOG_DEBUG << "nbd-proxy.onopen(" << &conn << ")";
252
253 for (const auto session : sessions)
254 {
255 if (session.second->getEndpointId() == conn.req.target())
256 {
257 BMCWEB_LOG_ERROR
258 << "Cannot open new connection - socket is in use";
259 return;
260 }
261 }
262
263 auto openHandler = [asyncResp, &conn](
264 const boost::system::error_code ec,
265 dbus::utility::ManagedObjectType& objects) {
266 const std::string* socketValue = nullptr;
267 const std::string* endpointValue = nullptr;
268 const std::string* endpointObjectPath = nullptr;
269
270 if (ec)
271 {
272 BMCWEB_LOG_ERROR << "DBus error: " << ec;
273 return;
274 }
275
276 for (const auto& objectPath : objects)
277 {
278 const auto interfaceMap = objectPath.second.find(
279 "xyz.openbmc_project.VirtualMedia.MountPoint");
280
281 if (interfaceMap == objectPath.second.end())
282 {
283 BMCWEB_LOG_DEBUG << "Cannot find MountPoint object";
284 continue;
285 }
286
287 const auto endpoint =
288 interfaceMap->second.find("EndpointId");
289 if (endpoint == interfaceMap->second.end())
290 {
291 BMCWEB_LOG_DEBUG << "Cannot find EndpointId property";
292 continue;
293 }
294
295 endpointValue = std::get_if<std::string>(&endpoint->second);
296
297 if (endpointValue == nullptr)
298 {
299 BMCWEB_LOG_ERROR << "EndpointId property value is null";
300 continue;
301 }
302
303 if (*endpointValue == conn.req.target())
304 {
305 const auto socket = interfaceMap->second.find("Socket");
306 if (socket == interfaceMap->second.end())
307 {
308 BMCWEB_LOG_DEBUG << "Cannot find Socket property";
309 continue;
310 }
311
312 socketValue = std::get_if<std::string>(&socket->second);
313 if (socketValue == nullptr)
314 {
315 BMCWEB_LOG_ERROR << "Socket property value is null";
316 continue;
317 }
318
319 endpointObjectPath = &objectPath.first.str;
320 break;
321 }
322 }
323
324 if (endpointObjectPath == nullptr)
325 {
326 BMCWEB_LOG_ERROR << "Cannot find requested EndpointId";
327 asyncResp->res.result(
328 boost::beast::http::status::not_found);
329 return;
330 }
331
332 // If the socket file exists (i.e. after bmcweb crash), we
333 // cannot reuse it.
334 std::remove((*socketValue).c_str());
335
336 sessions[&conn] = std::make_shared<NbdProxyServer>(
337 conn, std::move(*socketValue), std::move(*endpointValue),
338 std::move(*endpointObjectPath));
339
340 sessions[&conn]->run();
341
342 asyncResp->res.result(boost::beast::http::status::ok);
343 };
344 crow::connections::systemBus->async_method_call(
345 std::move(openHandler), "xyz.openbmc_project.VirtualMedia",
346 "/xyz/openbmc_project/VirtualMedia",
347 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
348 })
349 .onclose(
350 [](crow::websocket::Connection& conn, const std::string& reason) {
351 BMCWEB_LOG_DEBUG << "nbd-proxy.onclose(reason = '" << reason
352 << "')";
353 auto session = sessions.find(&conn);
354 if (session == sessions.end())
355 {
356 BMCWEB_LOG_DEBUG << "No session to close";
357 return;
358 }
359 // Remove reference to session in global map
360 session->second->close();
361 sessions.erase(session);
362 })
363 .onmessage([](crow::websocket::Connection& conn,
364 const std::string& data, bool isBinary) {
365 BMCWEB_LOG_DEBUG << "nbd-proxy.onmessage(len = " << data.length()
366 << ")";
367 // Acquire proxy from sessions
368 auto session = sessions.find(&conn);
369 if (session != sessions.end())
370 {
371 if (session->second)
372 {
373 session->second->send(data);
374 return;
375 }
376 }
377 conn.close();
378 });
379}
380} // namespace nbd_proxy
381} // namespace crow