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