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/beast/http/message.hpp> // IWYU pragma: keep
15 #include <boost/beast/http/status.hpp>
16 #include <boost/beast/http/verb.hpp>
17 #include <boost/url/params_view.hpp>
18 #include <nlohmann/json.hpp>
19 
20 #include <algorithm>
21 #include <array>
22 #include <cctype>
23 #include <charconv>
24 #include <compare>
25 #include <cstdint>
26 #include <functional>
27 #include <iterator>
28 #include <limits>
29 #include <map>
30 #include <memory>
31 #include <optional>
32 #include <ranges>
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(
477             "Members contains {} element, returning full collection.",
478             itMembers->size());
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 {}",
512                      logPtr(&asyncResp->res));
513     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
514     asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper());
515     app.handle(newReq, asyncResp);
516     return true;
517 }
518 
519 struct ExpandNode
520 {
521     nlohmann::json::json_pointer location;
522     std::string uri;
523 
524     inline bool operator==(const ExpandNode& other) const
525     {
526         return location == other.location && uri == other.uri;
527     }
528 };
529 
530 inline void findNavigationReferencesInArrayRecursive(
531     ExpandType eType, nlohmann::json::array_t& array,
532     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
533     bool inLinks, std::vector<ExpandNode>& out);
534 
535 inline void findNavigationReferencesInObjectRecursive(
536     ExpandType eType, nlohmann::json::object_t& obj,
537     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
538     bool inLinks, std::vector<ExpandNode>& out);
539 
540 // Walks a json object looking for Redfish NavigationReference entries that
541 // might need resolved.  It recursively walks the jsonResponse object, looking
542 // for links at every level, and returns a list (out) of locations within the
543 // tree that need to be expanded.  The current json pointer location p is passed
544 // in to reference the current node that's being expanded, so it can be combined
545 // with the keys from the jsonResponse object
546 inline void findNavigationReferencesRecursive(
547     ExpandType eType, nlohmann::json& jsonResponse,
548     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
549     bool inLinks, std::vector<ExpandNode>& out)
550 {
551     // If no expand is needed, return early
552     if (eType == ExpandType::None)
553     {
554         return;
555     }
556 
557     nlohmann::json::array_t* array =
558         jsonResponse.get_ptr<nlohmann::json::array_t*>();
559     if (array != nullptr)
560     {
561         findNavigationReferencesInArrayRecursive(eType, *array, jsonPtr, depth,
562                                                  skipDepth, inLinks, out);
563     }
564     nlohmann::json::object_t* obj =
565         jsonResponse.get_ptr<nlohmann::json::object_t*>();
566     if (obj == nullptr)
567     {
568         return;
569     }
570     findNavigationReferencesInObjectRecursive(eType, *obj, jsonPtr, depth,
571                                               skipDepth, inLinks, out);
572 }
573 
574 inline void findNavigationReferencesInArrayRecursive(
575     ExpandType eType, nlohmann::json::array_t& array,
576     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
577     bool inLinks, std::vector<ExpandNode>& out)
578 {
579     size_t index = 0;
580     // For arrays, walk every element in the array
581     for (auto& element : array)
582     {
583         nlohmann::json::json_pointer newPtr = jsonPtr / index;
584         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr.to_string());
585         findNavigationReferencesRecursive(eType, element, newPtr, depth,
586                                           skipDepth, inLinks, out);
587         index++;
588     }
589 }
590 
591 inline void findNavigationReferencesInObjectRecursive(
592     ExpandType eType, nlohmann::json::object_t& obj,
593     const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
594     bool inLinks, std::vector<ExpandNode>& out)
595 {
596     // Navigation References only ever have a single element
597     if (obj.size() == 1)
598     {
599         if (obj.begin()->first == "@odata.id")
600         {
601             const std::string* uri =
602                 obj.begin()->second.get_ptr<const std::string*>();
603             if (uri != nullptr)
604             {
605                 BMCWEB_LOG_DEBUG("Found {} at {}", *uri, jsonPtr.to_string());
606                 if (skipDepth == 0)
607                 {
608                     out.push_back({jsonPtr, *uri});
609                 }
610                 return;
611             }
612         }
613     }
614 
615     int newDepth = depth;
616     auto odataId = obj.find("@odata.id");
617     if (odataId != obj.end())
618     {
619         // The Redfish spec requires all resources to include the resource
620         // identifier.  If the object has multiple elements and one of them is
621         // "@odata.id" then that means we have entered a new level / expanded
622         // resource.  We need to stop traversing if we're already at the desired
623         // depth
624         if (obj.size() > 1)
625         {
626             if (depth == 0)
627             {
628                 return;
629             }
630             if (skipDepth > 0)
631             {
632                 skipDepth--;
633             }
634         }
635 
636         if (skipDepth == 0)
637         {
638             newDepth--;
639         }
640     }
641 
642     // Loop the object and look for links
643     for (auto& element : obj)
644     {
645         bool localInLinks = inLinks;
646         if (!localInLinks)
647         {
648             // Check if this is a links node
649             localInLinks = element.first == "Links";
650         }
651         // Only traverse the parts of the tree the user asked for
652         // Per section 7.3 of the redfish specification
653         if (localInLinks && eType == ExpandType::NotLinks)
654         {
655             continue;
656         }
657         if (!localInLinks && eType == ExpandType::Links)
658         {
659             continue;
660         }
661         nlohmann::json::json_pointer newPtr = jsonPtr / element.first;
662         BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr);
663 
664         findNavigationReferencesRecursive(eType, element.second, newPtr,
665                                           newDepth, skipDepth, localInLinks,
666                                           out);
667     }
668 }
669 
670 // TODO: When aggregation is enabled and we receive a partially expanded
671 // response we may need need additional handling when the original URI was
672 // up tree from a top level collection.
673 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556
674 // lands.  May want to avoid forwarding query params when request is uptree from
675 // a top level collection.
676 inline std::vector<ExpandNode>
677     findNavigationReferences(ExpandType eType, int depth, int skipDepth,
678                              nlohmann::json& jsonResponse)
679 {
680     std::vector<ExpandNode> ret;
681     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
682     // SkipDepth +1 since we are skipping the root by default.
683     findNavigationReferencesRecursive(eType, jsonResponse, root, depth,
684                                       skipDepth + 1, false, ret);
685     return ret;
686 }
687 
688 // Formats a query parameter string for the sub-query.
689 // Returns std::nullopt on failures.
690 // This function shall handle $select when it is added.
691 // There is no need to handle parameters that's not compatible with $expand,
692 // e.g., $only, since this function will only be called in side $expand handlers
693 inline std::optional<std::string> formatQueryForExpand(const Query& query)
694 {
695     // query.expandLevel<=1: no need to do subqueries
696     if (query.expandLevel <= 1)
697     {
698         return "";
699     }
700     std::string str = "?$expand=";
701     bool queryTypeExpected = false;
702     switch (query.expandType)
703     {
704         case ExpandType::None:
705             return "";
706         case ExpandType::Links:
707             queryTypeExpected = true;
708             str += '~';
709             break;
710         case ExpandType::NotLinks:
711             queryTypeExpected = true;
712             str += '.';
713             break;
714         case ExpandType::Both:
715             queryTypeExpected = true;
716             str += '*';
717             break;
718     }
719     if (!queryTypeExpected)
720     {
721         return std::nullopt;
722     }
723     str += "($levels=";
724     str += std::to_string(query.expandLevel - 1);
725     str += ')';
726     return str;
727 }
728 
729 // Propagates the worst error code to the final response.
730 // The order of error code is (from high to low)
731 // 500 Internal Server Error
732 // 511 Network Authentication Required
733 // 510 Not Extended
734 // 508 Loop Detected
735 // 507 Insufficient Storage
736 // 506 Variant Also Negotiates
737 // 505 HTTP Version Not Supported
738 // 504 Gateway Timeout
739 // 503 Service Unavailable
740 // 502 Bad Gateway
741 // 501 Not Implemented
742 // 401 Unauthorized
743 // 451 - 409 Error codes (not listed explicitly)
744 // 408 Request Timeout
745 // 407 Proxy Authentication Required
746 // 406 Not Acceptable
747 // 405 Method Not Allowed
748 // 404 Not Found
749 // 403 Forbidden
750 // 402 Payment Required
751 // 400 Bad Request
752 inline unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode)
753 {
754     // We keep a explicit list for error codes that this project often uses
755     // Higher priority codes are in lower indexes
756     constexpr std::array<unsigned, 13> orderedCodes = {
757         500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400};
758     size_t finalCodeIndex = std::numeric_limits<size_t>::max();
759     size_t subResponseCodeIndex = std::numeric_limits<size_t>::max();
760     for (size_t i = 0; i < orderedCodes.size(); ++i)
761     {
762         if (orderedCodes[i] == finalCode)
763         {
764             finalCodeIndex = i;
765         }
766         if (orderedCodes[i] == subResponseCode)
767         {
768             subResponseCodeIndex = i;
769         }
770     }
771     if (finalCodeIndex != std::numeric_limits<size_t>::max() &&
772         subResponseCodeIndex != std::numeric_limits<size_t>::max())
773     {
774         return finalCodeIndex <= subResponseCodeIndex ? finalCode
775                                                       : subResponseCode;
776     }
777     if (subResponseCode == 500 || finalCode == 500)
778     {
779         return 500;
780     }
781     if (subResponseCode > 500 || finalCode > 500)
782     {
783         return std::max(finalCode, subResponseCode);
784     }
785     if (subResponseCode == 401)
786     {
787         return subResponseCode;
788     }
789     return std::max(finalCode, subResponseCode);
790 }
791 
792 // Propagates all error messages into |finalResponse|
793 inline void propogateError(crow::Response& finalResponse,
794                            crow::Response& subResponse)
795 {
796     // no errors
797     if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400)
798     {
799         return;
800     }
801     messages::moveErrorsToErrorJson(finalResponse.jsonValue,
802                                     subResponse.jsonValue);
803     finalResponse.result(
804         propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt()));
805 }
806 
807 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
808 {
809   public:
810     // This object takes a single asyncResp object as the "final" one, then
811     // allows callers to attach sub-responses within the json tree that need
812     // to be executed and filled into their appropriate locations.  This
813     // class manages the final "merge" of the json resources.
814     MultiAsyncResp(crow::App& appIn,
815                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
816         app(appIn),
817         finalRes(std::move(finalResIn))
818     {}
819 
820     void addAwaitingResponse(
821         const std::shared_ptr<bmcweb::AsyncResp>& res,
822         const nlohmann::json::json_pointer& finalExpandLocation)
823     {
824         res->res.setCompleteRequestHandler(std::bind_front(
825             placeResultStatic, shared_from_this(), finalExpandLocation));
826     }
827 
828     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
829                      crow::Response& res)
830     {
831         BMCWEB_LOG_DEBUG("placeResult for {}", locationToPlace);
832         propogateError(finalRes->res, res);
833         if (!res.jsonValue.is_object() || res.jsonValue.empty())
834         {
835             return;
836         }
837         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
838         finalObj = std::move(res.jsonValue);
839     }
840 
841     // Handles the very first level of Expand, and starts a chain of sub-queries
842     // for deeper levels.
843     void startQuery(const Query& query, const Query& delegated)
844     {
845         std::vector<ExpandNode> nodes = findNavigationReferences(
846             query.expandType, query.expandLevel, delegated.expandLevel,
847             finalRes->res.jsonValue);
848         BMCWEB_LOG_DEBUG("{} nodes to traverse", nodes.size());
849         const std::optional<std::string> queryStr = formatQueryForExpand(query);
850         if (!queryStr)
851         {
852             messages::internalError(finalRes->res);
853             return;
854         }
855         for (const ExpandNode& node : nodes)
856         {
857             const std::string subQuery = node.uri + *queryStr;
858             BMCWEB_LOG_DEBUG("URL of subquery:  {}", subQuery);
859             std::error_code ec;
860             crow::Request newReq({boost::beast::http::verb::get, subQuery, 11},
861                                  ec);
862             if (ec)
863             {
864                 messages::internalError(finalRes->res);
865                 return;
866             }
867 
868             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
869             BMCWEB_LOG_DEBUG("setting completion handler on {}",
870                              logPtr(&asyncResp->res));
871 
872             addAwaitingResponse(asyncResp, node.location);
873             app.handle(newReq, asyncResp);
874         }
875     }
876 
877   private:
878     static void
879         placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
880                           const nlohmann::json::json_pointer& locationToPlace,
881                           crow::Response& res)
882     {
883         multi->placeResult(locationToPlace, res);
884     }
885 
886     crow::App& app;
887     std::shared_ptr<bmcweb::AsyncResp> finalRes;
888 };
889 
890 inline void processTopAndSkip(const Query& query, crow::Response& res)
891 {
892     if (!query.skip && !query.top)
893     {
894         // No work to do.
895         return;
896     }
897     nlohmann::json::object_t* obj =
898         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
899     if (obj == nullptr)
900     {
901         // Shouldn't be possible.  All responses should be objects.
902         messages::internalError(res);
903         return;
904     }
905 
906     BMCWEB_LOG_DEBUG("Handling top/skip");
907     nlohmann::json::object_t::iterator members = obj->find("Members");
908     if (members == obj->end())
909     {
910         // From the Redfish specification 7.3.1
911         // ... the HTTP 400 Bad Request status code with the
912         // QueryNotSupportedOnResource message from the Base Message Registry
913         // for any supported query parameters that apply only to resource
914         // collections but are used on singular resources.
915         messages::queryNotSupportedOnResource(res);
916         return;
917     }
918 
919     nlohmann::json::array_t* arr =
920         members->second.get_ptr<nlohmann::json::array_t*>();
921     if (arr == nullptr)
922     {
923         messages::internalError(res);
924         return;
925     }
926 
927     if (query.skip)
928     {
929         // Per section 7.3.1 of the Redfish specification, $skip is run before
930         // $top Can only skip as many values as we have
931         size_t skip = std::min(arr->size(), *query.skip);
932         arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
933     }
934     if (query.top)
935     {
936         size_t top = std::min(arr->size(), *query.top);
937         arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
938     }
939 }
940 
941 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are
942 // not in the |currNode| Trie node.
943 inline void recursiveSelect(nlohmann::json& currRoot,
944                             const SelectTrieNode& currNode)
945 {
946     nlohmann::json::object_t* object =
947         currRoot.get_ptr<nlohmann::json::object_t*>();
948     if (object != nullptr)
949     {
950         BMCWEB_LOG_DEBUG("Current JSON is an object");
951         auto it = currRoot.begin();
952         while (it != currRoot.end())
953         {
954             auto nextIt = std::next(it);
955             BMCWEB_LOG_DEBUG("key={}", it.key());
956             const SelectTrieNode* nextNode = currNode.find(it.key());
957             // Per the Redfish spec section 7.3.3, the service shall select
958             // certain properties as if $select was omitted. This applies to
959             // every TrieNode that contains leaves and the root.
960             constexpr std::array<std::string_view, 5> reservedProperties = {
961                 "@odata.id", "@odata.type", "@odata.context", "@odata.etag",
962                 "error"};
963             bool reserved = std::ranges::find(reservedProperties, it.key()) !=
964                             reservedProperties.end();
965             if (reserved || (nextNode != nullptr && nextNode->isSelected()))
966             {
967                 it = nextIt;
968                 continue;
969             }
970             if (nextNode != nullptr)
971             {
972                 BMCWEB_LOG_DEBUG("Recursively select: {}", it.key());
973                 recursiveSelect(*it, *nextNode);
974                 it = nextIt;
975                 continue;
976             }
977             BMCWEB_LOG_DEBUG("{} is getting removed!", it.key());
978             it = currRoot.erase(it);
979         }
980     }
981     nlohmann::json::array_t* array =
982         currRoot.get_ptr<nlohmann::json::array_t*>();
983     if (array != nullptr)
984     {
985         BMCWEB_LOG_DEBUG("Current JSON is an array");
986         // Array index is omitted, so reuse the same Trie node
987         for (nlohmann::json& nextRoot : *array)
988         {
989             recursiveSelect(nextRoot, currNode);
990         }
991     }
992 }
993 
994 // The current implementation of $select still has the following TODOs due to
995 //  ambiguity and/or complexity.
996 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was
997 // created for clarification.
998 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*),
999 // etc.
1000 inline void processSelect(crow::Response& intermediateResponse,
1001                           const SelectTrieNode& trieRoot)
1002 {
1003     BMCWEB_LOG_DEBUG("Process $select quary parameter");
1004     recursiveSelect(intermediateResponse.jsonValue, trieRoot);
1005 }
1006 
1007 inline void
1008     processAllParams(crow::App& app, const Query& query, const Query& delegated,
1009                      std::function<void(crow::Response&)>& completionHandler,
1010                      crow::Response& intermediateResponse)
1011 {
1012     if (!completionHandler)
1013     {
1014         BMCWEB_LOG_DEBUG("Function was invalid?");
1015         return;
1016     }
1017 
1018     BMCWEB_LOG_DEBUG("Processing query params");
1019     // If the request failed, there's no reason to even try to run query
1020     // params.
1021     if (intermediateResponse.resultInt() < 200 ||
1022         intermediateResponse.resultInt() >= 400)
1023     {
1024         completionHandler(intermediateResponse);
1025         return;
1026     }
1027     if (query.isOnly)
1028     {
1029         processOnly(app, intermediateResponse, completionHandler);
1030         return;
1031     }
1032 
1033     if (query.top || query.skip)
1034     {
1035         processTopAndSkip(query, intermediateResponse);
1036     }
1037 
1038     if (query.expandType != ExpandType::None)
1039     {
1040         BMCWEB_LOG_DEBUG("Executing expand query");
1041         auto asyncResp = std::make_shared<bmcweb::AsyncResp>(
1042             std::move(intermediateResponse));
1043 
1044         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
1045         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
1046         multi->startQuery(query, delegated);
1047         return;
1048     }
1049 
1050     // According to Redfish Spec Section 7.3.1, $select is the last parameter to
1051     // to process
1052     if (!query.selectTrie.root.empty())
1053     {
1054         processSelect(intermediateResponse, query.selectTrie.root);
1055     }
1056 
1057     completionHandler(intermediateResponse);
1058 }
1059 
1060 } // namespace query_param
1061 } // namespace redfish
1062