| #include "external_storer.hpp" |
| |
| #include "app.hpp" |
| #include "error_messages.hpp" |
| #include "openbmc_dbus_rest.hpp" |
| #include "query.hpp" |
| #include "registries/privilege_registry.hpp" |
| |
| #include <boost/uuid/uuid.hpp> |
| #include <boost/uuid/uuid_generators.hpp> |
| #include <boost/uuid/uuid_io.hpp> |
| |
| #include <cmath> |
| #include <cstddef> |
| #include <cstdint> |
| #include <fstream> |
| #include <sstream> |
| |
| namespace external_storer |
| { |
| |
| std::filesystem::path safeAppend(const std::filesystem::path& a, |
| const std::filesystem::path& b) |
| { |
| std::filesystem::path result{a}; |
| |
| // Unfortunately, a / b returns surprising/wrong results if b is absolute |
| if (b.is_absolute()) |
| { |
| // The absolute path already starts with necessary directory separator |
| result += b; |
| return result; |
| } |
| |
| result /= b; |
| return result; |
| } |
| |
| std::filesystem::path Hook::locBase() const |
| { |
| return safeAppend(redfishPrefix, pathBase); |
| } |
| |
| std::filesystem::path Hook::locInstance(const std::string& instance) const |
| { |
| return safeAppend(locBase(), instance); |
| } |
| |
| std::filesystem::path Hook::locMiddle(const std::string& instance) const |
| { |
| // The middle component is optional, some schemas might not need it |
| if (pathMiddle.empty()) |
| { |
| return locInstance(instance); |
| } |
| |
| return safeAppend(locInstance(instance), pathMiddle); |
| } |
| |
| std::filesystem::path Hook::locEntry(const std::string& instance, |
| const std::string& entry) const |
| { |
| return safeAppend(locMiddle(instance), entry); |
| } |
| |
| std::filesystem::path Hook::locToFileDir(const std::filesystem::path& loc) const |
| { |
| return safeAppend(pathPrefix, loc); |
| } |
| |
| std::filesystem::path |
| Hook::locToFileJson(const std::filesystem::path& loc) const |
| { |
| // Safe to use / operator here, jsonFilename constant always relative |
| return locToFileDir(loc) / jsonFilename; |
| } |
| |
| // Helper function to get size of an open file, also rewinds file |
| // Uses the C library (not C++) to be called from fileToString |
| std::optional<size_t> fileToSize(FILE* f) |
| { |
| int ret = std::fseek(f, 0L, SEEK_END); |
| if (ret != 0) |
| { |
| int err = errno; |
| BMCWEB_LOG_ERROR << "File failed to seek: " << std::strerror(err); |
| return std::nullopt; |
| } |
| |
| // Assumes file has been opened in binary mode |
| int64_t rawSize = ftell(f); |
| if (rawSize < 0) |
| { |
| int err = errno; |
| BMCWEB_LOG_ERROR << "File failed to tell: " << std::strerror(err); |
| return std::nullopt; |
| } |
| |
| ret = std::fseek(f, 0L, SEEK_SET); |
| if (ret != 0) |
| { |
| int err = errno; |
| BMCWEB_LOG_ERROR << "File failed to seek: " << std::strerror(err); |
| return std::nullopt; |
| } |
| |
| // Historical C library API legacy: long versus size_t |
| auto size = static_cast<size_t>(rawSize); |
| |
| BMCWEB_LOG_DEBUG << "File open success: " << size << " bytes"; |
| return {size}; |
| } |
| |
| // Use the C library (not C++) to read entire file into a C++ string first |
| // Tested earlier to be faster than nlohmann::json::parse() of std::ifstream |
| std::optional<std::string> fileToString(FILE* f) |
| { |
| auto sizeOpt = fileToSize(f); |
| if (!sizeOpt.has_value()) |
| { |
| // Helper function has already printed an error message |
| return std::nullopt; |
| } |
| |
| size_t fileSize = *sizeOpt; |
| |
| if (fileSize == 0) |
| { |
| BMCWEB_LOG_ERROR << "File is empty"; |
| return std::nullopt; |
| } |
| if (fileSize > maxFileSize) |
| { |
| BMCWEB_LOG_ERROR << "File is too large: " << fileSize << "/" |
| << maxFileSize; |
| return std::nullopt; |
| } |
| |
| std::string empty; |
| std::optional<std::string> contentOpt{empty}; |
| |
| // Avoid making expensive copies of string after it gets larger |
| std::string& content = *contentOpt; |
| |
| // Preallocate the buffer to fit the file size |
| content.resize(fileSize); |
| |
| // Read the whole file in one go |
| // C++11 no longer disallows data() from being written to |
| size_t readSize = fread(content.data(), 1, fileSize, f); |
| |
| // Read of 0 must be error, because empty file already excluded earlier |
| if (readSize == 0) |
| { |
| int err = errno; |
| BMCWEB_LOG_ERROR << "File read error: " << std::strerror(err); |
| return std::nullopt; |
| } |
| |
| // Consider a short read to also be an error |
| if (readSize != fileSize) |
| { |
| BMCWEB_LOG_ERROR << "File read size mismatch: " << readSize << "/" |
| << fileSize; |
| return std::nullopt; |
| } |
| |
| BMCWEB_LOG_DEBUG << "File read success: " << readSize << " bytes"; |
| return contentOpt; |
| } |
| |
| std::optional<nlohmann::json> |
| readJsonFile(const std::filesystem::path& filename) |
| { |
| // Open in binary mode to ensure accurate size measurement |
| FILE* f = std::fopen(filename.c_str(), "rb"); |
| if (f == nullptr) |
| { |
| int err = errno; |
| BMCWEB_LOG_ERROR << "File " << filename |
| << " failed to open: " << std::strerror(err); |
| return std::nullopt; |
| } |
| |
| // While we have the file open, use helper function to read it |
| auto bufferOpt = fileToString(f); |
| |
| int ret = std::fclose(f); |
| if (ret != 0) |
| { |
| int err = errno; |
| BMCWEB_LOG_ERROR << "File " << filename |
| << " failed to close: " << std::strerror(err); |
| return std::nullopt; |
| } |
| |
| // All further error checking takes place after the close |
| if (!bufferOpt.has_value()) |
| { |
| // Helper function has already printed an error message |
| return std::nullopt; |
| } |
| |
| const std::string& buffer = *bufferOpt; |
| |
| // Must supply 3rd argument to avoid throwing exceptions |
| nlohmann::json content = nlohmann::json::parse(buffer, nullptr, false); |
| |
| if (content.is_discarded()) |
| { |
| BMCWEB_LOG_ERROR << "File " << filename << " not valid JSON"; |
| return std::nullopt; |
| } |
| |
| if (!(content.is_object())) |
| { |
| BMCWEB_LOG_ERROR << "File " << filename << " not JSON dictionary"; |
| return std::nullopt; |
| } |
| |
| BMCWEB_LOG_DEBUG << "File JSON success: " << filename; |
| return {content}; |
| } |
| |
| std::optional<std::size_t> writeJsonFile(const std::filesystem::path& filename, |
| const nlohmann::json& content) |
| { |
| std::stringstream stream; |
| |
| // Must supply 4th argument to avoid throwing exceptions |
| stream << content.dump(-1, ' ', false, |
| nlohmann::json::error_handler_t::replace); |
| |
| std::string buffer = stream.str(); |
| size_t fileSize = buffer.size(); |
| if (fileSize > maxFileSize) |
| { |
| BMCWEB_LOG_ERROR << "File is too large: " << fileSize << "/" |
| << maxFileSize; |
| return std::nullopt; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Output write size: " << fileSize << " bytes"; |
| |
| std::ofstream output; |
| output.open(filename, std::ofstream::trunc); |
| if (!output) |
| { |
| int err = errno; |
| BMCWEB_LOG_ERROR << "Error opening " << filename |
| << " output: " << strerror(err); |
| return std::nullopt; |
| } |
| |
| output.write(buffer.data(), static_cast<std::streamsize>(fileSize)); |
| |
| bool writeGood = output.good(); |
| |
| if (!writeGood) |
| { |
| // Do not return here, need to defer until after the close |
| int err = errno; |
| BMCWEB_LOG_ERROR << "Error writing " << filename |
| << " output: " << strerror(err); |
| } |
| |
| // Always do this, no matter what, even if write failed |
| output.close(); |
| |
| bool closeGood = output.good(); |
| |
| // This is the deferred error return from write() above |
| if (!writeGood) |
| { |
| // The errno from write() already printed |
| return std::nullopt; |
| } |
| |
| if (!closeGood) |
| { |
| int err = errno; |
| BMCWEB_LOG_ERROR << "Error closing " << filename |
| << " output: " << strerror(err); |
| return std::nullopt; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Output write success: " << filename; |
| return {fileSize}; |
| } |
| |
| // The "proposedName" should be a basename, with no directory separators |
| // Conservative filename rules to begin with, can relax later if needed |
| bool validateFilename(const std::filesystem::path& proposedName) |
| { |
| if (!(crow::openbmc_mapper::validateFilename(proposedName))) |
| { |
| BMCWEB_LOG_ERROR << "Filename contains invalid characters"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool validateFilename(const std::filesystem::path& name, |
| const std::vector<std::string>& denyList) |
| { |
| if (!(validateFilename(name))) |
| { |
| // Error message has already been printed |
| return false; |
| } |
| |
| // Must not be within the denylist |
| if (std::find(denyList.begin(), denyList.end(), name) != denyList.end()) |
| { |
| BMCWEB_LOG_ERROR << "Filename " << name << " is reserved"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| std::string extractId(const nlohmann::json& content) |
| { |
| std::string id; |
| |
| if (content.is_object()) |
| { |
| auto foundId = content.find("Id"); |
| if (foundId != content.end()) |
| { |
| if (foundId->is_string()) |
| { |
| id = foundId.value(); |
| if (!(id.empty())) |
| { |
| return id; |
| } |
| } |
| } |
| } |
| |
| boost::uuids::random_generator gen; |
| |
| // Roll a random UUID for server-assigned ID |
| id = boost::uuids::to_string(gen()); |
| BMCWEB_LOG_INFO << "Generated UUID " << id; |
| |
| return id; |
| } |
| |
| void stripFieldsId(nlohmann::json& content) |
| { |
| if (!(content.is_object())) |
| { |
| return; |
| } |
| |
| // No need, this is already implied by the filename on disk |
| auto foundId = content.find("Id"); |
| if (foundId != content.end()) |
| { |
| content.erase(foundId); |
| } |
| |
| // No need, this will be dynamically built when output to user |
| auto foundOdataId = content.find("@odata.id"); |
| if (foundOdataId != content.end()) |
| { |
| content.erase(foundOdataId); |
| } |
| } |
| |
| void stripFieldsMembers(nlohmann::json& content) |
| { |
| if (!(content.is_object())) |
| { |
| return; |
| } |
| |
| // Entries must be added one at a time, using separate POST commands |
| auto foundMembers = content.find("Members"); |
| if (foundMembers != content.end()) |
| { |
| content.erase(foundMembers); |
| } |
| |
| // No need, this will be dynamically built when output to user |
| auto foundCount = content.find("Members@odata.count"); |
| if (foundCount != content.end()) |
| { |
| content.erase(foundCount); |
| } |
| } |
| |
| void insertResponseLocation(crow::Response& response, |
| const std::string& location) |
| { |
| // Add Location to header |
| response.addHeader(boost::beast::http::field::location, location); |
| |
| // Add Location to body, but must dig through schema first |
| if (!(response.jsonValue.is_object())) |
| { |
| BMCWEB_LOG_ERROR << "No Location because not object"; |
| return; |
| } |
| |
| // ExtendedInfo must already be an array of at least 1 element (object) |
| auto ei = response.jsonValue.find("@Message.ExtendedInfo"); |
| if (ei == response.jsonValue.end()) |
| { |
| BMCWEB_LOG_ERROR << "No Location because no ExtendedInfo"; |
| return; |
| } |
| if (!(ei->is_array())) |
| { |
| BMCWEB_LOG_ERROR << "No Location because ExtendedInfo not array"; |
| return; |
| } |
| if (ei->empty()) |
| { |
| BMCWEB_LOG_ERROR << "No Location because ExtendedInfo empty"; |
| return; |
| } |
| if (!((*ei)[0].is_object())) |
| { |
| BMCWEB_LOG_ERROR |
| << "No Location because ExtendedInfo element not object"; |
| return; |
| } |
| |
| // MessageArgs must be an array, create if it does not already exist |
| auto ma = (*ei)[0].find("MessageArgs"); |
| if (ma == (*ei)[0].end()) |
| { |
| (*ei)[0]["MessageArgs"] = nlohmann::json::array(); |
| ma = (*ei)[0].find("MessageArgs"); |
| } |
| if (!(ma->is_array())) |
| { |
| BMCWEB_LOG_ERROR << "No Location because MessageArgs not array"; |
| return; |
| } |
| |
| ma->emplace_back(location); |
| } |
| |
| void Hook::handleCreateInstance( |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) |
| { |
| nlohmann::json content; |
| content = nlohmann::json::parse(req.body(), nullptr, false); |
| if (content.is_discarded()) |
| { |
| BMCWEB_LOG_ERROR << "Uploaded content not JSON"; |
| redfish::messages::malformedJSON(asyncResp->res); |
| return; |
| } |
| if (!(content.is_object())) |
| { |
| BMCWEB_LOG_ERROR << "Uploaded JSON type not a dictionary"; |
| redfish::messages::unrecognizedRequestBody(asyncResp->res); |
| return; |
| } |
| |
| std::string idInstance = extractId(content); |
| stripFieldsId(content); |
| |
| auto innerContent = nlohmann::json::object(); |
| |
| if (!(pathMiddle.empty())) |
| { |
| // Promote the inner layer to its own JSON object |
| auto foundMiddle = content.find(pathMiddle); |
| if (foundMiddle != content.end()) |
| { |
| innerContent = foundMiddle.value(); |
| content.erase(foundMiddle); |
| |
| if (!(innerContent.is_object())) |
| { |
| BMCWEB_LOG_ERROR << "Interior JSON type not a dictionary"; |
| redfish::messages::unrecognizedRequestBody(asyncResp->res); |
| return; |
| } |
| |
| // Also trim "Id" and "@odata.id" from the inner layer |
| stripFieldsId(innerContent); |
| |
| // Trim "Members" as well, user not allowed bulk upload yet |
| stripFieldsMembers(innerContent); |
| } |
| } |
| |
| if (!(validateFilename(idInstance, denyList))) |
| { |
| BMCWEB_LOG_ERROR << "Uploaded instance ID not acceptable"; |
| redfish::messages::actionParameterValueFormatError( |
| asyncResp->res, idInstance, "Id", "POST"); |
| return; |
| } |
| |
| std::filesystem::path outerUrl = locInstance(idInstance); |
| std::filesystem::path outerDir = locToFileDir(outerUrl); |
| |
| std::filesystem::path outerFilename = locToFileJson(outerUrl); |
| |
| std::filesystem::path innerUrl = locMiddle(idInstance); |
| std::filesystem::path innerDir = locToFileDir(innerUrl); |
| |
| std::filesystem::path innerFilename = locToFileJson(innerUrl); |
| |
| // If no middle keyword, then no need to create multiple layers |
| if (pathMiddle.empty()) |
| { |
| innerDir = outerDir; |
| } |
| |
| std::error_code ec; |
| |
| BMCWEB_LOG_DEBUG << "Create instance " << idInstance << " checking " |
| << outerDir; |
| |
| bool outerExists = std::filesystem::exists(outerFilename, ec); |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << outerFilename |
| << " duplicate: " << ec.message(); |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| // If no middle keyword, only outer file necessary to declare dupe |
| bool innerExists = true; |
| if (!pathMiddle.empty()) |
| { |
| innerExists = std::filesystem::exists(innerFilename, ec); |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << innerFilename |
| << " duplicate: " << ec.message(); |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| } |
| |
| // It is only considered a dupe error if both files already exist |
| if (outerExists && innerExists) |
| { |
| BMCWEB_LOG_ERROR << "Uploaded instance ID already exists on system"; |
| redfish::messages::resourceAlreadyExists(asyncResp->res, "String", "Id", |
| idInstance); |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Create instance " << idInstance << " making " |
| << innerDir; |
| |
| std::filesystem::create_directories(innerDir, ec); |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem making " << innerDir |
| << " directories: " << ec.message(); |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Create instance " << idInstance << " writing " |
| << outerFilename; |
| if (!(writeJsonFile(outerFilename, content).has_value())) |
| { |
| BMCWEB_LOG_ERROR << "Problem writing file " << outerFilename; |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| if (!(pathMiddle.empty())) |
| { |
| BMCWEB_LOG_DEBUG << "Create instance " << idInstance << "/" |
| << pathMiddle << " writing " << innerFilename; |
| if (!(writeJsonFile(innerFilename, innerContent).has_value())) |
| { |
| BMCWEB_LOG_ERROR << "Problem writing file " << innerFilename; |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| } |
| |
| redfish::messages::created(asyncResp->res); |
| |
| insertResponseLocation(asyncResp->res, outerUrl); |
| } |
| |
| void Hook::handleCreateMiddle( |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& idInstance) |
| { |
| // Before doing anything with filesystem, validate naming restrictions |
| if (!(validateFilename(idInstance, denyList))) |
| { |
| BMCWEB_LOG_ERROR << "Instance ID within URL is not acceptable"; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| nlohmann::json content; |
| |
| // Keep this in sync with post-I/O validation in readJsonFile() |
| content = nlohmann::json::parse(req.body(), nullptr, false); |
| if (content.is_discarded()) |
| { |
| BMCWEB_LOG_ERROR << "Uploaded content not JSON"; |
| redfish::messages::malformedJSON(asyncResp->res); |
| return; |
| } |
| if (!(content.is_object())) |
| { |
| BMCWEB_LOG_ERROR << "Uploaded JSON type not a dictionary"; |
| redfish::messages::unrecognizedRequestBody(asyncResp->res); |
| return; |
| } |
| |
| std::string idEntry = extractId(content); |
| |
| // Unlike instance, no need to do a second layer of trimming underneath |
| stripFieldsId(content); |
| |
| // Unlike instance, names on denyList are perfectly OK for entry |
| if (!(validateFilename(idEntry))) |
| { |
| BMCWEB_LOG_ERROR << "Uploaded entry ID not acceptable"; |
| redfish::messages::actionParameterValueFormatError( |
| asyncResp->res, idEntry, "Id", "POST"); |
| return; |
| } |
| |
| std::filesystem::path outerUrl = locInstance(idInstance); |
| std::filesystem::path outerDir = locToFileDir(outerUrl); |
| |
| std::filesystem::path entryUrl = locEntry(idInstance, idEntry); |
| std::filesystem::path entryDir = locToFileDir(entryUrl); |
| |
| std::filesystem::path entryFilename = locToFileJson(entryUrl); |
| |
| std::error_code ec; |
| |
| // The instance must already have been created earlier |
| BMCWEB_LOG_DEBUG << "Create entry " << idInstance << " checking " |
| << outerDir; |
| if (!(std::filesystem::exists(outerDir, ec))) |
| { |
| BMCWEB_LOG_ERROR << "Cannot add entry to nonexistent instance " |
| << idInstance; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << outerDir |
| << " existence: " << ec.message(); |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Create entry " << idInstance << " making " << entryDir; |
| std::filesystem::create_directories(entryDir, ec); |
| |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem making " << entryDir |
| << " directories: " << ec.message(); |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| BMCWEB_LOG_DEBUG << "Create entry " << idInstance << " writing " |
| << entryFilename; |
| if (std::filesystem::exists(entryFilename, ec)) |
| { |
| BMCWEB_LOG_ERROR << "Uploaded entry ID already exists within instance"; |
| redfish::messages::resourceAlreadyExists(asyncResp->res, "String", "Id", |
| idEntry); |
| return; |
| } |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << entryFilename |
| << " duplicate: " << ec.message(); |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| if (!(writeJsonFile(entryFilename, content).has_value())) |
| { |
| BMCWEB_LOG_ERROR << "Problem writing file " << entryFilename; |
| redfish::messages::operationFailed(asyncResp->res); |
| return; |
| } |
| |
| redfish::messages::created(asyncResp->res); |
| |
| insertResponseLocation(asyncResp->res, entryUrl); |
| } |
| |
| void Hook::handleCreateEntry( |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& idInstance, const std::string& keywordMiddle) |
| { |
| // Validate the middle path component in URL is the expected constant |
| if (keywordMiddle != pathMiddle) |
| { |
| BMCWEB_LOG_ERROR << "URL middle path component is not " << pathMiddle; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| // This handler has the same function as if the middle were omitted |
| handleCreateMiddle(req, asyncResp, idInstance); |
| } |
| |
| // Given a dir, list its subdirs, but only those subdirs which are themselves |
| // directories, and contain an "index.json" file within them. |
| // Those "index.json" files are only checked for existence, nothing more. |
| std::vector<std::filesystem::path> |
| listJsonDirs(const std::filesystem::path& dir) |
| { |
| std::vector<std::filesystem::path> files; |
| std::error_code ec; |
| |
| // If containing directory not found, there can be no subdirectories |
| if (!(std::filesystem::exists(dir, ec))) |
| { |
| BMCWEB_LOG_INFO << "Location " << dir << " nonexistent"; |
| return files; |
| } |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << dir |
| << " existence: " << ec.message(); |
| return files; |
| } |
| |
| // Old-style C++ iter loop, to get error checking, not using ranged for |
| for (auto entries = std::filesystem::directory_iterator{dir}; |
| entries != std::filesystem::end(entries); entries.increment(ec)) |
| { |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem with " << dir |
| << " iterating: " << ec.message(); |
| break; |
| } |
| |
| const auto& entry = *entries; |
| |
| // Only match directories |
| if (!(entry.is_directory())) |
| { |
| continue; |
| } |
| |
| auto dirBasename = entry.path().filename(); |
| |
| // Validating against denyList not needed for entry, only for instance |
| if (!(validateFilename(dirBasename))) |
| { |
| continue; |
| } |
| |
| // Safe to use / operator here, jsonFilename constant always relative |
| auto jsonWithin = entry.path() / jsonFilename; |
| |
| // The directory must contain the special JSON filename |
| if (!(std::filesystem::exists(jsonWithin, ec))) |
| { |
| continue; |
| } |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << jsonWithin |
| << " existence: " << ec.message(); |
| continue; |
| } |
| |
| files.emplace_back(dirBasename); |
| } |
| |
| return files; |
| } |
| |
| // Returns all existing instances under this hook, as a list of basenames |
| std::vector<std::string> Hook::listInstances() const |
| { |
| auto instanceDirs = listJsonDirs(locToFileDir(locBase())); |
| |
| std::vector<std::string> result; |
| for (const auto& instanceDir : instanceDirs) |
| { |
| if (!(validateFilename(instanceDir, denyList))) |
| { |
| continue; |
| } |
| |
| result.emplace_back(instanceDir.string()); |
| } |
| |
| return result; |
| } |
| |
| void Hook::handleGetInstance( |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& idInstance) |
| { |
| // If optional middle keyword not in use, instance same as middle |
| if (pathMiddle.empty()) |
| { |
| return handleGetMiddle(asyncResp, idInstance, pathMiddle); |
| } |
| |
| // Before doing anything with filesystem, validate naming restrictions |
| if (!(validateFilename(idInstance, denyList))) |
| { |
| BMCWEB_LOG_ERROR << "Instance ID within URL is not acceptable"; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto outerUrl = locInstance(idInstance); |
| auto outerFilename = locToFileJson(outerUrl); |
| |
| std::error_code ec; |
| |
| BMCWEB_LOG_DEBUG << "Get instance " << idInstance << " checking " |
| << outerFilename; |
| if (!(std::filesystem::exists(outerFilename, ec))) |
| { |
| BMCWEB_LOG_ERROR << "Instance not found with ID " << idInstance; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << outerFilename |
| << " existence: " << ec.message(); |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto contentOpt = readJsonFile(outerFilename); |
| if (!(contentOpt.has_value())) |
| { |
| BMCWEB_LOG_ERROR << "Problem reading file " << outerFilename; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto& content = *contentOpt; |
| |
| // Regenerate these, as they were intentionally trimmed before storage |
| content["Id"] = idInstance; |
| content["@odata.id"] = outerUrl; |
| |
| auto innerUrl = locMiddle(idInstance); |
| |
| // Synthesize a correct link to middle layer |
| auto middleObject = nlohmann::json::object(); |
| middleObject["@odata.id"] = innerUrl; |
| content[pathMiddle] = middleObject; |
| |
| redfish::messages::success(asyncResp->res); |
| |
| asyncResp->res.jsonValue = std::move(content); |
| } |
| |
| void Hook::handleGetMiddle(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& idInstance, |
| const std::string& keywordMiddle) |
| { |
| // Before doing anything with filesystem, validate naming restrictions |
| if (!(validateFilename(idInstance, denyList))) |
| { |
| BMCWEB_LOG_ERROR << "Instance ID within URL is not acceptable"; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| // Validate the middle path component in URL is the expected constant |
| if (keywordMiddle != pathMiddle) |
| { |
| BMCWEB_LOG_ERROR << "URL middle path component is not " << pathMiddle; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto innerUrl = locMiddle(idInstance); |
| auto innerDir = locToFileDir(innerUrl); |
| auto innerFilename = locToFileJson(innerUrl); |
| |
| std::error_code ec; |
| |
| BMCWEB_LOG_DEBUG << "Get middle " << idInstance |
| << (keywordMiddle.empty() ? "" : "/") << keywordMiddle |
| << " checking " << innerFilename; |
| if (!(std::filesystem::exists(innerFilename, ec))) |
| { |
| BMCWEB_LOG_ERROR << "Instance not found with ID " << idInstance; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << idInstance |
| << " existence: " << ec.message(); |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto contentOpt = readJsonFile(innerFilename); |
| if (!(contentOpt.has_value())) |
| { |
| BMCWEB_LOG_ERROR << "Problem reading file " << innerFilename; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto& content = *contentOpt; |
| |
| // Regenerate these, as they were intentionally trimmed before storage |
| content["Id"] = pathMiddle; |
| content["@odata.id"] = innerUrl; |
| |
| // Do not pass denylist in here, it is only for instance, not entry |
| auto files = listJsonDirs(innerDir); |
| |
| // Synthesize special "Members" array with links to all our entries |
| auto membersArray = nlohmann::json::array(); |
| for (const auto& file : files) |
| { |
| // Safe to use / operator here, "file" already known to be relative |
| std::filesystem::path entryUrl = innerUrl / file; |
| |
| auto fileObject = nlohmann::json::object(); |
| fileObject["@odata.id"] = entryUrl; |
| fileObject["Id"] = file; |
| |
| // Automatically expand only the fields listed in expandList |
| if (!(expandList.empty())) |
| { |
| std::filesystem::path entryFilename = locToFileJson(entryUrl); |
| auto entryContentOpt = readJsonFile(entryFilename); |
| if (entryContentOpt.has_value()) |
| { |
| auto entryContent = *entryContentOpt; |
| |
| for (const auto& key : expandList) |
| { |
| auto valueIter = entryContent.find(key); |
| if (valueIter != entryContent.end()) |
| { |
| fileObject[key] = *valueIter; |
| } |
| } |
| } |
| } |
| |
| membersArray += fileObject; |
| } |
| |
| // Finish putting the pieces together |
| content["Members"] = membersArray; |
| content["Members@odata.count"] = files.size(); |
| |
| redfish::messages::success(asyncResp->res); |
| |
| asyncResp->res.jsonValue = std::move(content); |
| } |
| |
| void Hook::handleGetEntry(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const nlohmann::json::json_pointer& jsonPtr, |
| const std::string& idInstance, |
| const std::string& keywordMiddle, |
| const std::string& idEntry) |
| { |
| // Before doing anything with filesystem, validate naming restrictions |
| if (!(validateFilename(idInstance, denyList))) |
| { |
| BMCWEB_LOG_ERROR << "Instance ID within URL is not acceptable"; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| // Validate the middle path component in URL is the expected constant |
| if (keywordMiddle != pathMiddle) |
| { |
| BMCWEB_LOG_ERROR << "URL middle path component is not " << pathMiddle; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| // Unlike instance, names on denyList are perfectly OK at this layer |
| if (!(validateFilename(idEntry))) |
| { |
| BMCWEB_LOG_ERROR << "Entry ID within URL not acceptable"; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto entryUrl = locEntry(idInstance, idEntry); |
| auto entryFilename = locToFileJson(entryUrl); |
| |
| std::error_code ec; |
| |
| BMCWEB_LOG_DEBUG << "Get entry " << idInstance |
| << (keywordMiddle.empty() ? "" : "/") << keywordMiddle |
| << "/" << idEntry << " checking " << entryFilename; |
| const auto doesExist = (std::filesystem::exists(entryFilename, ec)); |
| if (!doesExist) |
| { |
| BMCWEB_LOG_ERROR << "Entry not found with ID " << idEntry; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem checking for " << idEntry |
| << " existence: " << ec.message(); |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto contentOpt = readJsonFile(entryFilename); |
| if (!(contentOpt.has_value())) |
| { |
| BMCWEB_LOG_ERROR << "Problem reading file " << entryFilename; |
| asyncResp->res.result(boost::beast::http::status::not_found); |
| return; |
| } |
| |
| auto& content = *contentOpt; |
| |
| // Regenerate these, as they were intentionally trimmed before storage |
| content["Id"] = idEntry; |
| content["@odata.id"] = entryUrl; |
| |
| redfish::messages::success(asyncResp->res); |
| |
| asyncResp->res.jsonValue[jsonPtr] = std::move(content); |
| } |
| |
| void Hook::deleteAll() |
| { |
| std::error_code ec; |
| |
| auto count = std::filesystem::remove_all(pathPrefix, ec); |
| |
| if (ec) |
| { |
| BMCWEB_LOG_ERROR << "Problem with " << pathPrefix |
| << " deleting: " << ec.message(); |
| } |
| |
| if (count > 0) |
| { |
| BMCWEB_LOG_INFO << "Deleted all " << count << " files/dirs from " |
| << pathPrefix; |
| } |
| } |
| |
| void Hook::setPathPrefix(const std::filesystem::path& newPrefix) |
| { |
| // This function is only for testing, loudly warn if used |
| BMCWEB_LOG_WARNING << "Changing path prefix to " << newPrefix; |
| |
| pathPrefix = newPrefix; |
| } |
| |
| // Constructs a hook with known-good settings for usage with LogServices |
| Hook makeLogServices() |
| { |
| const std::string pathBase{"Systems/system/LogServices"}; |
| const std::string midWord{"Entries"}; |
| |
| // These names come from requestRoutesSystemLogServiceCollection() |
| std::vector<std::string> denyList{"EventLog", "Dump", "Crashdump", |
| "HostLogger"}; |
| |
| // These names come from the "required" field of LogEntry JSON schema |
| std::vector<std::string> expandList{"EntryType", "@odata.id", "@odata.type", |
| "Id", "Name"}; |
| |
| // Additional useful names to pre-expand |
| expandList.emplace_back("Created"); |
| |
| return {pathBase, midWord, denyList, expandList}; |
| } |
| |
| std::shared_ptr<Hook> |
| rememberLogServices(const std::shared_ptr<Hook>& hookIncoming) |
| { |
| static std::shared_ptr<Hook> hookLogServices = nullptr; |
| |
| // If incoming pointer is valid, remember it for next time |
| if (hookIncoming) |
| { |
| hookLogServices = hookIncoming; |
| } |
| |
| return hookLogServices; |
| } |
| |
| } // namespace external_storer |
| |
| namespace redfish |
| { |
| |
| inline void handleLogServiceInstancePost( |
| App& app, const std::shared_ptr<external_storer::Hook>& hook, |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& systemName) |
| { |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) |
| { |
| return; |
| } |
| |
| if (systemName != "system") |
| { |
| messages::resourceNotFound(asyncResp->res, "ComputerSystem", |
| systemName); |
| return; |
| } |
| |
| hook->handleCreateInstance(req, asyncResp); |
| } |
| |
| inline void handleLogServiceMiddlePost( |
| App& app, const std::shared_ptr<external_storer::Hook>& hook, |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& systemName, const std::string& instance) |
| { |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) |
| { |
| return; |
| } |
| |
| if (systemName != "system") |
| { |
| messages::resourceNotFound(asyncResp->res, "ComputerSystem", |
| systemName); |
| return; |
| } |
| |
| hook->handleCreateMiddle(req, asyncResp, instance); |
| } |
| |
| inline void handleLogServiceEntryPost( |
| App& app, const std::shared_ptr<external_storer::Hook>& hook, |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& systemName, const std::string& instance, |
| const std::string& middle) |
| { |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) |
| { |
| return; |
| } |
| |
| if (systemName != "system") |
| { |
| messages::resourceNotFound(asyncResp->res, "ComputerSystem", |
| systemName); |
| return; |
| } |
| |
| hook->handleCreateEntry(req, asyncResp, instance, middle); |
| } |
| |
| inline void handleLogServiceInstanceGet( |
| App& app, const std::shared_ptr<external_storer::Hook>& hook, |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& systemName, const std::string& instance) |
| { |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) |
| { |
| return; |
| } |
| |
| if (systemName != "system") |
| { |
| messages::resourceNotFound(asyncResp->res, "ComputerSystem", |
| systemName); |
| return; |
| } |
| |
| hook->handleGetInstance(asyncResp, instance); |
| } |
| |
| inline void handleLogServiceMiddleGet( |
| App& app, const std::shared_ptr<external_storer::Hook>& hook, |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& systemName, const std::string& instance, |
| const std::string& middle) |
| { |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) |
| { |
| return; |
| } |
| |
| if (systemName != "system") |
| { |
| messages::resourceNotFound(asyncResp->res, "ComputerSystem", |
| systemName); |
| return; |
| } |
| |
| hook->handleGetMiddle(asyncResp, instance, middle); |
| } |
| |
| inline void handleLogServiceEntryGet( |
| App& app, const std::shared_ptr<external_storer::Hook>& hook, |
| const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string& systemName, const std::string& instance, |
| const std::string& middle, const std::string& entry) |
| { |
| if (!redfish::setUpRedfishRoute(app, req, asyncResp)) |
| { |
| return; |
| } |
| |
| if (systemName != "system") |
| { |
| messages::resourceNotFound(asyncResp->res, "ComputerSystem", |
| systemName); |
| return; |
| } |
| |
| hook->handleGetEntry(asyncResp, ""_json_pointer, instance, middle, entry); |
| } |
| |
| void requestRoutesExternalStorerLogServices( |
| App& app, const std::shared_ptr<external_storer::Hook>& hook) |
| { |
| // Only 0-argument, 1-argument, and 2-argument POST routes exist |
| // There intentionally is no 3-argument POST handler |
| BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/") |
| .privileges(redfish::privileges::postLogService) |
| .methods(boost::beast::http::verb::post)( |
| std::bind_front(handleLogServiceInstancePost, std::ref(app), hook)); |
| |
| BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/<str>/") |
| .privileges(redfish::privileges::postLogService) |
| .methods(boost::beast::http::verb::post)( |
| std::bind_front(handleLogServiceMiddlePost, std::ref(app), hook)); |
| |
| BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/<str>/<str>/") |
| .privileges(redfish::privileges::postLogService) |
| .methods(boost::beast::http::verb::post)( |
| std::bind_front(handleLogServiceEntryPost, std::ref(app), hook)); |
| |
| // Only 1-argument, 2-argument, and 3-argument GET routes are here |
| // The 0-argument GET route is already handled by the integration point |
| // It is at log_services.hpp requestRoutesSystemLogServiceCollection() |
| BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/<str>/") |
| .privileges(redfish::privileges::getLogService) |
| .methods(boost::beast::http::verb::get)( |
| std::bind_front(handleLogServiceInstanceGet, std::ref(app), hook)); |
| |
| BMCWEB_ROUTE(app, "/redfish/v1/Systems/<str>/LogServices/<str>/<str>/") |
| .privileges(redfish::privileges::getLogService) |
| .methods(boost::beast::http::verb::get)( |
| std::bind_front(handleLogServiceMiddleGet, std::ref(app), hook)); |
| |
| BMCWEB_ROUTE(app, |
| "/redfish/v1/Systems/<str>/LogServices/<str>/<str>/<str>/") |
| .privileges(redfish::privileges::getLogService) |
| .methods(boost::beast::http::verb::get)( |
| std::bind_front(handleLogServiceEntryGet, std::ref(app), hook)); |
| |
| // The integration point also needs to access the correct hook |
| external_storer::rememberLogServices(hook); |
| } |
| |
| void requestRoutesExternalStorer(App& app) |
| { |
| auto hookLogServices = std::make_shared<external_storer::Hook>( |
| external_storer::makeLogServices()); |
| |
| // The shared_ptr will be copied, stretching out its lifetime |
| requestRoutesExternalStorerLogServices(app, hookLogServices); |
| } |
| |
| } // namespace redfish |