blob: fbbf3cce13ffe82ea13db78653826053c874bbf8 [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.hpp"
#include "dbus_utility.hpp"
#include "privileges.hpp"
#include <boost/asio/buffer.hpp>
#include <boost/asio/local/stream_protocol.hpp>
#include <boost/asio/write.hpp>
#include <boost/beast/core/buffers_to_string.hpp>
#include <boost/beast/core/multi_buffer.hpp>
#include <boost/container/flat_map.hpp>
#include <websocket.hpp>
namespace crow
{
namespace nbd_proxy
{
using boost::asio::local::stream_protocol;
static constexpr auto nbdBufferSize = 131088;
constexpr const char* requiredPrivilegeString = "ConfigureManager";
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.getIoContext(), stream_protocol::endpoint(socketId)),
connection(connIn)
{}
NbdProxyServer(const NbdProxyServer&) = delete;
NbdProxyServer(NbdProxyServer&&) = delete;
NbdProxyServer& operator=(const NbdProxyServer&) = delete;
NbdProxyServer& operator=(NbdProxyServer&&) = delete;
~NbdProxyServer()
{
BMCWEB_LOG_DEBUG << "NbdProxyServer destructor";
}
std::string getEndpointId() const
{
return endpointId;
}
void run()
{
acceptor.async_accept([this, self(shared_from_this())](
const 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 =
[this, self(shared_from_this())](
const boost::system::error_code& ec, const bool) {
if (ec)
{
BMCWEB_LOG_ERROR << "DBus error: cannot call mount method = "
<< ec.message();
connection.close("Failed to mount media");
return;
}
};
crow::connections::systemBus->async_method_call(
std::move(mountHandler), "xyz.openbmc_project.VirtualMedia", path,
"xyz.openbmc_project.VirtualMedia.Proxy", "Mount");
}
void send(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())](
const 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())](
const 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;
};
using SessionMap = boost::container::flat_map<crow::websocket::Connection*,
std::shared_ptr<NbdProxyServer>>;
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static SessionMap sessions;
inline void onOpen(crow::websocket::Connection& conn)
{
BMCWEB_LOG_DEBUG << "nbd-proxy.onopen(" << &conn << ")";
auto getUserInfoHandler =
[&conn](const boost::system::error_code& ec,
const dbus::utility::DBusPropertiesMap& userInfo) {
if (ec)
{
BMCWEB_LOG_ERROR << "GetUserInfo failed...";
conn.close("Failed to get user information");
return;
}
const std::string* userRolePtr = nullptr;
auto userInfoIter =
std::find_if(userInfo.begin(), userInfo.end(), [](const auto& p) {
return p.first == "UserPrivilege";
});
if (userInfoIter != userInfo.end())
{
userRolePtr = std::get_if<std::string>(&userInfoIter->second);
}
std::string userRole{};
if (userRolePtr != nullptr)
{
userRole = *userRolePtr;
BMCWEB_LOG_DEBUG << "userName = " << conn.getUserName()
<< " userRole = " << *userRolePtr;
}
// Get the user privileges from the role
::redfish::Privileges userPrivileges =
::redfish::getUserPrivileges(userRole);
const ::redfish::Privileges requiredPrivileges{requiredPrivilegeString};
if (!userPrivileges.isSupersetOf(requiredPrivileges))
{
BMCWEB_LOG_DEBUG << "User " << conn.getUserName()
<< " not authorized for nbd connection";
conn.close("Unathourized access");
return;
}
auto openHandler =
[&conn](const boost::system::error_code& ec2,
const dbus::utility::ManagedObjectType& objects) {
const std::string* socketValue = nullptr;
const std::string* endpointValue = nullptr;
const std::string* endpointObjectPath = nullptr;
if (ec2)
{
BMCWEB_LOG_ERROR << "DBus error: " << ec2.message();
conn.close("Failed to create mount point");
return;
}
for (const auto& [objectPath, interfaces] : objects)
{
for (const auto& [interface, properties] : interfaces)
{
if (interface !=
"xyz.openbmc_project.VirtualMedia.MountPoint")
{
continue;
}
for (const auto& [name, value] : properties)
{
if (name == "EndpointId")
{
endpointValue = std::get_if<std::string>(&value);
if (endpointValue == nullptr)
{
BMCWEB_LOG_ERROR
<< "EndpointId property value is null";
}
}
if (name == "Socket")
{
socketValue = std::get_if<std::string>(&value);
if (socketValue == nullptr)
{
BMCWEB_LOG_ERROR
<< "Socket property value is null";
}
}
}
}
if ((endpointValue != nullptr) && (socketValue != nullptr) &&
*endpointValue == conn.req.target())
{
endpointObjectPath = &objectPath.str;
break;
}
}
if (objects.empty() || endpointObjectPath == nullptr)
{
BMCWEB_LOG_ERROR << "Cannot find requested EndpointId";
conn.close("Failed to match EndpointId");
return;
}
for (const auto& session : sessions)
{
if (session.second->getEndpointId() == conn.req.target())
{
BMCWEB_LOG_ERROR
<< "Cannot open new connection - socket is in use";
conn.close("Slot is in use");
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, *socketValue, *endpointValue, *endpointObjectPath);
sessions[&conn]->run();
};
crow::connections::systemBus->async_method_call(
std::move(openHandler), "xyz.openbmc_project.VirtualMedia",
"/xyz/openbmc_project/VirtualMedia",
"org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
};
crow::connections::systemBus->async_method_call(
std::move(getUserInfoHandler), "xyz.openbmc_project.User.Manager",
"/xyz/openbmc_project/user", "xyz.openbmc_project.User.Manager",
"GetUserInfo", conn.getUserName());
}
inline void 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);
}
inline void 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;
}
}
}
inline void requestRoutes(App& app)
{
BMCWEB_ROUTE(app, "/nbd/<str>")
.websocket()
.onopen(onOpen)
.onclose(onClose)
.onmessage(onMessage);
}
} // namespace nbd_proxy
} // namespace crow