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