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 crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec); 507 if (ec) 508 { 509 messages::internalError(res); 510 completionHandler(res); 511 return false; 512 } 513 514 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 515 BMCWEB_LOG_DEBUG("setting completion handler on {}", 516 logPtr(&asyncResp->res)); 517 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 518 app.handle(newReq, asyncResp); 519 return true; 520 } 521 522 struct ExpandNode 523 { 524 nlohmann::json::json_pointer location; 525 std::string uri; 526 527 bool operator==(const ExpandNode& other) const 528 { 529 return location == other.location && uri == other.uri; 530 } 531 }; 532 533 inline void findNavigationReferencesInArrayRecursive( 534 ExpandType eType, nlohmann::json::array_t& array, 535 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 536 bool inLinks, std::vector<ExpandNode>& out); 537 538 inline void findNavigationReferencesInObjectRecursive( 539 ExpandType eType, nlohmann::json::object_t& obj, 540 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 541 bool inLinks, std::vector<ExpandNode>& out); 542 543 // Walks a json object looking for Redfish NavigationReference entries that 544 // might need resolved. It recursively walks the jsonResponse object, looking 545 // for links at every level, and returns a list (out) of locations within the 546 // tree that need to be expanded. The current json pointer location p is passed 547 // in to reference the current node that's being expanded, so it can be combined 548 // with the keys from the jsonResponse object 549 inline void findNavigationReferencesRecursive( 550 ExpandType eType, nlohmann::json& jsonResponse, 551 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 552 bool inLinks, std::vector<ExpandNode>& out) 553 { 554 // If no expand is needed, return early 555 if (eType == ExpandType::None) 556 { 557 return; 558 } 559 560 nlohmann::json::array_t* array = 561 jsonResponse.get_ptr<nlohmann::json::array_t*>(); 562 if (array != nullptr) 563 { 564 findNavigationReferencesInArrayRecursive(eType, *array, jsonPtr, depth, 565 skipDepth, inLinks, out); 566 } 567 nlohmann::json::object_t* obj = 568 jsonResponse.get_ptr<nlohmann::json::object_t*>(); 569 if (obj == nullptr) 570 { 571 return; 572 } 573 findNavigationReferencesInObjectRecursive(eType, *obj, jsonPtr, depth, 574 skipDepth, inLinks, out); 575 } 576 577 inline void findNavigationReferencesInArrayRecursive( 578 ExpandType eType, nlohmann::json::array_t& array, 579 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 580 bool inLinks, std::vector<ExpandNode>& out) 581 { 582 size_t index = 0; 583 // For arrays, walk every element in the array 584 for (auto& element : array) 585 { 586 nlohmann::json::json_pointer newPtr = jsonPtr / index; 587 BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr.to_string()); 588 findNavigationReferencesRecursive(eType, element, newPtr, depth, 589 skipDepth, inLinks, out); 590 index++; 591 } 592 } 593 594 inline void findNavigationReferencesInObjectRecursive( 595 ExpandType eType, nlohmann::json::object_t& obj, 596 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 597 bool inLinks, std::vector<ExpandNode>& out) 598 { 599 // Navigation References only ever have a single element 600 if (obj.size() == 1) 601 { 602 if (obj.begin()->first == "@odata.id") 603 { 604 const std::string* uri = 605 obj.begin()->second.get_ptr<const std::string*>(); 606 if (uri != nullptr) 607 { 608 BMCWEB_LOG_DEBUG("Found {} at {}", *uri, jsonPtr.to_string()); 609 if (skipDepth == 0) 610 { 611 out.push_back({jsonPtr, *uri}); 612 } 613 return; 614 } 615 } 616 } 617 618 int newDepth = depth; 619 auto odataId = obj.find("@odata.id"); 620 if (odataId != obj.end()) 621 { 622 // The Redfish spec requires all resources to include the resource 623 // identifier. If the object has multiple elements and one of them is 624 // "@odata.id" then that means we have entered a new level / expanded 625 // resource. We need to stop traversing if we're already at the desired 626 // depth 627 if (obj.size() > 1) 628 { 629 if (depth == 0) 630 { 631 return; 632 } 633 if (skipDepth > 0) 634 { 635 skipDepth--; 636 } 637 } 638 639 if (skipDepth == 0) 640 { 641 newDepth--; 642 } 643 } 644 645 // Loop the object and look for links 646 for (auto& element : obj) 647 { 648 bool localInLinks = inLinks; 649 if (!localInLinks) 650 { 651 // Check if this is a links node 652 localInLinks = element.first == "Links"; 653 } 654 // Only traverse the parts of the tree the user asked for 655 // Per section 7.3 of the redfish specification 656 if (localInLinks && eType == ExpandType::NotLinks) 657 { 658 continue; 659 } 660 if (!localInLinks && eType == ExpandType::Links) 661 { 662 continue; 663 } 664 nlohmann::json::json_pointer newPtr = jsonPtr / element.first; 665 BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr); 666 667 findNavigationReferencesRecursive(eType, element.second, newPtr, 668 newDepth, skipDepth, localInLinks, 669 out); 670 } 671 } 672 673 // TODO: When aggregation is enabled and we receive a partially expanded 674 // response we may need need additional handling when the original URI was 675 // up tree from a top level collection. 676 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556 677 // lands. May want to avoid forwarding query params when request is uptree from 678 // a top level collection. 679 inline std::vector<ExpandNode> 680 findNavigationReferences(ExpandType eType, int depth, int skipDepth, 681 nlohmann::json& jsonResponse) 682 { 683 std::vector<ExpandNode> ret; 684 const nlohmann::json::json_pointer root = nlohmann::json::json_pointer(""); 685 // SkipDepth +1 since we are skipping the root by default. 686 findNavigationReferencesRecursive(eType, jsonResponse, root, depth, 687 skipDepth + 1, false, ret); 688 return ret; 689 } 690 691 // Formats a query parameter string for the sub-query. 692 // Returns std::nullopt on failures. 693 // This function shall handle $select when it is added. 694 // There is no need to handle parameters that's not compatible with $expand, 695 // e.g., $only, since this function will only be called in side $expand handlers 696 inline std::optional<std::string> formatQueryForExpand(const Query& query) 697 { 698 // query.expandLevel<=1: no need to do subqueries 699 if (query.expandLevel <= 1) 700 { 701 return ""; 702 } 703 std::string str = "?$expand="; 704 switch (query.expandType) 705 { 706 case ExpandType::Links: 707 str += '~'; 708 break; 709 case ExpandType::NotLinks: 710 str += '.'; 711 break; 712 case ExpandType::Both: 713 str += '*'; 714 break; 715 case ExpandType::None: 716 return ""; 717 default: 718 return std::nullopt; 719 } 720 str += "($levels="; 721 str += std::to_string(query.expandLevel - 1); 722 str += ')'; 723 return str; 724 } 725 726 // Propagates the worst error code to the final response. 727 // The order of error code is (from high to low) 728 // 500 Internal Server Error 729 // 511 Network Authentication Required 730 // 510 Not Extended 731 // 508 Loop Detected 732 // 507 Insufficient Storage 733 // 506 Variant Also Negotiates 734 // 505 HTTP Version Not Supported 735 // 504 Gateway Timeout 736 // 503 Service Unavailable 737 // 502 Bad Gateway 738 // 501 Not Implemented 739 // 401 Unauthorized 740 // 451 - 409 Error codes (not listed explicitly) 741 // 408 Request Timeout 742 // 407 Proxy Authentication Required 743 // 406 Not Acceptable 744 // 405 Method Not Allowed 745 // 404 Not Found 746 // 403 Forbidden 747 // 402 Payment Required 748 // 400 Bad Request 749 inline unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode) 750 { 751 // We keep a explicit list for error codes that this project often uses 752 // Higher priority codes are in lower indexes 753 constexpr std::array<unsigned, 13> orderedCodes = { 754 500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400}; 755 size_t finalCodeIndex = std::numeric_limits<size_t>::max(); 756 size_t subResponseCodeIndex = std::numeric_limits<size_t>::max(); 757 for (size_t i = 0; i < orderedCodes.size(); ++i) 758 { 759 if (orderedCodes[i] == finalCode) 760 { 761 finalCodeIndex = i; 762 } 763 if (orderedCodes[i] == subResponseCode) 764 { 765 subResponseCodeIndex = i; 766 } 767 } 768 if (finalCodeIndex != std::numeric_limits<size_t>::max() && 769 subResponseCodeIndex != std::numeric_limits<size_t>::max()) 770 { 771 return finalCodeIndex <= subResponseCodeIndex ? finalCode 772 : subResponseCode; 773 } 774 if (subResponseCode == 500 || finalCode == 500) 775 { 776 return 500; 777 } 778 if (subResponseCode > 500 || finalCode > 500) 779 { 780 return std::max(finalCode, subResponseCode); 781 } 782 if (subResponseCode == 401) 783 { 784 return subResponseCode; 785 } 786 return std::max(finalCode, subResponseCode); 787 } 788 789 // Propagates all error messages into |finalResponse| 790 inline void propogateError(crow::Response& finalResponse, 791 crow::Response& subResponse) 792 { 793 // no errors 794 if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400) 795 { 796 return; 797 } 798 messages::moveErrorsToErrorJson(finalResponse.jsonValue, 799 subResponse.jsonValue); 800 finalResponse.result( 801 propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt())); 802 } 803 804 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp> 805 { 806 public: 807 // This object takes a single asyncResp object as the "final" one, then 808 // allows callers to attach sub-responses within the json tree that need 809 // to be executed and filled into their appropriate locations. This 810 // class manages the final "merge" of the json resources. 811 MultiAsyncResp(crow::App& appIn, 812 std::shared_ptr<bmcweb::AsyncResp> finalResIn) : 813 app(appIn), 814 finalRes(std::move(finalResIn)) 815 {} 816 817 void addAwaitingResponse( 818 const std::shared_ptr<bmcweb::AsyncResp>& res, 819 const nlohmann::json::json_pointer& finalExpandLocation) 820 { 821 res->res.setCompleteRequestHandler(std::bind_front( 822 placeResultStatic, shared_from_this(), finalExpandLocation)); 823 } 824 825 void placeResult(const nlohmann::json::json_pointer& locationToPlace, 826 crow::Response& res) 827 { 828 BMCWEB_LOG_DEBUG("placeResult for {}", locationToPlace); 829 propogateError(finalRes->res, res); 830 if (!res.jsonValue.is_object() || res.jsonValue.empty()) 831 { 832 return; 833 } 834 nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace]; 835 finalObj = std::move(res.jsonValue); 836 } 837 838 // Handles the very first level of Expand, and starts a chain of sub-queries 839 // for deeper levels. 840 void startQuery(const Query& query, const Query& delegated) 841 { 842 std::vector<ExpandNode> nodes = findNavigationReferences( 843 query.expandType, query.expandLevel, delegated.expandLevel, 844 finalRes->res.jsonValue); 845 BMCWEB_LOG_DEBUG("{} nodes to traverse", nodes.size()); 846 const std::optional<std::string> queryStr = formatQueryForExpand(query); 847 if (!queryStr) 848 { 849 messages::internalError(finalRes->res); 850 return; 851 } 852 for (const ExpandNode& node : nodes) 853 { 854 const std::string subQuery = node.uri + *queryStr; 855 BMCWEB_LOG_DEBUG("URL of subquery: {}", subQuery); 856 std::error_code ec; 857 crow::Request newReq({boost::beast::http::verb::get, subQuery, 11}, 858 ec); 859 if (ec) 860 { 861 messages::internalError(finalRes->res); 862 return; 863 } 864 865 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 866 BMCWEB_LOG_DEBUG("setting completion handler on {}", 867 logPtr(&asyncResp->res)); 868 869 addAwaitingResponse(asyncResp, node.location); 870 app.handle(newReq, asyncResp); 871 } 872 } 873 874 private: 875 static void 876 placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi, 877 const nlohmann::json::json_pointer& locationToPlace, 878 crow::Response& res) 879 { 880 multi->placeResult(locationToPlace, res); 881 } 882 883 crow::App& app; 884 std::shared_ptr<bmcweb::AsyncResp> finalRes; 885 }; 886 887 inline void processTopAndSkip(const Query& query, crow::Response& res) 888 { 889 if (!query.skip && !query.top) 890 { 891 // No work to do. 892 return; 893 } 894 nlohmann::json::object_t* obj = 895 res.jsonValue.get_ptr<nlohmann::json::object_t*>(); 896 if (obj == nullptr) 897 { 898 // Shouldn't be possible. All responses should be objects. 899 messages::internalError(res); 900 return; 901 } 902 903 BMCWEB_LOG_DEBUG("Handling top/skip"); 904 nlohmann::json::object_t::iterator members = obj->find("Members"); 905 if (members == obj->end()) 906 { 907 // From the Redfish specification 7.3.1 908 // ... the HTTP 400 Bad Request status code with the 909 // QueryNotSupportedOnResource message from the Base Message Registry 910 // for any supported query parameters that apply only to resource 911 // collections but are used on singular resources. 912 messages::queryNotSupportedOnResource(res); 913 return; 914 } 915 916 nlohmann::json::array_t* arr = 917 members->second.get_ptr<nlohmann::json::array_t*>(); 918 if (arr == nullptr) 919 { 920 messages::internalError(res); 921 return; 922 } 923 924 if (query.skip) 925 { 926 // Per section 7.3.1 of the Redfish specification, $skip is run before 927 // $top Can only skip as many values as we have 928 size_t skip = std::min(arr->size(), *query.skip); 929 arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip)); 930 } 931 if (query.top) 932 { 933 size_t top = std::min(arr->size(), *query.top); 934 arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end()); 935 } 936 } 937 938 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are 939 // not in the |currNode| Trie node. 940 inline void recursiveSelect(nlohmann::json& currRoot, 941 const SelectTrieNode& currNode) 942 { 943 nlohmann::json::object_t* object = 944 currRoot.get_ptr<nlohmann::json::object_t*>(); 945 if (object != nullptr) 946 { 947 BMCWEB_LOG_DEBUG("Current JSON is an object"); 948 auto it = currRoot.begin(); 949 while (it != currRoot.end()) 950 { 951 auto nextIt = std::next(it); 952 BMCWEB_LOG_DEBUG("key={}", it.key()); 953 const SelectTrieNode* nextNode = currNode.find(it.key()); 954 // Per the Redfish spec section 7.3.3, the service shall select 955 // certain properties as if $select was omitted. This applies to 956 // every TrieNode that contains leaves and the root. 957 constexpr std::array<std::string_view, 5> reservedProperties = { 958 "@odata.id", "@odata.type", "@odata.context", "@odata.etag", 959 "error"}; 960 bool reserved = std::ranges::find(reservedProperties, it.key()) != 961 reservedProperties.end(); 962 if (reserved || (nextNode != nullptr && nextNode->isSelected())) 963 { 964 it = nextIt; 965 continue; 966 } 967 if (nextNode != nullptr) 968 { 969 BMCWEB_LOG_DEBUG("Recursively select: {}", it.key()); 970 recursiveSelect(*it, *nextNode); 971 it = nextIt; 972 continue; 973 } 974 BMCWEB_LOG_DEBUG("{} is getting removed!", it.key()); 975 it = currRoot.erase(it); 976 } 977 } 978 nlohmann::json::array_t* array = 979 currRoot.get_ptr<nlohmann::json::array_t*>(); 980 if (array != nullptr) 981 { 982 BMCWEB_LOG_DEBUG("Current JSON is an array"); 983 // Array index is omitted, so reuse the same Trie node 984 for (nlohmann::json& nextRoot : *array) 985 { 986 recursiveSelect(nextRoot, currNode); 987 } 988 } 989 } 990 991 // The current implementation of $select still has the following TODOs due to 992 // ambiguity and/or complexity. 993 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was 994 // created for clarification. 995 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*), 996 // etc. 997 inline void processSelect(crow::Response& intermediateResponse, 998 const SelectTrieNode& trieRoot) 999 { 1000 BMCWEB_LOG_DEBUG("Process $select quary parameter"); 1001 recursiveSelect(intermediateResponse.jsonValue, trieRoot); 1002 } 1003 1004 inline void 1005 processAllParams(crow::App& app, const Query& query, const Query& delegated, 1006 std::function<void(crow::Response&)>& completionHandler, 1007 crow::Response& intermediateResponse) 1008 { 1009 if (!completionHandler) 1010 { 1011 BMCWEB_LOG_DEBUG("Function was invalid?"); 1012 return; 1013 } 1014 1015 BMCWEB_LOG_DEBUG("Processing query params"); 1016 // If the request failed, there's no reason to even try to run query 1017 // params. 1018 if (intermediateResponse.resultInt() < 200 || 1019 intermediateResponse.resultInt() >= 400) 1020 { 1021 completionHandler(intermediateResponse); 1022 return; 1023 } 1024 if (query.isOnly) 1025 { 1026 processOnly(app, intermediateResponse, completionHandler); 1027 return; 1028 } 1029 1030 if (query.top || query.skip) 1031 { 1032 processTopAndSkip(query, intermediateResponse); 1033 } 1034 1035 if (query.expandType != ExpandType::None) 1036 { 1037 BMCWEB_LOG_DEBUG("Executing expand query"); 1038 auto asyncResp = std::make_shared<bmcweb::AsyncResp>( 1039 std::move(intermediateResponse)); 1040 1041 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 1042 auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp); 1043 multi->startQuery(query, delegated); 1044 return; 1045 } 1046 1047 // According to Redfish Spec Section 7.3.1, $select is the last parameter to 1048 // to process 1049 if (!query.selectTrie.root.empty()) 1050 { 1051 processSelect(intermediateResponse, query.selectTrie.root); 1052 } 1053 1054 completionHandler(intermediateResponse); 1055 } 1056 1057 } // namespace query_param 1058 } // namespace redfish 1059