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