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