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 <string>
34 #include <string_view>
35 #include <system_error>
36 #include <utility>
37 #include <vector>
38 
39 // IWYU pragma: no_include <boost/url/impl/params_view.hpp>
40 // IWYU pragma: no_include <boost/beast/http/impl/message.hpp>
41 // IWYU pragma: no_include <boost/intrusive/detail/list_iterator.hpp>
42 // IWYU pragma: no_include <boost/algorithm/string/detail/classification.hpp>
43 // IWYU pragma: no_include <boost/iterator/iterator_facade.hpp>
44 // IWYU pragma: no_include <boost/type_index/type_index_facade.hpp>
45 // IWYU pragma: no_include <stdint.h>
46 
47 namespace redfish
48 {
49 namespace query_param
50 {
51 
52 enum class ExpandType : uint8_t
53 {
54     None,
55     Links,
56     NotLinks,
57     Both,
58 };
59 
60 // A simple implementation of Trie to help |recursiveSelect|.
61 class SelectTrieNode
62 {
63   public:
64     SelectTrieNode() = default;
65 
66     const SelectTrieNode* find(const std::string& jsonKey) const
67     {
68         auto it = children.find(jsonKey);
69         if (it == children.end())
70         {
71             return nullptr;
72         }
73         return &it->second;
74     }
75 
76     // Creates a new node if the key doesn't exist, returns the reference to the
77     // newly created node; otherwise, return the reference to the existing node
78     SelectTrieNode* emplace(std::string_view jsonKey)
79     {
80         auto [it, _] = children.emplace(jsonKey, SelectTrieNode{});
81         return &it->second;
82     }
83 
84     bool empty() const
85     {
86         return children.empty();
87     }
88 
89     void clear()
90     {
91         children.clear();
92     }
93 
94     void setToSelected()
95     {
96         selected = true;
97     }
98 
99     bool isSelected() const
100     {
101         return selected;
102     }
103 
104   private:
105     std::map<std::string, SelectTrieNode, std::less<>> children;
106     bool selected = false;
107 };
108 
109 // Validates the property in the $select parameter. Every character is among
110 // [a-zA-Z0-9#@_.] (taken from Redfish spec, section 9.6 Properties)
111 inline bool isSelectedPropertyAllowed(std::string_view property)
112 {
113     // These a magic number, but with it it's less likely that this code
114     // introduces CVE; e.g., too large properties crash the service.
115     constexpr int maxPropertyLength = 60;
116     if (property.empty() || property.size() > maxPropertyLength)
117     {
118         return false;
119     }
120     for (char ch : property)
121     {
122         if (std::isalnum(static_cast<unsigned char>(ch)) == 0 && ch != '#' &&
123             ch != '@' && ch != '.')
124         {
125             return false;
126         }
127     }
128     return true;
129 }
130 
131 struct SelectTrie
132 {
133     SelectTrie() = default;
134 
135     // Inserts a $select value; returns false if the nestedProperty is illegal.
136     bool insertNode(std::string_view nestedProperty)
137     {
138         if (nestedProperty.empty())
139         {
140             return false;
141         }
142         SelectTrieNode* currNode = &root;
143         size_t index = nestedProperty.find_first_of('/');
144         while (!nestedProperty.empty())
145         {
146             std::string_view property = nestedProperty.substr(0, index);
147             if (!isSelectedPropertyAllowed(property))
148             {
149                 return false;
150             }
151             currNode = currNode->emplace(property);
152             if (index == std::string::npos)
153             {
154                 break;
155             }
156             nestedProperty.remove_prefix(index + 1);
157             index = nestedProperty.find_first_of('/');
158         }
159         currNode->setToSelected();
160         return true;
161     }
162 
163     SelectTrieNode root;
164 };
165 
166 // The struct stores the parsed query parameters of the default Redfish route.
167 struct Query
168 {
169     // Only
170     bool isOnly = false;
171     // Expand
172     uint8_t expandLevel = 0;
173     ExpandType expandType = ExpandType::None;
174 
175     // Skip
176     std::optional<size_t> skip = std::nullopt;
177 
178     // Top
179     static constexpr size_t maxTop = 1000; // Max entries a response contain
180     std::optional<size_t> top = std::nullopt;
181 
182     // Select
183     SelectTrie selectTrie = {};
184 };
185 
186 // The struct defines how resource handlers in redfish-core/lib/ can handle
187 // query parameters themselves, so that the default Redfish route will delegate
188 // the processing.
189 struct QueryCapabilities
190 {
191     bool canDelegateOnly = false;
192     bool canDelegateTop = false;
193     bool canDelegateSkip = false;
194     uint8_t canDelegateExpandLevel = 0;
195     bool canDelegateSelect = false;
196 };
197 
198 // Delegates query parameters according to the given |queryCapabilities|
199 // This function doesn't check query parameter conflicts since the parse
200 // function will take care of it.
201 // Returns a delegated query object which can be used by individual resource
202 // handlers so that handlers don't need to query again.
203 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
204 {
205     Query delegated;
206     // delegate only
207     if (query.isOnly && queryCapabilities.canDelegateOnly)
208     {
209         delegated.isOnly = true;
210         query.isOnly = false;
211     }
212     // delegate expand as much as we can
213     if (query.expandType != ExpandType::None)
214     {
215         delegated.expandType = query.expandType;
216         if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
217         {
218             query.expandType = ExpandType::None;
219             delegated.expandLevel = query.expandLevel;
220             query.expandLevel = 0;
221         }
222         else
223         {
224             query.expandLevel -= queryCapabilities.canDelegateExpandLevel;
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 << "Members contains " << itMembers->size()
478                          << " element, returning full collection.";
479         completionHandler(res);
480         return false;
481     }
482 
483     auto itUrl = itMemBegin->find("@odata.id");
484     if (itUrl == itMemBegin->end())
485     {
486         BMCWEB_LOG_DEBUG << "No found odata.id";
487         messages::internalError(res);
488         completionHandler(res);
489         return false;
490     }
491     const std::string* url = itUrl->get_ptr<const std::string*>();
492     if (url == nullptr)
493     {
494         BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????";
495         messages::internalError(res);
496         completionHandler(res);
497         return false;
498     }
499     // TODO(Ed) copy request headers?
500     // newReq.session = req.session;
501     std::error_code ec;
502     crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec);
503     if (ec)
504     {
505         messages::internalError(res);
506         completionHandler(res);
507         return false;
508     }
509 
510     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
511     BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res;
512     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
513     asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper());
514     app.handle(newReq, asyncResp);
515     return true;
516 }
517 
518 struct ExpandNode
519 {
520     nlohmann::json::json_pointer location;
521     std::string uri;
522 
523     inline bool operator==(const ExpandNode& other) const
524     {
525         return location == other.location && uri == other.uri;
526     }
527 };
528 
529 // Walks a json object looking for Redfish NavigationReference entries that
530 // might need resolved.  It recursively walks the jsonResponse object, looking
531 // for links at every level, and returns a list (out) of locations within the
532 // tree that need to be expanded.  The current json pointer location p is passed
533 // in to reference the current node that's being expanded, so it can be combined
534 // with the keys from the jsonResponse object
535 inline void findNavigationReferencesRecursive(
536     ExpandType eType, nlohmann::json& jsonResponse,
537     const nlohmann::json::json_pointer& p, int depth, bool inLinks,
538     std::vector<ExpandNode>& out)
539 {
540     // If no expand is needed, return early
541     if (eType == ExpandType::None)
542     {
543         return;
544     }
545 
546     nlohmann::json::array_t* array =
547         jsonResponse.get_ptr<nlohmann::json::array_t*>();
548     if (array != nullptr)
549     {
550         size_t index = 0;
551         // For arrays, walk every element in the array
552         for (auto& element : *array)
553         {
554             nlohmann::json::json_pointer newPtr = p / index;
555             BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string();
556             findNavigationReferencesRecursive(eType, element, newPtr, depth,
557                                               inLinks, out);
558             index++;
559         }
560     }
561     nlohmann::json::object_t* obj =
562         jsonResponse.get_ptr<nlohmann::json::object_t*>();
563     if (obj == nullptr)
564     {
565         return;
566     }
567     // Navigation References only ever have a single element
568     if (obj->size() == 1)
569     {
570         if (obj->begin()->first == "@odata.id")
571         {
572             const std::string* uri =
573                 obj->begin()->second.get_ptr<const std::string*>();
574             if (uri != nullptr)
575             {
576                 BMCWEB_LOG_DEBUG << "Found " << *uri << " at " << p.to_string();
577                 out.push_back({p, *uri});
578                 return;
579             }
580         }
581     }
582 
583     int newDepth = depth;
584     auto odataId = obj->find("@odata.id");
585     if (odataId != obj->end())
586     {
587         // The Redfish spec requires all resources to include the resource
588         // identifier.  If the object has multiple elements and one of them is
589         // "@odata.id" then that means we have entered a new level / expanded
590         // resource.  We need to stop traversing if we're already at the desired
591         // depth
592         if ((obj->size() > 1) && (depth == 0))
593         {
594             return;
595         }
596         newDepth--;
597     }
598 
599     // Loop the object and look for links
600     for (auto& element : *obj)
601     {
602         bool localInLinks = inLinks;
603         if (!localInLinks)
604         {
605             // Check if this is a links node
606             localInLinks = element.first == "Links";
607         }
608         // Only traverse the parts of the tree the user asked for
609         // Per section 7.3 of the redfish specification
610         if (localInLinks && eType == ExpandType::NotLinks)
611         {
612             continue;
613         }
614         if (!localInLinks && eType == ExpandType::Links)
615         {
616             continue;
617         }
618         nlohmann::json::json_pointer newPtr = p / element.first;
619         BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr;
620 
621         findNavigationReferencesRecursive(eType, element.second, newPtr,
622                                           newDepth, localInLinks, out);
623     }
624 }
625 
626 // TODO: When aggregation is enabled and we receive a partially expanded
627 // response we may need need additional handling when the original URI was
628 // up tree from a top level collection.
629 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556
630 // lands.  May want to avoid forwarding query params when request is uptree from
631 // a top level collection.
632 inline std::vector<ExpandNode>
633     findNavigationReferences(ExpandType eType, int depth,
634                              nlohmann::json& jsonResponse)
635 {
636     std::vector<ExpandNode> ret;
637     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
638     findNavigationReferencesRecursive(eType, jsonResponse, root, depth, false,
639                                       ret);
640     return ret;
641 }
642 
643 // Formats a query parameter string for the sub-query.
644 // Returns std::nullopt on failures.
645 // This function shall handle $select when it is added.
646 // There is no need to handle parameters that's not campatible with $expand,
647 // e.g., $only, since this function will only be called in side $expand handlers
648 inline std::optional<std::string> formatQueryForExpand(const Query& query)
649 {
650     // query.expandLevel<=1: no need to do subqueries
651     if (query.expandLevel <= 1)
652     {
653         return "";
654     }
655     std::string str = "?$expand=";
656     bool queryTypeExpected = false;
657     switch (query.expandType)
658     {
659         case ExpandType::None:
660             return "";
661         case ExpandType::Links:
662             queryTypeExpected = true;
663             str += '~';
664             break;
665         case ExpandType::NotLinks:
666             queryTypeExpected = true;
667             str += '.';
668             break;
669         case ExpandType::Both:
670             queryTypeExpected = true;
671             str += '*';
672             break;
673     }
674     if (!queryTypeExpected)
675     {
676         return std::nullopt;
677     }
678     str += "($levels=";
679     str += std::to_string(query.expandLevel - 1);
680     str += ')';
681     return str;
682 }
683 
684 // Propogates the worst error code to the final response.
685 // The order of error code is (from high to low)
686 // 500 Internal Server Error
687 // 511 Network Authentication Required
688 // 510 Not Extended
689 // 508 Loop Detected
690 // 507 Insufficient Storage
691 // 506 Variant Also Negotiates
692 // 505 HTTP Version Not Supported
693 // 504 Gateway Timeout
694 // 503 Service Unavailable
695 // 502 Bad Gateway
696 // 501 Not Implemented
697 // 401 Unauthorized
698 // 451 - 409 Error codes (not listed explictly)
699 // 408 Request Timeout
700 // 407 Proxy Authentication Required
701 // 406 Not Acceptable
702 // 405 Method Not Allowed
703 // 404 Not Found
704 // 403 Forbidden
705 // 402 Payment Required
706 // 400 Bad Request
707 inline unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode)
708 {
709     // We keep a explicit list for error codes that this project often uses
710     // Higer priority codes are in lower indexes
711     constexpr std::array<unsigned, 13> orderedCodes = {
712         500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400};
713     size_t finalCodeIndex = std::numeric_limits<size_t>::max();
714     size_t subResponseCodeIndex = std::numeric_limits<size_t>::max();
715     for (size_t i = 0; i < orderedCodes.size(); ++i)
716     {
717         if (orderedCodes[i] == finalCode)
718         {
719             finalCodeIndex = i;
720         }
721         if (orderedCodes[i] == subResponseCode)
722         {
723             subResponseCodeIndex = i;
724         }
725     }
726     if (finalCodeIndex != std::numeric_limits<size_t>::max() &&
727         subResponseCodeIndex != std::numeric_limits<size_t>::max())
728     {
729         return finalCodeIndex <= subResponseCodeIndex ? finalCode
730                                                       : subResponseCode;
731     }
732     if (subResponseCode == 500 || finalCode == 500)
733     {
734         return 500;
735     }
736     if (subResponseCode > 500 || finalCode > 500)
737     {
738         return std::max(finalCode, subResponseCode);
739     }
740     if (subResponseCode == 401)
741     {
742         return subResponseCode;
743     }
744     return std::max(finalCode, subResponseCode);
745 }
746 
747 // Propogates all error messages into |finalResponse|
748 inline void propogateError(crow::Response& finalResponse,
749                            crow::Response& subResponse)
750 {
751     // no errors
752     if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400)
753     {
754         return;
755     }
756     messages::moveErrorsToErrorJson(finalResponse.jsonValue,
757                                     subResponse.jsonValue);
758     finalResponse.result(
759         propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt()));
760 }
761 
762 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
763 {
764   public:
765     // This object takes a single asyncResp object as the "final" one, then
766     // allows callers to attach sub-responses within the json tree that need
767     // to be executed and filled into their appropriate locations.  This
768     // class manages the final "merge" of the json resources.
769     MultiAsyncResp(crow::App& appIn,
770                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
771         app(appIn),
772         finalRes(std::move(finalResIn))
773     {}
774 
775     void addAwaitingResponse(
776         const std::shared_ptr<bmcweb::AsyncResp>& res,
777         const nlohmann::json::json_pointer& finalExpandLocation)
778     {
779         res->res.setCompleteRequestHandler(std::bind_front(
780             placeResultStatic, shared_from_this(), finalExpandLocation));
781     }
782 
783     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
784                      crow::Response& res)
785     {
786         BMCWEB_LOG_DEBUG << "placeResult for " << locationToPlace;
787         propogateError(finalRes->res, res);
788         if (!res.jsonValue.is_object() || res.jsonValue.empty())
789         {
790             return;
791         }
792         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
793         finalObj = std::move(res.jsonValue);
794     }
795 
796     // Handles the very first level of Expand, and starts a chain of sub-queries
797     // for deeper levels.
798     void startQuery(const Query& query)
799     {
800         std::vector<ExpandNode> nodes = findNavigationReferences(
801             query.expandType, query.expandLevel, finalRes->res.jsonValue);
802         BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse";
803         const std::optional<std::string> queryStr = formatQueryForExpand(query);
804         if (!queryStr)
805         {
806             messages::internalError(finalRes->res);
807             return;
808         }
809         for (const ExpandNode& node : nodes)
810         {
811             const std::string subQuery = node.uri + *queryStr;
812             BMCWEB_LOG_DEBUG << "URL of subquery:  " << subQuery;
813             std::error_code ec;
814             crow::Request newReq({boost::beast::http::verb::get, subQuery, 11},
815                                  ec);
816             if (ec)
817             {
818                 messages::internalError(finalRes->res);
819                 return;
820             }
821 
822             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
823             BMCWEB_LOG_DEBUG << "setting completion handler on "
824                              << &asyncResp->res;
825 
826             addAwaitingResponse(asyncResp, node.location);
827             app.handle(newReq, asyncResp);
828         }
829     }
830 
831   private:
832     static void
833         placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
834                           const nlohmann::json::json_pointer& locationToPlace,
835                           crow::Response& res)
836     {
837         multi->placeResult(locationToPlace, res);
838     }
839 
840     crow::App& app;
841     std::shared_ptr<bmcweb::AsyncResp> finalRes;
842 };
843 
844 inline void processTopAndSkip(const Query& query, crow::Response& res)
845 {
846     if (!query.skip && !query.top)
847     {
848         // No work to do.
849         return;
850     }
851     nlohmann::json::object_t* obj =
852         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
853     if (obj == nullptr)
854     {
855         // Shouldn't be possible.  All responses should be objects.
856         messages::internalError(res);
857         return;
858     }
859 
860     BMCWEB_LOG_DEBUG << "Handling top/skip";
861     nlohmann::json::object_t::iterator members = obj->find("Members");
862     if (members == obj->end())
863     {
864         // From the Redfish specification 7.3.1
865         // ... the HTTP 400 Bad Request status code with the
866         // QueryNotSupportedOnResource message from the Base Message Registry
867         // for any supported query parameters that apply only to resource
868         // collections but are used on singular resources.
869         messages::queryNotSupportedOnResource(res);
870         return;
871     }
872 
873     nlohmann::json::array_t* arr =
874         members->second.get_ptr<nlohmann::json::array_t*>();
875     if (arr == nullptr)
876     {
877         messages::internalError(res);
878         return;
879     }
880 
881     if (query.skip)
882     {
883         // Per section 7.3.1 of the Redfish specification, $skip is run before
884         // $top Can only skip as many values as we have
885         size_t skip = std::min(arr->size(), *query.skip);
886         arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
887     }
888     if (query.top)
889     {
890         size_t top = std::min(arr->size(), *query.top);
891         arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
892     }
893 }
894 
895 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are
896 // not in the |currNode| Trie node.
897 inline void recursiveSelect(nlohmann::json& currRoot,
898                             const SelectTrieNode& currNode)
899 {
900     nlohmann::json::object_t* object =
901         currRoot.get_ptr<nlohmann::json::object_t*>();
902     if (object != nullptr)
903     {
904         BMCWEB_LOG_DEBUG << "Current JSON is an object";
905         auto it = currRoot.begin();
906         while (it != currRoot.end())
907         {
908             auto nextIt = std::next(it);
909             BMCWEB_LOG_DEBUG << "key=" << it.key();
910             const SelectTrieNode* nextNode = currNode.find(it.key());
911             // Per the Redfish spec section 7.3.3, the service shall select
912             // certain properties as if $select was omitted. This applies to
913             // every TrieNode that contains leaves and the root.
914             constexpr std::array<std::string_view, 5> reservedProperties = {
915                 "@odata.id", "@odata.type", "@odata.context", "@odata.etag",
916                 "error"};
917             bool reserved = std::find(reservedProperties.begin(),
918                                       reservedProperties.end(),
919                                       it.key()) != reservedProperties.end();
920             if (reserved || (nextNode != nullptr && nextNode->isSelected()))
921             {
922                 it = nextIt;
923                 continue;
924             }
925             if (nextNode != nullptr)
926             {
927                 BMCWEB_LOG_DEBUG << "Recursively select: " << it.key();
928                 recursiveSelect(*it, *nextNode);
929                 it = nextIt;
930                 continue;
931             }
932             BMCWEB_LOG_DEBUG << it.key() << " is getting removed!";
933             it = currRoot.erase(it);
934         }
935     }
936     nlohmann::json::array_t* array =
937         currRoot.get_ptr<nlohmann::json::array_t*>();
938     if (array != nullptr)
939     {
940         BMCWEB_LOG_DEBUG << "Current JSON is an array";
941         // Array index is omitted, so reuse the same Trie node
942         for (nlohmann::json& nextRoot : *array)
943         {
944             recursiveSelect(nextRoot, currNode);
945         }
946     }
947 }
948 
949 // The current implementation of $select still has the following TODOs due to
950 //  ambiguity and/or complexity.
951 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was
952 // created for clarification.
953 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*),
954 // etc.
955 inline void processSelect(crow::Response& intermediateResponse,
956                           const SelectTrieNode& trieRoot)
957 {
958     BMCWEB_LOG_DEBUG << "Process $select quary parameter";
959     recursiveSelect(intermediateResponse.jsonValue, trieRoot);
960 }
961 
962 inline void
963     processAllParams(crow::App& app, const Query& query,
964                      std::function<void(crow::Response&)>& completionHandler,
965                      crow::Response& intermediateResponse)
966 {
967     if (!completionHandler)
968     {
969         BMCWEB_LOG_DEBUG << "Function was invalid?";
970         return;
971     }
972 
973     BMCWEB_LOG_DEBUG << "Processing query params";
974     // If the request failed, there's no reason to even try to run query
975     // params.
976     if (intermediateResponse.resultInt() < 200 ||
977         intermediateResponse.resultInt() >= 400)
978     {
979         completionHandler(intermediateResponse);
980         return;
981     }
982     if (query.isOnly)
983     {
984         processOnly(app, intermediateResponse, completionHandler);
985         return;
986     }
987 
988     if (query.top || query.skip)
989     {
990         processTopAndSkip(query, intermediateResponse);
991     }
992 
993     if (query.expandType != ExpandType::None)
994     {
995         BMCWEB_LOG_DEBUG << "Executing expand query";
996         auto asyncResp = std::make_shared<bmcweb::AsyncResp>(
997             std::move(intermediateResponse));
998 
999         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
1000         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
1001         multi->startQuery(query);
1002         return;
1003     }
1004 
1005     // According to Redfish Spec Section 7.3.1, $select is the last parameter to
1006     // to process
1007     if (!query.selectTrie.root.empty())
1008     {
1009         processSelect(intermediateResponse, query.selectTrie.root);
1010     }
1011 
1012     completionHandler(intermediateResponse);
1013 }
1014 
1015 } // namespace query_param
1016 } // namespace redfish
1017