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