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