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