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