blob: a2ce6550e544885a368122580f5fad28d21b1172 [file] [log] [blame]
#include "query.hpp"
#include "absl/synchronization/mutex.h"
#include "bmcweb_config.h"
#include "app.hpp"
#include "async_resp.hpp"
#include "error_messages.hpp"
#include "logging.hpp"
#include "managed_store.hpp"
#include "redfish_aggregator.hpp"
#include "str_utility.hpp"
#include <boost/beast/http/verb.hpp>
#include <boost/system/error_code.hpp>
#include <boost/url/params_view.hpp>
#include <boost/url/url_view.hpp>
#include <cstdint>
#include <functional>
#include <memory>
#include <new>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#ifdef UNIT_TEST_BUILD
#include "test/g3/mock_managed_store.hpp" // NOLINT
#endif
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;
}
bool getExpandType(std::string_view value, Query& query)
{
if (value.empty())
{
return false;
}
switch (value[0])
{
case '*':
query.expandType = ExpandType::Both;
break;
case '.':
query.expandType = ExpandType::NotLinks;
break;
case '~':
query.expandType = ExpandType::Links;
break;
default:
return false;
break;
}
value.remove_prefix(1);
if (value.empty())
{
query.expandLevel = 1;
return true;
}
constexpr std::string_view levels = "($levels=";
if (!value.starts_with(levels))
{
return false;
}
value.remove_prefix(levels.size());
auto it = std::from_chars(value.data(), value.data() + value.size(),
query.expandLevel);
if (it.ec != std::errc())
{
return false;
}
value.remove_prefix(static_cast<size_t>(it.ptr - value.data()));
return value == ")";
}
enum class QueryError : std::uint8_t
{
Ok,
OutOfRange,
ValueFormat,
};
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)
{
if (!getExpandType(it.value, ret))
{
messages::queryParameterValueFormatError(res, it.value, it.key);
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 =
query_param::parseParameters(req.url().params(), asyncResp->res);
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