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