blob: 6922ae276583369fdec019d63d90ac87c4c9acd9 [file] [log] [blame]
/*
// Copyright (c) 2019 Intel Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
*/
#pragma once
#include <app.h>
#include <websocket.h>
#include <boost/asio.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/multi_buffer.hpp>
#include <boost/container/flat_map.hpp>
#include <dbus_utility.hpp>
#include <experimental/filesystem>
#include <variant>
#include <webserver_common.hpp>
namespace crow
{
namespace nbd_proxy
{
using boost::asio::local::stream_protocol;
static constexpr auto nbdBufferSize = 131088;
struct NbdProxyServer : std::enable_shared_from_this<NbdProxyServer>
{
NbdProxyServer(crow::websocket::Connection& connIn,
const std::string& socketIdIn,
const std::string& endpointIdIn, const std::string& pathIn) :
socketId(socketIdIn),
endpointId(endpointIdIn), path(pathIn),
acceptor(connIn.get_io_context(), stream_protocol::endpoint(socketId)),
connection(connIn)
{
}
~NbdProxyServer()
{
BMCWEB_LOG_DEBUG << "NbdProxyServer destructor";
close();
}
std::string getEndpointId() const
{
return endpointId;
}
void run()
{
acceptor.async_accept(
[this, self(shared_from_this())](boost::system::error_code ec,
stream_protocol::socket socket) {
if (ec)
{
BMCWEB_LOG_ERROR << "UNIX socket: async_accept error = "
<< ec.message();
return;
}
if (peerSocket)
{
// Something is wrong - socket shouldn't be acquired at this
// point
BMCWEB_LOG_ERROR
<< "Failed to open connection - socket already used";
return;
}
BMCWEB_LOG_DEBUG << "Connection opened";
peerSocket = std::move(socket);
doRead();
// Trigger Write if any data was sent from server
// Initially this is negotiation chunk
doWrite();
});
auto mountHandler = [](const boost::system::error_code ec,
const bool status) {
if (ec)
{
BMCWEB_LOG_ERROR << "DBus error: cannot call mount method = "
<< ec.message();
return;
}
};
crow::connections::systemBus->async_method_call(
std::move(mountHandler), "xyz.openbmc_project.VirtualMedia", path,
"xyz.openbmc_project.VirtualMedia.Proxy", "Mount");
}
void send(const std::string_view data)
{
boost::asio::buffer_copy(ws2uxBuf.prepare(data.size()),
boost::asio::buffer(data));
ws2uxBuf.commit(data.size());
doWrite();
}
void close()
{
acceptor.close();
if (peerSocket)
{
BMCWEB_LOG_DEBUG << "peerSocket->close()";
peerSocket->close();
peerSocket.reset();
BMCWEB_LOG_DEBUG << "std::remove(" << socketId << ")";
std::remove(socketId.c_str());
}
// The reference to session should exists until unmount is
// called
auto unmountHandler = [](const boost::system::error_code ec) {
if (ec)
{
BMCWEB_LOG_ERROR << "DBus error: " << ec
<< ", cannot call unmount method";
return;
}
};
crow::connections::systemBus->async_method_call(
std::move(unmountHandler), "xyz.openbmc_project.VirtualMedia", path,
"xyz.openbmc_project.VirtualMedia.Proxy", "Unmount");
}
private:
void doRead()
{
if (!peerSocket)
{
BMCWEB_LOG_DEBUG << "UNIX socket isn't created yet";
// Skip if UNIX socket is not created yet.
return;
}
// Trigger async read
peerSocket->async_read_some(
ux2wsBuf.prepare(nbdBufferSize),
[this, self(shared_from_this())](boost::system::error_code ec,
std::size_t bytesRead) {
if (ec)
{
BMCWEB_LOG_ERROR << "UNIX socket: async_read_some error = "
<< ec.message();
// UNIX socket has been closed by peer, best we can do is to
// break all connections
close();
return;
}
// Fetch data from UNIX socket
ux2wsBuf.commit(bytesRead);
// Paste it to WebSocket as binary
connection.sendBinary(
boost::beast::buffers_to_string(ux2wsBuf.data()));
ux2wsBuf.consume(bytesRead);
// Allow further reads
doRead();
});
}
void doWrite()
{
if (!peerSocket)
{
BMCWEB_LOG_DEBUG << "UNIX socket isn't created yet";
// Skip if UNIX socket is not created yet. Collect data, and wait
// for nbd-client connection
return;
}
if (uxWriteInProgress)
{
BMCWEB_LOG_ERROR << "Write in progress";
return;
}
if (ws2uxBuf.size() == 0)
{
BMCWEB_LOG_ERROR << "No data to write to UNIX socket";
return;
}
uxWriteInProgress = true;
boost::asio::async_write(
*peerSocket, ws2uxBuf.data(),
[this, self(shared_from_this())](boost::system::error_code ec,
std::size_t bytesWritten) {
ws2uxBuf.consume(bytesWritten);
uxWriteInProgress = false;
if (ec)
{
BMCWEB_LOG_ERROR << "UNIX: async_write error = "
<< ec.message();
return;
}
// Retrigger doWrite if there is something in buffer
if (ws2uxBuf.size() > 0)
{
doWrite();
}
});
}
// Keeps UNIX socket endpoint file path
const std::string socketId;
const std::string endpointId;
const std::string path;
bool uxWriteInProgress = false;
// UNIX => WebSocket buffer
boost::beast::multi_buffer ux2wsBuf;
// WebSocket <= UNIX buffer
boost::beast::multi_buffer ws2uxBuf;
// Default acceptor for UNIX socket
stream_protocol::acceptor acceptor;
// The socket used to communicate with the client.
std::optional<stream_protocol::socket> peerSocket;
crow::websocket::Connection& connection;
};
static boost::container::flat_map<crow::websocket::Connection*,
std::shared_ptr<NbdProxyServer>>
sessions;
void requestRoutes(CrowApp& app)
{
BMCWEB_ROUTE(app, "/nbd/<str>")
.websocket()
.onopen([&app](crow::websocket::Connection& conn,
std::shared_ptr<bmcweb::AsyncResp> asyncResp) {
BMCWEB_LOG_DEBUG << "nbd-proxy.onopen(" << &conn << ")";
for (const auto session : sessions)
{
if (session.second->getEndpointId() == conn.req.target())
{
BMCWEB_LOG_ERROR
<< "Cannot open new connection - socket is in use";
return;
}
}
auto openHandler = [asyncResp, &conn](
const boost::system::error_code ec,
dbus::utility::ManagedObjectType& objects) {
const std::string* socketValue = nullptr;
const std::string* endpointValue = nullptr;
const std::string* endpointObjectPath = nullptr;
if (ec)
{
BMCWEB_LOG_ERROR << "DBus error: " << ec.message();
return;
}
for (const auto& objectPath : objects)
{
const auto interfaceMap = objectPath.second.find(
"xyz.openbmc_project.VirtualMedia.MountPoint");
if (interfaceMap == objectPath.second.end())
{
BMCWEB_LOG_DEBUG << "Cannot find MountPoint object";
continue;
}
const auto endpoint =
interfaceMap->second.find("EndpointId");
if (endpoint == interfaceMap->second.end())
{
BMCWEB_LOG_DEBUG << "Cannot find EndpointId property";
continue;
}
endpointValue = std::get_if<std::string>(&endpoint->second);
if (endpointValue == nullptr)
{
BMCWEB_LOG_ERROR << "EndpointId property value is null";
continue;
}
if (*endpointValue == conn.req.target())
{
const auto socket = interfaceMap->second.find("Socket");
if (socket == interfaceMap->second.end())
{
BMCWEB_LOG_DEBUG << "Cannot find Socket property";
continue;
}
socketValue = std::get_if<std::string>(&socket->second);
if (socketValue == nullptr)
{
BMCWEB_LOG_ERROR << "Socket property value is null";
continue;
}
endpointObjectPath = &objectPath.first.str;
break;
}
}
if (endpointObjectPath == nullptr)
{
BMCWEB_LOG_ERROR << "Cannot find requested EndpointId";
asyncResp->res.result(
boost::beast::http::status::not_found);
return;
}
// If the socket file exists (i.e. after bmcweb crash), we
// cannot reuse it.
std::remove((*socketValue).c_str());
sessions[&conn] = std::make_shared<NbdProxyServer>(
conn, std::move(*socketValue), std::move(*endpointValue),
std::move(*endpointObjectPath));
sessions[&conn]->run();
asyncResp->res.result(boost::beast::http::status::ok);
};
crow::connections::systemBus->async_method_call(
std::move(openHandler), "xyz.openbmc_project.VirtualMedia",
"/xyz/openbmc_project/VirtualMedia",
"org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
})
.onclose(
[](crow::websocket::Connection& conn, const std::string& reason) {
BMCWEB_LOG_DEBUG << "nbd-proxy.onclose(reason = '" << reason
<< "')";
auto session = sessions.find(&conn);
if (session == sessions.end())
{
BMCWEB_LOG_DEBUG << "No session to close";
return;
}
session->second->close();
// Remove reference to session in global map
sessions.erase(session);
})
.onmessage([](crow::websocket::Connection& conn,
const std::string& data, bool isBinary) {
BMCWEB_LOG_DEBUG << "nbd-proxy.onmessage(len = " << data.length()
<< ")";
// Acquire proxy from sessions
auto session = sessions.find(&conn);
if (session != sessions.end())
{
if (session->second)
{
session->second->send(data);
return;
}
}
conn.close();
});
}
} // namespace nbd_proxy
} // namespace crow