| #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 |