| #include "query.hpp" | |
| #include <algorithm> | |
| #include <array> | |
| #include <cctype> | |
| #include <charconv> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <functional> | |
| #include <iterator> | |
| #include <limits> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <string_view> | |
| #include <system_error> // NOLINT | |
| #include <unordered_set> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/synchronization/mutex.h" | |
| #include "boost/beast/http/verb.hpp" // NOLINT | |
| #include "boost/system/error_code.hpp" // NOLINT | |
| #include "boost/url/params_view.hpp" // NOLINT | |
| #include "boost/url/url_view.hpp" // NOLINT | |
| #include "bmcweb_config.h" | |
| #include "app.hpp" | |
| #include "http_request.hpp" | |
| #include "http_response.hpp" | |
| #include "logging.hpp" | |
| #include "async_resp.hpp" | |
| #include "str_utility.hpp" | |
| #include "error_messages.hpp" | |
| #include "redfish_aggregator.hpp" // NOLINT | |
| #include <nlohmann/json.hpp> | |
| namespace redfish { | |
| namespace query_param { | |
| // Validates the property in the $select parameter. Every character is among | |
| // [a-zA-Z0-9#@_.] (taken from Redfish spec, section 9.6 Properties) | |
| bool isSelectedPropertyAllowed(std::string_view property) { | |
| // These a magic number, but with it it's less likely that this code | |
| // introduces CVE; e.g., too large properties crash the service. | |
| constexpr int maxPropertyLength = 60; | |
| if (property.empty() || property.size() > maxPropertyLength) { | |
| return false; | |
| } | |
| for (char ch : property) { | |
| if (std::isalnum(static_cast<unsigned char>(ch)) == 0 && ch != '#' && | |
| ch != '@' && ch != '.') { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool SelectTrie::insertNode(std::string_view nestedProperty) { | |
| if (nestedProperty.empty()) { | |
| return false; | |
| } | |
| SelectTrieNode* currNode = &root; | |
| size_t index = nestedProperty.find_first_of('/'); | |
| while (!nestedProperty.empty()) { | |
| std::string_view property = nestedProperty.substr(0, index); | |
| if (!isSelectedPropertyAllowed(property)) { | |
| return false; | |
| } | |
| currNode = currNode->emplace(property); | |
| if (index == std::string::npos) { | |
| break; | |
| } | |
| nestedProperty.remove_prefix(index + 1); | |
| index = nestedProperty.find_first_of('/'); | |
| } | |
| currNode->setToSelected(); | |
| return true; | |
| } | |
| // Delegates query parameters according to the given |queryCapabilities| | |
| // This function doesn't check query parameter conflicts since the parse | |
| // function will take care of it. | |
| // Returns a delegated query object which can be used by individual resource | |
| // handlers so that handlers don't need to query again. | |
| Query delegate(const QueryCapabilities& queryCapabilities, Query& query, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { | |
| Query delegated; | |
| // delegate only | |
| if (query.isOnly && queryCapabilities.canDelegateOnly) { | |
| delegated.isOnly = true; | |
| query.isOnly = false; | |
| } | |
| // delegate expand as much as we can | |
| if (query.expandType != ExpandType::None) { | |
| delegated.expandType = query.expandType; | |
| if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel) { | |
| query.expandType = ExpandType::None; | |
| delegated.expandLevel = query.expandLevel; | |
| query.expandLevel = 0; | |
| } else { | |
| // We actually dont need to subtract query.expandType here because | |
| // When we expand navigation references, we actually start from | |
| // the root of the entire response instead of the delegated level | |
| delegated.expandLevel = queryCapabilities.canDelegateExpandLevel; | |
| } | |
| asyncResp->delegatedExpandLevel = delegated.expandLevel; | |
| } | |
| // delegate top | |
| if (query.top && queryCapabilities.canDelegateTop) { | |
| delegated.top = query.top; | |
| query.top = std::nullopt; | |
| } | |
| // delegate skip | |
| if (query.skip && queryCapabilities.canDelegateSkip) { | |
| delegated.skip = query.skip; | |
| query.skip = 0; | |
| } | |
| // delegate select | |
| if (!query.selectTrie.root.empty() && queryCapabilities.canDelegateSelect) { | |
| delegated.selectTrie = std::move(query.selectTrie); | |
| query.selectTrie.root.clear(); | |
| } | |
| // delegate filter | |
| if (!query.filter.empty() && queryCapabilities.canDelegateFilter) { | |
| delegated.filter = query.filter; | |
| query.filter = ""; | |
| } | |
| return delegated; | |
| } | |
| QueryError getExpandType(std::string_view value, Query& query) { | |
| if (value.empty()) { | |
| return QueryError::ValueFormat; | |
| } | |
| switch (value[0]) { | |
| case '*': | |
| query.expandType = ExpandType::Both; | |
| break; | |
| case '.': | |
| query.expandType = ExpandType::NotLinks; | |
| break; | |
| case '~': | |
| query.expandType = ExpandType::Links; | |
| break; | |
| default: | |
| return QueryError::ValueFormat; | |
| } | |
| value.remove_prefix(1); | |
| if (value.empty()) { | |
| query.expandLevel = 1; | |
| return QueryError::Ok; | |
| } | |
| constexpr std::string_view levels = "($levels="; | |
| if (!value.starts_with(levels)) { | |
| return QueryError::ValueFormat; | |
| } | |
| value.remove_prefix(levels.size()); | |
| if (value.back() != ')') { | |
| return QueryError::ValueFormat; | |
| } | |
| std::string_view numberStr = value.substr(0, value.size() - 1); | |
| int8_t expandLevel = 0; | |
| auto it = std::from_chars(numberStr.data(), | |
| numberStr.data() + numberStr.size(), expandLevel); | |
| if (it.ec == std::errc::result_out_of_range) { | |
| return QueryError::OutOfRange; | |
| } | |
| if (it.ec != std::errc() || it.ptr != numberStr.data() + numberStr.size()) { | |
| return QueryError::ValueFormat; | |
| } | |
| if (expandLevel < Query::kMinExpandLevel || | |
| expandLevel > Query::kMaxExpandLevel) { | |
| return QueryError::OutOfRange; | |
| } | |
| query.expandLevel = static_cast<uint8_t>(expandLevel); | |
| return QueryError::Ok; | |
| } | |
| QueryError getNumericParam(std::string_view value, size_t& param) { | |
| std::from_chars_result r = | |
| std::from_chars(value.data(), value.data() + value.size(), param); | |
| // If the number wasn't representable in the type, it's out of range | |
| if (r.ec == std::errc::result_out_of_range) { | |
| return QueryError::OutOfRange; | |
| } | |
| // All other errors are value format | |
| if (r.ec != std::errc()) { | |
| return QueryError::ValueFormat; | |
| } | |
| return QueryError::Ok; | |
| } | |
| QueryError getSkipParam(std::string_view value, Query& query) { | |
| return getNumericParam(value, query.skip.emplace()); | |
| } | |
| QueryError getTopParam(std::string_view value, Query& query) { | |
| QueryError ret = getNumericParam(value, query.top.emplace()); | |
| if (ret != QueryError::Ok) { | |
| return ret; | |
| } | |
| // Range check for sanity. | |
| if (query.top > Query::maxTop) { | |
| return QueryError::OutOfRange; | |
| } | |
| return QueryError::Ok; | |
| } | |
| // Parses and validates the $select parameter. | |
| // As per OData URL Conventions and Redfish Spec, the $select values shall be | |
| // comma separated Resource Path | |
| // Ref: | |
| // 1. https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 | |
| // 2. | |
| // https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt | |
| bool getSelectParam(std::string_view value, Query& query) { | |
| std::vector<std::string> properties; | |
| bmcweb::split(properties, value, ','); | |
| if (properties.empty()) { | |
| return false; | |
| } | |
| // These a magic number, but with it it's less likely that this code | |
| // introduces CVE; e.g., too large properties crash the service. | |
| constexpr int maxNumProperties = 10; | |
| if (properties.size() > maxNumProperties) { | |
| return false; | |
| } | |
| for (const auto& property : properties) { | |
| if (!query.selectTrie.insertNode(property)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool getFilterParam(std::string_view value, Query& query) { | |
| if (value.empty()) { | |
| return false; | |
| } | |
| query.filter = std::string(value); | |
| return true; | |
| } | |
| std::optional<Query> parseParameters(boost::urls::params_view urlParams, | |
| crow::Response& res) { | |
| Query ret; | |
| for (const boost::urls::params_view::value_type& it : urlParams) { | |
| if (it.key == "only") { | |
| if (!it.value.empty()) { | |
| messages::queryParameterValueFormatError(res, it.value, it.key); | |
| return std::nullopt; | |
| } | |
| ret.isOnly = true; | |
| } else if (it.key == "$expand" && bmcwebInsecureEnableQueryParams) { | |
| QueryError expandRet = getExpandType(it.value, ret); | |
| if (expandRet == QueryError::ValueFormat) { | |
| messages::queryParameterValueFormatError(res, it.value, it.key); | |
| return std::nullopt; | |
| } | |
| if (expandRet == QueryError::OutOfRange) { | |
| messages::queryParameterOutOfRange( | |
| res, it.value, "$expand", | |
| absl::StrCat(Query::kMinExpandLevel, "-", Query::kMaxExpandLevel)); | |
| return std::nullopt; | |
| } | |
| } else if (it.key == "$top") { | |
| QueryError topRet = getTopParam(it.value, ret); | |
| if (topRet == QueryError::ValueFormat) { | |
| messages::queryParameterValueFormatError(res, it.value, it.key); | |
| return std::nullopt; | |
| } | |
| if (topRet == QueryError::OutOfRange) { | |
| messages::queryParameterOutOfRange( | |
| res, it.value, "$top", "0-" + std::to_string(Query::maxTop)); | |
| return std::nullopt; | |
| } | |
| } else if (it.key == "$skip") { | |
| QueryError topRet = getSkipParam(it.value, ret); | |
| if (topRet == QueryError::ValueFormat) { | |
| messages::queryParameterValueFormatError(res, it.value, it.key); | |
| return std::nullopt; | |
| } | |
| if (topRet == QueryError::OutOfRange) { | |
| messages::queryParameterOutOfRange( | |
| res, it.value, it.key, | |
| "0-" + std::to_string(std::numeric_limits<size_t>::max())); | |
| return std::nullopt; | |
| } | |
| } else if (it.key == "$select") { | |
| if (!getSelectParam(it.value, ret)) { | |
| messages::queryParameterValueFormatError(res, it.value, it.key); | |
| return std::nullopt; | |
| } | |
| } else if (it.key == "$filter") { | |
| if (!getFilterParam(it.value, ret)) { | |
| messages::queryParameterValueFormatError(res, it.value, it.key); | |
| return std::nullopt; | |
| } | |
| } else { | |
| // Intentionally ignore other errors Redfish spec, 7.3.1 | |
| if (it.key.starts_with("$")) { | |
| // Services shall return... The HTTP 501 Not Implemented | |
| // status code for any unsupported query parameters that | |
| // start with $ . | |
| messages::queryParameterValueFormatError(res, it.value, it.key); | |
| res.result(boost::beast::http::status::not_implemented); | |
| return std::nullopt; | |
| } | |
| // "Shall ignore unknown or unsupported query parameters that do | |
| // not begin with $ ." | |
| } | |
| } | |
| if (ret.expandType != ExpandType::None && !ret.selectTrie.root.empty()) { | |
| messages::queryCombinationInvalid(res); | |
| return std::nullopt; | |
| } | |
| return ret; | |
| } | |
| bool processOnly( | |
| crow::App& app, crow::Response& res, | |
| std::function<void(crow::Response&)>& completionHandler, | |
| const std::shared_ptr<boost::asio::io_context::strand>& strand) { | |
| BMCWEB_LOG_DEBUG << "Processing only query param"; | |
| auto itMembers = res.jsonValue.find("Members"); | |
| if (itMembers == res.jsonValue.end()) { | |
| messages::queryNotSupportedOnResource(res); | |
| completionHandler(res); | |
| return false; | |
| } | |
| auto itMemBegin = itMembers->begin(); | |
| if (itMemBegin == itMembers->end() || itMembers->size() != 1) { | |
| BMCWEB_LOG_DEBUG << "Members contains " << itMembers->size() | |
| << " element, returning full collection."; | |
| completionHandler(res); | |
| return false; | |
| } | |
| auto itUrl = itMemBegin->find("@odata.id"); | |
| if (itUrl == itMemBegin->end()) { | |
| BMCWEB_LOG_DEBUG << "No found odata.id"; | |
| messages::internalError(res); | |
| completionHandler(res); | |
| return false; | |
| } | |
| const std::string* url = itUrl->get_ptr<const std::string*>(); | |
| if (url == nullptr) { | |
| BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????"; | |
| messages::internalError(res); | |
| completionHandler(res); | |
| return false; | |
| } | |
| // TODO(Ed) copy request headers? | |
| // newReq.session = req.session; | |
| std::error_code ec; | |
| crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec); | |
| if (ec) { | |
| messages::internalError(res); | |
| completionHandler(res); | |
| return false; | |
| } | |
| auto asyncResp = std::make_shared<bmcweb::AsyncResp>(strand); | |
| BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res; | |
| asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); | |
| asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper()); | |
| app.handle(newReq, asyncResp); | |
| return true; | |
| } | |
| // Walks a json object looking for Redfish NavigationReference entries that | |
| // might need resolved. It recursively walks the jsonResponse object, looking | |
| // for links at every level, and returns a list (out) of locations within the | |
| // tree that need to be expanded. The current json pointer location p is passed | |
| // in to reference the current node that's being expanded, so it can be combined | |
| // with the keys from the jsonResponse object | |
| void findNavigationReferencesRecursive(ExpandType eType, | |
| nlohmann::json& jsonResponse, | |
| const nlohmann::json::json_pointer& p, | |
| int depth, bool inLinks, | |
| std::vector<ExpandNode>& out) { | |
| // If no expand is needed, return early | |
| if (eType == ExpandType::None) { | |
| return; | |
| } | |
| nlohmann::json::array_t* array = | |
| jsonResponse.get_ptr<nlohmann::json::array_t*>(); | |
| if (array != nullptr) { | |
| size_t index = 0; | |
| // For arrays, walk every element in the array | |
| for (auto& element : *array) { | |
| nlohmann::json::json_pointer newPtr = p / index; | |
| BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string(); | |
| findNavigationReferencesRecursive(eType, element, newPtr, depth, inLinks, | |
| out); | |
| index++; | |
| } | |
| } | |
| nlohmann::json::object_t* obj = | |
| jsonResponse.get_ptr<nlohmann::json::object_t*>(); | |
| if (obj == nullptr) { | |
| return; | |
| } | |
| // Navigation References only ever have a single element | |
| if (obj->size() == 1) { | |
| if (obj->begin()->first == "@odata.id") { | |
| const std::string* uri = | |
| obj->begin()->second.get_ptr<const std::string*>(); | |
| if (uri != nullptr) { | |
| BMCWEB_LOG_DEBUG << "Found " << *uri << " at " << p.to_string(); | |
| out.push_back({p, *uri}); | |
| return; | |
| } | |
| } | |
| } | |
| int newDepth = depth; | |
| auto odataId = obj->find("@odata.id"); | |
| if (odataId != obj->end()) { | |
| // The Redfish spec requires all resources to include the resource | |
| // identifier. If the object has multiple elements and one of them is | |
| // "@odata.id" then that means we have entered a new level / expanded | |
| // resource. We need to stop traversing if we're already at the desired | |
| // depth | |
| if ((obj->size() > 1) && (depth == 0)) { | |
| return; | |
| } | |
| newDepth--; | |
| } | |
| // Loop the object and look for links | |
| for (auto& element : *obj) { | |
| bool localInLinks = inLinks; | |
| if (!localInLinks) { | |
| // Check if this is a links node | |
| localInLinks = element.first == "Links"; | |
| } | |
| // Only traverse the parts of the tree the user asked for | |
| // Per section 7.3 of the redfish specification | |
| if (localInLinks && eType == ExpandType::NotLinks) { | |
| continue; | |
| } | |
| if (!localInLinks && eType == ExpandType::Links) { | |
| continue; | |
| } | |
| nlohmann::json::json_pointer newPtr = p / element.first; | |
| BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr; | |
| findNavigationReferencesRecursive(eType, element.second, newPtr, newDepth, | |
| localInLinks, out); | |
| } | |
| } | |
| // TODO: When aggregation is enabled and we receive a partially expanded | |
| // response we may need need additional handling when the original URI was | |
| // up tree from a top level collection. | |
| // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556 | |
| // lands. May want to avoid forwarding query params when request is uptree from | |
| // a top level collection. | |
| std::vector<ExpandNode> findNavigationReferences(ExpandType eType, int depth, | |
| nlohmann::json& jsonResponse) { | |
| std::vector<ExpandNode> ret; | |
| const nlohmann::json::json_pointer root = nlohmann::json::json_pointer(""); | |
| findNavigationReferencesRecursive(eType, jsonResponse, root, depth, false, | |
| ret); | |
| return ret; | |
| } | |
| // Formats a query parameter string for the sub-query. | |
| // Returns std::nullopt on failures. | |
| // This function shall handle $select when it is added. | |
| // There is no need to handle parameters that's not campatible with $expand, | |
| // e.g., $only, since this function will only be called in side $expand handlers | |
| std::optional<std::string> formatQueryForExpand(const Query& query) { | |
| // query.expandLevel<=1: no need to do subqueries | |
| if (query.expandLevel <= 1) { | |
| return ""; | |
| } | |
| std::string str = "?$expand="; | |
| bool queryTypeExpected = false; | |
| switch (query.expandType) { | |
| case ExpandType::None: | |
| return ""; | |
| case ExpandType::Links: | |
| queryTypeExpected = true; | |
| str += '~'; | |
| break; | |
| case ExpandType::NotLinks: | |
| queryTypeExpected = true; | |
| str += '.'; | |
| break; | |
| case ExpandType::Both: | |
| queryTypeExpected = true; | |
| str += '*'; | |
| break; | |
| } | |
| if (!queryTypeExpected) { | |
| return std::nullopt; | |
| } | |
| str += "($levels="; | |
| str += std::to_string(query.expandLevel - 1); | |
| str += ')'; | |
| return str; | |
| } | |
| // Propogates the worst error code to the final response. | |
| // The order of error code is (from high to low) | |
| // 500 Internal Server Error | |
| // 511 Network Authentication Required | |
| // 510 Not Extended | |
| // 508 Loop Detected | |
| // 507 Insufficient Storage | |
| // 506 Variant Also Negotiates | |
| // 505 HTTP Version Not Supported | |
| // 504 Gateway Timeout | |
| // 503 Service Unavailable | |
| // 502 Bad Gateway | |
| // 501 Not Implemented | |
| // 401 Unauthorized | |
| // 451 - 409 Error codes (not listed explictly) | |
| // 408 Request Timeout | |
| // 407 Proxy Authentication Required | |
| // 406 Not Acceptable | |
| // 405 Method Not Allowed | |
| // 404 Not Found | |
| // 403 Forbidden | |
| // 402 Payment Required | |
| // 400 Bad Request | |
| unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode) { | |
| // We keep a explicit list for error codes that this project often uses | |
| // Higer priority codes are in lower indexes | |
| constexpr std::array<unsigned, 13> orderedCodes = { | |
| 500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400}; | |
| size_t finalCodeIndex = std::numeric_limits<size_t>::max(); | |
| size_t subResponseCodeIndex = std::numeric_limits<size_t>::max(); | |
| for (size_t i = 0; i < orderedCodes.size(); ++i) { | |
| if (orderedCodes[i] == finalCode) { | |
| finalCodeIndex = i; | |
| } | |
| if (orderedCodes[i] == subResponseCode) { | |
| subResponseCodeIndex = i; | |
| } | |
| } | |
| if (finalCodeIndex != std::numeric_limits<size_t>::max() && | |
| subResponseCodeIndex != std::numeric_limits<size_t>::max()) { | |
| return finalCodeIndex <= subResponseCodeIndex ? finalCode : subResponseCode; | |
| } | |
| if (subResponseCode == 500 || finalCode == 500) { | |
| return 500; | |
| } | |
| if (subResponseCode > 500 || finalCode > 500) { | |
| return std::max(finalCode, subResponseCode); | |
| } | |
| if (subResponseCode == 401) { | |
| return subResponseCode; | |
| } | |
| return std::max(finalCode, subResponseCode); | |
| } | |
| // Propogates all error messages into |finalResponse| | |
| void propogateError(crow::Response& finalResponse, | |
| crow::Response& subResponse) { | |
| // no errors | |
| if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400) { | |
| return; | |
| } | |
| messages::moveErrorsToErrorJson(finalResponse.jsonValue, | |
| subResponse.jsonValue); | |
| finalResponse.result( | |
| propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt())); | |
| } | |
| class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp> { | |
| public: | |
| // This object takes a single asyncResp object as the "final" one, then | |
| // allows callers to attach sub-responses within the json tree that need | |
| // to be executed and filled into their appropriate locations. This | |
| // class manages the final "merge" of the json resources. | |
| MultiAsyncResp(crow::App& appIn, | |
| std::shared_ptr<bmcweb::AsyncResp> finalResIn, | |
| const crow::Request& request) | |
| : app(appIn), | |
| finalRes(std::move(finalResIn)), | |
| fromGrpc(request.fromGrpc), | |
| with_trust_bundle(request.with_trust_bundle), | |
| peer_authenticated(request.peer_authenticated), | |
| peer_privileges(request.peer_privileges) {} | |
| void addAwaitingResponse( | |
| const std::shared_ptr<bmcweb::AsyncResp>& res, | |
| const nlohmann::json::json_pointer& finalExpandLocation) { | |
| res->res.setCompleteRequestHandler(std::bind_front( | |
| placeResultStatic, shared_from_this(), finalExpandLocation)); | |
| } | |
| void placeResult(const nlohmann::json::json_pointer& locationToPlace, | |
| crow::Response& res) { | |
| propogateError(finalRes->res, res); | |
| if (!res.jsonValue.is_object() || res.jsonValue.empty()) { | |
| return; | |
| } | |
| nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace]; | |
| finalObj = std::move(res.jsonValue); | |
| BMCWEB_LOG_DEBUG << "placeResult for " << locationToPlace; | |
| } | |
| // Handles the very first level of Expand, and starts a chain of sub-queries | |
| // for deeper levels. | |
| void startQuery(const Query& query) { | |
| std::vector<ExpandNode> nodes = findNavigationReferences( | |
| query.expandType, query.expandLevel, finalRes->res.jsonValue); | |
| BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse"; | |
| const std::optional<std::string> queryStr = formatQueryForExpand(query); | |
| if (!queryStr) { | |
| messages::internalError(finalRes->res); | |
| return; | |
| } | |
| for (const ExpandNode& node : nodes) { | |
| const std::string subQuery = node.uri + *queryStr; | |
| BMCWEB_LOG_DEBUG << "URL of subquery: " << subQuery; | |
| std::error_code ec; | |
| crow::Request newReq({boost::beast::http::verb::get, subQuery, 11}, ec); | |
| if (ec) { | |
| messages::internalError(finalRes->res); | |
| return; | |
| } | |
| newReq.fromGrpc = fromGrpc; | |
| newReq.with_trust_bundle = with_trust_bundle; | |
| newReq.peer_authenticated = peer_authenticated; | |
| newReq.peer_privileges = peer_privileges; | |
| auto asyncResp = std::make_shared<bmcweb::AsyncResp>(finalRes->strand_); | |
| BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res; | |
| addAwaitingResponse(asyncResp, node.location); | |
| BMCWEB_LOG_DEBUG << "Single-threaded expand"; | |
| app.handle(newReq, asyncResp); | |
| } | |
| } | |
| private: | |
| static void placeResultStatic( | |
| const std::shared_ptr<MultiAsyncResp>& multi, | |
| const nlohmann::json::json_pointer& locationToPlace, | |
| crow::Response& res) { | |
| multi->placeResult(locationToPlace, res); | |
| } | |
| crow::App& app; | |
| std::shared_ptr<bmcweb::AsyncResp> finalRes; | |
| bool fromGrpc; | |
| bool with_trust_bundle; | |
| bool peer_authenticated; | |
| std::unordered_set<std::string> peer_privileges; | |
| absl::Mutex mutex_; | |
| }; | |
| void processTopAndSkip(const Query& query, crow::Response& res) { | |
| if (!query.skip && !query.top) { | |
| // No work to do. | |
| return; | |
| } | |
| nlohmann::json::object_t* obj = | |
| res.jsonValue.get_ptr<nlohmann::json::object_t*>(); | |
| if (obj == nullptr) { | |
| // Shouldn't be possible. All responses should be objects. | |
| messages::internalError(res); | |
| return; | |
| } | |
| BMCWEB_LOG_DEBUG << "Handling top/skip"; | |
| nlohmann::json::object_t::iterator members = obj->find("Members"); | |
| if (members == obj->end()) { | |
| // From the Redfish specification 7.3.1 | |
| // ... the HTTP 400 Bad Request status code with the | |
| // QueryNotSupportedOnResource message from the Base Message Registry | |
| // for any supported query parameters that apply only to resource | |
| // collections but are used on singular resources. | |
| messages::queryNotSupportedOnResource(res); | |
| return; | |
| } | |
| nlohmann::json::array_t* arr = | |
| members->second.get_ptr<nlohmann::json::array_t*>(); | |
| if (arr == nullptr) { | |
| messages::internalError(res); | |
| return; | |
| } | |
| if (query.skip) { | |
| // Per section 7.3.1 of the Redfish specification, $skip is run before | |
| // $top Can only skip as many values as we have | |
| size_t skip = std::min(arr->size(), *query.skip); | |
| arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip)); | |
| } | |
| if (query.top) { | |
| size_t top = std::min(arr->size(), *query.top); | |
| arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end()); | |
| } | |
| } | |
| // Given a JSON subtree |currRoot|, this function erases leaves whose keys are | |
| // not in the |currNode| Trie node. | |
| void recursiveSelect(nlohmann::json& currRoot, const SelectTrieNode& currNode) { | |
| nlohmann::json::object_t* object = | |
| currRoot.get_ptr<nlohmann::json::object_t*>(); | |
| if (object != nullptr) { | |
| BMCWEB_LOG_DEBUG << "Current JSON is an object"; | |
| auto it = currRoot.begin(); | |
| while (it != currRoot.end()) { | |
| auto nextIt = std::next(it); | |
| BMCWEB_LOG_DEBUG << "key=" << it.key(); | |
| const SelectTrieNode* nextNode = currNode.find(it.key()); | |
| // Per the Redfish spec section 7.3.3, the service shall select | |
| // certain properties as if $select was omitted. This applies to | |
| // every TrieNode that contains leaves and the root. | |
| constexpr std::array<std::string_view, 5> reservedProperties = { | |
| "@odata.id", "@odata.type", "@odata.context", "@odata.etag", "error"}; | |
| bool reserved = | |
| std::find(reservedProperties.begin(), reservedProperties.end(), | |
| it.key()) != reservedProperties.end(); | |
| if (reserved || (nextNode != nullptr && nextNode->isSelected())) { | |
| it = nextIt; | |
| continue; | |
| } | |
| if (nextNode != nullptr) { | |
| BMCWEB_LOG_DEBUG << "Recursively select: " << it.key(); | |
| recursiveSelect(*it, *nextNode); | |
| it = nextIt; | |
| continue; | |
| } | |
| BMCWEB_LOG_DEBUG << it.key() << " is getting removed!"; | |
| it = currRoot.erase(it); | |
| } | |
| } | |
| nlohmann::json::array_t* array = currRoot.get_ptr<nlohmann::json::array_t*>(); | |
| if (array != nullptr) { | |
| BMCWEB_LOG_DEBUG << "Current JSON is an array"; | |
| // Array index is omitted, so reuse the same Trie node | |
| for (nlohmann::json& nextRoot : *array) { | |
| recursiveSelect(nextRoot, currNode); | |
| } | |
| } | |
| } | |
| // The current implementation of $select still has the following TODOs due to | |
| // ambiguity and/or complexity. | |
| // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was | |
| // created for clarification. | |
| // 2. respect the full odata spec; e.g., deduplication, namespace, star (*), | |
| // etc. | |
| void processSelect(crow::Response& intermediateResponse, | |
| const SelectTrieNode& trieRoot) { | |
| BMCWEB_LOG_DEBUG << "Process $select quary parameter"; | |
| recursiveSelect(intermediateResponse.jsonValue, trieRoot); | |
| } | |
| void processAllParams( | |
| crow::App& app, const Query& query, const crow::Request& request, | |
| std::function<void(crow::Response&)>& completionHandler, | |
| crow::Response& intermediateResponse, | |
| const std::shared_ptr<boost::asio::io_context::strand>& strand) { | |
| if (!completionHandler) { | |
| BMCWEB_LOG_DEBUG << "Function was invalid?"; | |
| return; | |
| } | |
| BMCWEB_LOG_DEBUG << "Processing query params"; | |
| // If the request failed, there's no reason to even try to run query | |
| // params. | |
| if (intermediateResponse.resultInt() < 200 || | |
| intermediateResponse.resultInt() >= 400) { | |
| completionHandler(intermediateResponse); | |
| return; | |
| } | |
| if (query.isOnly) { | |
| processOnly(app, intermediateResponse, completionHandler, strand); | |
| return; | |
| } | |
| if (query.top || query.skip) { | |
| processTopAndSkip(query, intermediateResponse); | |
| } | |
| if (query.expandType != ExpandType::None) { | |
| BMCWEB_LOG_DEBUG << "Executing expand query"; | |
| auto asyncResp = std::make_shared<bmcweb::AsyncResp>( | |
| std::move(intermediateResponse), strand); | |
| asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); | |
| auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp, request); | |
| multi->startQuery(query); | |
| return; | |
| } | |
| // According to Redfish Spec Section 7.3.1, $select is the last parameter to | |
| // to process | |
| if (!query.selectTrie.root.empty()) { | |
| processSelect(intermediateResponse, query.selectTrie.root); | |
| } | |
| completionHandler(intermediateResponse); | |
| } | |
| } // namespace query_param | |
| namespace { | |
| void afterIfMatchRequest(crow::App& app, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| crow::Request& req, const std::string& ifMatchHeader, | |
| const crow::Response& resIn) { | |
| std::string computedEtag = resIn.computeEtag(); | |
| BMCWEB_LOG_DEBUG << "User provided if-match etag " << ifMatchHeader | |
| << " computed etag " << computedEtag; | |
| if (computedEtag != ifMatchHeader) { | |
| messages::preconditionFailed(asyncResp->res); | |
| return; | |
| } | |
| // Restart the request without if-match | |
| req.req.erase(boost::beast::http::field::if_match); | |
| BMCWEB_LOG_DEBUG << "Restarting request"; | |
| app.handle(req, asyncResp); | |
| } | |
| bool handleIfMatch(crow::App& app, const crow::Request& req, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { | |
| if (req.session == nullptr) { | |
| // If the user isn't authenticated, don't even attempt to parse match | |
| // parameters | |
| return true; | |
| } | |
| std::string ifMatch{req.getHeaderValue(boost::beast::http::field::if_match)}; | |
| if (ifMatch.empty()) { | |
| // No If-Match header. Nothing to do | |
| return true; | |
| } | |
| if (req.req.method() != boost::beast::http::verb::patch && | |
| req.req.method() != boost::beast::http::verb::post && | |
| req.req.method() != boost::beast::http::verb::delete_) { | |
| messages::preconditionFailed(asyncResp->res); | |
| return false; | |
| } | |
| boost::system::error_code ec; | |
| // Try to GET the same resource | |
| crow::Request newReq( | |
| {boost::beast::http::verb::get, req.url().encoded_path(), 11}, ec); | |
| if (ec) { | |
| messages::internalError(asyncResp->res); | |
| return false; | |
| } | |
| // New request has the same credentials as the old request | |
| newReq.session = req.session; | |
| // Construct a new response object to fill in, and check the hash of before | |
| // we modify the Resource. | |
| std::shared_ptr<bmcweb::AsyncResp> getReqAsyncResp = | |
| std::make_shared<bmcweb::AsyncResp>(asyncResp->strand_); | |
| getReqAsyncResp->res.setCompleteRequestHandler(std::bind_front( | |
| afterIfMatchRequest, std::ref(app), asyncResp, req, ifMatch)); | |
| app.handle(newReq, getReqAsyncResp); | |
| return false; | |
| } | |
| } // namespace | |
| bool setUpRedfishRouteWithDelegation( | |
| crow::App& app, const crow::Request& req, | |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, | |
| query_param::Query& delegated, | |
| const query_param::QueryCapabilities& queryCapabilities) { | |
| BMCWEB_LOG_DEBUG << "setup redfish route"; | |
| if (asyncResp->response_type == | |
| bmcweb::AsyncResp::ResponseType::kSubscription && | |
| !queryCapabilities.canHandleSubscription) { | |
| BMCWEB_LOG_ALWAYS << "Cannot delegate subscription for route: " | |
| << req.url(); | |
| return false; | |
| } | |
| // Section 7.4 of the redfish spec "Redfish Services shall process the | |
| // [OData-Version header] in the following table as defined by the HTTP 1.1 | |
| // specification..." | |
| // Required to pass redfish-protocol-validator REQ_HEADERS_ODATA_VERSION | |
| std::string_view odataHeader = req.getHeaderValue("OData-Version"); | |
| if (!odataHeader.empty() && odataHeader != "4.0") { | |
| messages::preconditionFailed(asyncResp->res); | |
| return false; | |
| } | |
| asyncResp->res.addHeader("OData-Version", "4.0"); | |
| std::optional<query_param::Query> queryOpt; | |
| if (req.url().has_query() && !req.url().query().empty()) { | |
| queryOpt = query_param::parseParameters(req.url().params(), asyncResp->res); | |
| } else { | |
| queryOpt.emplace(); | |
| } | |
| if (queryOpt == std::nullopt) { | |
| return false; | |
| } | |
| if (!handleIfMatch(app, req, asyncResp)) { | |
| return false; | |
| } | |
| bool needToCallHandlers = true; | |
| #ifdef BMCWEB_ENABLE_REDFISH_AGGREGATION | |
| needToCallHandlers = RedfishAggregator::getInstance().beginAggregation( | |
| req, asyncResp) == Result::LocalHandle; | |
| // If the request should be forwarded to a satellite BMC then we don't want | |
| // to write anything to the asyncResp since it will get overwritten later. | |
| #endif | |
| // If this isn't a get, no need to do anything with parameters | |
| if (req.method() != boost::beast::http::verb::get) { | |
| return needToCallHandlers; | |
| } | |
| delegated = query_param::delegate(queryCapabilities, *queryOpt, asyncResp); | |
| std::function<void(crow::Response&)> handler = | |
| asyncResp->res.releaseCompleteRequestHandler(); | |
| auto strand = asyncResp->strand_; | |
| asyncResp->res.setCompleteRequestHandler( | |
| [&app, handler(std::move(handler)), query{std::move(*queryOpt)}, | |
| request(req), strand](crow::Response& resIn) mutable { | |
| query_param::processAllParams(app, query, request, handler, resIn, | |
| strand); | |
| }); | |
| return needToCallHandlers; | |
| } | |
| } // namespace redfish |