1 #pragma once
2 #include "bmcweb_config.h"
3 
4 #include "app.hpp"
5 #include "async_resp.hpp"
6 #include "error_messages.hpp"
7 #include "filter_expr_executor.hpp"
8 #include "filter_expr_printer.hpp"
9 #include "http_request.hpp"
10 #include "http_response.hpp"
11 #include "json_formatters.hpp"
12 #include "logging.hpp"
13 #include "str_utility.hpp"
14 
15 #include <sys/types.h>
16 
17 #include <boost/beast/http/message.hpp>
18 #include <boost/beast/http/status.hpp>
19 #include <boost/beast/http/verb.hpp>
20 #include <boost/url/params_view.hpp>
21 #include <nlohmann/json.hpp>
22 
23 #include <algorithm>
24 #include <array>
25 #include <cctype>
26 #include <charconv>
27 #include <compare>
28 #include <cstdint>
29 #include <functional>
30 #include <iterator>
31 #include <limits>
32 #include <map>
33 #include <memory>
34 #include <optional>
35 #include <ranges>
36 #include <string>
37 #include <string_view>
38 #include <system_error>
39 #include <utility>
40 #include <vector>
41 
42 namespace redfish
43 {
44 namespace query_param
45 {
46 
47 enum class ExpandType : uint8_t
48 {
49     None,
50     Links,
51     NotLinks,
52     Both,
53 };
54 
55 // A simple implementation of Trie to help |recursiveSelect|.
56 class SelectTrieNode
57 {
58   public:
59     SelectTrieNode() = default;
60 
find(const std::string & jsonKey) const61     const SelectTrieNode* find(const std::string& jsonKey) const
62     {
63         auto it = children.find(jsonKey);
64         if (it == children.end())
65         {
66             return nullptr;
67         }
68         return &it->second;
69     }
70 
71     // Creates a new node if the key doesn't exist, returns the reference to the
72     // newly created node; otherwise, return the reference to the existing node
emplace(std::string_view jsonKey)73     SelectTrieNode* emplace(std::string_view jsonKey)
74     {
75         auto [it, _] = children.emplace(jsonKey, SelectTrieNode{});
76         return &it->second;
77     }
78 
empty() const79     bool empty() const
80     {
81         return children.empty();
82     }
83 
clear()84     void clear()
85     {
86         children.clear();
87     }
88 
setToSelected()89     void setToSelected()
90     {
91         selected = true;
92     }
93 
isSelected() const94     bool isSelected() const
95     {
96         return selected;
97     }
98 
99   private:
100     std::map<std::string, SelectTrieNode, std::less<>> children;
101     bool selected = false;
102 };
103 
104 // Validates the property in the $select parameter. Every character is among
105 // [a-zA-Z0-9#@_.] (taken from Redfish spec, section 9.6 Properties)
isSelectedPropertyAllowed(std::string_view property)106 inline bool isSelectedPropertyAllowed(std::string_view property)
107 {
108     // These a magic number, but with it it's less likely that this code
109     // introduces CVE; e.g., too large properties crash the service.
110     constexpr int maxPropertyLength = 60;
111     if (property.empty() || property.size() > maxPropertyLength)
112     {
113         return false;
114     }
115     for (char ch : property)
116     {
117         if (std::isalnum(static_cast<unsigned char>(ch)) == 0 && ch != '#' &&
118             ch != '@' && ch != '.')
119         {
120             return false;
121         }
122     }
123     return true;
124 }
125 
126 struct SelectTrie
127 {
128     SelectTrie() = default;
129 
130     // Inserts a $select value; returns false if the nestedProperty is illegal.
insertNoderedfish::query_param::SelectTrie131     bool insertNode(std::string_view nestedProperty)
132     {
133         if (nestedProperty.empty())
134         {
135             return false;
136         }
137         SelectTrieNode* currNode = &root;
138         size_t index = nestedProperty.find_first_of('/');
139         while (!nestedProperty.empty())
140         {
141             std::string_view property = nestedProperty.substr(0, index);
142             if (!isSelectedPropertyAllowed(property))
143             {
144                 return false;
145             }
146             currNode = currNode->emplace(property);
147             if (index == std::string::npos)
148             {
149                 break;
150             }
151             nestedProperty.remove_prefix(index + 1);
152             index = nestedProperty.find_first_of('/');
153         }
154         currNode->setToSelected();
155         return true;
156     }
157 
158     SelectTrieNode root;
159 };
160 
161 // The struct stores the parsed query parameters of the default Redfish route.
162 struct Query
163 {
164     // Only
165     bool isOnly = false;
166 
167     // Expand
168     uint8_t expandLevel = 0;
169     ExpandType expandType = ExpandType::None;
170 
171     // Skip
172     std::optional<size_t> skip = std::nullopt;
173 
174     // Top
175     static constexpr size_t maxTop = 1000; // Max entries a response contain
176     std::optional<size_t> top = std::nullopt;
177 
178     // Filter
179     std::optional<filter_ast::LogicalAnd> filter = std::nullopt;
180 
181     // Select
182     // Unclear how to make this use structured initialization without this.
183     // Might be a tidy bug?  Ignore for now
184     // NOLINTNEXTLINE(readability-redundant-member-init)
185     SelectTrie selectTrie{};
186 };
187 
188 // The struct defines how resource handlers in redfish-core/lib/ can handle
189 // query parameters themselves, so that the default Redfish route will delegate
190 // the processing.
191 struct QueryCapabilities
192 {
193     bool canDelegateOnly = false;
194     bool canDelegateTop = false;
195     bool canDelegateSkip = false;
196     uint8_t canDelegateExpandLevel = 0;
197     bool canDelegateSelect = false;
198 };
199 
200 // Delegates query parameters according to the given |queryCapabilities|
201 // This function doesn't check query parameter conflicts since the parse
202 // function will take care of it.
203 // Returns a delegated query object which can be used by individual resource
204 // handlers so that handlers don't need to query again.
delegate(const QueryCapabilities & queryCapabilities,Query & query)205 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
206 {
207     Query delegated{};
208     // delegate only
209     if (query.isOnly && queryCapabilities.canDelegateOnly)
210     {
211         delegated.isOnly = true;
212         query.isOnly = false;
213     }
214     // delegate expand as much as we can
215     if (query.expandType != ExpandType::None)
216     {
217         delegated.expandType = query.expandType;
218         if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
219         {
220             query.expandType = ExpandType::None;
221             delegated.expandLevel = query.expandLevel;
222             query.expandLevel = 0;
223         }
224         else
225         {
226             delegated.expandLevel = queryCapabilities.canDelegateExpandLevel;
227         }
228     }
229 
230     // delegate top
231     if (query.top && queryCapabilities.canDelegateTop)
232     {
233         delegated.top = query.top;
234         query.top = std::nullopt;
235     }
236 
237     // delegate skip
238     if (query.skip && queryCapabilities.canDelegateSkip)
239     {
240         delegated.skip = query.skip;
241         query.skip = 0;
242     }
243 
244     // delegate select
245     if (!query.selectTrie.root.empty() && queryCapabilities.canDelegateSelect)
246     {
247         delegated.selectTrie = std::move(query.selectTrie);
248         query.selectTrie.root.clear();
249     }
250     return delegated;
251 }
252 
getExpandType(std::string_view value,Query & query)253 inline bool getExpandType(std::string_view value, Query& query)
254 {
255     if (value.empty())
256     {
257         return false;
258     }
259     switch (value[0])
260     {
261         case '*':
262             query.expandType = ExpandType::Both;
263             break;
264         case '.':
265             query.expandType = ExpandType::NotLinks;
266             break;
267         case '~':
268             query.expandType = ExpandType::Links;
269             break;
270         default:
271             return false;
272     }
273     value.remove_prefix(1);
274     if (value.empty())
275     {
276         query.expandLevel = 1;
277         return true;
278     }
279     constexpr std::string_view levels = "($levels=";
280     if (!value.starts_with(levels))
281     {
282         return false;
283     }
284     value.remove_prefix(levels.size());
285 
286     auto it = std::from_chars(value.begin(), value.end(), query.expandLevel);
287     if (it.ec != std::errc())
288     {
289         return false;
290     }
291     value.remove_prefix(
292         static_cast<size_t>(std::distance(value.begin(), it.ptr)));
293     return value == ")";
294 }
295 
296 enum class QueryError
297 {
298     Ok,
299     OutOfRange,
300     ValueFormat,
301 };
302 
getNumericParam(std::string_view value,size_t & param)303 inline QueryError getNumericParam(std::string_view value, size_t& param)
304 {
305     std::from_chars_result r =
306         std::from_chars(value.begin(), value.end(), param);
307 
308     // If the number wasn't representable in the type, it's out of range
309     if (r.ec == std::errc::result_out_of_range)
310     {
311         return QueryError::OutOfRange;
312     }
313     // All other errors are value format
314     if (r.ec != std::errc())
315     {
316         return QueryError::ValueFormat;
317     }
318     return QueryError::Ok;
319 }
320 
getSkipParam(std::string_view value,Query & query)321 inline QueryError getSkipParam(std::string_view value, Query& query)
322 {
323     return getNumericParam(value, query.skip.emplace());
324 }
325 
getTopParam(std::string_view value,Query & query)326 inline QueryError getTopParam(std::string_view value, Query& query)
327 {
328     QueryError ret = getNumericParam(value, query.top.emplace());
329     if (ret != QueryError::Ok)
330     {
331         return ret;
332     }
333 
334     // Range check for sanity.
335     if (query.top > Query::maxTop)
336     {
337         return QueryError::OutOfRange;
338     }
339 
340     return QueryError::Ok;
341 }
342 
343 // Parses and validates the $select parameter.
344 // As per OData URL Conventions and Redfish Spec, the $select values shall be
345 // comma separated Resource Path
346 // Ref:
347 // 1. https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
348 // 2.
349 // https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt
getSelectParam(std::string_view value,Query & query)350 inline bool getSelectParam(std::string_view value, Query& query)
351 {
352     std::vector<std::string> properties;
353     bmcweb::split(properties, value, ',');
354     if (properties.empty())
355     {
356         return false;
357     }
358     // These a magic number, but with it it's less likely that this code
359     // introduces CVE; e.g., too large properties crash the service.
360     constexpr int maxNumProperties = 10;
361     if (properties.size() > maxNumProperties)
362     {
363         return false;
364     }
365     for (const auto& property : properties)
366     {
367         if (!query.selectTrie.insertNode(property))
368         {
369             return false;
370         }
371     }
372     return true;
373 }
374 
375 // Parses and validates the $filter parameter.
getFilterParam(std::string_view value,Query & query)376 inline bool getFilterParam(std::string_view value, Query& query)
377 {
378     query.filter = parseFilter(value);
379     return query.filter.has_value();
380 }
381 
382 inline std::optional<Query>
parseParameters(boost::urls::params_view urlParams,crow::Response & res)383     parseParameters(boost::urls::params_view urlParams, crow::Response& res)
384 {
385     Query ret{};
386     for (const boost::urls::params_view::value_type& it : urlParams)
387     {
388         if (it.key == "only")
389         {
390             if (!it.value.empty())
391             {
392                 messages::queryParameterValueFormatError(res, it.value, it.key);
393                 return std::nullopt;
394             }
395             ret.isOnly = true;
396         }
397         else if (it.key == "$expand" && BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
398         {
399             if (!getExpandType(it.value, ret))
400             {
401                 messages::queryParameterValueFormatError(res, it.value, it.key);
402                 return std::nullopt;
403             }
404         }
405         else if (it.key == "$top")
406         {
407             QueryError topRet = getTopParam(it.value, ret);
408             if (topRet == QueryError::ValueFormat)
409             {
410                 messages::queryParameterValueFormatError(res, it.value, it.key);
411                 return std::nullopt;
412             }
413             if (topRet == QueryError::OutOfRange)
414             {
415                 messages::queryParameterOutOfRange(
416                     res, it.value, "$top",
417                     "0-" + std::to_string(Query::maxTop));
418                 return std::nullopt;
419             }
420         }
421         else if (it.key == "$skip")
422         {
423             QueryError topRet = getSkipParam(it.value, ret);
424             if (topRet == QueryError::ValueFormat)
425             {
426                 messages::queryParameterValueFormatError(res, it.value, it.key);
427                 return std::nullopt;
428             }
429             if (topRet == QueryError::OutOfRange)
430             {
431                 messages::queryParameterOutOfRange(
432                     res, it.value, it.key,
433                     "0-" + std::to_string(std::numeric_limits<size_t>::max()));
434                 return std::nullopt;
435             }
436         }
437         else if (it.key == "$select")
438         {
439             if (!getSelectParam(it.value, ret))
440             {
441                 messages::queryParameterValueFormatError(res, it.value, it.key);
442                 return std::nullopt;
443             }
444         }
445         else if (it.key == "$filter" && BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
446         {
447             if (!getFilterParam(it.value, ret))
448             {
449                 messages::queryParameterValueFormatError(res, it.value, it.key);
450                 return std::nullopt;
451             }
452         }
453         else
454         {
455             // Intentionally ignore other errors Redfish spec, 7.3.1
456             if (it.key.starts_with("$"))
457             {
458                 // Services shall return... The HTTP 501 Not Implemented
459                 // status code for any unsupported query parameters that
460                 // start with $ .
461                 messages::queryParameterValueFormatError(res, it.value, it.key);
462                 res.result(boost::beast::http::status::not_implemented);
463                 return std::nullopt;
464             }
465             // "Shall ignore unknown or unsupported query parameters that do
466             // not begin with $ ."
467         }
468     }
469 
470     if (ret.expandType != ExpandType::None && !ret.selectTrie.root.empty())
471     {
472         messages::queryCombinationInvalid(res);
473         return std::nullopt;
474     }
475 
476     return ret;
477 }
478 
processOnly(crow::App & app,crow::Response & res,std::function<void (crow::Response &)> & completionHandler)479 inline bool processOnly(crow::App& app, crow::Response& res,
480                         std::function<void(crow::Response&)>& completionHandler)
481 {
482     BMCWEB_LOG_DEBUG("Processing only query param");
483     auto itMembers = res.jsonValue.find("Members");
484     if (itMembers == res.jsonValue.end())
485     {
486         messages::queryNotSupportedOnResource(res);
487         completionHandler(res);
488         return false;
489     }
490     auto itMemBegin = itMembers->begin();
491     if (itMemBegin == itMembers->end() || itMembers->size() != 1)
492     {
493         BMCWEB_LOG_DEBUG(
494             "Members contains {} element, returning full collection.",
495             itMembers->size());
496         completionHandler(res);
497         return false;
498     }
499 
500     auto itUrl = itMemBegin->find("@odata.id");
501     if (itUrl == itMemBegin->end())
502     {
503         BMCWEB_LOG_DEBUG("No found odata.id");
504         messages::internalError(res);
505         completionHandler(res);
506         return false;
507     }
508     const std::string* url = itUrl->get_ptr<const std::string*>();
509     if (url == nullptr)
510     {
511         BMCWEB_LOG_DEBUG("@odata.id wasn't a string????");
512         messages::internalError(res);
513         completionHandler(res);
514         return false;
515     }
516     // TODO(Ed) copy request headers?
517     // newReq.session = req.session;
518     std::error_code ec;
519     auto newReq = std::make_shared<crow::Request>(
520         crow::Request::Body{boost::beast::http::verb::get, *url, 11}, ec);
521     if (ec)
522     {
523         messages::internalError(res);
524         completionHandler(res);
525         return false;
526     }
527 
528     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
529     BMCWEB_LOG_DEBUG("setting completion handler on {}",
530                      logPtr(&asyncResp->res));
531     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
532     app.handle(newReq, asyncResp);
533     return true;
534 }
535 
536 struct ExpandNode
537 {
538     nlohmann::json::json_pointer location;
539     std::string uri;
540 
operator ==redfish::query_param::ExpandNode541     bool operator==(const ExpandNode& other) const
542     {
543         return location == other.location && uri == other.uri;
544     }
545 };
546 
547 inline void findNavigationReferencesInArrayRecursive(
548     ExpandType eType, nlohmann::json::array_t& array,
549     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
550     bool inLinks, std::vector<ExpandNode>& out);
551 
552 inline void findNavigationReferencesInObjectRecursive(
553     ExpandType eType, nlohmann::json::object_t& obj,
554     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
555     bool inLinks, std::vector<ExpandNode>& out);
556 
557 // Walks a json object looking for Redfish NavigationReference entries that
558 // might need resolved.  It recursively walks the jsonResponse object, looking
559 // for links at every level, and returns a list (out) of locations within the
560 // tree that need to be expanded.  The current json pointer location p is passed
561 // in to reference the current node that's being expanded, so it can be combined
562 // with the keys from the jsonResponse object
findNavigationReferencesRecursive(ExpandType eType,nlohmann::json & jsonResponse,const nlohmann::json::json_pointer & jsonPtr,int depth,int skipDepth,bool inLinks,std::vector<ExpandNode> & out)563 inline void findNavigationReferencesRecursive(
564     ExpandType eType, nlohmann::json& jsonResponse,
565     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
566     bool inLinks, std::vector<ExpandNode>& out)
567 {
568     // If no expand is needed, return early
569     if (eType == ExpandType::None)
570     {
571         return;
572     }
573 
574     nlohmann::json::array_t* array =
575         jsonResponse.get_ptr<nlohmann::json::array_t*>();
576     if (array != nullptr)
577     {
578         findNavigationReferencesInArrayRecursive(eType, *array, jsonPtr, depth,
579                                                  skipDepth, inLinks, out);
580     }
581     nlohmann::json::object_t* obj =
582         jsonResponse.get_ptr<nlohmann::json::object_t*>();
583     if (obj == nullptr)
584     {
585         return;
586     }
587     findNavigationReferencesInObjectRecursive(eType, *obj, jsonPtr, depth,
588                                               skipDepth, inLinks, out);
589 }
590 
findNavigationReferencesInArrayRecursive(ExpandType eType,nlohmann::json::array_t & array,const nlohmann::json::json_pointer & jsonPtr,int depth,int skipDepth,bool inLinks,std::vector<ExpandNode> & out)591 inline void findNavigationReferencesInArrayRecursive(
592     ExpandType eType, nlohmann::json::array_t& array,
593     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
594     bool inLinks, std::vector<ExpandNode>& out)
595 {
596     size_t index = 0;
597     // For arrays, walk every element in the array
598     for (auto& element : array)
599     {
600         nlohmann::json::json_pointer newPtr = jsonPtr / index;
601         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr.to_string());
602         findNavigationReferencesRecursive(eType, element, newPtr, depth,
603                                           skipDepth, inLinks, out);
604         index++;
605     }
606 }
607 
findNavigationReferencesInObjectRecursive(ExpandType eType,nlohmann::json::object_t & obj,const nlohmann::json::json_pointer & jsonPtr,int depth,int skipDepth,bool inLinks,std::vector<ExpandNode> & out)608 inline void findNavigationReferencesInObjectRecursive(
609     ExpandType eType, nlohmann::json::object_t& obj,
610     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
611     bool inLinks, std::vector<ExpandNode>& out)
612 {
613     // Navigation References only ever have a single element
614     if (obj.size() == 1)
615     {
616         if (obj.begin()->first == "@odata.id")
617         {
618             const std::string* uri =
619                 obj.begin()->second.get_ptr<const std::string*>();
620             if (uri != nullptr)
621             {
622                 BMCWEB_LOG_DEBUG("Found {} at {}", *uri, jsonPtr.to_string());
623                 if (skipDepth == 0)
624                 {
625                     out.push_back({jsonPtr, *uri});
626                 }
627                 return;
628             }
629         }
630     }
631 
632     int newDepth = depth;
633     auto odataId = obj.find("@odata.id");
634     if (odataId != obj.end())
635     {
636         // The Redfish spec requires all resources to include the resource
637         // identifier.  If the object has multiple elements and one of them is
638         // "@odata.id" then that means we have entered a new level / expanded
639         // resource.  We need to stop traversing if we're already at the desired
640         // depth
641         if (obj.size() > 1)
642         {
643             if (depth == 0)
644             {
645                 return;
646             }
647             if (skipDepth > 0)
648             {
649                 skipDepth--;
650             }
651         }
652 
653         if (skipDepth == 0)
654         {
655             newDepth--;
656         }
657     }
658 
659     // Loop the object and look for links
660     for (auto& element : obj)
661     {
662         bool localInLinks = inLinks;
663         if (!localInLinks)
664         {
665             // Check if this is a links node
666             localInLinks = element.first == "Links";
667         }
668         // Only traverse the parts of the tree the user asked for
669         // Per section 7.3 of the redfish specification
670         if (localInLinks && eType == ExpandType::NotLinks)
671         {
672             continue;
673         }
674         if (!localInLinks && eType == ExpandType::Links)
675         {
676             continue;
677         }
678         nlohmann::json::json_pointer newPtr = jsonPtr / element.first;
679         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr);
680 
681         findNavigationReferencesRecursive(eType, element.second, newPtr,
682                                           newDepth, skipDepth, localInLinks,
683                                           out);
684     }
685 }
686 
687 // TODO: When aggregation is enabled and we receive a partially expanded
688 // response we may need need additional handling when the original URI was
689 // up tree from a top level collection.
690 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556
691 // lands.  May want to avoid forwarding query params when request is uptree from
692 // a top level collection.
findNavigationReferences(ExpandType eType,int depth,int skipDepth,nlohmann::json & jsonResponse)693 inline std::vector<ExpandNode> findNavigationReferences(
694     ExpandType eType, int depth, int skipDepth, nlohmann::json& jsonResponse)
695 {
696     std::vector<ExpandNode> ret;
697     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
698     // SkipDepth +1 since we are skipping the root by default.
699     findNavigationReferencesRecursive(eType, jsonResponse, root, depth,
700                                       skipDepth + 1, false, ret);
701     return ret;
702 }
703 
704 // Formats a query parameter string for the sub-query.
705 // Returns std::nullopt on failures.
706 // This function shall handle $select when it is added.
707 // There is no need to handle parameters that's not compatible with $expand,
708 // e.g., $only, since this function will only be called in side $expand handlers
formatQueryForExpand(const Query & query)709 inline std::optional<std::string> formatQueryForExpand(const Query& query)
710 {
711     // query.expandLevel<=1: no need to do subqueries
712     if (query.expandLevel <= 1)
713     {
714         return "";
715     }
716     std::string str = "?$expand=";
717     switch (query.expandType)
718     {
719         case ExpandType::Links:
720             str += '~';
721             break;
722         case ExpandType::NotLinks:
723             str += '.';
724             break;
725         case ExpandType::Both:
726             str += '*';
727             break;
728         case ExpandType::None:
729             return "";
730         default:
731             return std::nullopt;
732     }
733     str += "($levels=";
734     str += std::to_string(query.expandLevel - 1);
735     str += ')';
736     return str;
737 }
738 
739 // Propagates the worst error code to the final response.
740 // The order of error code is (from high to low)
741 // 500 Internal Server Error
742 // 511 Network Authentication Required
743 // 510 Not Extended
744 // 508 Loop Detected
745 // 507 Insufficient Storage
746 // 506 Variant Also Negotiates
747 // 505 HTTP Version Not Supported
748 // 504 Gateway Timeout
749 // 503 Service Unavailable
750 // 502 Bad Gateway
751 // 501 Not Implemented
752 // 401 Unauthorized
753 // 451 - 409 Error codes (not listed explicitly)
754 // 408 Request Timeout
755 // 407 Proxy Authentication Required
756 // 406 Not Acceptable
757 // 405 Method Not Allowed
758 // 404 Not Found
759 // 403 Forbidden
760 // 402 Payment Required
761 // 400 Bad Request
propogateErrorCode(unsigned finalCode,unsigned subResponseCode)762 inline unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode)
763 {
764     // We keep a explicit list for error codes that this project often uses
765     // Higher priority codes are in lower indexes
766     constexpr std::array<unsigned, 13> orderedCodes = {
767         500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400};
768     size_t finalCodeIndex = std::numeric_limits<size_t>::max();
769     size_t subResponseCodeIndex = std::numeric_limits<size_t>::max();
770     for (size_t i = 0; i < orderedCodes.size(); ++i)
771     {
772         if (orderedCodes[i] == finalCode)
773         {
774             finalCodeIndex = i;
775         }
776         if (orderedCodes[i] == subResponseCode)
777         {
778             subResponseCodeIndex = i;
779         }
780     }
781     if (finalCodeIndex != std::numeric_limits<size_t>::max() &&
782         subResponseCodeIndex != std::numeric_limits<size_t>::max())
783     {
784         return finalCodeIndex <= subResponseCodeIndex
785                    ? finalCode
786                    : subResponseCode;
787     }
788     if (subResponseCode == 500 || finalCode == 500)
789     {
790         return 500;
791     }
792     if (subResponseCode > 500 || finalCode > 500)
793     {
794         return std::max(finalCode, subResponseCode);
795     }
796     if (subResponseCode == 401)
797     {
798         return subResponseCode;
799     }
800     return std::max(finalCode, subResponseCode);
801 }
802 
803 // Propagates all error messages into |finalResponse|
propogateError(crow::Response & finalResponse,crow::Response & subResponse)804 inline void propogateError(crow::Response& finalResponse,
805                            crow::Response& subResponse)
806 {
807     // no errors
808     if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400)
809     {
810         return;
811     }
812     messages::moveErrorsToErrorJson(finalResponse.jsonValue,
813                                     subResponse.jsonValue);
814     finalResponse.result(
815         propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt()));
816 }
817 
818 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
819 {
820   public:
821     // This object takes a single asyncResp object as the "final" one, then
822     // allows callers to attach sub-responses within the json tree that need
823     // to be executed and filled into their appropriate locations.  This
824     // class manages the final "merge" of the json resources.
MultiAsyncResp(crow::App & appIn,std::shared_ptr<bmcweb::AsyncResp> finalResIn)825     MultiAsyncResp(crow::App& appIn,
826                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
827         app(appIn), finalRes(std::move(finalResIn))
828     {}
829 
addAwaitingResponse(const std::shared_ptr<bmcweb::AsyncResp> & res,const nlohmann::json::json_pointer & finalExpandLocation)830     void addAwaitingResponse(
831         const std::shared_ptr<bmcweb::AsyncResp>& res,
832         const nlohmann::json::json_pointer& finalExpandLocation)
833     {
834         res->res.setCompleteRequestHandler(std::bind_front(
835             placeResultStatic, shared_from_this(), finalExpandLocation));
836     }
837 
placeResult(const nlohmann::json::json_pointer & locationToPlace,crow::Response & res)838     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
839                      crow::Response& res)
840     {
841         BMCWEB_LOG_DEBUG("placeResult for {}", locationToPlace);
842         propogateError(finalRes->res, res);
843         if (!res.jsonValue.is_object() || res.jsonValue.empty())
844         {
845             return;
846         }
847         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
848         finalObj = std::move(res.jsonValue);
849     }
850 
851     // Handles the very first level of Expand, and starts a chain of sub-queries
852     // for deeper levels.
startQuery(const Query & query,const Query & delegated)853     void startQuery(const Query& query, const Query& delegated)
854     {
855         std::vector<ExpandNode> nodes = findNavigationReferences(
856             query.expandType, query.expandLevel, delegated.expandLevel,
857             finalRes->res.jsonValue);
858         BMCWEB_LOG_DEBUG("{} nodes to traverse", nodes.size());
859         const std::optional<std::string> queryStr = formatQueryForExpand(query);
860         if (!queryStr)
861         {
862             messages::internalError(finalRes->res);
863             return;
864         }
865         for (const ExpandNode& node : nodes)
866         {
867             const std::string subQuery = node.uri + *queryStr;
868             BMCWEB_LOG_DEBUG("URL of subquery:  {}", subQuery);
869             std::error_code ec;
870             auto newReq = std::make_shared<crow::Request>(
871                 crow::Request::Body{boost::beast::http::verb::get, subQuery,
872                                     11},
873                 ec);
874             if (ec)
875             {
876                 messages::internalError(finalRes->res);
877                 return;
878             }
879 
880             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
881             BMCWEB_LOG_DEBUG("setting completion handler on {}",
882                              logPtr(&asyncResp->res));
883 
884             addAwaitingResponse(asyncResp, node.location);
885             app.handle(newReq, asyncResp);
886         }
887     }
888 
889   private:
890     static void
placeResultStatic(const std::shared_ptr<MultiAsyncResp> & multi,const nlohmann::json::json_pointer & locationToPlace,crow::Response & res)891         placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
892                           const nlohmann::json::json_pointer& locationToPlace,
893                           crow::Response& res)
894     {
895         multi->placeResult(locationToPlace, res);
896     }
897 
898     crow::App& app;
899     std::shared_ptr<bmcweb::AsyncResp> finalRes;
900 };
901 
processTopAndSkip(const Query & query,crow::Response & res)902 inline void processTopAndSkip(const Query& query, crow::Response& res)
903 {
904     if (!query.skip && !query.top)
905     {
906         // No work to do.
907         return;
908     }
909     nlohmann::json::object_t* obj =
910         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
911     if (obj == nullptr)
912     {
913         // Shouldn't be possible.  All responses should be objects.
914         messages::internalError(res);
915         return;
916     }
917 
918     BMCWEB_LOG_DEBUG("Handling top/skip");
919     nlohmann::json::object_t::iterator members = obj->find("Members");
920     if (members == obj->end())
921     {
922         // From the Redfish specification 7.3.1
923         // ... the HTTP 400 Bad Request status code with the
924         // QueryNotSupportedOnResource message from the Base Message Registry
925         // for any supported query parameters that apply only to resource
926         // collections but are used on singular resources.
927         messages::queryNotSupportedOnResource(res);
928         return;
929     }
930 
931     nlohmann::json::array_t* arr =
932         members->second.get_ptr<nlohmann::json::array_t*>();
933     if (arr == nullptr)
934     {
935         messages::internalError(res);
936         return;
937     }
938 
939     if (query.skip)
940     {
941         // Per section 7.3.1 of the Redfish specification, $skip is run before
942         // $top Can only skip as many values as we have
943         size_t skip = std::min(arr->size(), *query.skip);
944         arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
945     }
946     if (query.top)
947     {
948         size_t top = std::min(arr->size(), *query.top);
949         arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
950     }
951 }
952 
953 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are
954 // not in the |currNode| Trie node.
recursiveSelect(nlohmann::json & currRoot,const SelectTrieNode & currNode)955 inline void recursiveSelect(nlohmann::json& currRoot,
956                             const SelectTrieNode& currNode)
957 {
958     nlohmann::json::object_t* object =
959         currRoot.get_ptr<nlohmann::json::object_t*>();
960     if (object != nullptr)
961     {
962         BMCWEB_LOG_DEBUG("Current JSON is an object");
963         auto it = currRoot.begin();
964         while (it != currRoot.end())
965         {
966             auto nextIt = std::next(it);
967             BMCWEB_LOG_DEBUG("key={}", it.key());
968             const SelectTrieNode* nextNode = currNode.find(it.key());
969             // Per the Redfish spec section 7.3.3, the service shall select
970             // certain properties as if $select was omitted. This applies to
971             // every TrieNode that contains leaves and the root.
972             constexpr std::array<std::string_view, 5> reservedProperties = {
973                 "@odata.id", "@odata.type", "@odata.context", "@odata.etag",
974                 "error"};
975             bool reserved = std::ranges::find(reservedProperties, it.key()) !=
976                             reservedProperties.end();
977             if (reserved || (nextNode != nullptr && nextNode->isSelected()))
978             {
979                 it = nextIt;
980                 continue;
981             }
982             if (nextNode != nullptr)
983             {
984                 BMCWEB_LOG_DEBUG("Recursively select: {}", it.key());
985                 recursiveSelect(*it, *nextNode);
986                 it = nextIt;
987                 continue;
988             }
989             BMCWEB_LOG_DEBUG("{} is getting removed!", it.key());
990             it = currRoot.erase(it);
991         }
992     }
993     nlohmann::json::array_t* array =
994         currRoot.get_ptr<nlohmann::json::array_t*>();
995     if (array != nullptr)
996     {
997         BMCWEB_LOG_DEBUG("Current JSON is an array");
998         // Array index is omitted, so reuse the same Trie node
999         for (nlohmann::json& nextRoot : *array)
1000         {
1001             recursiveSelect(nextRoot, currNode);
1002         }
1003     }
1004 }
1005 
1006 // The current implementation of $select still has the following TODOs due to
1007 //  ambiguity and/or complexity.
1008 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was
1009 // created for clarification.
1010 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*),
1011 // etc.
processSelect(crow::Response & intermediateResponse,const SelectTrieNode & trieRoot)1012 inline void processSelect(crow::Response& intermediateResponse,
1013                           const SelectTrieNode& trieRoot)
1014 {
1015     BMCWEB_LOG_DEBUG("Process $select quary parameter");
1016     recursiveSelect(intermediateResponse.jsonValue, trieRoot);
1017 }
1018 
1019 inline void
processAllParams(crow::App & app,const Query & query,const Query & delegated,std::function<void (crow::Response &)> & completionHandler,crow::Response & intermediateResponse)1020     processAllParams(crow::App& app, const Query& query, const Query& delegated,
1021                      std::function<void(crow::Response&)>& completionHandler,
1022                      crow::Response& intermediateResponse)
1023 {
1024     if (!completionHandler)
1025     {
1026         BMCWEB_LOG_DEBUG("Function was invalid?");
1027         return;
1028     }
1029 
1030     BMCWEB_LOG_DEBUG("Processing query params");
1031     // If the request failed, there's no reason to even try to run query
1032     // params.
1033     if (intermediateResponse.resultInt() < 200 ||
1034         intermediateResponse.resultInt() >= 400)
1035     {
1036         completionHandler(intermediateResponse);
1037         return;
1038     }
1039     if (query.isOnly)
1040     {
1041         processOnly(app, intermediateResponse, completionHandler);
1042         return;
1043     }
1044 
1045     if (query.top || query.skip)
1046     {
1047         processTopAndSkip(query, intermediateResponse);
1048     }
1049 
1050     if (query.expandType != ExpandType::None)
1051     {
1052         BMCWEB_LOG_DEBUG("Executing expand query");
1053         auto asyncResp = std::make_shared<bmcweb::AsyncResp>(
1054             std::move(intermediateResponse));
1055 
1056         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
1057         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
1058         multi->startQuery(query, delegated);
1059         return;
1060     }
1061 
1062     if (query.filter)
1063     {
1064         applyFilterToCollection(intermediateResponse.jsonValue, *query.filter);
1065     }
1066 
1067     // According to Redfish Spec Section 7.3.1, $select is the last parameter to
1068     // to process
1069     if (!query.selectTrie.root.empty())
1070     {
1071         processSelect(intermediateResponse, query.selectTrie.root);
1072     }
1073 
1074     completionHandler(intermediateResponse);
1075 }
1076 
1077 } // namespace query_param
1078 } // namespace redfish
1079