Rearrange features
The backends are different things compared to generic code. Today,
these are all included in the /include folder, but it's not very clear
what options control which backends, or how things map together. This
also means that we can't separate ownership between the various
companies.
This commit is a proposal to try to create a features folder,
separated by the code for the various backends, to make interacting
with this easier. It takes the form
features/<option name>/files.hpp
features/<option name>/files_test.hpp
Note, redfish-core was already at top level, and contains lots of code,
so to prevent lots of conflicts, it's simply symlinked into that folder
to make clear that it is a backend, but not to move the implementation
and cause code conflicts.
Tested: Unit tests pass. Code compiles.
Change-Id: Idcc80ffcfd99c876734ee41d53f894ca5583fed5
Signed-off-by: Ed Tanous <etanous@nvidia.com>
diff --git a/features/google/google_service_root.hpp b/features/google/google_service_root.hpp
new file mode 100644
index 0000000..3d21af7
--- /dev/null
+++ b/features/google/google_service_root.hpp
@@ -0,0 +1,239 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "app.hpp"
+#include "async_resp.hpp"
+#include "dbus_utility.hpp"
+#include "error_messages.hpp"
+#include "http_request.hpp"
+#include "logging.hpp"
+#include "utils/collection.hpp"
+#include "utils/hex_utils.hpp"
+#include "utils/json_utils.hpp"
+
+#include <boost/beast/http/verb.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/url/format.hpp>
+#include <boost/url/url.hpp>
+#include <nlohmann/json.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <array>
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace crow
+{
+namespace google_api
+{
+
+inline void handleGoogleV1Get(
+ const crow::Request& /*req*/,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ asyncResp->res.jsonValue["@odata.type"] =
+ "#GoogleServiceRoot.v1_0_0.GoogleServiceRoot";
+ asyncResp->res.jsonValue["@odata.id"] = "/google/v1";
+ asyncResp->res.jsonValue["Id"] = "Google Rest RootService";
+ asyncResp->res.jsonValue["Name"] = "Google Service Root";
+ asyncResp->res.jsonValue["Version"] = "1.0.0";
+ asyncResp->res.jsonValue["RootOfTrustCollection"]["@odata.id"] =
+ "/google/v1/RootOfTrustCollection";
+}
+
+inline void handleRootOfTrustCollectionGet(
+ const crow::Request& /*req*/,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ asyncResp->res.jsonValue["@odata.id"] = "/google/v1/RootOfTrustCollection";
+ asyncResp->res.jsonValue["@odata.type"] =
+ "#RootOfTrustCollection.RootOfTrustCollection";
+ const std::array<std::string_view, 1> interfaces{
+ "xyz.openbmc_project.Control.Hoth"};
+ redfish::collection_util::getCollectionMembers(
+ asyncResp, boost::urls::url("/google/v1/RootOfTrustCollection"),
+ interfaces, "/xyz/openbmc_project");
+}
+
+// Helper struct to identify a resolved D-Bus object interface
+struct ResolvedEntity
+{
+ std::string id;
+ std::string service;
+ std::string object;
+ std::string interface;
+};
+
+using ResolvedEntityHandler = std::function<void(
+ const std::string&, const std::shared_ptr<bmcweb::AsyncResp>&,
+ const ResolvedEntity&)>;
+
+inline void hothGetSubtreeCallback(
+ const std::string& command,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& rotId, const ResolvedEntityHandler& entityHandler,
+ const boost::system::error_code& ec,
+ const dbus::utility::MapperGetSubTreeResponse& subtree)
+{
+ if (ec)
+ {
+ redfish::messages::internalError(asyncResp->res);
+ return;
+ }
+ for (const auto& [path, services] : subtree)
+ {
+ sdbusplus::message::object_path objPath(path);
+ if (objPath.filename() != rotId || services.empty())
+ {
+ continue;
+ }
+
+ ResolvedEntity resolvedEntity = {
+ .id = rotId,
+ .service = services[0].first,
+ .object = path,
+ .interface = "xyz.openbmc_project.Control.Hoth"};
+ entityHandler(command, asyncResp, resolvedEntity);
+ return;
+ }
+
+ // Couldn't find an object with that name. return an error
+ redfish::messages::resourceNotFound(asyncResp->res, "RootOfTrust", rotId);
+}
+
+inline void resolveRoT(const std::string& command,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& rotId,
+ ResolvedEntityHandler&& entityHandler)
+{
+ constexpr std::array<std::string_view, 1> hothIfaces = {
+ "xyz.openbmc_project.Control.Hoth"};
+ dbus::utility::getSubTree(
+ "/xyz/openbmc_project", 0, hothIfaces,
+ [command, asyncResp, rotId, entityHandler{std::move(entityHandler)}](
+ const boost::system::error_code& ec,
+ const dbus::utility::MapperGetSubTreeResponse& subtree) {
+ hothGetSubtreeCallback(command, asyncResp, rotId, entityHandler, ec,
+ subtree);
+ });
+}
+
+inline void populateRootOfTrustEntity(
+ const std::string& /*unused*/,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const ResolvedEntity& resolvedEntity)
+{
+ asyncResp->res.jsonValue["@odata.type"] = "#RootOfTrust.v1_0_0.RootOfTrust";
+ asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
+ "/google/v1/RootOfTrustCollection/{}", resolvedEntity.id);
+
+ asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
+ asyncResp->res.jsonValue["Id"] = resolvedEntity.id;
+ // Need to fix this later to a stabler property.
+ asyncResp->res.jsonValue["Name"] = resolvedEntity.id;
+ asyncResp->res.jsonValue["Description"] = "Google Root Of Trust";
+ asyncResp->res.jsonValue["Actions"]["#RootOfTrust.SendCommand"]["target"] =
+ "/google/v1/RootOfTrustCollection/" + resolvedEntity.id +
+ "/Actions/RootOfTrust.SendCommand";
+
+ asyncResp->res.jsonValue["Location"]["PartLocation"]["ServiceLabel"] =
+ resolvedEntity.id;
+ asyncResp->res.jsonValue["Location"]["PartLocation"]["LocationType"] =
+ "Embedded";
+}
+
+inline void handleRootOfTrustGet(
+ const crow::Request& /*req*/,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& param)
+{
+ std::string emptyCommand;
+ resolveRoT(emptyCommand, asyncResp, param, populateRootOfTrustEntity);
+}
+
+inline void invocationCallback(
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const boost::system::error_code& ec,
+ const std::vector<uint8_t>& responseBytes)
+{
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("RootOfTrust.Actions.SendCommand failed: {}",
+ ec.message());
+ redfish::messages::internalError(asyncResp->res);
+ return;
+ }
+
+ asyncResp->res.jsonValue["CommandResponse"] =
+ bytesToHexString(responseBytes);
+}
+
+inline void invokeRoTCommand(
+ const std::string& command,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const ResolvedEntity& resolvedEntity)
+{
+ std::vector<uint8_t> bytes = hexStringToBytes(command);
+ if (bytes.empty())
+ {
+ BMCWEB_LOG_DEBUG("Invalid command: {}", command);
+ redfish::messages::actionParameterValueTypeError(command, "Command",
+ "SendCommand");
+ return;
+ }
+
+ dbus::utility::async_method_call(
+ asyncResp,
+ [asyncResp{asyncResp}](const boost::system::error_code& ec,
+ const std::vector<uint8_t>& responseBytes) {
+ invocationCallback(asyncResp, ec, responseBytes);
+ },
+ resolvedEntity.service, resolvedEntity.object, resolvedEntity.interface,
+ "SendHostCommand", bytes);
+}
+
+inline void handleRoTSendCommandPost(
+ const crow::Request& request,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& rotId)
+{
+ std::string command;
+ if (!redfish::json_util::readJsonAction(request, asyncResp->res, "Command",
+ command))
+ {
+ BMCWEB_LOG_DEBUG("Missing property Command.");
+ redfish::messages::actionParameterMissing(asyncResp->res, "SendCommand",
+ "Command");
+ return;
+ }
+
+ resolveRoT(command, asyncResp, rotId, invokeRoTCommand);
+}
+
+inline void requestRoutes(App& app)
+{
+ BMCWEB_ROUTE(app, "/google/v1/")
+ .methods(boost::beast::http::verb::get)(handleGoogleV1Get);
+
+ BMCWEB_ROUTE(app, "/google/v1/RootOfTrustCollection")
+ .privileges({{"ConfigureManager"}})
+ .methods(boost::beast::http::verb::get)(handleRootOfTrustCollectionGet);
+
+ BMCWEB_ROUTE(app, "/google/v1/RootOfTrustCollection/<str>")
+ .privileges({{"ConfigureManager"}})
+ .methods(boost::beast::http::verb::get)(handleRootOfTrustGet);
+
+ BMCWEB_ROUTE(
+ app,
+ "/google/v1/RootOfTrustCollection/<str>/Actions/RootOfTrust.SendCommand")
+ .privileges({{"ConfigureManager"}})
+ .methods(boost::beast::http::verb::post)(handleRoTSendCommandPost);
+}
+
+} // namespace google_api
+} // namespace crow
diff --git a/features/google/google_service_root_test.cpp b/features/google/google_service_root_test.cpp
new file mode 100644
index 0000000..c104494
--- /dev/null
+++ b/features/google/google_service_root_test.cpp
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#include "async_resp.hpp"
+#include "google_service_root.hpp"
+#include "http_request.hpp"
+#include "http_response.hpp"
+
+#include <boost/beast/http/verb.hpp>
+#include <nlohmann/json.hpp>
+
+#include <memory>
+#include <system_error>
+
+#include <gtest/gtest.h>
+
+namespace crow::google_api
+{
+namespace
+{
+
+void validateServiceRootGet(crow::Response& res)
+{
+ nlohmann::json& json = res.jsonValue;
+ EXPECT_EQ(json["@odata.id"], "/google/v1");
+ EXPECT_EQ(json["@odata.type"],
+ "#GoogleServiceRoot.v1_0_0.GoogleServiceRoot");
+ EXPECT_EQ(json["@odata.id"], "/google/v1");
+ EXPECT_EQ(json["Id"], "Google Rest RootService");
+ EXPECT_EQ(json["Name"], "Google Service Root");
+ EXPECT_EQ(json["Version"], "1.0.0");
+ EXPECT_EQ(json["RootOfTrustCollection"]["@odata.id"],
+ "/google/v1/RootOfTrustCollection");
+}
+
+TEST(HandleGoogleV1Get, OnSuccess)
+{
+ std::error_code ec;
+ auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
+
+ asyncResp->res.setCompleteRequestHandler(validateServiceRootGet);
+
+ crow::Request dummyRequest{{boost::beast::http::verb::get, "", 11}, ec};
+ handleGoogleV1Get(dummyRequest, asyncResp);
+}
+
+} // namespace
+} // namespace crow::google_api
diff --git a/features/google/meson.build b/features/google/meson.build
new file mode 100644
index 0000000..5df3400
--- /dev/null
+++ b/features/google/meson.build
@@ -0,0 +1,2 @@
+incdir += include_directories('.')
+test_sources += files('google_service_root_test.cpp')
diff --git a/features/ibm/configfile_test.cpp b/features/ibm/configfile_test.cpp
new file mode 100644
index 0000000..b64a2d3
--- /dev/null
+++ b/features/ibm/configfile_test.cpp
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#include "http_response.hpp"
+#include "ibm_management_console_rest.hpp"
+
+#include <string>
+
+#include <gtest/gtest.h>
+
+namespace crow
+{
+namespace ibm_mc
+{
+
+TEST(IsValidConfigFileName, FileNameValidCharReturnsTrue)
+{
+ crow::Response res;
+
+ EXPECT_TRUE(isValidConfigFileName("GoodConfigFile", res));
+}
+TEST(IsValidConfigFileName, FileNameInvalidCharReturnsFalse)
+{
+ crow::Response res;
+
+ EXPECT_FALSE(isValidConfigFileName("Bad@file", res));
+}
+TEST(IsValidConfigFileName, FileNameInvalidPathReturnsFalse)
+{
+ crow::Response res;
+
+ EXPECT_FALSE(isValidConfigFileName("/../../../../../etc/badpath", res));
+ EXPECT_FALSE(isValidConfigFileName("/../../etc/badpath", res));
+ EXPECT_FALSE(isValidConfigFileName("/mydir/configFile", res));
+}
+
+TEST(IsValidConfigFileName, EmptyFileNameReturnsFalse)
+{
+ crow::Response res;
+ EXPECT_FALSE(isValidConfigFileName("", res));
+}
+
+TEST(IsValidConfigFileName, SlashFileNameReturnsFalse)
+{
+ crow::Response res;
+ EXPECT_FALSE(isValidConfigFileName("/", res));
+}
+TEST(IsValidConfigFileName, FileNameMoreThan20CharReturnsFalse)
+{
+ crow::Response res;
+ EXPECT_FALSE(isValidConfigFileName("BadfileBadfileBadfile", res));
+}
+
+} // namespace ibm_mc
+} // namespace crow
diff --git a/features/ibm/ibm_management_console_rest.hpp b/features/ibm/ibm_management_console_rest.hpp
new file mode 100644
index 0000000..764e504
--- /dev/null
+++ b/features/ibm/ibm_management_console_rest.hpp
@@ -0,0 +1,486 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "app.hpp"
+#include "async_resp.hpp"
+#include "http_request.hpp"
+#include "logging.hpp"
+#include "str_utility.hpp"
+#include "utils.hpp"
+#include "utils/json_utils.hpp"
+
+#include <boost/beast/core/string_type.hpp>
+#include <boost/beast/http/field.hpp>
+#include <boost/beast/http/status.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <nlohmann/json.hpp>
+
+#include <cstddef>
+#include <cstdint>
+#include <filesystem>
+#include <fstream>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <system_error>
+#include <utility>
+#include <vector>
+
+namespace crow
+{
+namespace ibm_mc
+{
+constexpr const char* methodNotAllowedMsg = "Method Not Allowed";
+constexpr const char* resourceNotFoundMsg = "Resource Not Found";
+constexpr const char* contentNotAcceptableMsg = "Content Not Acceptable";
+constexpr const char* internalServerError = "Internal Server Error";
+
+constexpr size_t maxSaveareaDirSize =
+ 25000000; // Allow save area dir size to be max 25MB
+constexpr size_t minSaveareaFileSize =
+ 100; // Allow save area file size of minimum 100B
+constexpr size_t maxSaveareaFileSize =
+ 500000; // Allow save area file size upto 500KB
+constexpr size_t maxBroadcastMsgSize =
+ 1000; // Allow Broadcast message size upto 1KB
+
+inline void handleFilePut(const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& fileID)
+{
+ std::error_code ec;
+ // Check the content-type of the request
+ boost::beast::string_view contentType = req.getHeaderValue("content-type");
+ if (!bmcweb::asciiIEquals(contentType, "application/octet-stream"))
+ {
+ asyncResp->res.result(boost::beast::http::status::not_acceptable);
+ asyncResp->res.jsonValue["Description"] = contentNotAcceptableMsg;
+ return;
+ }
+ BMCWEB_LOG_DEBUG(
+ "File upload in application/octet-stream format. Continue..");
+
+ BMCWEB_LOG_DEBUG(
+ "handleIbmPut: Request to create/update the save-area file");
+ std::string_view path =
+ "/var/lib/bmcweb/ibm-management-console/configfiles";
+ if (!crow::ibm_utils::createDirectory(path))
+ {
+ asyncResp->res.result(boost::beast::http::status::not_found);
+ asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg;
+ return;
+ }
+
+ std::ofstream file;
+ std::filesystem::path loc(
+ "/var/lib/bmcweb/ibm-management-console/configfiles");
+
+ // Get the current size of the savearea directory
+ std::filesystem::recursive_directory_iterator iter(loc, ec);
+ if (ec)
+ {
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ asyncResp->res.jsonValue["Description"] = internalServerError;
+ BMCWEB_LOG_DEBUG("handleIbmPut: Failed to prepare save-area "
+ "directory iterator. ec : {}",
+ ec.message());
+ return;
+ }
+ std::uintmax_t saveAreaDirSize = 0;
+ for (const auto& it : iter)
+ {
+ if (!std::filesystem::is_directory(it, ec))
+ {
+ if (ec)
+ {
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ asyncResp->res.jsonValue["Description"] = internalServerError;
+ BMCWEB_LOG_DEBUG("handleIbmPut: Failed to find save-area "
+ "directory . ec : {}",
+ ec.message());
+ return;
+ }
+ std::uintmax_t fileSize = std::filesystem::file_size(it, ec);
+ if (ec)
+ {
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ asyncResp->res.jsonValue["Description"] = internalServerError;
+ BMCWEB_LOG_DEBUG("handleIbmPut: Failed to find save-area "
+ "file size inside the directory . ec : {}",
+ ec.message());
+ return;
+ }
+ saveAreaDirSize += fileSize;
+ }
+ }
+ BMCWEB_LOG_DEBUG("saveAreaDirSize: {}", saveAreaDirSize);
+
+ // Get the file size getting uploaded
+ const std::string& data = req.body();
+ BMCWEB_LOG_DEBUG("data length: {}", data.length());
+
+ if (data.length() < minSaveareaFileSize)
+ {
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ asyncResp->res.jsonValue["Description"] =
+ "File size is less than minimum allowed size[100B]";
+ return;
+ }
+ if (data.length() > maxSaveareaFileSize)
+ {
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ asyncResp->res.jsonValue["Description"] =
+ "File size exceeds maximum allowed size[500KB]";
+ return;
+ }
+
+ // Form the file path
+ loc /= fileID;
+ BMCWEB_LOG_DEBUG("Writing to the file: {}", loc.string());
+
+ // Check if the same file exists in the directory
+ bool fileExists = std::filesystem::exists(loc, ec);
+ if (ec)
+ {
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ asyncResp->res.jsonValue["Description"] = internalServerError;
+ BMCWEB_LOG_DEBUG("handleIbmPut: Failed to find if file exists. ec : {}",
+ ec.message());
+ return;
+ }
+
+ std::uintmax_t newSizeToWrite = 0;
+ if (fileExists)
+ {
+ // File exists. Get the current file size
+ std::uintmax_t currentFileSize = std::filesystem::file_size(loc, ec);
+ if (ec)
+ {
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ asyncResp->res.jsonValue["Description"] = internalServerError;
+ BMCWEB_LOG_DEBUG("handleIbmPut: Failed to find file size. ec : {}",
+ ec.message());
+ return;
+ }
+ // Calculate the difference in the file size.
+ // If the data.length is greater than the existing file size, then
+ // calculate the difference. Else consider the delta size as zero -
+ // because there is no increase in the total directory size.
+ // We need to add the diff only if the incoming data is larger than the
+ // existing filesize
+ if (data.length() > currentFileSize)
+ {
+ newSizeToWrite = data.length() - currentFileSize;
+ }
+ BMCWEB_LOG_DEBUG("newSizeToWrite: {}", newSizeToWrite);
+ }
+ else
+ {
+ // This is a new file upload
+ newSizeToWrite = data.length();
+ }
+
+ // Calculate the total dir size before writing the new file
+ BMCWEB_LOG_DEBUG("total new size: {}", saveAreaDirSize + newSizeToWrite);
+
+ if ((saveAreaDirSize + newSizeToWrite) > maxSaveareaDirSize)
+ {
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ asyncResp->res.jsonValue["Description"] =
+ "File size does not fit in the savearea "
+ "directory maximum allowed size[25MB]";
+ return;
+ }
+
+ file.open(loc, std::ofstream::out);
+
+ // set the permission of the file to 600
+ std::filesystem::perms permission = std::filesystem::perms::owner_write |
+ std::filesystem::perms::owner_read;
+ std::filesystem::permissions(loc, permission);
+
+ if (file.fail())
+ {
+ BMCWEB_LOG_DEBUG("Error while opening the file for writing");
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ asyncResp->res.jsonValue["Description"] =
+ "Error while creating the file";
+ return;
+ }
+ file << data;
+
+ // Push an event
+ if (fileExists)
+ {
+ BMCWEB_LOG_DEBUG("config file is updated");
+ asyncResp->res.jsonValue["Description"] = "File Updated";
+ }
+ else
+ {
+ BMCWEB_LOG_DEBUG("config file is created");
+ asyncResp->res.jsonValue["Description"] = "File Created";
+ }
+}
+
+inline void handleConfigFileList(
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ std::vector<std::string> pathObjList;
+ std::filesystem::path loc(
+ "/var/lib/bmcweb/ibm-management-console/configfiles");
+ if (std::filesystem::exists(loc) && std::filesystem::is_directory(loc))
+ {
+ for (const auto& file : std::filesystem::directory_iterator(loc))
+ {
+ const std::filesystem::path& pathObj = file.path();
+ if (std::filesystem::is_regular_file(pathObj))
+ {
+ pathObjList.emplace_back(
+ "/ibm/v1/Host/ConfigFiles/" + pathObj.filename().string());
+ }
+ }
+ }
+ asyncResp->res.jsonValue["@odata.type"] =
+ "#IBMConfigFile.v1_0_0.IBMConfigFile";
+ asyncResp->res.jsonValue["@odata.id"] = "/ibm/v1/Host/ConfigFiles/";
+ asyncResp->res.jsonValue["Id"] = "ConfigFiles";
+ asyncResp->res.jsonValue["Name"] = "ConfigFiles";
+
+ asyncResp->res.jsonValue["Members"] = std::move(pathObjList);
+ asyncResp->res.jsonValue["Actions"]["#IBMConfigFiles.DeleteAll"]["target"] =
+ "/ibm/v1/Host/ConfigFiles/Actions/IBMConfigFiles.DeleteAll";
+}
+
+inline void deleteConfigFiles(
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ std::error_code ec;
+ std::filesystem::path loc(
+ "/var/lib/bmcweb/ibm-management-console/configfiles");
+ if (std::filesystem::exists(loc) && std::filesystem::is_directory(loc))
+ {
+ std::filesystem::remove_all(loc, ec);
+ if (ec)
+ {
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ asyncResp->res.jsonValue["Description"] = internalServerError;
+ BMCWEB_LOG_DEBUG("deleteConfigFiles: Failed to delete the "
+ "config files directory. ec : {}",
+ ec.message());
+ }
+ }
+}
+
+inline void handleFileGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& fileID)
+{
+ BMCWEB_LOG_DEBUG("HandleGet on SaveArea files on path: {}", fileID);
+ std::filesystem::path loc(
+ "/var/lib/bmcweb/ibm-management-console/configfiles/" + fileID);
+ if (!std::filesystem::exists(loc) || !std::filesystem::is_regular_file(loc))
+ {
+ BMCWEB_LOG_WARNING("{} Not found", loc.string());
+ asyncResp->res.result(boost::beast::http::status::not_found);
+ asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg;
+ return;
+ }
+
+ std::ifstream readfile(loc.string());
+ if (!readfile)
+ {
+ BMCWEB_LOG_WARNING("{} Not found", loc.string());
+ asyncResp->res.result(boost::beast::http::status::not_found);
+ asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg;
+ return;
+ }
+
+ std::string contentDispositionParam =
+ "attachment; filename=\"" + fileID + "\"";
+ asyncResp->res.addHeader(boost::beast::http::field::content_disposition,
+ contentDispositionParam);
+ std::string fileData;
+ fileData = {std::istreambuf_iterator<char>(readfile),
+ std::istreambuf_iterator<char>()};
+ asyncResp->res.jsonValue["Data"] = fileData;
+}
+
+inline void handleFileDelete(
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& fileID)
+{
+ std::string filePath(
+ "/var/lib/bmcweb/ibm-management-console/configfiles/" + fileID);
+ BMCWEB_LOG_DEBUG("Removing the file : {}", filePath);
+ std::ifstream fileOpen(filePath.c_str());
+ if (static_cast<bool>(fileOpen))
+ {
+ if (remove(filePath.c_str()) == 0)
+ {
+ BMCWEB_LOG_DEBUG("File removed!");
+ asyncResp->res.jsonValue["Description"] = "File Deleted";
+ }
+ else
+ {
+ BMCWEB_LOG_ERROR("File not removed!");
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ asyncResp->res.jsonValue["Description"] = internalServerError;
+ }
+ }
+ else
+ {
+ BMCWEB_LOG_WARNING("File not found!");
+ asyncResp->res.result(boost::beast::http::status::not_found);
+ asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg;
+ }
+}
+
+inline void handleBroadcastService(
+ const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ std::string broadcastMsg;
+
+ if (!redfish::json_util::readJsonPatch(req, asyncResp->res, "Message",
+ broadcastMsg))
+ {
+ BMCWEB_LOG_DEBUG("Not a Valid JSON");
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ return;
+ }
+ if (broadcastMsg.size() > maxBroadcastMsgSize)
+ {
+ BMCWEB_LOG_ERROR("Message size exceeds maximum allowed size[1KB]");
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ return;
+ }
+}
+
+inline void handleFileUrl(const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& fileID)
+{
+ if (req.method() == boost::beast::http::verb::put)
+ {
+ handleFilePut(req, asyncResp, fileID);
+ return;
+ }
+ if (req.method() == boost::beast::http::verb::get)
+ {
+ handleFileGet(asyncResp, fileID);
+ return;
+ }
+ if (req.method() == boost::beast::http::verb::delete_)
+ {
+ handleFileDelete(asyncResp, fileID);
+ return;
+ }
+}
+
+inline bool isValidConfigFileName(const std::string& fileName,
+ crow::Response& res)
+{
+ if (fileName.empty())
+ {
+ BMCWEB_LOG_ERROR("Empty filename");
+ res.jsonValue["Description"] = "Empty file path in the url";
+ return false;
+ }
+
+ // ConfigFile name is allowed to take upper and lowercase letters,
+ // numbers and hyphen
+ std::size_t found = fileName.find_first_not_of(
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-");
+ if (found != std::string::npos)
+ {
+ BMCWEB_LOG_ERROR("Unsupported character in filename: {}", fileName);
+ res.jsonValue["Description"] = "Unsupported character in filename";
+ return false;
+ }
+
+ // Check the filename length
+ if (fileName.length() > 20)
+ {
+ BMCWEB_LOG_ERROR("Name must be maximum 20 characters. "
+ "Input filename length is: {}",
+ fileName.length());
+ res.jsonValue["Description"] = "Filename must be maximum 20 characters";
+ return false;
+ }
+
+ return true;
+}
+
+inline void requestRoutes(App& app)
+{
+ // allowed only for admin
+ BMCWEB_ROUTE(app, "/ibm/v1/")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request&,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ asyncResp->res.jsonValue["@odata.type"] =
+ "#ibmServiceRoot.v1_0_0.ibmServiceRoot";
+ asyncResp->res.jsonValue["@odata.id"] = "/ibm/v1/";
+ asyncResp->res.jsonValue["Id"] = "IBM Rest RootService";
+ asyncResp->res.jsonValue["Name"] = "IBM Service Root";
+ asyncResp->res.jsonValue["ConfigFiles"]["@odata.id"] =
+ "/ibm/v1/Host/ConfigFiles";
+ asyncResp->res.jsonValue["BroadcastService"]["@odata.id"] =
+ "/ibm/v1/HMC/BroadcastService";
+ });
+
+ BMCWEB_ROUTE(app, "/ibm/v1/Host/ConfigFiles")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request&,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ handleConfigFileList(asyncResp);
+ });
+
+ BMCWEB_ROUTE(app,
+ "/ibm/v1/Host/ConfigFiles/Actions/IBMConfigFiles.DeleteAll")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::post)(
+ [](const crow::Request&,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ deleteConfigFiles(asyncResp);
+ });
+
+ BMCWEB_ROUTE(app, "/ibm/v1/Host/ConfigFiles/<str>")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::put, boost::beast::http::verb::get,
+ boost::beast::http::verb::delete_)(
+ [](const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& fileName) {
+ BMCWEB_LOG_DEBUG("ConfigFile : {}", fileName);
+ // Validate the incoming fileName
+ if (!isValidConfigFileName(fileName, asyncResp->res))
+ {
+ asyncResp->res.result(
+ boost::beast::http::status::bad_request);
+ return;
+ }
+ handleFileUrl(req, asyncResp, fileName);
+ });
+
+ BMCWEB_ROUTE(app, "/ibm/v1/HMC/BroadcastService")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::post)(
+ [](const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ handleBroadcastService(req, asyncResp);
+ });
+}
+
+} // namespace ibm_mc
+} // namespace crow
diff --git a/features/ibm/meson.build b/features/ibm/meson.build
new file mode 100644
index 0000000..c73258f
--- /dev/null
+++ b/features/ibm/meson.build
@@ -0,0 +1,2 @@
+incdir += include_directories('.')
+test_sources += files('configfile_test.cpp')
diff --git a/features/ibm/utils.hpp b/features/ibm/utils.hpp
new file mode 100644
index 0000000..fe99f8e
--- /dev/null
+++ b/features/ibm/utils.hpp
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "logging.hpp"
+
+#include <filesystem>
+#include <string_view>
+#include <system_error>
+
+namespace crow
+{
+namespace ibm_utils
+{
+
+inline bool createDirectory(std::string_view path)
+{
+ // Create persistent directory
+ std::error_code ec;
+
+ BMCWEB_LOG_DEBUG("Creating persistent directory : {}", path);
+
+ bool dirCreated = std::filesystem::create_directories(path, ec);
+
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("Failed to create persistent directory : {}", path);
+ return false;
+ }
+
+ if (dirCreated)
+ {
+ // set the permission of the directory to 700
+ BMCWEB_LOG_DEBUG("Setting the permission to 700");
+ std::filesystem::perms permission = std::filesystem::perms::owner_all;
+ std::filesystem::permissions(path, permission);
+ }
+ else
+ {
+ BMCWEB_LOG_DEBUG("{} already exists", path);
+ }
+ return true;
+}
+
+} // namespace ibm_utils
+} // namespace crow
diff --git a/features/kvm/kvm_websocket.hpp b/features/kvm/kvm_websocket.hpp
new file mode 100644
index 0000000..adf4c5e
--- /dev/null
+++ b/features/kvm/kvm_websocket.hpp
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+#include "app.hpp"
+#include "io_context_singleton.hpp"
+#include "logging.hpp"
+#include "websocket.hpp"
+
+#include <sys/types.h>
+
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/error.hpp>
+#include <boost/asio/ip/address.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/beast/core/flat_static_buffer.hpp>
+#include <boost/container/flat_map.hpp>
+
+#include <cstddef>
+#include <memory>
+#include <string>
+
+namespace crow
+{
+namespace obmc_kvm
+{
+
+static constexpr const uint maxSessions = 4;
+
+class KvmSession : public std::enable_shared_from_this<KvmSession>
+{
+ public:
+ explicit KvmSession(crow::websocket::Connection& connIn) :
+ conn(connIn), hostSocket(getIoContext())
+ {
+ boost::asio::ip::tcp::endpoint endpoint(
+ boost::asio::ip::make_address("127.0.0.1"), 5900);
+ hostSocket.async_connect(
+ endpoint, [this, &connIn](const boost::system::error_code& ec) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "conn:{}, Couldn't connect to KVM socket port: {}",
+ logPtr(&conn), ec);
+ if (ec != boost::asio::error::operation_aborted)
+ {
+ connIn.close("Error in connecting to KVM port");
+ }
+ return;
+ }
+
+ doRead();
+ });
+ }
+
+ void onMessage(const std::string& data)
+ {
+ if (data.length() > inputBuffer.capacity())
+ {
+ BMCWEB_LOG_ERROR("conn:{}, Buffer overrun when writing {} bytes",
+ logPtr(&conn), data.length());
+ conn.close("Buffer overrun");
+ return;
+ }
+
+ BMCWEB_LOG_DEBUG("conn:{}, Read {} bytes from websocket", logPtr(&conn),
+ data.size());
+ size_t copied = boost::asio::buffer_copy(
+ inputBuffer.prepare(data.size()), boost::asio::buffer(data));
+ BMCWEB_LOG_DEBUG("conn:{}, Committing {} bytes from websocket",
+ logPtr(&conn), copied);
+ inputBuffer.commit(copied);
+
+ BMCWEB_LOG_DEBUG("conn:{}, inputbuffer size {}", logPtr(&conn),
+ inputBuffer.size());
+ doWrite();
+ }
+
+ protected:
+ void doRead()
+ {
+ std::size_t bytes = outputBuffer.capacity() - outputBuffer.size();
+ BMCWEB_LOG_DEBUG("conn:{}, Reading {} from kvm socket", logPtr(&conn),
+ bytes);
+ hostSocket.async_read_some(
+ outputBuffer.prepare(outputBuffer.capacity() - outputBuffer.size()),
+ [this, weak(weak_from_this())](const boost::system::error_code& ec,
+ std::size_t bytesRead) {
+ auto self = weak.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+ BMCWEB_LOG_DEBUG("conn:{}, read done. Read {} bytes",
+ logPtr(&conn), bytesRead);
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "conn:{}, Couldn't read from KVM socket port: {}",
+ logPtr(&conn), ec);
+ if (ec != boost::asio::error::operation_aborted)
+ {
+ conn.close("Error in connecting to KVM port");
+ }
+ return;
+ }
+
+ outputBuffer.commit(bytesRead);
+ std::string_view payload(
+ static_cast<const char*>(outputBuffer.data().data()),
+ bytesRead);
+ BMCWEB_LOG_DEBUG("conn:{}, Sending payload size {}",
+ logPtr(&conn), payload.size());
+ conn.sendBinary(payload);
+ outputBuffer.consume(bytesRead);
+
+ doRead();
+ });
+ }
+
+ void doWrite()
+ {
+ if (doingWrite)
+ {
+ BMCWEB_LOG_DEBUG("conn:{}, Already writing. Bailing out",
+ logPtr(&conn));
+ return;
+ }
+ if (inputBuffer.size() == 0)
+ {
+ BMCWEB_LOG_DEBUG("conn:{}, inputBuffer empty. Bailing out",
+ logPtr(&conn));
+ return;
+ }
+
+ doingWrite = true;
+ hostSocket.async_write_some(
+ inputBuffer.data(),
+ [this, weak(weak_from_this())](const boost::system::error_code& ec,
+ std::size_t bytesWritten) {
+ auto self = weak.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+ BMCWEB_LOG_DEBUG("conn:{}, Wrote {}bytes", logPtr(&conn),
+ bytesWritten);
+ doingWrite = false;
+ inputBuffer.consume(bytesWritten);
+
+ if (ec == boost::asio::error::eof)
+ {
+ conn.close("KVM socket port closed");
+ return;
+ }
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("conn:{}, Error in KVM socket write {}",
+ logPtr(&conn), ec);
+ if (ec != boost::asio::error::operation_aborted)
+ {
+ conn.close("Error in reading to host port");
+ }
+ return;
+ }
+
+ doWrite();
+ });
+ }
+
+ crow::websocket::Connection& conn;
+ boost::asio::ip::tcp::socket hostSocket;
+ boost::beast::flat_static_buffer<1024UL * 50UL> outputBuffer;
+ boost::beast::flat_static_buffer<1024UL> inputBuffer;
+ bool doingWrite{false};
+};
+
+using SessionMap = boost::container::flat_map<crow::websocket::Connection*,
+ std::shared_ptr<KvmSession>>;
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+static SessionMap sessions;
+
+inline void requestRoutes(App& app)
+{
+ sessions.reserve(maxSessions);
+
+ BMCWEB_ROUTE(app, "/kvm/0")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .websocket()
+ .onopen([](crow::websocket::Connection& conn) {
+ BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
+
+ if (sessions.size() == maxSessions)
+ {
+ conn.close("Max sessions are already connected");
+ return;
+ }
+
+ sessions[&conn] = std::make_shared<KvmSession>(conn);
+ })
+ .onclose([](crow::websocket::Connection& conn, const std::string&) {
+ sessions.erase(&conn);
+ })
+ .onmessage([](crow::websocket::Connection& conn,
+ const std::string& data, bool) {
+ if (sessions[&conn])
+ {
+ sessions[&conn]->onMessage(data);
+ }
+ });
+}
+
+} // namespace obmc_kvm
+} // namespace crow
diff --git a/features/kvm/meson.build b/features/kvm/meson.build
new file mode 100644
index 0000000..ab60765
--- /dev/null
+++ b/features/kvm/meson.build
@@ -0,0 +1 @@
+incdir += include_directories('.')
diff --git a/features/meson.build b/features/meson.build
new file mode 100644
index 0000000..ff01c88
--- /dev/null
+++ b/features/meson.build
@@ -0,0 +1,7 @@
+subdir('google')
+subdir('ibm')
+subdir('kvm')
+subdir('openbmc_rest')
+subdir('serial')
+subdir('virtual_media')
+subdir('webui_login')
diff --git a/features/openbmc_rest/dbus_monitor.hpp b/features/openbmc_rest/dbus_monitor.hpp
new file mode 100644
index 0000000..050bd34
--- /dev/null
+++ b/features/openbmc_rest/dbus_monitor.hpp
@@ -0,0 +1,272 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+#include "app.hpp"
+#include "dbus_singleton.hpp"
+#include "logging.hpp"
+#include "openbmc_dbus_rest.hpp"
+#include "websocket.hpp"
+
+#include <systemd/sd-bus.h>
+
+#include <boost/container/flat_map.hpp>
+#include <boost/container/flat_set.hpp>
+#include <nlohmann/json.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <cstddef>
+#include <cstring>
+#include <functional>
+#include <memory>
+#include <regex>
+#include <string>
+#include <vector>
+
+namespace crow
+{
+namespace dbus_monitor
+{
+
+struct DbusWebsocketSession
+{
+ std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches;
+ boost::container::flat_set<std::string, std::less<>,
+ std::vector<std::string>>
+ interfaces;
+};
+
+using SessionMap = boost::container::flat_map<crow::websocket::Connection*,
+ DbusWebsocketSession>;
+
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+static SessionMap sessions;
+
+inline int onPropertyUpdate(sd_bus_message* m, void* userdata,
+ sd_bus_error* retError)
+{
+ if (retError == nullptr || (sd_bus_error_is_set(retError) != 0))
+ {
+ BMCWEB_LOG_ERROR("Got sdbus error on match");
+ return 0;
+ }
+ crow::websocket::Connection* connection =
+ static_cast<crow::websocket::Connection*>(userdata);
+ auto thisSession = sessions.find(connection);
+ if (thisSession == sessions.end())
+ {
+ BMCWEB_LOG_ERROR("Couldn't find dbus connection {}",
+ logPtr(connection));
+ return 0;
+ }
+ sdbusplus::message_t message(m);
+ nlohmann::json json;
+ json["event"] = message.get_member();
+ json["path"] = message.get_path();
+ if (strcmp(message.get_member(), "PropertiesChanged") == 0)
+ {
+ nlohmann::json data;
+ int r = openbmc_mapper::convertDBusToJSON("sa{sv}as", message, data);
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("convertDBusToJSON failed with {}", r);
+ return 0;
+ }
+ if (!data.is_array())
+ {
+ BMCWEB_LOG_ERROR("No data in PropertiesChanged signal");
+ return 0;
+ }
+
+ // data is type sa{sv}as and is an array[3] of string, object, array
+ json["interface"] = data[0];
+ json["properties"] = data[1];
+ }
+ else if (strcmp(message.get_member(), "InterfacesAdded") == 0)
+ {
+ nlohmann::json data;
+ int r = openbmc_mapper::convertDBusToJSON("oa{sa{sv}}", message, data);
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("convertDBusToJSON failed with {}", r);
+ return 0;
+ }
+ nlohmann::json::array_t* arr = data.get_ptr<nlohmann::json::array_t*>();
+ if (arr == nullptr)
+ {
+ BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
+ return 0;
+ }
+ if (arr->size() < 2)
+ {
+ BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
+ return 0;
+ }
+
+ nlohmann::json::object_t* obj =
+ (*arr)[1].get_ptr<nlohmann::json::object_t*>();
+ if (obj == nullptr)
+ {
+ BMCWEB_LOG_ERROR("No data in InterfacesAdded signal");
+ return 0;
+ }
+ // data is type oa{sa{sv}} which is an array[2] of string, object
+ for (const auto& entry : *obj)
+ {
+ auto it = thisSession->second.interfaces.find(entry.first);
+ if (it != thisSession->second.interfaces.end())
+ {
+ json["interfaces"][entry.first] = entry.second;
+ }
+ }
+ }
+ else
+ {
+ BMCWEB_LOG_CRITICAL("message {} was unexpected", message.get_member());
+ return 0;
+ }
+
+ connection->sendText(
+ json.dump(2, ' ', true, nlohmann::json::error_handler_t::replace));
+ return 0;
+}
+
+inline void requestRoutes(App& app)
+{
+ BMCWEB_ROUTE(app, "/subscribe")
+ .privileges({{"Login"}})
+ .websocket()
+ .onopen([](crow::websocket::Connection& conn) {
+ BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
+ sessions.try_emplace(&conn);
+ })
+ .onclose([](crow::websocket::Connection& conn, const std::string&) {
+ sessions.erase(&conn);
+ })
+ .onmessage([](crow::websocket::Connection& conn,
+ const std::string& data, bool) {
+ const auto sessionPair = sessions.find(&conn);
+ if (sessionPair == sessions.end())
+ {
+ conn.close("Internal error");
+ }
+ DbusWebsocketSession& thisSession = sessionPair->second;
+ BMCWEB_LOG_DEBUG("Connection {} received {}", logPtr(&conn), data);
+ nlohmann::json j = nlohmann::json::parse(data, nullptr, false);
+ if (j.is_discarded())
+ {
+ BMCWEB_LOG_ERROR("Unable to parse json data for monitor");
+ conn.close("Unable to parse json request");
+ return;
+ }
+ nlohmann::json::iterator interfaces = j.find("interfaces");
+ if (interfaces != j.end())
+ {
+ thisSession.interfaces.reserve(interfaces->size());
+ for (auto& interface : *interfaces)
+ {
+ const std::string* str =
+ interface.get_ptr<const std::string*>();
+ if (str != nullptr)
+ {
+ thisSession.interfaces.insert(*str);
+ }
+ }
+ }
+
+ nlohmann::json::iterator paths = j.find("paths");
+ if (paths == j.end())
+ {
+ BMCWEB_LOG_ERROR("Unable to find paths in json data");
+ conn.close("Unable to find paths in json data");
+ return;
+ }
+
+ size_t interfaceCount = thisSession.interfaces.size();
+ if (interfaceCount == 0)
+ {
+ interfaceCount = 1;
+ }
+
+ // These regexes derived on the rules here:
+ // https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
+ static std::regex validPath("^/([A-Za-z0-9_]+/?)*$");
+ static std::regex validInterface(
+ "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$");
+
+ for (const auto& thisPath : *paths)
+ {
+ const std::string* thisPathString =
+ thisPath.get_ptr<const std::string*>();
+ if (thisPathString == nullptr)
+ {
+ BMCWEB_LOG_ERROR("subscribe path isn't a string?");
+ conn.close();
+ return;
+ }
+ if (!std::regex_match(*thisPathString, validPath))
+ {
+ BMCWEB_LOG_ERROR("Invalid path name {}", *thisPathString);
+ conn.close();
+ return;
+ }
+ std::string propertiesMatchString =
+ ("type='signal',"
+ "interface='org.freedesktop.DBus.Properties',"
+ "path_namespace='" +
+ *thisPathString +
+ "',"
+ "member='PropertiesChanged'");
+ // If interfaces weren't specified, add a single match for all
+ // interfaces
+ if (thisSession.interfaces.empty())
+ {
+ BMCWEB_LOG_DEBUG("Creating match {}",
+ propertiesMatchString);
+
+ thisSession.matches.emplace_back(
+ std::make_unique<sdbusplus::bus::match_t>(
+ *crow::connections::systemBus,
+ propertiesMatchString, onPropertyUpdate, &conn));
+ }
+ else
+ {
+ // If interfaces were specified, add a match for each
+ // interface
+ for (const std::string& interface : thisSession.interfaces)
+ {
+ if (!std::regex_match(interface, validInterface))
+ {
+ BMCWEB_LOG_ERROR("Invalid interface name {}",
+ interface);
+ conn.close();
+ return;
+ }
+ std::string ifaceMatchString = propertiesMatchString;
+ ifaceMatchString += ",arg0='";
+ ifaceMatchString += interface;
+ ifaceMatchString += "'";
+ BMCWEB_LOG_DEBUG("Creating match {}", ifaceMatchString);
+ thisSession.matches.emplace_back(
+ std::make_unique<sdbusplus::bus::match_t>(
+ *crow::connections::systemBus, ifaceMatchString,
+ onPropertyUpdate, &conn));
+ }
+ }
+ std::string objectManagerMatchString =
+ ("type='signal',"
+ "interface='org.freedesktop.DBus.ObjectManager',"
+ "path_namespace='" +
+ *thisPathString +
+ "',"
+ "member='InterfacesAdded'");
+ BMCWEB_LOG_DEBUG("Creating match {}", objectManagerMatchString);
+ thisSession.matches.emplace_back(
+ std::make_unique<sdbusplus::bus::match_t>(
+ *crow::connections::systemBus, objectManagerMatchString,
+ onPropertyUpdate, &conn));
+ }
+ });
+}
+} // namespace dbus_monitor
+} // namespace crow
diff --git a/features/openbmc_rest/image_upload.hpp b/features/openbmc_rest/image_upload.hpp
new file mode 100644
index 0000000..f6a0dfc
--- /dev/null
+++ b/features/openbmc_rest/image_upload.hpp
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "app.hpp"
+#include "async_resp.hpp"
+#include "dbus_singleton.hpp"
+#include "dbus_utility.hpp"
+#include "http_request.hpp"
+#include "io_context_singleton.hpp"
+#include "logging.hpp"
+#include "ossl_random.hpp"
+
+#include <boost/asio/error.hpp>
+#include <boost/asio/steady_timer.hpp>
+#include <boost/beast/http/status.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <algorithm>
+#include <chrono>
+#include <cstdio>
+#include <fstream>
+#include <functional>
+#include <memory>
+#include <ranges>
+#include <string>
+
+namespace crow
+{
+namespace image_upload
+{
+
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+static std::unique_ptr<sdbusplus::bus::match_t> fwUpdateMatcher;
+
+inline void uploadImageHandler(
+ const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ // Only allow one FW update at a time
+ if (fwUpdateMatcher != nullptr)
+ {
+ asyncResp->res.addHeader("Retry-After", "30");
+ asyncResp->res.result(boost::beast::http::status::service_unavailable);
+ return;
+ }
+ // Make this const static so it survives outside this method
+ static boost::asio::steady_timer timeout(getIoContext(),
+ std::chrono::seconds(5));
+
+ timeout.expires_after(std::chrono::seconds(15));
+
+ auto timeoutHandler = [asyncResp](const boost::system::error_code& ec) {
+ fwUpdateMatcher = nullptr;
+ if (ec == boost::asio::error::operation_aborted)
+ {
+ // expected, we were canceled before the timer completed.
+ return;
+ }
+ BMCWEB_LOG_ERROR("Timed out waiting for Version interface");
+
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("Async_wait failed {}", ec);
+ return;
+ }
+
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ asyncResp->res.jsonValue["data"]["description"] =
+ "Version already exists or failed to be extracted";
+ asyncResp->res.jsonValue["message"] = "400 Bad Request";
+ asyncResp->res.jsonValue["status"] = "error";
+ };
+
+ std::function<void(sdbusplus::message_t&)> callback =
+ [asyncResp](sdbusplus::message_t& m) {
+ BMCWEB_LOG_DEBUG("Match fired");
+
+ sdbusplus::message::object_path path;
+ dbus::utility::DBusInterfacesMap interfaces;
+ m.read(path, interfaces);
+
+ if (std::ranges::find_if(interfaces, [](const auto& i) {
+ return i.first == "xyz.openbmc_project.Software.Version";
+ }) != interfaces.end())
+ {
+ timeout.cancel();
+ std::string leaf = path.filename();
+ if (leaf.empty())
+ {
+ leaf = path.str;
+ }
+
+ asyncResp->res.jsonValue["data"] = leaf;
+ asyncResp->res.jsonValue["message"] = "200 OK";
+ asyncResp->res.jsonValue["status"] = "ok";
+ BMCWEB_LOG_DEBUG("ending response");
+ fwUpdateMatcher = nullptr;
+ }
+ };
+ fwUpdateMatcher = std::make_unique<sdbusplus::bus::match_t>(
+ *crow::connections::systemBus,
+ "interface='org.freedesktop.DBus.ObjectManager',type='signal',"
+ "member='InterfacesAdded',path='/xyz/openbmc_project/software'",
+ callback);
+
+ std::string filepath("/tmp/images/" + bmcweb::getRandomUUID());
+ BMCWEB_LOG_DEBUG("Writing file to {}", filepath);
+ std::ofstream out(filepath, std::ofstream::out | std::ofstream::binary |
+ std::ofstream::trunc);
+ out << req.body();
+ out.close();
+ timeout.async_wait(timeoutHandler);
+}
+
+inline void requestRoutes(App& app)
+{
+ BMCWEB_ROUTE(app, "/upload/image/<str>")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::post, boost::beast::http::verb::put)(
+ [](const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string&) { uploadImageHandler(req, asyncResp); });
+
+ BMCWEB_ROUTE(app, "/upload/image")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::post, boost::beast::http::verb::put)(
+ [](const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ uploadImageHandler(req, asyncResp);
+ });
+}
+} // namespace image_upload
+} // namespace crow
diff --git a/features/openbmc_rest/meson.build b/features/openbmc_rest/meson.build
new file mode 100644
index 0000000..cfceb79
--- /dev/null
+++ b/features/openbmc_rest/meson.build
@@ -0,0 +1,2 @@
+incdir += include_directories('.')
+test_sources += files('openbmc_dbus_rest_test.cpp')
diff --git a/features/openbmc_rest/openbmc_dbus_rest.hpp b/features/openbmc_rest/openbmc_dbus_rest.hpp
new file mode 100644
index 0000000..d2bd48f8
--- /dev/null
+++ b/features/openbmc_rest/openbmc_dbus_rest.hpp
@@ -0,0 +1,2650 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+// SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
+
+#pragma once
+#include "app.hpp"
+#include "async_resp.hpp"
+#include "boost_formatters.hpp"
+#include "dbus_singleton.hpp"
+#include "dbus_utility.hpp"
+#include "http_request.hpp"
+#include "http_response.hpp"
+#include "json_formatters.hpp"
+#include "logging.hpp"
+#include "parsing.hpp"
+#include "str_utility.hpp"
+
+#include <systemd/sd-bus-protocol.h>
+#include <systemd/sd-bus.h>
+#include <tinyxml2.h>
+
+#include <boost/beast/http/field.hpp>
+#include <boost/beast/http/status.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <boost/container/flat_map.hpp>
+#include <boost/system/error_code.hpp>
+#include <nlohmann/json.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/message.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <algorithm>
+#include <array>
+#include <cerrno>
+#include <cstdint>
+#include <cstring>
+#include <filesystem>
+#include <functional>
+#include <initializer_list>
+#include <limits>
+#include <map>
+#include <memory>
+#include <ranges>
+#include <regex>
+#include <string>
+#include <string_view>
+#include <type_traits>
+#include <utility>
+#include <variant>
+#include <vector>
+
+namespace crow
+{
+namespace openbmc_mapper
+{
+const constexpr char* notFoundMsg = "404 Not Found";
+const constexpr char* badReqMsg = "400 Bad Request";
+const constexpr char* methodNotAllowedMsg = "405 Method Not Allowed";
+const constexpr char* forbiddenMsg = "403 Forbidden";
+const constexpr char* unsupportedMediaMsg = "415 Unsupported Media Type";
+const constexpr char* methodFailedMsg = "500 Method Call Failed";
+const constexpr char* methodOutputFailedMsg = "500 Method Output Error";
+const constexpr char* notFoundDesc =
+ "org.freedesktop.DBus.Error.FileNotFound: path or object not found";
+const constexpr char* propNotFoundDesc =
+ "The specified property cannot be found";
+const constexpr char* noJsonDesc = "No JSON object could be decoded";
+const constexpr char* invalidContentType =
+ "Content-type header is missing or invalid";
+const constexpr char* methodNotFoundDesc =
+ "The specified method cannot be found";
+const constexpr char* methodNotAllowedDesc = "Method not allowed";
+const constexpr char* forbiddenPropDesc =
+ "The specified property cannot be created";
+const constexpr char* forbiddenResDesc =
+ "The specified resource cannot be created";
+
+inline bool validateFilename(const std::string& filename)
+{
+ static std::regex validFilename(R"(^[\w\- ]+(\.?[\w\- ]*)$)");
+
+ return std::regex_match(filename, validFilename);
+}
+
+inline void setErrorResponse(crow::Response& res,
+ boost::beast::http::status result,
+ const std::string& desc, std::string_view msg)
+{
+ res.result(result);
+ res.jsonValue["data"]["description"] = desc;
+ res.jsonValue["message"] = msg;
+ res.jsonValue["status"] = "error";
+}
+
+inline void introspectObjects(
+ const std::string& processName, const std::string& objectPath,
+ const std::shared_ptr<bmcweb::AsyncResp>& transaction)
+{
+ if (transaction->res.jsonValue.is_null())
+ {
+ transaction->res.jsonValue["status"] = "ok";
+ transaction->res.jsonValue["bus_name"] = processName;
+ transaction->res.jsonValue["objects"] = nlohmann::json::array();
+ }
+
+ dbus::utility::async_method_call(
+ [transaction, processName{std::string(processName)},
+ objectPath{std::string(objectPath)}](
+ const boost::system::error_code& ec,
+ const std::string& introspectXml) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "Introspect call failed with error: {} on process: {} path: {}",
+ ec.message(), processName, objectPath);
+ return;
+ }
+ nlohmann::json::object_t object;
+ object["path"] = objectPath;
+
+ transaction->res.jsonValue["objects"].emplace_back(
+ std::move(object));
+
+ tinyxml2::XMLDocument doc;
+
+ doc.Parse(introspectXml.c_str());
+ tinyxml2::XMLNode* pRoot = doc.FirstChildElement("node");
+ if (pRoot == nullptr)
+ {
+ BMCWEB_LOG_ERROR("XML document failed to parse {} {}",
+ processName, objectPath);
+ }
+ else
+ {
+ tinyxml2::XMLElement* node = pRoot->FirstChildElement("node");
+ while (node != nullptr)
+ {
+ const char* childPath = node->Attribute("name");
+ if (childPath != nullptr)
+ {
+ std::string newpath;
+ if (objectPath != "/")
+ {
+ newpath += objectPath;
+ }
+ newpath += std::string("/") + childPath;
+ // introspect the subobjects as well
+ introspectObjects(processName, newpath, transaction);
+ }
+
+ node = node->NextSiblingElement("node");
+ }
+ }
+ },
+ processName, objectPath, "org.freedesktop.DBus.Introspectable",
+ "Introspect");
+}
+
+inline void getPropertiesForEnumerate(
+ const std::string& objectPath, const std::string& service,
+ const std::string& interface,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ BMCWEB_LOG_DEBUG("getPropertiesForEnumerate {} {} {}", objectPath, service,
+ interface);
+
+ dbus::utility::getAllProperties(
+ service, objectPath, interface,
+ [asyncResp, objectPath, service,
+ interface](const boost::system::error_code& ec,
+ const dbus::utility::DBusPropertiesMap& propertiesList) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "GetAll on path {} iface {} service {} failed with code {}",
+ objectPath, interface, service, ec);
+ return;
+ }
+
+ nlohmann::json& dataJson = asyncResp->res.jsonValue["data"];
+ nlohmann::json& objectJson = dataJson[objectPath];
+ if (objectJson.is_null())
+ {
+ objectJson = nlohmann::json::object();
+ }
+
+ for (const auto& [name, value] : propertiesList)
+ {
+ nlohmann::json& propertyJson = objectJson[name];
+ std::visit([&propertyJson](auto&& val) { propertyJson = val; },
+ value);
+ }
+ });
+}
+
+// Find any results that weren't picked up by ObjectManagers, to be
+// called after all ObjectManagers are searched for and called.
+inline void findRemainingObjectsForEnumerate(
+ const std::string& objectPath,
+ const std::shared_ptr<dbus::utility::MapperGetSubTreeResponse>& subtree,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ BMCWEB_LOG_DEBUG("findRemainingObjectsForEnumerate");
+ const nlohmann::json& dataJson = asyncResp->res.jsonValue["data"];
+
+ for (const auto& [path, interface_map] : *subtree)
+ {
+ if (path == objectPath)
+ {
+ // An enumerate does not return the target path's properties
+ continue;
+ }
+ if (!dataJson.contains(path))
+ {
+ for (const auto& [service, interfaces] : interface_map)
+ {
+ for (const auto& interface : interfaces)
+ {
+ if (!interface.starts_with("org.freedesktop.DBus"))
+ {
+ getPropertiesForEnumerate(path, service, interface,
+ asyncResp);
+ }
+ }
+ }
+ }
+ }
+}
+
+struct InProgressEnumerateData
+{
+ InProgressEnumerateData(
+ const std::string& objectPathIn,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncRespIn) :
+ objectPath(objectPathIn), asyncResp(asyncRespIn)
+ {}
+
+ ~InProgressEnumerateData()
+ {
+ try
+ {
+ findRemainingObjectsForEnumerate(objectPath, subtree, asyncResp);
+ }
+ catch (...)
+ {
+ BMCWEB_LOG_CRITICAL(
+ "findRemainingObjectsForEnumerate threw exception");
+ }
+ }
+
+ InProgressEnumerateData(const InProgressEnumerateData&) = delete;
+ InProgressEnumerateData(InProgressEnumerateData&&) = delete;
+ InProgressEnumerateData& operator=(const InProgressEnumerateData&) = delete;
+ InProgressEnumerateData& operator=(InProgressEnumerateData&&) = delete;
+ const std::string objectPath;
+ std::shared_ptr<dbus::utility::MapperGetSubTreeResponse> subtree;
+ std::shared_ptr<bmcweb::AsyncResp> asyncResp;
+};
+
+inline void getManagedObjectsForEnumerate(
+ const std::string& objectName, const std::string& objectManagerPath,
+ const std::string& connectionName,
+ const std::shared_ptr<InProgressEnumerateData>& transaction)
+{
+ BMCWEB_LOG_DEBUG(
+ "getManagedObjectsForEnumerate {} object_manager_path {} connection_name {}",
+ objectName, objectManagerPath, connectionName);
+ sdbusplus::message::object_path path(objectManagerPath);
+ dbus::utility::getManagedObjects(
+ connectionName, path,
+ [transaction, objectName,
+ connectionName](const boost::system::error_code& ec,
+ const dbus::utility::ManagedObjectType& objects) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "GetManagedObjects on path {} on connection {} failed with code {}",
+ objectName, connectionName, ec);
+ return;
+ }
+
+ nlohmann::json& dataJson =
+ transaction->asyncResp->res.jsonValue["data"];
+
+ for (const auto& objectPath : objects)
+ {
+ if (objectPath.first.str.starts_with(objectName))
+ {
+ BMCWEB_LOG_DEBUG("Reading object {}", objectPath.first.str);
+ nlohmann::json& objectJson = dataJson[objectPath.first.str];
+ if (objectJson.is_null())
+ {
+ objectJson = nlohmann::json::object();
+ }
+ for (const auto& interface : objectPath.second)
+ {
+ for (const auto& property : interface.second)
+ {
+ nlohmann::json& propertyJson =
+ objectJson[property.first];
+ std::visit(
+ [&propertyJson](auto&& val) {
+ if constexpr (
+ std::is_same_v<
+ std::decay_t<decltype(val)>,
+ sdbusplus::message::unix_fd>)
+ {
+ propertyJson = val.fd;
+ }
+ else
+ {
+ propertyJson = val;
+ }
+ },
+ property.second);
+ }
+ }
+ }
+ for (const auto& interface : objectPath.second)
+ {
+ if (interface.first == "org.freedesktop.DBus.ObjectManager")
+ {
+ getManagedObjectsForEnumerate(
+ objectPath.first.str, objectPath.first.str,
+ connectionName, transaction);
+ }
+ }
+ }
+ });
+}
+
+inline void findObjectManagerPathForEnumerate(
+ const std::string& objectName, const std::string& connectionName,
+ const std::shared_ptr<InProgressEnumerateData>& transaction)
+{
+ BMCWEB_LOG_DEBUG("Finding objectmanager for path {} on connection:{}",
+ objectName, connectionName);
+ dbus::utility::async_method_call(
+ [transaction, objectName, connectionName](
+ const boost::system::error_code& ec,
+ const dbus::utility::MapperGetAncestorsResponse& objects) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("GetAncestors on path {} failed with code {}",
+ objectName, ec);
+ return;
+ }
+
+ for (const auto& pathGroup : objects)
+ {
+ for (const auto& connectionGroup : pathGroup.second)
+ {
+ if (connectionGroup.first == connectionName)
+ {
+ // Found the object manager path for this resource.
+ getManagedObjectsForEnumerate(
+ objectName, pathGroup.first, connectionName,
+ transaction);
+ return;
+ }
+ }
+ }
+ },
+ "xyz.openbmc_project.ObjectMapper",
+ "/xyz/openbmc_project/object_mapper",
+ "xyz.openbmc_project.ObjectMapper", "GetAncestors", objectName,
+ std::array<const char*, 1>{"org.freedesktop.DBus.ObjectManager"});
+}
+
+// Uses GetObject to add the object info about the target /enumerate path to
+// the results of GetSubTree, as GetSubTree will not return info for the
+// target path, and then continues on enumerating the rest of the tree.
+inline void getObjectAndEnumerate(
+ const std::shared_ptr<InProgressEnumerateData>& transaction)
+{
+ dbus::utility::getDbusObject(
+ transaction->objectPath, {},
+ [transaction](const boost::system::error_code& ec,
+ const dbus::utility::MapperGetObject& objects) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("GetObject for path {} failed with code {}",
+ transaction->objectPath, ec);
+ return;
+ }
+
+ BMCWEB_LOG_DEBUG("GetObject for {} has {} entries",
+ transaction->objectPath, objects.size());
+ if (!objects.empty())
+ {
+ transaction->subtree->emplace_back(transaction->objectPath,
+ objects);
+ }
+
+ // Map indicating connection name, and the path where the object
+ // manager exists
+ boost::container::flat_map<
+ std::string, std::string, std::less<>,
+ std::vector<std::pair<std::string, std::string>>>
+ connections;
+
+ for (const auto& object : *(transaction->subtree))
+ {
+ for (const auto& connection : object.second)
+ {
+ for (const auto& interface : connection.second)
+ {
+ BMCWEB_LOG_DEBUG("{} has interface {}",
+ connection.first, interface);
+ if (interface == "org.freedesktop.DBus.ObjectManager")
+ {
+ BMCWEB_LOG_DEBUG("found object manager path {}",
+ object.first);
+ connections[connection.first] = object.first;
+ }
+ }
+ }
+ }
+ BMCWEB_LOG_DEBUG("Got {} connections", connections.size());
+
+ for (const auto& connection : connections)
+ {
+ // If we already know where the object manager is, we don't
+ // need to search for it, we can call directly in to
+ // getManagedObjects
+ if (!connection.second.empty())
+ {
+ getManagedObjectsForEnumerate(
+ transaction->objectPath, connection.second,
+ connection.first, transaction);
+ }
+ else
+ {
+ // otherwise we need to find the object manager path
+ // before we can continue
+ findObjectManagerPathForEnumerate(
+ transaction->objectPath, connection.first, transaction);
+ }
+ }
+ });
+}
+
+// Structure for storing data on an in progress action
+struct InProgressActionData
+{
+ explicit InProgressActionData(
+ const std::shared_ptr<bmcweb::AsyncResp>& res) : asyncResp(res)
+ {}
+ ~InProgressActionData()
+ {
+ // Methods could have been called across different owners
+ // and interfaces, where some calls failed and some passed.
+ //
+ // The rules for this are:
+ // * if no method was called - error
+ // * if a method failed and none passed - error
+ // (converse: if at least one method passed - OK)
+ // * for the method output:
+ // * if output processing didn't fail, return the data
+
+ // Only deal with method returns if nothing failed earlier
+ if (asyncResp->res.result() == boost::beast::http::status::ok)
+ {
+ if (!methodPassed)
+ {
+ if (!methodFailed)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::not_found,
+ methodNotFoundDesc, notFoundMsg);
+ }
+ }
+ else
+ {
+ if (outputFailed)
+ {
+ setErrorResponse(
+ asyncResp->res,
+ boost::beast::http::status::internal_server_error,
+ "Method output failure", methodOutputFailedMsg);
+ }
+ else
+ {
+ asyncResp->res.jsonValue["status"] = "ok";
+ asyncResp->res.jsonValue["message"] = "200 OK";
+ asyncResp->res.jsonValue["data"] = methodResponse;
+ }
+ }
+ }
+ }
+ InProgressActionData(const InProgressActionData&) = delete;
+ InProgressActionData(InProgressActionData&&) = delete;
+ InProgressActionData& operator=(const InProgressActionData&) = delete;
+ InProgressActionData& operator=(InProgressActionData&&) = delete;
+
+ void setErrorStatus(const std::string& desc)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::bad_request, desc,
+ badReqMsg);
+ }
+ std::shared_ptr<bmcweb::AsyncResp> asyncResp;
+ std::string path;
+ std::string methodName;
+ std::string interfaceName;
+ bool methodPassed = false;
+ bool methodFailed = false;
+ bool outputFailed = false;
+ bool convertedToArray = false;
+ nlohmann::json methodResponse;
+ nlohmann::json arguments;
+};
+
+inline std::vector<std::string> dbusArgSplit(const std::string& string)
+{
+ std::vector<std::string> ret;
+ if (string.empty())
+ {
+ return ret;
+ }
+ ret.emplace_back("");
+ int containerDepth = 0;
+
+ for (std::string::const_iterator character = string.begin();
+ character != string.end(); character++)
+ {
+ ret.back() += *character;
+ switch (*character)
+ {
+ case ('a'):
+ break;
+ case ('('):
+ case ('{'):
+ containerDepth++;
+ break;
+ case ('}'):
+ case (')'):
+ containerDepth--;
+ if (containerDepth == 0)
+ {
+ if (character + 1 != string.end())
+ {
+ ret.emplace_back("");
+ }
+ }
+ break;
+ default:
+ if (containerDepth == 0)
+ {
+ if (character + 1 != string.end())
+ {
+ ret.emplace_back("");
+ }
+ }
+ break;
+ }
+ }
+
+ return ret;
+}
+
+inline int convertJsonToDbus(sd_bus_message* m, const std::string& argType,
+ const nlohmann::json& inputJson)
+{
+ int r = 0;
+ BMCWEB_LOG_DEBUG("Converting {} to type: {}", inputJson, argType);
+ const std::vector<std::string> argTypes = dbusArgSplit(argType);
+
+ // Assume a single object for now.
+ const nlohmann::json* j = &inputJson;
+ nlohmann::json::const_iterator jIt = inputJson.begin();
+
+ for (const std::string& argCode : argTypes)
+ {
+ // If we are decoding multiple objects, grab the pointer to the
+ // iterator, and increment it for the next loop
+ if (argTypes.size() > 1)
+ {
+ if (jIt == inputJson.end())
+ {
+ return -2;
+ }
+ j = &*jIt;
+ jIt++;
+ }
+ const int64_t* intValue = j->get_ptr<const int64_t*>();
+ const std::string* stringValue = j->get_ptr<const std::string*>();
+ const double* doubleValue = j->get_ptr<const double*>();
+ const bool* b = j->get_ptr<const bool*>();
+ int64_t v = 0;
+ double d = 0.0;
+
+ // Do some basic type conversions that make sense. uint can be
+ // converted to int. int and uint can be converted to double
+ if (intValue == nullptr)
+ {
+ const uint64_t* uintValue = j->get_ptr<const uint64_t*>();
+ if (uintValue != nullptr)
+ {
+ v = static_cast<int64_t>(*uintValue);
+ intValue = &v;
+ }
+ }
+ if (doubleValue == nullptr)
+ {
+ const uint64_t* uintValue = j->get_ptr<const uint64_t*>();
+ if (uintValue != nullptr)
+ {
+ d = static_cast<double>(*uintValue);
+ doubleValue = &d;
+ }
+ }
+ if (doubleValue == nullptr)
+ {
+ if (intValue != nullptr)
+ {
+ d = static_cast<double>(*intValue);
+ doubleValue = &d;
+ }
+ }
+
+ if (argCode == "s")
+ {
+ if (stringValue == nullptr)
+ {
+ return -1;
+ }
+ r = sd_bus_message_append_basic(
+ m, argCode[0], static_cast<const void*>(stringValue->data()));
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (argCode == "i")
+ {
+ if (intValue == nullptr)
+ {
+ return -1;
+ }
+ if ((*intValue < std::numeric_limits<int32_t>::lowest()) ||
+ (*intValue > std::numeric_limits<int32_t>::max()))
+ {
+ return -ERANGE;
+ }
+ int32_t i = static_cast<int32_t>(*intValue);
+ r = sd_bus_message_append_basic(m, argCode[0], &i);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (argCode == "b")
+ {
+ // lots of ways bool could be represented here. Try them all
+ int boolInt = 0;
+ if (intValue != nullptr)
+ {
+ if (*intValue == 1)
+ {
+ boolInt = 1;
+ }
+ else if (*intValue == 0)
+ {
+ boolInt = 0;
+ }
+ else
+ {
+ return -ERANGE;
+ }
+ }
+ else if (b != nullptr)
+ {
+ boolInt = *b ? 1 : 0;
+ }
+ else if (stringValue != nullptr)
+ {
+ if (!stringValue->empty())
+ {
+ if (stringValue->front() == 't' ||
+ stringValue->front() == 'T')
+ {
+ boolInt = 1;
+ }
+ }
+ }
+ else
+ {
+ return -1;
+ }
+ r = sd_bus_message_append_basic(m, argCode[0], &boolInt);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (argCode == "n")
+ {
+ if (intValue == nullptr)
+ {
+ return -1;
+ }
+ if ((*intValue < std::numeric_limits<int16_t>::lowest()) ||
+ (*intValue > std::numeric_limits<int16_t>::max()))
+ {
+ return -ERANGE;
+ }
+ int16_t n = static_cast<int16_t>(*intValue);
+ r = sd_bus_message_append_basic(m, argCode[0], &n);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (argCode == "x")
+ {
+ if (intValue == nullptr)
+ {
+ return -1;
+ }
+ r = sd_bus_message_append_basic(m, argCode[0], intValue);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (argCode == "y")
+ {
+ const uint64_t* uintValue = j->get_ptr<const uint64_t*>();
+ if (uintValue == nullptr)
+ {
+ return -1;
+ }
+ if (*uintValue > std::numeric_limits<uint8_t>::max())
+ {
+ return -ERANGE;
+ }
+ uint8_t y = static_cast<uint8_t>(*uintValue);
+ r = sd_bus_message_append_basic(m, argCode[0], &y);
+ }
+ else if (argCode == "q")
+ {
+ const uint64_t* uintValue = j->get_ptr<const uint64_t*>();
+ if (uintValue == nullptr)
+ {
+ return -1;
+ }
+ if (*uintValue > std::numeric_limits<uint16_t>::max())
+ {
+ return -ERANGE;
+ }
+ uint16_t q = static_cast<uint16_t>(*uintValue);
+ r = sd_bus_message_append_basic(m, argCode[0], &q);
+ }
+ else if (argCode == "u")
+ {
+ const uint64_t* uintValue = j->get_ptr<const uint64_t*>();
+ if (uintValue == nullptr)
+ {
+ return -1;
+ }
+ if (*uintValue > std::numeric_limits<uint32_t>::max())
+ {
+ return -ERANGE;
+ }
+ uint32_t u = static_cast<uint32_t>(*uintValue);
+ r = sd_bus_message_append_basic(m, argCode[0], &u);
+ }
+ else if (argCode == "t")
+ {
+ const uint64_t* uintValue = j->get_ptr<const uint64_t*>();
+ if (uintValue == nullptr)
+ {
+ return -1;
+ }
+ r = sd_bus_message_append_basic(m, argCode[0], uintValue);
+ }
+ else if (argCode == "d")
+ {
+ if (doubleValue == nullptr)
+ {
+ return -1;
+ }
+ if ((*doubleValue < std::numeric_limits<double>::lowest()) ||
+ (*doubleValue > std::numeric_limits<double>::max()))
+ {
+ return -ERANGE;
+ }
+ r = sd_bus_message_append_basic(m, argCode[0], doubleValue);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (argCode.starts_with("a"))
+ {
+ std::string containedType = argCode.substr(1);
+ r = sd_bus_message_open_container(m, SD_BUS_TYPE_ARRAY,
+ containedType.c_str());
+ if (r < 0)
+ {
+ return r;
+ }
+
+ for (const auto& it : *j)
+ {
+ r = convertJsonToDbus(m, containedType, it);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ sd_bus_message_close_container(m);
+ }
+ else if (argCode.starts_with("v"))
+ {
+ std::string containedType = argCode.substr(1);
+ BMCWEB_LOG_DEBUG("variant type: {} appending variant of type: {}",
+ argCode, containedType);
+ r = sd_bus_message_open_container(m, SD_BUS_TYPE_VARIANT,
+ containedType.c_str());
+ if (r < 0)
+ {
+ return r;
+ }
+
+ r = convertJsonToDbus(m, containedType, inputJson);
+ if (r < 0)
+ {
+ return r;
+ }
+
+ r = sd_bus_message_close_container(m);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (argCode.starts_with("(") && argCode.ends_with(")"))
+ {
+ std::string containedType = argCode.substr(1, argCode.size() - 2);
+ r = sd_bus_message_open_container(m, SD_BUS_TYPE_STRUCT,
+ containedType.c_str());
+ if (r < 0)
+ {
+ return r;
+ }
+
+ nlohmann::json::const_iterator it = j->begin();
+ for (const std::string& argCode2 : dbusArgSplit(containedType))
+ {
+ if (it == j->end())
+ {
+ return -1;
+ }
+ r = convertJsonToDbus(m, argCode2, *it);
+ if (r < 0)
+ {
+ return r;
+ }
+ it++;
+ }
+ r = sd_bus_message_close_container(m);
+ }
+ else if (argCode.starts_with("{") && argCode.ends_with("}"))
+ {
+ std::string containedType = argCode.substr(1, argCode.size() - 2);
+ r = sd_bus_message_open_container(m, SD_BUS_TYPE_DICT_ENTRY,
+ containedType.c_str());
+ if (r < 0)
+ {
+ return r;
+ }
+
+ std::vector<std::string> codes = dbusArgSplit(containedType);
+ if (codes.size() != 2)
+ {
+ return -1;
+ }
+ const std::string& keyType = codes[0];
+ const std::string& valueType = codes[1];
+ const nlohmann::json::object_t* arr =
+ j->get_ptr<const nlohmann::json::object_t*>();
+ if (arr == nullptr)
+ {
+ return -1;
+ }
+ for (const auto& it : *arr)
+ {
+ r = convertJsonToDbus(m, keyType, it.first);
+ if (r < 0)
+ {
+ return r;
+ }
+
+ r = convertJsonToDbus(m, valueType, it.second);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ r = sd_bus_message_close_container(m);
+ }
+ else
+ {
+ return -2;
+ }
+ if (r < 0)
+ {
+ return r;
+ }
+
+ if (argTypes.size() > 1)
+ {
+ jIt++;
+ }
+ }
+
+ return r;
+}
+
+template <typename T>
+int readMessageItem(const std::string& typeCode, sdbusplus::message_t& m,
+ nlohmann::json& data)
+{
+ T value;
+ // When T == char*, this warning fires. Unclear how to resolve
+ // Given that sd-bus takes a void pointer to a char*, and that's
+ // Not something we can fix.
+ // NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
+ int r = sd_bus_message_read_basic(m.get(), typeCode.front(), &value);
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_read_basic on type {} failed!",
+ typeCode);
+ return r;
+ }
+
+ data = value;
+ return 0;
+}
+
+int convertDBusToJSON(const std::string& returnType, sdbusplus::message_t& m,
+ nlohmann::json& response);
+
+inline int readDictEntryFromMessage(const std::string& typeCode,
+ sdbusplus::message_t& m,
+ nlohmann::json& object)
+{
+ std::vector<std::string> types = dbusArgSplit(typeCode);
+ if (types.size() != 2)
+ {
+ BMCWEB_LOG_ERROR("wrong number contained types in dictionary: {}",
+ types.size());
+ return -1;
+ }
+
+ int r = sd_bus_message_enter_container(m.get(), SD_BUS_TYPE_DICT_ENTRY,
+ typeCode.c_str());
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_enter_container with rc {}", r);
+ return r;
+ }
+
+ nlohmann::json key;
+ r = convertDBusToJSON(types[0], m, key);
+ if (r < 0)
+ {
+ return r;
+ }
+
+ const std::string* keyPtr = key.get_ptr<const std::string*>();
+ if (keyPtr == nullptr)
+ {
+ // json doesn't support non-string keys. If we hit this condition,
+ // convert the result to a string so we can proceed
+ key = key.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
+ keyPtr = key.get_ptr<const std::string*>();
+ // in theory this can't fail now, but lets be paranoid about it
+ // anyway
+ if (keyPtr == nullptr)
+ {
+ return -1;
+ }
+ }
+ nlohmann::json& value = object[*keyPtr];
+
+ r = convertDBusToJSON(types[1], m, value);
+ if (r < 0)
+ {
+ return r;
+ }
+
+ r = sd_bus_message_exit_container(m.get());
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_exit_container failed");
+ return r;
+ }
+
+ return 0;
+}
+
+inline int readArrayFromMessage(const std::string& typeCode,
+ sdbusplus::message_t& m, nlohmann::json& data)
+{
+ if (typeCode.size() < 2)
+ {
+ BMCWEB_LOG_ERROR("Type code {} too small for an array", typeCode);
+ return -1;
+ }
+
+ std::string containedType = typeCode.substr(1);
+
+ int r = sd_bus_message_enter_container(m.get(), SD_BUS_TYPE_ARRAY,
+ containedType.c_str());
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_enter_container failed with rc {}", r);
+ return r;
+ }
+
+ bool dict = containedType.starts_with("{") && containedType.ends_with("}");
+
+ if (dict)
+ {
+ // Remove the { }
+ containedType = containedType.substr(1, containedType.size() - 2);
+ data = nlohmann::json::object();
+ }
+ else
+ {
+ data = nlohmann::json::array();
+ }
+
+ while (true)
+ {
+ r = sd_bus_message_at_end(m.get(), 0);
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_at_end failed");
+ return r;
+ }
+
+ if (r > 0)
+ {
+ break;
+ }
+
+ // Dictionaries are only ever seen in an array
+ if (dict)
+ {
+ r = readDictEntryFromMessage(containedType, m, data);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else
+ {
+ data.push_back(nlohmann::json());
+
+ r = convertDBusToJSON(containedType, m, data.back());
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ }
+
+ r = sd_bus_message_exit_container(m.get());
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_exit_container failed");
+ return r;
+ }
+
+ return 0;
+}
+
+inline int readStructFromMessage(const std::string& typeCode,
+ sdbusplus::message_t& m, nlohmann::json& data)
+{
+ if (typeCode.size() < 3)
+ {
+ BMCWEB_LOG_ERROR("Type code {} too small for a struct", typeCode);
+ return -1;
+ }
+
+ std::string containedTypes = typeCode.substr(1, typeCode.size() - 2);
+ std::vector<std::string> types = dbusArgSplit(containedTypes);
+
+ int r = sd_bus_message_enter_container(m.get(), SD_BUS_TYPE_STRUCT,
+ containedTypes.c_str());
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_enter_container failed with rc {}", r);
+ return r;
+ }
+
+ for (const std::string& type : types)
+ {
+ data.push_back(nlohmann::json());
+ r = convertDBusToJSON(type, m, data.back());
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+
+ r = sd_bus_message_exit_container(m.get());
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_exit_container failed");
+ return r;
+ }
+ return 0;
+}
+
+inline int readVariantFromMessage(sdbusplus::message_t& m, nlohmann::json& data)
+{
+ const char* containerType = nullptr;
+ int r = sd_bus_message_peek_type(m.get(), nullptr, &containerType);
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_peek_type failed");
+ return r;
+ }
+
+ r = sd_bus_message_enter_container(m.get(), SD_BUS_TYPE_VARIANT,
+ containerType);
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_enter_container failed with rc {}", r);
+ return r;
+ }
+
+ r = convertDBusToJSON(containerType, m, data);
+ if (r < 0)
+ {
+ return r;
+ }
+
+ r = sd_bus_message_exit_container(m.get());
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR("sd_bus_message_enter_container failed");
+ return r;
+ }
+
+ return 0;
+}
+
+inline int convertDBusToJSON(const std::string& returnType,
+ sdbusplus::message_t& m, nlohmann::json& response)
+{
+ int r = 0;
+ const std::vector<std::string> returnTypes = dbusArgSplit(returnType);
+
+ for (const std::string& typeCode : returnTypes)
+ {
+ nlohmann::json* thisElement = &response;
+ if (returnTypes.size() > 1)
+ {
+ response.push_back(nlohmann::json{});
+ thisElement = &response.back();
+ }
+
+ if (typeCode == "s" || typeCode == "g" || typeCode == "o")
+ {
+ r = readMessageItem<char*>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "b")
+ {
+ r = readMessageItem<int>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+
+ *thisElement = static_cast<bool>(thisElement->get<int>());
+ }
+ else if (typeCode == "u")
+ {
+ r = readMessageItem<uint32_t>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "i")
+ {
+ r = readMessageItem<int32_t>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "x")
+ {
+ r = readMessageItem<int64_t>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "t")
+ {
+ r = readMessageItem<uint64_t>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "n")
+ {
+ r = readMessageItem<int16_t>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "q")
+ {
+ r = readMessageItem<uint16_t>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "y")
+ {
+ r = readMessageItem<uint8_t>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "d")
+ {
+ r = readMessageItem<double>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode == "h")
+ {
+ r = readMessageItem<int>(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode.starts_with("a"))
+ {
+ r = readArrayFromMessage(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode.starts_with("(") && typeCode.ends_with(")"))
+ {
+ r = readStructFromMessage(typeCode, m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else if (typeCode.starts_with("v"))
+ {
+ r = readVariantFromMessage(m, *thisElement);
+ if (r < 0)
+ {
+ return r;
+ }
+ }
+ else
+ {
+ BMCWEB_LOG_ERROR("Invalid D-Bus signature type {}", typeCode);
+ return -2;
+ }
+ }
+
+ return 0;
+}
+
+inline void handleMethodResponse(
+ const std::shared_ptr<InProgressActionData>& transaction,
+ sdbusplus::message_t& m, const std::string& returnType)
+{
+ nlohmann::json data;
+
+ int r = convertDBusToJSON(returnType, m, data);
+ if (r < 0)
+ {
+ transaction->outputFailed = true;
+ return;
+ }
+
+ if (data.is_null())
+ {
+ return;
+ }
+
+ if (transaction->methodResponse.is_null())
+ {
+ transaction->methodResponse = std::move(data);
+ return;
+ }
+
+ // If they're both dictionaries or arrays, merge into one.
+ // Otherwise, make the results an array with every result
+ // an entry. Could also just fail in that case, but it
+ // seems better to get the data back somehow.
+ nlohmann::json::object_t* dataobj =
+ data.get_ptr<nlohmann::json::object_t*>();
+
+ nlohmann::json::object_t* methodResponseObj =
+ transaction->methodResponse.get_ptr<nlohmann::json::object_t*>();
+ if (methodResponseObj != nullptr && dataobj != nullptr)
+ {
+ for (auto& obj : *dataobj)
+ {
+ // Note: Will overwrite the data for a duplicate key
+ methodResponseObj->emplace(obj.first, std::move(obj.second));
+ }
+ return;
+ }
+
+ nlohmann::json::array_t* dataarr = data.get_ptr<nlohmann::json::array_t*>();
+ nlohmann::json::array_t* methodResponseArr =
+ transaction->methodResponse.get_ptr<nlohmann::json::array_t*>();
+ if (methodResponseArr != nullptr && dataarr != nullptr)
+ {
+ for (auto& obj : *dataarr)
+ {
+ methodResponseArr->emplace_back(std::move(obj));
+ }
+ return;
+ }
+
+ if (!transaction->convertedToArray)
+ {
+ // They are different types. May as well turn them into an array
+ nlohmann::json j = std::move(transaction->methodResponse);
+ transaction->methodResponse = nlohmann::json::array();
+ transaction->methodResponse.emplace_back(std::move(j));
+ transaction->methodResponse.emplace_back(std::move(data));
+ transaction->convertedToArray = true;
+ }
+ else
+ {
+ transaction->methodResponse.emplace_back(std::move(data));
+ }
+}
+
+inline void findActionOnInterface(
+ const std::shared_ptr<InProgressActionData>& transaction,
+ const std::string& connectionName)
+{
+ BMCWEB_LOG_DEBUG("findActionOnInterface for connection {}", connectionName);
+ dbus::utility::async_method_call(
+ [transaction, connectionName{std::string(connectionName)}](
+ const boost::system::error_code& ec,
+ const std::string& introspectXml) {
+ BMCWEB_LOG_DEBUG("got xml:\n {}", introspectXml);
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "Introspect call failed with error: {} on process: {}",
+ ec.message(), connectionName);
+ return;
+ }
+ tinyxml2::XMLDocument doc;
+
+ doc.Parse(introspectXml.data(), introspectXml.size());
+ tinyxml2::XMLNode* pRoot = doc.FirstChildElement("node");
+ if (pRoot == nullptr)
+ {
+ BMCWEB_LOG_ERROR("XML document failed to parse {}",
+ connectionName);
+ return;
+ }
+ tinyxml2::XMLElement* interfaceNode =
+ pRoot->FirstChildElement("interface");
+ while (interfaceNode != nullptr)
+ {
+ const char* thisInterfaceName =
+ interfaceNode->Attribute("name");
+ if (thisInterfaceName != nullptr)
+ {
+ if (!transaction->interfaceName.empty() &&
+ (transaction->interfaceName != thisInterfaceName))
+ {
+ interfaceNode =
+ interfaceNode->NextSiblingElement("interface");
+ continue;
+ }
+
+ tinyxml2::XMLElement* methodNode =
+ interfaceNode->FirstChildElement("method");
+ while (methodNode != nullptr)
+ {
+ const char* thisMethodName =
+ methodNode->Attribute("name");
+ BMCWEB_LOG_DEBUG("Found method: {}", thisMethodName);
+ if (thisMethodName != nullptr &&
+ thisMethodName == transaction->methodName)
+ {
+ BMCWEB_LOG_DEBUG(
+ "Found method named {} on interface {}",
+ thisMethodName, thisInterfaceName);
+ sdbusplus::message_t m =
+ crow::connections::systemBus->new_method_call(
+ connectionName.c_str(),
+ transaction->path.c_str(),
+ thisInterfaceName,
+ transaction->methodName.c_str());
+
+ tinyxml2::XMLElement* argumentNode =
+ methodNode->FirstChildElement("arg");
+
+ std::string returnType;
+
+ // Find the output type
+ while (argumentNode != nullptr)
+ {
+ const char* argDirection =
+ argumentNode->Attribute("direction");
+ const char* argType =
+ argumentNode->Attribute("type");
+ if (argDirection != nullptr &&
+ argType != nullptr &&
+ std::string(argDirection) == "out")
+ {
+ returnType = argType;
+ break;
+ }
+ argumentNode =
+ argumentNode->NextSiblingElement("arg");
+ }
+
+ auto argIt = transaction->arguments.begin();
+
+ argumentNode = methodNode->FirstChildElement("arg");
+
+ while (argumentNode != nullptr)
+ {
+ const char* argDirection =
+ argumentNode->Attribute("direction");
+ const char* argType =
+ argumentNode->Attribute("type");
+ if (argDirection != nullptr &&
+ argType != nullptr &&
+ std::string(argDirection) == "in")
+ {
+ if (argIt == transaction->arguments.end())
+ {
+ transaction->setErrorStatus(
+ "Invalid method args");
+ return;
+ }
+ if (convertJsonToDbus(m.get(),
+ std::string(argType),
+ *argIt) < 0)
+ {
+ transaction->setErrorStatus(
+ "Invalid method arg type");
+ return;
+ }
+
+ argIt++;
+ }
+ argumentNode =
+ argumentNode->NextSiblingElement("arg");
+ }
+
+ crow::connections::systemBus->async_send(
+ m, [transaction, returnType](
+ const boost::system::error_code& ec2,
+ sdbusplus::message_t& m2) {
+ if (ec2)
+ {
+ transaction->methodFailed = true;
+ const sd_bus_error* e = m2.get_error();
+
+ if (e != nullptr)
+ {
+ setErrorResponse(
+ transaction->asyncResp->res,
+ boost::beast::http::status::
+ bad_request,
+ e->name, e->message);
+ }
+ else
+ {
+ setErrorResponse(
+ transaction->asyncResp->res,
+ boost::beast::http::status::
+ bad_request,
+ "Method call failed",
+ methodFailedMsg);
+ }
+ return;
+ }
+ transaction->methodPassed = true;
+
+ handleMethodResponse(transaction, m2,
+ returnType);
+ });
+ break;
+ }
+ methodNode = methodNode->NextSiblingElement("method");
+ }
+ }
+ interfaceNode = interfaceNode->NextSiblingElement("interface");
+ }
+ },
+ connectionName, transaction->path,
+ "org.freedesktop.DBus.Introspectable", "Introspect");
+}
+
+inline void handleAction(const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& objectPath,
+ const std::string& methodName)
+{
+ BMCWEB_LOG_DEBUG("handleAction on path: {} and method {}", objectPath,
+ methodName);
+ nlohmann::json requestDbusData;
+
+ JsonParseResult ret = parseRequestAsJson(req, requestDbusData);
+ if (ret == JsonParseResult::BadContentType)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::unsupported_media_type,
+ invalidContentType, unsupportedMediaMsg);
+ return;
+ }
+ if (ret != JsonParseResult::Success)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::bad_request, noJsonDesc,
+ badReqMsg);
+ return;
+ }
+ nlohmann::json::iterator data = requestDbusData.find("data");
+ if (data == requestDbusData.end())
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::bad_request, noJsonDesc,
+ badReqMsg);
+ return;
+ }
+
+ if (!data->is_array())
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::bad_request, noJsonDesc,
+ badReqMsg);
+ return;
+ }
+ auto transaction = std::make_shared<InProgressActionData>(asyncResp);
+
+ transaction->path = objectPath;
+ transaction->methodName = methodName;
+ transaction->arguments = std::move(*data);
+ dbus::utility::getDbusObject(
+ objectPath, {},
+ [transaction](
+ const boost::system::error_code& ec,
+ const std::vector<std::pair<std::string, std::vector<std::string>>>&
+ interfaceNames) {
+ if (ec || interfaceNames.empty())
+ {
+ BMCWEB_LOG_ERROR("Can't find object");
+ setErrorResponse(transaction->asyncResp->res,
+ boost::beast::http::status::not_found,
+ notFoundDesc, notFoundMsg);
+ return;
+ }
+
+ BMCWEB_LOG_DEBUG("GetObject returned {} object(s)",
+ interfaceNames.size());
+
+ for (const std::pair<std::string, std::vector<std::string>>&
+ object : interfaceNames)
+ {
+ findActionOnInterface(transaction, object.first);
+ }
+ });
+}
+
+inline void handleDelete(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& objectPath)
+{
+ BMCWEB_LOG_DEBUG("handleDelete on path: {}", objectPath);
+
+ dbus::utility::getDbusObject(
+ objectPath, {},
+ [asyncResp, objectPath](
+ const boost::system::error_code& ec,
+ const std::vector<std::pair<std::string, std::vector<std::string>>>&
+ interfaceNames) {
+ if (ec || interfaceNames.empty())
+ {
+ BMCWEB_LOG_ERROR("Can't find object");
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::method_not_allowed,
+ methodNotAllowedDesc, methodNotAllowedMsg);
+ return;
+ }
+
+ auto transaction =
+ std::make_shared<InProgressActionData>(asyncResp);
+ transaction->path = objectPath;
+ transaction->methodName = "Delete";
+ transaction->interfaceName = "xyz.openbmc_project.Object.Delete";
+
+ for (const std::pair<std::string, std::vector<std::string>>&
+ object : interfaceNames)
+ {
+ findActionOnInterface(transaction, object.first);
+ }
+ });
+}
+
+inline void handleList(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& objectPath, int32_t depth = 0)
+{
+ dbus::utility::getSubTreePaths(
+ objectPath, depth, {},
+ [asyncResp](
+ const boost::system::error_code& ec,
+ const dbus::utility::MapperGetSubTreePathsResponse& objectPaths) {
+ if (ec)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::not_found,
+ notFoundDesc, notFoundMsg);
+ }
+ else
+ {
+ asyncResp->res.jsonValue["status"] = "ok";
+ asyncResp->res.jsonValue["message"] = "200 OK";
+ asyncResp->res.jsonValue["data"] = objectPaths;
+ }
+ });
+}
+
+inline void handleEnumerate(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& objectPath)
+{
+ BMCWEB_LOG_DEBUG("Doing enumerate on {}", objectPath);
+
+ asyncResp->res.jsonValue["message"] = "200 OK";
+ asyncResp->res.jsonValue["status"] = "ok";
+ asyncResp->res.jsonValue["data"] = nlohmann::json::object();
+
+ dbus::utility::getSubTree(
+ objectPath, 0, {},
+ [objectPath, asyncResp](
+ const boost::system::error_code& ec,
+ const dbus::utility::MapperGetSubTreeResponse& objectNames) {
+ auto transaction = std::make_shared<InProgressEnumerateData>(
+ objectPath, asyncResp);
+
+ transaction->subtree =
+ std::make_shared<dbus::utility::MapperGetSubTreeResponse>(
+ objectNames);
+
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("GetSubTree failed on {}",
+ transaction->objectPath);
+ setErrorResponse(transaction->asyncResp->res,
+ boost::beast::http::status::not_found,
+ notFoundDesc, notFoundMsg);
+ return;
+ }
+
+ // Add the data for the path passed in to the results
+ // as if GetSubTree returned it, and continue on enumerating
+ getObjectAndEnumerate(transaction);
+ });
+}
+
+inline void handleGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ std::string& objectPath, std::string& destProperty)
+{
+ BMCWEB_LOG_DEBUG("handleGet: {} prop:{}", objectPath, destProperty);
+ std::shared_ptr<std::string> propertyName =
+ std::make_shared<std::string>(std::move(destProperty));
+
+ std::shared_ptr<std::string> path =
+ std::make_shared<std::string>(std::move(objectPath));
+
+ dbus::utility::getDbusObject(
+ *path, {},
+ [asyncResp, path,
+ propertyName](const boost::system::error_code& ec,
+ const dbus::utility::MapperGetObject& objectNames) {
+ if (ec || objectNames.empty())
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::not_found,
+ notFoundDesc, notFoundMsg);
+ return;
+ }
+ std::shared_ptr<nlohmann::json> response =
+ std::make_shared<nlohmann::json>(nlohmann::json::object());
+ for (const std::pair<std::string, std::vector<std::string>>&
+ connection : objectNames)
+ {
+ const std::vector<std::string>& interfaceNames =
+ connection.second;
+
+ if (interfaceNames.empty())
+ {
+ // mapper allows empty interfaces in case an
+ // object does not implement any interface.
+ continue;
+ }
+
+ for (const std::string& interface : interfaceNames)
+ {
+ sdbusplus::message_t m =
+ crow::connections::systemBus->new_method_call(
+ connection.first.c_str(), path->c_str(),
+ "org.freedesktop.DBus.Properties", "GetAll");
+ m.append(interface);
+ crow::connections::systemBus->async_send(
+ m, [asyncResp, response,
+ propertyName](const boost::system::error_code& ec2,
+ sdbusplus::message_t& msg) {
+ if (ec2)
+ {
+ BMCWEB_LOG_ERROR("Bad dbus request error: {}",
+ ec2);
+ }
+ else
+ {
+ nlohmann::json properties;
+ int r =
+ convertDBusToJSON("a{sv}", msg, properties);
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR(
+ "convertDBusToJSON failed");
+ }
+ else
+ {
+ nlohmann::json::object_t* obj =
+ properties.get_ptr<
+ nlohmann::json::object_t*>();
+ if (obj == nullptr)
+ {
+ return;
+ }
+ for (auto& prop : *obj)
+ {
+ // if property name is empty, or
+ // matches our search query, add it
+ // to the response json
+
+ if (propertyName->empty())
+ {
+ (*response)[prop.first] =
+ std::move(prop.second);
+ }
+ else if (prop.first == *propertyName)
+ {
+ *response = std::move(prop.second);
+ }
+ }
+ }
+ }
+ if (response.use_count() == 1)
+ {
+ if (!propertyName->empty() && response->empty())
+ {
+ setErrorResponse(
+ asyncResp->res,
+ boost::beast::http::status::not_found,
+ propNotFoundDesc, notFoundMsg);
+ }
+ else
+ {
+ asyncResp->res.jsonValue["status"] = "ok";
+ asyncResp->res.jsonValue["message"] =
+ "200 OK";
+ asyncResp->res.jsonValue["data"] =
+ *response;
+ }
+ }
+ });
+ }
+ }
+ });
+}
+
+struct AsyncPutRequest
+{
+ explicit AsyncPutRequest(const std::shared_ptr<bmcweb::AsyncResp>& resIn) :
+ asyncResp(resIn)
+ {}
+ ~AsyncPutRequest()
+ {
+ if (asyncResp->res.jsonValue.empty())
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::forbidden,
+ forbiddenMsg, forbiddenPropDesc);
+ }
+ }
+
+ AsyncPutRequest(const AsyncPutRequest&) = delete;
+ AsyncPutRequest(AsyncPutRequest&&) = delete;
+ AsyncPutRequest& operator=(const AsyncPutRequest&) = delete;
+ AsyncPutRequest& operator=(AsyncPutRequest&&) = delete;
+
+ void setErrorStatus(const std::string& desc)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::internal_server_error,
+ desc, badReqMsg);
+ }
+
+ const std::shared_ptr<bmcweb::AsyncResp> asyncResp;
+ std::string objectPath;
+ std::string propertyName;
+ nlohmann::json propertyValue;
+};
+
+inline void handlePut(const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& objectPath,
+ const std::string& destProperty)
+{
+ if (destProperty.empty())
+ {
+ setErrorResponse(asyncResp->res, boost::beast::http::status::forbidden,
+ forbiddenResDesc, forbiddenMsg);
+ return;
+ }
+ nlohmann::json requestDbusData;
+
+ JsonParseResult ret = parseRequestAsJson(req, requestDbusData);
+ if (ret == JsonParseResult::BadContentType)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::unsupported_media_type,
+ invalidContentType, unsupportedMediaMsg);
+ return;
+ }
+
+ if (ret != JsonParseResult::Success)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::bad_request, noJsonDesc,
+ badReqMsg);
+ return;
+ }
+
+ auto propertyIt = requestDbusData.find("data");
+ if (propertyIt == requestDbusData.end())
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::bad_request, noJsonDesc,
+ badReqMsg);
+ return;
+ }
+ const nlohmann::json& propertySetValue = *propertyIt;
+ auto transaction = std::make_shared<AsyncPutRequest>(asyncResp);
+ transaction->objectPath = objectPath;
+ transaction->propertyName = destProperty;
+ transaction->propertyValue = propertySetValue;
+
+ dbus::utility::getDbusObject(
+ transaction->objectPath, {},
+ [transaction](const boost::system::error_code& ec2,
+ const dbus::utility::MapperGetObject& objectNames) {
+ if (!ec2 && objectNames.empty())
+ {
+ setErrorResponse(transaction->asyncResp->res,
+ boost::beast::http::status::not_found,
+ propNotFoundDesc, notFoundMsg);
+ return;
+ }
+
+ for (const std::pair<std::string, std::vector<std::string>>&
+ connection : objectNames)
+ {
+ const std::string& connectionName = connection.first;
+
+ dbus::utility::async_method_call(
+ [connectionName{std::string(connectionName)},
+ transaction](const boost::system::error_code& ec3,
+ const std::string& introspectXml) {
+ if (ec3)
+ {
+ BMCWEB_LOG_ERROR(
+ "Introspect call failed with error: {} on process: {}",
+ ec3.message(), connectionName);
+ transaction->setErrorStatus("Unexpected Error");
+ return;
+ }
+ tinyxml2::XMLDocument doc;
+
+ doc.Parse(introspectXml.c_str());
+ tinyxml2::XMLNode* pRoot =
+ doc.FirstChildElement("node");
+ if (pRoot == nullptr)
+ {
+ BMCWEB_LOG_ERROR("XML document failed to parse: {}",
+ introspectXml);
+ transaction->setErrorStatus("Unexpected Error");
+ return;
+ }
+ tinyxml2::XMLElement* ifaceNode =
+ pRoot->FirstChildElement("interface");
+ while (ifaceNode != nullptr)
+ {
+ const char* interfaceName =
+ ifaceNode->Attribute("name");
+ BMCWEB_LOG_DEBUG("found interface {}",
+ interfaceName);
+ tinyxml2::XMLElement* propNode =
+ ifaceNode->FirstChildElement("property");
+ while (propNode != nullptr)
+ {
+ const char* propertyName =
+ propNode->Attribute("name");
+ if (propertyName == nullptr)
+ {
+ BMCWEB_LOG_DEBUG(
+ "Couldn't find name property");
+ continue;
+ }
+ BMCWEB_LOG_DEBUG("Found property {}",
+ propertyName);
+ if (propertyName == transaction->propertyName)
+ {
+ const char* argType =
+ propNode->Attribute("type");
+ if (argType != nullptr)
+ {
+ sdbusplus::message_t m =
+ crow::connections::systemBus
+ ->new_method_call(
+ connectionName.c_str(),
+ transaction->objectPath
+ .c_str(),
+ "org.freedesktop.DBus."
+ "Properties",
+ "Set");
+ m.append(interfaceName,
+ transaction->propertyName);
+ int r = sd_bus_message_open_container(
+ m.get(), SD_BUS_TYPE_VARIANT,
+ argType);
+ if (r < 0)
+ {
+ transaction->setErrorStatus(
+ "Unexpected Error");
+ return;
+ }
+ r = convertJsonToDbus(
+ m.get(), argType,
+ transaction->propertyValue);
+ if (r < 0)
+ {
+ if (r == -ERANGE)
+ {
+ transaction->setErrorStatus(
+ "Provided property value "
+ "is out of range for the "
+ "property type");
+ }
+ else
+ {
+ transaction->setErrorStatus(
+ "Invalid arg type");
+ }
+ return;
+ }
+ r = sd_bus_message_close_container(
+ m.get());
+ if (r < 0)
+ {
+ transaction->setErrorStatus(
+ "Unexpected Error");
+ return;
+ }
+ crow::connections::systemBus
+ ->async_send(
+ m,
+ [transaction](
+ const boost::system::
+ error_code& ec,
+ sdbusplus::message_t& m2) {
+ BMCWEB_LOG_DEBUG("sent");
+ if (ec)
+ {
+ const sd_bus_error* e =
+ m2.get_error();
+ setErrorResponse(
+ transaction
+ ->asyncResp
+ ->res,
+ boost::beast::http::
+ status::
+ forbidden,
+ (e) != nullptr
+ ? e->name
+ : ec.category()
+ .name(),
+ (e) != nullptr
+ ? e->message
+ : ec.message());
+ }
+ else
+ {
+ transaction->asyncResp
+ ->res.jsonValue
+ ["status"] =
+ "ok";
+ transaction->asyncResp
+ ->res.jsonValue
+ ["message"] =
+ "200 OK";
+ transaction->asyncResp
+ ->res
+ .jsonValue["data"] =
+ nullptr;
+ }
+ });
+ }
+ }
+ propNode =
+ propNode->NextSiblingElement("property");
+ }
+ ifaceNode =
+ ifaceNode->NextSiblingElement("interface");
+ }
+ },
+ connectionName, transaction->objectPath,
+ "org.freedesktop.DBus.Introspectable", "Introspect");
+ }
+ });
+}
+
+inline void handleDBusUrl(const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ std::string& objectPath)
+{
+ // If accessing a single attribute, fill in and update objectPath,
+ // otherwise leave destProperty blank
+ std::string destProperty;
+ const char* attrSeperator = "/attr/";
+ size_t attrPosition = objectPath.find(attrSeperator);
+ if (attrPosition != std::string::npos)
+ {
+ destProperty = objectPath.substr(attrPosition + strlen(attrSeperator),
+ objectPath.length());
+ objectPath.resize(attrPosition);
+ }
+
+ if (req.method() == boost::beast::http::verb::post)
+ {
+ constexpr const char* actionSeperator = "/action/";
+ size_t actionPosition = objectPath.find(actionSeperator);
+ if (actionPosition != std::string::npos)
+ {
+ std::string postProperty =
+ objectPath.substr((actionPosition + strlen(actionSeperator)),
+ objectPath.length());
+ objectPath.resize(actionPosition);
+ handleAction(req, asyncResp, objectPath, postProperty);
+ return;
+ }
+ }
+ else if (req.method() == boost::beast::http::verb::get)
+ {
+ if (objectPath.ends_with("/enumerate"))
+ {
+ objectPath.erase(objectPath.end() - sizeof("enumerate"),
+ objectPath.end());
+ handleEnumerate(asyncResp, objectPath);
+ }
+ else if (objectPath.ends_with("/list"))
+ {
+ objectPath.erase(objectPath.end() - sizeof("list"),
+ objectPath.end());
+ handleList(asyncResp, objectPath);
+ }
+ else
+ {
+ // Trim any trailing "/" at the end
+ if (objectPath.ends_with("/"))
+ {
+ objectPath.pop_back();
+ handleList(asyncResp, objectPath, 1);
+ }
+ else
+ {
+ handleGet(asyncResp, objectPath, destProperty);
+ }
+ }
+ return;
+ }
+ else if (req.method() == boost::beast::http::verb::put)
+ {
+ handlePut(req, asyncResp, objectPath, destProperty);
+ return;
+ }
+ else if (req.method() == boost::beast::http::verb::delete_)
+ {
+ handleDelete(asyncResp, objectPath);
+ return;
+ }
+
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::method_not_allowed,
+ methodNotAllowedDesc, methodNotAllowedMsg);
+}
+
+inline void handleBusSystemPost(
+ const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& processName, const std::string& requestedPath)
+{
+ std::vector<std::string> strs;
+
+ bmcweb::split(strs, requestedPath, '/');
+ std::string objectPath;
+ std::string interfaceName;
+ std::string methodName;
+ auto it = strs.begin();
+ if (it == strs.end())
+ {
+ objectPath = "/";
+ }
+ while (it != strs.end())
+ {
+ // Check if segment contains ".". If it does, it must be an
+ // interface
+ if (it->find(".") != std::string::npos)
+ {
+ break;
+ // This check is necessary as the trailing slash gets
+ // parsed as part of our <path> specifier above, which
+ // causes the normal trailing backslash redirector to
+ // fail.
+ }
+ if (!it->empty())
+ {
+ objectPath += "/" + *it;
+ }
+ it++;
+ }
+ if (it != strs.end())
+ {
+ interfaceName = *it;
+ it++;
+
+ // after interface, we might have a method name
+ if (it != strs.end())
+ {
+ methodName = *it;
+ it++;
+ }
+ }
+ if (it != strs.end())
+ {
+ // if there is more levels past the method name, something
+ // went wrong, return not found
+ asyncResp->res.result(boost::beast::http::status::not_found);
+ return;
+ }
+ if (interfaceName.empty())
+ {
+ dbus::utility::async_method_call(
+ [asyncResp, processName,
+ objectPath](const boost::system::error_code& ec,
+ const std::string& introspectXml) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "Introspect call failed with error: {} on process: {} path: {}",
+ ec.message(), processName, objectPath);
+ return;
+ }
+ tinyxml2::XMLDocument doc;
+
+ doc.Parse(introspectXml.c_str());
+ tinyxml2::XMLNode* pRoot = doc.FirstChildElement("node");
+ if (pRoot == nullptr)
+ {
+ BMCWEB_LOG_ERROR("XML document failed to parse {} {}",
+ processName, objectPath);
+ asyncResp->res.jsonValue["status"] = "XML parse error";
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ return;
+ }
+
+ BMCWEB_LOG_DEBUG("{}", introspectXml);
+ asyncResp->res.jsonValue["status"] = "ok";
+ asyncResp->res.jsonValue["bus_name"] = processName;
+ asyncResp->res.jsonValue["object_path"] = objectPath;
+
+ nlohmann::json& interfacesArray =
+ asyncResp->res.jsonValue["interfaces"];
+ interfacesArray = nlohmann::json::array();
+ tinyxml2::XMLElement* interface =
+ pRoot->FirstChildElement("interface");
+
+ while (interface != nullptr)
+ {
+ const char* ifaceName = interface->Attribute("name");
+ if (ifaceName != nullptr)
+ {
+ nlohmann::json::object_t interfaceObj;
+ interfaceObj["name"] = ifaceName;
+ interfacesArray.emplace_back(std::move(interfaceObj));
+ }
+
+ interface = interface->NextSiblingElement("interface");
+ }
+ },
+ processName, objectPath, "org.freedesktop.DBus.Introspectable",
+ "Introspect");
+ }
+ else if (methodName.empty())
+ {
+ dbus::utility::async_method_call(
+ [asyncResp, processName, objectPath,
+ interfaceName](const boost::system::error_code& ec,
+ const std::string& introspectXml) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "Introspect call failed with error: {} on process: {} path: {}",
+ ec.message(), processName, objectPath);
+ return;
+ }
+ tinyxml2::XMLDocument doc;
+
+ doc.Parse(introspectXml.data(), introspectXml.size());
+ tinyxml2::XMLNode* pRoot = doc.FirstChildElement("node");
+ if (pRoot == nullptr)
+ {
+ BMCWEB_LOG_ERROR("XML document failed to parse {} {}",
+ processName, objectPath);
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ return;
+ }
+
+ asyncResp->res.jsonValue["status"] = "ok";
+ asyncResp->res.jsonValue["bus_name"] = processName;
+ asyncResp->res.jsonValue["interface"] = interfaceName;
+ asyncResp->res.jsonValue["object_path"] = objectPath;
+
+ nlohmann::json& methodsArray =
+ asyncResp->res.jsonValue["methods"];
+ methodsArray = nlohmann::json::array();
+
+ nlohmann::json& signalsArray =
+ asyncResp->res.jsonValue["signals"];
+ signalsArray = nlohmann::json::array();
+
+ nlohmann::json& propertiesObj =
+ asyncResp->res.jsonValue["properties"];
+ propertiesObj = nlohmann::json::object();
+
+ // if we know we're the only call, build the
+ // json directly
+ tinyxml2::XMLElement* interface =
+ pRoot->FirstChildElement("interface");
+ while (interface != nullptr)
+ {
+ const char* ifaceName = interface->Attribute("name");
+
+ if (ifaceName != nullptr && ifaceName == interfaceName)
+ {
+ break;
+ }
+
+ interface = interface->NextSiblingElement("interface");
+ }
+ if (interface == nullptr)
+ {
+ // if we got to the end of the list and
+ // never found a match, throw 404
+ asyncResp->res.result(
+ boost::beast::http::status::not_found);
+ return;
+ }
+
+ tinyxml2::XMLElement* methods =
+ interface->FirstChildElement("method");
+ while (methods != nullptr)
+ {
+ nlohmann::json argsArray = nlohmann::json::array();
+ tinyxml2::XMLElement* arg =
+ methods->FirstChildElement("arg");
+ while (arg != nullptr)
+ {
+ nlohmann::json thisArg;
+ for (const char* fieldName : std::array<const char*, 3>{
+ "name", "direction", "type"})
+ {
+ const char* fieldValue = arg->Attribute(fieldName);
+ if (fieldValue != nullptr)
+ {
+ thisArg[fieldName] = fieldValue;
+ }
+ }
+ argsArray.emplace_back(std::move(thisArg));
+ arg = arg->NextSiblingElement("arg");
+ }
+
+ const char* name = methods->Attribute("name");
+ if (name != nullptr)
+ {
+ std::string uri;
+ uri.reserve(14 + processName.size() +
+ objectPath.size() + interfaceName.size() +
+ strlen(name));
+ uri += "/bus/system/";
+ uri += processName;
+ uri += objectPath;
+ uri += "/";
+ uri += interfaceName;
+ uri += "/";
+ uri += name;
+
+ nlohmann::json::object_t object;
+ object["name"] = name;
+ object["uri"] = std::move(uri);
+ object["args"] = argsArray;
+
+ methodsArray.emplace_back(std::move(object));
+ }
+ methods = methods->NextSiblingElement("method");
+ }
+ tinyxml2::XMLElement* signals =
+ interface->FirstChildElement("signal");
+ while (signals != nullptr)
+ {
+ nlohmann::json argsArray = nlohmann::json::array();
+
+ tinyxml2::XMLElement* arg =
+ signals->FirstChildElement("arg");
+ while (arg != nullptr)
+ {
+ const char* name = arg->Attribute("name");
+ const char* type = arg->Attribute("type");
+ if (name != nullptr && type != nullptr)
+ {
+ nlohmann::json::object_t params;
+ params["name"] = name;
+ params["type"] = type;
+ argsArray.push_back(std::move(params));
+ }
+ arg = arg->NextSiblingElement("arg");
+ }
+ const char* name = signals->Attribute("name");
+ if (name != nullptr)
+ {
+ nlohmann::json::object_t object;
+ object["name"] = name;
+ object["args"] = argsArray;
+ signalsArray.emplace_back(std::move(object));
+ }
+
+ signals = signals->NextSiblingElement("signal");
+ }
+
+ tinyxml2::XMLElement* property =
+ interface->FirstChildElement("property");
+ while (property != nullptr)
+ {
+ const char* name = property->Attribute("name");
+ const char* type = property->Attribute("type");
+ if (type != nullptr && name != nullptr)
+ {
+ sdbusplus::message_t m =
+ crow::connections::systemBus->new_method_call(
+ processName.c_str(), objectPath.c_str(),
+ "org.freedesktop."
+ "DBus."
+ "Properties",
+ "Get");
+ m.append(interfaceName, name);
+ nlohmann::json& propertyItem = propertiesObj[name];
+ crow::connections::systemBus->async_send(
+ m, [&propertyItem,
+ asyncResp](const boost::system::error_code& ec2,
+ sdbusplus::message_t& msg) {
+ if (ec2)
+ {
+ return;
+ }
+
+ int r =
+ convertDBusToJSON("v", msg, propertyItem);
+ if (r < 0)
+ {
+ BMCWEB_LOG_ERROR(
+ "Couldn't convert vector to json");
+ }
+ });
+ }
+ property = property->NextSiblingElement("property");
+ }
+ },
+ processName, objectPath, "org.freedesktop.DBus.Introspectable",
+ "Introspect");
+ }
+ else
+ {
+ if (req.method() != boost::beast::http::verb::post)
+ {
+ asyncResp->res.result(boost::beast::http::status::not_found);
+ return;
+ }
+
+ nlohmann::json requestDbusData;
+ JsonParseResult ret = parseRequestAsJson(req, requestDbusData);
+ if (ret == JsonParseResult::BadContentType)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::unsupported_media_type,
+ invalidContentType, unsupportedMediaMsg);
+ return;
+ }
+ if (ret != JsonParseResult::Success)
+ {
+ setErrorResponse(asyncResp->res,
+ boost::beast::http::status::bad_request,
+ noJsonDesc, badReqMsg);
+ return;
+ }
+
+ if (!requestDbusData.is_array())
+ {
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ return;
+ }
+ auto transaction = std::make_shared<InProgressActionData>(asyncResp);
+
+ transaction->path = objectPath;
+ transaction->methodName = methodName;
+ transaction->arguments = std::move(requestDbusData);
+
+ findActionOnInterface(transaction, processName);
+ }
+}
+
+inline void requestRoutes(App& app)
+{
+ BMCWEB_ROUTE(app, "/bus/")
+ .privileges({{"Login"}})
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request&,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ nlohmann::json::array_t buses;
+ nlohmann::json& bus = buses.emplace_back();
+ bus["name"] = "system";
+ asyncResp->res.jsonValue["busses"] = std::move(buses);
+ asyncResp->res.jsonValue["status"] = "ok";
+ });
+
+ BMCWEB_ROUTE(app, "/bus/system/")
+ .privileges({{"Login"}})
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request&,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ auto myCallback = [asyncResp](
+ const boost::system::error_code& ec,
+ std::vector<std::string>& names) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("Dbus call failed with code {}", ec);
+ asyncResp->res.result(
+ boost::beast::http::status::internal_server_error);
+ }
+ else
+ {
+ std::ranges::sort(names);
+ asyncResp->res.jsonValue["status"] = "ok";
+ auto& objectsSub = asyncResp->res.jsonValue["objects"];
+ for (const auto& name : names)
+ {
+ nlohmann::json::object_t object;
+ object["name"] = name;
+ objectsSub.emplace_back(std::move(object));
+ }
+ }
+ };
+ dbus::utility::async_method_call(
+ std::move(myCallback), "org.freedesktop.DBus", "/",
+ "org.freedesktop.DBus", "ListNames");
+ });
+
+ BMCWEB_ROUTE(app, "/list/")
+ .privileges({{"Login"}})
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request&,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
+ handleList(asyncResp, "/");
+ });
+
+ BMCWEB_ROUTE(app, "/xyz/<path>")
+ .privileges({{"Login"}})
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& path) {
+ std::string objectPath = "/xyz/" + path;
+ handleDBusUrl(req, asyncResp, objectPath);
+ });
+
+ BMCWEB_ROUTE(app, "/xyz/<path>")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::put, boost::beast::http::verb::post,
+ boost::beast::http::verb::delete_)(
+ [](const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& path) {
+ std::string objectPath = "/xyz/" + path;
+ handleDBusUrl(req, asyncResp, objectPath);
+ });
+
+ BMCWEB_ROUTE(app, "/org/<path>")
+ .privileges({{"Login"}})
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& path) {
+ std::string objectPath = "/org/" + path;
+ handleDBusUrl(req, asyncResp, objectPath);
+ });
+
+ BMCWEB_ROUTE(app, "/org/<path>")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::put, boost::beast::http::verb::post,
+ boost::beast::http::verb::delete_)(
+ [](const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& path) {
+ std::string objectPath = "/org/" + path;
+ handleDBusUrl(req, asyncResp, objectPath);
+ });
+
+ BMCWEB_ROUTE(app, "/download/dump/<str>/")
+ .privileges({{"ConfigureManager"}})
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request&,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& dumpId) {
+ if (!validateFilename(dumpId))
+ {
+ asyncResp->res.result(
+ boost::beast::http::status::bad_request);
+ return;
+ }
+ std::filesystem::path loc(
+ "/var/lib/phosphor-debug-collector/dumps");
+
+ loc /= dumpId;
+
+ if (!std::filesystem::exists(loc) ||
+ !std::filesystem::is_directory(loc))
+ {
+ BMCWEB_LOG_ERROR("{}Not found", loc.string());
+ asyncResp->res.result(
+ boost::beast::http::status::not_found);
+ return;
+ }
+ std::filesystem::directory_iterator files(loc);
+
+ for (const auto& file : files)
+ {
+ if (asyncResp->res.openFile(file) !=
+ crow::OpenCode::Success)
+ {
+ continue;
+ }
+
+ asyncResp->res.addHeader(
+ boost::beast::http::field::content_type,
+ "application/octet-stream");
+
+ // Assuming only one dump file will be present in the dump
+ // id directory
+ std::string dumpFileName = file.path().filename().string();
+
+ // Filename should be in alphanumeric, dot and underscore
+ // Its based on phosphor-debug-collector application
+ // dumpfile format
+ static std::regex dumpFileRegex("[a-zA-Z0-9\\._]+");
+ if (!std::regex_match(dumpFileName, dumpFileRegex))
+ {
+ BMCWEB_LOG_ERROR("Invalid dump filename {}",
+ dumpFileName);
+ asyncResp->res.result(
+ boost::beast::http::status::not_found);
+ return;
+ }
+ std::string contentDispositionParam =
+ "attachment; filename=\"" + dumpFileName + "\"";
+
+ asyncResp->res.addHeader(
+ boost::beast::http::field::content_disposition,
+ contentDispositionParam);
+
+ return;
+ }
+ asyncResp->res.result(boost::beast::http::status::not_found);
+ return;
+ });
+
+ BMCWEB_ROUTE(app, "/bus/system/<str>/")
+ .privileges({{"Login"}})
+
+ .methods(boost::beast::http::verb::get)(
+ [](const crow::Request&,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& connection) {
+ introspectObjects(connection, "/", asyncResp);
+ });
+
+ BMCWEB_ROUTE(app, "/bus/system/<str>/<path>")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .methods(boost::beast::http::verb::get,
+ boost::beast::http::verb::post)(handleBusSystemPost);
+}
+} // namespace openbmc_mapper
+} // namespace crow
diff --git a/features/openbmc_rest/openbmc_dbus_rest_test.cpp b/features/openbmc_rest/openbmc_dbus_rest_test.cpp
new file mode 100644
index 0000000..1bc320e
--- /dev/null
+++ b/features/openbmc_rest/openbmc_dbus_rest_test.cpp
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#include "openbmc_dbus_rest.hpp"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace crow::openbmc_mapper
+{
+namespace
+{
+
+using ::testing::ElementsAre;
+// Also see redfish-core/ut/configfile_test.cpp
+TEST(OpenbmcDbusRestTest, ValidFilenameGood)
+{
+ EXPECT_TRUE(validateFilename("GoodConfigFile"));
+ EXPECT_TRUE(validateFilename("_Underlines_"));
+ EXPECT_TRUE(validateFilename("8675309"));
+ EXPECT_TRUE(validateFilename("-Dashes-"));
+ EXPECT_TRUE(validateFilename("With Spaces"));
+ EXPECT_TRUE(validateFilename("One.Dot"));
+ EXPECT_TRUE(validateFilename("trailingdot."));
+ EXPECT_TRUE(validateFilename("-_ o _-"));
+ EXPECT_TRUE(validateFilename(" "));
+ EXPECT_TRUE(validateFilename(" ."));
+}
+
+// There is no length test yet because validateFilename() does not care yet
+TEST(OpenbmcDbusRestTest, ValidFilenameBad)
+{
+ EXPECT_FALSE(validateFilename(""));
+ EXPECT_FALSE(validateFilename("Bad@file"));
+ EXPECT_FALSE(validateFilename("/../../../../../etc/badpath"));
+ EXPECT_FALSE(validateFilename("/../../etc/badpath"));
+ EXPECT_FALSE(validateFilename("/mydir/configFile"));
+ EXPECT_FALSE(validateFilename("/"));
+ EXPECT_FALSE(validateFilename(".leadingdot"));
+ EXPECT_FALSE(validateFilename("Two..Dots"));
+ EXPECT_FALSE(validateFilename("../../../../../../etc/shadow"));
+ EXPECT_FALSE(validateFilename("."));
+}
+
+TEST(OpenBmcDbusTest, TestArgSplit)
+{
+ // test the basic types
+ EXPECT_THAT(dbusArgSplit("x"), ElementsAre("x"));
+ EXPECT_THAT(dbusArgSplit("y"), ElementsAre("y"));
+ EXPECT_THAT(dbusArgSplit("b"), ElementsAre("b"));
+ EXPECT_THAT(dbusArgSplit("n"), ElementsAre("n"));
+ EXPECT_THAT(dbusArgSplit("q"), ElementsAre("q"));
+ EXPECT_THAT(dbusArgSplit("i"), ElementsAre("i"));
+ EXPECT_THAT(dbusArgSplit("u"), ElementsAre("u"));
+ EXPECT_THAT(dbusArgSplit("x"), ElementsAre("x"));
+ EXPECT_THAT(dbusArgSplit("t"), ElementsAre("t"));
+ EXPECT_THAT(dbusArgSplit("d"), ElementsAre("d"));
+ EXPECT_THAT(dbusArgSplit("h"), ElementsAre("h"));
+ // test arrays
+ EXPECT_THAT(dbusArgSplit("ai"), ElementsAre("ai"));
+ EXPECT_THAT(dbusArgSplit("ax"), ElementsAre("ax"));
+ // test tuples
+ EXPECT_THAT(dbusArgSplit("(sss)"), ElementsAre("(sss)"));
+ EXPECT_THAT(dbusArgSplit("(sss)b"), ElementsAre("(sss)", "b"));
+ EXPECT_THAT(dbusArgSplit("b(sss)"), ElementsAre("b", "(sss)"));
+
+ // Test nested types
+ EXPECT_THAT(dbusArgSplit("a{si}b"), ElementsAre("a{si}", "b"));
+ EXPECT_THAT(dbusArgSplit("a(sss)b"), ElementsAre("a(sss)", "b"));
+ EXPECT_THAT(dbusArgSplit("aa{si}b"), ElementsAre("aa{si}", "b"));
+ EXPECT_THAT(dbusArgSplit("i{si}b"), ElementsAre("i", "{si}", "b"));
+}
+} // namespace
+} // namespace crow::openbmc_mapper
diff --git a/features/redfish b/features/redfish
new file mode 120000
index 0000000..635be9a
--- /dev/null
+++ b/features/redfish
@@ -0,0 +1 @@
+../redfish-core
\ No newline at end of file
diff --git a/features/serial/meson.build b/features/serial/meson.build
new file mode 100644
index 0000000..ab60765
--- /dev/null
+++ b/features/serial/meson.build
@@ -0,0 +1 @@
+incdir += include_directories('.')
diff --git a/features/serial/obmc_console.hpp b/features/serial/obmc_console.hpp
new file mode 100644
index 0000000..40b72fe
--- /dev/null
+++ b/features/serial/obmc_console.hpp
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+#include "app.hpp"
+#include "dbus_utility.hpp"
+#include "io_context_singleton.hpp"
+#include "logging.hpp"
+#include "websocket.hpp"
+
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/error.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/local/stream_protocol.hpp>
+#include <boost/beast/core/error.hpp>
+#include <boost/container/flat_map.hpp>
+#include <boost/system/error_code.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <array>
+#include <cstddef>
+#include <functional>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace crow
+{
+namespace obmc_console
+{
+
+// Update this value each time we add new console route.
+static constexpr const uint maxSessions = 32;
+
+class ConsoleHandler : public std::enable_shared_from_this<ConsoleHandler>
+{
+ public:
+ ConsoleHandler(boost::asio::io_context& ioc,
+ crow::websocket::Connection& connIn) :
+ hostSocket(ioc), conn(connIn)
+ {}
+
+ ~ConsoleHandler() = default;
+
+ ConsoleHandler(const ConsoleHandler&) = delete;
+ ConsoleHandler(ConsoleHandler&&) = delete;
+ ConsoleHandler& operator=(const ConsoleHandler&) = delete;
+ ConsoleHandler& operator=(ConsoleHandler&&) = delete;
+
+ void doWrite()
+ {
+ if (doingWrite)
+ {
+ BMCWEB_LOG_DEBUG("Already writing. Bailing out");
+ return;
+ }
+
+ if (inputBuffer.empty())
+ {
+ BMCWEB_LOG_DEBUG("Outbuffer empty. Bailing out");
+ return;
+ }
+
+ doingWrite = true;
+ hostSocket.async_write_some(
+ boost::asio::buffer(inputBuffer.data(), inputBuffer.size()),
+ [weak(weak_from_this())](const boost::beast::error_code& ec,
+ std::size_t bytesWritten) {
+ std::shared_ptr<ConsoleHandler> self = weak.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+
+ self->doingWrite = false;
+ self->inputBuffer.erase(0, bytesWritten);
+
+ if (ec == boost::asio::error::eof)
+ {
+ self->conn.close("Error in reading to host port");
+ return;
+ }
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("Error in host serial write {}",
+ ec.message());
+ return;
+ }
+ self->doWrite();
+ });
+ }
+
+ static void afterSendEx(const std::weak_ptr<ConsoleHandler>& weak)
+ {
+ std::shared_ptr<ConsoleHandler> self = weak.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+ self->doRead();
+ }
+
+ void doRead()
+ {
+ BMCWEB_LOG_DEBUG("Reading from socket");
+ hostSocket.async_read_some(
+ boost::asio::buffer(outputBuffer),
+ [this, weakSelf(weak_from_this())](
+ const boost::system::error_code& ec, std::size_t bytesRead) {
+ BMCWEB_LOG_DEBUG("read done. Read {} bytes", bytesRead);
+ std::shared_ptr<ConsoleHandler> self = weakSelf.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("Couldn't read from host serial port: {}",
+ ec.message());
+ conn.close("Error connecting to host port");
+ return;
+ }
+ std::string_view payload(outputBuffer.data(), bytesRead);
+ self->conn.sendEx(
+ crow::websocket::MessageType::Binary, payload,
+ std::bind_front(afterSendEx, weak_from_this()));
+ });
+ }
+
+ bool connect(int fd)
+ {
+ boost::system::error_code ec;
+ boost::asio::local::stream_protocol proto;
+
+ hostSocket.assign(proto, fd, ec);
+
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "Failed to assign the DBUS socket Socket assign error: {}",
+ ec.message());
+ return false;
+ }
+
+ conn.resumeRead();
+ doWrite();
+ doRead();
+ return true;
+ }
+
+ boost::asio::local::stream_protocol::socket hostSocket;
+
+ std::array<char, 4096> outputBuffer{};
+
+ std::string inputBuffer;
+ bool doingWrite = false;
+ crow::websocket::Connection& conn;
+};
+
+using ObmcConsoleMap = boost::container::flat_map<
+ crow::websocket::Connection*, std::shared_ptr<ConsoleHandler>, std::less<>,
+ std::vector<std::pair<crow::websocket::Connection*,
+ std::shared_ptr<ConsoleHandler>>>>;
+
+inline ObmcConsoleMap& getConsoleHandlerMap()
+{
+ static ObmcConsoleMap map;
+ return map;
+}
+
+// Remove connection from the connection map and if connection map is empty
+// then remove the handler from handlers map.
+inline void onClose(crow::websocket::Connection& conn, const std::string& err)
+{
+ BMCWEB_LOG_INFO("Closing websocket. Reason: {}", err);
+
+ auto iter = getConsoleHandlerMap().find(&conn);
+ if (iter == getConsoleHandlerMap().end())
+ {
+ BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn));
+ return;
+ }
+ BMCWEB_LOG_DEBUG("Remove connection {} from obmc console", logPtr(&conn));
+
+ // Removed last connection so remove the path
+ getConsoleHandlerMap().erase(iter);
+}
+
+inline void connectConsoleSocket(crow::websocket::Connection& conn,
+ const boost::system::error_code& ec,
+ const sdbusplus::message::unix_fd& unixfd)
+{
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR(
+ "Failed to call console Connect() method DBUS error: {}",
+ ec.message());
+ conn.close("Failed to connect");
+ return;
+ }
+
+ // Look up the handler
+ auto iter = getConsoleHandlerMap().find(&conn);
+ if (iter == getConsoleHandlerMap().end())
+ {
+ BMCWEB_LOG_ERROR("Connection was already closed");
+ return;
+ }
+
+ int fd = dup(unixfd);
+ if (fd == -1)
+ {
+ BMCWEB_LOG_ERROR("Failed to dup the DBUS unixfd error");
+ conn.close("Internal error");
+ return;
+ }
+
+ BMCWEB_LOG_DEBUG("Console duped FD: {}", fd);
+
+ if (!iter->second->connect(fd))
+ {
+ close(fd);
+ conn.close("Internal Error");
+ }
+}
+
+inline void processConsoleObject(
+ crow::websocket::Connection& conn, const std::string& consoleObjPath,
+ const boost::system::error_code& ec,
+ const ::dbus::utility::MapperGetObject& objInfo)
+{
+ // Look up the handler
+ auto iter = getConsoleHandlerMap().find(&conn);
+ if (iter == getConsoleHandlerMap().end())
+ {
+ BMCWEB_LOG_ERROR("Connection was already closed");
+ return;
+ }
+
+ if (ec)
+ {
+ BMCWEB_LOG_WARNING("getDbusObject() for consoles failed. DBUS error:{}",
+ ec.message());
+ conn.close("getDbusObject() for consoles failed.");
+ return;
+ }
+
+ const auto valueIface = objInfo.begin();
+ if (valueIface == objInfo.end())
+ {
+ BMCWEB_LOG_WARNING("getDbusObject() returned unexpected size: {}",
+ objInfo.size());
+ conn.close("getDbusObject() returned unexpected size");
+ return;
+ }
+
+ const std::string& consoleService = valueIface->first;
+ BMCWEB_LOG_DEBUG("Looking up unixFD for Service {} Path {}", consoleService,
+ consoleObjPath);
+ // Call Connect() method to get the unix FD
+ dbus::utility::async_method_call(
+ [&conn](const boost::system::error_code& ec1,
+ const sdbusplus::message::unix_fd& unixfd) {
+ connectConsoleSocket(conn, ec1, unixfd);
+ },
+ consoleService, consoleObjPath, "xyz.openbmc_project.Console.Access",
+ "Connect");
+}
+
+// Query consoles from DBUS and find the matching to the
+// rules string.
+inline void onOpen(crow::websocket::Connection& conn)
+{
+ std::string consoleLeaf;
+
+ BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
+
+ if (getConsoleHandlerMap().size() >= maxSessions)
+ {
+ conn.close("Max sessions are already connected");
+ return;
+ }
+
+ std::shared_ptr<ConsoleHandler> handler =
+ std::make_shared<ConsoleHandler>(getIoContext(), conn);
+ getConsoleHandlerMap().emplace(&conn, handler);
+
+ conn.deferRead();
+
+ // Keep old path for backward compatibility
+ if (conn.url().path() == "/console0")
+ {
+ consoleLeaf = "default";
+ }
+ else
+ {
+ // Get the console id from console router path and prepare the console
+ // object path and console service.
+ consoleLeaf = conn.url().segments().back();
+ }
+ std::string consolePath =
+ sdbusplus::message::object_path("/xyz/openbmc_project/console") /
+ consoleLeaf;
+
+ BMCWEB_LOG_DEBUG("Console Object path = {} Request target = {}",
+ consolePath, conn.url().path());
+
+ // mapper call lambda
+ constexpr std::array<std::string_view, 1> interfaces = {
+ "xyz.openbmc_project.Console.Access"};
+
+ dbus::utility::getDbusObject(
+ consolePath, interfaces,
+ [&conn, consolePath](const boost::system::error_code& ec,
+ const ::dbus::utility::MapperGetObject& objInfo) {
+ processConsoleObject(conn, consolePath, ec, objInfo);
+ });
+}
+
+inline void onMessage(crow::websocket::Connection& conn,
+ const std::string& data, bool /*isBinary*/)
+{
+ auto handler = getConsoleHandlerMap().find(&conn);
+ if (handler == getConsoleHandlerMap().end())
+ {
+ BMCWEB_LOG_CRITICAL("Unable to find connection {}", logPtr(&conn));
+ return;
+ }
+ handler->second->inputBuffer += data;
+ handler->second->doWrite();
+}
+
+inline void requestRoutes(App& app)
+{
+ BMCWEB_ROUTE(app, "/console0")
+ .privileges({{"OpenBMCHostConsole"}})
+ .websocket()
+ .onopen(onOpen)
+ .onclose(onClose)
+ .onmessage(onMessage);
+
+ BMCWEB_ROUTE(app, "/console/<str>")
+ .privileges({{"OpenBMCHostConsole"}})
+ .websocket()
+ .onopen(onOpen)
+ .onclose(onClose)
+ .onmessage(onMessage);
+}
+} // namespace obmc_console
+} // namespace crow
diff --git a/features/virtual_media/meson.build b/features/virtual_media/meson.build
new file mode 100644
index 0000000..ab60765
--- /dev/null
+++ b/features/virtual_media/meson.build
@@ -0,0 +1 @@
+incdir += include_directories('.')
diff --git a/features/virtual_media/vm_websocket.hpp b/features/virtual_media/vm_websocket.hpp
new file mode 100644
index 0000000..b930b64
--- /dev/null
+++ b/features/virtual_media/vm_websocket.hpp
@@ -0,0 +1,626 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "bmcweb_config.h"
+
+#include "app.hpp"
+#include "dbus_utility.hpp"
+#include "io_context_singleton.hpp"
+#include "logging.hpp"
+#include "websocket.hpp"
+
+#include <boost/asio/buffer.hpp>
+#include <boost/asio/error.hpp>
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/local/stream_protocol.hpp>
+#include <boost/asio/readable_pipe.hpp>
+#include <boost/asio/writable_pipe.hpp>
+#include <boost/beast/core/buffers_to_string.hpp>
+#include <boost/beast/core/error.hpp>
+#include <boost/beast/core/flat_static_buffer.hpp>
+#include <boost/container/flat_map.hpp>
+#include <boost/process/v2/process.hpp>
+#include <boost/process/v2/stdio.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <sdbusplus/unpack_properties.hpp>
+
+#include <cerrno>
+#include <csignal>
+#include <cstddef>
+#include <filesystem>
+#include <format>
+#include <functional>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <system_error>
+#include <utility>
+
+namespace crow
+{
+
+namespace obmc_vm
+{
+
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+static crow::websocket::Connection* session = nullptr;
+
+// The max network block device buffer size is 128kb plus 16bytes
+// for the message header:
+// https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message
+static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4;
+
+class Handler : public std::enable_shared_from_this<Handler>
+{
+ public:
+ Handler(const std::string& media, boost::asio::io_context& ios) :
+ pipeOut(ios), pipeIn(ios),
+ proxy(ios, "/usr/bin/nbd-proxy", {media},
+ boost::process::v2::process_stdio{
+ .in = pipeIn, .out = pipeOut, .err = nullptr})
+ {}
+
+ ~Handler() = default;
+
+ Handler(const Handler&) = delete;
+ Handler(Handler&&) = delete;
+ Handler& operator=(const Handler&) = delete;
+ Handler& operator=(Handler&&) = delete;
+
+ void doClose()
+ {
+ // boost::process::child::terminate uses SIGKILL, need to send SIGTERM
+ // to allow the proxy to stop nbd-client and the USB device gadget.
+ // NOLINTNEXTLINE(misc-include-cleaner)
+ int rc = kill(proxy.id(), SIGTERM);
+ if (rc != 0)
+ {
+ BMCWEB_LOG_ERROR("Failed to terminate nbd-proxy: {}", errno);
+ return;
+ }
+
+ proxy.wait();
+ }
+
+ void connect()
+ {
+ std::error_code ec;
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("Couldn't connect to nbd-proxy: {}", ec.message());
+ if (session != nullptr)
+ {
+ session->close("Error connecting to nbd-proxy");
+ }
+ return;
+ }
+ doWrite();
+ doRead();
+ }
+
+ void doWrite()
+ {
+ if (doingWrite)
+ {
+ BMCWEB_LOG_DEBUG("Already writing. Bailing out");
+ return;
+ }
+
+ if (inputBuffer.size() == 0)
+ {
+ BMCWEB_LOG_DEBUG("inputBuffer empty. Bailing out");
+ return;
+ }
+
+ doingWrite = true;
+ pipeIn.async_write_some(
+ inputBuffer.data(),
+ [this, self(shared_from_this())](const boost::beast::error_code& ec,
+ std::size_t bytesWritten) {
+ BMCWEB_LOG_DEBUG("Wrote {}bytes", bytesWritten);
+ doingWrite = false;
+ inputBuffer.consume(bytesWritten);
+
+ if (session == nullptr)
+ {
+ return;
+ }
+ if (ec == boost::asio::error::eof)
+ {
+ session->close("VM socket port closed");
+ return;
+ }
+ if (ec)
+ {
+ session->close("Error in writing to proxy port");
+ BMCWEB_LOG_ERROR("Error in VM socket write {}", ec);
+ return;
+ }
+ doWrite();
+ });
+ }
+
+ void doRead()
+ {
+ std::size_t bytes = outputBuffer.capacity() - outputBuffer.size();
+
+ pipeOut.async_read_some(
+ outputBuffer.prepare(bytes),
+ [this, self(shared_from_this())](
+ const boost::system::error_code& ec, std::size_t bytesRead) {
+ BMCWEB_LOG_DEBUG("Read done. Read {} bytes", bytesRead);
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("Couldn't read from VM port: {}", ec);
+ if (session != nullptr)
+ {
+ session->close("Error in connecting to VM port");
+ }
+ return;
+ }
+ if (session == nullptr)
+ {
+ return;
+ }
+
+ outputBuffer.commit(bytesRead);
+ std::string_view payload(
+ static_cast<const char*>(outputBuffer.data().data()),
+ bytesRead);
+ session->sendBinary(payload);
+ outputBuffer.consume(bytesRead);
+
+ doRead();
+ });
+ }
+
+ boost::asio::readable_pipe pipeOut;
+ boost::asio::writable_pipe pipeIn;
+ boost::process::v2::process proxy;
+ bool doingWrite{false};
+
+ boost::beast::flat_static_buffer<nbdBufferSize> outputBuffer;
+ boost::beast::flat_static_buffer<nbdBufferSize> inputBuffer;
+};
+
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+static std::shared_ptr<Handler> handler;
+
+} // namespace obmc_vm
+
+namespace nbd_proxy
+{
+using boost::asio::local::stream_protocol;
+
+// The max network block device buffer size is 128kb plus 16bytes
+// for the message header:
+// https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md#simple-reply-message
+static constexpr auto nbdBufferSize = (128 * 1024 + 16) * 4;
+
+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),
+
+ peerSocket(getIoContext()),
+ acceptor(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");
+
+ BMCWEB_LOG_DEBUG("peerSocket->close()");
+ boost::system::error_code ec;
+ peerSocket.close(ec);
+
+ BMCWEB_LOG_DEBUG("std::filesystem::remove({})", socketId);
+ std::error_code ec2;
+ std::filesystem::remove(socketId.c_str(), ec2);
+ if (ec2)
+ {
+ BMCWEB_LOG_DEBUG("Failed to remove file, ignoring");
+ }
+
+ dbus::utility::async_method_call(
+ dbus::utility::logError, "xyz.openbmc_project.VirtualMedia", path,
+ "xyz.openbmc_project.VirtualMedia.Proxy", "Unmount");
+ }
+
+ std::string getEndpointId() const
+ {
+ return endpointId;
+ }
+
+ static void afterMount(const std::weak_ptr<NbdProxyServer>& weak,
+ const boost::system::error_code& ec,
+ bool /*isBinary*/)
+ {
+ std::shared_ptr<NbdProxyServer> self = weak.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("DBus error: cannot call mount method = {}",
+ ec.message());
+
+ self->connection.close("Failed to mount media");
+ return;
+ }
+ }
+
+ static void afterAccept(const std::weak_ptr<NbdProxyServer>& weak,
+ const boost::system::error_code& ec,
+ stream_protocol::socket socket)
+ {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("UNIX socket: async_accept error = {}",
+ ec.message());
+ return;
+ }
+
+ BMCWEB_LOG_DEBUG("Connection opened");
+ std::shared_ptr<NbdProxyServer> self = weak.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+
+ self->connection.resumeRead();
+ self->peerSocket = std::move(socket);
+ // Start reading from socket
+ self->doRead();
+ }
+
+ void run()
+ {
+ acceptor.async_accept(
+ std::bind_front(&NbdProxyServer::afterAccept, weak_from_this()));
+
+ dbus::utility::async_method_call(
+ [weak{weak_from_this()}](const boost::system::error_code& ec,
+ bool isBinary) {
+ afterMount(weak, ec, isBinary);
+ },
+ "xyz.openbmc_project.VirtualMedia", path,
+ "xyz.openbmc_project.VirtualMedia.Proxy", "Mount");
+ }
+
+ void send(std::string_view buffer, std::function<void()>&& onDone)
+ {
+ size_t copied = boost::asio::buffer_copy(
+ ws2uxBuf.prepare(buffer.size()), boost::asio::buffer(buffer));
+ ws2uxBuf.commit(copied);
+
+ doWrite(std::move(onDone));
+ }
+
+ private:
+ static void afterSendEx(const std::weak_ptr<NbdProxyServer>& weak)
+ {
+ std::shared_ptr<NbdProxyServer> self2 = weak.lock();
+ if (self2 != nullptr)
+ {
+ self2->ux2wsBuf.consume(self2->ux2wsBuf.size());
+ self2->doRead();
+ }
+ }
+
+ void afterRead(const std::weak_ptr<NbdProxyServer>& weak,
+ const boost::system::error_code& ec, size_t bytesRead)
+ {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("UNIX socket: async_read_some error = {}",
+ ec.message());
+ return;
+ }
+ std::shared_ptr<NbdProxyServer> self = weak.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+
+ // Send to websocket
+ self->ux2wsBuf.commit(bytesRead);
+ self->connection.sendEx(
+ crow::websocket::MessageType::Binary,
+ boost::beast::buffers_to_string(self->ux2wsBuf.data()),
+ std::bind_front(&NbdProxyServer::afterSendEx, weak_from_this()));
+ }
+
+ void doRead()
+ {
+ // Trigger async read
+ peerSocket.async_read_some(ux2wsBuf.prepare(nbdBufferSize),
+ std::bind_front(&NbdProxyServer::afterRead,
+ this, weak_from_this()));
+ }
+
+ static void afterWrite(const std::weak_ptr<NbdProxyServer>& weak,
+ std::function<void()>&& onDone,
+ const boost::system::error_code& ec,
+ size_t bytesWritten)
+ {
+ std::shared_ptr<NbdProxyServer> self = weak.lock();
+ if (self == nullptr)
+ {
+ return;
+ }
+
+ self->ws2uxBuf.consume(bytesWritten);
+ self->uxWriteInProgress = false;
+
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("UNIX: async_write error = {}", ec.message());
+ self->connection.close("Internal error");
+ return;
+ }
+
+ // Retrigger doWrite if there is something in buffer
+ if (self->ws2uxBuf.size() > 0)
+ {
+ self->doWrite(std::move(onDone));
+ return;
+ }
+ onDone();
+ }
+
+ void doWrite(std::function<void()>&& onDone)
+ {
+ 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;
+ peerSocket.async_write_some(
+ ws2uxBuf.data(),
+ std::bind_front(&NbdProxyServer::afterWrite, weak_from_this(),
+ std::move(onDone)));
+ }
+
+ // 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::flat_static_buffer<nbdBufferSize> ux2wsBuf;
+
+ // WebSocket => UNIX buffer
+ boost::beast::flat_static_buffer<nbdBufferSize> ws2uxBuf;
+
+ // The socket used to communicate with the client.
+ stream_protocol::socket peerSocket;
+
+ // Default acceptor for UNIX socket
+ stream_protocol::acceptor acceptor;
+
+ 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 afterGetSocket(
+ crow::websocket::Connection& conn,
+ const sdbusplus::message::object_path& path,
+ const boost::system::error_code& ec,
+ const dbus::utility::DBusPropertiesMap& propertiesList)
+{
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR("DBus getAllProperties error: {}", ec.message());
+ conn.close("Internal Error");
+ return;
+ }
+ std::string endpointId;
+ std::string socket;
+
+ bool success = sdbusplus::unpackPropertiesNoThrow(
+ redfish::dbus_utils::UnpackErrorPrinter(), propertiesList, "EndpointId",
+ endpointId, "Socket", socket);
+
+ if (!success)
+ {
+ BMCWEB_LOG_ERROR("Failed to unpack properties");
+ conn.close("Internal Error");
+ return;
+ }
+
+ for (const auto& session : sessions)
+ {
+ if (session.second->getEndpointId() == conn.url().path())
+ {
+ 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::error_code ec2;
+ std::filesystem::remove(socket.c_str(), ec2);
+ // Ignore failures. File might not exist.
+
+ sessions[&conn] =
+ std::make_shared<NbdProxyServer>(conn, socket, endpointId, path);
+ sessions[&conn]->run();
+}
+
+inline void onOpen(crow::websocket::Connection& conn)
+{
+ BMCWEB_LOG_DEBUG("nbd-proxy.onopen({})", logPtr(&conn));
+
+ if (conn.url().segments().size() < 2)
+ {
+ BMCWEB_LOG_ERROR("Invalid path - \"{}\"", conn.url().path());
+ conn.close("Internal error");
+ return;
+ }
+
+ std::string index = conn.url().segments().back();
+ std::string path =
+ std::format("/xyz/openbmc_project/VirtualMedia/Proxy/Slot_{}", index);
+
+ dbus::utility::getAllProperties(
+ "xyz.openbmc_project.VirtualMedia", path,
+ "xyz.openbmc_project.VirtualMedia.MountPoint",
+ [&conn, path](const boost::system::error_code& ec,
+ const dbus::utility::DBusPropertiesMap& propertiesList) {
+ afterGetSocket(conn, path, ec, propertiesList);
+ });
+
+ // We need to wait for dbus and the websockets to hook up before data is
+ // sent/received. Tell the core to hold off messages until the sockets are
+ // up
+ conn.deferRead();
+}
+
+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;
+ }
+ // Remove reference to session in global map
+ sessions.erase(session);
+}
+
+inline void onMessage(crow::websocket::Connection& conn, std::string_view data,
+ crow::websocket::MessageType /*type*/,
+ std::function<void()>&& whenComplete)
+{
+ BMCWEB_LOG_DEBUG("nbd-proxy.onMessage(len = {})", data.size());
+
+ // Acquire proxy from sessions
+ auto session = sessions.find(&conn);
+ if (session == sessions.end() || session->second == nullptr)
+ {
+ whenComplete();
+ return;
+ }
+
+ session->second->send(data, std::move(whenComplete));
+}
+} // namespace nbd_proxy
+
+namespace obmc_vm
+{
+
+inline void requestRoutes(App& app)
+{
+ static_assert(
+ !(BMCWEB_VM_WEBSOCKET && BMCWEB_VM_NBDPROXY),
+ "nbd proxy cannot be turned on at the same time as vm websocket.");
+
+ if constexpr (BMCWEB_VM_NBDPROXY)
+ {
+ BMCWEB_ROUTE(app, "/nbd/<str>")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .websocket()
+ .onopen(nbd_proxy::onOpen)
+ .onclose(nbd_proxy::onClose)
+ .onmessageex(nbd_proxy::onMessage);
+
+ BMCWEB_ROUTE(app, "/vm/0/0")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .websocket()
+ .onopen(nbd_proxy::onOpen)
+ .onclose(nbd_proxy::onClose)
+ .onmessageex(nbd_proxy::onMessage);
+ }
+ if constexpr (BMCWEB_VM_WEBSOCKET)
+ {
+ BMCWEB_ROUTE(app, "/vm/0/0")
+ .privileges({{"ConfigureComponents", "ConfigureManager"}})
+ .websocket()
+ .onopen([](crow::websocket::Connection& conn) {
+ BMCWEB_LOG_DEBUG("Connection {} opened", logPtr(&conn));
+
+ if (session != nullptr)
+ {
+ conn.close("Session already connected");
+ return;
+ }
+
+ if (handler != nullptr)
+ {
+ conn.close("Handler already running");
+ return;
+ }
+
+ session = &conn;
+
+ // media is the last digit of the endpoint /vm/0/0. A future
+ // enhancement can include supporting different endpoint values.
+ const char* media = "0";
+ handler = std::make_shared<Handler>(media, getIoContext());
+ handler->connect();
+ })
+ .onclose([](crow::websocket::Connection& conn,
+ const std::string& /*reason*/) {
+ if (&conn != session)
+ {
+ return;
+ }
+
+ session = nullptr;
+ handler->doClose();
+ handler->inputBuffer.clear();
+ handler->outputBuffer.clear();
+ handler.reset();
+ })
+ .onmessage([](crow::websocket::Connection& conn,
+ const std::string& data, bool) {
+ if (data.length() > handler->inputBuffer.capacity() -
+ handler->inputBuffer.size())
+ {
+ BMCWEB_LOG_ERROR("Buffer overrun when writing {} bytes",
+ data.length());
+ conn.close("Buffer overrun");
+ return;
+ }
+
+ size_t copied = boost::asio::buffer_copy(
+ handler->inputBuffer.prepare(data.size()),
+ boost::asio::buffer(data));
+ handler->inputBuffer.commit(copied);
+ handler->doWrite();
+ });
+ }
+}
+
+} // namespace obmc_vm
+
+} // namespace crow
diff --git a/features/webui_login/login_routes.hpp b/features/webui_login/login_routes.hpp
new file mode 100644
index 0000000..4b4a621
--- /dev/null
+++ b/features/webui_login/login_routes.hpp
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "app.hpp"
+#include "async_resp.hpp"
+#include "cookies.hpp"
+#include "http_request.hpp"
+#include "http_response.hpp"
+#include "logging.hpp"
+#include "multipart_parser.hpp"
+#include "pam_authenticate.hpp"
+#include "sessions.hpp"
+
+#include <security/_pam_types.h>
+
+#include <boost/beast/http/fields.hpp>
+#include <boost/beast/http/status.hpp>
+#include <boost/beast/http/verb.hpp>
+#include <nlohmann/json.hpp>
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+
+namespace crow
+{
+
+namespace login_routes
+{
+
+inline void handleLogin(const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ MultipartParser parser;
+ std::string_view contentType = req.getHeaderValue("content-type");
+ std::string_view username;
+ std::string_view password;
+
+ // This object needs to be declared at this scope so the strings
+ // within it are not destroyed before we can use them
+ nlohmann::json loginCredentials;
+ // Check if auth was provided by a payload
+ if (contentType.starts_with("application/json"))
+ {
+ loginCredentials = nlohmann::json::parse(req.body(), nullptr, false);
+ if (loginCredentials.is_discarded())
+ {
+ BMCWEB_LOG_DEBUG("Bad json in request");
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ return;
+ }
+
+ // check for username/password in the root object
+ // THis method is how intel APIs authenticate
+ nlohmann::json::iterator userIt = loginCredentials.find("username");
+ nlohmann::json::iterator passIt = loginCredentials.find("password");
+ if (userIt != loginCredentials.end() &&
+ passIt != loginCredentials.end())
+ {
+ const std::string* userStr = userIt->get_ptr<const std::string*>();
+ const std::string* passStr = passIt->get_ptr<const std::string*>();
+ if (userStr != nullptr && passStr != nullptr)
+ {
+ username = *userStr;
+ password = *passStr;
+ }
+ }
+ else
+ {
+ // Openbmc appears to push a data object that contains the
+ // same keys (username and password), attempt to use that
+ auto dataIt = loginCredentials.find("data");
+ if (dataIt != loginCredentials.end())
+ {
+ // Some apis produce an array of value ["username",
+ // "password"]
+ if (dataIt->is_array())
+ {
+ if (dataIt->size() == 2)
+ {
+ nlohmann::json::iterator userIt2 = dataIt->begin();
+ nlohmann::json::iterator passIt2 = dataIt->begin() + 1;
+ if (userIt2 != dataIt->end() &&
+ passIt2 != dataIt->end())
+ {
+ const std::string* userStr =
+ userIt2->get_ptr<const std::string*>();
+ const std::string* passStr =
+ passIt2->get_ptr<const std::string*>();
+ if (userStr != nullptr && passStr != nullptr)
+ {
+ username = *userStr;
+ password = *passStr;
+ }
+ }
+ }
+ }
+ else
+ {
+ nlohmann::json::object_t* obj =
+ dataIt->get_ptr<nlohmann::json::object_t*>();
+ if (obj != nullptr)
+ {
+ nlohmann::json::object_t::iterator userIt2 =
+ obj->find("username");
+ nlohmann::json::object_t::iterator passIt2 =
+ obj->find("password");
+ if (userIt2 != obj->end() && passIt2 != obj->end())
+ {
+ const std::string* userStr =
+ userIt2->second.get_ptr<const std::string*>();
+ const std::string* passStr =
+ passIt2->second.get_ptr<const std::string*>();
+ if (userStr != nullptr && passStr != nullptr)
+ {
+ username = *userStr;
+ password = *passStr;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ else if (contentType.starts_with("multipart/form-data"))
+ {
+ ParserError ec = parser.parse(req);
+ if (ec != ParserError::PARSER_SUCCESS)
+ {
+ // handle error
+ BMCWEB_LOG_ERROR("MIME parse failed, ec : {}",
+ static_cast<int>(ec));
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ return;
+ }
+
+ for (const FormPart& formpart : parser.mime_fields)
+ {
+ boost::beast::http::fields::const_iterator it =
+ formpart.fields.find("Content-Disposition");
+ if (it == formpart.fields.end())
+ {
+ BMCWEB_LOG_ERROR("Couldn't find Content-Disposition");
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ continue;
+ }
+
+ BMCWEB_LOG_INFO("Parsing value {}", it->value());
+
+ if (it->value() == "form-data; name=\"username\"")
+ {
+ username = formpart.content;
+ }
+ else if (it->value() == "form-data; name=\"password\"")
+ {
+ password = formpart.content;
+ }
+ else
+ {
+ BMCWEB_LOG_INFO("Extra format, ignore it.{}", it->value());
+ }
+ }
+ }
+ else
+ {
+ // check if auth was provided as a headers
+ username = req.getHeaderValue("username");
+ password = req.getHeaderValue("password");
+ }
+
+ if (!username.empty() && !password.empty())
+ {
+ int pamrc = pamAuthenticateUser(username, password, std::nullopt);
+ bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
+ if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
+ {
+ asyncResp->res.result(boost::beast::http::status::unauthorized);
+ }
+ else
+ {
+ auto session =
+ persistent_data::SessionStore::getInstance()
+ .generateUserSession(username, req.ipAddress, std::nullopt,
+ persistent_data::SessionType::Session,
+ isConfigureSelfOnly);
+
+ bmcweb::setSessionCookies(asyncResp->res, *session);
+
+ // if content type is json, assume json token
+ asyncResp->res.jsonValue["token"] = session->sessionToken;
+ }
+ }
+ else
+ {
+ BMCWEB_LOG_DEBUG("Couldn't interpret password");
+ asyncResp->res.result(boost::beast::http::status::bad_request);
+ }
+}
+
+inline void handleLogout(const crow::Request& req,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
+{
+ const auto& session = req.session;
+ if (session != nullptr)
+ {
+ asyncResp->res.jsonValue["data"] =
+ "User '" + session->username + "' logged out";
+ asyncResp->res.jsonValue["message"] = "200 OK";
+ asyncResp->res.jsonValue["status"] = "ok";
+
+ bmcweb::clearSessionCookies(asyncResp->res);
+ persistent_data::SessionStore::getInstance().removeSession(session);
+ }
+}
+
+inline void requestRoutes(App& app)
+{
+ BMCWEB_ROUTE(app, "/login")
+ .methods(boost::beast::http::verb::post)(handleLogin);
+
+ BMCWEB_ROUTE(app, "/logout")
+ .methods(boost::beast::http::verb::post)(handleLogout);
+}
+} // namespace login_routes
+} // namespace crow
diff --git a/features/webui_login/meson.build b/features/webui_login/meson.build
new file mode 100644
index 0000000..ab60765
--- /dev/null
+++ b/features/webui_login/meson.build
@@ -0,0 +1 @@
+incdir += include_directories('.')