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