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