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