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 69 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 81 SelectTrieNode* emplace(std::string_view jsonKey) 82 { 83 auto [it, _] = children.emplace(jsonKey, SelectTrieNode{}); 84 return &it->second; 85 } 86 87 bool empty() const 88 { 89 return children.empty(); 90 } 91 92 void clear() 93 { 94 children.clear(); 95 } 96 97 void setToSelected() 98 { 99 selected = true; 100 } 101 102 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) 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. 139 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. 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 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 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 329 inline QueryError getSkipParam(std::string_view value, Query& query) 330 { 331 return getNumericParam(value, query.skip.emplace()); 332 } 333 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 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. 384 inline bool getFilterParam(std::string_view value, Query& query) 385 { 386 query.filter = parseFilter(value); 387 return query.filter.has_value(); 388 } 389 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 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 549 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 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 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 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> 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 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 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| 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. 833 MultiAsyncResp(crow::App& appIn, 834 std::shared_ptr<bmcweb::AsyncResp> finalResIn) : 835 app(appIn), 836 finalRes(std::move(finalResIn)) 837 {} 838 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 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. 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 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 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. 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. 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 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