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